Merge tag 'v1.15.4' into release/geth-1.x-fh3.0

This commit is contained in:
Matthieu Vachon 2025-03-01 19:32:21 -05:00
commit 6254a4bc6b
53 changed files with 1980 additions and 170 deletions

View file

@ -29,7 +29,7 @@ jobs:
- name: Run linters - name: Run linters
run: | run: |
go run build/ci.go lint go run build/ci.go lint
go run build/ci.go check_tidy go run build/ci.go check_generate
go run build/ci.go check_baddeps go run build/ci.go check_baddeps
build: build:

View file

@ -24,7 +24,6 @@ for:
- image: Ubuntu - image: Ubuntu
build_script: build_script:
- go run build/ci.go lint - go run build/ci.go lint
- go run build/ci.go check_tidy
- go run build/ci.go check_generate - go run build/ci.go check_generate
- go run build/ci.go check_baddeps - go run build/ci.go check_baddeps
- go run build/ci.go install -dlgo - go run build/ci.go install -dlgo

View file

@ -114,14 +114,18 @@ fc43f8c95d6bec8ba9a3557a0bb63cf9f504137ce6d24cfdc4c330d46f7779d2 golangci-lint-
# This version is fine to be old and full of security holes, we just use it # This version is fine to be old and full of security holes, we just use it
# to build the latest Go. Don't change it. # to build the latest Go. Don't change it.
# #
# version:ppa-builder-1 1.19.6 # version:ppa-builder-1.19 1.19.6
# https://go.dev/dl/ # https://go.dev/dl/
d7f0013f82e6d7f862cc6cb5c8cdb48eef5f2e239b35baa97e2f1a7466043767 go1.19.6.src.tar.gz d7f0013f82e6d7f862cc6cb5c8cdb48eef5f2e239b35baa97e2f1a7466043767 go1.19.6.src.tar.gz
# version:ppa-builder-2 1.21.9 # version:ppa-builder-1.21 1.21.9
# https://go.dev/dl/ # https://go.dev/dl/
58f0c5ced45a0012bce2ff7a9df03e128abcc8818ebabe5027bb92bafe20e421 go1.21.9.src.tar.gz 58f0c5ced45a0012bce2ff7a9df03e128abcc8818ebabe5027bb92bafe20e421 go1.21.9.src.tar.gz
# version:ppa-builder-1.23 1.23.6
# https://go.dev/dl/
039c5b04e65279daceee8a6f71e70bd05cf5b801782b6f77c6e19e2ed0511222 go1.23.6.src.tar.gz
# version:protoc 27.1 # version:protoc 27.1
# https://github.com/protocolbuffers/protobuf/releases/ # https://github.com/protocolbuffers/protobuf/releases/
# https://github.com/protocolbuffers/protobuf/releases/download/v27.1/ # https://github.com/protocolbuffers/protobuf/releases/download/v27.1/

View file

@ -25,8 +25,7 @@ Usage: go run build/ci.go <command> <command flags/arguments>
Available commands are: Available commands are:
lint -- runs certain pre-selected linters lint -- runs certain pre-selected linters
check_tidy -- verifies that everything is 'go mod tidy'-ed check_generate -- verifies that 'go generate' and 'go mod tidy' do not produce changes
check_generate -- verifies that everything is 'go generate'-ed
check_baddeps -- verifies that certain dependencies are avoided check_baddeps -- verifies that certain dependencies are avoided
install [ -arch architecture ] [ -cc compiler ] [ packages... ] -- builds packages and executables install [ -arch architecture ] [ -cc compiler ] [ packages... ] -- builds packages and executables
@ -156,8 +155,6 @@ func main() {
doTest(os.Args[2:]) doTest(os.Args[2:])
case "lint": case "lint":
doLint(os.Args[2:]) doLint(os.Args[2:])
case "check_tidy":
doCheckTidy()
case "check_generate": case "check_generate":
doCheckGenerate() doCheckGenerate()
case "check_baddeps": case "check_baddeps":
@ -353,22 +350,6 @@ func downloadSpecTestFixtures(csdb *build.ChecksumDB, cachedir string) string {
// doCheckTidy assets that the Go modules files are tidied already. // doCheckTidy assets that the Go modules files are tidied already.
func doCheckTidy() { func doCheckTidy() {
targets := []string{"go.mod", "go.sum"}
hashes, err := build.HashFiles(targets)
if err != nil {
log.Fatalf("failed to hash go.mod/go.sum: %v", err)
}
build.MustRun(new(build.GoToolchain).Go("mod", "tidy"))
tidied, err := build.HashFiles(targets)
if err != nil {
log.Fatalf("failed to rehash go.mod/go.sum: %v", err)
}
if updates := build.DiffHashes(hashes, tidied); len(updates) > 0 {
log.Fatalf("files changed on running 'go mod tidy': %v", updates)
}
fmt.Println("No untidy module files detected.")
} }
// doCheckGenerate ensures that re-generating generated files does not cause // doCheckGenerate ensures that re-generating generated files does not cause
@ -376,12 +357,13 @@ func doCheckTidy() {
func doCheckGenerate() { func doCheckGenerate() {
var ( var (
cachedir = flag.String("cachedir", "./build/cache", "directory for caching binaries.") cachedir = flag.String("cachedir", "./build/cache", "directory for caching binaries.")
tc = new(build.GoToolchain)
) )
// Compute the origin hashes of all the files // Compute the origin hashes of all the files
var hashes map[string][32]byte var hashes map[string][32]byte
var err error var err error
hashes, err = build.HashFolder(".", []string{"tests/testdata", "build/cache"}) hashes, err = build.HashFolder(".", []string{"tests/testdata", "build/cache", ".git"})
if err != nil { if err != nil {
log.Fatal("Error computing hashes", "err", err) log.Fatal("Error computing hashes", "err", err)
} }
@ -390,13 +372,13 @@ func doCheckGenerate() {
protocPath = downloadProtoc(*cachedir) protocPath = downloadProtoc(*cachedir)
protocGenGoPath = downloadProtocGenGo(*cachedir) protocGenGoPath = downloadProtocGenGo(*cachedir)
) )
c := new(build.GoToolchain).Go("generate", "./...") c := tc.Go("generate", "./...")
pathList := []string{filepath.Join(protocPath, "bin"), protocGenGoPath, os.Getenv("PATH")} pathList := []string{filepath.Join(protocPath, "bin"), protocGenGoPath, os.Getenv("PATH")}
c.Env = append(c.Env, "PATH="+strings.Join(pathList, string(os.PathListSeparator))) c.Env = append(c.Env, "PATH="+strings.Join(pathList, string(os.PathListSeparator)))
build.MustRun(c) build.MustRun(c)
// Check if generate file hashes have changed // Check if generate file hashes have changed
generated, err := build.HashFolder(".", []string{"tests/testdata", "build/cache"}) generated, err := build.HashFolder(".", []string{"tests/testdata", "build/cache", ".git"})
if err != nil { if err != nil {
log.Fatalf("Error re-computing hashes: %v", err) log.Fatalf("Error re-computing hashes: %v", err)
} }
@ -408,6 +390,10 @@ func doCheckGenerate() {
log.Fatal("One or more generated files were updated by running 'go generate ./...'") log.Fatal("One or more generated files were updated by running 'go generate ./...'")
} }
fmt.Println("No stale files detected.") fmt.Println("No stale files detected.")
// Run go mod tidy check.
build.MustRun(tc.Go("mod", "tidy", "-diff"))
fmt.Println("No untidy module files detected.")
} }
// doCheckBadDeps verifies whether certain unintended dependencies between some // doCheckBadDeps verifies whether certain unintended dependencies between some
@ -848,7 +834,7 @@ func downloadGoBootstrapSources(cachedir string) []string {
csdb := build.MustLoadChecksums("build/checksums.txt") csdb := build.MustLoadChecksums("build/checksums.txt")
var bundles []string var bundles []string
for _, booter := range []string{"ppa-builder-1", "ppa-builder-2"} { for _, booter := range []string{"ppa-builder-1.19", "ppa-builder-1.21", "ppa-builder-1.23"} {
gobootVersion, err := build.Version(csdb, booter) gobootVersion, err := build.Version(csdb, booter)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)

View file

@ -21,7 +21,8 @@ override_dh_auto_build:
# requirements opposed to older versions of Go. # requirements opposed to older versions of Go.
(mv .goboot-1 ../ && cd ../.goboot-1/src && ./make.bash) (mv .goboot-1 ../ && cd ../.goboot-1/src && ./make.bash)
(mv .goboot-2 ../ && cd ../.goboot-2/src && GOROOT_BOOTSTRAP=`pwd`/../../.goboot-1 ./make.bash) (mv .goboot-2 ../ && cd ../.goboot-2/src && GOROOT_BOOTSTRAP=`pwd`/../../.goboot-1 ./make.bash)
(mv .go ../ && cd ../.go/src && GOROOT_BOOTSTRAP=`pwd`/../../.goboot-2 ./make.bash) (mv .goboot-3 ../ && cd ../.goboot-3/src && GOROOT_BOOTSTRAP=`pwd`/../../.goboot-2 ./make.bash)
(mv .go ../ && cd ../.go/src && GOROOT_BOOTSTRAP=`pwd`/../../.goboot-3 ./make.bash)
# We can't download external go modules within Launchpad, so we're shipping the # We can't download external go modules within Launchpad, so we're shipping the
# entire dependency source cache with go-ethereum. # entire dependency source cache with go-ethereum.

View file

@ -93,7 +93,7 @@ func init() {
} }
func abigen(c *cli.Context) error { func abigen(c *cli.Context) error {
utils.CheckExclusive(c, abiFlag, jsonFlag) // Only one source can be selected. flags.CheckExclusive(c, abiFlag, jsonFlag) // Only one source can be selected.
if c.String(pkgFlag.Name) == "" { if c.String(pkgFlag.Name) == "" {
utils.Fatalf("No destination package specified (--pkg)") utils.Fatalf("No destination package specified (--pkg)")

View file

@ -24,6 +24,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"maps"
"math/big" "math/big"
"os" "os"
"path/filepath" "path/filepath"
@ -40,7 +41,6 @@ import (
"github.com/ethereum/go-ethereum/eth/protocols/eth" "github.com/ethereum/go-ethereum/eth/protocols/eth"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rlp"
"golang.org/x/exp/maps"
) )
// Chain is a lightweight blockchain-like store which can read a hivechain // Chain is a lightweight blockchain-like store which can read a hivechain
@ -162,8 +162,8 @@ func (c *Chain) RootAt(height int) common.Hash {
// GetSender returns the address associated with account at the index in the // GetSender returns the address associated with account at the index in the
// pre-funded accounts list. // pre-funded accounts list.
func (c *Chain) GetSender(idx int) (common.Address, uint64) { func (c *Chain) GetSender(idx int) (common.Address, uint64) {
accounts := maps.Keys(c.senders) accounts := slices.SortedFunc(maps.Keys(c.senders), common.Address.Cmp)
slices.SortFunc(accounts, common.Address.Cmp)
addr := accounts[idx] addr := accounts[idx]
return addr, c.senders[addr].Nonce return addr, c.senders[addr].Nonce
} }

View file

@ -130,11 +130,16 @@ func (c *Conn) Write(proto Proto, code uint64, msg any) error {
return err return err
} }
var errDisc error = fmt.Errorf("disconnect")
// ReadEth reads an Eth sub-protocol wire message. // ReadEth reads an Eth sub-protocol wire message.
func (c *Conn) ReadEth() (any, error) { func (c *Conn) ReadEth() (any, error) {
c.SetReadDeadline(time.Now().Add(timeout)) c.SetReadDeadline(time.Now().Add(timeout))
for { for {
code, data, _, err := c.Conn.Read() code, data, _, err := c.Conn.Read()
if code == discMsg {
return nil, errDisc
}
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -17,8 +17,12 @@
package ethtest package ethtest
import ( import (
"context"
"crypto/rand" "crypto/rand"
"fmt"
"reflect" "reflect"
"sync"
"time"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus/misc/eip4844" "github.com/ethereum/go-ethereum/consensus/misc/eip4844"
@ -79,6 +83,8 @@ func (s *Suite) EthTests() []utesting.Test {
{Name: "InvalidTxs", Fn: s.TestInvalidTxs}, {Name: "InvalidTxs", Fn: s.TestInvalidTxs},
{Name: "NewPooledTxs", Fn: s.TestNewPooledTxs}, {Name: "NewPooledTxs", Fn: s.TestNewPooledTxs},
{Name: "BlobViolations", Fn: s.TestBlobViolations}, {Name: "BlobViolations", Fn: s.TestBlobViolations},
{Name: "TestBlobTxWithoutSidecar", Fn: s.TestBlobTxWithoutSidecar},
{Name: "TestBlobTxWithMismatchedSidecar", Fn: s.TestBlobTxWithMismatchedSidecar},
} }
} }
@ -825,3 +831,194 @@ func (s *Suite) TestBlobViolations(t *utesting.T) {
conn.Close() conn.Close()
} }
} }
// mangleSidecar returns a copy of the given blob transaction where the sidecar
// data has been modified to produce a different commitment hash.
func mangleSidecar(tx *types.Transaction) *types.Transaction {
sidecar := tx.BlobTxSidecar()
copy := types.BlobTxSidecar{
Blobs: append([]kzg4844.Blob{}, sidecar.Blobs...),
Commitments: append([]kzg4844.Commitment{}, sidecar.Commitments...),
Proofs: append([]kzg4844.Proof{}, sidecar.Proofs...),
}
// zero the first commitment to alter the sidecar hash
copy.Commitments[0] = kzg4844.Commitment{}
return tx.WithBlobTxSidecar(&copy)
}
func (s *Suite) TestBlobTxWithoutSidecar(t *utesting.T) {
t.Log(`This test checks that a blob transaction first advertised/transmitted without blobs will result in the sending peer being disconnected, and the full transaction should be successfully retrieved from another peer.`)
tx := s.makeBlobTxs(1, 2, 42)[0]
badTx := tx.WithoutBlobTxSidecar()
s.testBadBlobTx(t, tx, badTx)
}
func (s *Suite) TestBlobTxWithMismatchedSidecar(t *utesting.T) {
t.Log(`This test checks that a blob transaction first advertised/transmitted without blobs, whose commitment don't correspond to the blob_versioned_hashes in the transaction, will result in the sending peer being disconnected, and the full transaction should be successfully retrieved from another peer.`)
tx := s.makeBlobTxs(1, 2, 43)[0]
badTx := mangleSidecar(tx)
s.testBadBlobTx(t, tx, badTx)
}
// readUntil reads eth protocol messages until a message of the target type is
// received. It returns an error if there is a disconnect, or if the context
// is cancelled before a message of the desired type can be read.
func readUntil[T any](ctx context.Context, conn *Conn) (*T, error) {
for {
select {
case <-ctx.Done():
return nil, context.Canceled
default:
}
received, err := conn.ReadEth()
if err != nil {
if err == errDisc {
return nil, errDisc
}
continue
}
switch res := received.(type) {
case *T:
return res, nil
}
}
}
// readUntilDisconnect reads eth protocol messages until the peer disconnects.
// It returns whether the peer disconnects in the next 100ms.
func readUntilDisconnect(conn *Conn) (disconnected bool) {
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
_, err := readUntil[struct{}](ctx, conn)
return err == errDisc
}
func (s *Suite) testBadBlobTx(t *utesting.T, tx *types.Transaction, badTx *types.Transaction) {
stage1, stage2, stage3 := new(sync.WaitGroup), new(sync.WaitGroup), new(sync.WaitGroup)
stage1.Add(1)
stage2.Add(1)
stage3.Add(1)
errc := make(chan error)
badPeer := func() {
// announce the correct hash from the bad peer.
// when the transaction is first requested before transmitting it from the bad peer,
// trigger step 2: connection and announcement by good peers
conn, err := s.dial()
if err != nil {
errc <- fmt.Errorf("dial fail: %v", err)
return
}
defer conn.Close()
if err := conn.peer(s.chain, nil); err != nil {
errc <- fmt.Errorf("bad peer: peering failed: %v", err)
return
}
ann := eth.NewPooledTransactionHashesPacket{
Types: []byte{types.BlobTxType},
Sizes: []uint32{uint32(badTx.Size())},
Hashes: []common.Hash{badTx.Hash()},
}
if err := conn.Write(ethProto, eth.NewPooledTransactionHashesMsg, ann); err != nil {
errc <- fmt.Errorf("sending announcement failed: %v", err)
return
}
req, err := readUntil[eth.GetPooledTransactionsPacket](context.Background(), conn)
if err != nil {
errc <- fmt.Errorf("failed to read GetPooledTransactions message: %v", err)
return
}
stage1.Done()
stage2.Wait()
// the good peer is connected, and has announced the tx.
// proceed to send the incorrect one from the bad peer.
resp := eth.PooledTransactionsPacket{RequestId: req.RequestId, PooledTransactionsResponse: eth.PooledTransactionsResponse(types.Transactions{badTx})}
if err := conn.Write(ethProto, eth.PooledTransactionsMsg, resp); err != nil {
errc <- fmt.Errorf("writing pooled tx response failed: %v", err)
return
}
if !readUntilDisconnect(conn) {
errc <- fmt.Errorf("expected bad peer to be disconnected")
return
}
stage3.Done()
}
goodPeer := func() {
stage1.Wait()
conn, err := s.dial()
if err != nil {
errc <- fmt.Errorf("dial fail: %v", err)
return
}
defer conn.Close()
if err := conn.peer(s.chain, nil); err != nil {
errc <- fmt.Errorf("peering failed: %v", err)
return
}
ann := eth.NewPooledTransactionHashesPacket{
Types: []byte{types.BlobTxType},
Sizes: []uint32{uint32(tx.Size())},
Hashes: []common.Hash{tx.Hash()},
}
if err := conn.Write(ethProto, eth.NewPooledTransactionHashesMsg, ann); err != nil {
errc <- fmt.Errorf("sending announcement failed: %v", err)
return
}
// wait until the bad peer has transmitted the incorrect transaction
stage2.Done()
stage3.Wait()
// the bad peer has transmitted the bad tx, and been disconnected.
// transmit the same tx but with correct sidecar from the good peer.
var req *eth.GetPooledTransactionsPacket
req, err = readUntil[eth.GetPooledTransactionsPacket](context.Background(), conn)
if err != nil {
errc <- fmt.Errorf("reading pooled tx request failed: %v", err)
return
}
if req.GetPooledTransactionsRequest[0] != tx.Hash() {
errc <- fmt.Errorf("requested unknown tx hash")
return
}
resp := eth.PooledTransactionsPacket{RequestId: req.RequestId, PooledTransactionsResponse: eth.PooledTransactionsResponse(types.Transactions{tx})}
if err := conn.Write(ethProto, eth.PooledTransactionsMsg, resp); err != nil {
errc <- fmt.Errorf("writing pooled tx response failed: %v", err)
return
}
if readUntilDisconnect(conn) {
errc <- fmt.Errorf("unexpected disconnect")
return
}
close(errc)
}
if err := s.engine.sendForkchoiceUpdated(); err != nil {
t.Fatalf("send fcu failed: %v", err)
}
go goodPeer()
go badPeer()
err := <-errc
if err != nil {
t.Fatalf("%v", err)
}
}

View file

@ -20,6 +20,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"maps"
"os" "os"
"regexp" "regexp"
"slices" "slices"
@ -28,7 +29,6 @@ import (
"github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/tests" "github.com/ethereum/go-ethereum/tests"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"golang.org/x/exp/maps"
) )
var blockTestCommand = &cli.Command{ var blockTestCommand = &cli.Command{
@ -80,8 +80,7 @@ func runBlockTest(ctx *cli.Context, fname string) ([]testResult, error) {
tracer := tracerFromFlags(ctx) tracer := tracerFromFlags(ctx)
// Pull out keys to sort and ensure tests are run in order. // Pull out keys to sort and ensure tests are run in order.
keys := maps.Keys(tests) keys := slices.Sorted(maps.Keys(tests))
slices.Sort(keys)
// Run all the tests. // Run all the tests.
var results []testResult var results []testResult

View file

@ -1198,7 +1198,7 @@ func setWS(ctx *cli.Context, cfg *node.Config) {
// setIPC creates an IPC path configuration from the set command line flags, // setIPC creates an IPC path configuration from the set command line flags,
// returning an empty string if IPC was explicitly disabled, or the set path. // returning an empty string if IPC was explicitly disabled, or the set path.
func setIPC(ctx *cli.Context, cfg *node.Config) { func setIPC(ctx *cli.Context, cfg *node.Config) {
CheckExclusive(ctx, IPCDisabledFlag, IPCPathFlag) flags.CheckExclusive(ctx, IPCDisabledFlag, IPCPathFlag)
switch { switch {
case ctx.Bool(IPCDisabledFlag.Name): case ctx.Bool(IPCDisabledFlag.Name):
cfg.IPCPath = "" cfg.IPCPath = ""
@ -1296,8 +1296,8 @@ func SetP2PConfig(ctx *cli.Context, cfg *p2p.Config) {
cfg.NoDiscovery = true cfg.NoDiscovery = true
} }
CheckExclusive(ctx, DiscoveryV4Flag, NoDiscoverFlag) flags.CheckExclusive(ctx, DiscoveryV4Flag, NoDiscoverFlag)
CheckExclusive(ctx, DiscoveryV5Flag, NoDiscoverFlag) flags.CheckExclusive(ctx, DiscoveryV5Flag, NoDiscoverFlag)
cfg.DiscoveryV4 = ctx.Bool(DiscoveryV4Flag.Name) cfg.DiscoveryV4 = ctx.Bool(DiscoveryV4Flag.Name)
cfg.DiscoveryV5 = ctx.Bool(DiscoveryV5Flag.Name) cfg.DiscoveryV5 = ctx.Bool(DiscoveryV5Flag.Name)
@ -1529,52 +1529,11 @@ func setRequiredBlocks(ctx *cli.Context, cfg *ethconfig.Config) {
} }
} }
// CheckExclusive verifies that only a single instance of the provided flags was
// set by the user. Each flag might optionally be followed by a string type to
// specialize it further.
func CheckExclusive(ctx *cli.Context, args ...interface{}) {
set := make([]string, 0, 1)
for i := 0; i < len(args); i++ {
// Make sure the next argument is a flag and skip if not set
flag, ok := args[i].(cli.Flag)
if !ok {
panic(fmt.Sprintf("invalid argument, not cli.Flag type: %T", args[i]))
}
// Check if next arg extends current and expand its name if so
name := flag.Names()[0]
if i+1 < len(args) {
switch option := args[i+1].(type) {
case string:
// Extended flag check, make sure value set doesn't conflict with passed in option
if ctx.String(flag.Names()[0]) == option {
name += "=" + option
set = append(set, "--"+name)
}
// shift arguments and continue
i++
continue
case cli.Flag:
default:
panic(fmt.Sprintf("invalid argument, not cli.Flag or string extension: %T", args[i+1]))
}
}
// Mark the flag if it's set
if ctx.IsSet(flag.Names()[0]) {
set = append(set, "--"+name)
}
}
if len(set) > 1 {
Fatalf("Flags %v can't be used at the same time", strings.Join(set, ", "))
}
}
// SetEthConfig applies eth-related command line flags to the config. // SetEthConfig applies eth-related command line flags to the config.
func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) { func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) {
// Avoid conflicting network flags // Avoid conflicting network flags
CheckExclusive(ctx, MainnetFlag, DeveloperFlag, SepoliaFlag, HoleskyFlag) flags.CheckExclusive(ctx, MainnetFlag, DeveloperFlag, SepoliaFlag, HoleskyFlag)
CheckExclusive(ctx, DeveloperFlag, ExternalSignerFlag) // Can't use both ephemeral unlocked and external signer flags.CheckExclusive(ctx, DeveloperFlag, ExternalSignerFlag) // Can't use both ephemeral unlocked and external signer
// Set configurations from CLI flags // Set configurations from CLI flags
setEtherbase(ctx, cfg) setEtherbase(ctx, cfg)
@ -1840,7 +1799,7 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) {
func MakeBeaconLightConfig(ctx *cli.Context) bparams.ClientConfig { func MakeBeaconLightConfig(ctx *cli.Context) bparams.ClientConfig {
var config bparams.ClientConfig var config bparams.ClientConfig
customConfig := ctx.IsSet(BeaconConfigFlag.Name) customConfig := ctx.IsSet(BeaconConfigFlag.Name)
CheckExclusive(ctx, MainnetFlag, SepoliaFlag, HoleskyFlag, BeaconConfigFlag) flags.CheckExclusive(ctx, MainnetFlag, SepoliaFlag, HoleskyFlag, BeaconConfigFlag)
switch { switch {
case ctx.Bool(MainnetFlag.Name): case ctx.Bool(MainnetFlag.Name):
config.ChainConfig = *bparams.MainnetLightConfig config.ChainConfig = *bparams.MainnetLightConfig

29
cmd/workload/README.md Normal file
View file

@ -0,0 +1,29 @@
## Workload Testing Tool
This tool performs RPC calls against a live node. It has tests for the Sepolia testnet and
Mainnet. Note the tests require a fully synced node.
To run the tests against a Sepolia node, use:
```shell
> ./workload test --sepolia http://host:8545
```
To run a specific test, use the `--run` flag to filter the test cases. Filtering works
similar to the `go test` command. For example, to run only tests for `eth_getBlockByHash`
and `eth_getBlockByNumber`, use this command:
```
> ./workload test --sepolia --run History/getBlockBy http://host:8545
```
### Regenerating tests
There is a facility for updating the tests from the chain. This can also be used to
generate the tests for a new network. As an example, to recreate tests for mainnet, run
the following commands (in this directory) against a synced mainnet node:
```shell
> go run . filtergen --queries queries/filter_queries_mainnet.json http://host:8545
> go run . historygen --history-tests queries/history_mainnet.json http://host:8545
```

216
cmd/workload/filtertest.go Normal file
View file

@ -0,0 +1,216 @@
// Copyright 2025 The go-ethereum Authors
// This file is part of go-ethereum.
//
// go-ethereum is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// go-ethereum 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
package main
import (
"context"
"encoding/json"
"fmt"
"math/big"
"time"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/internal/utesting"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/rpc"
)
type filterTestSuite struct {
cfg testConfig
queries [][]*filterQuery
}
func newFilterTestSuite(cfg testConfig) *filterTestSuite {
s := &filterTestSuite{cfg: cfg}
if err := s.loadQueries(); err != nil {
exit(err)
}
return s
}
func (s *filterTestSuite) allTests() []utesting.Test {
return []utesting.Test{
{Name: "Filter/ShortRange", Fn: s.filterShortRange},
{Name: "Filter/LongRange", Fn: s.filterLongRange, Slow: true},
{Name: "Filter/FullRange", Fn: s.filterFullRange, Slow: true},
}
}
func (s *filterTestSuite) filterRange(t *utesting.T, test func(query *filterQuery) bool, do func(t *utesting.T, query *filterQuery)) {
var count, total int
for _, bucket := range s.queries {
for _, query := range bucket {
if test(query) {
total++
}
}
}
if total == 0 {
t.Fatalf("No suitable queries available")
}
start := time.Now()
last := start
for _, bucket := range s.queries {
for _, query := range bucket {
if test(query) {
do(t, query)
count++
if time.Since(last) > time.Second*5 {
t.Logf("Making filter query %d/%d (elapsed: %v)", count, total, time.Since(start))
last = time.Now()
}
}
}
}
t.Logf("Made %d filter queries (elapsed: %v)", count, time.Since(start))
}
const filterRangeThreshold = 10000
// filterShortRange runs all short-range filter tests.
func (s *filterTestSuite) filterShortRange(t *utesting.T) {
s.filterRange(t, func(query *filterQuery) bool {
return query.ToBlock+1-query.FromBlock <= filterRangeThreshold
}, s.queryAndCheck)
}
// filterShortRange runs all long-range filter tests.
func (s *filterTestSuite) filterLongRange(t *utesting.T) {
s.filterRange(t, func(query *filterQuery) bool {
return query.ToBlock+1-query.FromBlock > filterRangeThreshold
}, s.queryAndCheck)
}
// filterFullRange runs all filter tests, extending their range from genesis up
// to the latest block. Note that results are only partially verified in this mode.
func (s *filterTestSuite) filterFullRange(t *utesting.T) {
finalized := mustGetFinalizedBlock(s.cfg.client)
s.filterRange(t, func(query *filterQuery) bool {
return query.ToBlock+1-query.FromBlock > finalized/2
}, s.fullRangeQueryAndCheck)
}
func (s *filterTestSuite) queryAndCheck(t *utesting.T, query *filterQuery) {
query.run(s.cfg.client)
if query.Err != nil {
t.Errorf("Filter query failed (fromBlock: %d toBlock: %d addresses: %v topics: %v error: %v)", query.FromBlock, query.ToBlock, query.Address, query.Topics, query.Err)
return
}
if *query.ResultHash != query.calculateHash() {
t.Fatalf("Filter query result mismatch (fromBlock: %d toBlock: %d addresses: %v topics: %v)", query.FromBlock, query.ToBlock, query.Address, query.Topics)
}
}
func (s *filterTestSuite) fullRangeQueryAndCheck(t *utesting.T, query *filterQuery) {
frQuery := &filterQuery{ // create full range query
FromBlock: 0,
ToBlock: int64(rpc.LatestBlockNumber),
Address: query.Address,
Topics: query.Topics,
}
frQuery.run(s.cfg.client)
if frQuery.Err != nil {
t.Errorf("Full range filter query failed (addresses: %v topics: %v error: %v)", frQuery.Address, frQuery.Topics, frQuery.Err)
return
}
// filter out results outside the original query range
j := 0
for _, log := range frQuery.results {
if int64(log.BlockNumber) >= query.FromBlock && int64(log.BlockNumber) <= query.ToBlock {
frQuery.results[j] = log
j++
}
}
frQuery.results = frQuery.results[:j]
if *query.ResultHash != frQuery.calculateHash() {
t.Fatalf("Full range filter query result mismatch (fromBlock: %d toBlock: %d addresses: %v topics: %v)", query.FromBlock, query.ToBlock, query.Address, query.Topics)
}
}
func (s *filterTestSuite) loadQueries() error {
file, err := s.cfg.fsys.Open(s.cfg.filterQueryFile)
if err != nil {
return fmt.Errorf("can't open filterQueryFile: %v", err)
}
defer file.Close()
var queries [][]*filterQuery
if err := json.NewDecoder(file).Decode(&queries); err != nil {
return fmt.Errorf("invalid JSON in %s: %v", s.cfg.filterQueryFile, err)
}
var count int
for _, bucket := range queries {
count += len(bucket)
}
if count == 0 {
return fmt.Errorf("filterQueryFile %s is empty", s.cfg.filterQueryFile)
}
s.queries = queries
return nil
}
// filterQuery is a single query for testing.
type filterQuery struct {
FromBlock int64 `json:"fromBlock"`
ToBlock int64 `json:"toBlock"`
Address []common.Address `json:"address"`
Topics [][]common.Hash `json:"topics"`
ResultHash *common.Hash `json:"resultHash,omitempty"`
results []types.Log
Err error `json:"error,omitempty"`
}
func (fq *filterQuery) isWildcard() bool {
if len(fq.Address) != 0 {
return false
}
for _, topics := range fq.Topics {
if len(topics) != 0 {
return false
}
}
return true
}
func (fq *filterQuery) calculateHash() common.Hash {
enc, err := rlp.EncodeToBytes(&fq.results)
if err != nil {
exit(fmt.Errorf("Error encoding logs: %v", err))
}
return crypto.Keccak256Hash(enc)
}
func (fq *filterQuery) run(client *client) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
defer cancel()
logs, err := client.Eth.FilterLogs(ctx, ethereum.FilterQuery{
FromBlock: big.NewInt(fq.FromBlock),
ToBlock: big.NewInt(fq.ToBlock),
Addresses: fq.Address,
Topics: fq.Topics,
})
if err != nil {
fq.Err = err
fmt.Printf("Filter query failed: fromBlock: %d toBlock: %d addresses: %v topics: %v error: %v\n",
fq.FromBlock, fq.ToBlock, fq.Address, fq.Topics, err)
return
}
fq.results = logs
}

View file

@ -0,0 +1,382 @@
// Copyright 2025 The go-ethereum Authors
// This file is part of go-ethereum.
//
// go-ethereum is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// go-ethereum 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
package main
import (
"context"
"encoding/json"
"fmt"
"math"
"math/big"
"math/rand"
"os"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/internal/flags"
"github.com/ethereum/go-ethereum/rpc"
"github.com/urfave/cli/v2"
)
var (
filterGenerateCommand = &cli.Command{
Name: "filtergen",
Usage: "Generates query set for log filter workload test",
ArgsUsage: "<RPC endpoint URL>",
Action: filterGenCmd,
Flags: []cli.Flag{
filterQueryFileFlag,
filterErrorFileFlag,
},
}
filterQueryFileFlag = &cli.StringFlag{
Name: "queries",
Usage: "JSON file containing filter test queries",
Value: "filter_queries.json",
Category: flags.TestingCategory,
}
filterErrorFileFlag = &cli.StringFlag{
Name: "errors",
Usage: "JSON file containing failed filter queries",
Value: "filter_errors.json",
Category: flags.TestingCategory,
}
)
// filterGenCmd is the main function of the filter tests generator.
func filterGenCmd(ctx *cli.Context) error {
f := newFilterTestGen(ctx)
lastWrite := time.Now()
for {
select {
case <-ctx.Done():
return nil
default:
}
f.updateFinalizedBlock()
query := f.newQuery()
query.run(f.client)
if query.Err != nil {
f.errors = append(f.errors, query)
continue
}
if len(query.results) > 0 && len(query.results) <= maxFilterResultSize {
for {
extQuery := f.extendRange(query)
if extQuery == nil {
break
}
extQuery.run(f.client)
if extQuery.Err == nil && len(extQuery.results) < len(query.results) {
extQuery.Err = fmt.Errorf("invalid result length; old range %d %d; old length %d; new range %d %d; new length %d; address %v; Topics %v",
query.FromBlock, query.ToBlock, len(query.results),
extQuery.FromBlock, extQuery.ToBlock, len(extQuery.results),
extQuery.Address, extQuery.Topics,
)
}
if extQuery.Err != nil {
f.errors = append(f.errors, extQuery)
break
}
if len(extQuery.results) > maxFilterResultSize {
break
}
query = extQuery
}
f.storeQuery(query)
if time.Since(lastWrite) > time.Second*10 {
f.writeQueries()
f.writeErrors()
lastWrite = time.Now()
}
}
}
}
// filterTestGen is the filter query test generator.
type filterTestGen struct {
client *client
queryFile string
errorFile string
finalizedBlock int64
queries [filterBuckets][]*filterQuery
errors []*filterQuery
}
func newFilterTestGen(ctx *cli.Context) *filterTestGen {
return &filterTestGen{
client: makeClient(ctx),
queryFile: ctx.String(filterQueryFileFlag.Name),
errorFile: ctx.String(filterErrorFileFlag.Name),
}
}
func (s *filterTestGen) updateFinalizedBlock() {
s.finalizedBlock = mustGetFinalizedBlock(s.client)
}
const (
// Parameter of the random filter query generator.
maxFilterRange = 10000000
maxFilterResultSize = 300
filterBuckets = 10
maxFilterBucketSize = 100
filterSeedChance = 10
filterMergeChance = 45
)
// storeQuery adds a filter query to the output file.
func (s *filterTestGen) storeQuery(query *filterQuery) {
query.ResultHash = new(common.Hash)
*query.ResultHash = query.calculateHash()
logRatio := math.Log(float64(len(query.results))*float64(s.finalizedBlock)/float64(query.ToBlock+1-query.FromBlock)) / math.Log(float64(s.finalizedBlock)*maxFilterResultSize)
bucket := int(math.Floor(logRatio * filterBuckets))
if bucket >= filterBuckets {
bucket = filterBuckets - 1
}
if len(s.queries[bucket]) < maxFilterBucketSize {
s.queries[bucket] = append(s.queries[bucket], query)
} else {
s.queries[bucket][rand.Intn(len(s.queries[bucket]))] = query
}
fmt.Print("Generated queries per bucket:")
for _, list := range s.queries {
fmt.Print(" ", len(list))
}
fmt.Println()
}
func (s *filterTestGen) extendRange(q *filterQuery) *filterQuery {
rangeLen := q.ToBlock + 1 - q.FromBlock
extLen := rand.Int63n(rangeLen) + 1
if rangeLen+extLen > s.finalizedBlock {
return nil
}
extBefore := min(rand.Int63n(extLen+1), q.FromBlock)
extAfter := extLen - extBefore
if q.ToBlock+extAfter > s.finalizedBlock {
d := q.ToBlock + extAfter - s.finalizedBlock
extAfter -= d
if extBefore+d <= q.FromBlock {
extBefore += d
} else {
extBefore = q.FromBlock
}
}
return &filterQuery{
FromBlock: q.FromBlock - extBefore,
ToBlock: q.ToBlock + extAfter,
Address: q.Address,
Topics: q.Topics,
}
}
// newQuery generates a new filter query.
func (s *filterTestGen) newQuery() *filterQuery {
for {
t := rand.Intn(100)
if t < filterSeedChance {
return s.newSeedQuery()
}
if t < filterSeedChance+filterMergeChance {
if query := s.newMergedQuery(); query != nil {
return query
}
continue
}
if query := s.newNarrowedQuery(); query != nil {
return query
}
}
}
// newSeedQuery creates a query that gets all logs in a random non-finalized block.
func (s *filterTestGen) newSeedQuery() *filterQuery {
block := rand.Int63n(s.finalizedBlock + 1)
return &filterQuery{
FromBlock: block,
ToBlock: block,
}
}
// newMergedQuery creates a new query by combining (with OR) the filter criteria
// of two existing queries (chosen at random).
func (s *filterTestGen) newMergedQuery() *filterQuery {
q1 := s.randomQuery()
q2 := s.randomQuery()
if q1 == nil || q2 == nil || q1 == q2 {
return nil
}
var (
block int64
topicCount int
)
if rand.Intn(2) == 0 {
block = q1.FromBlock + rand.Int63n(q1.ToBlock+1-q1.FromBlock)
topicCount = len(q1.Topics)
} else {
block = q2.FromBlock + rand.Int63n(q2.ToBlock+1-q2.FromBlock)
topicCount = len(q2.Topics)
}
m := &filterQuery{
FromBlock: block,
ToBlock: block,
Topics: make([][]common.Hash, topicCount),
}
for _, addr := range q1.Address {
if rand.Intn(2) == 0 {
m.Address = append(m.Address, addr)
}
}
for _, addr := range q2.Address {
if rand.Intn(2) == 0 {
m.Address = append(m.Address, addr)
}
}
for i := range m.Topics {
if len(q1.Topics) > i {
for _, topic := range q1.Topics[i] {
if rand.Intn(2) == 0 {
m.Topics[i] = append(m.Topics[i], topic)
}
}
}
if len(q2.Topics) > i {
for _, topic := range q2.Topics[i] {
if rand.Intn(2) == 0 {
m.Topics[i] = append(m.Topics[i], topic)
}
}
}
}
return m
}
// newNarrowedQuery creates a new query by 'narrowing' an existing (randomly chosen)
// query. The new query is made more specific by analyzing the filter criteria and adding
// topics/addresses from the known result set.
func (s *filterTestGen) newNarrowedQuery() *filterQuery {
q := s.randomQuery()
if q == nil {
return nil
}
log := q.results[rand.Intn(len(q.results))]
var emptyCount int
if len(q.Address) == 0 {
emptyCount++
}
for i := range log.Topics {
if len(q.Topics) <= i || len(q.Topics[i]) == 0 {
emptyCount++
}
}
if emptyCount == 0 {
return nil
}
query := &filterQuery{
FromBlock: q.FromBlock,
ToBlock: q.ToBlock,
Address: make([]common.Address, len(q.Address)),
Topics: make([][]common.Hash, len(q.Topics)),
}
copy(query.Address, q.Address)
for i, topics := range q.Topics {
if len(topics) > 0 {
query.Topics[i] = make([]common.Hash, len(topics))
copy(query.Topics[i], topics)
}
}
pick := rand.Intn(emptyCount)
if len(query.Address) == 0 {
if pick == 0 {
query.Address = []common.Address{log.Address}
return query
}
pick--
}
for i := range log.Topics {
if len(query.Topics) <= i || len(query.Topics[i]) == 0 {
if pick == 0 {
if len(query.Topics) <= i {
query.Topics = append(query.Topics, make([][]common.Hash, i+1-len(query.Topics))...)
}
query.Topics[i] = []common.Hash{log.Topics[i]}
return query
}
pick--
}
}
panic("unreachable")
}
// randomQuery returns a random query from the ones that were already generated.
func (s *filterTestGen) randomQuery() *filterQuery {
var bucket, bucketCount int
for _, list := range s.queries {
if len(list) > 0 {
bucketCount++
}
}
if bucketCount == 0 {
return nil
}
pick := rand.Intn(bucketCount)
for i, list := range s.queries {
if len(list) > 0 {
if pick == 0 {
bucket = i
break
}
pick--
}
}
return s.queries[bucket][rand.Intn(len(s.queries[bucket]))]
}
// writeQueries serializes the generated queries to the output file.
func (s *filterTestGen) writeQueries() {
file, err := os.Create(s.queryFile)
if err != nil {
exit(fmt.Errorf("Error creating filter test query file %s: %v", s.queryFile, err))
return
}
json.NewEncoder(file).Encode(&s.queries)
file.Close()
}
// writeQueries serializes the generated errors to the error file.
func (s *filterTestGen) writeErrors() {
file, err := os.Create(s.errorFile)
if err != nil {
exit(fmt.Errorf("Error creating filter error file %s: %v", s.errorFile, err))
return
}
defer file.Close()
json.NewEncoder(file).Encode(s.errors)
}
func mustGetFinalizedBlock(client *client) int64 {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
header, err := client.Eth.HeaderByNumber(ctx, big.NewInt(int64(rpc.FinalizedBlockNumber)))
if err != nil {
exit(fmt.Errorf("could not fetch finalized header (error: %v)", err))
}
return header.Number.Int64()
}

View file

@ -0,0 +1,137 @@
// Copyright 2025 The go-ethereum Authors
// This file is part of go-ethereum.
//
// go-ethereum is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// go-ethereum 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
package main
import (
"fmt"
"math/rand"
"slices"
"sort"
"time"
"github.com/urfave/cli/v2"
)
var (
filterPerfCommand = &cli.Command{
Name: "filterperf",
Usage: "Runs log filter performance test against an RPC endpoint",
ArgsUsage: "<RPC endpoint URL>",
Action: filterPerfCmd,
Flags: []cli.Flag{
testSepoliaFlag,
testMainnetFlag,
filterQueryFileFlag,
filterErrorFileFlag,
},
}
)
const passCount = 1
func filterPerfCmd(ctx *cli.Context) error {
cfg := testConfigFromCLI(ctx)
f := newFilterTestSuite(cfg)
type queryTest struct {
query *filterQuery
bucket, index int
runtime []time.Duration
medianTime time.Duration
}
var queries, processed []queryTest
for i, bucket := range f.queries[:] {
for j, query := range bucket {
queries = append(queries, queryTest{query: query, bucket: i, index: j})
}
}
// Run test queries.
var failed, mismatch int
for i := 1; i <= passCount; i++ {
fmt.Println("Performance test pass", i, "/", passCount)
for len(queries) > 0 {
pick := rand.Intn(len(queries))
qt := queries[pick]
queries[pick] = queries[len(queries)-1]
queries = queries[:len(queries)-1]
start := time.Now()
qt.query.run(cfg.client)
qt.runtime = append(qt.runtime, time.Since(start))
slices.Sort(qt.runtime)
qt.medianTime = qt.runtime[len(qt.runtime)/2]
if qt.query.Err != nil {
failed++
continue
}
if rhash := qt.query.calculateHash(); *qt.query.ResultHash != rhash {
fmt.Printf("Filter query result mismatch: fromBlock: %d toBlock: %d addresses: %v topics: %v expected hash: %064x calculated hash: %064x\n", qt.query.FromBlock, qt.query.ToBlock, qt.query.Address, qt.query.Topics, *qt.query.ResultHash, rhash)
continue
}
processed = append(processed, qt)
if len(processed)%50 == 0 {
fmt.Println(" processed:", len(processed), "remaining", len(queries), "failed:", failed, "result mismatch:", mismatch)
}
}
queries, processed = processed, nil
}
// Show results and stats.
fmt.Println("Performance test finished; processed:", len(queries), "failed:", failed, "result mismatch:", mismatch)
stats := make([]bucketStats, len(f.queries))
var wildcardStats bucketStats
for _, qt := range queries {
bs := &stats[qt.bucket]
if qt.query.isWildcard() {
bs = &wildcardStats
}
bs.blocks += qt.query.ToBlock + 1 - qt.query.FromBlock
bs.count++
bs.logs += len(qt.query.results)
bs.runtime += qt.medianTime
}
fmt.Println()
for i := range stats {
stats[i].print(fmt.Sprintf("bucket #%d", i+1))
}
wildcardStats.print("wild card queries")
fmt.Println()
sort.Slice(queries, func(i, j int) bool {
return queries[i].medianTime > queries[j].medianTime
})
for i := 0; i < 10; i++ {
q := queries[i]
fmt.Printf("Most expensive query #%-2d median runtime: %13v max runtime: %13v result count: %4d fromBlock: %9d toBlock: %9d addresses: %v topics: %v\n",
i+1, q.medianTime, q.runtime[len(q.runtime)-1], len(q.query.results), q.query.FromBlock, q.query.ToBlock, q.query.Address, q.query.Topics)
}
return nil
}
type bucketStats struct {
blocks int64
count, logs int
runtime time.Duration
}
func (st *bucketStats) print(name string) {
if st.count == 0 {
return
}
fmt.Printf("%-20s queries: %4d average block length: %12.2f average log count: %7.2f average runtime: %13v\n",
name, st.count, float64(st.blocks)/float64(st.count), float64(st.logs)/float64(st.count), st.runtime/time.Duration(st.count))
}

309
cmd/workload/historytest.go Normal file
View file

@ -0,0 +1,309 @@
// Copyright 2025 The go-ethereum Authors
// This file is part of go-ethereum.
//
// go-ethereum is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// go-ethereum 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
package main
import (
"context"
"encoding/json"
"fmt"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/internal/utesting"
)
// historyTest is the content of a history test.
type historyTest struct {
BlockNumbers []uint64 `json:"blockNumbers"`
BlockHashes []common.Hash `json:"blockHashes"`
TxCounts []int `json:"txCounts"`
TxHashIndex []int `json:"txHashIndex"`
TxHashes []*common.Hash `json:"txHashes"`
ReceiptsHashes []common.Hash `json:"blockReceiptsHashes"`
}
type historyTestSuite struct {
cfg testConfig
tests historyTest
}
func newHistoryTestSuite(cfg testConfig) *historyTestSuite {
s := &historyTestSuite{cfg: cfg}
if err := s.loadTests(); err != nil {
exit(err)
}
return s
}
func (s *historyTestSuite) loadTests() error {
file, err := s.cfg.fsys.Open(s.cfg.historyTestFile)
if err != nil {
return fmt.Errorf("can't open historyTestFile: %v", err)
}
defer file.Close()
if err := json.NewDecoder(file).Decode(&s.tests); err != nil {
return fmt.Errorf("invalid JSON in %s: %v", s.cfg.historyTestFile, err)
}
if len(s.tests.BlockNumbers) == 0 {
return fmt.Errorf("historyTestFile %s has no test data", s.cfg.historyTestFile)
}
return nil
}
func (s *historyTestSuite) allTests() []utesting.Test {
return []utesting.Test{
{
Name: "History/getBlockByHash",
Fn: s.testGetBlockByHash,
},
{
Name: "History/getBlockByNumber",
Fn: s.testGetBlockByNumber,
},
{
Name: "History/getBlockReceiptsByHash",
Fn: s.testGetBlockReceiptsByHash,
},
{
Name: "History/getBlockReceiptsByNumber",
Fn: s.testGetBlockReceiptsByNumber,
},
{
Name: "History/getBlockTransactionCountByHash",
Fn: s.testGetBlockTransactionCountByHash,
},
{
Name: "History/getBlockTransactionCountByNumber",
Fn: s.testGetBlockTransactionCountByNumber,
},
{
Name: "History/getTransactionByBlockHashAndIndex",
Fn: s.testGetTransactionByBlockHashAndIndex,
},
{
Name: "History/getTransactionByBlockNumberAndIndex",
Fn: s.testGetTransactionByBlockNumberAndIndex,
},
}
}
func (s *historyTestSuite) testGetBlockByHash(t *utesting.T) {
ctx := context.Background()
for i, num := range s.tests.BlockNumbers {
bhash := s.tests.BlockHashes[i]
b, err := s.cfg.client.getBlockByHash(ctx, bhash, false)
if err != nil {
t.Fatalf("block %d (hash %v): error %v", num, bhash, err)
}
if b == nil {
t.Errorf("block %d (hash %v): not found", num, bhash)
continue
}
if b.Hash != bhash || uint64(b.Number) != num {
t.Errorf("block %d (hash %v): invalid number/hash", num, bhash)
}
}
}
func (s *historyTestSuite) testGetBlockByNumber(t *utesting.T) {
ctx := context.Background()
for i, num := range s.tests.BlockNumbers {
bhash := s.tests.BlockHashes[i]
b, err := s.cfg.client.getBlockByNumber(ctx, num, false)
if err != nil {
t.Fatalf("block %d (hash %v): error %v", num, bhash, err)
}
if b == nil {
t.Errorf("block %d (hash %v): not found", num, bhash)
continue
}
if b.Hash != bhash || uint64(b.Number) != num {
t.Errorf("block %d (hash %v): invalid number/hash", num, bhash)
}
}
}
func (s *historyTestSuite) testGetBlockTransactionCountByHash(t *utesting.T) {
ctx := context.Background()
for i, num := range s.tests.BlockNumbers {
bhash := s.tests.BlockHashes[i]
count, err := s.cfg.client.getBlockTransactionCountByHash(ctx, bhash)
if err != nil {
t.Fatalf("block %d (hash %v): error %v", num, bhash, err)
}
expectedCount := uint64(s.tests.TxCounts[i])
if count != expectedCount {
t.Errorf("block %d (hash %v): wrong txcount %d, want %d", count, expectedCount)
}
}
}
func (s *historyTestSuite) testGetBlockTransactionCountByNumber(t *utesting.T) {
ctx := context.Background()
for i, num := range s.tests.BlockNumbers {
bhash := s.tests.BlockHashes[i]
count, err := s.cfg.client.getBlockTransactionCountByNumber(ctx, num)
if err != nil {
t.Fatalf("block %d (hash %v): error %v", num, bhash, err)
}
expectedCount := uint64(s.tests.TxCounts[i])
if count != expectedCount {
t.Errorf("block %d (hash %v): wrong txcount %d, want %d", count, expectedCount)
}
}
}
func (s *historyTestSuite) testGetBlockReceiptsByHash(t *utesting.T) {
ctx := context.Background()
for i, num := range s.tests.BlockNumbers {
bhash := s.tests.BlockHashes[i]
receipts, err := s.cfg.client.getBlockReceipts(ctx, bhash)
if err != nil {
t.Fatalf("block %d (hash %v): error %v", num, bhash, err)
}
hash := calcReceiptsHash(receipts)
expectedHash := s.tests.ReceiptsHashes[i]
if hash != expectedHash {
t.Errorf("block %d (hash %v): wrong receipts hash %v, want %v", num, bhash, hash, expectedHash)
}
}
}
func (s *historyTestSuite) testGetBlockReceiptsByNumber(t *utesting.T) {
ctx := context.Background()
for i, num := range s.tests.BlockNumbers {
bhash := s.tests.BlockHashes[i]
receipts, err := s.cfg.client.getBlockReceipts(ctx, hexutil.Uint64(num))
if err != nil {
t.Fatalf("block %d (hash %v): error %v", num, bhash, err)
}
hash := calcReceiptsHash(receipts)
expectedHash := s.tests.ReceiptsHashes[i]
if hash != expectedHash {
t.Errorf("block %d (hash %v): wrong receipts hash %v, want %v", num, bhash, hash, expectedHash)
}
}
}
func (s *historyTestSuite) testGetTransactionByBlockHashAndIndex(t *utesting.T) {
ctx := context.Background()
for i, num := range s.tests.BlockNumbers {
bhash := s.tests.BlockHashes[i]
txIndex := s.tests.TxHashIndex[i]
expectedHash := s.tests.TxHashes[i]
if expectedHash == nil {
continue // no txs in block
}
tx, err := s.cfg.client.getTransactionByBlockHashAndIndex(ctx, bhash, uint64(txIndex))
if err != nil {
t.Fatalf("block %d (hash %v): error %v", num, bhash, err)
}
if tx == nil {
t.Errorf("block %d (hash %v): txIndex %d not found", num, bhash, txIndex)
continue
}
if tx.Hash != *expectedHash || uint64(tx.TransactionIndex) != uint64(txIndex) {
t.Errorf("block %d (hash %v): txIndex %d has wrong txHash/Index", num, bhash, txIndex)
}
}
}
func (s *historyTestSuite) testGetTransactionByBlockNumberAndIndex(t *utesting.T) {
ctx := context.Background()
for i, num := range s.tests.BlockNumbers {
bhash := s.tests.BlockHashes[i]
txIndex := s.tests.TxHashIndex[i]
expectedHash := s.tests.TxHashes[i]
if expectedHash == nil {
continue // no txs in block
}
tx, err := s.cfg.client.getTransactionByBlockNumberAndIndex(ctx, num, uint64(txIndex))
if err != nil {
t.Fatalf("block %d (hash %v): error %v", num, bhash, err)
}
if tx == nil {
t.Errorf("block %d (hash %v): txIndex %d not found", num, bhash, txIndex)
continue
}
if tx.Hash != *expectedHash || uint64(tx.TransactionIndex) != uint64(txIndex) {
t.Errorf("block %d (hash %v): txIndex %d has wrong txHash/Index", num, bhash, txIndex)
}
}
}
type simpleBlock struct {
Number hexutil.Uint64 `json:"number"`
Hash common.Hash `json:"hash"`
}
type simpleTransaction struct {
Hash common.Hash `json:"hash"`
TransactionIndex hexutil.Uint64 `json:"transactionIndex"`
}
func (c *client) getBlockByHash(ctx context.Context, arg common.Hash, fullTx bool) (*simpleBlock, error) {
var r *simpleBlock
err := c.RPC.CallContext(ctx, &r, "eth_getBlockByHash", arg, fullTx)
return r, err
}
func (c *client) getBlockByNumber(ctx context.Context, arg uint64, fullTx bool) (*simpleBlock, error) {
var r *simpleBlock
err := c.RPC.CallContext(ctx, &r, "eth_getBlockByNumber", hexutil.Uint64(arg), fullTx)
return r, err
}
func (c *client) getTransactionByBlockHashAndIndex(ctx context.Context, block common.Hash, index uint64) (*simpleTransaction, error) {
var r *simpleTransaction
err := c.RPC.CallContext(ctx, &r, "eth_getTransactionByBlockHashAndIndex", block, hexutil.Uint64(index))
return r, err
}
func (c *client) getTransactionByBlockNumberAndIndex(ctx context.Context, block uint64, index uint64) (*simpleTransaction, error) {
var r *simpleTransaction
err := c.RPC.CallContext(ctx, &r, "eth_getTransactionByBlockNumberAndIndex", hexutil.Uint64(block), hexutil.Uint64(index))
return r, err
}
func (c *client) getBlockTransactionCountByHash(ctx context.Context, block common.Hash) (uint64, error) {
var r hexutil.Uint64
err := c.RPC.CallContext(ctx, &r, "eth_getBlockTransactionCountByHash", block)
return uint64(r), err
}
func (c *client) getBlockTransactionCountByNumber(ctx context.Context, block uint64) (uint64, error) {
var r hexutil.Uint64
err := c.RPC.CallContext(ctx, &r, "eth_getBlockTransactionCountByNumber", hexutil.Uint64(block))
return uint64(r), err
}
func (c *client) getBlockReceipts(ctx context.Context, arg any) ([]*types.Receipt, error) {
var result []*types.Receipt
err := c.RPC.CallContext(ctx, &result, "eth_getBlockReceipts", arg)
return result, err
}

View file

@ -0,0 +1,147 @@
// Copyright 2025 The go-ethereum Authors
// This file is part of go-ethereum.
//
// go-ethereum is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// go-ethereum 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
package main
import (
"context"
"encoding/json"
"fmt"
"math/big"
"os"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/internal/flags"
"github.com/ethereum/go-ethereum/rlp"
"github.com/urfave/cli/v2"
)
var (
historyGenerateCommand = &cli.Command{
Name: "historygen",
Usage: "Generates history retrieval tests",
ArgsUsage: "<RPC endpoint URL>",
Action: generateHistoryTests,
Flags: []cli.Flag{
historyTestFileFlag,
historyTestEarliestFlag,
},
}
historyTestFileFlag = &cli.StringFlag{
Name: "history-tests",
Usage: "JSON file containing filter test queries",
Value: "history_tests.json",
Category: flags.TestingCategory,
}
historyTestEarliestFlag = &cli.IntFlag{
Name: "earliest",
Usage: "JSON file containing filter test queries",
Value: 0,
Category: flags.TestingCategory,
}
)
const historyTestBlockCount = 2000
func generateHistoryTests(clictx *cli.Context) error {
var (
client = makeClient(clictx)
earliest = uint64(clictx.Int(historyTestEarliestFlag.Name))
outputFile = clictx.String(historyTestFileFlag.Name)
ctx = context.Background()
)
test := new(historyTest)
// Create the block numbers. Here we choose 1k blocks between earliest and head.
latest, err := client.Eth.BlockNumber(ctx)
if err != nil {
exit(err)
}
if latest < historyTestBlockCount {
exit(fmt.Errorf("node seems not synced, latest block is %d", latest))
}
test.BlockNumbers = make([]uint64, 0, historyTestBlockCount)
stride := (latest - earliest) / historyTestBlockCount
for b := earliest; b < latest; b += stride {
test.BlockNumbers = append(test.BlockNumbers, b)
}
// Get blocks and assign block info into the test
fmt.Println("Fetching blocks")
blocks := make([]*types.Block, len(test.BlockNumbers))
for i, blocknum := range test.BlockNumbers {
b, err := client.Eth.BlockByNumber(ctx, new(big.Int).SetUint64(blocknum))
if err != nil {
exit(fmt.Errorf("error fetching block %d: %v", blocknum, err))
}
blocks[i] = b
}
test.BlockHashes = make([]common.Hash, len(blocks))
test.TxCounts = make([]int, len(blocks))
for i, block := range blocks {
test.BlockHashes[i] = block.Hash()
test.TxCounts[i] = len(block.Transactions())
}
// Fill tx index.
test.TxHashIndex = make([]int, len(blocks))
test.TxHashes = make([]*common.Hash, len(blocks))
for i, block := range blocks {
txs := block.Transactions()
if len(txs) == 0 {
continue
}
index := len(txs) / 2
txhash := txs[index].Hash()
test.TxHashIndex[i] = index
test.TxHashes[i] = &txhash
}
// Get receipts.
fmt.Println("Fetching receipts")
test.ReceiptsHashes = make([]common.Hash, len(blocks))
for i, blockHash := range test.BlockHashes {
receipts, err := client.getBlockReceipts(ctx, blockHash)
if err != nil {
exit(fmt.Errorf("error fetching block %v receipts: %v", blockHash, err))
}
test.ReceiptsHashes[i] = calcReceiptsHash(receipts)
}
// Write output file.
writeJSON(outputFile, test)
return nil
}
func calcReceiptsHash(rcpt []*types.Receipt) common.Hash {
h := crypto.NewKeccakState()
rlp.Encode(h, rcpt)
return common.Hash(h.Sum(nil))
}
func writeJSON(fileName string, value any) {
file, err := os.Create(fileName)
if err != nil {
exit(fmt.Errorf("Error creating %s: %v", fileName, err))
return
}
defer file.Close()
json.NewEncoder(file).Encode(value)
}

86
cmd/workload/main.go Normal file
View file

@ -0,0 +1,86 @@
// Copyright 2025 The go-ethereum Authors
// This file is part of go-ethereum.
//
// go-ethereum is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// go-ethereum 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
package main
import (
"fmt"
"os"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/internal/debug"
"github.com/ethereum/go-ethereum/internal/flags"
"github.com/ethereum/go-ethereum/rpc"
"github.com/urfave/cli/v2"
)
var app = flags.NewApp("go-ethereum workload test tool")
func init() {
app.Flags = append(app.Flags, debug.Flags...)
app.Before = func(ctx *cli.Context) error {
flags.MigrateGlobalFlags(ctx)
return debug.Setup(ctx)
}
app.After = func(ctx *cli.Context) error {
debug.Exit()
return nil
}
app.CommandNotFound = func(ctx *cli.Context, cmd string) {
fmt.Fprintf(os.Stderr, "No such command: %s\n", cmd)
os.Exit(1)
}
// Add subcommands.
app.Commands = []*cli.Command{
runTestCommand,
historyGenerateCommand,
filterGenerateCommand,
filterPerfCommand,
}
}
func main() {
exit(app.Run(os.Args))
}
type client struct {
Eth *ethclient.Client
RPC *rpc.Client
}
func makeClient(ctx *cli.Context) *client {
if ctx.NArg() < 1 {
exit("missing RPC endpoint URL as command-line argument")
}
url := ctx.Args().First()
cl, err := rpc.Dial(url)
if err != nil {
exit(fmt.Errorf("Could not create RPC client at %s: %v", url, err))
}
return &client{
RPC: cl,
Eth: ethclient.NewClient(cl),
}
}
func exit(err any) {
if err == nil {
os.Exit(0)
}
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

145
cmd/workload/testsuite.go Normal file
View file

@ -0,0 +1,145 @@
// Copyright 2020 The go-ethereum Authors
// This file is part of go-ethereum.
//
// go-ethereum is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// go-ethereum 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
package main
import (
"embed"
"io/fs"
"os"
"slices"
"github.com/ethereum/go-ethereum/internal/flags"
"github.com/ethereum/go-ethereum/internal/utesting"
"github.com/ethereum/go-ethereum/log"
"github.com/urfave/cli/v2"
)
//go:embed queries
var builtinTestFiles embed.FS
var (
runTestCommand = &cli.Command{
Name: "test",
Usage: "Runs workload tests against an RPC endpoint",
ArgsUsage: "<RPC endpoint URL>",
Action: runTestCmd,
Flags: []cli.Flag{
testPatternFlag,
testTAPFlag,
testSlowFlag,
testSepoliaFlag,
testMainnetFlag,
filterQueryFileFlag,
historyTestFileFlag,
},
}
testPatternFlag = &cli.StringFlag{
Name: "run",
Usage: "Pattern of test suite(s) to run",
Category: flags.TestingCategory,
}
testTAPFlag = &cli.BoolFlag{
Name: "tap",
Usage: "Output test results in TAP format",
Category: flags.TestingCategory,
}
testSlowFlag = &cli.BoolFlag{
Name: "slow",
Usage: "Enable slow tests",
Value: false,
Category: flags.TestingCategory,
}
testSepoliaFlag = &cli.BoolFlag{
Name: "sepolia",
Usage: "Use test cases for sepolia network",
Category: flags.TestingCategory,
}
testMainnetFlag = &cli.BoolFlag{
Name: "mainnet",
Usage: "Use test cases for mainnet network",
Category: flags.TestingCategory,
}
)
// testConfig holds the parameters for testing.
type testConfig struct {
client *client
fsys fs.FS
filterQueryFile string
historyTestFile string
}
func testConfigFromCLI(ctx *cli.Context) (cfg testConfig) {
flags.CheckExclusive(ctx, testMainnetFlag, testSepoliaFlag)
if (ctx.IsSet(testMainnetFlag.Name) || ctx.IsSet(testSepoliaFlag.Name)) && ctx.IsSet(filterQueryFileFlag.Name) {
exit(filterQueryFileFlag.Name + " cannot be used with " + testMainnetFlag.Name + " or " + testSepoliaFlag.Name)
}
// configure ethclient
cfg.client = makeClient(ctx)
// configure test files
switch {
case ctx.Bool(testMainnetFlag.Name):
cfg.fsys = builtinTestFiles
cfg.filterQueryFile = "queries/filter_queries_mainnet.json"
cfg.historyTestFile = "queries/history_mainnet.json"
case ctx.Bool(testSepoliaFlag.Name):
cfg.fsys = builtinTestFiles
cfg.filterQueryFile = "queries/filter_queries_sepolia.json"
cfg.historyTestFile = "queries/history_sepolia.json"
default:
cfg.fsys = os.DirFS(".")
cfg.filterQueryFile = ctx.String(filterQueryFileFlag.Name)
cfg.historyTestFile = ctx.String(historyTestFileFlag.Name)
}
return cfg
}
func runTestCmd(ctx *cli.Context) error {
cfg := testConfigFromCLI(ctx)
filterSuite := newFilterTestSuite(cfg)
historySuite := newHistoryTestSuite(cfg)
// Filter test cases.
tests := filterSuite.allTests()
tests = append(tests, historySuite.allTests()...)
if ctx.IsSet(testPatternFlag.Name) {
tests = utesting.MatchTests(tests, ctx.String(testPatternFlag.Name))
}
if !ctx.Bool(testSlowFlag.Name) {
tests = slices.DeleteFunc(tests, func(test utesting.Test) bool {
return test.Slow
})
}
// Disable logging unless explicitly enabled.
if !ctx.IsSet("verbosity") && !ctx.IsSet("vmodule") {
log.SetDefault(log.NewLogger(log.DiscardHandler()))
}
// Run the tests.
var run = utesting.RunTests
if ctx.Bool(testTAPFlag.Name) {
run = utesting.RunTAP
}
results := run(tests, os.Stdout)
if utesting.CountFailures(results) > 0 {
os.Exit(1)
}
return nil
}

View file

@ -19,6 +19,7 @@ package snapshot
import ( import (
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"maps"
"math" "math"
"math/rand" "math/rand"
"slices" "slices"
@ -30,7 +31,6 @@ import (
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rlp"
bloomfilter "github.com/holiman/bloomfilter/v2" bloomfilter "github.com/holiman/bloomfilter/v2"
"golang.org/x/exp/maps"
) )
var ( var (
@ -431,8 +431,7 @@ func (dl *diffLayer) AccountList() []common.Hash {
dl.lock.Lock() dl.lock.Lock()
defer dl.lock.Unlock() defer dl.lock.Unlock()
dl.accountList = maps.Keys(dl.accountData) dl.accountList = slices.SortedFunc(maps.Keys(dl.accountData), common.Hash.Cmp)
slices.SortFunc(dl.accountList, common.Hash.Cmp)
dl.memory += uint64(len(dl.accountList) * common.HashLength) dl.memory += uint64(len(dl.accountList) * common.HashLength)
return dl.accountList return dl.accountList
} }
@ -464,8 +463,7 @@ func (dl *diffLayer) StorageList(accountHash common.Hash) []common.Hash {
dl.lock.Lock() dl.lock.Lock()
defer dl.lock.Unlock() defer dl.lock.Unlock()
storageList := maps.Keys(dl.storageData[accountHash]) storageList := slices.SortedFunc(maps.Keys(dl.storageData[accountHash]), common.Hash.Cmp)
slices.SortFunc(storageList, common.Hash.Cmp)
dl.storageList[accountHash] = storageList dl.storageList[accountHash] = storageList
dl.memory += uint64(len(dl.storageList)*common.HashLength + common.HashLength) dl.memory += uint64(len(dl.storageList)*common.HashLength + common.HashLength)
return storageList return storageList

View file

@ -83,8 +83,8 @@ func TestHooks(t *testing.T) {
inner.SetTxContext(common.Hash{0x11}, 100) // For the log inner.SetTxContext(common.Hash{0x11}, 100) // For the log
var result []string var result []string
var wants = []string{ var wants = []string{
"0xaa00000000000000000000000000000000000000.balance: 0->100 (BalanceChangeUnspecified)", "0xaa00000000000000000000000000000000000000.balance: 0->100 (Unspecified)",
"0xaa00000000000000000000000000000000000000.balance: 100->50 (BalanceChangeTransfer)", "0xaa00000000000000000000000000000000000000.balance: 100->50 (Transfer)",
"0xaa00000000000000000000000000000000000000.nonce: 0->1337", "0xaa00000000000000000000000000000000000000.nonce: 0->1337",
"0xaa00000000000000000000000000000000000000.code: (0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470) ->0x1325 (0xa12ae05590de0c93a00bc7ac773c2fdb621e44f814985e72194f921c0050f728)", "0xaa00000000000000000000000000000000000000.code: (0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470) ->0x1325 (0xa12ae05590de0c93a00bc7ac773c2fdb621e44f814985e72194f921c0050f728)",
"0xaa00000000000000000000000000000000000000.storage slot 0x0000000000000000000000000000000000000000000000000000000000000001: 0x0000000000000000000000000000000000000000000000000000000000000000 ->0x0000000000000000000000000000000000000000000000000000000000000011", "0xaa00000000000000000000000000000000000000.storage slot 0x0000000000000000000000000000000000000000000000000000000000000001: 0x0000000000000000000000000000000000000000000000000000000000000000 ->0x0000000000000000000000000000000000000000000000000000000000000011",

View file

@ -1,4 +1,4 @@
// Code generated by "stringer -type=BalanceChangeReason -output gen_balance_change_reason_stringer.go"; DO NOT EDIT. // Code generated by "stringer -type=BalanceChangeReason -trimprefix=BalanceChange -output gen_balance_change_reason_stringer.go"; DO NOT EDIT.
package tracing package tracing
@ -26,9 +26,9 @@ func _() {
_ = x[BalanceChangeRevert-15] _ = x[BalanceChangeRevert-15]
} }
const _BalanceChangeReason_name = "BalanceChangeUnspecifiedBalanceIncreaseRewardMineUncleBalanceIncreaseRewardMineBlockBalanceIncreaseWithdrawalBalanceIncreaseGenesisBalanceBalanceIncreaseRewardTransactionFeeBalanceDecreaseGasBuyBalanceIncreaseGasReturnBalanceIncreaseDaoContractBalanceDecreaseDaoAccountBalanceChangeTransferBalanceChangeTouchAccountBalanceIncreaseSelfdestructBalanceDecreaseSelfdestructBalanceDecreaseSelfdestructBurnBalanceChangeRevert" const _BalanceChangeReason_name = "UnspecifiedBalanceIncreaseRewardMineUncleBalanceIncreaseRewardMineBlockBalanceIncreaseWithdrawalBalanceIncreaseGenesisBalanceBalanceIncreaseRewardTransactionFeeBalanceDecreaseGasBuyBalanceIncreaseGasReturnBalanceIncreaseDaoContractBalanceDecreaseDaoAccountTransferTouchAccountBalanceIncreaseSelfdestructBalanceDecreaseSelfdestructBalanceDecreaseSelfdestructBurnRevert"
var _BalanceChangeReason_index = [...]uint16{0, 24, 54, 84, 109, 138, 173, 194, 218, 244, 269, 290, 315, 342, 369, 400, 419} var _BalanceChangeReason_index = [...]uint16{0, 11, 41, 71, 96, 125, 160, 181, 205, 231, 256, 264, 276, 303, 330, 361, 367}
func (i BalanceChangeReason) String() string { func (i BalanceChangeReason) String() string {
if i >= BalanceChangeReason(len(_BalanceChangeReason_index)-1) { if i >= BalanceChangeReason(len(_BalanceChangeReason_index)-1) {

View file

@ -1,4 +1,4 @@
// Code generated by "stringer -type=GasChangeReason -output gen_gas_change_reason_stringer.go"; DO NOT EDIT. // Code generated by "stringer -type=GasChangeReason -trimprefix=GasChange -output gen_gas_change_reason_stringer.go"; DO NOT EDIT.
package tracing package tracing
@ -32,12 +32,12 @@ func _() {
} }
const ( const (
_GasChangeReason_name_0 = "GasChangeUnspecifiedGasChangeTxInitialBalanceGasChangeTxIntrinsicGasGasChangeTxRefundsGasChangeTxLeftOverReturnedGasChangeCallInitialBalanceGasChangeCallLeftOverReturnedGasChangeCallLeftOverRefundedGasChangeCallContractCreationGasChangeCallContractCreation2GasChangeCallCodeStorageGasChangeCallOpCodeGasChangeCallPrecompiledContractGasChangeCallStorageColdAccessGasChangeCallFailedExecutionGasChangeWitnessContractInitGasChangeWitnessContractCreationGasChangeWitnessCodeChunkGasChangeWitnessContractCollisionCheckGasChangeTxDataFloor" _GasChangeReason_name_0 = "UnspecifiedTxInitialBalanceTxIntrinsicGasTxRefundsTxLeftOverReturnedCallInitialBalanceCallLeftOverReturnedCallLeftOverRefundedCallContractCreationCallContractCreation2CallCodeStorageCallOpCodeCallPrecompiledContractCallStorageColdAccessCallFailedExecutionWitnessContractInitWitnessContractCreationWitnessCodeChunkWitnessContractCollisionCheckTxDataFloor"
_GasChangeReason_name_1 = "GasChangeIgnored" _GasChangeReason_name_1 = "Ignored"
) )
var ( var (
_GasChangeReason_index_0 = [...]uint16{0, 20, 45, 68, 86, 113, 140, 169, 198, 227, 257, 281, 300, 332, 362, 390, 418, 450, 475, 513, 533} _GasChangeReason_index_0 = [...]uint16{0, 11, 27, 41, 50, 68, 86, 106, 126, 146, 167, 182, 192, 215, 236, 255, 274, 297, 313, 342, 353}
) )
func (i GasChangeReason) String() string { func (i GasChangeReason) String() string {

View file

@ -0,0 +1,29 @@
// Code generated by "stringer -type=NonceChangeReason -trimprefix NonceChange -output gen_nonce_change_reason_stringer.go"; DO NOT EDIT.
package tracing
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[NonceChangeUnspecified-0]
_ = x[NonceChangeGenesis-1]
_ = x[NonceChangeEoACall-2]
_ = x[NonceChangeContractCreator-3]
_ = x[NonceChangeNewContract-4]
_ = x[NonceChangeAuthorization-5]
_ = x[NonceChangeRevert-6]
}
const _NonceChangeReason_name = "UnspecifiedGenesisEoACallContractCreatorNewContractAuthorizationRevert"
var _NonceChangeReason_index = [...]uint8{0, 11, 18, 25, 40, 51, 64, 70}
func (i NonceChangeReason) String() string {
if i >= NonceChangeReason(len(_NonceChangeReason_index)-1) {
return "NonceChangeReason(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _NonceChangeReason_name[_NonceChangeReason_index[i]:_NonceChangeReason_index[i+1]]
}

View file

@ -228,7 +228,7 @@ type Hooks struct {
// for tracing and reporting. // for tracing and reporting.
type BalanceChangeReason byte type BalanceChangeReason byte
//go:generate go run golang.org/x/tools/cmd/stringer -type=BalanceChangeReason -output gen_balance_change_reason_stringer.go //go:generate go run golang.org/x/tools/cmd/stringer -type=BalanceChangeReason -trimprefix=BalanceChange -output gen_balance_change_reason_stringer.go
const ( const (
BalanceChangeUnspecified BalanceChangeReason = 0 BalanceChangeUnspecified BalanceChangeReason = 0
@ -289,7 +289,7 @@ const (
// once per transaction, while those that start with `GasChangeCall` are emitted on a call basis. // once per transaction, while those that start with `GasChangeCall` are emitted on a call basis.
type GasChangeReason byte type GasChangeReason byte
//go:generate go run golang.org/x/tools/cmd/stringer -type=GasChangeReason -output gen_gas_change_reason_stringer.go //go:generate go run golang.org/x/tools/cmd/stringer -type=GasChangeReason -trimprefix=GasChange -output gen_gas_change_reason_stringer.go
const ( const (
GasChangeUnspecified GasChangeReason = 0 GasChangeUnspecified GasChangeReason = 0
@ -355,6 +355,8 @@ const (
// NonceChangeReason is used to indicate the reason for a nonce change. // NonceChangeReason is used to indicate the reason for a nonce change.
type NonceChangeReason byte type NonceChangeReason byte
//go:generate go run golang.org/x/tools/cmd/stringer -type=NonceChangeReason -trimprefix NonceChange -output gen_nonce_change_reason_stringer.go
const ( const (
NonceChangeUnspecified NonceChangeReason = 0 NonceChangeUnspecified NonceChangeReason = 0

View file

@ -1085,19 +1085,24 @@ func (p *BlobPool) SetGasTip(tip *big.Int) {
p.updateStorageMetrics() p.updateStorageMetrics()
} }
// validateTx checks whether a transaction is valid according to the consensus // ValidateTxBasics checks whether a transaction is valid according to the consensus
// rules and adheres to some heuristic limits of the local node (price and size). // rules, but does not check state-dependent validation such as sufficient balance.
func (p *BlobPool) validateTx(tx *types.Transaction) error { // This check is meant as an early check which only needs to be performed once,
// Ensure the transaction adheres to basic pool filters (type, size, tip) and // and does not require the pool mutex to be held.
// consensus rules func (p *BlobPool) ValidateTxBasics(tx *types.Transaction) error {
baseOpts := &txpool.ValidationOptions{ opts := &txpool.ValidationOptions{
Config: p.chain.Config(), Config: p.chain.Config(),
Accept: 1 << types.BlobTxType, Accept: 1 << types.BlobTxType,
MaxSize: txMaxSize, MaxSize: txMaxSize,
MinTip: p.gasTip.ToBig(), MinTip: p.gasTip.ToBig(),
} }
return txpool.ValidateTransaction(tx, p.head, p.signer, opts)
}
if err := p.txValidationFn(tx, p.head, p.signer, baseOpts); err != nil { // validateTx checks whether a transaction is valid according to the consensus
// rules and adheres to some heuristic limits of the local node (price and size).
func (p *BlobPool) validateTx(tx *types.Transaction) error {
if err := p.ValidateTxBasics(tx); err != nil {
return err return err
} }
// Ensure the transaction adheres to the stateful pool filters (nonce, balance) // Ensure the transaction adheres to the stateful pool filters (nonce, balance)

View file

@ -18,12 +18,12 @@ package blobpool
import ( import (
"container/heap" "container/heap"
"maps"
"math" "math"
"slices" "slices"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/holiman/uint256" "github.com/holiman/uint256"
"golang.org/x/exp/maps"
) )
// evictHeap is a helper data structure to keep track of the cheapest bottleneck // evictHeap is a helper data structure to keep track of the cheapest bottleneck
@ -54,8 +54,7 @@ func newPriceHeap(basefee *uint256.Int, blobfee *uint256.Int, index map[common.A
// Populate the heap in account sort order. Not really needed in practice, // Populate the heap in account sort order. Not really needed in practice,
// but it makes the heap initialization deterministic and less annoying to // but it makes the heap initialization deterministic and less annoying to
// test in unit tests. // test in unit tests.
heap.addrs = maps.Keys(index) heap.addrs = slices.SortedFunc(maps.Keys(index), common.Address.Cmp)
slices.SortFunc(heap.addrs, common.Address.Cmp)
for i, addr := range heap.addrs { for i, addr := range heap.addrs {
heap.index[addr] = i heap.index[addr] = i
} }

View file

@ -19,6 +19,7 @@ package legacypool
import ( import (
"errors" "errors"
"maps"
"math" "math"
"math/big" "math/big"
"slices" "slices"
@ -40,7 +41,6 @@ import (
"github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/metrics"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
"github.com/holiman/uint256" "github.com/holiman/uint256"
"golang.org/x/exp/maps"
) )
const ( const (
@ -558,11 +558,11 @@ func (pool *LegacyPool) Pending(filter txpool.PendingFilter) map[common.Address]
return pending return pending
} }
// validateTxBasics checks whether a transaction is valid according to the consensus // ValidateTxBasics checks whether a transaction is valid according to the consensus
// rules, but does not check state-dependent validation such as sufficient balance. // rules, but does not check state-dependent validation such as sufficient balance.
// This check is meant as an early check which only needs to be performed once, // This check is meant as an early check which only needs to be performed once,
// and does not require the pool mutex to be held. // and does not require the pool mutex to be held.
func (pool *LegacyPool) validateTxBasics(tx *types.Transaction) error { func (pool *LegacyPool) ValidateTxBasics(tx *types.Transaction) error {
opts := &txpool.ValidationOptions{ opts := &txpool.ValidationOptions{
Config: pool.chainconfig, Config: pool.chainconfig,
Accept: 0 | Accept: 0 |
@ -573,10 +573,7 @@ func (pool *LegacyPool) validateTxBasics(tx *types.Transaction) error {
MaxSize: txMaxSize, MaxSize: txMaxSize,
MinTip: pool.gasTip.Load().ToBig(), MinTip: pool.gasTip.Load().ToBig(),
} }
if err := txpool.ValidateTransaction(tx, pool.currentHead.Load(), pool.signer, opts); err != nil { return txpool.ValidateTransaction(tx, pool.currentHead.Load(), pool.signer, opts)
return err
}
return nil
} }
// validateTx checks whether a transaction is valid according to the consensus // validateTx checks whether a transaction is valid according to the consensus
@ -929,7 +926,7 @@ func (pool *LegacyPool) Add(txs []*types.Transaction, sync bool) []error {
// Exclude transactions with basic errors, e.g invalid signatures and // Exclude transactions with basic errors, e.g invalid signatures and
// insufficient intrinsic gas as soon as possible and cache senders // insufficient intrinsic gas as soon as possible and cache senders
// in transactions before obtaining lock // in transactions before obtaining lock
if err := pool.validateTxBasics(tx); err != nil { if err := pool.ValidateTxBasics(tx); err != nil {
errs[i] = err errs[i] = err
log.Trace("Discarding invalid transaction", "hash", tx.Hash(), "err", err) log.Trace("Discarding invalid transaction", "hash", tx.Hash(), "err", err)
invalidTxMeter.Mark(1) invalidTxMeter.Mark(1)
@ -1674,7 +1671,7 @@ func (as *accountSet) addTx(tx *types.Transaction) {
// reuse. The returned slice should not be changed! // reuse. The returned slice should not be changed!
func (as *accountSet) flatten() []common.Address { func (as *accountSet) flatten() []common.Address {
if as.cache == nil { if as.cache == nil {
as.cache = maps.Keys(as.accounts) as.cache = slices.Collect(maps.Keys(as.accounts))
} }
return as.cache return as.cache
} }
@ -1765,12 +1762,12 @@ func (t *lookup) Remove(hash common.Hash) {
t.lock.Lock() t.lock.Lock()
defer t.lock.Unlock() defer t.lock.Unlock()
t.removeAuthorities(hash)
tx, ok := t.txs[hash] tx, ok := t.txs[hash]
if !ok { if !ok {
log.Error("No transaction found to be deleted", "hash", hash) log.Error("No transaction found to be deleted", "hash", hash)
return return
} }
t.removeAuthorities(tx)
t.slots -= numSlots(tx) t.slots -= numSlots(tx)
slotsGauge.Update(int64(t.slots)) slotsGauge.Update(int64(t.slots))
@ -1808,8 +1805,9 @@ func (t *lookup) addAuthorities(tx *types.Transaction) {
// removeAuthorities stops tracking the supplied tx in relation to its // removeAuthorities stops tracking the supplied tx in relation to its
// authorities. // authorities.
func (t *lookup) removeAuthorities(hash common.Hash) { func (t *lookup) removeAuthorities(tx *types.Transaction) {
for addr := range t.auths { hash := tx.Hash()
for _, addr := range tx.SetCodeAuthorities() {
list := t.auths[addr] list := t.auths[addr]
// Remove tx from tracker. // Remove tx from tracker.
if i := slices.Index(list, hash); i >= 0 { if i := slices.Index(list, hash); i >= 0 {

View file

@ -23,6 +23,7 @@ import (
"fmt" "fmt"
"math/big" "math/big"
"math/rand" "math/rand"
"slices"
"sync" "sync"
"sync/atomic" "sync/atomic"
"testing" "testing"
@ -238,6 +239,23 @@ func validatePoolInternals(pool *LegacyPool) error {
return fmt.Errorf("pending nonce mismatch: have %v, want %v", nonce, last+1) return fmt.Errorf("pending nonce mismatch: have %v, want %v", nonce, last+1)
} }
} }
// Ensure all auths in pool are tracked
for _, tx := range pool.all.txs {
for _, addr := range tx.SetCodeAuthorities() {
list := pool.all.auths[addr]
if i := slices.Index(list, tx.Hash()); i < 0 {
return fmt.Errorf("authority not tracked: addr %s, tx %s", addr, tx.Hash())
}
}
}
// Ensure all auths in pool have an associated tx.
for addr, hashes := range pool.all.auths {
for _, hash := range hashes {
if _, ok := pool.all.txs[hash]; !ok {
return fmt.Errorf("dangling authority, missing originating tx: addr %s, hash %s", addr, hash.Hex())
}
}
}
return nil return nil
} }
@ -2381,6 +2399,32 @@ func TestSetCodeTransactions(t *testing.T) {
} }
}, },
}, },
{
name: "remove-hash-from-authority-tracker",
pending: 10,
run: func(name string) {
var keys []*ecdsa.PrivateKey
for i := 0; i < 30; i++ {
key, _ := crypto.GenerateKey()
keys = append(keys, key)
addr := crypto.PubkeyToAddress(key.PublicKey)
testAddBalance(pool, addr, big.NewInt(params.Ether))
}
// Create a transactions with 3 unique auths so the lookup's auth map is
// filled with addresses.
for i := 0; i < 30; i += 3 {
if err := pool.addRemoteSync(pricedSetCodeTx(0, 250000, uint256.NewInt(10), uint256.NewInt(3), keys[i], []unsignedAuth{{0, keys[i]}, {0, keys[i+1]}, {0, keys[i+2]}})); err != nil {
t.Fatalf("%s: failed to add with remote setcode transaction: %v", name, err)
}
}
// Replace one of the transactions with a normal transaction so that the
// original hash is removed from the tracker. The hash should be
// associated with 3 different authorities.
if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(1000), keys[0])); err != nil {
t.Fatalf("%s: failed to replace with remote transaction: %v", name, err)
}
},
},
} { } {
tt.run(tt.name) tt.run(tt.name)
pending, queued := pool.Stats() pending, queued := pool.Stats()

View file

@ -18,6 +18,7 @@
package locals package locals
import ( import (
"slices"
"sync" "sync"
"time" "time"
@ -28,7 +29,6 @@ import (
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/metrics"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
"golang.org/x/exp/slices"
) )
var ( var (
@ -74,28 +74,45 @@ func New(journalPath string, journalTime time.Duration, chainConfig *params.Chai
// Track adds a transaction to the tracked set. // Track adds a transaction to the tracked set.
// Note: blob-type transactions are ignored. // Note: blob-type transactions are ignored.
func (tracker *TxTracker) Track(tx *types.Transaction) { func (tracker *TxTracker) Track(tx *types.Transaction) error {
tracker.TrackAll([]*types.Transaction{tx}) return tracker.TrackAll([]*types.Transaction{tx})[0]
} }
// TrackAll adds a list of transactions to the tracked set. // TrackAll adds a list of transactions to the tracked set.
// Note: blob-type transactions are ignored. // Note: blob-type transactions are ignored.
func (tracker *TxTracker) TrackAll(txs []*types.Transaction) { func (tracker *TxTracker) TrackAll(txs []*types.Transaction) []error {
tracker.mu.Lock() tracker.mu.Lock()
defer tracker.mu.Unlock() defer tracker.mu.Unlock()
var errors []error
for _, tx := range txs { for _, tx := range txs {
if tx.Type() == types.BlobTxType { if tx.Type() == types.BlobTxType {
errors = append(errors, nil)
continue
}
// Ignore the transactions which are failed for fundamental
// validation such as invalid parameters.
if err := tracker.pool.ValidateTxBasics(tx); err != nil {
log.Debug("Invalid transaction submitted", "hash", tx.Hash(), "err", err)
errors = append(errors, err)
continue continue
} }
// If we're already tracking it, it's a no-op // If we're already tracking it, it's a no-op
if _, ok := tracker.all[tx.Hash()]; ok { if _, ok := tracker.all[tx.Hash()]; ok {
errors = append(errors, nil)
continue continue
} }
// Theoretically, checking the error here is unnecessary since sender recovery
// is already part of basic validation. However, retrieving the sender address
// from the transaction cache is effectively a no-op if it was previously verified.
// Therefore, the error is still checked just in case.
addr, err := types.Sender(tracker.signer, tx) addr, err := types.Sender(tracker.signer, tx)
if err != nil { // Ignore this tx if err != nil {
errors = append(errors, err)
continue continue
} }
errors = append(errors, nil)
tracker.all[tx.Hash()] = tx tracker.all[tx.Hash()] = tx
if tracker.byAddr[addr] == nil { if tracker.byAddr[addr] == nil {
tracker.byAddr[addr] = legacypool.NewSortedMap() tracker.byAddr[addr] = legacypool.NewSortedMap()
@ -107,6 +124,7 @@ func (tracker *TxTracker) TrackAll(txs []*types.Transaction) {
} }
} }
localGauge.Update(int64(len(tracker.all))) localGauge.Update(int64(len(tracker.all)))
return errors
} }
// recheck checks and returns any transactions that needs to be resubmitted. // recheck checks and returns any transactions that needs to be resubmitted.

View file

@ -129,6 +129,12 @@ type SubPool interface {
// retrieve blobs from the pools directly instead of the network. // retrieve blobs from the pools directly instead of the network.
GetBlobs(vhashes []common.Hash) ([]*kzg4844.Blob, []*kzg4844.Proof) GetBlobs(vhashes []common.Hash) ([]*kzg4844.Blob, []*kzg4844.Proof)
// ValidateTxBasics checks whether a transaction is valid according to the consensus
// rules, but does not check state-dependent validation such as sufficient balance.
// This check is meant as a static check which can be performed without holding the
// pool mutex.
ValidateTxBasics(tx *types.Transaction) error
// Add enqueues a batch of transactions into the pool if they are valid. Due // Add enqueues a batch of transactions into the pool if they are valid. Due
// to the large transaction churn, add may postpone fully integrating the tx // to the large transaction churn, add may postpone fully integrating the tx
// to a later point to batch multiple ones together. // to a later point to batch multiple ones together.

View file

@ -325,6 +325,17 @@ func (p *TxPool) GetBlobs(vhashes []common.Hash) ([]*kzg4844.Blob, []*kzg4844.Pr
return nil, nil return nil, nil
} }
// ValidateTxBasics checks whether a transaction is valid according to the consensus
// rules, but does not check state-dependent validation such as sufficient balance.
func (p *TxPool) ValidateTxBasics(tx *types.Transaction) error {
for _, subpool := range p.subpools {
if subpool.Filter(tx) {
return subpool.ValidateTxBasics(tx)
}
}
return fmt.Errorf("%w: received type %d", core.ErrTxTypeNotSupported, tx.Type())
}
// Add enqueues a batch of transactions into the pool if they are valid. Due // Add enqueues a batch of transactions into the pool if they are valid. Due
// to the large transaction churn, add may postpone fully integrating the tx // to the large transaction churn, add may postpone fully integrating the tx
// to a later point to batch multiple ones together. // to a later point to batch multiple ones together.

View file

@ -17,7 +17,6 @@
package txpool package txpool
import ( import (
"crypto/sha256"
"errors" "errors"
"fmt" "fmt"
"math/big" "math/big"
@ -170,20 +169,11 @@ func validateBlobSidecar(hashes []common.Hash, sidecar *types.BlobTxSidecar) err
if len(sidecar.Blobs) != len(hashes) { if len(sidecar.Blobs) != len(hashes) {
return fmt.Errorf("invalid number of %d blobs compared to %d blob hashes", len(sidecar.Blobs), len(hashes)) return fmt.Errorf("invalid number of %d blobs compared to %d blob hashes", len(sidecar.Blobs), len(hashes))
} }
if len(sidecar.Commitments) != len(hashes) {
return fmt.Errorf("invalid number of %d blob commitments compared to %d blob hashes", len(sidecar.Commitments), len(hashes))
}
if len(sidecar.Proofs) != len(hashes) { if len(sidecar.Proofs) != len(hashes) {
return fmt.Errorf("invalid number of %d blob proofs compared to %d blob hashes", len(sidecar.Proofs), len(hashes)) return fmt.Errorf("invalid number of %d blob proofs compared to %d blob hashes", len(sidecar.Proofs), len(hashes))
} }
// Blob quantities match up, validate that the provers match with the if err := sidecar.ValidateBlobCommitmentHashes(hashes); err != nil {
// transaction hash before getting to the cryptography return err
hasher := sha256.New()
for i, vhash := range hashes {
computed := kzg4844.CalcBlobHashV1(hasher, &sidecar.Commitments[i])
if vhash != computed {
return fmt.Errorf("blob %d: computed hash %#x mismatches transaction one %#x", i, computed, vhash)
}
} }
// Blob commitments match with the hashes in the transaction, verify the // Blob commitments match with the hashes in the transaction, verify the
// blobs themselves via KZG // blobs themselves via KZG

View file

@ -483,15 +483,23 @@ func (tx *Transaction) SetCodeAuthorizations() []SetCodeAuthorization {
return setcodetx.AuthList return setcodetx.AuthList
} }
// SetCodeAuthorities returns a list of each authorization's corresponding authority. // SetCodeAuthorities returns a list of unique authorities from the
// authorization list.
func (tx *Transaction) SetCodeAuthorities() []common.Address { func (tx *Transaction) SetCodeAuthorities() []common.Address {
setcodetx, ok := tx.inner.(*SetCodeTx) setcodetx, ok := tx.inner.(*SetCodeTx)
if !ok { if !ok {
return nil return nil
} }
auths := make([]common.Address, 0, len(setcodetx.AuthList)) var (
marks = make(map[common.Address]bool)
auths = make([]common.Address, 0, len(setcodetx.AuthList))
)
for _, auth := range setcodetx.AuthList { for _, auth := range setcodetx.AuthList {
if addr, err := auth.Authority(); err == nil { if addr, err := auth.Authority(); err == nil {
if marks[addr] {
continue
}
marks[addr] = true
auths = append(auths, addr) auths = append(auths, addr)
} }
} }

View file

@ -19,6 +19,7 @@ package types
import ( import (
"bytes" "bytes"
"crypto/sha256" "crypto/sha256"
"fmt"
"math/big" "math/big"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
@ -85,6 +86,22 @@ func (sc *BlobTxSidecar) encodedSize() uint64 {
return rlp.ListSize(blobs) + rlp.ListSize(commitments) + rlp.ListSize(proofs) return rlp.ListSize(blobs) + rlp.ListSize(commitments) + rlp.ListSize(proofs)
} }
// ValidateBlobCommitmentHashes checks whether the given hashes correspond to the
// commitments in the sidecar
func (sc *BlobTxSidecar) ValidateBlobCommitmentHashes(hashes []common.Hash) error {
if len(sc.Commitments) != len(hashes) {
return fmt.Errorf("invalid number of %d blob commitments compared to %d blob hashes", len(sc.Commitments), len(hashes))
}
hasher := sha256.New()
for i, vhash := range hashes {
computed := kzg4844.CalcBlobHashV1(hasher, &sc.Commitments[i])
if vhash != computed {
return fmt.Errorf("blob %d: computed hash %#x mismatches transaction one %#x", i, computed, vhash)
}
}
return nil
}
// blobTxWithBlobs is used for encoding of transactions when blobs are present. // blobTxWithBlobs is used for encoding of transactions when blobs are present.
type blobTxWithBlobs struct { type blobTxWithBlobs struct {
BlobTx *BlobTx BlobTx *BlobTx

View file

@ -272,10 +272,20 @@ func (b *EthAPIBackend) SubscribeLogsEvent(ch chan<- []*types.Log) event.Subscri
} }
func (b *EthAPIBackend) SendTx(ctx context.Context, signedTx *types.Transaction) error { func (b *EthAPIBackend) SendTx(ctx context.Context, signedTx *types.Transaction) error {
if locals := b.eth.localTxTracker; locals != nil { locals := b.eth.localTxTracker
locals.Track(signedTx) if locals != nil {
if err := locals.Track(signedTx); err != nil {
return err
}
} }
return b.eth.txPool.Add([]*types.Transaction{signedTx}, false)[0] // No error will be returned to user if the transaction fails stateful
// validation (e.g., no available slot), as the locally submitted transactions
// may be resubmitted later via the local tracker.
err := b.eth.txPool.Add([]*types.Transaction{signedTx}, false)[0]
if err != nil && locals == nil {
return err
}
return nil
} }
func (b *EthAPIBackend) GetPoolTransactions() (types.Transactions, error) { func (b *EthAPIBackend) GetPoolTransactions() (types.Transactions, error) {
@ -368,10 +378,6 @@ func (b *EthAPIBackend) ChainDb() ethdb.Database {
return b.eth.ChainDb() return b.eth.ChainDb()
} }
func (b *EthAPIBackend) EventMux() *event.TypeMux {
return b.eth.EventMux()
}
func (b *EthAPIBackend) AccountManager() *accounts.Manager { func (b *EthAPIBackend) AccountManager() *accounts.Manager {
return b.eth.AccountManager() return b.eth.AccountManager()
} }

View file

@ -345,7 +345,6 @@ func (s *Ethereum) Miner() *miner.Miner { return s.miner }
func (s *Ethereum) AccountManager() *accounts.Manager { return s.accountManager } func (s *Ethereum) AccountManager() *accounts.Manager { return s.accountManager }
func (s *Ethereum) BlockChain() *core.BlockChain { return s.blockchain } func (s *Ethereum) BlockChain() *core.BlockChain { return s.blockchain }
func (s *Ethereum) TxPool() *txpool.TxPool { return s.txPool } func (s *Ethereum) TxPool() *txpool.TxPool { return s.txPool }
func (s *Ethereum) EventMux() *event.TypeMux { return s.eventMux }
func (s *Ethereum) Engine() consensus.Engine { return s.engine } func (s *Ethereum) Engine() consensus.Engine { return s.engine }
func (s *Ethereum) ChainDb() ethdb.Database { return s.chainDb } func (s *Ethereum) ChainDb() ethdb.Database { return s.chainDb }
func (s *Ethereum) IsListening() bool { return true } // Always listening func (s *Ethereum) IsListening() bool { return true } // Always listening

View file

@ -107,8 +107,8 @@ func (oracle *Oracle) processBlock(bf *blockFees, percentiles []float64) {
// Compute gas used ratio for normal and blob gas. // Compute gas used ratio for normal and blob gas.
bf.results.gasUsedRatio = float64(bf.header.GasUsed) / float64(bf.header.GasLimit) bf.results.gasUsedRatio = float64(bf.header.GasUsed) / float64(bf.header.GasLimit)
if blobGasUsed := bf.header.BlobGasUsed; blobGasUsed != nil { if blobGasUsed := bf.header.BlobGasUsed; blobGasUsed != nil {
maxBlobs := eip4844.MaxBlobsPerBlock(config, bf.header.Time) maxBlobGas := eip4844.MaxBlobGasPerBlock(config, bf.header.Time)
bf.results.blobGasUsedRatio = float64(*blobGasUsed) / float64(maxBlobs) bf.results.blobGasUsedRatio = float64(*blobGasUsed) / float64(maxBlobGas)
} }
if len(percentiles) == 0 { if len(percentiles) == 0 {

View file

@ -84,9 +84,19 @@ func TestFeeHistory(t *testing.T) {
if len(ratio) != c.expCount { if len(ratio) != c.expCount {
t.Fatalf("Test case %d: gasUsedRatio array length mismatch, want %d, got %d", i, c.expCount, len(ratio)) t.Fatalf("Test case %d: gasUsedRatio array length mismatch, want %d, got %d", i, c.expCount, len(ratio))
} }
for _, ratio := range ratio {
if ratio > 1 {
t.Fatalf("Test case %d: gasUsedRatio greater than 1, got %f", i, ratio)
}
}
if len(blobRatio) != c.expCount { if len(blobRatio) != c.expCount {
t.Fatalf("Test case %d: blobGasUsedRatio array length mismatch, want %d, got %d", i, c.expCount, len(blobRatio)) t.Fatalf("Test case %d: blobGasUsedRatio array length mismatch, want %d, got %d", i, c.expCount, len(blobRatio))
} }
for _, ratio := range blobRatio {
if ratio > 1 {
t.Fatalf("Test case %d: blobGasUsedRatio greater than 1, got %f", i, ratio)
}
}
if len(blobBaseFee) != len(baseFee) { if len(blobBaseFee) != len(baseFee) {
t.Fatalf("Test case %d: blobBaseFee array length mismatch, want %d, got %d", i, len(baseFee), len(blobBaseFee)) t.Fatalf("Test case %d: blobBaseFee array length mismatch, want %d, got %d", i, len(baseFee), len(blobBaseFee))
} }

View file

@ -69,6 +69,19 @@ func (h *ethHandler) Handle(peer *eth.Peer, packet eth.Packet) error {
return h.txFetcher.Enqueue(peer.ID(), *packet, false) return h.txFetcher.Enqueue(peer.ID(), *packet, false)
case *eth.PooledTransactionsResponse: case *eth.PooledTransactionsResponse:
// If we receive any blob transactions missing sidecars, or with
// sidecars that don't correspond to the versioned hashes reported
// in the header, disconnect from the sending peer.
for _, tx := range *packet {
if tx.Type() == types.BlobTxType {
if tx.BlobTxSidecar() == nil {
return errors.New("received sidecar-less blob transaction")
}
if err := tx.BlobTxSidecar().ValidateBlobCommitmentHashes(tx.BlobHashes()); err != nil {
return err
}
}
}
return h.txFetcher.Enqueue(peer.ID(), *packet, true) return h.txFetcher.Enqueue(peer.ID(), *packet, true)
default: default:

View file

@ -598,6 +598,15 @@ func (ec *Client) SuggestGasTipCap(ctx context.Context) (*big.Int, error) {
return (*big.Int)(&hex), nil return (*big.Int)(&hex), nil
} }
// BlobBaseFee retrieves the current blob base fee.
func (ec *Client) BlobBaseFee(ctx context.Context) (*big.Int, error) {
var hex hexutil.Big
if err := ec.c.CallContext(ctx, &hex, "eth_blobBaseFee"); err != nil {
return nil, err
}
return (*big.Int)(&hex), nil
}
type feeHistoryResultMarshaling struct { type feeHistoryResultMarshaling struct {
OldestBlock *hexutil.Big `json:"oldestBlock"` OldestBlock *hexutil.Big `json:"oldestBlock"`
Reward [][]*hexutil.Big `json:"reward,omitempty"` Reward [][]*hexutil.Big `json:"reward,omitempty"`

View file

@ -404,6 +404,15 @@ func testStatusFunctions(t *testing.T, client *rpc.Client) {
t.Fatalf("unexpected gas tip cap: %v", gasTipCap) t.Fatalf("unexpected gas tip cap: %v", gasTipCap)
} }
// BlobBaseFee
blobBaseFee, err := ec.BlobBaseFee(context.Background())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if blobBaseFee.Cmp(big.NewInt(1)) != 0 {
t.Fatalf("unexpected blob base fee: %v", blobBaseFee)
}
// FeeHistory // FeeHistory
history, err := ec.FeeHistory(context.Background(), 1, big.NewInt(2), []float64{95, 99}) history, err := ec.FeeHistory(context.Background(), 1, big.NewInt(2), []float64{95, 99})
if err != nil { if err != nil {

2
go.mod
View file

@ -66,7 +66,6 @@ require (
go.uber.org/automaxprocs v1.5.2 go.uber.org/automaxprocs v1.5.2
go.uber.org/goleak v1.3.0 go.uber.org/goleak v1.3.0
golang.org/x/crypto v0.32.0 golang.org/x/crypto v0.32.0
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa
golang.org/x/sync v0.10.0 golang.org/x/sync v0.10.0
golang.org/x/sys v0.29.0 golang.org/x/sys v0.29.0
golang.org/x/text v0.21.0 golang.org/x/text v0.21.0
@ -143,6 +142,7 @@ require (
github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect github.com/tklauser/numcpus v0.6.1 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect
golang.org/x/mod v0.22.0 // indirect golang.org/x/mod v0.22.0 // indirect
golang.org/x/net v0.34.0 // indirect golang.org/x/net v0.34.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect

4
go.sum
View file

@ -555,8 +555,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME=
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=

View file

@ -295,3 +295,45 @@ func CheckEnvVars(ctx *cli.Context, flags []cli.Flag, prefix string) {
} }
} }
} }
// CheckExclusive verifies that only a single instance of the provided flags was
// set by the user. Each flag might optionally be followed by a string type to
// specialize it further.
func CheckExclusive(ctx *cli.Context, args ...any) {
set := make([]string, 0, 1)
for i := 0; i < len(args); i++ {
// Make sure the next argument is a flag and skip if not set
flag, ok := args[i].(cli.Flag)
if !ok {
panic(fmt.Sprintf("invalid argument, not cli.Flag type: %T", args[i]))
}
// Check if next arg extends current and expand its name if so
name := flag.Names()[0]
if i+1 < len(args) {
switch option := args[i+1].(type) {
case string:
// Extended flag check, make sure value set doesn't conflict with passed in option
if ctx.String(flag.Names()[0]) == option {
name += "=" + option
set = append(set, "--"+name)
}
// shift arguments and continue
i++
continue
case cli.Flag:
default:
panic(fmt.Sprintf("invalid argument, not cli.Flag or string extension: %T", args[i+1]))
}
}
// Mark the flag if it's set
if ctx.IsSet(flag.Names()[0]) {
set = append(set, "--"+name)
}
}
if len(set) > 1 {
fmt.Fprintf(os.Stderr, "Flags %v can't be used at the same time", strings.Join(set, ", "))
os.Exit(1)
}
}

View file

@ -21,12 +21,11 @@ import (
"bytes" "bytes"
"errors" "errors"
"fmt" "fmt"
"maps"
"net" "net"
"net/netip" "net/netip"
"slices" "slices"
"strings" "strings"
"golang.org/x/exp/maps"
) )
var special4, special6 Netlist var special4, special6 Netlist
@ -324,8 +323,7 @@ func (s *DistinctNetSet) key(ip netip.Addr) netip.Prefix {
// String implements fmt.Stringer // String implements fmt.Stringer
func (s DistinctNetSet) String() string { func (s DistinctNetSet) String() string {
keys := maps.Keys(s.members) keys := slices.SortedFunc(maps.Keys(s.members), func(a, b netip.Prefix) int {
slices.SortFunc(keys, func(a, b netip.Prefix) int {
return strings.Compare(a.String(), b.String()) return strings.Compare(a.String(), b.String())
}) })

View file

@ -21,6 +21,7 @@ import (
"encoding/binary" "encoding/binary"
"errors" "errors"
"fmt" "fmt"
"maps"
"slices" "slices"
"time" "time"
@ -29,7 +30,6 @@ import (
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"golang.org/x/exp/maps"
) )
// State history records the state changes involved in executing a block. The // State history records the state changes involved in executing a block. The
@ -250,15 +250,11 @@ type history struct {
// newHistory constructs the state history object with provided state change set. // newHistory constructs the state history object with provided state change set.
func newHistory(root common.Hash, parent common.Hash, block uint64, accounts map[common.Address][]byte, storages map[common.Address]map[common.Hash][]byte, rawStorageKey bool) *history { func newHistory(root common.Hash, parent common.Hash, block uint64, accounts map[common.Address][]byte, storages map[common.Address]map[common.Hash][]byte, rawStorageKey bool) *history {
var ( var (
accountList = maps.Keys(accounts) accountList = slices.SortedFunc(maps.Keys(accounts), common.Address.Cmp)
storageList = make(map[common.Address][]common.Hash) storageList = make(map[common.Address][]common.Hash)
) )
slices.SortFunc(accountList, common.Address.Cmp)
for addr, slots := range storages { for addr, slots := range storages {
slist := maps.Keys(slots) storageList[addr] = slices.SortedFunc(maps.Keys(slots), common.Hash.Cmp)
slices.SortFunc(slist, common.Hash.Cmp)
storageList[addr] = slist
} }
version := historyVersion version := historyVersion
if !rawStorageKey { if !rawStorageKey {

View file

@ -19,6 +19,7 @@ package pathdb
import ( import (
"fmt" "fmt"
"io" "io"
"maps"
"slices" "slices"
"sync" "sync"
@ -27,7 +28,6 @@ import (
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/metrics"
"github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rlp"
"golang.org/x/exp/maps"
) )
// counter helps in tracking items and their corresponding sizes. // counter helps in tracking items and their corresponding sizes.
@ -174,8 +174,7 @@ func (s *stateSet) accountList() []common.Hash {
s.listLock.Lock() s.listLock.Lock()
defer s.listLock.Unlock() defer s.listLock.Unlock()
list = maps.Keys(s.accountData) list = slices.SortedFunc(maps.Keys(s.accountData), common.Hash.Cmp)
slices.SortFunc(list, common.Hash.Cmp)
s.accountListSorted = list s.accountListSorted = list
return list return list
} }
@ -205,8 +204,7 @@ func (s *stateSet) storageList(accountHash common.Hash) []common.Hash {
s.listLock.Lock() s.listLock.Lock()
defer s.listLock.Unlock() defer s.listLock.Unlock()
list := maps.Keys(s.storageData[accountHash]) list := slices.SortedFunc(maps.Keys(s.storageData[accountHash]), common.Hash.Cmp)
slices.SortFunc(list, common.Hash.Cmp)
s.storageListSorted[accountHash] = list s.storageListSorted[accountHash] = list
return list return list
} }

View file

@ -19,6 +19,6 @@ package version
const ( const (
Major = 1 // Major version component of the current release Major = 1 // Major version component of the current release
Minor = 15 // Minor version component of the current release Minor = 15 // Minor version component of the current release
Patch = 3 // Patch version component of the current release Patch = 4 // Patch version component of the current release
Meta = "stable" // Version metadata to append to the version string Meta = "stable" // Version metadata to append to the version string
) )