mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-12 15:03:45 +00:00
Merge branch 'ethereum:master' into gethintegration
This commit is contained in:
commit
eec66c2d57
26 changed files with 1601 additions and 59 deletions
|
|
@ -380,7 +380,7 @@ func doCheckGenerate() {
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
@ -395,7 +395,7 @@ func doCheckGenerate() {
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)")
|
||||||
|
|
|
||||||
|
|
@ -1197,7 +1197,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 = ""
|
||||||
|
|
@ -1295,8 +1295,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)
|
||||||
|
|
||||||
|
|
@ -1528,52 +1528,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)
|
||||||
|
|
@ -1841,7 +1800,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
29
cmd/workload/README.md
Normal 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
216
cmd/workload/filtertest.go
Normal 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
|
||||||
|
}
|
||||||
382
cmd/workload/filtertestgen.go
Normal file
382
cmd/workload/filtertestgen.go
Normal 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()
|
||||||
|
}
|
||||||
137
cmd/workload/filtertestperf.go
Normal file
137
cmd/workload/filtertestperf.go
Normal 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
309
cmd/workload/historytest.go
Normal 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
|
||||||
|
}
|
||||||
147
cmd/workload/historytestgen.go
Normal file
147
cmd/workload/historytestgen.go
Normal 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
86
cmd/workload/main.go
Normal 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)
|
||||||
|
}
|
||||||
1
cmd/workload/queries/filter_queries_mainnet.json
Normal file
1
cmd/workload/queries/filter_queries_mainnet.json
Normal file
File diff suppressed because one or more lines are too long
1
cmd/workload/queries/filter_queries_sepolia.json
Normal file
1
cmd/workload/queries/filter_queries_sepolia.json
Normal file
File diff suppressed because one or more lines are too long
1
cmd/workload/queries/history_mainnet.json
Normal file
1
cmd/workload/queries/history_mainnet.json
Normal file
File diff suppressed because one or more lines are too long
1
cmd/workload/queries/history_sepolia.json
Normal file
1
cmd/workload/queries/history_sepolia.json
Normal file
File diff suppressed because one or more lines are too long
145
cmd/workload/testsuite.go
Normal file
145
cmd/workload/testsuite.go
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -1765,12 +1765,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 +1808,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 {
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ func StartENRUpdater(chain *core.BlockChain, ln *enode.LocalNode) {
|
||||||
var newHead = make(chan core.ChainHeadEvent, 10)
|
var newHead = make(chan core.ChainHeadEvent, 10)
|
||||||
sub := chain.SubscribeChainHeadEvent(newHead)
|
sub := chain.SubscribeChainHeadEvent(newHead)
|
||||||
|
|
||||||
|
ln.Set(currentENREntry(chain))
|
||||||
go func() {
|
go func() {
|
||||||
defer sub.Unsubscribe()
|
defer sub.Unsubscribe()
|
||||||
for {
|
for {
|
||||||
|
|
@ -71,7 +72,7 @@ func NewNodeFilter(chain *core.BlockChain) func(*enode.Node) bool {
|
||||||
filter := forkid.NewFilter(chain)
|
filter := forkid.NewFilter(chain)
|
||||||
return func(n *enode.Node) bool {
|
return func(n *enode.Node) bool {
|
||||||
var entry enrEntry
|
var entry enrEntry
|
||||||
if err := n.Load(entry); err != nil {
|
if err := n.Load(&entry); err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
err := filter(entry.ForkID)
|
err := filter(entry.ForkID)
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"go.uber.org/goleak"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/crypto/kzg4844"
|
"github.com/ethereum/go-ethereum/crypto/kzg4844"
|
||||||
"github.com/holiman/uint256"
|
"github.com/holiman/uint256"
|
||||||
|
|
||||||
|
|
@ -350,3 +352,20 @@ func TestAdjustTimeAfterFork(t *testing.T) {
|
||||||
t.Errorf("failed to build block on fork")
|
t.Errorf("failed to build block on fork")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createAndCloseSimBackend() {
|
||||||
|
genesisData := types.GenesisAlloc{}
|
||||||
|
simulatedBackend := NewBackend(genesisData)
|
||||||
|
defer simulatedBackend.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCheckSimBackendGoroutineLeak checks whether creation of a simulated backend leaks go-routines. Any long-lived go-routines
|
||||||
|
// spawned by global variables are not considered leaked.
|
||||||
|
func TestCheckSimBackendGoroutineLeak(t *testing.T) {
|
||||||
|
createAndCloseSimBackend()
|
||||||
|
ignoreCur := goleak.IgnoreCurrent()
|
||||||
|
// ignore this leveldb function: this go-routine is guaranteed to be terminated 1 second after closing db handle
|
||||||
|
ignoreLdb := goleak.IgnoreAnyFunction("github.com/syndtr/goleveldb/leveldb.(*DB).mpoolDrain")
|
||||||
|
createAndCloseSimBackend()
|
||||||
|
goleak.VerifyNone(t, ignoreCur, ignoreLdb)
|
||||||
|
}
|
||||||
|
|
|
||||||
1
go.mod
1
go.mod
|
|
@ -63,6 +63,7 @@ require (
|
||||||
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7
|
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7
|
||||||
github.com/urfave/cli/v2 v2.27.5
|
github.com/urfave/cli/v2 v2.27.5
|
||||||
go.uber.org/automaxprocs v1.5.2
|
go.uber.org/automaxprocs v1.5.2
|
||||||
|
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/exp v0.0.0-20231110203233-9a3e6036ecaa
|
||||||
golang.org/x/sync v0.10.0
|
golang.org/x/sync v0.10.0
|
||||||
|
|
|
||||||
2
go.sum
2
go.sum
|
|
@ -528,6 +528,8 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
go.uber.org/automaxprocs v1.5.2 h1:2LxUOGiR3O6tw8ui5sZa2LAaHnsviZdVOUZw4fvbnME=
|
go.uber.org/automaxprocs v1.5.2 h1:2LxUOGiR3O6tw8ui5sZa2LAaHnsviZdVOUZw4fvbnME=
|
||||||
go.uber.org/automaxprocs v1.5.2/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0=
|
go.uber.org/automaxprocs v1.5.2/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0=
|
||||||
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 = "unstable" // Version metadata to append to the version string
|
Meta = "unstable" // Version metadata to append to the version string
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue