Skip to content

Commit 91289e4

Browse files
committed
Adds chain snapshotting for benchmark runs
Introduces the ability to run benchmarks from a pre-existing chain state using snapshots. This allows restoring a datadir via a user-defined command and loading a corresponding genesis file, enabling more flexible test scenarios. Key changes include: - Mechanism to define and execute snapshot creation/restoration. - Integration of snapshot handling into the test setup and execution flow. - Loading genesis configuration from a file when using snapshots. - Updates engine API calls from V3 to V4. - Refactors mempool to distinguish between regular and sequencer-specific transactions. - Implements funding of test accounts using deposit transactions. - Updates default devnet genesis configuration.
1 parent 00be92c commit 91289e4

15 files changed

Lines changed: 639 additions & 163 deletions

File tree

configs/snapshot.yml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
- name: Sepolia Alpha snapshot test
2+
description: Sepolia Alpha snapshot test
3+
snapshot:
4+
# skip non-empty for testing so we don't copy every time we run this
5+
# just delete the snapshot directory to force a full copy
6+
command: ./scripts/setup-snapshot.sh --skip-if-nonempty
7+
genesis_file: ../../sepolia-alpha/sepolia-alpha-genesis.json
8+
# force_clean is true by default to ensure consistency, but we can skip it for testing
9+
force_clean: false
10+
variables:
11+
- type: transaction_workload
12+
values:
13+
- transfer-only
14+
- type: node_type
15+
values:
16+
- reth
17+
- type: num_blocks
18+
value: 10
19+
- type: gas_limit
20+
values:
21+
- 15000000
22+
- 30000000
23+
- 60000000
24+
- 90000000
25+
- name: Devnet snapshot test
26+
description: Devnet snapshot test
27+
variables:
28+
- type: transaction_workload
29+
values:
30+
- transfer-only
31+
- type: node_type
32+
values:
33+
- reth
34+
- type: num_blocks
35+
value: 10
36+
- type: gas_limit
37+
values:
38+
- 15000000
39+
- 30000000
40+
- 60000000
41+
- 90000000

runner/benchmark/benchmark.go

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ package benchmark
22

33
import (
44
"fmt"
5+
"math"
56
"math/big"
67
"strings"
78
"time"
89

910
"github.com/base/base-bench/runner/config"
10-
"github.com/ethereum/go-ethereum/common"
1111
"github.com/ethereum/go-ethereum/consensus/misc/eip1559"
1212
"github.com/ethereum/go-ethereum/core"
1313
gethTypes "github.com/ethereum/go-ethereum/core/types"
@@ -111,22 +111,19 @@ func (p Params) ClientOptions(prevClientOptions config.ClientOptions) config.Cli
111111
return prevClientOptions
112112
}
113113

114-
// Genesis returns the genesis block for a given genesis time.
115-
func (p Params) Genesis(genesisTime time.Time) core.Genesis {
114+
const MAX_GAS_LIMIT = math.MaxUint64
115+
116+
// DefaultGenesis returns the genesis block for a devnet.
117+
func DefaultDevnetGenesis(genesisTime time.Time) *core.Genesis {
116118
zero := uint64(0)
117119
fifty := uint64(50)
118120

119121
allocs := make(gethTypes.GenesisAlloc)
120-
// private key: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
121-
allocs[common.HexToAddress("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266")] = gethTypes.Account{
122-
Balance: new(big.Int).Mul(big.NewInt(1e6), big.NewInt(params.Ether)), // 100,000 ETH
123-
}
124-
125-
return core.Genesis{
122+
return &core.Genesis{
126123
Nonce: 0,
127124
Timestamp: uint64(genesisTime.Unix()),
128125
ExtraData: eip1559.EncodeHoloceneExtraData(50, 1),
129-
GasLimit: p.GasLimit,
126+
GasLimit: MAX_GAS_LIMIT, // adjust gas limit in FCU call, not here
130127
Difficulty: big.NewInt(1),
131128
Alloc: allocs,
132129
Config: &params.ChainConfig{
@@ -150,7 +147,7 @@ func (p Params) Genesis(genesisTime time.Time) core.Genesis {
150147
TerminalTotalDifficulty: big.NewInt(1),
151148
ShanghaiTime: new(uint64),
152149
CancunTime: new(uint64),
153-
PragueTime: nil,
150+
PragueTime: new(uint64),
154151
VerkleTime: nil,
155152
// OP-Stack forks are disabled, since we use this for L1.
156153
BedrockBlock: big.NewInt(0),
@@ -160,9 +157,8 @@ func (p Params) Genesis(genesisTime time.Time) core.Genesis {
160157
FjordTime: &zero,
161158
GraniteTime: &zero,
162159
HoloceneTime: &zero,
163-
// Disabled due to reth/geth mismatch
164-
IsthmusTime: nil,
165-
InteropTime: nil,
160+
IsthmusTime: &zero,
161+
InteropTime: nil,
166162
Optimism: &params.OptimismConfig{
167163
EIP1559Elasticity: 1,
168164
EIP1559Denominator: 50,

runner/benchmark/matrix.go

Lines changed: 65 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package benchmark
33
import (
44
"errors"
55
"fmt"
6+
"os"
7+
"os/exec"
68
"path"
79
"regexp"
810
"strings"
@@ -95,12 +97,60 @@ func (bp *Param) Check() error {
9597
return nil
9698
}
9799

100+
// SnapshotDefinition is the user-facing YAML configuration for specifying
101+
// a snapshot to be restored before running a benchmark.
102+
type SnapshotDefinition struct {
103+
Command string `yaml:"command"`
104+
GenesisFile string `yaml:"genesis_file"`
105+
ForceClean *bool `yaml:"force_clean"`
106+
}
107+
108+
// CreateSnapshot copies the snapshot to the output directory for the given
109+
// node type.
110+
func (s SnapshotDefinition) CreateSnapshot(nodeType string, outputDir string) error {
111+
// default to true if not set
112+
forceClean := s.ForceClean == nil || *s.ForceClean
113+
if _, err := os.Stat(outputDir); err == nil && forceClean {
114+
// TODO: we could reuse it here potentially
115+
if err := os.RemoveAll(outputDir); err != nil {
116+
return fmt.Errorf("failed to remove existing snapshot: %w", err)
117+
}
118+
}
119+
120+
// get absolute path of outputDir
121+
currentDir, err := os.Getwd()
122+
if err != nil {
123+
return fmt.Errorf("failed to get absolute path of outputDir: %w", err)
124+
}
125+
126+
outputDir = path.Join(currentDir, outputDir)
127+
128+
var cmdBin string
129+
var args []string
130+
// split out default args from command
131+
parts := strings.SplitN(s.Command, " ", 2)
132+
if len(parts) < 2 {
133+
cmdBin = parts[0]
134+
} else {
135+
cmdBin = parts[0]
136+
args = strings.Split(parts[1], " ")
137+
}
138+
139+
args = append(args, nodeType, outputDir)
140+
141+
cmd := exec.Command(cmdBin, args...)
142+
cmd.Stdout = os.Stdout
143+
cmd.Stderr = os.Stderr
144+
return cmd.Run()
145+
}
146+
98147
// TestDefinition is the user-facing YAML configuration for specifying a
99148
// matrix of benchmark runs.
100149
type TestDefinition struct {
101-
Name string `yaml:"name"`
102-
Description string `yaml:"desciption"`
103-
Variables []Param `yaml:"variables"`
150+
Name string `yaml:"name"`
151+
Snapshot *SnapshotDefinition `yaml:"snapshot"`
152+
Description string `yaml:"description"`
153+
Variables []Param `yaml:"variables"`
104154
}
105155

106156
func (bc *TestDefinition) Check() error {
@@ -120,20 +170,21 @@ func (bc *TestDefinition) Check() error {
120170
}
121171

122172
// TestPlan represents a list of test runs to be executed.
123-
type TestPlan []TestRun
124-
125-
func NewTestPlanFromConfig(c []TestDefinition, testFileName string) (TestPlan, error) {
126-
testPlan := make(TestPlan, 0, len(c))
173+
type TestPlan struct {
174+
Runs []TestRun
175+
Snapshot *SnapshotDefinition
176+
}
127177

128-
for _, m := range c {
129-
params, err := ResolveTestRunsFromMatrix(m, testFileName)
130-
if err != nil {
131-
return nil, err
132-
}
133-
testPlan = append(testPlan, params...)
178+
func NewTestPlanFromConfig(c TestDefinition, testFileName string) (*TestPlan, error) {
179+
testRuns, err := ResolveTestRunsFromMatrix(c, testFileName)
180+
if err != nil {
181+
return nil, err
134182
}
135183

136-
return testPlan, nil
184+
return &TestPlan{
185+
Runs: testRuns,
186+
Snapshot: c.Snapshot,
187+
}, nil
137188
}
138189

139190
var alphaNumericRegex = regexp.MustCompile(`[^a-zA-Z0-9]+`)

runner/benchmark/result_metadata.go

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,20 +48,22 @@ func (runs *BenchmarkRuns) AddResult(testIdx int, runResult BenchmarkRunResult)
4848
runs.Runs[testIdx].Result = &runResult
4949
}
5050

51-
func BenchmarkMetadataFromTestPlan(testPlan TestPlan) BenchmarkRuns {
51+
func BenchmarkMetadataFromTestPlans(testPlans []TestPlan) BenchmarkRuns {
5252
metadata := BenchmarkRuns{
53-
Runs: make([]BenchmarkRun, 0, len(testPlan)),
53+
Runs: make([]BenchmarkRun, 0),
5454
CreatedAt: time.Now(),
5555
}
5656

57-
for _, params := range testPlan {
58-
metadata.Runs = append(metadata.Runs, BenchmarkRun{
59-
SourceFile: params.TestFile,
60-
TestName: params.Name,
61-
TestDescription: params.Description,
62-
TestConfig: params.Params.ToConfig(),
63-
OutputDir: params.OutputDir,
64-
})
57+
for _, testPlan := range testPlans {
58+
for _, params := range testPlan.Runs {
59+
metadata.Runs = append(metadata.Runs, BenchmarkRun{
60+
SourceFile: params.TestFile,
61+
TestName: params.Name,
62+
TestDescription: params.Description,
63+
TestConfig: params.Params.ToConfig(),
64+
OutputDir: params.OutputDir,
65+
})
66+
}
6567
}
6668

6769
return metadata

runner/benchmark/snapshots.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package benchmark
2+
3+
import (
4+
"crypto/sha256"
5+
"fmt"
6+
"path/filepath"
7+
)
8+
9+
// SnapshotManager is an interface that manages snapshots for different node types
10+
// and roles.
11+
type SnapshotManager interface {
12+
// EnsureSnapshot ensures that a snapshot exists for the given node type and
13+
// role. If it does not exist, it will create it using the given snapshot
14+
// definition. It returns the path to the snapshot.
15+
EnsureSnapshot(definition SnapshotDefinition, nodeType string, role string) (string, error)
16+
}
17+
18+
type snapshotStoragePath struct {
19+
// nodeType is the type of node that is using this snapshot.
20+
nodeType string
21+
22+
// role is "validator" or "sequencer". Each must have their own datadir
23+
// because we need to re-execute blocks from scratch on the validator.
24+
role string
25+
26+
// command is the command that created this snapshot.
27+
command string
28+
}
29+
30+
func (s *snapshotStoragePath) Equals(other *snapshotStoragePath) bool {
31+
if s.nodeType != other.nodeType {
32+
return false
33+
}
34+
if s.role != other.role {
35+
return false
36+
}
37+
if s.command != other.command {
38+
return false
39+
}
40+
return true
41+
}
42+
43+
type benchmarkDatadirState struct {
44+
// currentDataDirs is a map of node types to their datadir. Datadirs can be
45+
// reused by multiple tests ro reduce the amount of copying that needs to be
46+
// done.
47+
currentDataDirs map[snapshotStoragePath]string
48+
49+
// snapshotsDir is the directory where all the snapshots are stored. Each
50+
// file will have the format <nodeType>_<role>_<hash_command>.
51+
snapshotsDir string
52+
}
53+
54+
func NewSnapshotManager(snapshotsDir string) SnapshotManager {
55+
return &benchmarkDatadirState{
56+
currentDataDirs: make(map[snapshotStoragePath]string),
57+
snapshotsDir: snapshotsDir,
58+
}
59+
}
60+
61+
func (b *benchmarkDatadirState) EnsureSnapshot(definition SnapshotDefinition, nodeType string, role string) (string, error) {
62+
snapshotDatadir := snapshotStoragePath{
63+
nodeType: nodeType,
64+
role: role,
65+
command: definition.Command,
66+
}
67+
68+
if datadir, ok := b.currentDataDirs[snapshotDatadir]; ok {
69+
return datadir, nil
70+
}
71+
72+
hashCommand := sha256.New().Sum([]byte(definition.Command))
73+
74+
snapshotPath := filepath.Join(b.snapshotsDir, fmt.Sprintf("%s_%s_%x", nodeType, role, hashCommand[:12]))
75+
76+
// Create a new datadir for this snapshot.
77+
err := definition.CreateSnapshot(nodeType, snapshotPath)
78+
if err != nil {
79+
return "", err
80+
}
81+
b.currentDataDirs[snapshotDatadir] = snapshotPath
82+
return snapshotPath, nil
83+
}

runner/clients/reth/client.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,14 @@ func (r *RethClient) Run(ctx context.Context, cfg *types.RuntimeConfig) error {
6969
args = append(args, "--txpool.pending-max-size", "100")
7070
args = append(args, "--txpool.queued-max-size", "100")
7171

72+
// delete datadir/txpool-transactions-backup.rlp if it exists
73+
txpoolBackupPath := fmt.Sprintf("%s/txpool-transactions-backup.rlp", r.options.DataDirPath)
74+
if _, err := os.Stat(txpoolBackupPath); err == nil {
75+
if err := os.Remove(txpoolBackupPath); err != nil {
76+
return errors.Wrap(err, "failed to remove txpool backup")
77+
}
78+
}
79+
7280
// read jwt secret
7381
jwtSecretStr, err := os.ReadFile(r.options.JWTSecretPath)
7482
if err != nil {

0 commit comments

Comments
 (0)