mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-19 10:22:23 +00:00
fix merge conflict
This commit is contained in:
commit
265a5c2069
80 changed files with 2395 additions and 2764 deletions
4
.github/workflows/go.yml
vendored
4
.github/workflows/go.yml
vendored
|
|
@ -11,9 +11,9 @@ jobs:
|
|||
build:
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.4
|
||||
- name: Run tests
|
||||
|
|
|
|||
24
Makefile
24
Makefile
|
|
@ -2,31 +2,35 @@
|
|||
# with Go source code. If you know what GOPATH is then you probably
|
||||
# don't need to bother with make.
|
||||
|
||||
.PHONY: geth all test lint clean devtools help
|
||||
.PHONY: geth all test lint fmt clean devtools help
|
||||
|
||||
GOBIN = ./build/bin
|
||||
GO ?= latest
|
||||
GORUN = go run
|
||||
|
||||
#? geth: Build geth
|
||||
#? geth: Build geth.
|
||||
geth:
|
||||
$(GORUN) build/ci.go install ./cmd/geth
|
||||
@echo "Done building."
|
||||
@echo "Run \"$(GOBIN)/geth\" to launch geth."
|
||||
|
||||
#? all: Build all packages and executables
|
||||
#? all: Build all packages and executables.
|
||||
all:
|
||||
$(GORUN) build/ci.go install
|
||||
|
||||
#? test: Run the tests
|
||||
#? test: Run the tests.
|
||||
test: all
|
||||
$(GORUN) build/ci.go test
|
||||
|
||||
#? lint: Run certain pre-selected linters
|
||||
#? lint: Run certain pre-selected linters.
|
||||
lint: ## Run linters.
|
||||
$(GORUN) build/ci.go lint
|
||||
|
||||
#? clean: Clean go cache, built executables, and the auto generated folder
|
||||
#? fmt: Ensure consistent code formatting.
|
||||
fmt:
|
||||
gofmt -s -w $(shell find . -name "*.go")
|
||||
|
||||
#? clean: Clean go cache, built executables, and the auto generated folder.
|
||||
clean:
|
||||
go clean -cache
|
||||
rm -fr build/_workspace/pkg/ $(GOBIN)/*
|
||||
|
|
@ -34,7 +38,7 @@ clean:
|
|||
# The devtools target installs tools required for 'go generate'.
|
||||
# You need to put $GOBIN (or $GOPATH/bin) in your PATH to use 'go generate'.
|
||||
|
||||
#? devtools: Install recommended developer tools
|
||||
#? devtools: Install recommended developer tools.
|
||||
devtools:
|
||||
env GOBIN= go install golang.org/x/tools/cmd/stringer@latest
|
||||
env GOBIN= go install github.com/fjl/gencodec@latest
|
||||
|
|
@ -45,5 +49,9 @@ devtools:
|
|||
|
||||
#? help: Get more info on make commands.
|
||||
help: Makefile
|
||||
@echo " Choose a command run in go-ethereum:"
|
||||
@echo ''
|
||||
@echo 'Usage:'
|
||||
@echo ' make [target]'
|
||||
@echo ''
|
||||
@echo 'Targets:'
|
||||
@sed -n 's/^#?//p' $< | column -t -s ':' | sort | sed -e 's/^/ /'
|
||||
|
|
|
|||
|
|
@ -326,6 +326,11 @@ func TestUpdatedKeyfileContents(t *testing.T) {
|
|||
|
||||
// Create a temporary keystore to test with
|
||||
dir := filepath.Join(os.TempDir(), fmt.Sprintf("eth-keystore-updatedkeyfilecontents-test-%d-%d", os.Getpid(), rand.Int()))
|
||||
|
||||
// Create the directory
|
||||
os.MkdirAll(dir, 0700)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
ks := NewKeyStore(dir, LightScryptN, LightScryptP)
|
||||
|
||||
list := ks.Accounts()
|
||||
|
|
@ -335,9 +340,7 @@ func TestUpdatedKeyfileContents(t *testing.T) {
|
|||
if !waitWatcherStart(ks) {
|
||||
t.Fatal("keystore watcher didn't start in time")
|
||||
}
|
||||
// Create the directory and copy a key file into it.
|
||||
os.MkdirAll(dir, 0700)
|
||||
defer os.RemoveAll(dir)
|
||||
// Copy a key file into it
|
||||
file := filepath.Join(dir, "aaa")
|
||||
|
||||
// Place one of our testfiles in there
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ func convertPayload[T payloadType](payload T, parentRoot *zrntcommon.Root) (*typ
|
|||
|
||||
block := types.NewBlockWithHeader(&header).WithBody(types.Body{Transactions: transactions, Withdrawals: withdrawals})
|
||||
if hash := block.Hash(); hash != expectedHash {
|
||||
return nil, fmt.Errorf("Sanity check failed, payload hash does not match (expected %x, got %x)", expectedHash, hash)
|
||||
return nil, fmt.Errorf("sanity check failed, payload hash does not match (expected %x, got %x)", expectedHash, hash)
|
||||
}
|
||||
return block, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -552,7 +552,7 @@ func listWallets(c *cli.Context) error {
|
|||
// accountImport imports a raw hexadecimal private key via CLI.
|
||||
func accountImport(c *cli.Context) error {
|
||||
if c.Args().Len() != 1 {
|
||||
return errors.New("<keyfile> must be given as first argument.")
|
||||
return errors.New("<keyfile> must be given as first argument")
|
||||
}
|
||||
internalApi, ui, err := initInternalApi(c)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
|
@ -28,9 +29,11 @@ import (
|
|||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/internal/flags"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/p2p/discover"
|
||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
|
|
@ -45,6 +48,7 @@ var (
|
|||
discv4ResolveJSONCommand,
|
||||
discv4CrawlCommand,
|
||||
discv4TestCommand,
|
||||
discv4ListenCommand,
|
||||
},
|
||||
}
|
||||
discv4PingCommand = &cli.Command{
|
||||
|
|
@ -75,6 +79,14 @@ var (
|
|||
Flags: discoveryNodeFlags,
|
||||
ArgsUsage: "<nodes.json file>",
|
||||
}
|
||||
discv4ListenCommand = &cli.Command{
|
||||
Name: "listen",
|
||||
Usage: "Runs a discovery node",
|
||||
Action: discv4Listen,
|
||||
Flags: flags.Merge(discoveryNodeFlags, []cli.Flag{
|
||||
httpAddrFlag,
|
||||
}),
|
||||
}
|
||||
discv4CrawlCommand = &cli.Command{
|
||||
Name: "crawl",
|
||||
Usage: "Updates a nodes.json file with random nodes found in the DHT",
|
||||
|
|
@ -131,6 +143,10 @@ var (
|
|||
Usage: "Enode of the remote node under test",
|
||||
EnvVars: []string{"REMOTE_ENODE"},
|
||||
}
|
||||
httpAddrFlag = &cli.StringFlag{
|
||||
Name: "rpc",
|
||||
Usage: "HTTP server listening address",
|
||||
}
|
||||
)
|
||||
|
||||
var discoveryNodeFlags = []cli.Flag{
|
||||
|
|
@ -154,6 +170,27 @@ func discv4Ping(ctx *cli.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func discv4Listen(ctx *cli.Context) error {
|
||||
disc, _ := startV4(ctx)
|
||||
defer disc.Close()
|
||||
|
||||
fmt.Println(disc.Self())
|
||||
|
||||
httpAddr := ctx.String(httpAddrFlag.Name)
|
||||
if httpAddr == "" {
|
||||
// Non-HTTP mode.
|
||||
select {}
|
||||
}
|
||||
|
||||
api := &discv4API{disc}
|
||||
log.Info("Starting RPC API server", "addr", httpAddr)
|
||||
srv := rpc.NewServer()
|
||||
srv.RegisterName("discv4", api)
|
||||
http.DefaultServeMux.Handle("/", srv)
|
||||
httpsrv := http.Server{Addr: httpAddr, Handler: http.DefaultServeMux}
|
||||
return httpsrv.ListenAndServe()
|
||||
}
|
||||
|
||||
func discv4RequestRecord(ctx *cli.Context) error {
|
||||
n := getNodeArg(ctx)
|
||||
disc, _ := startV4(ctx)
|
||||
|
|
@ -362,3 +399,23 @@ func parseBootnodes(ctx *cli.Context) ([]*enode.Node, error) {
|
|||
}
|
||||
return nodes, nil
|
||||
}
|
||||
|
||||
type discv4API struct {
|
||||
host *discover.UDPv4
|
||||
}
|
||||
|
||||
func (api *discv4API) LookupRandom(n int) (ns []*enode.Node) {
|
||||
it := api.host.RandomNodes()
|
||||
for len(ns) < n && it.Next() {
|
||||
ns = append(ns, it.Node())
|
||||
}
|
||||
return ns
|
||||
}
|
||||
|
||||
func (api *discv4API) Buckets() [][]discover.BucketNode {
|
||||
return api.host.TableBuckets()
|
||||
}
|
||||
|
||||
func (api *discv4API) Self() *enode.Node {
|
||||
return api.host.Self()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -234,7 +234,7 @@ func TestT8n(t *testing.T) {
|
|||
{ // Test post-merge transition
|
||||
base: "./testdata/24",
|
||||
input: t8nInput{
|
||||
"alloc.json", "txs.json", "env.json", "Merge", "",
|
||||
"alloc.json", "txs.json", "env.json", "Paris", "",
|
||||
},
|
||||
output: t8nOutput{alloc: true, result: true},
|
||||
expOut: "exp.json",
|
||||
|
|
@ -242,7 +242,7 @@ func TestT8n(t *testing.T) {
|
|||
{ // Test post-merge transition where input is missing random
|
||||
base: "./testdata/24",
|
||||
input: t8nInput{
|
||||
"alloc.json", "txs.json", "env-missingrandom.json", "Merge", "",
|
||||
"alloc.json", "txs.json", "env-missingrandom.json", "Paris", "",
|
||||
},
|
||||
output: t8nOutput{alloc: false, result: false},
|
||||
expExitCode: 3,
|
||||
|
|
@ -250,7 +250,7 @@ func TestT8n(t *testing.T) {
|
|||
{ // Test base fee calculation
|
||||
base: "./testdata/25",
|
||||
input: t8nInput{
|
||||
"alloc.json", "txs.json", "env.json", "Merge", "",
|
||||
"alloc.json", "txs.json", "env.json", "Paris", "",
|
||||
},
|
||||
output: t8nOutput{alloc: true, result: true},
|
||||
expOut: "exp.json",
|
||||
|
|
@ -378,7 +378,7 @@ func TestT8nTracing(t *testing.T) {
|
|||
{
|
||||
base: "./testdata/32",
|
||||
input: t8nInput{
|
||||
"alloc.json", "txs.json", "env.json", "Merge", "",
|
||||
"alloc.json", "txs.json", "env.json", "Paris", "",
|
||||
},
|
||||
extraArgs: []string{"--trace", "--trace.callframes"},
|
||||
expectedTraces: []string{"trace-0-0x47806361c0fa084be3caa18afe8c48156747c01dbdfc1ee11b5aecdbe4fcf23e.jsonl"},
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ data, and verifies that all snapshot storage data has a corresponding account.
|
|||
},
|
||||
{
|
||||
Name: "inspect-account",
|
||||
Usage: "Check all snapshot layers for the a specific account",
|
||||
Usage: "Check all snapshot layers for the specific account",
|
||||
ArgsUsage: "<address | hash>",
|
||||
Action: checkAccount,
|
||||
Flags: flags.Merge(utils.NetworkFlags, utils.DatabaseFlags),
|
||||
|
|
|
|||
32
cmd/geth/testdata/vcheck/vulnerabilities.json
vendored
32
cmd/geth/testdata/vcheck/vulnerabilities.json
vendored
|
|
@ -166,5 +166,37 @@
|
|||
"severity": "Low",
|
||||
"CVE": "CVE-2022-29177",
|
||||
"check": "(Geth\\/v1\\.10\\.(0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16)-.*)$"
|
||||
},
|
||||
{
|
||||
"name": "DoS via malicious p2p message",
|
||||
"uid": "GETH-2023-01",
|
||||
"summary": "A vulnerable node can be made to consume unbounded amounts of memory when handling specially crafted p2p messages sent from an attacker node.",
|
||||
"description": "The p2p handler spawned a new goroutine to respond to ping requests. By flooding a node with ping requests, an unbounded number of goroutines can be created, leading to resource exhaustion and potentially crash due to OOM.",
|
||||
"links": [
|
||||
"https://github.com/ethereum/go-ethereum/security/advisories/GHSA-ppjg-v974-84cm",
|
||||
"https://geth.ethereum.org/docs/vulnerabilities/vulnerabilities"
|
||||
],
|
||||
"introduced": "v1.10.0",
|
||||
"fixed": "v1.12.1",
|
||||
"published": "2023-09-06",
|
||||
"severity": "High",
|
||||
"CVE": "CVE-2023-40591",
|
||||
"check": "(Geth\\/v1\\.(10|11)\\..*)|(Geth\\/v1\\.12\\.0-.*)$"
|
||||
},
|
||||
{
|
||||
"name": "DoS via malicious p2p message",
|
||||
"uid": "GETH-2024-01",
|
||||
"summary": "A vulnerable node can be made to consume very large amounts of memory when handling specially crafted p2p messages sent from an attacker node.",
|
||||
"description": "A vulnerable node can be made to consume very large amounts of memory when handling specially crafted p2p messages sent from an attacker node. Full details will be available at the Github security [advisory](https://github.com/ethereum/go-ethereum/security/advisories/GHSA-4xc9-8hmq-j652)",
|
||||
"links": [
|
||||
"https://github.com/ethereum/go-ethereum/security/advisories/GHSA-4xc9-8hmq-j652",
|
||||
"https://geth.ethereum.org/docs/vulnerabilities/vulnerabilities"
|
||||
],
|
||||
"introduced": "v1.10.0",
|
||||
"fixed": "v1.13.15",
|
||||
"published": "2024-05-06",
|
||||
"severity": "High",
|
||||
"CVE": "CVE-2024-32972",
|
||||
"check": "(Geth\\/v1\\.(10|11|12)\\..*)|(Geth\\/v1\\.13\\.\\d-.*)|(Geth\\/v1\\.13\\.1(0|1|2|3|4)-.*)$"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1805,8 +1805,12 @@ func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool) (int, error)
|
|||
}
|
||||
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")
|
||||
}
|
||||
activeState = statedb
|
||||
|
||||
// If we have a followup block, run that against the current state to pre-cache
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import (
|
|||
// request represents a bloom retrieval task to prioritize and pull from the local
|
||||
// database or remotely from the network.
|
||||
type request struct {
|
||||
section uint64 // Section index to retrieve the a bit-vector from
|
||||
section uint64 // Section index to retrieve the bit-vector from
|
||||
bit uint // Bit index within the section to retrieve the vector of
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -228,7 +228,7 @@ func (b *testChainIndexBackend) Process(ctx context.Context, header *types.Heade
|
|||
b.t.Error("Unexpected call to Process")
|
||||
// Can't use Fatal since this is not the test's goroutine.
|
||||
// Returning error stops the chainIndexer's updateLoop
|
||||
return errors.New("Unexpected call to Process")
|
||||
return errors.New("unexpected call to Process")
|
||||
case b.processCh <- header.Number.Uint64():
|
||||
}
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -43,12 +43,11 @@ func TestGeneratePOSChain(t *testing.T) {
|
|||
bb = common.Address{0xbb}
|
||||
funds = big.NewInt(0).Mul(big.NewInt(1337), big.NewInt(params.Ether))
|
||||
config = *params.AllEthashProtocolChanges
|
||||
asm4788 = common.Hex2Bytes("3373fffffffffffffffffffffffffffffffffffffffe14604d57602036146024575f5ffd5b5f35801560495762001fff810690815414603c575f5ffd5b62001fff01545f5260205ff35b5f5ffd5b62001fff42064281555f359062001fff015500")
|
||||
gspec = &Genesis{
|
||||
Config: &config,
|
||||
Alloc: types.GenesisAlloc{
|
||||
address: {Balance: funds},
|
||||
params.BeaconRootsAddress: {Balance: common.Big0, Code: asm4788},
|
||||
params.BeaconRootsAddress: {Code: params.BeaconRootsCode},
|
||||
},
|
||||
BaseFee: big.NewInt(params.InitialBaseFee),
|
||||
Difficulty: common.Big1,
|
||||
|
|
|
|||
|
|
@ -593,6 +593,8 @@ func DeveloperGenesisBlock(gasLimit uint64, faucet *common.Address) *Genesis {
|
|||
common.BytesToAddress([]byte{7}): {Balance: big.NewInt(1)}, // ECScalarMul
|
||||
common.BytesToAddress([]byte{8}): {Balance: big.NewInt(1)}, // ECPairing
|
||||
common.BytesToAddress([]byte{9}): {Balance: big.NewInt(1)}, // BLAKE2b
|
||||
// Pre-deploy EIP-4788 system contract
|
||||
params.BeaconRootsAddress: {Nonce: 1, Code: params.BeaconRootsCode},
|
||||
// Pre-deploy EIP-2935 history contract.
|
||||
params.HistoryStorageAddress: types.Account{Nonce: 1, Code: params.HistoryStorageCode},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ type freezerOpenFunc = func() (*Freezer, error)
|
|||
// resettableFreezer is a wrapper of the freezer which makes the
|
||||
// freezer resettable.
|
||||
type resettableFreezer struct {
|
||||
readOnly bool
|
||||
freezer *Freezer
|
||||
opener freezerOpenFunc
|
||||
datadir string
|
||||
|
|
@ -60,6 +61,7 @@ func newResettableFreezer(datadir string, namespace string, readonly bool, maxTa
|
|||
return nil, err
|
||||
}
|
||||
return &resettableFreezer{
|
||||
readOnly: readonly,
|
||||
freezer: freezer,
|
||||
opener: opener,
|
||||
datadir: datadir,
|
||||
|
|
@ -74,6 +76,9 @@ func (f *resettableFreezer) Reset() error {
|
|||
f.lock.Lock()
|
||||
defer f.lock.Unlock()
|
||||
|
||||
if f.readOnly {
|
||||
return errReadOnly
|
||||
}
|
||||
if err := f.freezer.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
|
|
@ -33,6 +34,14 @@ import (
|
|||
"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
|
||||
|
||||
func (s Storage) Copy() Storage {
|
||||
|
|
@ -118,16 +127,12 @@ func (s *stateObject) touch() {
|
|||
}
|
||||
}
|
||||
|
||||
// getTrie returns the associated storage trie. The trie will be opened
|
||||
// if it's not loaded previously. An error will be returned if trie can't
|
||||
// be loaded.
|
||||
// getTrie returns the associated storage trie. The trie will be opened if it's
|
||||
// not loaded previously. An error will be returned if trie can't be loaded.
|
||||
//
|
||||
// If a new trie is opened, it will be cached within the state object to allow
|
||||
// subsequent reads to expand the same trie instead of reloading from disk.
|
||||
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 {
|
||||
tr, err := s.db.db.OpenStorageTrie(s.db.originalRoot, s.address, s.data.Root, s.db.trie)
|
||||
if err != nil {
|
||||
|
|
@ -135,10 +140,26 @@ func (s *stateObject) getTrie() (Trie, error) {
|
|||
}
|
||||
s.trie = tr
|
||||
}
|
||||
}
|
||||
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 {
|
||||
// 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
|
||||
}
|
||||
// Attempt to retrieve the trie from the prefetcher
|
||||
return s.db.prefetcher.trie(s.addrHash, s.data.Root)
|
||||
}
|
||||
|
||||
// GetState retrieves a value from the account storage trie.
|
||||
func (s *stateObject) GetState(key common.Hash) common.Hash {
|
||||
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
|
||||
// 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))
|
||||
for key, value := range s.dirtyStorage {
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
if s.db.prefetcher != nil && prefetch && len(slotsToPrefetch) > 0 && s.data.Root != types.EmptyRootHash {
|
||||
s.db.prefetcher.prefetch(s.addrHash, s.data.Root, s.address, slotsToPrefetch)
|
||||
if s.db.prefetcher != nil && len(slotsToPrefetch) > 0 && s.data.Root != types.EmptyRootHash {
|
||||
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 {
|
||||
s.dirtyStorage = make(Storage)
|
||||
|
|
@ -281,27 +304,39 @@ func (s *stateObject) finalise(prefetch bool) {
|
|||
// loading or updating of the trie, an error will be returned. Furthermore,
|
||||
// this function will return the mutated storage trie, or nil if there is no
|
||||
// storage change at all.
|
||||
//
|
||||
// It assumes all the dirty storage slots have been finalized before.
|
||||
func (s *stateObject) updateTrie() (Trie, error) {
|
||||
// Make sure all dirty slots are finalized into the pending storage area
|
||||
s.finalise(false)
|
||||
|
||||
// Short circuit if nothing changed, don't bother with hashing anything
|
||||
if len(s.pendingStorage) == 0 {
|
||||
return s.trie, nil
|
||||
}
|
||||
// Retrieve a pretecher populated trie, or fall back to the database
|
||||
tr := s.getPrefetchedTrie()
|
||||
if tr != nil {
|
||||
// Prefetcher returned a live trie, swap it out for the current one
|
||||
s.trie = tr
|
||||
} else {
|
||||
// Fetcher not running or empty trie, fallback to the database trie
|
||||
var err error
|
||||
tr, err = s.getTrie()
|
||||
if err != nil {
|
||||
s.db.setError(err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// The snapshot storage map for the object
|
||||
var (
|
||||
storage 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
|
||||
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
|
||||
// in circumstances similar to the following:
|
||||
//
|
||||
|
|
@ -330,26 +365,30 @@ func (s *stateObject) updateTrie() (Trie, error) {
|
|||
s.db.setError(err)
|
||||
return nil, err
|
||||
}
|
||||
s.db.StorageUpdated += 1
|
||||
s.db.StorageUpdated.Add(1)
|
||||
} else {
|
||||
deletions = append(deletions, key)
|
||||
}
|
||||
// Cache the mutated storage slots until commit
|
||||
if storage == nil {
|
||||
s.db.storagesLock.Lock()
|
||||
if storage = s.db.storages[s.addrHash]; storage == nil {
|
||||
storage = make(map[common.Hash][]byte)
|
||||
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
|
||||
|
||||
// Cache the original value of mutated storage slots
|
||||
if origin == nil {
|
||||
s.db.storagesLock.Lock()
|
||||
if origin = s.db.storagesOrigin[s.address]; origin == nil {
|
||||
origin = make(map[common.Hash][]byte)
|
||||
s.db.storagesOrigin[s.address] = origin
|
||||
}
|
||||
s.db.storagesLock.Unlock()
|
||||
}
|
||||
// Track the original value of slot only if it's mutated first time
|
||||
if _, ok := origin[khash]; !ok {
|
||||
|
|
@ -369,7 +408,7 @@ func (s *stateObject) updateTrie() (Trie, error) {
|
|||
s.db.setError(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
|
||||
// the above work in the first place
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import (
|
|||
"slices"
|
||||
"sort"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
|
|
@ -97,9 +98,11 @@ type StateDB struct {
|
|||
// These maps hold the state changes (including the corresponding
|
||||
// original value) that occurred in this **block**.
|
||||
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
|
||||
|
||||
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
|
||||
storagesLock sync.Mutex // Mutex protecting the maps during concurrent updates/commits
|
||||
|
||||
// This map holds 'live' objects, which will get modified while
|
||||
// processing a state transition.
|
||||
|
|
@ -165,9 +168,9 @@ type StateDB struct {
|
|||
TrieDBCommits time.Duration
|
||||
|
||||
AccountUpdated int
|
||||
StorageUpdated int
|
||||
StorageUpdated atomic.Int64
|
||||
AccountDeleted int
|
||||
StorageDeleted int
|
||||
StorageDeleted atomic.Int64
|
||||
|
||||
// Testing hooks
|
||||
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.
|
||||
func (s *StateDB) StartPrefetcher(namespace string) {
|
||||
if s.prefetcher != nil {
|
||||
s.prefetcher.close()
|
||||
s.prefetcher.terminate(false)
|
||||
s.prefetcher.report()
|
||||
s.prefetcher = nil
|
||||
}
|
||||
if s.snap != nil {
|
||||
|
|
@ -226,7 +230,8 @@ func (s *StateDB) StartPrefetcher(namespace string) {
|
|||
// from the gathered metrics.
|
||||
func (s *StateDB) StopPrefetcher() {
|
||||
if s.prefetcher != nil {
|
||||
s.prefetcher.close()
|
||||
s.prefetcher.terminate(false)
|
||||
s.prefetcher.report()
|
||||
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.
|
||||
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
|
||||
addr := obj.Address()
|
||||
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.
|
||||
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 {
|
||||
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.
|
||||
state.accessList = s.accessList.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
|
||||
}
|
||||
|
||||
|
|
@ -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.storagesOrigin, obj.address) // Clear out any previously updated storage data (may be recreated via a resurrect)
|
||||
} else {
|
||||
obj.finalise(true) // Prefetch slots in the background
|
||||
obj.finalise()
|
||||
s.markUpdate(addr)
|
||||
}
|
||||
// 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
|
||||
}
|
||||
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.
|
||||
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
|
||||
s.Finalise(deleteEmptyObjects)
|
||||
|
||||
// If there was a trie prefetcher operating, it gets aborted and irrevocably
|
||||
// modified after we start retrieving tries. Remove it from the statedb after
|
||||
// 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 there was a trie prefetcher operating, terminate it async so that the
|
||||
// individual storage tries can be updated as soon as the disk load finishes.
|
||||
if s.prefetcher != nil {
|
||||
s.prefetcher.terminate(true)
|
||||
defer func() {
|
||||
s.prefetcher.close()
|
||||
s.prefetcher = nil
|
||||
s.prefetcher.report()
|
||||
s.prefetcher = nil // Pre-byzantium, unset any used up prefetcher
|
||||
}()
|
||||
}
|
||||
// Although naively it makes sense to retrieve the account trie and then do
|
||||
// the contract storage and account updates sequentially, that short circuits
|
||||
// the account prefetcher. Instead, let's process all the storage updates
|
||||
// first, giving the account prefetches just a few more milliseconds of time
|
||||
// to pull useful data from disk.
|
||||
start := time.Now()
|
||||
// Process all storage updates concurrently. The state object update root
|
||||
// method will internally call a blocking trie fetch from the prefetcher,
|
||||
// so there's no need to explicitly wait for the prefetchers to finish.
|
||||
var (
|
||||
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 {
|
||||
if op.applied {
|
||||
if op.applied || op.isDelete() {
|
||||
continue
|
||||
}
|
||||
if op.isDelete() {
|
||||
continue
|
||||
}
|
||||
s.stateObjects[addr].updateRoot()
|
||||
obj := s.stateObjects[addr] // closure for the task runner below
|
||||
workers.Go(func() error {
|
||||
obj.updateRoot()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
workers.Wait()
|
||||
s.StorageUpdates += time.Since(start)
|
||||
|
||||
// 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
|
||||
// which has the same root, but also has some content loaded into it.
|
||||
if prefetcher != nil {
|
||||
if trie := prefetcher.trie(common.Hash{}, s.originalRoot); trie != nil {
|
||||
start = time.Now()
|
||||
|
||||
if s.prefetcher != nil {
|
||||
if trie := s.prefetcher.trie(common.Hash{}, s.originalRoot); trie == nil {
|
||||
log.Error("Failed to retrieve account pre-fetcher trie")
|
||||
} else {
|
||||
s.trie = trie
|
||||
}
|
||||
}
|
||||
|
|
@ -913,8 +916,10 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
|
|||
s.deleteStateObject(deletedAddr)
|
||||
s.AccountDeleted += 1
|
||||
}
|
||||
if prefetcher != nil {
|
||||
prefetcher.used(common.Hash{}, s.originalRoot, usedAddrs)
|
||||
s.AccountUpdates += time.Since(start)
|
||||
|
||||
if s.prefetcher != nil {
|
||||
s.prefetcher.used(common.Hash{}, s.originalRoot, usedAddrs)
|
||||
}
|
||||
// Track the amount of time wasted on hashing the account trie
|
||||
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
|
||||
}
|
||||
accountUpdatedMeter.Mark(int64(s.AccountUpdated))
|
||||
storageUpdatedMeter.Mark(int64(s.StorageUpdated))
|
||||
storageUpdatedMeter.Mark(s.StorageUpdated.Load())
|
||||
accountDeletedMeter.Mark(int64(s.AccountDeleted))
|
||||
storageDeletedMeter.Mark(int64(s.StorageDeleted))
|
||||
storageDeletedMeter.Mark(s.StorageDeleted.Load())
|
||||
accountTrieUpdatedMeter.Mark(int64(accountTrieNodesUpdated))
|
||||
accountTrieDeletedMeter.Mark(int64(accountTrieNodesDeleted))
|
||||
storageTriesUpdatedMeter.Mark(int64(storageTrieNodesUpdated))
|
||||
storageTriesDeletedMeter.Mark(int64(storageTrieNodesDeleted))
|
||||
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 s.snap != nil {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
package state
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
|
|
@ -27,6 +28,10 @@ import (
|
|||
var (
|
||||
// triePrefetchMetricsPrefix is the prefix under which to publish the metrics.
|
||||
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
|
||||
|
|
@ -37,52 +42,64 @@ var (
|
|||
type triePrefetcher struct {
|
||||
db Database // Database to fetch trie nodes through
|
||||
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
|
||||
term chan struct{} // Channel to signal interruption
|
||||
|
||||
deliveryMissMeter metrics.Meter
|
||||
accountLoadMeter metrics.Meter
|
||||
accountDupMeter metrics.Meter
|
||||
accountSkipMeter metrics.Meter
|
||||
accountWasteMeter metrics.Meter
|
||||
storageLoadMeter metrics.Meter
|
||||
storageDupMeter metrics.Meter
|
||||
storageSkipMeter metrics.Meter
|
||||
storageWasteMeter metrics.Meter
|
||||
}
|
||||
|
||||
func newTriePrefetcher(db Database, root common.Hash, namespace string) *triePrefetcher {
|
||||
prefix := triePrefetchMetricsPrefix + namespace
|
||||
p := &triePrefetcher{
|
||||
return &triePrefetcher{
|
||||
db: db,
|
||||
root: root,
|
||||
fetchers: make(map[string]*subfetcher), // Active prefetchers use the fetchers map
|
||||
term: make(chan struct{}),
|
||||
|
||||
deliveryMissMeter: metrics.GetOrRegisterMeter(prefix+"/deliverymiss", nil),
|
||||
accountLoadMeter: metrics.GetOrRegisterMeter(prefix+"/account/load", nil),
|
||||
accountDupMeter: metrics.GetOrRegisterMeter(prefix+"/account/dup", nil),
|
||||
accountSkipMeter: metrics.GetOrRegisterMeter(prefix+"/account/skip", nil),
|
||||
accountWasteMeter: metrics.GetOrRegisterMeter(prefix+"/account/waste", nil),
|
||||
storageLoadMeter: metrics.GetOrRegisterMeter(prefix+"/storage/load", nil),
|
||||
storageDupMeter: metrics.GetOrRegisterMeter(prefix+"/storage/dup", nil),
|
||||
storageSkipMeter: metrics.GetOrRegisterMeter(prefix+"/storage/skip", nil),
|
||||
storageWasteMeter: metrics.GetOrRegisterMeter(prefix+"/storage/waste", nil),
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// close iterates over all the subfetchers, aborts any that were left spinning
|
||||
// and reports the stats to the metrics subsystem.
|
||||
func (p *triePrefetcher) close() {
|
||||
// terminate iterates over all the subfetchers and issues a termination request
|
||||
// to all of them. Depending on the async parameter, the method will either block
|
||||
// until all subfetchers spin down, or return immediately.
|
||||
func (p *triePrefetcher) terminate(async bool) {
|
||||
// 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 {
|
||||
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 {
|
||||
p.accountLoadMeter.Mark(int64(len(fetcher.seen)))
|
||||
p.accountDupMeter.Mark(int64(fetcher.dups))
|
||||
p.accountSkipMeter.Mark(int64(len(fetcher.tasks)))
|
||||
|
||||
for _, key := range fetcher.used {
|
||||
delete(fetcher.seen, string(key))
|
||||
}
|
||||
|
|
@ -90,107 +107,61 @@ func (p *triePrefetcher) close() {
|
|||
} else {
|
||||
p.storageLoadMeter.Mark(int64(len(fetcher.seen)))
|
||||
p.storageDupMeter.Mark(int64(fetcher.dups))
|
||||
p.storageSkipMeter.Mark(int64(len(fetcher.tasks)))
|
||||
|
||||
for _, key := range fetcher.used {
|
||||
delete(fetcher.seen, string(key))
|
||||
}
|
||||
p.storageWasteMeter.Mark(int64(len(fetcher.seen)))
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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
|
||||
// already loaded will be copied over, but no goroutines will be started. This
|
||||
// is mostly used in the miner which creates a copy of it's actively mutated
|
||||
// state to be sealed while it may further mutate the state.
|
||||
func (p *triePrefetcher) copy() *triePrefetcher {
|
||||
copy := &triePrefetcher{
|
||||
db: p.db,
|
||||
root: p.root,
|
||||
fetches: make(map[string]Trie), // Active prefetchers use the fetches map
|
||||
|
||||
deliveryMissMeter: p.deliveryMissMeter,
|
||||
accountLoadMeter: p.accountLoadMeter,
|
||||
accountDupMeter: p.accountDupMeter,
|
||||
accountSkipMeter: p.accountSkipMeter,
|
||||
accountWasteMeter: p.accountWasteMeter,
|
||||
storageLoadMeter: p.storageLoadMeter,
|
||||
storageDupMeter: p.storageDupMeter,
|
||||
storageSkipMeter: p.storageSkipMeter,
|
||||
storageWasteMeter: p.storageWasteMeter,
|
||||
// prefetch schedules a batch of trie items to prefetch. After the prefetcher is
|
||||
// closed, all the following tasks scheduled will not be executed and an error
|
||||
// will be returned.
|
||||
//
|
||||
// prefetch is called from two locations:
|
||||
//
|
||||
// 1. Finalize of the state-objects storage roots. This happens at the end
|
||||
// of every transaction, meaning that if several transactions touches
|
||||
// upon the same contract, the parameters invoking this method may be
|
||||
// repeated.
|
||||
// 2. Finalize of the main account trie. This happens only once per block.
|
||||
func (p *triePrefetcher) prefetch(owner common.Hash, root common.Hash, addr common.Address, keys [][]byte) error {
|
||||
// Ensure the subfetcher is still alive
|
||||
select {
|
||||
case <-p.term:
|
||||
return errTerminated
|
||||
default:
|
||||
}
|
||||
// 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)
|
||||
fetcher := p.fetchers[id]
|
||||
if fetcher == nil {
|
||||
fetcher = newSubfetcher(p.db, p.root, owner, root, addr)
|
||||
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
|
||||
// have it.
|
||||
// trie returns the trie matching the root hash, blocking until the fetcher of
|
||||
// the given trie terminates. If no fetcher exists for the request, nil will be
|
||||
// returned.
|
||||
func (p *triePrefetcher) trie(owner common.Hash, root common.Hash) Trie {
|
||||
// If the prefetcher is inactive, return from existing deep copies
|
||||
id := p.trieID(owner, root)
|
||||
if p.fetches != nil {
|
||||
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]
|
||||
// Bail if no trie was prefetched for this root
|
||||
fetcher := p.fetchers[p.trieID(owner, root)]
|
||||
if fetcher == nil {
|
||||
log.Error("Prefetcher missed to load trie", "owner", owner, "root", root)
|
||||
p.deliveryMissMeter.Mark(1)
|
||||
return nil
|
||||
}
|
||||
// Interrupt the prefetcher if it's by any chance still running and return
|
||||
// a copy of any pre-loaded trie.
|
||||
fetcher.abort() // safe to do multiple times
|
||||
|
||||
trie := fetcher.peek()
|
||||
if trie == nil {
|
||||
p.deliveryMissMeter.Mark(1)
|
||||
return nil
|
||||
}
|
||||
return trie
|
||||
// Subfetcher exists, retrieve its trie
|
||||
return fetcher.peek()
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -221,7 +192,6 @@ type subfetcher struct {
|
|||
wake chan struct{} // Wake channel if a new task is scheduled
|
||||
stop chan struct{} // Channel to interrupt processing
|
||||
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
|
||||
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),
|
||||
stop: make(chan struct{}),
|
||||
term: make(chan struct{}),
|
||||
copy: make(chan chan Trie),
|
||||
seen: make(map[string]struct{}),
|
||||
}
|
||||
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.
|
||||
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
|
||||
sf.lock.Lock()
|
||||
sf.tasks = append(sf.tasks, keys...)
|
||||
sf.lock.Unlock()
|
||||
|
||||
// Notify the prefetcher, it's fine if it's already terminated
|
||||
// Notify the background thread to execute scheduled tasks
|
||||
select {
|
||||
case sf.wake <- struct{}{}:
|
||||
// Wake signal sent
|
||||
default:
|
||||
// Wake signal not sent as a previous one 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 sf.db.CopyTrie(sf.trie)
|
||||
}
|
||||
}
|
||||
|
||||
// abort interrupts the subfetcher immediately. It is safe to call abort multiple
|
||||
// times but it is not thread safe.
|
||||
func (sf *subfetcher) abort() {
|
||||
// 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
|
||||
}
|
||||
|
||||
// peek retrieves the fetcher's trie, populated with any pre-fetched data. The
|
||||
// returned trie will be a shallow copy, so modifying it will break subsequent
|
||||
// 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 fetcher 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 {
|
||||
case <-sf.stop:
|
||||
default:
|
||||
close(sf.stop)
|
||||
}
|
||||
if async {
|
||||
return
|
||||
}
|
||||
<-sf.term
|
||||
}
|
||||
|
||||
// loop waits for new tasks to be scheduled and keeps loading them until it runs
|
||||
// out of tasks or its underlying trie is retrieved for committing.
|
||||
// loop loads newly-scheduled trie tasks as they are received and loads them, stopping
|
||||
// when requested.
|
||||
func (sf *subfetcher) loop() {
|
||||
// No matter how the loop stops, signal anyone waiting that it's terminated
|
||||
defer close(sf.term)
|
||||
|
|
@ -305,8 +285,6 @@ func (sf *subfetcher) loop() {
|
|||
}
|
||||
sf.trie = trie
|
||||
} 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)
|
||||
if err != nil {
|
||||
log.Warn("Trie prefetcher failed opening trie", "root", sf.root, "err", err)
|
||||
|
|
@ -318,31 +296,17 @@ func (sf *subfetcher) loop() {
|
|||
for {
|
||||
select {
|
||||
case <-sf.wake:
|
||||
// Subfetcher was woken up, retrieve any tasks to avoid spinning the lock
|
||||
// Execute all remaining tasks in a single run
|
||||
sf.lock.Lock()
|
||||
tasks := sf.tasks
|
||||
sf.tasks = nil
|
||||
sf.lock.Unlock()
|
||||
|
||||
// Prefetch any tasks until the loop is interrupted
|
||||
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
|
||||
for _, task := range tasks {
|
||||
if _, ok := sf.seen[string(task)]; ok {
|
||||
sf.dups++
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
if len(task) == common.AddressLength {
|
||||
sf.trie.GetAccount(common.BytesToAddress(task))
|
||||
} else {
|
||||
|
|
@ -350,16 +314,20 @@ func (sf *subfetcher) loop() {
|
|||
}
|
||||
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:
|
||||
// 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
|
||||
}
|
||||
// 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 (
|
||||
"math/big"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||
|
|
@ -46,68 +45,20 @@ func filledStateDB() *StateDB {
|
|||
return state
|
||||
}
|
||||
|
||||
func TestCopyAndClose(t *testing.T) {
|
||||
func TestUseAfterTerminate(t *testing.T) {
|
||||
db := filledStateDB()
|
||||
prefetcher := newTriePrefetcher(db.db, db.originalRoot, "")
|
||||
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) {
|
||||
db := filledStateDB()
|
||||
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 err := prefetcher.prefetch(common.Hash{}, db.originalRoot, common.Address{}, [][]byte{skey.Bytes()}); err != nil {
|
||||
t.Errorf("Prefetch failed before terminate: %v", err)
|
||||
}
|
||||
if b != nil {
|
||||
t.Fatal("Trie after close should return nil")
|
||||
}
|
||||
}
|
||||
prefetcher.terminate(false)
|
||||
|
||||
func TestCopyClose(t *testing.T) {
|
||||
db := filledStateDB()
|
||||
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 err := prefetcher.prefetch(common.Hash{}, db.originalRoot, common.Address{}, [][]byte{skey.Bytes()}); err == nil {
|
||||
t.Errorf("Prefetch succeeded after terminate: %v", err)
|
||||
}
|
||||
if b == nil {
|
||||
t.Fatal("Copy trie should return nil")
|
||||
}
|
||||
if c != nil {
|
||||
t.Fatal("Trie after close should return nil")
|
||||
}
|
||||
if d == nil {
|
||||
t.Fatal("Copy trie should not return nil")
|
||||
if tr := prefetcher.trie(common.Hash{}, db.originalRoot); tr == nil {
|
||||
t.Errorf("Prefetcher returned nil trie after terminate")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -240,8 +240,9 @@ func (st *StateTransition) buyGas() error {
|
|||
if st.msg.GasFeeCap != nil {
|
||||
balanceCheck.SetUint64(st.msg.GasLimit)
|
||||
balanceCheck = balanceCheck.Mul(balanceCheck, st.msg.GasFeeCap)
|
||||
balanceCheck.Add(balanceCheck, st.msg.Value)
|
||||
}
|
||||
balanceCheck.Add(balanceCheck, st.msg.Value)
|
||||
|
||||
if st.evm.ChainConfig().IsCancun(st.evm.Context.BlockNumber, st.evm.Context.Time) {
|
||||
if blobGas := st.blobGasUsed(); blobGas > 0 {
|
||||
// Check that the user has enough funds to cover blobGasUsed * tx.BlobGasFeeCap
|
||||
|
|
|
|||
|
|
@ -556,7 +556,7 @@ func (s Transactions) EncodeIndex(i int, w *bytes.Buffer) {
|
|||
}
|
||||
}
|
||||
|
||||
// TxDifference returns a new set which is the difference between a and b.
|
||||
// TxDifference returns a new set of transactions that are present in a but not in b.
|
||||
func TxDifference(a, b Transactions) Transactions {
|
||||
keep := make(Transactions, 0, len(a))
|
||||
|
||||
|
|
@ -574,7 +574,7 @@ func TxDifference(a, b Transactions) Transactions {
|
|||
return keep
|
||||
}
|
||||
|
||||
// HashDifference returns a new set which is the difference between a and b.
|
||||
// HashDifference returns a new set of hashes that are present in a but not in b.
|
||||
func HashDifference(a, b []common.Hash) []common.Hash {
|
||||
keep := make([]common.Hash, 0, len(a))
|
||||
|
||||
|
|
|
|||
|
|
@ -379,7 +379,7 @@ func assertEqual(orig *Transaction, cpy *Transaction) error {
|
|||
}
|
||||
if orig.AccessList() != nil {
|
||||
if !reflect.DeepEqual(orig.AccessList(), cpy.AccessList()) {
|
||||
return errors.New("access list wrong!")
|
||||
return errors.New("access list wrong")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -57,6 +57,10 @@ type Config struct {
|
|||
// sets defaults on the config
|
||||
func setDefaults(cfg *Config) {
|
||||
if cfg.ChainConfig == nil {
|
||||
var (
|
||||
shanghaiTime = uint64(0)
|
||||
cancunTime = uint64(0)
|
||||
)
|
||||
cfg.ChainConfig = ¶ms.ChainConfig{
|
||||
ChainID: big.NewInt(1),
|
||||
HomesteadBlock: new(big.Int),
|
||||
|
|
@ -72,9 +76,14 @@ func setDefaults(cfg *Config) {
|
|||
MuirGlacierBlock: new(big.Int),
|
||||
BerlinBlock: new(big.Int),
|
||||
LondonBlock: new(big.Int),
|
||||
ArrowGlacierBlock: nil,
|
||||
GrayGlacierBlock: nil,
|
||||
TerminalTotalDifficulty: big.NewInt(0),
|
||||
TerminalTotalDifficultyPassed: true,
|
||||
MergeNetsplitBlock: nil,
|
||||
ShanghaiTime: &shanghaiTime,
|
||||
CancunTime: &cancunTime}
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.Difficulty == nil {
|
||||
cfg.Difficulty = new(big.Int)
|
||||
}
|
||||
|
|
@ -101,6 +110,10 @@ func setDefaults(cfg *Config) {
|
|||
if cfg.BlobBaseFee == nil {
|
||||
cfg.BlobBaseFee = big.NewInt(params.BlobTxMinBlobGasprice)
|
||||
}
|
||||
// Merge indicators
|
||||
if t := cfg.ChainConfig.ShanghaiTime; cfg.ChainConfig.TerminalTotalDifficultyPassed || (t != nil && *t == 0) {
|
||||
cfg.Random = &(common.Hash{})
|
||||
}
|
||||
}
|
||||
|
||||
// Execute executes the code using the input as call data during the execution.
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ func TestExecute(t *testing.T) {
|
|||
|
||||
func TestCall(t *testing.T) {
|
||||
state, _ := state.New(types.EmptyRootHash, state.NewDatabase(rawdb.NewMemoryDatabase()), nil)
|
||||
address := common.HexToAddress("0x0a")
|
||||
address := common.HexToAddress("0xaa")
|
||||
state.SetCode(address, []byte{
|
||||
byte(vm.PUSH1), 10,
|
||||
byte(vm.PUSH1), 0,
|
||||
|
|
@ -725,7 +725,7 @@ func TestRuntimeJSTracer(t *testing.T) {
|
|||
byte(vm.CREATE),
|
||||
byte(vm.POP),
|
||||
},
|
||||
results: []string{`"1,1,952855,6,12"`, `"1,1,952855,6,0"`},
|
||||
results: []string{`"1,1,952853,6,12"`, `"1,1,952853,6,0"`},
|
||||
},
|
||||
{
|
||||
// CREATE2
|
||||
|
|
@ -741,7 +741,7 @@ func TestRuntimeJSTracer(t *testing.T) {
|
|||
byte(vm.CREATE2),
|
||||
byte(vm.POP),
|
||||
},
|
||||
results: []string{`"1,1,952846,6,13"`, `"1,1,952846,6,0"`},
|
||||
results: []string{`"1,1,952844,6,13"`, `"1,1,952844,6,0"`},
|
||||
},
|
||||
{
|
||||
// CALL
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ package eth
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"runtime"
|
||||
|
|
@ -105,9 +104,6 @@ type Ethereum struct {
|
|||
// whose lifecycle will be managed by the provided node.
|
||||
func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) {
|
||||
// Ensure configuration values are compatible and sane
|
||||
if config.SyncMode == downloader.LightSync {
|
||||
return nil, errors.New("can't run eth.Ethereum in light sync mode, light mode has been deprecated")
|
||||
}
|
||||
if !config.SyncMode.IsValid() {
|
||||
return nil, fmt.Errorf("invalid sync mode %d", config.SyncMode)
|
||||
}
|
||||
|
|
@ -208,7 +204,7 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) {
|
|||
}
|
||||
t, err := tracers.LiveDirectory.New(config.VMTrace, traceConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to create tracer %s: %v", config.VMTrace, err)
|
||||
return nil, fmt.Errorf("failed to create tracer %s: %v", config.VMTrace, err)
|
||||
}
|
||||
vmConfig.Tracer = t
|
||||
}
|
||||
|
|
|
|||
|
|
@ -979,11 +979,11 @@ func TestSimultaneousNewBlock(t *testing.T) {
|
|||
defer wg.Done()
|
||||
if newResp, err := api.NewPayloadV1(*execData); err != nil {
|
||||
errMu.Lock()
|
||||
testErr = fmt.Errorf("Failed to insert block: %w", err)
|
||||
testErr = fmt.Errorf("failed to insert block: %w", err)
|
||||
errMu.Unlock()
|
||||
} else if newResp.Status != "VALID" {
|
||||
errMu.Lock()
|
||||
testErr = fmt.Errorf("Failed to insert block: %v", newResp.Status)
|
||||
testErr = fmt.Errorf("failed to insert block: %v", newResp.Status)
|
||||
errMu.Unlock()
|
||||
}
|
||||
}()
|
||||
|
|
@ -1018,7 +1018,7 @@ func TestSimultaneousNewBlock(t *testing.T) {
|
|||
defer wg.Done()
|
||||
if _, err := api.ForkchoiceUpdatedV1(fcState, nil); err != nil {
|
||||
errMu.Lock()
|
||||
testErr = fmt.Errorf("Failed to insert block: %w", err)
|
||||
testErr = fmt.Errorf("failed to insert block: %w", err)
|
||||
errMu.Unlock()
|
||||
}
|
||||
}()
|
||||
|
|
|
|||
|
|
@ -202,7 +202,7 @@ func (d *Downloader) findBeaconAncestor() (uint64, error) {
|
|||
case SnapSync:
|
||||
chainHead = d.blockchain.CurrentSnapBlock()
|
||||
default:
|
||||
chainHead = d.lightchain.CurrentHeader()
|
||||
panic("unknown sync mode")
|
||||
}
|
||||
number := chainHead.Number.Uint64()
|
||||
|
||||
|
|
@ -222,7 +222,7 @@ func (d *Downloader) findBeaconAncestor() (uint64, error) {
|
|||
case SnapSync:
|
||||
linked = d.blockchain.HasFastBlock(beaconTail.ParentHash, beaconTail.Number.Uint64()-1)
|
||||
default:
|
||||
linked = d.blockchain.HasHeader(beaconTail.ParentHash, beaconTail.Number.Uint64()-1)
|
||||
panic("unknown sync mode")
|
||||
}
|
||||
if !linked {
|
||||
// This is a programming error. The chain backfiller was called with a
|
||||
|
|
@ -257,7 +257,7 @@ func (d *Downloader) findBeaconAncestor() (uint64, error) {
|
|||
case SnapSync:
|
||||
known = d.blockchain.HasFastBlock(h.Hash(), n)
|
||||
default:
|
||||
known = d.lightchain.HasHeader(h.Hash(), n)
|
||||
panic("unknown sync mode")
|
||||
}
|
||||
if !known {
|
||||
end = check
|
||||
|
|
|
|||
|
|
@ -67,7 +67,6 @@ var (
|
|||
errCancelContentProcessing = errors.New("content processing canceled (requested)")
|
||||
errCanceled = errors.New("syncing canceled (requested)")
|
||||
errNoPivotHeader = errors.New("pivot header is not found")
|
||||
ErrMergeTransition = errors.New("legacy sync reached the merge")
|
||||
)
|
||||
|
||||
// peerDropFn is a callback type for dropping a peer detected as malicious.
|
||||
|
|
@ -98,7 +97,6 @@ type Downloader struct {
|
|||
syncStatsChainHeight uint64 // Highest block number known when syncing started
|
||||
syncStatsLock sync.RWMutex // Lock protecting the sync stats fields
|
||||
|
||||
lightchain LightChain
|
||||
blockchain BlockChain
|
||||
|
||||
// Callbacks
|
||||
|
|
@ -143,8 +141,8 @@ type Downloader struct {
|
|||
syncLogTime time.Time // Time instance when status was last reported
|
||||
}
|
||||
|
||||
// LightChain encapsulates functions required to synchronise a light chain.
|
||||
type LightChain interface {
|
||||
// BlockChain encapsulates functions required to sync a (full or snap) blockchain.
|
||||
type BlockChain interface {
|
||||
// HasHeader verifies a header's presence in the local chain.
|
||||
HasHeader(common.Hash, uint64) bool
|
||||
|
||||
|
|
@ -162,11 +160,6 @@ type LightChain interface {
|
|||
|
||||
// SetHead rewinds the local chain to a new head.
|
||||
SetHead(uint64) error
|
||||
}
|
||||
|
||||
// BlockChain encapsulates functions required to sync a (full or snap) blockchain.
|
||||
type BlockChain interface {
|
||||
LightChain
|
||||
|
||||
// HasBlock verifies a block's presence in the local chain.
|
||||
HasBlock(common.Hash, uint64) bool
|
||||
|
|
@ -201,17 +194,13 @@ type BlockChain interface {
|
|||
}
|
||||
|
||||
// New creates a new downloader to fetch hashes and blocks from remote peers.
|
||||
func New(stateDb ethdb.Database, mux *event.TypeMux, chain BlockChain, lightchain LightChain, dropPeer peerDropFn, success func()) *Downloader {
|
||||
if lightchain == nil {
|
||||
lightchain = chain
|
||||
}
|
||||
func New(stateDb ethdb.Database, mux *event.TypeMux, chain BlockChain, dropPeer peerDropFn, success func()) *Downloader {
|
||||
dl := &Downloader{
|
||||
stateDB: stateDb,
|
||||
mux: mux,
|
||||
queue: newQueue(blockCacheMaxItems, blockCacheInitialItems),
|
||||
peers: newPeerSet(),
|
||||
blockchain: chain,
|
||||
lightchain: lightchain,
|
||||
dropPeer: dropPeer,
|
||||
headerProcCh: make(chan *headerTask, 1),
|
||||
quitCh: make(chan struct{}),
|
||||
|
|
@ -240,15 +229,13 @@ func (d *Downloader) Progress() ethereum.SyncProgress {
|
|||
|
||||
current := uint64(0)
|
||||
mode := d.getMode()
|
||||
switch {
|
||||
case d.blockchain != nil && mode == FullSync:
|
||||
switch mode {
|
||||
case FullSync:
|
||||
current = d.blockchain.CurrentBlock().Number.Uint64()
|
||||
case d.blockchain != nil && mode == SnapSync:
|
||||
case SnapSync:
|
||||
current = d.blockchain.CurrentSnapBlock().Number.Uint64()
|
||||
case d.lightchain != nil:
|
||||
current = d.lightchain.CurrentHeader().Number.Uint64()
|
||||
default:
|
||||
log.Error("Unknown downloader chain/mode combo", "light", d.lightchain != nil, "full", d.blockchain != nil, "mode", mode)
|
||||
log.Error("Unknown downloader mode", "mode", mode)
|
||||
}
|
||||
progress, pending := d.SnapSyncer.Progress()
|
||||
|
||||
|
|
@ -402,7 +389,7 @@ func (d *Downloader) syncToHead() (err error) {
|
|||
if err != nil {
|
||||
d.mux.Post(FailedEvent{err})
|
||||
} else {
|
||||
latest := d.lightchain.CurrentHeader()
|
||||
latest := d.blockchain.CurrentHeader()
|
||||
d.mux.Post(DoneEvent{latest})
|
||||
}
|
||||
}()
|
||||
|
|
@ -520,7 +507,7 @@ func (d *Downloader) syncToHead() (err error) {
|
|||
}
|
||||
// Rewind the ancient store and blockchain if reorg happens.
|
||||
if origin+1 < frozen {
|
||||
if err := d.lightchain.SetHead(origin); err != nil {
|
||||
if err := d.blockchain.SetHead(origin); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Info("Truncated excess ancient chain segment", "oldhead", frozen-1, "newhead", origin)
|
||||
|
|
@ -690,19 +677,17 @@ func (d *Downloader) processHeaders(origin uint64) error {
|
|||
chunkHashes := hashes[:limit]
|
||||
|
||||
// In case of header only syncing, validate the chunk immediately
|
||||
if mode == SnapSync || mode == LightSync {
|
||||
if mode == SnapSync {
|
||||
// Although the received headers might be all valid, a legacy
|
||||
// PoW/PoA sync must not accept post-merge headers. Make sure
|
||||
// that any transition is rejected at this point.
|
||||
if len(chunkHeaders) > 0 {
|
||||
if n, err := d.lightchain.InsertHeaderChain(chunkHeaders); err != nil {
|
||||
if n, err := d.blockchain.InsertHeaderChain(chunkHeaders); err != nil {
|
||||
log.Warn("Invalid header encountered", "number", chunkHeaders[n].Number, "hash", chunkHashes[n], "parent", chunkHeaders[n].ParentHash, "err", err)
|
||||
return fmt.Errorf("%w: %v", errInvalidChain, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Unless we're doing light chains, schedule the headers for associated content retrieval
|
||||
if mode == FullSync || mode == SnapSync {
|
||||
// If we've reached the allowed number of pending headers, stall a bit
|
||||
for d.queue.PendingBodies() >= maxQueuedHeaders || d.queue.PendingReceipts() >= maxQueuedHeaders {
|
||||
timer.Reset(time.Second)
|
||||
|
|
@ -717,7 +702,7 @@ func (d *Downloader) processHeaders(origin uint64) error {
|
|||
if len(inserts) != len(chunkHeaders) {
|
||||
return fmt.Errorf("%w: stale headers", errBadPeer)
|
||||
}
|
||||
}
|
||||
|
||||
headers = headers[limit:]
|
||||
hashes = hashes[limit:]
|
||||
origin += uint64(limit)
|
||||
|
|
@ -1056,7 +1041,7 @@ func (d *Downloader) readHeaderRange(last *types.Header, count int) []*types.Hea
|
|||
headers []*types.Header
|
||||
)
|
||||
for {
|
||||
parent := d.lightchain.GetHeaderByHash(current.ParentHash)
|
||||
parent := d.blockchain.GetHeaderByHash(current.ParentHash)
|
||||
if parent == nil {
|
||||
break // The chain is not continuous, or the chain is exhausted
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ func newTesterWithNotification(t *testing.T, success func()) *downloadTester {
|
|||
chain: chain,
|
||||
peers: make(map[string]*downloadTesterPeer),
|
||||
}
|
||||
tester.downloader = New(db, new(event.TypeMux), tester.chain, nil, tester.dropPeer, success)
|
||||
tester.downloader = New(db, new(event.TypeMux), tester.chain, tester.dropPeer, success)
|
||||
return tester
|
||||
}
|
||||
|
||||
|
|
@ -384,9 +384,6 @@ func assertOwnChain(t *testing.T, tester *downloadTester, length int) {
|
|||
t.Helper()
|
||||
|
||||
headers, blocks, receipts := length, length, length
|
||||
if tester.downloader.getMode() == LightSync {
|
||||
blocks, receipts = 1, 1
|
||||
}
|
||||
if hs := int(tester.chain.CurrentHeader().Number.Uint64()) + 1; hs != headers {
|
||||
t.Fatalf("synchronised headers mismatch: have %v, want %v", hs, headers)
|
||||
}
|
||||
|
|
@ -400,7 +397,6 @@ func assertOwnChain(t *testing.T, tester *downloadTester, length int) {
|
|||
|
||||
func TestCanonicalSynchronisation68Full(t *testing.T) { testCanonSync(t, eth.ETH68, FullSync) }
|
||||
func TestCanonicalSynchronisation68Snap(t *testing.T) { testCanonSync(t, eth.ETH68, SnapSync) }
|
||||
func TestCanonicalSynchronisation68Light(t *testing.T) { testCanonSync(t, eth.ETH68, LightSync) }
|
||||
|
||||
func testCanonSync(t *testing.T, protocol uint, mode SyncMode) {
|
||||
success := make(chan struct{})
|
||||
|
|
@ -507,7 +503,6 @@ func testThrottling(t *testing.T, protocol uint, mode SyncMode) {
|
|||
// Tests that a canceled download wipes all previously accumulated state.
|
||||
func TestCancel68Full(t *testing.T) { testCancel(t, eth.ETH68, FullSync) }
|
||||
func TestCancel68Snap(t *testing.T) { testCancel(t, eth.ETH68, SnapSync) }
|
||||
func TestCancel68Light(t *testing.T) { testCancel(t, eth.ETH68, LightSync) }
|
||||
|
||||
func testCancel(t *testing.T, protocol uint, mode SyncMode) {
|
||||
complete := make(chan struct{})
|
||||
|
|
@ -540,7 +535,6 @@ func testCancel(t *testing.T, protocol uint, mode SyncMode) {
|
|||
// and not wreak havoc on other nodes in the network.
|
||||
func TestMultiProtoSynchronisation68Full(t *testing.T) { testMultiProtoSync(t, eth.ETH68, FullSync) }
|
||||
func TestMultiProtoSynchronisation68Snap(t *testing.T) { testMultiProtoSync(t, eth.ETH68, SnapSync) }
|
||||
func TestMultiProtoSynchronisation68Light(t *testing.T) { testMultiProtoSync(t, eth.ETH68, LightSync) }
|
||||
|
||||
func testMultiProtoSync(t *testing.T, protocol uint, mode SyncMode) {
|
||||
complete := make(chan struct{})
|
||||
|
|
@ -580,7 +574,6 @@ func testMultiProtoSync(t *testing.T, protocol uint, mode SyncMode) {
|
|||
// made, and instead the header should be assembled into a whole block in itself.
|
||||
func TestEmptyShortCircuit68Full(t *testing.T) { testEmptyShortCircuit(t, eth.ETH68, FullSync) }
|
||||
func TestEmptyShortCircuit68Snap(t *testing.T) { testEmptyShortCircuit(t, eth.ETH68, SnapSync) }
|
||||
func TestEmptyShortCircuit68Light(t *testing.T) { testEmptyShortCircuit(t, eth.ETH68, LightSync) }
|
||||
|
||||
func testEmptyShortCircuit(t *testing.T, protocol uint, mode SyncMode) {
|
||||
success := make(chan struct{})
|
||||
|
|
@ -619,7 +612,7 @@ func testEmptyShortCircuit(t *testing.T, protocol uint, mode SyncMode) {
|
|||
// Validate the number of block bodies that should have been requested
|
||||
bodiesNeeded, receiptsNeeded := 0, 0
|
||||
for _, block := range chain.blocks[1:] {
|
||||
if mode != LightSync && (len(block.Transactions()) > 0 || len(block.Uncles()) > 0) {
|
||||
if len(block.Transactions()) > 0 || len(block.Uncles()) > 0 {
|
||||
bodiesNeeded++
|
||||
}
|
||||
}
|
||||
|
|
@ -696,7 +689,6 @@ func testBeaconSync(t *testing.T, protocol uint, mode SyncMode) {
|
|||
// and highest block number) is tracked and updated correctly.
|
||||
func TestSyncProgress68Full(t *testing.T) { testSyncProgress(t, eth.ETH68, FullSync) }
|
||||
func TestSyncProgress68Snap(t *testing.T) { testSyncProgress(t, eth.ETH68, SnapSync) }
|
||||
func TestSyncProgress68Light(t *testing.T) { testSyncProgress(t, eth.ETH68, LightSync) }
|
||||
|
||||
func testSyncProgress(t *testing.T, protocol uint, mode SyncMode) {
|
||||
success := make(chan struct{})
|
||||
|
|
@ -734,17 +726,7 @@ func testSyncProgress(t *testing.T, protocol uint, mode SyncMode) {
|
|||
if err := tester.downloader.BeaconSync(mode, chain.blocks[len(chain.blocks)-1].Header(), nil); err != nil {
|
||||
t.Fatalf("failed to beacon-sync chain: %v", err)
|
||||
}
|
||||
var startingBlock uint64
|
||||
if mode == LightSync {
|
||||
// in light-sync mode:
|
||||
// * the starting block is 0 on the second sync cycle because blocks
|
||||
// are never downloaded.
|
||||
// * The current/highest blocks reported in the progress reflect the
|
||||
// current/highest header.
|
||||
startingBlock = 0
|
||||
} else {
|
||||
startingBlock = uint64(len(chain.blocks)/2 - 1)
|
||||
}
|
||||
startingBlock := uint64(len(chain.blocks)/2 - 1)
|
||||
|
||||
select {
|
||||
case <-success:
|
||||
|
|
|
|||
|
|
@ -25,11 +25,10 @@ type SyncMode uint32
|
|||
const (
|
||||
FullSync SyncMode = iota // Synchronise the entire blockchain history from full blocks
|
||||
SnapSync // Download the chain and the state via compact snapshots
|
||||
LightSync // Download only the headers and terminate afterwards
|
||||
)
|
||||
|
||||
func (mode SyncMode) IsValid() bool {
|
||||
return mode >= FullSync && mode <= LightSync
|
||||
return mode == FullSync || mode == SnapSync
|
||||
}
|
||||
|
||||
// String implements the stringer interface.
|
||||
|
|
@ -39,8 +38,6 @@ func (mode SyncMode) String() string {
|
|||
return "full"
|
||||
case SnapSync:
|
||||
return "snap"
|
||||
case LightSync:
|
||||
return "light"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
|
|
@ -52,8 +49,6 @@ func (mode SyncMode) MarshalText() ([]byte, error) {
|
|||
return []byte("full"), nil
|
||||
case SnapSync:
|
||||
return []byte("snap"), nil
|
||||
case LightSync:
|
||||
return []byte("light"), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown sync mode %d", mode)
|
||||
}
|
||||
|
|
@ -65,10 +60,8 @@ func (mode *SyncMode) UnmarshalText(text []byte) error {
|
|||
*mode = FullSync
|
||||
case "snap":
|
||||
*mode = SnapSync
|
||||
case "light":
|
||||
*mode = LightSync
|
||||
default:
|
||||
return fmt.Errorf(`unknown sync mode %q, want "full", "snap" or "light"`, text)
|
||||
return fmt.Errorf(`unknown sync mode %q, want "full" or "snap"`, text)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import (
|
|||
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/eth/protocols/eth"
|
||||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
|
|
@ -376,20 +377,9 @@ func TestSkeletonSyncInit(t *testing.T) {
|
|||
skeleton.Terminate()
|
||||
|
||||
// Ensure the correct resulting sync status
|
||||
var progress skeletonProgress
|
||||
json.Unmarshal(rawdb.ReadSkeletonSyncStatus(db), &progress)
|
||||
|
||||
if len(progress.Subchains) != len(tt.newstate) {
|
||||
t.Errorf("test %d: subchain count mismatch: have %d, want %d", i, len(progress.Subchains), len(tt.newstate))
|
||||
continue
|
||||
}
|
||||
for j := 0; j < len(progress.Subchains); j++ {
|
||||
if progress.Subchains[j].Head != tt.newstate[j].Head {
|
||||
t.Errorf("test %d: subchain %d head mismatch: have %d, want %d", i, j, progress.Subchains[j].Head, tt.newstate[j].Head)
|
||||
}
|
||||
if progress.Subchains[j].Tail != tt.newstate[j].Tail {
|
||||
t.Errorf("test %d: subchain %d tail mismatch: have %d, want %d", i, j, progress.Subchains[j].Tail, tt.newstate[j].Tail)
|
||||
}
|
||||
expect := skeletonExpect{state: tt.newstate}
|
||||
if err := checkSkeletonProgress(db, false, nil, expect); err != nil {
|
||||
t.Errorf("test %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -493,28 +483,36 @@ func TestSkeletonSyncExtend(t *testing.T) {
|
|||
skeleton.Terminate()
|
||||
|
||||
// Ensure the correct resulting sync status
|
||||
var progress skeletonProgress
|
||||
json.Unmarshal(rawdb.ReadSkeletonSyncStatus(db), &progress)
|
||||
expect := skeletonExpect{state: tt.newstate}
|
||||
if err := checkSkeletonProgress(db, false, nil, expect); err != nil {
|
||||
t.Errorf("test %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(progress.Subchains) != len(tt.newstate) {
|
||||
t.Errorf("test %d: subchain count mismatch: have %d, want %d", i, len(progress.Subchains), len(tt.newstate))
|
||||
continue
|
||||
}
|
||||
for j := 0; j < len(progress.Subchains); j++ {
|
||||
if progress.Subchains[j].Head != tt.newstate[j].Head {
|
||||
t.Errorf("test %d: subchain %d head mismatch: have %d, want %d", i, j, progress.Subchains[j].Head, tt.newstate[j].Head)
|
||||
}
|
||||
if progress.Subchains[j].Tail != tt.newstate[j].Tail {
|
||||
t.Errorf("test %d: subchain %d tail mismatch: have %d, want %d", i, j, progress.Subchains[j].Tail, tt.newstate[j].Tail)
|
||||
}
|
||||
}
|
||||
}
|
||||
type skeletonExpect struct {
|
||||
state []*subchain // Expected sync state after the post-init event
|
||||
serve uint64 // Expected number of header retrievals after initial cycle
|
||||
drop uint64 // Expected number of peers dropped after initial cycle
|
||||
}
|
||||
|
||||
type skeletonTest struct {
|
||||
fill bool // Whether to run a real backfiller in this test case
|
||||
unpredictable bool // Whether to ignore drops/serves due to uncertain packet assignments
|
||||
|
||||
head *types.Header // New head header to announce to reorg to
|
||||
peers []*skeletonTestPeer // Initial peer set to start the sync with
|
||||
mid skeletonExpect
|
||||
|
||||
newHead *types.Header // New header to anoint on top of the old one
|
||||
newPeer *skeletonTestPeer // New peer to join the skeleton syncer
|
||||
end skeletonExpect
|
||||
}
|
||||
|
||||
// Tests that the skeleton sync correctly retrieves headers from one or more
|
||||
// peers without duplicates or other strange side effects.
|
||||
func TestSkeletonSyncRetrievals(t *testing.T) {
|
||||
//log.Root().SetHandler(log.LvlFilterHandler(log.LvlTrace, log.StreamHandler(os.Stderr, log.TerminalFormat(true))))
|
||||
//log.SetDefault(log.NewLogger(log.NewGlogHandler(log.NewTerminalHandler(os.Stderr, false))))
|
||||
|
||||
// Since skeleton headers don't need to be meaningful, beyond a parent hash
|
||||
// progression, create a long fake chain to test with.
|
||||
|
|
@ -537,22 +535,7 @@ func TestSkeletonSyncRetrievals(t *testing.T) {
|
|||
Extra: []byte("B"), // force a different hash
|
||||
})
|
||||
}
|
||||
tests := []struct {
|
||||
fill bool // Whether to run a real backfiller in this test case
|
||||
unpredictable bool // Whether to ignore drops/serves due to uncertain packet assignments
|
||||
|
||||
head *types.Header // New head header to announce to reorg to
|
||||
peers []*skeletonTestPeer // Initial peer set to start the sync with
|
||||
midstate []*subchain // Expected sync state after initial cycle
|
||||
midserve uint64 // Expected number of header retrievals after initial cycle
|
||||
middrop uint64 // Expected number of peers dropped after initial cycle
|
||||
|
||||
newHead *types.Header // New header to anoint on top of the old one
|
||||
newPeer *skeletonTestPeer // New peer to join the skeleton syncer
|
||||
endstate []*subchain // Expected sync state after the post-init event
|
||||
endserve uint64 // Expected number of header retrievals after the post-init event
|
||||
enddrop uint64 // Expected number of peers dropped after the post-init event
|
||||
}{
|
||||
tests := []skeletonTest{
|
||||
// Completely empty database with only the genesis set. The sync is expected
|
||||
// to create a single subchain with the requested head. No peers however, so
|
||||
// the sync should be stuck without any progression.
|
||||
|
|
@ -561,11 +544,15 @@ func TestSkeletonSyncRetrievals(t *testing.T) {
|
|||
// to the genesis block.
|
||||
{
|
||||
head: chain[len(chain)-1],
|
||||
midstate: []*subchain{{Head: uint64(len(chain) - 1), Tail: uint64(len(chain) - 1)}},
|
||||
mid: skeletonExpect{
|
||||
state: []*subchain{{Head: uint64(len(chain) - 1), Tail: uint64(len(chain) - 1)}},
|
||||
},
|
||||
|
||||
newPeer: newSkeletonTestPeer("test-peer", chain),
|
||||
endstate: []*subchain{{Head: uint64(len(chain) - 1), Tail: 1}},
|
||||
endserve: uint64(len(chain) - 2), // len - head - genesis
|
||||
end: skeletonExpect{
|
||||
state: []*subchain{{Head: uint64(len(chain) - 1), Tail: 1}},
|
||||
serve: uint64(len(chain) - 2), // len - head - genesis
|
||||
},
|
||||
},
|
||||
// Completely empty database with only the genesis set. The sync is expected
|
||||
// to create a single subchain with the requested head. With one valid peer,
|
||||
|
|
@ -575,12 +562,16 @@ func TestSkeletonSyncRetrievals(t *testing.T) {
|
|||
{
|
||||
head: chain[len(chain)-1],
|
||||
peers: []*skeletonTestPeer{newSkeletonTestPeer("test-peer-1", chain)},
|
||||
midstate: []*subchain{{Head: uint64(len(chain) - 1), Tail: 1}},
|
||||
midserve: uint64(len(chain) - 2), // len - head - genesis
|
||||
mid: skeletonExpect{
|
||||
state: []*subchain{{Head: uint64(len(chain) - 1), Tail: 1}},
|
||||
serve: uint64(len(chain) - 2), // len - head - genesis
|
||||
},
|
||||
|
||||
newPeer: newSkeletonTestPeer("test-peer-2", chain),
|
||||
endstate: []*subchain{{Head: uint64(len(chain) - 1), Tail: 1}},
|
||||
endserve: uint64(len(chain) - 2), // len - head - genesis
|
||||
end: skeletonExpect{
|
||||
state: []*subchain{{Head: uint64(len(chain) - 1), Tail: 1}},
|
||||
serve: uint64(len(chain) - 2), // len - head - genesis
|
||||
},
|
||||
},
|
||||
// Completely empty database with only the genesis set. The sync is expected
|
||||
// to create a single subchain with the requested head. With many valid peers,
|
||||
|
|
@ -594,12 +585,16 @@ func TestSkeletonSyncRetrievals(t *testing.T) {
|
|||
newSkeletonTestPeer("test-peer-2", chain),
|
||||
newSkeletonTestPeer("test-peer-3", chain),
|
||||
},
|
||||
midstate: []*subchain{{Head: uint64(len(chain) - 1), Tail: 1}},
|
||||
midserve: uint64(len(chain) - 2), // len - head - genesis
|
||||
mid: skeletonExpect{
|
||||
state: []*subchain{{Head: uint64(len(chain) - 1), Tail: 1}},
|
||||
serve: uint64(len(chain) - 2), // len - head - genesis
|
||||
},
|
||||
|
||||
newPeer: newSkeletonTestPeer("test-peer-4", chain),
|
||||
endstate: []*subchain{{Head: uint64(len(chain) - 1), Tail: 1}},
|
||||
endserve: uint64(len(chain) - 2), // len - head - genesis
|
||||
end: skeletonExpect{
|
||||
state: []*subchain{{Head: uint64(len(chain) - 1), Tail: 1}},
|
||||
serve: uint64(len(chain) - 2), // len - head - genesis
|
||||
},
|
||||
},
|
||||
// This test checks if a peer tries to withhold a header - *on* the sync
|
||||
// boundary - instead of sending the requested amount. The malicious short
|
||||
|
|
@ -611,14 +606,18 @@ func TestSkeletonSyncRetrievals(t *testing.T) {
|
|||
peers: []*skeletonTestPeer{
|
||||
newSkeletonTestPeer("header-skipper", append(append(append([]*types.Header{}, chain[:99]...), nil), chain[100:]...)),
|
||||
},
|
||||
midstate: []*subchain{{Head: requestHeaders + 100, Tail: 100}},
|
||||
midserve: requestHeaders + 101 - 3, // len - head - genesis - missing
|
||||
middrop: 1, // penalize shortened header deliveries
|
||||
mid: skeletonExpect{
|
||||
state: []*subchain{{Head: requestHeaders + 100, Tail: 100}},
|
||||
serve: requestHeaders + 101 - 3, // len - head - genesis - missing
|
||||
drop: 1, // penalize shortened header deliveries
|
||||
},
|
||||
|
||||
newPeer: newSkeletonTestPeer("good-peer", chain),
|
||||
endstate: []*subchain{{Head: requestHeaders + 100, Tail: 1}},
|
||||
endserve: (requestHeaders + 101 - 3) + (100 - 1), // midserve + lenrest - genesis
|
||||
enddrop: 1, // no new drops
|
||||
end: skeletonExpect{
|
||||
state: []*subchain{{Head: requestHeaders + 100, Tail: 1}},
|
||||
serve: (requestHeaders + 101 - 3) + (100 - 1), // midserve + lenrest - genesis
|
||||
drop: 1, // no new drops
|
||||
},
|
||||
},
|
||||
// This test checks if a peer tries to withhold a header - *off* the sync
|
||||
// boundary - instead of sending the requested amount. The malicious short
|
||||
|
|
@ -630,14 +629,18 @@ func TestSkeletonSyncRetrievals(t *testing.T) {
|
|||
peers: []*skeletonTestPeer{
|
||||
newSkeletonTestPeer("header-skipper", append(append(append([]*types.Header{}, chain[:50]...), nil), chain[51:]...)),
|
||||
},
|
||||
midstate: []*subchain{{Head: requestHeaders + 100, Tail: 100}},
|
||||
midserve: requestHeaders + 101 - 3, // len - head - genesis - missing
|
||||
middrop: 1, // penalize shortened header deliveries
|
||||
mid: skeletonExpect{
|
||||
state: []*subchain{{Head: requestHeaders + 100, Tail: 100}},
|
||||
serve: requestHeaders + 101 - 3, // len - head - genesis - missing
|
||||
drop: 1, // penalize shortened header deliveries
|
||||
},
|
||||
|
||||
newPeer: newSkeletonTestPeer("good-peer", chain),
|
||||
endstate: []*subchain{{Head: requestHeaders + 100, Tail: 1}},
|
||||
endserve: (requestHeaders + 101 - 3) + (100 - 1), // midserve + lenrest - genesis
|
||||
enddrop: 1, // no new drops
|
||||
end: skeletonExpect{
|
||||
state: []*subchain{{Head: requestHeaders + 100, Tail: 1}},
|
||||
serve: (requestHeaders + 101 - 3) + (100 - 1), // midserve + lenrest - genesis
|
||||
drop: 1, // no new drops
|
||||
},
|
||||
},
|
||||
// This test checks if a peer tries to duplicate a header - *on* the sync
|
||||
// boundary - instead of sending the correct sequence. The malicious duped
|
||||
|
|
@ -649,14 +652,18 @@ func TestSkeletonSyncRetrievals(t *testing.T) {
|
|||
peers: []*skeletonTestPeer{
|
||||
newSkeletonTestPeer("header-duper", append(append(append([]*types.Header{}, chain[:99]...), chain[98]), chain[100:]...)),
|
||||
},
|
||||
midstate: []*subchain{{Head: requestHeaders + 100, Tail: 100}},
|
||||
midserve: requestHeaders + 101 - 2, // len - head - genesis
|
||||
middrop: 1, // penalize invalid header sequences
|
||||
mid: skeletonExpect{
|
||||
state: []*subchain{{Head: requestHeaders + 100, Tail: 100}},
|
||||
serve: requestHeaders + 101 - 2, // len - head - genesis
|
||||
drop: 1, // penalize invalid header sequences
|
||||
},
|
||||
|
||||
newPeer: newSkeletonTestPeer("good-peer", chain),
|
||||
endstate: []*subchain{{Head: requestHeaders + 100, Tail: 1}},
|
||||
endserve: (requestHeaders + 101 - 2) + (100 - 1), // midserve + lenrest - genesis
|
||||
enddrop: 1, // no new drops
|
||||
end: skeletonExpect{
|
||||
state: []*subchain{{Head: requestHeaders + 100, Tail: 1}},
|
||||
serve: (requestHeaders + 101 - 2) + (100 - 1), // midserve + lenrest - genesis
|
||||
drop: 1, // no new drops
|
||||
},
|
||||
},
|
||||
// This test checks if a peer tries to duplicate a header - *off* the sync
|
||||
// boundary - instead of sending the correct sequence. The malicious duped
|
||||
|
|
@ -668,14 +675,18 @@ func TestSkeletonSyncRetrievals(t *testing.T) {
|
|||
peers: []*skeletonTestPeer{
|
||||
newSkeletonTestPeer("header-duper", append(append(append([]*types.Header{}, chain[:50]...), chain[49]), chain[51:]...)),
|
||||
},
|
||||
midstate: []*subchain{{Head: requestHeaders + 100, Tail: 100}},
|
||||
midserve: requestHeaders + 101 - 2, // len - head - genesis
|
||||
middrop: 1, // penalize invalid header sequences
|
||||
mid: skeletonExpect{
|
||||
state: []*subchain{{Head: requestHeaders + 100, Tail: 100}},
|
||||
serve: requestHeaders + 101 - 2, // len - head - genesis
|
||||
drop: 1, // penalize invalid header sequences
|
||||
},
|
||||
|
||||
newPeer: newSkeletonTestPeer("good-peer", chain),
|
||||
endstate: []*subchain{{Head: requestHeaders + 100, Tail: 1}},
|
||||
endserve: (requestHeaders + 101 - 2) + (100 - 1), // midserve + lenrest - genesis
|
||||
enddrop: 1, // no new drops
|
||||
end: skeletonExpect{
|
||||
state: []*subchain{{Head: requestHeaders + 100, Tail: 1}},
|
||||
serve: (requestHeaders + 101 - 2) + (100 - 1), // midserve + lenrest - genesis
|
||||
drop: 1, // no new drops
|
||||
},
|
||||
},
|
||||
// This test checks if a peer tries to inject a different header - *on*
|
||||
// the sync boundary - instead of sending the correct sequence. The bad
|
||||
|
|
@ -698,14 +709,18 @@ func TestSkeletonSyncRetrievals(t *testing.T) {
|
|||
),
|
||||
),
|
||||
},
|
||||
midstate: []*subchain{{Head: requestHeaders + 100, Tail: 100}},
|
||||
midserve: requestHeaders + 101 - 2, // len - head - genesis
|
||||
middrop: 1, // different set of headers, drop // TODO(karalabe): maybe just diff sync?
|
||||
mid: skeletonExpect{
|
||||
state: []*subchain{{Head: requestHeaders + 100, Tail: 100}},
|
||||
serve: requestHeaders + 101 - 2, // len - head - genesis
|
||||
drop: 1, // different set of headers, drop // TODO(karalabe): maybe just diff sync?
|
||||
},
|
||||
|
||||
newPeer: newSkeletonTestPeer("good-peer", chain),
|
||||
endstate: []*subchain{{Head: requestHeaders + 100, Tail: 1}},
|
||||
endserve: (requestHeaders + 101 - 2) + (100 - 1), // midserve + lenrest - genesis
|
||||
enddrop: 1, // no new drops
|
||||
end: skeletonExpect{
|
||||
state: []*subchain{{Head: requestHeaders + 100, Tail: 1}},
|
||||
serve: (requestHeaders + 101 - 2) + (100 - 1), // midserve + lenrest - genesis
|
||||
drop: 1, // no new drops
|
||||
},
|
||||
},
|
||||
// This test checks if a peer tries to inject a different header - *off*
|
||||
// the sync boundary - instead of sending the correct sequence. The bad
|
||||
|
|
@ -728,14 +743,18 @@ func TestSkeletonSyncRetrievals(t *testing.T) {
|
|||
),
|
||||
),
|
||||
},
|
||||
midstate: []*subchain{{Head: requestHeaders + 100, Tail: 100}},
|
||||
midserve: requestHeaders + 101 - 2, // len - head - genesis
|
||||
middrop: 1, // different set of headers, drop
|
||||
mid: skeletonExpect{
|
||||
state: []*subchain{{Head: requestHeaders + 100, Tail: 100}},
|
||||
serve: requestHeaders + 101 - 2, // len - head - genesis
|
||||
drop: 1, // different set of headers, drop
|
||||
},
|
||||
|
||||
newPeer: newSkeletonTestPeer("good-peer", chain),
|
||||
endstate: []*subchain{{Head: requestHeaders + 100, Tail: 1}},
|
||||
endserve: (requestHeaders + 101 - 2) + (100 - 1), // midserve + lenrest - genesis
|
||||
enddrop: 1, // no new drops
|
||||
end: skeletonExpect{
|
||||
state: []*subchain{{Head: requestHeaders + 100, Tail: 1}},
|
||||
serve: (requestHeaders + 101 - 2) + (100 - 1), // midserve + lenrest - genesis
|
||||
drop: 1, // no new drops
|
||||
},
|
||||
},
|
||||
// This test reproduces a bug caught during review (kudos to @holiman)
|
||||
// where a subchain is merged with a previously interrupted one, causing
|
||||
|
|
@ -765,12 +784,16 @@ func TestSkeletonSyncRetrievals(t *testing.T) {
|
|||
return nil // Fallback to default behavior, just delayed
|
||||
}),
|
||||
},
|
||||
midstate: []*subchain{{Head: 2 * requestHeaders, Tail: 1}},
|
||||
midserve: 2*requestHeaders - 1, // len - head - genesis
|
||||
mid: skeletonExpect{
|
||||
state: []*subchain{{Head: 2 * requestHeaders, Tail: 1}},
|
||||
serve: 2*requestHeaders - 1, // len - head - genesis
|
||||
},
|
||||
|
||||
newHead: chain[2*requestHeaders+2],
|
||||
endstate: []*subchain{{Head: 2*requestHeaders + 2, Tail: 1}},
|
||||
endserve: 4 * requestHeaders,
|
||||
end: skeletonExpect{
|
||||
state: []*subchain{{Head: 2*requestHeaders + 2, Tail: 1}},
|
||||
serve: 4 * requestHeaders,
|
||||
},
|
||||
},
|
||||
// This test reproduces a bug caught by (@rjl493456442) where a skeleton
|
||||
// header goes missing, causing the sync to get stuck and/or panic.
|
||||
|
|
@ -794,11 +817,15 @@ func TestSkeletonSyncRetrievals(t *testing.T) {
|
|||
|
||||
head: chain[len(chain)/2+1], // Sync up until the sidechain common ancestor + 2
|
||||
peers: []*skeletonTestPeer{newSkeletonTestPeer("test-peer-oldchain", chain)},
|
||||
midstate: []*subchain{{Head: uint64(len(chain)/2 + 1), Tail: 1}},
|
||||
mid: skeletonExpect{
|
||||
state: []*subchain{{Head: uint64(len(chain)/2 + 1), Tail: 1}},
|
||||
},
|
||||
|
||||
newHead: sidechain[len(sidechain)/2+3], // Sync up until the sidechain common ancestor + 4
|
||||
newPeer: newSkeletonTestPeer("test-peer-newchain", sidechain),
|
||||
endstate: []*subchain{{Head: uint64(len(sidechain)/2 + 3), Tail: uint64(len(chain) / 2)}},
|
||||
end: skeletonExpect{
|
||||
state: []*subchain{{Head: uint64(len(sidechain)/2 + 3), Tail: uint64(len(chain) / 2)}},
|
||||
},
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
|
|
@ -861,115 +888,83 @@ func TestSkeletonSyncRetrievals(t *testing.T) {
|
|||
skeleton := newSkeleton(db, peerset, drop, filler)
|
||||
skeleton.Sync(tt.head, nil, true)
|
||||
|
||||
var progress skeletonProgress
|
||||
// Wait a bit (bleah) for the initial sync loop to go to idle. This might
|
||||
// be either a finish or a never-start hence why there's no event to hook.
|
||||
check := func() error {
|
||||
if len(progress.Subchains) != len(tt.midstate) {
|
||||
return fmt.Errorf("test %d, mid state: subchain count mismatch: have %d, want %d", i, len(progress.Subchains), len(tt.midstate))
|
||||
}
|
||||
for j := 0; j < len(progress.Subchains); j++ {
|
||||
if progress.Subchains[j].Head != tt.midstate[j].Head {
|
||||
return fmt.Errorf("test %d, mid state: subchain %d head mismatch: have %d, want %d", i, j, progress.Subchains[j].Head, tt.midstate[j].Head)
|
||||
}
|
||||
if progress.Subchains[j].Tail != tt.midstate[j].Tail {
|
||||
return fmt.Errorf("test %d, mid state: subchain %d tail mismatch: have %d, want %d", i, j, progress.Subchains[j].Tail, tt.midstate[j].Tail)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
waitStart := time.Now()
|
||||
for waitTime := 20 * time.Millisecond; time.Since(waitStart) < 2*time.Second; waitTime = waitTime * 2 {
|
||||
time.Sleep(waitTime)
|
||||
// Check the post-init end state if it matches the required results
|
||||
json.Unmarshal(rawdb.ReadSkeletonSyncStatus(db), &progress)
|
||||
if err := check(); err == nil {
|
||||
if err := checkSkeletonProgress(db, tt.unpredictable, tt.peers, tt.mid); err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err := check(); err != nil {
|
||||
t.Error(err)
|
||||
if err := checkSkeletonProgress(db, tt.unpredictable, tt.peers, tt.mid); err != nil {
|
||||
t.Errorf("test %d, mid: %v", i, err)
|
||||
continue
|
||||
}
|
||||
if !tt.unpredictable {
|
||||
var served uint64
|
||||
for _, peer := range tt.peers {
|
||||
served += peer.served.Load()
|
||||
}
|
||||
if served != tt.midserve {
|
||||
t.Errorf("test %d, mid state: served headers mismatch: have %d, want %d", i, served, tt.midserve)
|
||||
}
|
||||
var drops uint64
|
||||
for _, peer := range tt.peers {
|
||||
drops += peer.dropped.Load()
|
||||
}
|
||||
if drops != tt.middrop {
|
||||
t.Errorf("test %d, mid state: dropped peers mismatch: have %d, want %d", i, drops, tt.middrop)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the post-init events if there's any
|
||||
if tt.newHead != nil {
|
||||
skeleton.Sync(tt.newHead, nil, true)
|
||||
}
|
||||
endpeers := tt.peers
|
||||
if tt.newPeer != nil {
|
||||
if err := peerset.Register(newPeerConnection(tt.newPeer.id, eth.ETH68, tt.newPeer, log.New("id", tt.newPeer.id))); err != nil {
|
||||
t.Errorf("test %d: failed to register new peer: %v", i, err)
|
||||
}
|
||||
time.Sleep(time.Millisecond * 50) // given time for peer registration
|
||||
endpeers = append(tt.peers, tt.newPeer)
|
||||
}
|
||||
if tt.newHead != nil {
|
||||
skeleton.Sync(tt.newHead, nil, true)
|
||||
}
|
||||
|
||||
// Wait a bit (bleah) for the second sync loop to go to idle. This might
|
||||
// be either a finish or a never-start hence why there's no event to hook.
|
||||
check = func() error {
|
||||
if len(progress.Subchains) != len(tt.endstate) {
|
||||
return fmt.Errorf("test %d, end state: subchain count mismatch: have %d, want %d", i, len(progress.Subchains), len(tt.endstate))
|
||||
}
|
||||
for j := 0; j < len(progress.Subchains); j++ {
|
||||
if progress.Subchains[j].Head != tt.endstate[j].Head {
|
||||
return fmt.Errorf("test %d, end state: subchain %d head mismatch: have %d, want %d", i, j, progress.Subchains[j].Head, tt.endstate[j].Head)
|
||||
}
|
||||
if progress.Subchains[j].Tail != tt.endstate[j].Tail {
|
||||
return fmt.Errorf("test %d, end state: subchain %d tail mismatch: have %d, want %d", i, j, progress.Subchains[j].Tail, tt.endstate[j].Tail)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
waitStart = time.Now()
|
||||
for waitTime := 20 * time.Millisecond; time.Since(waitStart) < 2*time.Second; waitTime = waitTime * 2 {
|
||||
time.Sleep(waitTime)
|
||||
// Check the post-init end state if it matches the required results
|
||||
json.Unmarshal(rawdb.ReadSkeletonSyncStatus(db), &progress)
|
||||
if err := check(); err == nil {
|
||||
if err := checkSkeletonProgress(db, tt.unpredictable, endpeers, tt.end); err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err := check(); err != nil {
|
||||
t.Error(err)
|
||||
if err := checkSkeletonProgress(db, tt.unpredictable, endpeers, tt.end); err != nil {
|
||||
t.Errorf("test %d, end: %v", i, err)
|
||||
continue
|
||||
}
|
||||
// Check that the peers served no more headers than we actually needed
|
||||
if !tt.unpredictable {
|
||||
served := uint64(0)
|
||||
for _, peer := range tt.peers {
|
||||
served += peer.served.Load()
|
||||
}
|
||||
if tt.newPeer != nil {
|
||||
served += tt.newPeer.served.Load()
|
||||
}
|
||||
if served != tt.endserve {
|
||||
t.Errorf("test %d, end state: served headers mismatch: have %d, want %d", i, served, tt.endserve)
|
||||
}
|
||||
drops := uint64(0)
|
||||
for _, peer := range tt.peers {
|
||||
drops += peer.dropped.Load()
|
||||
}
|
||||
if tt.newPeer != nil {
|
||||
drops += tt.newPeer.dropped.Load()
|
||||
}
|
||||
if drops != tt.enddrop {
|
||||
t.Errorf("test %d, end state: dropped peers mismatch: have %d, want %d", i, drops, tt.middrop)
|
||||
}
|
||||
}
|
||||
// Clean up any leftover skeleton sync resources
|
||||
skeleton.Terminate()
|
||||
}
|
||||
}
|
||||
|
||||
func checkSkeletonProgress(db ethdb.KeyValueReader, unpredictable bool, peers []*skeletonTestPeer, expected skeletonExpect) error {
|
||||
var progress skeletonProgress
|
||||
// Check the post-init end state if it matches the required results
|
||||
json.Unmarshal(rawdb.ReadSkeletonSyncStatus(db), &progress)
|
||||
|
||||
if len(progress.Subchains) != len(expected.state) {
|
||||
return fmt.Errorf("subchain count mismatch: have %d, want %d", len(progress.Subchains), len(expected.state))
|
||||
}
|
||||
for j := 0; j < len(progress.Subchains); j++ {
|
||||
if progress.Subchains[j].Head != expected.state[j].Head {
|
||||
return fmt.Errorf("subchain %d head mismatch: have %d, want %d", j, progress.Subchains[j].Head, expected.state[j].Head)
|
||||
}
|
||||
if progress.Subchains[j].Tail != expected.state[j].Tail {
|
||||
return fmt.Errorf("subchain %d tail mismatch: have %d, want %d", j, progress.Subchains[j].Tail, expected.state[j].Tail)
|
||||
}
|
||||
}
|
||||
if !unpredictable {
|
||||
var served uint64
|
||||
for _, peer := range peers {
|
||||
served += peer.served.Load()
|
||||
}
|
||||
if served != expected.serve {
|
||||
return fmt.Errorf("served headers mismatch: have %d, want %d", served, expected.serve)
|
||||
}
|
||||
var drops uint64
|
||||
for _, peer := range peers {
|
||||
drops += peer.dropped.Load()
|
||||
}
|
||||
if drops != expected.drop {
|
||||
return fmt.Errorf("dropped peers mismatch: have %d, want %d", drops, expected.drop)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -180,7 +180,7 @@ func newHandler(config *handlerConfig) (*handler, error) {
|
|||
return nil, errors.New("snap sync not supported with snapshots disabled")
|
||||
}
|
||||
// Construct the downloader (long sync)
|
||||
h.downloader = downloader.New(config.Database, h.eventMux, h.chain, nil, h.removePeer, h.enableSyncedFeatures)
|
||||
h.downloader = downloader.New(config.Database, h.eventMux, h.chain, h.removePeer, h.enableSyncedFeatures)
|
||||
|
||||
fetchTx := func(peer string, hashes []common.Hash) error {
|
||||
p := h.peers.peer(peer)
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ func makeLegacyProgress() legacyProgress {
|
|||
Next: common.Hash{},
|
||||
Last: common.Hash{0x77},
|
||||
SubTasks: map[common.Hash][]*legacyStorageTask{
|
||||
common.Hash{0x1}: {
|
||||
{0x1}: {
|
||||
{
|
||||
Next: common.Hash{},
|
||||
Last: common.Hash{0xff},
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ var (
|
|||
// to allow concurrent retrievals.
|
||||
accountConcurrency = 16
|
||||
|
||||
// storageConcurrency is the number of chunks to split the a large contract
|
||||
// storageConcurrency is the number of chunks to split a large contract
|
||||
// storage trie into to allow concurrent retrievals.
|
||||
storageConcurrency = 16
|
||||
)
|
||||
|
|
@ -2358,7 +2358,7 @@ func (s *Syncer) commitHealer(force bool) {
|
|||
}
|
||||
batch := s.db.NewBatch()
|
||||
if err := s.healer.scheduler.Commit(batch); err != nil {
|
||||
log.Error("Failed to commit healing data", "err", err)
|
||||
log.Crit("Failed to commit healing data", "err", err)
|
||||
}
|
||||
if err := batch.Write(); err != nil {
|
||||
log.Crit("Failed to persist healing data", "err", err)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
|
@ -805,9 +804,13 @@ func (api *API) standardTraceBlockToFile(ctx context.Context, block *types.Block
|
|||
// Execute the transaction and flush any traces to disk
|
||||
vmenv := vm.NewEVM(vmctx, txContext, statedb, chainConfig, vmConf)
|
||||
statedb.SetTxContext(tx.Hash(), i)
|
||||
if vmConf.Tracer.OnTxStart != nil {
|
||||
vmConf.Tracer.OnTxStart(vmenv.GetVMContext(), tx, msg.From)
|
||||
}
|
||||
vmRet, err := core.ApplyMessage(vmenv, msg, new(core.GasPool).AddGas(msg.GasLimit))
|
||||
if vmConf.Tracer.OnTxEnd != nil {
|
||||
vmConf.Tracer.OnTxEnd(&types.Receipt{GasUsed: vmRet.UsedGas}, err)
|
||||
}
|
||||
if writer != nil {
|
||||
writer.Flush()
|
||||
}
|
||||
|
|
@ -982,7 +985,8 @@ func (api *API) traceTx(ctx context.Context, tx *types.Transaction, message *cor
|
|||
return nil, err
|
||||
}
|
||||
}
|
||||
vmenv := vm.NewEVM(vmctx, vm.TxContext{GasPrice: big.NewInt(0)}, statedb, api.backend.ChainConfig(), vm.Config{Tracer: tracer.Hooks, NoBaseFee: true})
|
||||
// The actual TxContext will be created as part of ApplyTransactionWithEVM.
|
||||
vmenv := vm.NewEVM(vmctx, vm.TxContext{GasPrice: message.GasPrice, BlobFeeCap: message.BlobGasFeeCap}, statedb, api.backend.ChainConfig(), vm.Config{Tracer: tracer.Hooks, NoBaseFee: true})
|
||||
statedb.SetLogger(tracer.Hooks)
|
||||
|
||||
// Define a meaningful timeout of a single transaction trace
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import (
|
|||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"github.com/ethereum/go-ethereum/consensus"
|
||||
"github.com/ethereum/go-ethereum/consensus/beacon"
|
||||
"github.com/ethereum/go-ethereum/consensus/ethash"
|
||||
"github.com/ethereum/go-ethereum/core"
|
||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||
|
|
@ -994,3 +995,90 @@ func TestTraceChain(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// newTestMergedBackend creates a post-merge chain
|
||||
func newTestMergedBackend(t *testing.T, n int, gspec *core.Genesis, generator func(i int, b *core.BlockGen)) *testBackend {
|
||||
backend := &testBackend{
|
||||
chainConfig: gspec.Config,
|
||||
engine: beacon.NewFaker(),
|
||||
chaindb: rawdb.NewMemoryDatabase(),
|
||||
}
|
||||
// Generate blocks for testing
|
||||
_, blocks, _ := core.GenerateChainWithGenesis(gspec, backend.engine, n, generator)
|
||||
|
||||
// Import the canonical chain
|
||||
cacheConfig := &core.CacheConfig{
|
||||
TrieCleanLimit: 256,
|
||||
TrieDirtyLimit: 256,
|
||||
TrieTimeLimit: 5 * time.Minute,
|
||||
SnapshotLimit: 0,
|
||||
TrieDirtyDisabled: true, // Archive mode
|
||||
}
|
||||
chain, err := core.NewBlockChain(backend.chaindb, cacheConfig, gspec, nil, backend.engine, vm.Config{}, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create tester chain: %v", err)
|
||||
}
|
||||
if n, err := chain.InsertChain(blocks); err != nil {
|
||||
t.Fatalf("block %d: failed to insert into chain: %v", n, err)
|
||||
}
|
||||
backend.chain = chain
|
||||
return backend
|
||||
}
|
||||
|
||||
func TestTraceBlockWithBasefee(t *testing.T) {
|
||||
t.Parallel()
|
||||
accounts := newAccounts(1)
|
||||
target := common.HexToAddress("0x1111111111111111111111111111111111111111")
|
||||
genesis := &core.Genesis{
|
||||
Config: params.AllDevChainProtocolChanges,
|
||||
Alloc: types.GenesisAlloc{
|
||||
accounts[0].addr: {Balance: big.NewInt(1 * params.Ether)},
|
||||
target: {Nonce: 1, Code: []byte{
|
||||
byte(vm.BASEFEE), byte(vm.STOP),
|
||||
}},
|
||||
},
|
||||
}
|
||||
genBlocks := 1
|
||||
signer := types.HomesteadSigner{}
|
||||
var txHash common.Hash
|
||||
var baseFee = new(big.Int)
|
||||
backend := newTestMergedBackend(t, genBlocks, genesis, func(i int, b *core.BlockGen) {
|
||||
tx, _ := types.SignTx(types.NewTx(&types.LegacyTx{
|
||||
Nonce: uint64(i),
|
||||
To: &target,
|
||||
Value: big.NewInt(0),
|
||||
Gas: 5 * params.TxGas,
|
||||
GasPrice: b.BaseFee(),
|
||||
Data: nil}),
|
||||
signer, accounts[0].key)
|
||||
b.AddTx(tx)
|
||||
txHash = tx.Hash()
|
||||
baseFee.Set(b.BaseFee())
|
||||
})
|
||||
defer backend.chain.Stop()
|
||||
api := NewAPI(backend)
|
||||
|
||||
var testSuite = []struct {
|
||||
blockNumber rpc.BlockNumber
|
||||
config *TraceConfig
|
||||
want string
|
||||
}{
|
||||
// Trace head block
|
||||
{
|
||||
blockNumber: rpc.BlockNumber(genBlocks),
|
||||
want: fmt.Sprintf(`[{"txHash":"%#x","result":{"gas":21002,"failed":false,"returnValue":"","structLogs":[{"pc":0,"op":"BASEFEE","gas":84000,"gasCost":2,"depth":1,"stack":[]},{"pc":1,"op":"STOP","gas":83998,"gasCost":0,"depth":1,"stack":["%#x"]}]}}]`, txHash, baseFee),
|
||||
},
|
||||
}
|
||||
for i, tc := range testSuite {
|
||||
result, err := api.TraceBlockByNumber(context.Background(), tc.blockNumber, tc.config)
|
||||
if err != nil {
|
||||
t.Errorf("test %d, want no error, have %v", i, err)
|
||||
continue
|
||||
}
|
||||
have, _ := json.Marshal(result)
|
||||
want := tc.want
|
||||
if string(have) != want {
|
||||
t.Errorf("test %d, result mismatch\nhave: %v\nwant: %v\n", i, string(have), want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -58,6 +58,7 @@ type jsonLogger struct {
|
|||
encoder *json.Encoder
|
||||
cfg *Config
|
||||
env *tracing.VMContext
|
||||
hooks *tracing.Hooks
|
||||
}
|
||||
|
||||
// NewJSONLogger creates a new EVM tracer that prints execution steps as JSON objects
|
||||
|
|
@ -67,12 +68,14 @@ func NewJSONLogger(cfg *Config, writer io.Writer) *tracing.Hooks {
|
|||
if l.cfg == nil {
|
||||
l.cfg = &Config{}
|
||||
}
|
||||
return &tracing.Hooks{
|
||||
l.hooks = &tracing.Hooks{
|
||||
OnTxStart: l.OnTxStart,
|
||||
OnExit: l.OnExit,
|
||||
OnSystemCallStart: l.onSystemCallStart,
|
||||
OnExit: l.OnEnd,
|
||||
OnOpcode: l.OnOpcode,
|
||||
OnFault: l.OnFault,
|
||||
}
|
||||
return l.hooks
|
||||
}
|
||||
|
||||
// NewJSONLoggerWithCallFrames creates a new EVM tracer that prints execution steps as JSON objects
|
||||
|
|
@ -82,13 +85,15 @@ func NewJSONLoggerWithCallFrames(cfg *Config, writer io.Writer) *tracing.Hooks {
|
|||
if l.cfg == nil {
|
||||
l.cfg = &Config{}
|
||||
}
|
||||
return &tracing.Hooks{
|
||||
l.hooks = &tracing.Hooks{
|
||||
OnTxStart: l.OnTxStart,
|
||||
OnSystemCallStart: l.onSystemCallStart,
|
||||
OnEnter: l.OnEnter,
|
||||
OnExit: l.OnExit,
|
||||
OnOpcode: l.OnOpcode,
|
||||
OnFault: l.OnFault,
|
||||
}
|
||||
return l.hooks
|
||||
}
|
||||
|
||||
func (l *jsonLogger) OnFault(pc uint64, op byte, gas uint64, cost uint64, scope tracing.OpContext, depth int, err error) {
|
||||
|
|
@ -122,6 +127,16 @@ func (l *jsonLogger) OnOpcode(pc uint64, op byte, gas, cost uint64, scope tracin
|
|||
l.encoder.Encode(log)
|
||||
}
|
||||
|
||||
func (l *jsonLogger) onSystemCallStart() {
|
||||
// Process no events while in system call.
|
||||
hooks := *l.hooks
|
||||
*l.hooks = tracing.Hooks{
|
||||
OnSystemCallEnd: func() {
|
||||
*l.hooks = hooks
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// OnEnter is not enabled by default.
|
||||
func (l *jsonLogger) OnEnter(depth int, typ byte, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) {
|
||||
frame := callFrame{
|
||||
|
|
|
|||
|
|
@ -74,6 +74,11 @@ func (f callFrame) failed() bool {
|
|||
|
||||
func (f *callFrame) processOutput(output []byte, err error, reverted bool) {
|
||||
output = common.CopyBytes(output)
|
||||
// Clear error if tx wasn't reverted. This happened
|
||||
// for pre-homestead contract storage OOG.
|
||||
if err != nil && !reverted {
|
||||
err = nil
|
||||
}
|
||||
if err == nil {
|
||||
f.Output = output
|
||||
return
|
||||
|
|
|
|||
|
|
@ -400,7 +400,7 @@ func (b *batch) Put(key, value []byte) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Delete inserts the a key removal into the batch for later committing.
|
||||
// Delete inserts the key removal into the batch for later committing.
|
||||
func (b *batch) Delete(key []byte) error {
|
||||
b.b.Delete(key)
|
||||
b.size += len(key)
|
||||
|
|
|
|||
|
|
@ -227,7 +227,7 @@ func (b *batch) Put(key, value []byte) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Delete inserts the a key removal into the batch for later committing.
|
||||
// Delete inserts the key removal into the batch for later committing.
|
||||
func (b *batch) Delete(key []byte) error {
|
||||
b.writes = append(b.writes, keyvalue{string(key), nil, true})
|
||||
b.size += len(key)
|
||||
|
|
|
|||
|
|
@ -575,7 +575,7 @@ func (b *batch) Put(key, value []byte) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Delete inserts the a key removal into the batch for later committing.
|
||||
// Delete inserts the key removal into the batch for later committing.
|
||||
func (b *batch) Delete(key []byte) error {
|
||||
b.b.Delete(key, nil)
|
||||
b.size += len(key)
|
||||
|
|
|
|||
16
go.mod
16
go.mod
|
|
@ -4,8 +4,8 @@ go 1.21
|
|||
|
||||
require (
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.2.0
|
||||
github.com/Microsoft/go-winio v0.6.1
|
||||
github.com/VictoriaMetrics/fastcache v1.12.1
|
||||
github.com/Microsoft/go-winio v0.6.2
|
||||
github.com/VictoriaMetrics/fastcache v1.12.2
|
||||
github.com/aws/aws-sdk-go-v2 v1.21.2
|
||||
github.com/aws/aws-sdk-go-v2/config v1.18.45
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.43
|
||||
|
|
@ -18,12 +18,12 @@ require (
|
|||
github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c
|
||||
github.com/crate-crypto/go-kzg-4844 v1.0.0
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/deckarep/golang-set/v2 v2.1.0
|
||||
github.com/deckarep/golang-set/v2 v2.6.0
|
||||
github.com/donovanhide/eventsource v0.0.0-20210830082556-c59027999da0
|
||||
github.com/dop251/goja v0.0.0-20230605162241-28ee0ee714f3
|
||||
github.com/ethereum/c-kzg-4844 v1.0.0
|
||||
github.com/ethereum/go-verkle v0.1.1-0.20240306133620-7d920df305f0
|
||||
github.com/fatih/color v1.13.0
|
||||
github.com/fatih/color v1.16.0
|
||||
github.com/ferranbt/fastssz v0.1.2
|
||||
github.com/fjl/gencodec v0.0.0-20230517082657-f9840df7b83e
|
||||
github.com/fjl/memsize v0.0.2
|
||||
|
|
@ -51,7 +51,7 @@ require (
|
|||
github.com/kilic/bls12-381 v0.1.0
|
||||
github.com/kylelemons/godebug v1.1.0
|
||||
github.com/mattn/go-colorable v0.1.13
|
||||
github.com/mattn/go-isatty v0.0.17
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416
|
||||
github.com/olekukonko/tablewriter v0.0.5
|
||||
github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7
|
||||
|
|
@ -69,11 +69,11 @@ require (
|
|||
go.uber.org/automaxprocs v1.5.2
|
||||
golang.org/x/crypto v0.22.0
|
||||
golang.org/x/sync v0.7.0
|
||||
golang.org/x/sys v0.19.0
|
||||
golang.org/x/sys v0.20.0
|
||||
golang.org/x/text v0.14.0
|
||||
golang.org/x/time v0.5.0
|
||||
golang.org/x/tools v0.20.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
|
|
@ -93,7 +93,7 @@ require (
|
|||
github.com/aws/smithy-go v1.15.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.10.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cockroachdb/errors v1.11.1 // indirect
|
||||
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect
|
||||
github.com/cockroachdb/redact v1.1.5 // indirect
|
||||
|
|
|
|||
39
go.sum
39
go.sum
|
|
@ -44,17 +44,15 @@ github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.2.0/go.mod h1:+6KLcKIVgx
|
|||
github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 h1:OBhqkivkhkMqLPymWEppkm7vgPQY2XsHoEkaMQ0AdZY=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
|
||||
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
|
||||
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
|
||||
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
||||
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA=
|
||||
github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
|
||||
github.com/VictoriaMetrics/fastcache v1.12.1 h1:i0mICQuojGDL3KblA7wUNlY5lOK6a4bwt3uRKnkZU40=
|
||||
github.com/VictoriaMetrics/fastcache v1.12.1/go.mod h1:tX04vaqcNoQeGLD+ra5pU5sWkuxnzWhEzLwhP9w653o=
|
||||
github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI=
|
||||
github.com/VictoriaMetrics/fastcache v1.12.2/go.mod h1:AmC+Nzz1+3G2eCPapF6UcsnkThDcMsQicp4xDukwJYI=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
|
|
@ -103,8 +101,9 @@ github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk=
|
|||
github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
|
|
@ -142,8 +141,8 @@ github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV
|
|||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/deckarep/golang-set/v2 v2.1.0 h1:g47V4Or+DUdzbs8FxCCmgb6VYd+ptPAngjM6dtGktsI=
|
||||
github.com/deckarep/golang-set/v2 v2.1.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
|
||||
github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM=
|
||||
github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
|
||||
|
|
@ -171,8 +170,8 @@ github.com/ethereum/c-kzg-4844 v1.0.0 h1:0X1LBXxaEtYD9xsyj9B9ctQEZIpnvVDeoBx8aHE
|
|||
github.com/ethereum/c-kzg-4844 v1.0.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0=
|
||||
github.com/ethereum/go-verkle v0.1.1-0.20240306133620-7d920df305f0 h1:KrE8I4reeVvf7C1tm8elRjj4BdscTYzz/WAbYyf/JI4=
|
||||
github.com/ethereum/go-verkle v0.1.1-0.20240306133620-7d920df305f0/go.mod h1:D9AJLVXSyZQXJQVk8oh1EwjISE+sJTn2duYIZC0dy3w=
|
||||
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/ferranbt/fastssz v0.1.2 h1:Dky6dXlngF6Qjc+EfDipAkE83N5I5DE68bY6O0VLNPk=
|
||||
github.com/ferranbt/fastssz v0.1.2/go.mod h1:X5UPrE2u1UJjxHA8X54u04SBwdAQjG2sFtWs39YxyWs=
|
||||
github.com/fjl/gencodec v0.0.0-20230517082657-f9840df7b83e h1:bBLctRc7kr01YGvaDfgLbTwjFNW5jdp5y5rj8XXBHfY=
|
||||
|
|
@ -377,16 +376,14 @@ github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIG
|
|||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
||||
|
|
@ -682,7 +679,6 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
|
@ -690,11 +686,12 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
|
|
@ -851,8 +848,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
|
|||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
|
|
|||
|
|
@ -966,7 +966,7 @@ func (s *BlockChainAPI) GetBlockReceipts(ctx context.Context, blockNrOrHash rpc.
|
|||
// of a message call.
|
||||
// Note, state and stateDiff can't be specified at the same time. If state is
|
||||
// set, message execution will only use the data in the given state. Otherwise
|
||||
// if statDiff is set, all diff will be applied first and then execute the call
|
||||
// if stateDiff is set, all diff will be applied first and then execute the call
|
||||
// message.
|
||||
type OverrideAccount struct {
|
||||
Nonce *hexutil.Uint64 `json:"nonce"`
|
||||
|
|
@ -1203,7 +1203,7 @@ func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNr
|
|||
return 0, err
|
||||
}
|
||||
call := args.ToMessage(header.BaseFee)
|
||||
// Run the gas estimation andwrap any revertals into a custom return
|
||||
// Run the gas estimation and wrap any revertals into a custom return
|
||||
estimate, revert, err := gasestimator.Estimate(ctx, call, opts, gasCap)
|
||||
if err != nil {
|
||||
if len(revert) > 0 {
|
||||
|
|
|
|||
|
|
@ -751,7 +751,7 @@ func TestEstimateGas(t *testing.T) {
|
|||
From: &accounts[0].addr,
|
||||
To: &accounts[1].addr,
|
||||
Value: (*hexutil.Big)(big.NewInt(1)),
|
||||
BlobHashes: []common.Hash{common.Hash{0x01, 0x22}},
|
||||
BlobHashes: []common.Hash{{0x01, 0x22}},
|
||||
BlobFeeCap: (*hexutil.Big)(big.NewInt(1)),
|
||||
},
|
||||
want: 21000,
|
||||
|
|
@ -939,7 +939,7 @@ func TestCall(t *testing.T) {
|
|||
call: TransactionArgs{
|
||||
From: &accounts[1].addr,
|
||||
To: &randomAccounts[2].addr,
|
||||
BlobHashes: []common.Hash{common.Hash{0x01, 0x22}},
|
||||
BlobHashes: []common.Hash{{0x01, 0x22}},
|
||||
BlobFeeCap: (*hexutil.Big)(big.NewInt(1)),
|
||||
},
|
||||
overrides: StateOverride{
|
||||
|
|
@ -1063,7 +1063,7 @@ func TestSendBlobTransaction(t *testing.T) {
|
|||
From: &b.acc.Address,
|
||||
To: &to,
|
||||
Value: (*hexutil.Big)(big.NewInt(1)),
|
||||
BlobHashes: []common.Hash{common.Hash{0x01, 0x22}},
|
||||
BlobHashes: []common.Hash{{0x01, 0x22}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to fill tx defaults: %v\n", err)
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ func (h *bufHandler) Handle(_ context.Context, r slog.Record) error {
|
|||
}
|
||||
|
||||
func (h *bufHandler) Enabled(_ context.Context, lvl slog.Level) bool {
|
||||
return lvl <= h.level
|
||||
return lvl >= h.level
|
||||
}
|
||||
|
||||
func (h *bufHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ func benchmarkLogger(b *testing.B, l Logger) {
|
|||
tt = time.Now()
|
||||
bigint = big.NewInt(100)
|
||||
nilbig *big.Int
|
||||
err = errors.New("Oh nooes it's crap")
|
||||
err = errors.New("oh nooes it's crap")
|
||||
)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
|
@ -126,7 +126,7 @@ func TestLoggerOutput(t *testing.T) {
|
|||
tt = time.Time{}
|
||||
bigint = big.NewInt(100)
|
||||
nilbig *big.Int
|
||||
err = errors.New("Oh nooes it's crap")
|
||||
err = errors.New("oh nooes it's crap")
|
||||
smallUint = uint256.NewInt(500_000)
|
||||
bigUint = &uint256.Int{0xff, 0xff, 0xff, 0xff}
|
||||
)
|
||||
|
|
@ -150,7 +150,7 @@ func TestLoggerOutput(t *testing.T) {
|
|||
|
||||
have := out.String()
|
||||
t.Logf("output %v", out.String())
|
||||
want := `INFO [11-07|19:14:33.821] This is a message foo=123 bytes="[0 0 0 0 0 0 0 0 0 0]" bonk="a string with text" time=0001-01-01T00:00:00+0000 bigint=100 nilbig=<nil> err="Oh nooes it's crap" struct="{A:Foo B:12}" struct="{A:Foo\nLinebreak B:122}" ptrstruct="&{A:Foo B:12}" smalluint=500,000 bigUint=1,600,660,942,523,603,594,864,898,306,482,794,244,293,965,082,972,225,630,372,095
|
||||
want := `INFO [11-07|19:14:33.821] This is a message foo=123 bytes="[0 0 0 0 0 0 0 0 0 0]" bonk="a string with text" time=0001-01-01T00:00:00+0000 bigint=100 nilbig=<nil> err="oh nooes it's crap" struct="{A:Foo B:12}" struct="{A:Foo\nLinebreak B:122}" ptrstruct="&{A:Foo B:12}" smalluint=500,000 bigUint=1,600,660,942,523,603,594,864,898,306,482,794,244,293,965,082,972,225,630,372,095
|
||||
`
|
||||
if !bytes.Equal([]byte(have)[25:], []byte(want)[25:]) {
|
||||
t.Errorf("Error\nhave: %q\nwant: %q", have, want)
|
||||
|
|
|
|||
|
|
@ -19,18 +19,18 @@ var (
|
|||
gcStats debug.GCStats
|
||||
)
|
||||
|
||||
// Capture new values for the Go garbage collector statistics exported in
|
||||
// debug.GCStats. This is designed to be called as a goroutine.
|
||||
// CaptureDebugGCStats captures new values for the Go garbage collector statistics
|
||||
// exported in debug.GCStats. This is designed to be called as a goroutine.
|
||||
func CaptureDebugGCStats(r Registry, d time.Duration) {
|
||||
for range time.Tick(d) {
|
||||
CaptureDebugGCStatsOnce(r)
|
||||
}
|
||||
}
|
||||
|
||||
// Capture new values for the Go garbage collector statistics exported in
|
||||
// debug.GCStats. This is designed to be called in a background goroutine.
|
||||
// Giving a registry which has not been given to RegisterDebugGCStats will
|
||||
// panic.
|
||||
// CaptureDebugGCStatsOnce captures new values for the Go garbage collector
|
||||
// statistics exported in debug.GCStats. This is designed to be called in
|
||||
// a background goroutine. Giving a registry which has not been given to
|
||||
// RegisterDebugGCStats will panic.
|
||||
//
|
||||
// Be careful (but much less so) with this because debug.ReadGCStats calls
|
||||
// the C function runtime·lock(runtime·mheap) which, while not a stop-the-world
|
||||
|
|
@ -50,9 +50,9 @@ func CaptureDebugGCStatsOnce(r Registry) {
|
|||
debugMetrics.GCStats.PauseTotal.Update(int64(gcStats.PauseTotal))
|
||||
}
|
||||
|
||||
// Register metrics for the Go garbage collector statistics exported in
|
||||
// debug.GCStats. The metrics are named by their fully-qualified Go symbols,
|
||||
// i.e. debug.GCStats.PauseTotal.
|
||||
// RegisterDebugGCStats registers metrics for the Go garbage collector statistics
|
||||
// exported in debug.GCStats. The metrics are named by their fully-qualified Go
|
||||
// symbols, i.e. debug.GCStats.PauseTotal.
|
||||
func RegisterDebugGCStats(r Registry) {
|
||||
debugMetrics.GCStats.LastGC = NewGauge()
|
||||
debugMetrics.GCStats.NumGC = NewGauge()
|
||||
|
|
|
|||
|
|
@ -103,18 +103,18 @@ func TestExpDecaySample(t *testing.T) {
|
|||
}
|
||||
snap := sample.Snapshot()
|
||||
if have, want := int(snap.Count()), tc.updates; have != want {
|
||||
t.Errorf("have %d want %d", have, want)
|
||||
t.Errorf("unexpected count: have %d want %d", have, want)
|
||||
}
|
||||
if have, want := snap.Size(), min(tc.updates, tc.reservoirSize); have != want {
|
||||
t.Errorf("have %d want %d", have, want)
|
||||
t.Errorf("unexpected size: have %d want %d", have, want)
|
||||
}
|
||||
values := snap.(*sampleSnapshot).values
|
||||
if have, want := len(values), min(tc.updates, tc.reservoirSize); have != want {
|
||||
t.Errorf("have %d want %d", have, want)
|
||||
t.Errorf("unexpected values length: have %d want %d", have, want)
|
||||
}
|
||||
for _, v := range values {
|
||||
if v > int64(tc.updates) || v < 0 {
|
||||
t.Errorf("out of range [0, %d): %v", tc.updates, v)
|
||||
t.Errorf("out of range [0, %d]: %v", tc.updates, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -125,12 +125,12 @@ func TestExpDecaySample(t *testing.T) {
|
|||
// The priority becomes +Inf quickly after starting if this is done,
|
||||
// effectively freezing the set of samples until a rescale step happens.
|
||||
func TestExpDecaySampleNanosecondRegression(t *testing.T) {
|
||||
sw := NewExpDecaySample(100, 0.99)
|
||||
for i := 0; i < 100; i++ {
|
||||
sw := NewExpDecaySample(1000, 0.99)
|
||||
for i := 0; i < 1000; i++ {
|
||||
sw.Update(10)
|
||||
}
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
for i := 0; i < 100; i++ {
|
||||
for i := 0; i < 1000; i++ {
|
||||
sw.Update(20)
|
||||
}
|
||||
s := sw.Snapshot()
|
||||
|
|
@ -195,7 +195,7 @@ func TestUniformSample(t *testing.T) {
|
|||
}
|
||||
for _, v := range values {
|
||||
if v > 1000 || v < 0 {
|
||||
t.Errorf("out of range [0, 100): %v\n", v)
|
||||
t.Errorf("out of range [0, 1000]: %v\n", v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -251,6 +251,9 @@ func benchmarkSample(b *testing.B, s Sample) {
|
|||
}
|
||||
|
||||
func testExpDecaySampleStatistics(t *testing.T, s SampleSnapshot) {
|
||||
if sum := s.Sum(); sum != 496598 {
|
||||
t.Errorf("s.Sum(): 496598 != %v\n", sum)
|
||||
}
|
||||
if count := s.Count(); count != 10000 {
|
||||
t.Errorf("s.Count(): 10000 != %v\n", count)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ func (b *testWorkerBackend) TxPool() *txpool.TxPool { return b.txPool }
|
|||
|
||||
func newTestWorker(t *testing.T, chainConfig *params.ChainConfig, engine consensus.Engine, db ethdb.Database, blocks int) (*Miner, *testWorkerBackend) {
|
||||
backend := newTestWorkerBackend(t, chainConfig, engine, db, blocks)
|
||||
backend.txPool.Add(pendingTxs, true, false)
|
||||
backend.txPool.Add(pendingTxs, true, true)
|
||||
w := New(backend, testConfig, engine)
|
||||
return w, backend
|
||||
}
|
||||
|
|
|
|||
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/log"
|
||||
"github.com/ethereum/go-ethereum/p2p"
|
||||
"github.com/ethereum/go-ethereum/p2p/discover"
|
||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
)
|
||||
|
|
@ -39,6 +40,9 @@ func (n *Node) apis() []rpc.API {
|
|||
}, {
|
||||
Namespace: "debug",
|
||||
Service: debug.Handler,
|
||||
}, {
|
||||
Namespace: "debug",
|
||||
Service: &p2pDebugAPI{n},
|
||||
}, {
|
||||
Namespace: "web3",
|
||||
Service: &web3API{n},
|
||||
|
|
@ -333,3 +337,16 @@ func (s *web3API) ClientVersion() string {
|
|||
func (s *web3API) Sha3(input hexutil.Bytes) hexutil.Bytes {
|
||||
return crypto.Keccak256(input)
|
||||
}
|
||||
|
||||
// p2pDebugAPI provides access to p2p internals for debugging.
|
||||
type p2pDebugAPI struct {
|
||||
stack *Node
|
||||
}
|
||||
|
||||
func (s *p2pDebugAPI) DiscoveryV4Table() [][]discover.BucketNode {
|
||||
disc := s.stack.server.DiscoveryV4()
|
||||
if disc != nil {
|
||||
return disc.TableBuckets()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,11 @@ package discover
|
|||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
crand "crypto/rand"
|
||||
"encoding/binary"
|
||||
"math/rand"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common/mclock"
|
||||
|
|
@ -62,7 +66,7 @@ type Config struct {
|
|||
func (cfg Config) withDefaults() Config {
|
||||
// Node table configuration:
|
||||
if cfg.PingInterval == 0 {
|
||||
cfg.PingInterval = 10 * time.Second
|
||||
cfg.PingInterval = 3 * time.Second
|
||||
}
|
||||
if cfg.RefreshInterval == 0 {
|
||||
cfg.RefreshInterval = 30 * time.Minute
|
||||
|
|
@ -92,3 +96,44 @@ type ReadPacket struct {
|
|||
Data []byte
|
||||
Addr *net.UDPAddr
|
||||
}
|
||||
|
||||
type randomSource interface {
|
||||
Intn(int) int
|
||||
Int63n(int64) int64
|
||||
Shuffle(int, func(int, int))
|
||||
}
|
||||
|
||||
// reseedingRandom is a random number generator that tracks when it was last re-seeded.
|
||||
type reseedingRandom struct {
|
||||
mu sync.Mutex
|
||||
cur *rand.Rand
|
||||
}
|
||||
|
||||
func (r *reseedingRandom) seed() {
|
||||
var b [8]byte
|
||||
crand.Read(b[:])
|
||||
seed := binary.BigEndian.Uint64(b[:])
|
||||
new := rand.New(rand.NewSource(int64(seed)))
|
||||
|
||||
r.mu.Lock()
|
||||
r.cur = new
|
||||
r.mu.Unlock()
|
||||
}
|
||||
|
||||
func (r *reseedingRandom) Intn(n int) int {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return r.cur.Intn(n)
|
||||
}
|
||||
|
||||
func (r *reseedingRandom) Int63n(n int64) int64 {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return r.cur.Int63n(n)
|
||||
}
|
||||
|
||||
func (r *reseedingRandom) Shuffle(n int, swap func(i, j int)) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.cur.Shuffle(n, swap)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -140,32 +140,13 @@ func (it *lookup) slowdown() {
|
|||
}
|
||||
|
||||
func (it *lookup) query(n *node, reply chan<- []*node) {
|
||||
fails := it.tab.db.FindFails(n.ID(), n.IP())
|
||||
r, err := it.queryfunc(n)
|
||||
if errors.Is(err, errClosed) {
|
||||
// Avoid recording failures on shutdown.
|
||||
reply <- nil
|
||||
return
|
||||
} else if len(r) == 0 {
|
||||
fails++
|
||||
it.tab.db.UpdateFindFails(n.ID(), n.IP(), fails)
|
||||
// Remove the node from the local table if it fails to return anything useful too
|
||||
// many times, but only if there are enough other nodes in the bucket.
|
||||
dropped := false
|
||||
if fails >= maxFindnodeFailures && it.tab.bucketLen(n.ID()) >= bucketSize/2 {
|
||||
dropped = true
|
||||
it.tab.delete(n)
|
||||
if !errors.Is(err, errClosed) { // avoid recording failures on shutdown.
|
||||
success := len(r) > 0
|
||||
it.tab.trackRequest(n, success, r)
|
||||
if err != nil {
|
||||
it.tab.log.Trace("FINDNODE failed", "id", n.ID(), "err", err)
|
||||
}
|
||||
it.tab.log.Trace("FINDNODE failed", "id", n.ID(), "failcount", fails, "dropped", dropped, "err", err)
|
||||
} else if fails > 0 {
|
||||
// Reset failure counter because it counts _consecutive_ failures.
|
||||
it.tab.db.UpdateFindFails(n.ID(), n.IP(), 0)
|
||||
}
|
||||
|
||||
// Grab as many nodes as possible. Some of them might not be alive anymore, but we'll
|
||||
// just remove those again during revalidation.
|
||||
for _, n := range r {
|
||||
it.tab.addSeenNode(n)
|
||||
}
|
||||
reply <- r
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,12 +29,23 @@ import (
|
|||
"github.com/ethereum/go-ethereum/p2p/enode"
|
||||
)
|
||||
|
||||
type BucketNode struct {
|
||||
Node *enode.Node `json:"node"`
|
||||
AddedToTable time.Time `json:"addedToTable"`
|
||||
AddedToBucket time.Time `json:"addedToBucket"`
|
||||
Checks int `json:"checks"`
|
||||
Live bool `json:"live"`
|
||||
}
|
||||
|
||||
// node represents a host on the network.
|
||||
// The fields of Node may not be modified.
|
||||
type node struct {
|
||||
enode.Node
|
||||
addedAt time.Time // time when the node was added to the table
|
||||
*enode.Node
|
||||
revalList *revalidationList
|
||||
addedToTable time.Time // first time node was added to bucket or replacement list
|
||||
addedToBucket time.Time // time it was added in the actual bucket
|
||||
livenessChecks uint // how often liveness was checked
|
||||
isValidatedLive bool // true if existence of node is considered validated right now
|
||||
}
|
||||
|
||||
type encPubkey [64]byte
|
||||
|
|
@ -65,7 +76,7 @@ func (e encPubkey) id() enode.ID {
|
|||
}
|
||||
|
||||
func wrapNode(n *enode.Node) *node {
|
||||
return &node{Node: *n}
|
||||
return &node{Node: n}
|
||||
}
|
||||
|
||||
func wrapNodes(ns []*enode.Node) []*node {
|
||||
|
|
@ -77,7 +88,7 @@ func wrapNodes(ns []*enode.Node) []*node {
|
|||
}
|
||||
|
||||
func unwrapNode(n *node) *enode.Node {
|
||||
return &n.Node
|
||||
return n.Node
|
||||
}
|
||||
|
||||
func unwrapNodes(ns []*node) []*enode.Node {
|
||||
|
|
|
|||
|
|
@ -24,16 +24,15 @@ package discover
|
|||
|
||||
import (
|
||||
"context"
|
||||
crand "crypto/rand"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
mrand "math/rand"
|
||||
"net"
|
||||
"slices"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/mclock"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/metrics"
|
||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
||||
|
|
@ -55,7 +54,6 @@ const (
|
|||
bucketIPLimit, bucketSubnet = 2, 24 // at most 2 addresses from the same /24
|
||||
tableIPLimit, tableSubnet = 10, 24
|
||||
|
||||
copyNodesInterval = 30 * time.Second
|
||||
seedMinTableTime = 5 * time.Minute
|
||||
seedCount = 30
|
||||
seedMaxAge = 5 * 24 * time.Hour
|
||||
|
|
@ -68,8 +66,9 @@ type Table struct {
|
|||
mutex sync.Mutex // protects buckets, bucket content, nursery, rand
|
||||
buckets [nBuckets]*bucket // index of known nodes by distance
|
||||
nursery []*node // bootstrap nodes
|
||||
rand *mrand.Rand // source of randomness, periodically reseeded
|
||||
rand reseedingRandom // source of randomness, periodically reseeded
|
||||
ips netutil.DistinctNetSet
|
||||
revalidation tableRevalidation
|
||||
|
||||
db *enode.DB // database of known nodes
|
||||
net transport
|
||||
|
|
@ -78,6 +77,10 @@ type Table struct {
|
|||
|
||||
// loop channels
|
||||
refreshReq chan chan struct{}
|
||||
revalResponseCh chan revalidationResponse
|
||||
addNodeCh chan addNodeOp
|
||||
addNodeHandled chan bool
|
||||
trackRequestCh chan trackRequestOp
|
||||
initDone chan struct{}
|
||||
closeReq chan struct{}
|
||||
closed chan struct{}
|
||||
|
|
@ -104,6 +107,17 @@ type bucket struct {
|
|||
index int
|
||||
}
|
||||
|
||||
type addNodeOp struct {
|
||||
node *node
|
||||
isInbound bool
|
||||
}
|
||||
|
||||
type trackRequestOp struct {
|
||||
node *node
|
||||
foundNodes []*node
|
||||
success bool
|
||||
}
|
||||
|
||||
func newTable(t transport, db *enode.DB, cfg Config) (*Table, error) {
|
||||
cfg = cfg.withDefaults()
|
||||
tab := &Table{
|
||||
|
|
@ -112,56 +126,49 @@ func newTable(t transport, db *enode.DB, cfg Config) (*Table, error) {
|
|||
cfg: cfg,
|
||||
log: cfg.Log,
|
||||
refreshReq: make(chan chan struct{}),
|
||||
revalResponseCh: make(chan revalidationResponse),
|
||||
addNodeCh: make(chan addNodeOp),
|
||||
addNodeHandled: make(chan bool),
|
||||
trackRequestCh: make(chan trackRequestOp),
|
||||
initDone: make(chan struct{}),
|
||||
closeReq: make(chan struct{}),
|
||||
closed: make(chan struct{}),
|
||||
rand: mrand.New(mrand.NewSource(0)),
|
||||
ips: netutil.DistinctNetSet{Subnet: tableSubnet, Limit: tableIPLimit},
|
||||
}
|
||||
if err := tab.setFallbackNodes(cfg.Bootnodes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := range tab.buckets {
|
||||
tab.buckets[i] = &bucket{
|
||||
index: i,
|
||||
ips: netutil.DistinctNetSet{Subnet: bucketSubnet, Limit: bucketIPLimit},
|
||||
}
|
||||
}
|
||||
tab.seedRand()
|
||||
tab.rand.seed()
|
||||
tab.revalidation.init(&cfg)
|
||||
|
||||
// initial table content
|
||||
if err := tab.setFallbackNodes(cfg.Bootnodes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tab.loadSeedNodes()
|
||||
|
||||
return tab, nil
|
||||
}
|
||||
|
||||
func newMeteredTable(t transport, db *enode.DB, cfg Config) (*Table, error) {
|
||||
tab, err := newTable(t, db, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if metrics.Enabled {
|
||||
tab.nodeAddedHook = func(b *bucket, n *node) {
|
||||
bucketsCounter[b.index].Inc(1)
|
||||
}
|
||||
tab.nodeRemovedHook = func(b *bucket, n *node) {
|
||||
bucketsCounter[b.index].Dec(1)
|
||||
}
|
||||
}
|
||||
return tab, nil
|
||||
}
|
||||
|
||||
// Nodes returns all nodes contained in the table.
|
||||
func (tab *Table) Nodes() []*enode.Node {
|
||||
if !tab.isInitDone() {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tab *Table) Nodes() [][]BucketNode {
|
||||
tab.mutex.Lock()
|
||||
defer tab.mutex.Unlock()
|
||||
|
||||
var nodes []*enode.Node
|
||||
for _, b := range &tab.buckets {
|
||||
for _, n := range b.entries {
|
||||
nodes = append(nodes, unwrapNode(n))
|
||||
nodes := make([][]BucketNode, len(tab.buckets))
|
||||
for i, b := range &tab.buckets {
|
||||
nodes[i] = make([]BucketNode, len(b.entries))
|
||||
for j, n := range b.entries {
|
||||
nodes[i][j] = BucketNode{
|
||||
Node: n.Node,
|
||||
Checks: int(n.livenessChecks),
|
||||
Live: n.isValidatedLive,
|
||||
AddedToTable: n.addedToTable,
|
||||
AddedToBucket: n.addedToBucket,
|
||||
}
|
||||
}
|
||||
}
|
||||
return nodes
|
||||
|
|
@ -171,15 +178,6 @@ func (tab *Table) self() *enode.Node {
|
|||
return tab.net.Self()
|
||||
}
|
||||
|
||||
func (tab *Table) seedRand() {
|
||||
var b [8]byte
|
||||
crand.Read(b[:])
|
||||
|
||||
tab.mutex.Lock()
|
||||
tab.rand.Seed(int64(binary.BigEndian.Uint64(b[:])))
|
||||
tab.mutex.Unlock()
|
||||
}
|
||||
|
||||
// getNode returns the node with the given ID or nil if it isn't in the table.
|
||||
func (tab *Table) getNode(id enode.ID) *enode.Node {
|
||||
tab.mutex.Lock()
|
||||
|
|
@ -239,52 +237,173 @@ func (tab *Table) refresh() <-chan struct{} {
|
|||
return done
|
||||
}
|
||||
|
||||
// loop schedules runs of doRefresh, doRevalidate and copyLiveNodes.
|
||||
// findnodeByID returns the n nodes in the table that are closest to the given id.
|
||||
// This is used by the FINDNODE/v4 handler.
|
||||
//
|
||||
// The preferLive parameter says whether the caller wants liveness-checked results. If
|
||||
// preferLive is true and the table contains any verified nodes, the result will not
|
||||
// contain unverified nodes. However, if there are no verified nodes at all, the result
|
||||
// will contain unverified nodes.
|
||||
func (tab *Table) findnodeByID(target enode.ID, nresults int, preferLive bool) *nodesByDistance {
|
||||
tab.mutex.Lock()
|
||||
defer tab.mutex.Unlock()
|
||||
|
||||
// Scan all buckets. There might be a better way to do this, but there aren't that many
|
||||
// buckets, so this solution should be fine. The worst-case complexity of this loop
|
||||
// is O(tab.len() * nresults).
|
||||
nodes := &nodesByDistance{target: target}
|
||||
liveNodes := &nodesByDistance{target: target}
|
||||
for _, b := range &tab.buckets {
|
||||
for _, n := range b.entries {
|
||||
nodes.push(n, nresults)
|
||||
if preferLive && n.isValidatedLive {
|
||||
liveNodes.push(n, nresults)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if preferLive && len(liveNodes.entries) > 0 {
|
||||
return liveNodes
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
// appendLiveNodes adds nodes at the given distance to the result slice.
|
||||
// This is used by the FINDNODE/v5 handler.
|
||||
func (tab *Table) appendLiveNodes(dist uint, result []*enode.Node) []*enode.Node {
|
||||
if dist > 256 {
|
||||
return result
|
||||
}
|
||||
if dist == 0 {
|
||||
return append(result, tab.self())
|
||||
}
|
||||
|
||||
tab.mutex.Lock()
|
||||
for _, n := range tab.bucketAtDistance(int(dist)).entries {
|
||||
if n.isValidatedLive {
|
||||
result = append(result, n.Node)
|
||||
}
|
||||
}
|
||||
tab.mutex.Unlock()
|
||||
|
||||
// Shuffle result to avoid always returning same nodes in FINDNODE/v5.
|
||||
tab.rand.Shuffle(len(result), func(i, j int) {
|
||||
result[i], result[j] = result[j], result[i]
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
// len returns the number of nodes in the table.
|
||||
func (tab *Table) len() (n int) {
|
||||
tab.mutex.Lock()
|
||||
defer tab.mutex.Unlock()
|
||||
|
||||
for _, b := range &tab.buckets {
|
||||
n += len(b.entries)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// addFoundNode adds a node which may not be live. If the bucket has space available,
|
||||
// adding the node succeeds immediately. Otherwise, the node is added to the replacements
|
||||
// list.
|
||||
//
|
||||
// The caller must not hold tab.mutex.
|
||||
func (tab *Table) addFoundNode(n *node) bool {
|
||||
op := addNodeOp{node: n, isInbound: false}
|
||||
select {
|
||||
case tab.addNodeCh <- op:
|
||||
return <-tab.addNodeHandled
|
||||
case <-tab.closeReq:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// addInboundNode adds a node from an inbound contact. If the bucket has no space, the
|
||||
// node is added to the replacements list.
|
||||
//
|
||||
// There is an additional safety measure: if the table is still initializing the node is
|
||||
// not added. This prevents an attack where the table could be filled by just sending ping
|
||||
// repeatedly.
|
||||
//
|
||||
// The caller must not hold tab.mutex.
|
||||
func (tab *Table) addInboundNode(n *node) bool {
|
||||
op := addNodeOp{node: n, isInbound: true}
|
||||
select {
|
||||
case tab.addNodeCh <- op:
|
||||
return <-tab.addNodeHandled
|
||||
case <-tab.closeReq:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (tab *Table) trackRequest(n *node, success bool, foundNodes []*node) {
|
||||
op := trackRequestOp{n, foundNodes, success}
|
||||
select {
|
||||
case tab.trackRequestCh <- op:
|
||||
case <-tab.closeReq:
|
||||
}
|
||||
}
|
||||
|
||||
// loop is the main loop of Table.
|
||||
func (tab *Table) loop() {
|
||||
var (
|
||||
revalidate = time.NewTimer(tab.nextRevalidateTime())
|
||||
refresh = time.NewTimer(tab.nextRefreshTime())
|
||||
copyNodes = time.NewTicker(copyNodesInterval)
|
||||
refreshDone = make(chan struct{}) // where doRefresh reports completion
|
||||
revalidateDone chan struct{} // where doRevalidate reports completion
|
||||
waiting = []chan struct{}{tab.initDone} // holds waiting callers while doRefresh runs
|
||||
revalTimer = mclock.NewAlarm(tab.cfg.Clock)
|
||||
reseedRandTimer = time.NewTicker(10 * time.Minute)
|
||||
)
|
||||
defer refresh.Stop()
|
||||
defer revalidate.Stop()
|
||||
defer copyNodes.Stop()
|
||||
defer revalTimer.Stop()
|
||||
defer reseedRandTimer.Stop()
|
||||
|
||||
// Start initial refresh.
|
||||
go tab.doRefresh(refreshDone)
|
||||
|
||||
loop:
|
||||
for {
|
||||
nextTime := tab.revalidation.run(tab, tab.cfg.Clock.Now())
|
||||
revalTimer.Schedule(nextTime)
|
||||
|
||||
select {
|
||||
case <-reseedRandTimer.C:
|
||||
tab.rand.seed()
|
||||
|
||||
case <-revalTimer.C():
|
||||
|
||||
case r := <-tab.revalResponseCh:
|
||||
tab.revalidation.handleResponse(tab, r)
|
||||
|
||||
case op := <-tab.addNodeCh:
|
||||
tab.mutex.Lock()
|
||||
ok := tab.handleAddNode(op)
|
||||
tab.mutex.Unlock()
|
||||
tab.addNodeHandled <- ok
|
||||
|
||||
case op := <-tab.trackRequestCh:
|
||||
tab.handleTrackRequest(op)
|
||||
|
||||
case <-refresh.C:
|
||||
tab.seedRand()
|
||||
if refreshDone == nil {
|
||||
refreshDone = make(chan struct{})
|
||||
go tab.doRefresh(refreshDone)
|
||||
}
|
||||
|
||||
case req := <-tab.refreshReq:
|
||||
waiting = append(waiting, req)
|
||||
if refreshDone == nil {
|
||||
refreshDone = make(chan struct{})
|
||||
go tab.doRefresh(refreshDone)
|
||||
}
|
||||
|
||||
case <-refreshDone:
|
||||
for _, ch := range waiting {
|
||||
close(ch)
|
||||
}
|
||||
waiting, refreshDone = nil, nil
|
||||
refresh.Reset(tab.nextRefreshTime())
|
||||
case <-revalidate.C:
|
||||
revalidateDone = make(chan struct{})
|
||||
go tab.doRevalidate(revalidateDone)
|
||||
case <-revalidateDone:
|
||||
revalidate.Reset(tab.nextRevalidateTime())
|
||||
revalidateDone = nil
|
||||
case <-copyNodes.C:
|
||||
go tab.copyLiveNodes()
|
||||
|
||||
case <-tab.closeReq:
|
||||
break loop
|
||||
}
|
||||
|
|
@ -296,9 +415,6 @@ loop:
|
|||
for _, ch := range waiting {
|
||||
close(ch)
|
||||
}
|
||||
if revalidateDone != nil {
|
||||
<-revalidateDone
|
||||
}
|
||||
close(tab.closed)
|
||||
}
|
||||
|
||||
|
|
@ -335,169 +451,15 @@ func (tab *Table) loadSeedNodes() {
|
|||
age := time.Since(tab.db.LastPongReceived(seed.ID(), seed.IP()))
|
||||
tab.log.Trace("Found seed node in database", "id", seed.ID(), "addr", seed.addr(), "age", age)
|
||||
}
|
||||
tab.addSeenNode(seed)
|
||||
tab.handleAddNode(addNodeOp{node: seed, isInbound: false})
|
||||
}
|
||||
}
|
||||
|
||||
// doRevalidate checks that the last node in a random bucket is still live and replaces or
|
||||
// deletes the node if it isn't.
|
||||
func (tab *Table) doRevalidate(done chan<- struct{}) {
|
||||
defer func() { done <- struct{}{} }()
|
||||
|
||||
last, bi := tab.nodeToRevalidate()
|
||||
if last == nil {
|
||||
// No non-empty bucket found.
|
||||
return
|
||||
}
|
||||
|
||||
// Ping the selected node and wait for a pong.
|
||||
remoteSeq, err := tab.net.ping(unwrapNode(last))
|
||||
|
||||
// Also fetch record if the node replied and returned a higher sequence number.
|
||||
if last.Seq() < remoteSeq {
|
||||
n, err := tab.net.RequestENR(unwrapNode(last))
|
||||
if err != nil {
|
||||
tab.log.Debug("ENR request failed", "id", last.ID(), "addr", last.addr(), "err", err)
|
||||
} else {
|
||||
last = &node{Node: *n, addedAt: last.addedAt, livenessChecks: last.livenessChecks}
|
||||
}
|
||||
}
|
||||
|
||||
tab.mutex.Lock()
|
||||
defer tab.mutex.Unlock()
|
||||
b := tab.buckets[bi]
|
||||
if err == nil {
|
||||
// The node responded, move it to the front.
|
||||
last.livenessChecks++
|
||||
tab.log.Debug("Revalidated node", "b", bi, "id", last.ID(), "checks", last.livenessChecks)
|
||||
tab.bumpInBucket(b, last)
|
||||
return
|
||||
}
|
||||
// No reply received, pick a replacement or delete the node if there aren't
|
||||
// any replacements.
|
||||
if r := tab.replace(b, last); r != nil {
|
||||
tab.log.Debug("Replaced dead node", "b", bi, "id", last.ID(), "ip", last.IP(), "checks", last.livenessChecks, "r", r.ID(), "rip", r.IP())
|
||||
} else {
|
||||
tab.log.Debug("Removed dead node", "b", bi, "id", last.ID(), "ip", last.IP(), "checks", last.livenessChecks)
|
||||
}
|
||||
}
|
||||
|
||||
// nodeToRevalidate returns the last node in a random, non-empty bucket.
|
||||
func (tab *Table) nodeToRevalidate() (n *node, bi int) {
|
||||
tab.mutex.Lock()
|
||||
defer tab.mutex.Unlock()
|
||||
|
||||
for _, bi = range tab.rand.Perm(len(tab.buckets)) {
|
||||
b := tab.buckets[bi]
|
||||
if len(b.entries) > 0 {
|
||||
last := b.entries[len(b.entries)-1]
|
||||
return last, bi
|
||||
}
|
||||
}
|
||||
return nil, 0
|
||||
}
|
||||
|
||||
func (tab *Table) nextRevalidateTime() time.Duration {
|
||||
tab.mutex.Lock()
|
||||
defer tab.mutex.Unlock()
|
||||
|
||||
return time.Duration(tab.rand.Int63n(int64(tab.cfg.PingInterval)))
|
||||
}
|
||||
|
||||
func (tab *Table) nextRefreshTime() time.Duration {
|
||||
tab.mutex.Lock()
|
||||
defer tab.mutex.Unlock()
|
||||
|
||||
half := tab.cfg.RefreshInterval / 2
|
||||
return half + time.Duration(tab.rand.Int63n(int64(half)))
|
||||
}
|
||||
|
||||
// copyLiveNodes adds nodes from the table to the database if they have been in the table
|
||||
// longer than seedMinTableTime.
|
||||
func (tab *Table) copyLiveNodes() {
|
||||
tab.mutex.Lock()
|
||||
defer tab.mutex.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
for _, b := range &tab.buckets {
|
||||
for _, n := range b.entries {
|
||||
if n.livenessChecks > 0 && now.Sub(n.addedAt) >= seedMinTableTime {
|
||||
tab.db.UpdateNode(unwrapNode(n))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// findnodeByID returns the n nodes in the table that are closest to the given id.
|
||||
// This is used by the FINDNODE/v4 handler.
|
||||
//
|
||||
// The preferLive parameter says whether the caller wants liveness-checked results. If
|
||||
// preferLive is true and the table contains any verified nodes, the result will not
|
||||
// contain unverified nodes. However, if there are no verified nodes at all, the result
|
||||
// will contain unverified nodes.
|
||||
func (tab *Table) findnodeByID(target enode.ID, nresults int, preferLive bool) *nodesByDistance {
|
||||
tab.mutex.Lock()
|
||||
defer tab.mutex.Unlock()
|
||||
|
||||
// Scan all buckets. There might be a better way to do this, but there aren't that many
|
||||
// buckets, so this solution should be fine. The worst-case complexity of this loop
|
||||
// is O(tab.len() * nresults).
|
||||
nodes := &nodesByDistance{target: target}
|
||||
liveNodes := &nodesByDistance{target: target}
|
||||
for _, b := range &tab.buckets {
|
||||
for _, n := range b.entries {
|
||||
nodes.push(n, nresults)
|
||||
if preferLive && n.livenessChecks > 0 {
|
||||
liveNodes.push(n, nresults)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if preferLive && len(liveNodes.entries) > 0 {
|
||||
return liveNodes
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
// appendLiveNodes adds nodes at the given distance to the result slice.
|
||||
func (tab *Table) appendLiveNodes(dist uint, result []*enode.Node) []*enode.Node {
|
||||
if dist > 256 {
|
||||
return result
|
||||
}
|
||||
if dist == 0 {
|
||||
return append(result, tab.self())
|
||||
}
|
||||
|
||||
tab.mutex.Lock()
|
||||
defer tab.mutex.Unlock()
|
||||
for _, n := range tab.bucketAtDistance(int(dist)).entries {
|
||||
if n.livenessChecks >= 1 {
|
||||
node := n.Node // avoid handing out pointer to struct field
|
||||
result = append(result, &node)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// len returns the number of nodes in the table.
|
||||
func (tab *Table) len() (n int) {
|
||||
tab.mutex.Lock()
|
||||
defer tab.mutex.Unlock()
|
||||
|
||||
for _, b := range &tab.buckets {
|
||||
n += len(b.entries)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// bucketLen returns the number of nodes in the bucket for the given ID.
|
||||
func (tab *Table) bucketLen(id enode.ID) int {
|
||||
tab.mutex.Lock()
|
||||
defer tab.mutex.Unlock()
|
||||
|
||||
return len(tab.bucket(id).entries)
|
||||
}
|
||||
|
||||
// bucket returns the bucket for the given node ID hash.
|
||||
func (tab *Table) bucket(id enode.ID) *bucket {
|
||||
d := enode.LogDist(tab.self().ID(), id)
|
||||
|
|
@ -511,95 +473,6 @@ func (tab *Table) bucketAtDistance(d int) *bucket {
|
|||
return tab.buckets[d-bucketMinDistance-1]
|
||||
}
|
||||
|
||||
// addSeenNode adds a node which may or may not be live to the end of a bucket. If the
|
||||
// bucket has space available, adding the node succeeds immediately. Otherwise, the node is
|
||||
// added to the replacements list.
|
||||
//
|
||||
// The caller must not hold tab.mutex.
|
||||
func (tab *Table) addSeenNode(n *node) {
|
||||
if n.ID() == tab.self().ID() {
|
||||
return
|
||||
}
|
||||
|
||||
tab.mutex.Lock()
|
||||
defer tab.mutex.Unlock()
|
||||
b := tab.bucket(n.ID())
|
||||
if contains(b.entries, n.ID()) {
|
||||
// Already in bucket, don't add.
|
||||
return
|
||||
}
|
||||
if len(b.entries) >= bucketSize {
|
||||
// Bucket full, maybe add as replacement.
|
||||
tab.addReplacement(b, n)
|
||||
return
|
||||
}
|
||||
if !tab.addIP(b, n.IP()) {
|
||||
// Can't add: IP limit reached.
|
||||
return
|
||||
}
|
||||
|
||||
// Add to end of bucket:
|
||||
b.entries = append(b.entries, n)
|
||||
b.replacements = deleteNode(b.replacements, n)
|
||||
n.addedAt = time.Now()
|
||||
|
||||
if tab.nodeAddedHook != nil {
|
||||
tab.nodeAddedHook(b, n)
|
||||
}
|
||||
}
|
||||
|
||||
// addVerifiedNode adds a node whose existence has been verified recently to the front of a
|
||||
// bucket. If the node is already in the bucket, it is moved to the front. If the bucket
|
||||
// has no space, the node is added to the replacements list.
|
||||
//
|
||||
// There is an additional safety measure: if the table is still initializing the node
|
||||
// is not added. This prevents an attack where the table could be filled by just sending
|
||||
// ping repeatedly.
|
||||
//
|
||||
// The caller must not hold tab.mutex.
|
||||
func (tab *Table) addVerifiedNode(n *node) {
|
||||
if !tab.isInitDone() {
|
||||
return
|
||||
}
|
||||
if n.ID() == tab.self().ID() {
|
||||
return
|
||||
}
|
||||
|
||||
tab.mutex.Lock()
|
||||
defer tab.mutex.Unlock()
|
||||
b := tab.bucket(n.ID())
|
||||
if tab.bumpInBucket(b, n) {
|
||||
// Already in bucket, moved to front.
|
||||
return
|
||||
}
|
||||
if len(b.entries) >= bucketSize {
|
||||
// Bucket full, maybe add as replacement.
|
||||
tab.addReplacement(b, n)
|
||||
return
|
||||
}
|
||||
if !tab.addIP(b, n.IP()) {
|
||||
// Can't add: IP limit reached.
|
||||
return
|
||||
}
|
||||
|
||||
// Add to front of bucket.
|
||||
b.entries, _ = pushNode(b.entries, n, bucketSize)
|
||||
b.replacements = deleteNode(b.replacements, n)
|
||||
n.addedAt = time.Now()
|
||||
|
||||
if tab.nodeAddedHook != nil {
|
||||
tab.nodeAddedHook(b, n)
|
||||
}
|
||||
}
|
||||
|
||||
// delete removes an entry from the node table. It is used to evacuate dead nodes.
|
||||
func (tab *Table) delete(node *node) {
|
||||
tab.mutex.Lock()
|
||||
defer tab.mutex.Unlock()
|
||||
|
||||
tab.deleteInBucket(tab.bucket(node.ID()), node)
|
||||
}
|
||||
|
||||
func (tab *Table) addIP(b *bucket, ip net.IP) bool {
|
||||
if len(ip) == 0 {
|
||||
return false // Nodes without IP cannot be added.
|
||||
|
|
@ -627,15 +500,52 @@ func (tab *Table) removeIP(b *bucket, ip net.IP) {
|
|||
b.ips.Remove(ip)
|
||||
}
|
||||
|
||||
func (tab *Table) addReplacement(b *bucket, n *node) {
|
||||
for _, e := range b.replacements {
|
||||
if e.ID() == n.ID() {
|
||||
return // already in list
|
||||
// handleAddNode adds the node in the request to the table, if there is space.
|
||||
// The caller must hold tab.mutex.
|
||||
func (tab *Table) handleAddNode(req addNodeOp) bool {
|
||||
if req.node.ID() == tab.self().ID() {
|
||||
return false
|
||||
}
|
||||
// For nodes from inbound contact, there is an additional safety measure: if the table
|
||||
// is still initializing the node is not added.
|
||||
if req.isInbound && !tab.isInitDone() {
|
||||
return false
|
||||
}
|
||||
|
||||
b := tab.bucket(req.node.ID())
|
||||
n, _ := tab.bumpInBucket(b, req.node.Node, req.isInbound)
|
||||
if n != nil {
|
||||
// Already in bucket.
|
||||
return false
|
||||
}
|
||||
if len(b.entries) >= bucketSize {
|
||||
// Bucket full, maybe add as replacement.
|
||||
tab.addReplacement(b, req.node)
|
||||
return false
|
||||
}
|
||||
if !tab.addIP(b, req.node.IP()) {
|
||||
// Can't add: IP limit reached.
|
||||
return false
|
||||
}
|
||||
|
||||
// Add to bucket.
|
||||
b.entries = append(b.entries, req.node)
|
||||
b.replacements = deleteNode(b.replacements, req.node)
|
||||
tab.nodeAdded(b, req.node)
|
||||
return true
|
||||
}
|
||||
|
||||
// addReplacement adds n to the replacement cache of bucket b.
|
||||
func (tab *Table) addReplacement(b *bucket, n *node) {
|
||||
if contains(b.replacements, n.ID()) {
|
||||
// TODO: update ENR
|
||||
return
|
||||
}
|
||||
if !tab.addIP(b, n.IP()) {
|
||||
return
|
||||
}
|
||||
|
||||
n.addedToTable = time.Now()
|
||||
var removed *node
|
||||
b.replacements, removed = pushNode(b.replacements, n, maxReplacements)
|
||||
if removed != nil {
|
||||
|
|
@ -643,60 +553,127 @@ func (tab *Table) addReplacement(b *bucket, n *node) {
|
|||
}
|
||||
}
|
||||
|
||||
// replace removes n from the replacement list and replaces 'last' with it if it is the
|
||||
// last entry in the bucket. If 'last' isn't the last entry, it has either been replaced
|
||||
// with someone else or became active.
|
||||
func (tab *Table) replace(b *bucket, last *node) *node {
|
||||
if len(b.entries) == 0 || b.entries[len(b.entries)-1].ID() != last.ID() {
|
||||
// Entry has moved, don't replace it.
|
||||
return nil
|
||||
func (tab *Table) nodeAdded(b *bucket, n *node) {
|
||||
if n.addedToTable == (time.Time{}) {
|
||||
n.addedToTable = time.Now()
|
||||
}
|
||||
// Still the last entry.
|
||||
if len(b.replacements) == 0 {
|
||||
tab.deleteInBucket(b, last)
|
||||
return nil
|
||||
n.addedToBucket = time.Now()
|
||||
tab.revalidation.nodeAdded(tab, n)
|
||||
if tab.nodeAddedHook != nil {
|
||||
tab.nodeAddedHook(b, n)
|
||||
}
|
||||
if metrics.Enabled {
|
||||
bucketsCounter[b.index].Inc(1)
|
||||
}
|
||||
r := b.replacements[tab.rand.Intn(len(b.replacements))]
|
||||
b.replacements = deleteNode(b.replacements, r)
|
||||
b.entries[len(b.entries)-1] = r
|
||||
tab.removeIP(b, last.IP())
|
||||
return r
|
||||
}
|
||||
|
||||
// bumpInBucket moves the given node to the front of the bucket entry list
|
||||
// if it is contained in that list.
|
||||
func (tab *Table) bumpInBucket(b *bucket, n *node) bool {
|
||||
for i := range b.entries {
|
||||
if b.entries[i].ID() == n.ID() {
|
||||
if !n.IP().Equal(b.entries[i].IP()) {
|
||||
// Endpoint has changed, ensure that the new IP fits into table limits.
|
||||
tab.removeIP(b, b.entries[i].IP())
|
||||
if !tab.addIP(b, n.IP()) {
|
||||
// It doesn't, put the previous one back.
|
||||
tab.addIP(b, b.entries[i].IP())
|
||||
return false
|
||||
}
|
||||
}
|
||||
// Move it to the front.
|
||||
copy(b.entries[1:], b.entries[:i])
|
||||
b.entries[0] = n
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (tab *Table) deleteInBucket(b *bucket, n *node) {
|
||||
// Check if the node is actually in the bucket so the removed hook
|
||||
// isn't called multiple times for the same node.
|
||||
if !contains(b.entries, n.ID()) {
|
||||
return
|
||||
}
|
||||
b.entries = deleteNode(b.entries, n)
|
||||
tab.removeIP(b, n.IP())
|
||||
func (tab *Table) nodeRemoved(b *bucket, n *node) {
|
||||
tab.revalidation.nodeRemoved(n)
|
||||
if tab.nodeRemovedHook != nil {
|
||||
tab.nodeRemovedHook(b, n)
|
||||
}
|
||||
if metrics.Enabled {
|
||||
bucketsCounter[b.index].Dec(1)
|
||||
}
|
||||
}
|
||||
|
||||
// deleteInBucket removes node n from the table.
|
||||
// If there are replacement nodes in the bucket, the node is replaced.
|
||||
func (tab *Table) deleteInBucket(b *bucket, id enode.ID) *node {
|
||||
index := slices.IndexFunc(b.entries, func(e *node) bool { return e.ID() == id })
|
||||
if index == -1 {
|
||||
// Entry has been removed already.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove the node.
|
||||
n := b.entries[index]
|
||||
b.entries = slices.Delete(b.entries, index, index+1)
|
||||
tab.removeIP(b, n.IP())
|
||||
tab.nodeRemoved(b, n)
|
||||
|
||||
// Add replacement.
|
||||
if len(b.replacements) == 0 {
|
||||
tab.log.Debug("Removed dead node", "b", b.index, "id", n.ID(), "ip", n.IP())
|
||||
return nil
|
||||
}
|
||||
rindex := tab.rand.Intn(len(b.replacements))
|
||||
rep := b.replacements[rindex]
|
||||
b.replacements = slices.Delete(b.replacements, rindex, rindex+1)
|
||||
b.entries = append(b.entries, rep)
|
||||
tab.nodeAdded(b, rep)
|
||||
tab.log.Debug("Replaced dead node", "b", b.index, "id", n.ID(), "ip", n.IP(), "r", rep.ID(), "rip", rep.IP())
|
||||
return rep
|
||||
}
|
||||
|
||||
// bumpInBucket updates a node record if it exists in the bucket.
|
||||
// The second return value reports whether the node's endpoint (IP/port) was updated.
|
||||
func (tab *Table) bumpInBucket(b *bucket, newRecord *enode.Node, isInbound bool) (n *node, endpointChanged bool) {
|
||||
i := slices.IndexFunc(b.entries, func(elem *node) bool {
|
||||
return elem.ID() == newRecord.ID()
|
||||
})
|
||||
if i == -1 {
|
||||
return nil, false // not in bucket
|
||||
}
|
||||
n = b.entries[i]
|
||||
|
||||
// For inbound updates (from the node itself) we accept any change, even if it sets
|
||||
// back the sequence number. For found nodes (!isInbound), seq has to advance. Note
|
||||
// this check also ensures found discv4 nodes (which always have seq=0) can't be
|
||||
// updated.
|
||||
if newRecord.Seq() <= n.Seq() && !isInbound {
|
||||
return n, false
|
||||
}
|
||||
|
||||
// Check endpoint update against IP limits.
|
||||
ipchanged := newRecord.IPAddr() != n.IPAddr()
|
||||
portchanged := newRecord.UDP() != n.UDP()
|
||||
if ipchanged {
|
||||
tab.removeIP(b, n.IP())
|
||||
if !tab.addIP(b, newRecord.IP()) {
|
||||
// It doesn't fit with the limit, put the previous record back.
|
||||
tab.addIP(b, n.IP())
|
||||
return n, false
|
||||
}
|
||||
}
|
||||
|
||||
// Apply update.
|
||||
n.Node = newRecord
|
||||
if ipchanged || portchanged {
|
||||
// Ensure node is revalidated quickly for endpoint changes.
|
||||
tab.revalidation.nodeEndpointChanged(tab, n)
|
||||
return n, true
|
||||
}
|
||||
return n, 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)
|
||||
}
|
||||
|
||||
tab.mutex.Lock()
|
||||
defer tab.mutex.Unlock()
|
||||
|
||||
b := tab.bucket(op.node.ID())
|
||||
// Remove the node from the local table if it fails to return anything useful too
|
||||
// many times, but only if there are enough other nodes in the bucket. This latter
|
||||
// condition specifically exists to make bootstrapping in smaller test networks more
|
||||
// reliable.
|
||||
if fails >= maxFindnodeFailures && len(b.entries) >= bucketSize/4 {
|
||||
tab.deleteInBucket(b, op.node.ID())
|
||||
}
|
||||
|
||||
// Add found nodes.
|
||||
for _, n := range op.foundNodes {
|
||||
tab.handleAddNode(addNodeOp{n, false})
|
||||
}
|
||||
}
|
||||
|
||||
func contains(ns []*node, id enode.ID) bool {
|
||||
|
|
|
|||
244
p2p/discover/table_reval.go
Normal file
244
p2p/discover/table_reval.go
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
// 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)
|
||||
|
||||
const slowRevalidationFactor = 3
|
||||
|
||||
// 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
|
||||
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 * slowRevalidationFactor
|
||||
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 n.revalList == nil {
|
||||
panic(fmt.Errorf("removed node %v has nil revalList", n.ID()))
|
||||
}
|
||||
n.revalList.remove(n)
|
||||
}
|
||||
|
||||
// nodeEndpointChanged is called when a change in IP or port is detected.
|
||||
func (tr *tableRevalidation) nodeEndpointChanged(tab *Table, n *node) {
|
||||
n.isValidatedLive = false
|
||||
tr.moveToList(&tr.fast, n, tab.cfg.Clock.Now(), &tab.rand)
|
||||
}
|
||||
|
||||
// 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, n)
|
||||
tr.fast.schedule(now, &tab.rand)
|
||||
}
|
||||
if n := tr.slow.get(now, &tab.rand, tr.activeReq); n != nil {
|
||||
tr.startRequest(tab, 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, n *node) {
|
||||
if _, ok := tr.activeReq[n.ID()]; ok {
|
||||
panic(fmt.Errorf("duplicate startRequest (node %v)", n.ID()))
|
||||
}
|
||||
tr.activeReq[n.ID()] = struct{}{}
|
||||
resp := revalidationResponse{n: n}
|
||||
|
||||
// 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) {
|
||||
var (
|
||||
now = tab.cfg.Clock.Now()
|
||||
n = resp.n
|
||||
b = tab.bucket(n.ID())
|
||||
)
|
||||
delete(tr.activeReq, n.ID())
|
||||
|
||||
// If the node was removed from the table while getting checked, we need to stop
|
||||
// processing here to avoid re-adding it.
|
||||
if n.revalList == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Store potential seeds in database.
|
||||
// This is done via defer to avoid holding Table lock while writing to DB.
|
||||
defer func() {
|
||||
if n.isValidatedLive && n.livenessChecks > 5 {
|
||||
tab.db.UpdateNode(resp.n.Node)
|
||||
}
|
||||
}()
|
||||
|
||||
// Remaining logic needs access to Table internals.
|
||||
tab.mutex.Lock()
|
||||
defer tab.mutex.Unlock()
|
||||
|
||||
if !resp.didRespond {
|
||||
n.livenessChecks /= 3
|
||||
if n.livenessChecks <= 0 {
|
||||
tab.deleteInBucket(b, n.ID())
|
||||
} else {
|
||||
tab.log.Debug("Node revalidation failed", "b", b.index, "id", n.ID(), "checks", n.livenessChecks, "q", n.revalList.name)
|
||||
tr.moveToList(&tr.fast, n, now, &tab.rand)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// The node responded.
|
||||
n.livenessChecks++
|
||||
n.isValidatedLive = true
|
||||
tab.log.Debug("Node revalidated", "b", b.index, "id", n.ID(), "checks", n.livenessChecks, "q", n.revalList.name)
|
||||
var endpointChanged bool
|
||||
if resp.newRecord != nil {
|
||||
_, endpointChanged = tab.bumpInBucket(b, resp.newRecord, false)
|
||||
}
|
||||
|
||||
// Node moves to slow list if it passed and hasn't changed.
|
||||
if !endpointChanged {
|
||||
tr.moveToList(&tr.slow, n, now, &tab.rand)
|
||||
}
|
||||
}
|
||||
|
||||
// moveToList ensures n is in the 'dest' list.
|
||||
func (tr *tableRevalidation) moveToList(dest *revalidationList, n *node, now mclock.AbsTime, rand randomSource) {
|
||||
if n.revalList == dest {
|
||||
return
|
||||
}
|
||||
if n.revalList != nil {
|
||||
n.revalList.remove(n)
|
||||
}
|
||||
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)
|
||||
}
|
||||
n.revalList = list
|
||||
}
|
||||
|
||||
func (list *revalidationList) remove(n *node) {
|
||||
i := slices.Index(list.nodes, n)
|
||||
if i == -1 {
|
||||
panic(fmt.Errorf("node %v not found in list", n.ID()))
|
||||
}
|
||||
list.nodes = slices.Delete(list.nodes, i, i+1)
|
||||
if len(list.nodes) == 0 {
|
||||
list.nextTime = never
|
||||
}
|
||||
n.revalList = nil
|
||||
}
|
||||
|
||||
func (list *revalidationList) contains(id enode.ID) bool {
|
||||
return slices.ContainsFunc(list.nodes, func(n *node) bool {
|
||||
return n.ID() == id
|
||||
})
|
||||
}
|
||||
119
p2p/discover/table_reval_test.go
Normal file
119
p2p/discover/table_reval_test.go
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
// 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 (
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common/mclock"
|
||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
||||
"github.com/ethereum/go-ethereum/p2p/enr"
|
||||
)
|
||||
|
||||
// This test checks that revalidation can handle a node disappearing while
|
||||
// a request is active.
|
||||
func TestRevalidation_nodeRemoved(t *testing.T) {
|
||||
var (
|
||||
clock mclock.Simulated
|
||||
transport = newPingRecorder()
|
||||
tab, db = newInactiveTestTable(transport, Config{Clock: &clock})
|
||||
tr = &tab.revalidation
|
||||
)
|
||||
defer db.Close()
|
||||
|
||||
// Add a node to the table.
|
||||
node := nodeAtDistance(tab.self().ID(), 255, net.IP{77, 88, 99, 1})
|
||||
tab.handleAddNode(addNodeOp{node: node})
|
||||
|
||||
// Start a revalidation request. Schedule once to get the next start time,
|
||||
// then advance the clock to that point and schedule again to start.
|
||||
next := tr.run(tab, clock.Now())
|
||||
clock.Run(time.Duration(next + 1))
|
||||
tr.run(tab, clock.Now())
|
||||
if len(tr.activeReq) != 1 {
|
||||
t.Fatal("revalidation request did not start:", tr.activeReq)
|
||||
}
|
||||
|
||||
// Delete the node.
|
||||
tab.deleteInBucket(tab.bucket(node.ID()), node.ID())
|
||||
|
||||
// Now finish the revalidation request.
|
||||
var resp revalidationResponse
|
||||
select {
|
||||
case resp = <-tab.revalResponseCh:
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Fatal("timed out waiting for revalidation")
|
||||
}
|
||||
tr.handleResponse(tab, resp)
|
||||
|
||||
// Ensure the node was not re-added to the table.
|
||||
if tab.getNode(node.ID()) != nil {
|
||||
t.Fatal("node was re-added to Table")
|
||||
}
|
||||
if tr.fast.contains(node.ID()) || tr.slow.contains(node.ID()) {
|
||||
t.Fatal("removed node contained in revalidation list")
|
||||
}
|
||||
}
|
||||
|
||||
// This test checks that nodes with an updated endpoint remain in the fast revalidation list.
|
||||
func TestRevalidation_endpointUpdate(t *testing.T) {
|
||||
var (
|
||||
clock mclock.Simulated
|
||||
transport = newPingRecorder()
|
||||
tab, db = newInactiveTestTable(transport, Config{Clock: &clock})
|
||||
tr = &tab.revalidation
|
||||
)
|
||||
defer db.Close()
|
||||
|
||||
// Add node to table.
|
||||
node := nodeAtDistance(tab.self().ID(), 255, net.IP{77, 88, 99, 1})
|
||||
tab.handleAddNode(addNodeOp{node: node})
|
||||
|
||||
// Update the record in transport, including endpoint update.
|
||||
record := node.Record()
|
||||
record.Set(enr.IP{100, 100, 100, 100})
|
||||
record.Set(enr.UDP(9999))
|
||||
nodev2 := enode.SignNull(record, node.ID())
|
||||
transport.updateRecord(nodev2)
|
||||
|
||||
// Start a revalidation request. Schedule once to get the next start time,
|
||||
// then advance the clock to that point and schedule again to start.
|
||||
next := tr.run(tab, clock.Now())
|
||||
clock.Run(time.Duration(next + 1))
|
||||
tr.run(tab, clock.Now())
|
||||
if len(tr.activeReq) != 1 {
|
||||
t.Fatal("revalidation request did not start:", tr.activeReq)
|
||||
}
|
||||
|
||||
// Now finish the revalidation request.
|
||||
var resp revalidationResponse
|
||||
select {
|
||||
case resp = <-tab.revalResponseCh:
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Fatal("timed out waiting for revalidation")
|
||||
}
|
||||
tr.handleResponse(tab, resp)
|
||||
|
||||
if !tr.fast.contains(node.ID()) {
|
||||
t.Fatal("node not contained in fast revalidation list")
|
||||
}
|
||||
if node.isValidatedLive {
|
||||
t.Fatal("node is marked live after endpoint change")
|
||||
}
|
||||
}
|
||||
|
|
@ -20,14 +20,16 @@ import (
|
|||
"crypto/ecdsa"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
|
||||
"net"
|
||||
"reflect"
|
||||
"testing"
|
||||
"testing/quick"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common/mclock"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/internal/testlog"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
||||
"github.com/ethereum/go-ethereum/p2p/enr"
|
||||
"github.com/ethereum/go-ethereum/p2p/netutil"
|
||||
|
|
@ -49,106 +51,109 @@ func TestTable_pingReplace(t *testing.T) {
|
|||
}
|
||||
|
||||
func testPingReplace(t *testing.T, newNodeIsResponding, lastInBucketIsResponding bool) {
|
||||
simclock := new(mclock.Simulated)
|
||||
transport := newPingRecorder()
|
||||
tab, db := newTestTable(transport)
|
||||
tab, db := newTestTable(transport, Config{
|
||||
Clock: simclock,
|
||||
Log: testlog.Logger(t, log.LevelTrace),
|
||||
})
|
||||
defer db.Close()
|
||||
defer tab.close()
|
||||
|
||||
<-tab.initDone
|
||||
|
||||
// Fill up the sender's bucket.
|
||||
pingKey, _ := crypto.HexToECDSA("45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8")
|
||||
pingSender := wrapNode(enode.NewV4(&pingKey.PublicKey, net.IP{127, 0, 0, 1}, 99, 99))
|
||||
last := fillBucket(tab, pingSender)
|
||||
replacementNodeKey, _ := crypto.HexToECDSA("45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8")
|
||||
replacementNode := wrapNode(enode.NewV4(&replacementNodeKey.PublicKey, net.IP{127, 0, 0, 1}, 99, 99))
|
||||
last := fillBucket(tab, replacementNode.ID())
|
||||
tab.mutex.Lock()
|
||||
nodeEvents := newNodeEventRecorder(128)
|
||||
tab.nodeAddedHook = nodeEvents.nodeAdded
|
||||
tab.nodeRemovedHook = nodeEvents.nodeRemoved
|
||||
tab.mutex.Unlock()
|
||||
|
||||
// Add the sender as if it just pinged us. Revalidate should replace the last node in
|
||||
// its bucket if it is unresponsive. Revalidate again to ensure that
|
||||
// The revalidation process should replace
|
||||
// this node in the bucket if it is unresponsive.
|
||||
transport.dead[last.ID()] = !lastInBucketIsResponding
|
||||
transport.dead[pingSender.ID()] = !newNodeIsResponding
|
||||
tab.addSeenNode(pingSender)
|
||||
tab.doRevalidate(make(chan struct{}, 1))
|
||||
tab.doRevalidate(make(chan struct{}, 1))
|
||||
transport.dead[replacementNode.ID()] = !newNodeIsResponding
|
||||
|
||||
if !transport.pinged[last.ID()] {
|
||||
// Oldest node in bucket is pinged to see whether it is still alive.
|
||||
t.Error("table did not ping last node in bucket")
|
||||
// Add replacement node to table.
|
||||
tab.addFoundNode(replacementNode)
|
||||
|
||||
t.Log("last:", last.ID())
|
||||
t.Log("replacement:", replacementNode.ID())
|
||||
|
||||
// Wait until the last node was pinged.
|
||||
waitForRevalidationPing(t, transport, tab, last.ID())
|
||||
|
||||
if !lastInBucketIsResponding {
|
||||
if !nodeEvents.waitNodeAbsent(last.ID(), 2*time.Second) {
|
||||
t.Error("last node was not removed")
|
||||
}
|
||||
if !nodeEvents.waitNodePresent(replacementNode.ID(), 2*time.Second) {
|
||||
t.Error("replacement node was not added")
|
||||
}
|
||||
|
||||
// If a replacement is expected, we also need to wait until the replacement node
|
||||
// was pinged and added/removed.
|
||||
waitForRevalidationPing(t, transport, tab, replacementNode.ID())
|
||||
if !newNodeIsResponding {
|
||||
if !nodeEvents.waitNodeAbsent(replacementNode.ID(), 2*time.Second) {
|
||||
t.Error("replacement node was not removed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check bucket content.
|
||||
tab.mutex.Lock()
|
||||
defer tab.mutex.Unlock()
|
||||
wantSize := bucketSize
|
||||
if !lastInBucketIsResponding && !newNodeIsResponding {
|
||||
wantSize--
|
||||
}
|
||||
if l := len(tab.bucket(pingSender.ID()).entries); l != wantSize {
|
||||
t.Errorf("wrong bucket size after bond: got %d, want %d", l, wantSize)
|
||||
bucket := tab.bucket(replacementNode.ID())
|
||||
if l := len(bucket.entries); l != wantSize {
|
||||
t.Errorf("wrong bucket size after revalidation: got %d, want %d", l, wantSize)
|
||||
}
|
||||
if found := contains(tab.bucket(pingSender.ID()).entries, last.ID()); found != lastInBucketIsResponding {
|
||||
t.Errorf("last entry found: %t, want: %t", found, lastInBucketIsResponding)
|
||||
if ok := contains(bucket.entries, last.ID()); ok != lastInBucketIsResponding {
|
||||
t.Errorf("revalidated node found: %t, want: %t", ok, lastInBucketIsResponding)
|
||||
}
|
||||
wantNewEntry := newNodeIsResponding && !lastInBucketIsResponding
|
||||
if found := contains(tab.bucket(pingSender.ID()).entries, pingSender.ID()); found != wantNewEntry {
|
||||
t.Errorf("new entry found: %t, want: %t", found, wantNewEntry)
|
||||
if ok := contains(bucket.entries, replacementNode.ID()); ok != wantNewEntry {
|
||||
t.Errorf("replacement node found: %t, want: %t", ok, wantNewEntry)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBucket_bumpNoDuplicates(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := &quick.Config{
|
||||
MaxCount: 1000,
|
||||
Rand: rand.New(rand.NewSource(time.Now().Unix())),
|
||||
Values: func(args []reflect.Value, rand *rand.Rand) {
|
||||
// generate a random list of nodes. this will be the content of the bucket.
|
||||
n := rand.Intn(bucketSize-1) + 1
|
||||
nodes := make([]*node, n)
|
||||
for i := range nodes {
|
||||
nodes[i] = nodeAtDistance(enode.ID{}, 200, intIP(200))
|
||||
}
|
||||
args[0] = reflect.ValueOf(nodes)
|
||||
// generate random bump positions.
|
||||
bumps := make([]int, rand.Intn(100))
|
||||
for i := range bumps {
|
||||
bumps[i] = rand.Intn(len(nodes))
|
||||
}
|
||||
args[1] = reflect.ValueOf(bumps)
|
||||
},
|
||||
}
|
||||
// waitForRevalidationPing waits until a PING message is sent to a node with the given id.
|
||||
func waitForRevalidationPing(t *testing.T, transport *pingRecorder, tab *Table, id enode.ID) *enode.Node {
|
||||
t.Helper()
|
||||
|
||||
prop := func(nodes []*node, bumps []int) (ok bool) {
|
||||
tab, db := newTestTable(newPingRecorder())
|
||||
defer db.Close()
|
||||
defer tab.close()
|
||||
|
||||
b := &bucket{entries: make([]*node, len(nodes))}
|
||||
copy(b.entries, nodes)
|
||||
for i, pos := range bumps {
|
||||
tab.bumpInBucket(b, b.entries[pos])
|
||||
if hasDuplicates(b.entries) {
|
||||
t.Logf("bucket has duplicates after %d/%d bumps:", i+1, len(bumps))
|
||||
for _, n := range b.entries {
|
||||
t.Logf(" %p", n)
|
||||
simclock := tab.cfg.Clock.(*mclock.Simulated)
|
||||
maxAttempts := tab.len() * 8
|
||||
for i := 0; i < maxAttempts; i++ {
|
||||
simclock.Run(tab.cfg.PingInterval * slowRevalidationFactor)
|
||||
p := transport.waitPing(2 * time.Second)
|
||||
if p == nil {
|
||||
t.Fatal("Table did not send revalidation ping")
|
||||
}
|
||||
return false
|
||||
if id == (enode.ID{}) || p.ID() == id {
|
||||
return p
|
||||
}
|
||||
}
|
||||
checkIPLimitInvariant(t, tab)
|
||||
return true
|
||||
}
|
||||
if err := quick.Check(prop, cfg); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
t.Fatalf("Table did not ping node %v (%d attempts)", id, maxAttempts)
|
||||
return nil
|
||||
}
|
||||
|
||||
// This checks that the table-wide IP limit is applied correctly.
|
||||
func TestTable_IPLimit(t *testing.T) {
|
||||
transport := newPingRecorder()
|
||||
tab, db := newTestTable(transport)
|
||||
tab, db := newTestTable(transport, Config{})
|
||||
defer db.Close()
|
||||
defer tab.close()
|
||||
|
||||
for i := 0; i < tableIPLimit+1; i++ {
|
||||
n := nodeAtDistance(tab.self().ID(), i, net.IP{172, 0, 1, byte(i)})
|
||||
tab.addSeenNode(n)
|
||||
tab.addFoundNode(n)
|
||||
}
|
||||
if tab.len() > tableIPLimit {
|
||||
t.Errorf("too many nodes in table")
|
||||
|
|
@ -159,14 +164,14 @@ func TestTable_IPLimit(t *testing.T) {
|
|||
// This checks that the per-bucket IP limit is applied correctly.
|
||||
func TestTable_BucketIPLimit(t *testing.T) {
|
||||
transport := newPingRecorder()
|
||||
tab, db := newTestTable(transport)
|
||||
tab, db := newTestTable(transport, Config{})
|
||||
defer db.Close()
|
||||
defer tab.close()
|
||||
|
||||
d := 3
|
||||
for i := 0; i < bucketIPLimit+1; i++ {
|
||||
n := nodeAtDistance(tab.self().ID(), d, net.IP{172, 0, 1, byte(i)})
|
||||
tab.addSeenNode(n)
|
||||
tab.addFoundNode(n)
|
||||
}
|
||||
if tab.len() > bucketIPLimit {
|
||||
t.Errorf("too many nodes in table")
|
||||
|
|
@ -196,7 +201,7 @@ func TestTable_findnodeByID(t *testing.T) {
|
|||
test := func(test *closeTest) bool {
|
||||
// for any node table, Target and N
|
||||
transport := newPingRecorder()
|
||||
tab, db := newTestTable(transport)
|
||||
tab, db := newTestTable(transport, Config{})
|
||||
defer db.Close()
|
||||
defer tab.close()
|
||||
fillTable(tab, test.All, true)
|
||||
|
|
@ -270,8 +275,8 @@ func (*closeTest) Generate(rand *rand.Rand, size int) reflect.Value {
|
|||
return reflect.ValueOf(t)
|
||||
}
|
||||
|
||||
func TestTable_addVerifiedNode(t *testing.T) {
|
||||
tab, db := newTestTable(newPingRecorder())
|
||||
func TestTable_addInboundNode(t *testing.T) {
|
||||
tab, db := newTestTable(newPingRecorder(), Config{})
|
||||
<-tab.initDone
|
||||
defer db.Close()
|
||||
defer tab.close()
|
||||
|
|
@ -279,31 +284,29 @@ func TestTable_addVerifiedNode(t *testing.T) {
|
|||
// Insert two nodes.
|
||||
n1 := nodeAtDistance(tab.self().ID(), 256, net.IP{88, 77, 66, 1})
|
||||
n2 := nodeAtDistance(tab.self().ID(), 256, net.IP{88, 77, 66, 2})
|
||||
tab.addSeenNode(n1)
|
||||
tab.addSeenNode(n2)
|
||||
tab.addFoundNode(n1)
|
||||
tab.addFoundNode(n2)
|
||||
checkBucketContent(t, tab, []*enode.Node{n1.Node, n2.Node})
|
||||
|
||||
// Verify bucket content:
|
||||
bcontent := []*node{n1, n2}
|
||||
if !reflect.DeepEqual(tab.bucket(n1.ID()).entries, bcontent) {
|
||||
t.Fatalf("wrong bucket content: %v", tab.bucket(n1.ID()).entries)
|
||||
}
|
||||
|
||||
// Add a changed version of n2.
|
||||
// Add a changed version of n2. The bucket should be updated.
|
||||
newrec := n2.Record()
|
||||
newrec.Set(enr.IP{99, 99, 99, 99})
|
||||
newn2 := wrapNode(enode.SignNull(newrec, n2.ID()))
|
||||
tab.addVerifiedNode(newn2)
|
||||
n2v2 := enode.SignNull(newrec, n2.ID())
|
||||
tab.addInboundNode(wrapNode(n2v2))
|
||||
checkBucketContent(t, tab, []*enode.Node{n1.Node, n2v2})
|
||||
|
||||
// Check that bucket is updated correctly.
|
||||
newBcontent := []*node{newn2, n1}
|
||||
if !reflect.DeepEqual(tab.bucket(n1.ID()).entries, newBcontent) {
|
||||
t.Fatalf("wrong bucket content after update: %v", tab.bucket(n1.ID()).entries)
|
||||
}
|
||||
checkIPLimitInvariant(t, tab)
|
||||
// Try updating n2 without sequence number change. The update is accepted
|
||||
// because it's inbound.
|
||||
newrec = n2.Record()
|
||||
newrec.Set(enr.IP{100, 100, 100, 100})
|
||||
newrec.SetSeq(n2.Seq())
|
||||
n2v3 := enode.SignNull(newrec, n2.ID())
|
||||
tab.addInboundNode(wrapNode(n2v3))
|
||||
checkBucketContent(t, tab, []*enode.Node{n1.Node, n2v3})
|
||||
}
|
||||
|
||||
func TestTable_addSeenNode(t *testing.T) {
|
||||
tab, db := newTestTable(newPingRecorder())
|
||||
func TestTable_addFoundNode(t *testing.T) {
|
||||
tab, db := newTestTable(newPingRecorder(), Config{})
|
||||
<-tab.initDone
|
||||
defer db.Close()
|
||||
defer tab.close()
|
||||
|
|
@ -311,25 +314,86 @@ func TestTable_addSeenNode(t *testing.T) {
|
|||
// Insert two nodes.
|
||||
n1 := nodeAtDistance(tab.self().ID(), 256, net.IP{88, 77, 66, 1})
|
||||
n2 := nodeAtDistance(tab.self().ID(), 256, net.IP{88, 77, 66, 2})
|
||||
tab.addSeenNode(n1)
|
||||
tab.addSeenNode(n2)
|
||||
tab.addFoundNode(n1)
|
||||
tab.addFoundNode(n2)
|
||||
checkBucketContent(t, tab, []*enode.Node{n1.Node, n2.Node})
|
||||
|
||||
// Verify bucket content:
|
||||
bcontent := []*node{n1, n2}
|
||||
if !reflect.DeepEqual(tab.bucket(n1.ID()).entries, bcontent) {
|
||||
t.Fatalf("wrong bucket content: %v", tab.bucket(n1.ID()).entries)
|
||||
}
|
||||
|
||||
// Add a changed version of n2.
|
||||
// Add a changed version of n2. The bucket should be updated.
|
||||
newrec := n2.Record()
|
||||
newrec.Set(enr.IP{99, 99, 99, 99})
|
||||
newn2 := wrapNode(enode.SignNull(newrec, n2.ID()))
|
||||
tab.addSeenNode(newn2)
|
||||
n2v2 := enode.SignNull(newrec, n2.ID())
|
||||
tab.addFoundNode(wrapNode(n2v2))
|
||||
checkBucketContent(t, tab, []*enode.Node{n1.Node, n2v2})
|
||||
|
||||
// Check that bucket content is unchanged.
|
||||
if !reflect.DeepEqual(tab.bucket(n1.ID()).entries, bcontent) {
|
||||
t.Fatalf("wrong bucket content after update: %v", tab.bucket(n1.ID()).entries)
|
||||
// Try updating n2 without a sequence number change.
|
||||
// The update should not be accepted.
|
||||
newrec = n2.Record()
|
||||
newrec.Set(enr.IP{100, 100, 100, 100})
|
||||
newrec.SetSeq(n2.Seq())
|
||||
n2v3 := enode.SignNull(newrec, n2.ID())
|
||||
tab.addFoundNode(wrapNode(n2v3))
|
||||
checkBucketContent(t, tab, []*enode.Node{n1.Node, n2v2})
|
||||
}
|
||||
|
||||
// This test checks that discv4 nodes can update their own endpoint via PING.
|
||||
func TestTable_addInboundNodeUpdateV4Accept(t *testing.T) {
|
||||
tab, db := newTestTable(newPingRecorder(), Config{})
|
||||
<-tab.initDone
|
||||
defer db.Close()
|
||||
defer tab.close()
|
||||
|
||||
// Add a v4 node.
|
||||
key, _ := crypto.HexToECDSA("dd3757a8075e88d0f2b1431e7d3c5b1562e1c0aab9643707e8cbfcc8dae5cfe3")
|
||||
n1 := enode.NewV4(&key.PublicKey, net.IP{88, 77, 66, 1}, 9000, 9000)
|
||||
tab.addInboundNode(wrapNode(n1))
|
||||
checkBucketContent(t, tab, []*enode.Node{n1})
|
||||
|
||||
// Add an updated version with changed IP.
|
||||
// The update will be accepted because it is inbound.
|
||||
n1v2 := enode.NewV4(&key.PublicKey, net.IP{99, 99, 99, 99}, 9000, 9000)
|
||||
tab.addInboundNode(wrapNode(n1v2))
|
||||
checkBucketContent(t, tab, []*enode.Node{n1v2})
|
||||
}
|
||||
|
||||
// This test checks that discv4 node entries will NOT be updated when a
|
||||
// changed record is found.
|
||||
func TestTable_addFoundNodeV4UpdateReject(t *testing.T) {
|
||||
tab, db := newTestTable(newPingRecorder(), Config{})
|
||||
<-tab.initDone
|
||||
defer db.Close()
|
||||
defer tab.close()
|
||||
|
||||
// Add a v4 node.
|
||||
key, _ := crypto.HexToECDSA("dd3757a8075e88d0f2b1431e7d3c5b1562e1c0aab9643707e8cbfcc8dae5cfe3")
|
||||
n1 := enode.NewV4(&key.PublicKey, net.IP{88, 77, 66, 1}, 9000, 9000)
|
||||
tab.addFoundNode(wrapNode(n1))
|
||||
checkBucketContent(t, tab, []*enode.Node{n1})
|
||||
|
||||
// Add an updated version with changed IP.
|
||||
// The update won't be accepted because it isn't inbound.
|
||||
n1v2 := enode.NewV4(&key.PublicKey, net.IP{99, 99, 99, 99}, 9000, 9000)
|
||||
tab.addFoundNode(wrapNode(n1v2))
|
||||
checkBucketContent(t, tab, []*enode.Node{n1})
|
||||
}
|
||||
|
||||
func checkBucketContent(t *testing.T, tab *Table, nodes []*enode.Node) {
|
||||
t.Helper()
|
||||
|
||||
b := tab.bucket(nodes[0].ID())
|
||||
if reflect.DeepEqual(unwrapNodes(b.entries), nodes) {
|
||||
return
|
||||
}
|
||||
t.Log("wrong bucket content. have nodes:")
|
||||
for _, n := range b.entries {
|
||||
t.Logf(" %v (seq=%v, ip=%v)", n.ID(), n.Seq(), n.IP())
|
||||
}
|
||||
t.Log("want nodes:")
|
||||
for _, n := range nodes {
|
||||
t.Logf(" %v (seq=%v, ip=%v)", n.ID(), n.Seq(), n.IP())
|
||||
}
|
||||
t.FailNow()
|
||||
|
||||
// Also check IP limits.
|
||||
checkIPLimitInvariant(t, tab)
|
||||
}
|
||||
|
||||
|
|
@ -337,7 +401,10 @@ func TestTable_addSeenNode(t *testing.T) {
|
|||
// announces a new sequence number, the new record should be pulled.
|
||||
func TestTable_revalidateSyncRecord(t *testing.T) {
|
||||
transport := newPingRecorder()
|
||||
tab, db := newTestTable(transport)
|
||||
tab, db := newTestTable(transport, Config{
|
||||
Clock: new(mclock.Simulated),
|
||||
Log: testlog.Logger(t, log.LevelTrace),
|
||||
})
|
||||
<-tab.initDone
|
||||
defer db.Close()
|
||||
defer tab.close()
|
||||
|
|
@ -347,14 +414,18 @@ func TestTable_revalidateSyncRecord(t *testing.T) {
|
|||
r.Set(enr.IP(net.IP{127, 0, 0, 1}))
|
||||
id := enode.ID{1}
|
||||
n1 := wrapNode(enode.SignNull(&r, id))
|
||||
tab.addSeenNode(n1)
|
||||
tab.addFoundNode(n1)
|
||||
|
||||
// Update the node record.
|
||||
r.Set(enr.WithEntry("foo", "bar"))
|
||||
n2 := enode.SignNull(&r, id)
|
||||
transport.updateRecord(n2)
|
||||
|
||||
tab.doRevalidate(make(chan struct{}, 1))
|
||||
// Wait for revalidation. We wait for the node to be revalidated two times
|
||||
// in order to synchronize with the update in the table.
|
||||
waitForRevalidationPing(t, transport, tab, n2.ID())
|
||||
waitForRevalidationPing(t, transport, tab, n2.ID())
|
||||
|
||||
intable := tab.getNode(id)
|
||||
if !reflect.DeepEqual(intable, n2) {
|
||||
t.Fatalf("table contains old record with seq %d, want seq %d", intable.Seq(), n2.Seq())
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ import (
|
|||
"net"
|
||||
"slices"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
||||
|
|
@ -40,11 +42,16 @@ func init() {
|
|||
nullNode = enode.SignNull(&r, enode.ID{})
|
||||
}
|
||||
|
||||
func newTestTable(t transport) (*Table, *enode.DB) {
|
||||
cfg := Config{}
|
||||
func newTestTable(t transport, cfg Config) (*Table, *enode.DB) {
|
||||
tab, db := newInactiveTestTable(t, cfg)
|
||||
go tab.loop()
|
||||
return tab, db
|
||||
}
|
||||
|
||||
// newInactiveTestTable creates a Table without running the main loop.
|
||||
func newInactiveTestTable(t transport, cfg Config) (*Table, *enode.DB) {
|
||||
db, _ := enode.OpenDB("")
|
||||
tab, _ := newTable(t, db, cfg)
|
||||
go tab.loop()
|
||||
return tab, db
|
||||
}
|
||||
|
||||
|
|
@ -98,11 +105,14 @@ func intIP(i int) net.IP {
|
|||
}
|
||||
|
||||
// fillBucket inserts nodes into the given bucket until it is full.
|
||||
func fillBucket(tab *Table, n *node) (last *node) {
|
||||
ld := enode.LogDist(tab.self().ID(), n.ID())
|
||||
b := tab.bucket(n.ID())
|
||||
func fillBucket(tab *Table, id enode.ID) (last *node) {
|
||||
ld := enode.LogDist(tab.self().ID(), id)
|
||||
b := tab.bucket(id)
|
||||
for len(b.entries) < bucketSize {
|
||||
b.entries = append(b.entries, nodeAtDistance(tab.self().ID(), ld, intIP(ld)))
|
||||
node := nodeAtDistance(tab.self().ID(), ld, intIP(ld))
|
||||
if !tab.addFoundNode(node) {
|
||||
panic("node not added")
|
||||
}
|
||||
}
|
||||
return b.entries[bucketSize-1]
|
||||
}
|
||||
|
|
@ -113,15 +123,18 @@ func fillTable(tab *Table, nodes []*node, setLive bool) {
|
|||
for _, n := range nodes {
|
||||
if setLive {
|
||||
n.livenessChecks = 1
|
||||
n.isValidatedLive = true
|
||||
}
|
||||
tab.addSeenNode(n)
|
||||
tab.addFoundNode(n)
|
||||
}
|
||||
}
|
||||
|
||||
type pingRecorder struct {
|
||||
mu sync.Mutex
|
||||
dead, pinged map[enode.ID]bool
|
||||
cond *sync.Cond
|
||||
dead map[enode.ID]bool
|
||||
records map[enode.ID]*enode.Node
|
||||
pinged []*enode.Node
|
||||
n *enode.Node
|
||||
}
|
||||
|
||||
|
|
@ -130,12 +143,13 @@ func newPingRecorder() *pingRecorder {
|
|||
r.Set(enr.IP{0, 0, 0, 0})
|
||||
n := enode.SignNull(&r, enode.ID{})
|
||||
|
||||
return &pingRecorder{
|
||||
t := &pingRecorder{
|
||||
dead: make(map[enode.ID]bool),
|
||||
pinged: make(map[enode.ID]bool),
|
||||
records: make(map[enode.ID]*enode.Node),
|
||||
n: n,
|
||||
}
|
||||
t.cond = sync.NewCond(&t.mu)
|
||||
return t
|
||||
}
|
||||
|
||||
// updateRecord updates a node record. Future calls to ping and
|
||||
|
|
@ -151,12 +165,40 @@ func (t *pingRecorder) Self() *enode.Node { return nullNode }
|
|||
func (t *pingRecorder) lookupSelf() []*enode.Node { return nil }
|
||||
func (t *pingRecorder) lookupRandom() []*enode.Node { return nil }
|
||||
|
||||
func (t *pingRecorder) waitPing(timeout time.Duration) *enode.Node {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
// Wake up the loop on timeout.
|
||||
var timedout atomic.Bool
|
||||
timer := time.AfterFunc(timeout, func() {
|
||||
timedout.Store(true)
|
||||
t.cond.Broadcast()
|
||||
})
|
||||
defer timer.Stop()
|
||||
|
||||
// Wait for a ping.
|
||||
for {
|
||||
if timedout.Load() {
|
||||
return nil
|
||||
}
|
||||
if len(t.pinged) > 0 {
|
||||
n := t.pinged[0]
|
||||
t.pinged = append(t.pinged[:0], t.pinged[1:]...)
|
||||
return n
|
||||
}
|
||||
t.cond.Wait()
|
||||
}
|
||||
}
|
||||
|
||||
// ping simulates a ping request.
|
||||
func (t *pingRecorder) ping(n *enode.Node) (seq uint64, err error) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
t.pinged[n.ID()] = true
|
||||
t.pinged = append(t.pinged, n)
|
||||
t.cond.Broadcast()
|
||||
|
||||
if t.dead[n.ID()] {
|
||||
return 0, errTimeout
|
||||
}
|
||||
|
|
@ -256,3 +298,57 @@ func hexEncPubkey(h string) (ret encPubkey) {
|
|||
copy(ret[:], b)
|
||||
return ret
|
||||
}
|
||||
|
||||
type nodeEventRecorder struct {
|
||||
evc chan recordedNodeEvent
|
||||
}
|
||||
|
||||
type recordedNodeEvent struct {
|
||||
node *node
|
||||
added bool
|
||||
}
|
||||
|
||||
func newNodeEventRecorder(buffer int) *nodeEventRecorder {
|
||||
return &nodeEventRecorder{
|
||||
evc: make(chan recordedNodeEvent, buffer),
|
||||
}
|
||||
}
|
||||
|
||||
func (set *nodeEventRecorder) nodeAdded(b *bucket, n *node) {
|
||||
select {
|
||||
case set.evc <- recordedNodeEvent{n, true}:
|
||||
default:
|
||||
panic("no space in event buffer")
|
||||
}
|
||||
}
|
||||
|
||||
func (set *nodeEventRecorder) nodeRemoved(b *bucket, n *node) {
|
||||
select {
|
||||
case set.evc <- recordedNodeEvent{n, false}:
|
||||
default:
|
||||
panic("no space in event buffer")
|
||||
}
|
||||
}
|
||||
|
||||
func (set *nodeEventRecorder) waitNodePresent(id enode.ID, timeout time.Duration) bool {
|
||||
return set.waitNodeEvent(id, timeout, true)
|
||||
}
|
||||
|
||||
func (set *nodeEventRecorder) waitNodeAbsent(id enode.ID, timeout time.Duration) bool {
|
||||
return set.waitNodeEvent(id, timeout, false)
|
||||
}
|
||||
|
||||
func (set *nodeEventRecorder) waitNodeEvent(id enode.ID, timeout time.Duration, added bool) bool {
|
||||
timer := time.NewTimer(timeout)
|
||||
defer timer.Stop()
|
||||
for {
|
||||
select {
|
||||
case ev := <-set.evc:
|
||||
if ev.node.ID() == id && ev.added == added {
|
||||
return true
|
||||
}
|
||||
case <-timer.C:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ func ListenV4(c UDPConn, ln *enode.LocalNode, cfg Config) (*UDPv4, error) {
|
|||
log: cfg.Log,
|
||||
}
|
||||
|
||||
tab, err := newMeteredTable(t, ln.Database(), cfg)
|
||||
tab, err := newTable(t, ln.Database(), cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -375,6 +375,10 @@ func (t *UDPv4) RequestENR(n *enode.Node) (*enode.Node, error) {
|
|||
return respN, nil
|
||||
}
|
||||
|
||||
func (t *UDPv4) TableBuckets() [][]BucketNode {
|
||||
return t.tab.Nodes()
|
||||
}
|
||||
|
||||
// pending adds a reply matcher to the pending reply queue.
|
||||
// see the documentation of type replyMatcher for a detailed explanation.
|
||||
func (t *UDPv4) pending(id enode.ID, ip net.IP, ptype byte, callback replyMatchFunc) *replyMatcher {
|
||||
|
|
@ -669,10 +673,10 @@ func (t *UDPv4) handlePing(h *packetHandlerV4, from *net.UDPAddr, fromID enode.I
|
|||
n := wrapNode(enode.NewV4(h.senderKey, from.IP, int(req.From.TCP), from.Port))
|
||||
if time.Since(t.db.LastPongReceived(n.ID(), from.IP)) > bondExpiration {
|
||||
t.sendPing(fromID, from, func() {
|
||||
t.tab.addVerifiedNode(n)
|
||||
t.tab.addInboundNode(n)
|
||||
})
|
||||
} else {
|
||||
t.tab.addVerifiedNode(n)
|
||||
t.tab.addInboundNode(n)
|
||||
}
|
||||
|
||||
// Update node database and endpoint predictor.
|
||||
|
|
|
|||
|
|
@ -264,7 +264,7 @@ func TestUDPv4_findnode(t *testing.T) {
|
|||
n := wrapNode(enode.NewV4(&key.PublicKey, ip, 0, 2000))
|
||||
// Ensure half of table content isn't verified live yet.
|
||||
if i > numCandidates/2 {
|
||||
n.livenessChecks = 1
|
||||
n.isValidatedLive = true
|
||||
live[n.ID()] = true
|
||||
}
|
||||
nodes.push(n, numCandidates)
|
||||
|
|
|
|||
|
|
@ -175,7 +175,7 @@ func newUDPv5(conn UDPConn, ln *enode.LocalNode, cfg Config) (*UDPv5, error) {
|
|||
cancelCloseCtx: cancelCloseCtx,
|
||||
}
|
||||
t.talk = newTalkSystem(t)
|
||||
tab, err := newMeteredTable(t, t.db, cfg)
|
||||
tab, err := newTable(t, t.db, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -699,7 +699,7 @@ func (t *UDPv5) handlePacket(rawpacket []byte, fromAddr *net.UDPAddr) error {
|
|||
}
|
||||
if fromNode != nil {
|
||||
// Handshake succeeded, add to table.
|
||||
t.tab.addSeenNode(wrapNode(fromNode))
|
||||
t.tab.addInboundNode(wrapNode(fromNode))
|
||||
}
|
||||
if packet.Kind() != v5wire.WhoareyouPacket {
|
||||
// WHOAREYOU logged separately to report errors.
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ func TestUDPv5_unknownPacket(t *testing.T) {
|
|||
|
||||
// Make node known.
|
||||
n := test.getNode(test.remotekey, test.remoteaddr).Node()
|
||||
test.table.addSeenNode(wrapNode(n))
|
||||
test.table.addFoundNode(wrapNode(n))
|
||||
|
||||
test.packetIn(&v5wire.Unknown{Nonce: nonce})
|
||||
test.waitPacketOut(func(p *v5wire.Whoareyou, addr *net.UDPAddr, _ v5wire.Nonce) {
|
||||
|
|
|
|||
|
|
@ -157,5 +157,5 @@ func SignNull(r *enr.Record, id ID) *Node {
|
|||
if err := r.SetSig(NullID{}, []byte{}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &Node{r: *r, id: id}
|
||||
return newNodeWithID(r, id)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import (
|
|||
"fmt"
|
||||
"math/bits"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/p2p/enr"
|
||||
|
|
@ -36,6 +37,10 @@ var errMissingPrefix = errors.New("missing 'enr:' prefix for base64-encoded reco
|
|||
type Node struct {
|
||||
r enr.Record
|
||||
id ID
|
||||
// endpoint information
|
||||
ip netip.Addr
|
||||
udp uint16
|
||||
tcp uint16
|
||||
}
|
||||
|
||||
// New wraps a node record. The record must be valid according to the given
|
||||
|
|
@ -44,11 +49,76 @@ func New(validSchemes enr.IdentityScheme, r *enr.Record) (*Node, error) {
|
|||
if err := r.VerifySignature(validSchemes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
node := &Node{r: *r}
|
||||
if n := copy(node.id[:], validSchemes.NodeAddr(&node.r)); n != len(ID{}) {
|
||||
return nil, fmt.Errorf("invalid node ID length %d, need %d", n, len(ID{}))
|
||||
var id ID
|
||||
if n := copy(id[:], validSchemes.NodeAddr(r)); n != len(id) {
|
||||
return nil, fmt.Errorf("invalid node ID length %d, need %d", n, len(id))
|
||||
}
|
||||
return newNodeWithID(r, id), nil
|
||||
}
|
||||
|
||||
func newNodeWithID(r *enr.Record, id ID) *Node {
|
||||
n := &Node{r: *r, id: id}
|
||||
// Set the preferred endpoint.
|
||||
// Here we decide between IPv4 and IPv6, choosing the 'most global' address.
|
||||
var ip4 netip.Addr
|
||||
var ip6 netip.Addr
|
||||
n.Load((*enr.IPv4Addr)(&ip4))
|
||||
n.Load((*enr.IPv6Addr)(&ip6))
|
||||
valid4 := validIP(ip4)
|
||||
valid6 := validIP(ip6)
|
||||
switch {
|
||||
case valid4 && valid6:
|
||||
if localityScore(ip4) >= localityScore(ip6) {
|
||||
n.setIP4(ip4)
|
||||
} else {
|
||||
n.setIP6(ip6)
|
||||
}
|
||||
case valid4:
|
||||
n.setIP4(ip4)
|
||||
case valid6:
|
||||
n.setIP6(ip6)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// validIP reports whether 'ip' is a valid node endpoint IP address.
|
||||
func validIP(ip netip.Addr) bool {
|
||||
return ip.IsValid() && !ip.IsMulticast()
|
||||
}
|
||||
|
||||
func localityScore(ip netip.Addr) int {
|
||||
switch {
|
||||
case ip.IsUnspecified():
|
||||
return 0
|
||||
case ip.IsLoopback():
|
||||
return 1
|
||||
case ip.IsLinkLocalUnicast():
|
||||
return 2
|
||||
case ip.IsPrivate():
|
||||
return 3
|
||||
default:
|
||||
return 4
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Node) setIP4(ip netip.Addr) {
|
||||
n.ip = ip
|
||||
n.Load((*enr.UDP)(&n.udp))
|
||||
n.Load((*enr.TCP)(&n.tcp))
|
||||
}
|
||||
|
||||
func (n *Node) setIP6(ip netip.Addr) {
|
||||
if ip.Is4In6() {
|
||||
n.setIP4(ip)
|
||||
return
|
||||
}
|
||||
n.ip = ip
|
||||
if err := n.Load((*enr.UDP6)(&n.udp)); err != nil {
|
||||
n.Load((*enr.UDP)(&n.udp))
|
||||
}
|
||||
if err := n.Load((*enr.TCP6)(&n.tcp)); err != nil {
|
||||
n.Load((*enr.TCP)(&n.tcp))
|
||||
}
|
||||
return node, nil
|
||||
}
|
||||
|
||||
// MustParse parses a node record or enode:// URL. It panics if the input is invalid.
|
||||
|
|
@ -89,43 +159,45 @@ func (n *Node) Seq() uint64 {
|
|||
return n.r.Seq()
|
||||
}
|
||||
|
||||
// Incomplete returns true for nodes with no IP address.
|
||||
func (n *Node) Incomplete() bool {
|
||||
return n.IP() == nil
|
||||
}
|
||||
|
||||
// Load retrieves an entry from the underlying record.
|
||||
func (n *Node) Load(k enr.Entry) error {
|
||||
return n.r.Load(k)
|
||||
}
|
||||
|
||||
// IP returns the IP address of the node. This prefers IPv4 addresses.
|
||||
// IP returns the IP address of the node.
|
||||
func (n *Node) IP() net.IP {
|
||||
var (
|
||||
ip4 enr.IPv4
|
||||
ip6 enr.IPv6
|
||||
)
|
||||
if n.Load(&ip4) == nil {
|
||||
return net.IP(ip4)
|
||||
}
|
||||
if n.Load(&ip6) == nil {
|
||||
return net.IP(ip6)
|
||||
}
|
||||
return nil
|
||||
return net.IP(n.ip.AsSlice())
|
||||
}
|
||||
|
||||
// IPAddr returns the IP address of the node.
|
||||
func (n *Node) IPAddr() netip.Addr {
|
||||
return n.ip
|
||||
}
|
||||
|
||||
// UDP returns the UDP port of the node.
|
||||
func (n *Node) UDP() int {
|
||||
var port enr.UDP
|
||||
n.Load(&port)
|
||||
return int(port)
|
||||
return int(n.udp)
|
||||
}
|
||||
|
||||
// TCP returns the TCP port of the node.
|
||||
func (n *Node) TCP() int {
|
||||
var port enr.TCP
|
||||
n.Load(&port)
|
||||
return int(port)
|
||||
return int(n.tcp)
|
||||
}
|
||||
|
||||
// UDPEndpoint returns the announced UDP endpoint.
|
||||
func (n *Node) UDPEndpoint() (netip.AddrPort, bool) {
|
||||
if !n.ip.IsValid() || n.ip.IsUnspecified() || n.udp == 0 {
|
||||
return netip.AddrPort{}, false
|
||||
}
|
||||
return netip.AddrPortFrom(n.ip, n.udp), true
|
||||
}
|
||||
|
||||
// TCPEndpoint returns the announced TCP endpoint.
|
||||
func (n *Node) TCPEndpoint() (netip.AddrPort, bool) {
|
||||
if !n.ip.IsValid() || n.ip.IsUnspecified() || n.tcp == 0 {
|
||||
return netip.AddrPort{}, false
|
||||
}
|
||||
return netip.AddrPortFrom(n.ip, n.tcp), true
|
||||
}
|
||||
|
||||
// Pubkey returns the secp256k1 public key of the node, if present.
|
||||
|
|
@ -147,16 +219,15 @@ func (n *Node) Record() *enr.Record {
|
|||
// ValidateComplete checks whether n has a valid IP and UDP port.
|
||||
// Deprecated: don't use this method.
|
||||
func (n *Node) ValidateComplete() error {
|
||||
if n.Incomplete() {
|
||||
if !n.ip.IsValid() {
|
||||
return errors.New("missing IP address")
|
||||
}
|
||||
if n.UDP() == 0 {
|
||||
return errors.New("missing UDP port")
|
||||
}
|
||||
ip := n.IP()
|
||||
if ip.IsMulticast() || ip.IsUnspecified() {
|
||||
if n.ip.IsMulticast() || n.ip.IsUnspecified() {
|
||||
return errors.New("invalid IP (multicast/unspecified)")
|
||||
}
|
||||
if n.udp == 0 {
|
||||
return errors.New("missing UDP port")
|
||||
}
|
||||
// Validate the node key (on curve, etc.).
|
||||
var key Secp256k1
|
||||
return n.Load(&key)
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import (
|
|||
"encoding/hex"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/netip"
|
||||
"testing"
|
||||
"testing/quick"
|
||||
|
||||
|
|
@ -64,6 +65,167 @@ func TestPythonInterop(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestNodeEndpoints(t *testing.T) {
|
||||
id := HexID("00000000000000806ad9b61fa5ae014307ebdc964253adcd9f2c0a392aa11abc")
|
||||
type endpointTest struct {
|
||||
name string
|
||||
node *Node
|
||||
wantIP netip.Addr
|
||||
wantUDP int
|
||||
wantTCP int
|
||||
}
|
||||
tests := []endpointTest{
|
||||
{
|
||||
name: "no-addr",
|
||||
node: func() *Node {
|
||||
var r enr.Record
|
||||
return SignNull(&r, id)
|
||||
}(),
|
||||
},
|
||||
{
|
||||
name: "udp-only",
|
||||
node: func() *Node {
|
||||
var r enr.Record
|
||||
r.Set(enr.UDP(9000))
|
||||
return SignNull(&r, id)
|
||||
}(),
|
||||
},
|
||||
{
|
||||
name: "tcp-only",
|
||||
node: func() *Node {
|
||||
var r enr.Record
|
||||
r.Set(enr.TCP(9000))
|
||||
return SignNull(&r, id)
|
||||
}(),
|
||||
},
|
||||
{
|
||||
name: "ipv4-only-loopback",
|
||||
node: func() *Node {
|
||||
var r enr.Record
|
||||
r.Set(enr.IPv4Addr(netip.MustParseAddr("127.0.0.1")))
|
||||
return SignNull(&r, id)
|
||||
}(),
|
||||
wantIP: netip.MustParseAddr("127.0.0.1"),
|
||||
},
|
||||
{
|
||||
name: "ipv4-only-unspecified",
|
||||
node: func() *Node {
|
||||
var r enr.Record
|
||||
r.Set(enr.IPv4Addr(netip.MustParseAddr("0.0.0.0")))
|
||||
return SignNull(&r, id)
|
||||
}(),
|
||||
wantIP: netip.MustParseAddr("0.0.0.0"),
|
||||
},
|
||||
{
|
||||
name: "ipv4-only",
|
||||
node: func() *Node {
|
||||
var r enr.Record
|
||||
r.Set(enr.IPv4Addr(netip.MustParseAddr("99.22.33.1")))
|
||||
return SignNull(&r, id)
|
||||
}(),
|
||||
wantIP: netip.MustParseAddr("99.22.33.1"),
|
||||
},
|
||||
{
|
||||
name: "ipv6-only",
|
||||
node: func() *Node {
|
||||
var r enr.Record
|
||||
r.Set(enr.IPv6Addr(netip.MustParseAddr("2001::ff00:0042:8329")))
|
||||
return SignNull(&r, id)
|
||||
}(),
|
||||
wantIP: netip.MustParseAddr("2001::ff00:0042:8329"),
|
||||
},
|
||||
{
|
||||
name: "ipv4-loopback-and-ipv6-global",
|
||||
node: func() *Node {
|
||||
var r enr.Record
|
||||
r.Set(enr.IPv4Addr(netip.MustParseAddr("127.0.0.1")))
|
||||
r.Set(enr.UDP(30304))
|
||||
r.Set(enr.IPv6Addr(netip.MustParseAddr("2001::ff00:0042:8329")))
|
||||
r.Set(enr.UDP6(30306))
|
||||
return SignNull(&r, id)
|
||||
}(),
|
||||
wantIP: netip.MustParseAddr("2001::ff00:0042:8329"),
|
||||
wantUDP: 30306,
|
||||
},
|
||||
{
|
||||
name: "ipv4-unspecified-and-ipv6-loopback",
|
||||
node: func() *Node {
|
||||
var r enr.Record
|
||||
r.Set(enr.IPv4Addr(netip.MustParseAddr("0.0.0.0")))
|
||||
r.Set(enr.IPv6Addr(netip.MustParseAddr("::1")))
|
||||
return SignNull(&r, id)
|
||||
}(),
|
||||
wantIP: netip.MustParseAddr("::1"),
|
||||
},
|
||||
{
|
||||
name: "ipv4-private-and-ipv6-global",
|
||||
node: func() *Node {
|
||||
var r enr.Record
|
||||
r.Set(enr.IPv4Addr(netip.MustParseAddr("192.168.2.2")))
|
||||
r.Set(enr.UDP(30304))
|
||||
r.Set(enr.IPv6Addr(netip.MustParseAddr("2001::ff00:0042:8329")))
|
||||
r.Set(enr.UDP6(30306))
|
||||
return SignNull(&r, id)
|
||||
}(),
|
||||
wantIP: netip.MustParseAddr("2001::ff00:0042:8329"),
|
||||
wantUDP: 30306,
|
||||
},
|
||||
{
|
||||
name: "ipv4-local-and-ipv6-global",
|
||||
node: func() *Node {
|
||||
var r enr.Record
|
||||
r.Set(enr.IPv4Addr(netip.MustParseAddr("169.254.2.6")))
|
||||
r.Set(enr.UDP(30304))
|
||||
r.Set(enr.IPv6Addr(netip.MustParseAddr("2001::ff00:0042:8329")))
|
||||
r.Set(enr.UDP6(30306))
|
||||
return SignNull(&r, id)
|
||||
}(),
|
||||
wantIP: netip.MustParseAddr("2001::ff00:0042:8329"),
|
||||
wantUDP: 30306,
|
||||
},
|
||||
{
|
||||
name: "ipv4-private-and-ipv6-private",
|
||||
node: func() *Node {
|
||||
var r enr.Record
|
||||
r.Set(enr.IPv4Addr(netip.MustParseAddr("192.168.2.2")))
|
||||
r.Set(enr.UDP(30304))
|
||||
r.Set(enr.IPv6Addr(netip.MustParseAddr("fd00::abcd:1")))
|
||||
r.Set(enr.UDP6(30306))
|
||||
return SignNull(&r, id)
|
||||
}(),
|
||||
wantIP: netip.MustParseAddr("192.168.2.2"),
|
||||
wantUDP: 30304,
|
||||
},
|
||||
{
|
||||
name: "ipv4-private-and-ipv6-link-local",
|
||||
node: func() *Node {
|
||||
var r enr.Record
|
||||
r.Set(enr.IPv4Addr(netip.MustParseAddr("192.168.2.2")))
|
||||
r.Set(enr.UDP(30304))
|
||||
r.Set(enr.IPv6Addr(netip.MustParseAddr("fe80::1")))
|
||||
r.Set(enr.UDP6(30306))
|
||||
return SignNull(&r, id)
|
||||
}(),
|
||||
wantIP: netip.MustParseAddr("192.168.2.2"),
|
||||
wantUDP: 30304,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if test.wantIP != test.node.IPAddr() {
|
||||
t.Errorf("node has wrong IP %v, want %v", test.node.IPAddr(), test.wantIP)
|
||||
}
|
||||
if test.wantUDP != test.node.UDP() {
|
||||
t.Errorf("node has wrong UDP port %d, want %d", test.node.UDP(), test.wantUDP)
|
||||
}
|
||||
if test.wantTCP != test.node.TCP() {
|
||||
t.Errorf("node has wrong TCP port %d, want %d", test.node.TCP(), test.wantTCP)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHexID(t *testing.T) {
|
||||
ref := ID{0, 0, 0, 0, 0, 0, 0, 128, 106, 217, 182, 31, 165, 174, 1, 67, 7, 235, 220, 150, 66, 83, 173, 205, 159, 44, 10, 57, 42, 161, 26, 188}
|
||||
id1 := HexID("0x00000000000000806ad9b61fa5ae014307ebdc964253adcd9f2c0a392aa11abc")
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/p2p/enr"
|
||||
"github.com/ethereum/go-ethereum/rlp"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/errors"
|
||||
|
|
@ -242,13 +243,14 @@ func (db *DB) Node(id ID) *Node {
|
|||
}
|
||||
|
||||
func mustDecodeNode(id, data []byte) *Node {
|
||||
node := new(Node)
|
||||
if err := rlp.DecodeBytes(data, &node.r); err != nil {
|
||||
var r enr.Record
|
||||
if err := rlp.DecodeBytes(data, &r); err != nil {
|
||||
panic(fmt.Errorf("p2p/enode: can't decode node %x in DB: %v", id, err))
|
||||
}
|
||||
// Restore node id cache.
|
||||
copy(node.id[:], id)
|
||||
return node
|
||||
if len(id) != len(ID{}) {
|
||||
panic(fmt.Errorf("invalid id length %d", len(id)))
|
||||
}
|
||||
return newNodeWithID(&r, ID(id))
|
||||
}
|
||||
|
||||
// UpdateNode inserts - potentially overwriting - a node into the peer database.
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ func (n *Node) URLv4() string {
|
|||
nodeid = fmt.Sprintf("%s.%x", scheme, n.id[:])
|
||||
}
|
||||
u := url.URL{Scheme: "enode"}
|
||||
if n.Incomplete() {
|
||||
if !n.ip.IsValid() {
|
||||
u.Host = nodeid
|
||||
} else {
|
||||
addr := net.TCPAddr{IP: n.IP(), Port: n.TCP()}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
|
||||
"github.com/ethereum/go-ethereum/rlp"
|
||||
)
|
||||
|
|
@ -167,6 +168,60 @@ func (v *IPv6) DecodeRLP(s *rlp.Stream) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// IPv4Addr is the "ip" key, which holds the IP address of the node.
|
||||
type IPv4Addr netip.Addr
|
||||
|
||||
func (v IPv4Addr) ENRKey() string { return "ip" }
|
||||
|
||||
// EncodeRLP implements rlp.Encoder.
|
||||
func (v IPv4Addr) EncodeRLP(w io.Writer) error {
|
||||
addr := netip.Addr(v)
|
||||
if !addr.Is4() {
|
||||
return fmt.Errorf("address is not IPv4")
|
||||
}
|
||||
enc := rlp.NewEncoderBuffer(w)
|
||||
bytes := addr.As4()
|
||||
enc.WriteBytes(bytes[:])
|
||||
return enc.Flush()
|
||||
}
|
||||
|
||||
// DecodeRLP implements rlp.Decoder.
|
||||
func (v *IPv4Addr) DecodeRLP(s *rlp.Stream) error {
|
||||
var bytes [4]byte
|
||||
if err := s.ReadBytes(bytes[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
*v = IPv4Addr(netip.AddrFrom4(bytes))
|
||||
return nil
|
||||
}
|
||||
|
||||
// IPv6Addr is the "ip6" key, which holds the IP address of the node.
|
||||
type IPv6Addr netip.Addr
|
||||
|
||||
func (v IPv6Addr) ENRKey() string { return "ip6" }
|
||||
|
||||
// EncodeRLP implements rlp.Encoder.
|
||||
func (v IPv6Addr) EncodeRLP(w io.Writer) error {
|
||||
addr := netip.Addr(v)
|
||||
if !addr.Is6() {
|
||||
return fmt.Errorf("address is not IPv6")
|
||||
}
|
||||
enc := rlp.NewEncoderBuffer(w)
|
||||
bytes := addr.As16()
|
||||
enc.WriteBytes(bytes[:])
|
||||
return enc.Flush()
|
||||
}
|
||||
|
||||
// DecodeRLP implements rlp.Decoder.
|
||||
func (v *IPv6Addr) DecodeRLP(s *rlp.Stream) error {
|
||||
var bytes [16]byte
|
||||
if err := s.ReadBytes(bytes[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
*v = IPv6Addr(netip.AddrFrom16(bytes))
|
||||
return nil
|
||||
}
|
||||
|
||||
// KeyError is an error related to a key.
|
||||
type KeyError struct {
|
||||
Key string
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,407 +0,0 @@
|
|||
// Copyright 2020 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package nodestate
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common/mclock"
|
||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
||||
"github.com/ethereum/go-ethereum/p2p/enr"
|
||||
"github.com/ethereum/go-ethereum/rlp"
|
||||
)
|
||||
|
||||
func testSetup(flagPersist []bool, fieldType []reflect.Type) (*Setup, []Flags, []Field) {
|
||||
setup := &Setup{}
|
||||
flags := make([]Flags, len(flagPersist))
|
||||
for i, persist := range flagPersist {
|
||||
if persist {
|
||||
flags[i] = setup.NewPersistentFlag(fmt.Sprintf("flag-%d", i))
|
||||
} else {
|
||||
flags[i] = setup.NewFlag(fmt.Sprintf("flag-%d", i))
|
||||
}
|
||||
}
|
||||
fields := make([]Field, len(fieldType))
|
||||
for i, ftype := range fieldType {
|
||||
switch ftype {
|
||||
case reflect.TypeOf(uint64(0)):
|
||||
fields[i] = setup.NewPersistentField(fmt.Sprintf("field-%d", i), ftype, uint64FieldEnc, uint64FieldDec)
|
||||
case reflect.TypeOf(""):
|
||||
fields[i] = setup.NewPersistentField(fmt.Sprintf("field-%d", i), ftype, stringFieldEnc, stringFieldDec)
|
||||
default:
|
||||
fields[i] = setup.NewField(fmt.Sprintf("field-%d", i), ftype)
|
||||
}
|
||||
}
|
||||
return setup, flags, fields
|
||||
}
|
||||
|
||||
func testNode(b byte) *enode.Node {
|
||||
r := &enr.Record{}
|
||||
r.SetSig(dummyIdentity{b}, []byte{42})
|
||||
n, _ := enode.New(dummyIdentity{b}, r)
|
||||
return n
|
||||
}
|
||||
|
||||
func TestCallback(t *testing.T) {
|
||||
mdb, clock := rawdb.NewMemoryDatabase(), &mclock.Simulated{}
|
||||
|
||||
s, flags, _ := testSetup([]bool{false, false, false}, nil)
|
||||
ns := NewNodeStateMachine(mdb, []byte("-ns"), clock, s)
|
||||
|
||||
set0 := make(chan struct{}, 1)
|
||||
set1 := make(chan struct{}, 1)
|
||||
set2 := make(chan struct{}, 1)
|
||||
ns.SubscribeState(flags[0], func(n *enode.Node, oldState, newState Flags) { set0 <- struct{}{} })
|
||||
ns.SubscribeState(flags[1], func(n *enode.Node, oldState, newState Flags) { set1 <- struct{}{} })
|
||||
ns.SubscribeState(flags[2], func(n *enode.Node, oldState, newState Flags) { set2 <- struct{}{} })
|
||||
|
||||
ns.Start()
|
||||
|
||||
ns.SetState(testNode(1), flags[0], Flags{}, 0)
|
||||
ns.SetState(testNode(1), flags[1], Flags{}, time.Second)
|
||||
ns.SetState(testNode(1), flags[2], Flags{}, 2*time.Second)
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
select {
|
||||
case <-set0:
|
||||
case <-set1:
|
||||
case <-set2:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatalf("failed to invoke callback")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPersistentFlags(t *testing.T) {
|
||||
mdb, clock := rawdb.NewMemoryDatabase(), &mclock.Simulated{}
|
||||
|
||||
s, flags, _ := testSetup([]bool{true, true, true, false}, nil)
|
||||
ns := NewNodeStateMachine(mdb, []byte("-ns"), clock, s)
|
||||
|
||||
saveNode := make(chan *nodeInfo, 5)
|
||||
ns.saveNodeHook = func(node *nodeInfo) {
|
||||
saveNode <- node
|
||||
}
|
||||
|
||||
ns.Start()
|
||||
|
||||
ns.SetState(testNode(1), flags[0], Flags{}, time.Second) // state with timeout should not be saved
|
||||
ns.SetState(testNode(2), flags[1], Flags{}, 0)
|
||||
ns.SetState(testNode(3), flags[2], Flags{}, 0)
|
||||
ns.SetState(testNode(4), flags[3], Flags{}, 0)
|
||||
ns.SetState(testNode(5), flags[0], Flags{}, 0)
|
||||
ns.Persist(testNode(5))
|
||||
select {
|
||||
case <-saveNode:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatalf("Timeout")
|
||||
}
|
||||
ns.Stop()
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
select {
|
||||
case <-saveNode:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatalf("Timeout")
|
||||
}
|
||||
}
|
||||
select {
|
||||
case <-saveNode:
|
||||
t.Fatalf("Unexpected saveNode")
|
||||
case <-time.After(time.Millisecond * 100):
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetField(t *testing.T) {
|
||||
mdb, clock := rawdb.NewMemoryDatabase(), &mclock.Simulated{}
|
||||
|
||||
s, flags, fields := testSetup([]bool{true}, []reflect.Type{reflect.TypeOf("")})
|
||||
ns := NewNodeStateMachine(mdb, []byte("-ns"), clock, s)
|
||||
|
||||
saveNode := make(chan *nodeInfo, 1)
|
||||
ns.saveNodeHook = func(node *nodeInfo) {
|
||||
saveNode <- node
|
||||
}
|
||||
|
||||
ns.Start()
|
||||
|
||||
// Set field before setting state
|
||||
ns.SetField(testNode(1), fields[0], "hello world")
|
||||
field := ns.GetField(testNode(1), fields[0])
|
||||
if field == nil {
|
||||
t.Fatalf("Field should be set before setting states")
|
||||
}
|
||||
ns.SetField(testNode(1), fields[0], nil)
|
||||
field = ns.GetField(testNode(1), fields[0])
|
||||
if field != nil {
|
||||
t.Fatalf("Field should be unset")
|
||||
}
|
||||
// Set field after setting state
|
||||
ns.SetState(testNode(1), flags[0], Flags{}, 0)
|
||||
ns.SetField(testNode(1), fields[0], "hello world")
|
||||
field = ns.GetField(testNode(1), fields[0])
|
||||
if field == nil {
|
||||
t.Fatalf("Field should be set after setting states")
|
||||
}
|
||||
if err := ns.SetField(testNode(1), fields[0], 123); err == nil {
|
||||
t.Fatalf("Invalid field should be rejected")
|
||||
}
|
||||
// Dirty node should be written back
|
||||
ns.Stop()
|
||||
select {
|
||||
case <-saveNode:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatalf("Timeout")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetState(t *testing.T) {
|
||||
mdb, clock := rawdb.NewMemoryDatabase(), &mclock.Simulated{}
|
||||
|
||||
s, flags, _ := testSetup([]bool{false, false, false}, nil)
|
||||
ns := NewNodeStateMachine(mdb, []byte("-ns"), clock, s)
|
||||
|
||||
type change struct{ old, new Flags }
|
||||
set := make(chan change, 1)
|
||||
ns.SubscribeState(flags[0].Or(flags[1]), func(n *enode.Node, oldState, newState Flags) {
|
||||
set <- change{
|
||||
old: oldState,
|
||||
new: newState,
|
||||
}
|
||||
})
|
||||
|
||||
ns.Start()
|
||||
|
||||
check := func(expectOld, expectNew Flags, expectChange bool) {
|
||||
if expectChange {
|
||||
select {
|
||||
case c := <-set:
|
||||
if !c.old.Equals(expectOld) {
|
||||
t.Fatalf("Old state mismatch")
|
||||
}
|
||||
if !c.new.Equals(expectNew) {
|
||||
t.Fatalf("New state mismatch")
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
}
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-set:
|
||||
t.Fatalf("Unexpected change")
|
||||
case <-time.After(time.Millisecond * 100):
|
||||
return
|
||||
}
|
||||
}
|
||||
ns.SetState(testNode(1), flags[0], Flags{}, 0)
|
||||
check(Flags{}, flags[0], true)
|
||||
|
||||
ns.SetState(testNode(1), flags[1], Flags{}, 0)
|
||||
check(flags[0], flags[0].Or(flags[1]), true)
|
||||
|
||||
ns.SetState(testNode(1), flags[2], Flags{}, 0)
|
||||
check(Flags{}, Flags{}, false)
|
||||
|
||||
ns.SetState(testNode(1), Flags{}, flags[0], 0)
|
||||
check(flags[0].Or(flags[1]), flags[1], true)
|
||||
|
||||
ns.SetState(testNode(1), Flags{}, flags[1], 0)
|
||||
check(flags[1], Flags{}, true)
|
||||
|
||||
ns.SetState(testNode(1), Flags{}, flags[2], 0)
|
||||
check(Flags{}, Flags{}, false)
|
||||
|
||||
ns.SetState(testNode(1), flags[0].Or(flags[1]), Flags{}, time.Second)
|
||||
check(Flags{}, flags[0].Or(flags[1]), true)
|
||||
clock.Run(time.Second)
|
||||
check(flags[0].Or(flags[1]), Flags{}, true)
|
||||
}
|
||||
|
||||
func uint64FieldEnc(field interface{}) ([]byte, error) {
|
||||
if u, ok := field.(uint64); ok {
|
||||
enc, err := rlp.EncodeToBytes(&u)
|
||||
return enc, err
|
||||
}
|
||||
return nil, errors.New("invalid field type")
|
||||
}
|
||||
|
||||
func uint64FieldDec(enc []byte) (interface{}, error) {
|
||||
var u uint64
|
||||
err := rlp.DecodeBytes(enc, &u)
|
||||
return u, err
|
||||
}
|
||||
|
||||
func stringFieldEnc(field interface{}) ([]byte, error) {
|
||||
if s, ok := field.(string); ok {
|
||||
return []byte(s), nil
|
||||
}
|
||||
return nil, errors.New("invalid field type")
|
||||
}
|
||||
|
||||
func stringFieldDec(enc []byte) (interface{}, error) {
|
||||
return string(enc), nil
|
||||
}
|
||||
|
||||
func TestPersistentFields(t *testing.T) {
|
||||
mdb, clock := rawdb.NewMemoryDatabase(), &mclock.Simulated{}
|
||||
|
||||
s, flags, fields := testSetup([]bool{true}, []reflect.Type{reflect.TypeOf(uint64(0)), reflect.TypeOf("")})
|
||||
ns := NewNodeStateMachine(mdb, []byte("-ns"), clock, s)
|
||||
|
||||
ns.Start()
|
||||
ns.SetState(testNode(1), flags[0], Flags{}, 0)
|
||||
ns.SetField(testNode(1), fields[0], uint64(100))
|
||||
ns.SetField(testNode(1), fields[1], "hello world")
|
||||
ns.Stop()
|
||||
|
||||
ns2 := NewNodeStateMachine(mdb, []byte("-ns"), clock, s)
|
||||
|
||||
ns2.Start()
|
||||
field0 := ns2.GetField(testNode(1), fields[0])
|
||||
if !reflect.DeepEqual(field0, uint64(100)) {
|
||||
t.Fatalf("Field changed")
|
||||
}
|
||||
field1 := ns2.GetField(testNode(1), fields[1])
|
||||
if !reflect.DeepEqual(field1, "hello world") {
|
||||
t.Fatalf("Field changed")
|
||||
}
|
||||
|
||||
s.Version++
|
||||
ns3 := NewNodeStateMachine(mdb, []byte("-ns"), clock, s)
|
||||
ns3.Start()
|
||||
if ns3.GetField(testNode(1), fields[0]) != nil {
|
||||
t.Fatalf("Old field version should have been discarded")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFieldSub(t *testing.T) {
|
||||
mdb, clock := rawdb.NewMemoryDatabase(), &mclock.Simulated{}
|
||||
|
||||
s, flags, fields := testSetup([]bool{true}, []reflect.Type{reflect.TypeOf(uint64(0))})
|
||||
ns := NewNodeStateMachine(mdb, []byte("-ns"), clock, s)
|
||||
|
||||
var (
|
||||
lastState Flags
|
||||
lastOldValue, lastNewValue interface{}
|
||||
)
|
||||
ns.SubscribeField(fields[0], func(n *enode.Node, state Flags, oldValue, newValue interface{}) {
|
||||
lastState, lastOldValue, lastNewValue = state, oldValue, newValue
|
||||
})
|
||||
check := func(state Flags, oldValue, newValue interface{}) {
|
||||
if !lastState.Equals(state) || lastOldValue != oldValue || lastNewValue != newValue {
|
||||
t.Fatalf("Incorrect field sub callback (expected [%v %v %v], got [%v %v %v])", state, oldValue, newValue, lastState, lastOldValue, lastNewValue)
|
||||
}
|
||||
}
|
||||
ns.Start()
|
||||
ns.SetState(testNode(1), flags[0], Flags{}, 0)
|
||||
ns.SetField(testNode(1), fields[0], uint64(100))
|
||||
check(flags[0], nil, uint64(100))
|
||||
ns.Stop()
|
||||
check(s.OfflineFlag(), uint64(100), nil)
|
||||
|
||||
ns2 := NewNodeStateMachine(mdb, []byte("-ns"), clock, s)
|
||||
ns2.SubscribeField(fields[0], func(n *enode.Node, state Flags, oldValue, newValue interface{}) {
|
||||
lastState, lastOldValue, lastNewValue = state, oldValue, newValue
|
||||
})
|
||||
ns2.Start()
|
||||
check(s.OfflineFlag(), nil, uint64(100))
|
||||
ns2.SetState(testNode(1), Flags{}, flags[0], 0)
|
||||
ns2.SetField(testNode(1), fields[0], nil)
|
||||
check(Flags{}, uint64(100), nil)
|
||||
ns2.Stop()
|
||||
}
|
||||
|
||||
func TestDuplicatedFlags(t *testing.T) {
|
||||
mdb, clock := rawdb.NewMemoryDatabase(), &mclock.Simulated{}
|
||||
|
||||
s, flags, _ := testSetup([]bool{true}, nil)
|
||||
ns := NewNodeStateMachine(mdb, []byte("-ns"), clock, s)
|
||||
|
||||
type change struct{ old, new Flags }
|
||||
set := make(chan change, 1)
|
||||
ns.SubscribeState(flags[0], func(n *enode.Node, oldState, newState Flags) {
|
||||
set <- change{oldState, newState}
|
||||
})
|
||||
|
||||
ns.Start()
|
||||
defer ns.Stop()
|
||||
|
||||
check := func(expectOld, expectNew Flags, expectChange bool) {
|
||||
if expectChange {
|
||||
select {
|
||||
case c := <-set:
|
||||
if !c.old.Equals(expectOld) {
|
||||
t.Fatalf("Old state mismatch")
|
||||
}
|
||||
if !c.new.Equals(expectNew) {
|
||||
t.Fatalf("New state mismatch")
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
}
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-set:
|
||||
t.Fatalf("Unexpected change")
|
||||
case <-time.After(time.Millisecond * 100):
|
||||
return
|
||||
}
|
||||
}
|
||||
ns.SetState(testNode(1), flags[0], Flags{}, time.Second)
|
||||
check(Flags{}, flags[0], true)
|
||||
ns.SetState(testNode(1), flags[0], Flags{}, 2*time.Second) // extend the timeout to 2s
|
||||
check(Flags{}, flags[0], false)
|
||||
|
||||
clock.Run(2 * time.Second)
|
||||
check(flags[0], Flags{}, true)
|
||||
}
|
||||
|
||||
func TestCallbackOrder(t *testing.T) {
|
||||
mdb, clock := rawdb.NewMemoryDatabase(), &mclock.Simulated{}
|
||||
|
||||
s, flags, _ := testSetup([]bool{false, false, false, false}, nil)
|
||||
ns := NewNodeStateMachine(mdb, []byte("-ns"), clock, s)
|
||||
|
||||
ns.SubscribeState(flags[0], func(n *enode.Node, oldState, newState Flags) {
|
||||
if newState.Equals(flags[0]) {
|
||||
ns.SetStateSub(n, flags[1], Flags{}, 0)
|
||||
ns.SetStateSub(n, flags[2], Flags{}, 0)
|
||||
}
|
||||
})
|
||||
ns.SubscribeState(flags[1], func(n *enode.Node, oldState, newState Flags) {
|
||||
if newState.Equals(flags[1]) {
|
||||
ns.SetStateSub(n, flags[3], Flags{}, 0)
|
||||
}
|
||||
})
|
||||
lastState := Flags{}
|
||||
ns.SubscribeState(MergeFlags(flags[1], flags[2], flags[3]), func(n *enode.Node, oldState, newState Flags) {
|
||||
if !oldState.Equals(lastState) {
|
||||
t.Fatalf("Wrong callback order")
|
||||
}
|
||||
lastState = newState
|
||||
})
|
||||
|
||||
ns.Start()
|
||||
defer ns.Stop()
|
||||
|
||||
ns.SetState(testNode(1), flags[0], Flags{}, 0)
|
||||
}
|
||||
|
|
@ -190,8 +190,8 @@ type Server struct {
|
|||
|
||||
nodedb *enode.DB
|
||||
localnode *enode.LocalNode
|
||||
ntab *discover.UDPv4
|
||||
DiscV5 *discover.UDPv5
|
||||
discv4 *discover.UDPv4
|
||||
discv5 *discover.UDPv5
|
||||
discmix *enode.FairMix
|
||||
dialsched *dialScheduler
|
||||
|
||||
|
|
@ -400,6 +400,16 @@ func (srv *Server) Self() *enode.Node {
|
|||
return ln.Node()
|
||||
}
|
||||
|
||||
// DiscoveryV4 returns the discovery v4 instance, if configured.
|
||||
func (srv *Server) DiscoveryV4() *discover.UDPv4 {
|
||||
return srv.discv4
|
||||
}
|
||||
|
||||
// DiscoveryV5 returns the discovery v5 instance, if configured.
|
||||
func (srv *Server) DiscoveryV5() *discover.UDPv5 {
|
||||
return srv.discv5
|
||||
}
|
||||
|
||||
// Stop terminates the server and all active peer connections.
|
||||
// It blocks until all active connections have been closed.
|
||||
func (srv *Server) Stop() {
|
||||
|
|
@ -547,13 +557,13 @@ func (srv *Server) setupDiscovery() error {
|
|||
)
|
||||
// If both versions of discovery are running, setup a shared
|
||||
// connection, so v5 can read unhandled messages from v4.
|
||||
if srv.DiscoveryV4 && srv.DiscoveryV5 {
|
||||
if srv.Config.DiscoveryV4 && srv.Config.DiscoveryV5 {
|
||||
unhandled = make(chan discover.ReadPacket, 100)
|
||||
sconn = &sharedUDPConn{conn, unhandled}
|
||||
}
|
||||
|
||||
// Start discovery services.
|
||||
if srv.DiscoveryV4 {
|
||||
if srv.Config.DiscoveryV4 {
|
||||
cfg := discover.Config{
|
||||
PrivateKey: srv.PrivateKey,
|
||||
NetRestrict: srv.NetRestrict,
|
||||
|
|
@ -565,17 +575,17 @@ func (srv *Server) setupDiscovery() error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srv.ntab = ntab
|
||||
srv.discv4 = ntab
|
||||
srv.discmix.AddSource(ntab.RandomNodes())
|
||||
}
|
||||
if srv.DiscoveryV5 {
|
||||
if srv.Config.DiscoveryV5 {
|
||||
cfg := discover.Config{
|
||||
PrivateKey: srv.PrivateKey,
|
||||
NetRestrict: srv.NetRestrict,
|
||||
Bootnodes: srv.BootstrapNodesV5,
|
||||
Log: srv.log,
|
||||
}
|
||||
srv.DiscV5, err = discover.ListenV5(sconn, srv.localnode, cfg)
|
||||
srv.discv5, err = discover.ListenV5(sconn, srv.localnode, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -602,8 +612,8 @@ func (srv *Server) setupDialScheduler() {
|
|||
dialer: srv.Dialer,
|
||||
clock: srv.clock,
|
||||
}
|
||||
if srv.ntab != nil {
|
||||
config.resolver = srv.ntab
|
||||
if srv.discv4 != nil {
|
||||
config.resolver = srv.discv4
|
||||
}
|
||||
if config.dialer == nil {
|
||||
config.dialer = tcpDialer{&net.Dialer{Timeout: defaultDialTimeout}}
|
||||
|
|
@ -799,11 +809,11 @@ running:
|
|||
srv.log.Trace("P2P networking is spinning down")
|
||||
|
||||
// Terminate discovery. If there is a running lookup it will terminate soon.
|
||||
if srv.ntab != nil {
|
||||
srv.ntab.Close()
|
||||
if srv.discv4 != nil {
|
||||
srv.discv4.Close()
|
||||
}
|
||||
if srv.DiscV5 != nil {
|
||||
srv.DiscV5.Close()
|
||||
if srv.discv5 != nil {
|
||||
srv.discv5.Close()
|
||||
}
|
||||
// Disconnect all peers.
|
||||
for _, p := range peers {
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@ import (
|
|||
//
|
||||
// - SimNode, an in-memory node in the same process
|
||||
// - ExecNode, a child process node
|
||||
// - DockerNode, a node running in a Docker container
|
||||
type Node interface {
|
||||
// Addr returns the node's address (e.g. an Enode URL)
|
||||
Addr() []byte
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ import (
|
|||
"github.com/ethereum/go-ethereum/p2p/simulations/adapters"
|
||||
)
|
||||
|
||||
var adapterType = flag.String("adapter", "sim", `node adapter to use (one of "sim", "exec" or "docker")`)
|
||||
var adapterType = flag.String("adapter", "sim", `node adapter to use (one of "sim" or "exec")`)
|
||||
|
||||
// main() starts a simulation network which contains nodes running a simple
|
||||
// ping-pong protocol
|
||||
|
|
|
|||
|
|
@ -189,6 +189,10 @@ var (
|
|||
|
||||
// BeaconRootsAddress is the address where historical beacon roots are stored as per EIP-4788
|
||||
BeaconRootsAddress = common.HexToAddress("0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02")
|
||||
|
||||
// BeaconRootsCode is the code where historical beacon roots are stored as per EIP-4788
|
||||
BeaconRootsCode = common.FromHex("3373fffffffffffffffffffffffffffffffffffffffe14604d57602036146024575f5ffd5b5f35801560495762001fff810690815414603c575f5ffd5b62001fff01545f5260205ff35b5f5ffd5b62001fff42064281555f359062001fff015500")
|
||||
|
||||
// SystemAddress is where the system-transaction is sent from as per EIP-4788
|
||||
SystemAddress = common.HexToAddress("0xfffffffffffffffffffffffffffffffffffffffe")
|
||||
// HistoryStorageAddress is where the historical block hashes are stored.
|
||||
|
|
|
|||
|
|
@ -212,7 +212,7 @@ var Forks = map[string]*params.ChainConfig{
|
|||
LondonBlock: big.NewInt(0),
|
||||
ArrowGlacierBlock: big.NewInt(0),
|
||||
},
|
||||
"ArrowGlacierToMergeAtDiffC0000": {
|
||||
"ArrowGlacierToParisAtDiffC0000": {
|
||||
ChainID: big.NewInt(1),
|
||||
HomesteadBlock: big.NewInt(0),
|
||||
EIP150Block: big.NewInt(0),
|
||||
|
|
@ -246,6 +246,23 @@ var Forks = map[string]*params.ChainConfig{
|
|||
ArrowGlacierBlock: 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": {
|
||||
ChainID: big.NewInt(1),
|
||||
HomesteadBlock: big.NewInt(0),
|
||||
|
|
@ -281,7 +298,7 @@ var Forks = map[string]*params.ChainConfig{
|
|||
TerminalTotalDifficulty: big.NewInt(0),
|
||||
ShanghaiTime: u64(0),
|
||||
},
|
||||
"MergeToShanghaiAtTime15k": {
|
||||
"ParisToShanghaiAtTime15k": {
|
||||
ChainID: big.NewInt(1),
|
||||
HomesteadBlock: big.NewInt(0),
|
||||
EIP150Block: big.NewInt(0),
|
||||
|
|
|
|||
|
|
@ -54,14 +54,6 @@ func initMatcher(st *testMatcher) {
|
|||
// Uses 1GB RAM per tested fork
|
||||
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:
|
||||
// EOF is not part of cancun
|
||||
st.skipLoad(`^stEOF/`)
|
||||
|
|
|
|||
40
trie/sync.go
40
trie/sync.go
|
|
@ -22,6 +22,7 @@ import (
|
|||
"sync"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"github.com/ethereum/go-ethereum/common/prque"
|
||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
|
|
@ -149,15 +150,42 @@ type CodeSyncResult struct {
|
|||
// nodeOp represents an operation upon the trie node. It can either represent a
|
||||
// deletion to the specific node or a node write for persisting retrieved node.
|
||||
type nodeOp struct {
|
||||
del bool // flag if op stands for a delete operation
|
||||
owner common.Hash // identifier of the trie (empty for account trie)
|
||||
path []byte // path from the root to the specified node.
|
||||
blob []byte // the content of the node (nil for deletion)
|
||||
hash common.Hash // hash of the node content (empty for node deletion)
|
||||
}
|
||||
|
||||
// isDelete indicates if the operation is a database deletion.
|
||||
func (op *nodeOp) isDelete() bool {
|
||||
return len(op.blob) == 0
|
||||
// valid checks whether the node operation is valid.
|
||||
func (op *nodeOp) valid() bool {
|
||||
if op.del && len(op.blob) != 0 {
|
||||
return false
|
||||
}
|
||||
if !op.del && len(op.blob) == 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// string returns the node operation in string representation.
|
||||
func (op *nodeOp) string() string {
|
||||
var node string
|
||||
if op.owner == (common.Hash{}) {
|
||||
node = fmt.Sprintf("node: (%v)", op.path)
|
||||
} else {
|
||||
node = fmt.Sprintf("node: (%x-%v)", op.owner, op.path)
|
||||
}
|
||||
var blobHex string
|
||||
if len(op.blob) == 0 {
|
||||
blobHex = "nil"
|
||||
} else {
|
||||
blobHex = hexutil.Encode(op.blob)
|
||||
}
|
||||
if op.del {
|
||||
return fmt.Sprintf("del %s %s %s", node, blobHex, op.hash.Hex())
|
||||
}
|
||||
return fmt.Sprintf("write %s %s %s", node, blobHex, op.hash.Hex())
|
||||
}
|
||||
|
||||
// syncMemBatch is an in-memory buffer of successfully downloaded but not yet
|
||||
|
|
@ -220,6 +248,7 @@ func (batch *syncMemBatch) delNode(owner common.Hash, path []byte) {
|
|||
batch.size += common.HashLength + uint64(len(path))
|
||||
}
|
||||
batch.nodes = append(batch.nodes, nodeOp{
|
||||
del: true,
|
||||
owner: owner,
|
||||
path: path,
|
||||
})
|
||||
|
|
@ -428,7 +457,10 @@ func (s *Sync) Commit(dbw ethdb.Batch) error {
|
|||
storage int
|
||||
)
|
||||
for _, op := range s.membatch.nodes {
|
||||
if op.isDelete() {
|
||||
if !op.valid() {
|
||||
return fmt.Errorf("invalid op, %s", op.string())
|
||||
}
|
||||
if op.del {
|
||||
// node deletion is only supported in path mode.
|
||||
if op.owner == (common.Hash{}) {
|
||||
rawdb.DeleteAccountTrieNode(dbw, op.path)
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ func (db *Database) repairHistory() error {
|
|||
// all of them. Fix the tests first.
|
||||
return nil
|
||||
}
|
||||
freezer, err := rawdb.NewStateFreezer(ancient, false)
|
||||
freezer, err := rawdb.NewStateFreezer(ancient, db.readOnly)
|
||||
if err != nil {
|
||||
log.Crit("Failed to open state history freezer", "err", err)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue