Skip to content

Commit 3ac69be

Browse files
authored
feat(rollup-relayer): add a tool to analyze chunk/batch/bundle proposing (#1645)
Co-authored-by: colinlyguo <[email protected]>
1 parent e80f030 commit 3ac69be

15 files changed

+574
-5
lines changed

common/utils/flags.go

+10
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ var (
2222
RollupRelayerFlags = []cli.Flag{
2323
&MinCodecVersionFlag,
2424
}
25+
// ProposerToolFlags contains flags only used in proposer tool
26+
ProposerToolFlags = []cli.Flag{
27+
&StartL2BlockFlag,
28+
}
2529
// ConfigFileFlag load json type config file.
2630
ConfigFileFlag = cli.StringFlag{
2731
Name: "config",
@@ -90,4 +94,10 @@ var (
9094
Usage: "Minimum required codec version for the chunk/batch/bundle proposers",
9195
Required: true,
9296
}
97+
// StartL2BlockFlag indicates the start L2 block number for proposer tool
98+
StartL2BlockFlag = cli.Uint64Flag{
99+
Name: "start-l2-block",
100+
Usage: "Start L2 block number for proposer tool",
101+
Value: 0,
102+
}
93103
)

common/version/version.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import (
55
"runtime/debug"
66
)
77

8-
var tag = "v4.5.5"
8+
var tag = "v4.5.6"
99

1010
var commit = func() string {
1111
if info, ok := debug.ReadBuildInfo(); ok {

rollup/README.md

+42
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,45 @@ make rollup_bins
3333
./build/bin/gas_oracle --config ./conf/config.json
3434
./build/bin/rollup_relayer --config ./conf/config.json
3535
```
36+
37+
## Proposer Tool
38+
39+
The Proposer Tool replays historical blocks with custom configurations (e.g., future hardfork configs, custom chunk/batch/bundle proposer configs) to generate chunks/batches/bundles, helping test parameter changes before protocol upgrade.
40+
41+
You can:
42+
43+
1. Enable different hardforks in the genesis configuration.
44+
2. Set custom chunk-proposer, batch-proposer, and bundle-proposer parameters.
45+
3. Analyze resulting metrics (blob size, block count, transaction count, gas usage).
46+
47+
## How to run the proposer tool?
48+
49+
### Set the configs
50+
51+
1. Set genesis config to enable desired hardforks in [`proposer-tool-genesis.json`](./proposer-tool-genesis.json).
52+
2. Set proposer config in [`proposer-tool-config.json`](./proposer-tool-config.json) for data analysis.
53+
3. Set `start-l2-block` in the launch command of proposer-tool in [`docker-compose-proposer-tool.yml`](./docker-compose-proposer-tool.yml) to the block number you want to start from. The default is `0`, which means starting from the genesis block.
54+
55+
### Start the proposer tool using docker-compose
56+
57+
Prerequisite: an RPC URL to an archive L2 node. The default url in [`proposer-tool-config.json`](./proposer-tool-config.json) is `https://rpc.scroll.io`.
58+
59+
```
60+
cd rollup
61+
DOCKER_BUILDKIT=1 docker-compose -f docker-compose-proposer-tool.yml up -d
62+
```
63+
64+
> Note: The port 5432 of database is mapped to the host machine. You can use `psql` or any db clients to connect to the database.
65+
66+
> The DSN for the database is `postgres://postgres:postgres@db:5432/scroll?sslmode=disable`.
67+
68+
69+
### Reset env
70+
```
71+
docker-compose -f docker-compose-proposer-tool.yml down -v
72+
```
73+
74+
If you need to rebuild the images, removing the old images is necessary. You can do this by running the following command:
75+
```
76+
docker images | grep rollup | awk '{print $3}' | xargs docker rmi -f
77+
```

rollup/cmd/proposer_tool/app/app.go

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package app
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"os/signal"
8+
9+
"github.com/scroll-tech/da-codec/encoding"
10+
"github.com/scroll-tech/go-ethereum/log"
11+
"github.com/urfave/cli/v2"
12+
13+
"scroll-tech/common/utils"
14+
"scroll-tech/common/version"
15+
16+
"scroll-tech/rollup/internal/config"
17+
"scroll-tech/rollup/internal/controller/watcher"
18+
)
19+
20+
var app *cli.App
21+
22+
func init() {
23+
// Set up proposer-tool app info.
24+
app = cli.NewApp()
25+
app.Action = action
26+
app.Name = "proposer-tool"
27+
app.Usage = "The Scroll Proposer Tool"
28+
app.Version = version.Version
29+
app.Flags = append(app.Flags, utils.CommonFlags...)
30+
app.Flags = append(app.Flags, utils.RollupRelayerFlags...)
31+
app.Flags = append(app.Flags, utils.ProposerToolFlags...)
32+
app.Commands = []*cli.Command{}
33+
app.Before = func(ctx *cli.Context) error {
34+
return utils.LogSetup(ctx)
35+
}
36+
}
37+
38+
func action(ctx *cli.Context) error {
39+
// Load config file.
40+
cfgFile := ctx.String(utils.ConfigFileFlag.Name)
41+
cfg, err := config.NewConfigForReplay(cfgFile)
42+
if err != nil {
43+
log.Crit("failed to load config file", "config file", cfgFile, "error", err)
44+
}
45+
46+
subCtx, cancel := context.WithCancel(ctx.Context)
47+
48+
startL2BlockHeight := ctx.Uint64(utils.StartL2BlockFlag.Name)
49+
50+
genesisPath := ctx.String(utils.Genesis.Name)
51+
genesis, err := utils.ReadGenesis(genesisPath)
52+
if err != nil {
53+
log.Crit("failed to read genesis", "genesis file", genesisPath, "error", err)
54+
}
55+
56+
minCodecVersion := encoding.CodecVersion(ctx.Uint(utils.MinCodecVersionFlag.Name))
57+
58+
// sanity check config
59+
if cfg.L2Config.BatchProposerConfig.MaxChunksPerBatch <= 0 {
60+
log.Crit("cfg.L2Config.BatchProposerConfig.MaxChunksPerBatch must be greater than 0")
61+
}
62+
if cfg.L2Config.ChunkProposerConfig.MaxL2GasPerChunk <= 0 {
63+
log.Crit("cfg.L2Config.ChunkProposerConfig.MaxL2GasPerChunk must be greater than 0")
64+
}
65+
66+
proposerTool, err := watcher.NewProposerTool(subCtx, cancel, cfg, startL2BlockHeight, minCodecVersion, genesis.Config)
67+
if err != nil {
68+
log.Crit("failed to create proposer tool", "startL2BlockHeight", startL2BlockHeight, "minCodecVersion", minCodecVersion, "error", err)
69+
}
70+
proposerTool.Start()
71+
72+
log.Info("Start proposer-tool successfully", "version", version.Version)
73+
74+
// Catch CTRL-C to ensure a graceful shutdown.
75+
interrupt := make(chan os.Signal, 1)
76+
signal.Notify(interrupt, os.Interrupt)
77+
78+
// Wait until the interrupt signal is received from an OS signal.
79+
<-interrupt
80+
81+
cancel()
82+
proposerTool.Stop()
83+
84+
return nil
85+
}
86+
87+
// Run proposer tool cmd instance.
88+
func Run() {
89+
if err := app.Run(os.Args); err != nil {
90+
_, _ = fmt.Fprintln(os.Stderr, err)
91+
os.Exit(1)
92+
}
93+
}

rollup/cmd/proposer_tool/main.go

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package main
2+
3+
import "scroll-tech/rollup/cmd/proposer_tool/app"
4+
5+
func main() {
6+
app.Run()
7+
}
+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
version: '3'
2+
3+
services:
4+
db:
5+
image: postgres:14
6+
environment:
7+
- POSTGRES_USER=postgres
8+
- POSTGRES_PASSWORD=postgres
9+
- POSTGRES_DB=scroll
10+
ports:
11+
- "5432:5432"
12+
volumes:
13+
- postgres_data:/var/lib/postgresql/data
14+
healthcheck:
15+
test: ["CMD-SHELL", "pg_isready -U postgres"]
16+
interval: 5s
17+
timeout: 5s
18+
retries: 5
19+
20+
proposer-tool:
21+
build:
22+
context: ..
23+
dockerfile: ./rollup/proposer_tool.Dockerfile
24+
depends_on:
25+
db:
26+
condition: service_healthy
27+
command: [
28+
"--config", "/app/conf/proposer-tool-config.json",
29+
"--genesis", "/app/conf/proposer-tool-genesis.json",
30+
"--min-codec-version", "4",
31+
"--start-l2-block", "10000",
32+
"--log.debug", "--verbosity", "3"
33+
]
34+
volumes:
35+
- ./proposer-tool-config.json:/app/conf/proposer-tool-config.json
36+
- ./proposer-tool-genesis.json:/app/conf/proposer-tool-genesis.json
37+
restart: unless-stopped
38+
39+
volumes:
40+
postgres_data:

rollup/internal/config/config.go

+24
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package config
22

33
import (
4+
"encoding/json"
45
"fmt"
6+
"os"
7+
"path/filepath"
58
"reflect"
69
"strings"
710

@@ -20,6 +23,11 @@ type Config struct {
2023
DBConfig *database.Config `json:"db_config"`
2124
}
2225

26+
type ConfigForReplay struct {
27+
Config
28+
DBConfigForReplay *database.Config `json:"db_config_for_replay"`
29+
}
30+
2331
// NewConfig returns a new instance of Config.
2432
func NewConfig(file string) (*Config, error) {
2533
v := viper.New()
@@ -87,3 +95,19 @@ func NewConfig(file string) (*Config, error) {
8795

8896
return cfg, nil
8997
}
98+
99+
// NewConfigForReplay returns a new instance of ConfigForReplay.
100+
func NewConfigForReplay(file string) (*ConfigForReplay, error) {
101+
buf, err := os.ReadFile(filepath.Clean(file))
102+
if err != nil {
103+
return nil, err
104+
}
105+
106+
cfg := &ConfigForReplay{}
107+
err = json.Unmarshal(buf, cfg)
108+
if err != nil {
109+
return nil, err
110+
}
111+
112+
return cfg, nil
113+
}

rollup/internal/controller/watcher/batch_proposer.go

+21
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import (
1313
"github.com/scroll-tech/go-ethereum/params"
1414
"gorm.io/gorm"
1515

16+
"scroll-tech/common/types"
17+
1618
"scroll-tech/rollup/internal/config"
1719
"scroll-tech/rollup/internal/orm"
1820
"scroll-tech/rollup/internal/utils"
@@ -34,6 +36,7 @@ type BatchProposer struct {
3436
maxUncompressedBatchBytesSize uint64
3537
maxChunksPerBatch int
3638

39+
replayMode bool
3740
minCodecVersion encoding.CodecVersion
3841
chainCfg *params.ChainConfig
3942

@@ -80,6 +83,7 @@ func NewBatchProposer(ctx context.Context, cfg *config.BatchProposerConfig, minC
8083
gasCostIncreaseMultiplier: cfg.GasCostIncreaseMultiplier,
8184
maxUncompressedBatchBytesSize: cfg.MaxUncompressedBatchBytesSize,
8285
maxChunksPerBatch: cfg.MaxChunksPerBatch,
86+
replayMode: false,
8387
minCodecVersion: minCodecVersion,
8488
chainCfg: chainCfg,
8589

@@ -152,6 +156,14 @@ func NewBatchProposer(ctx context.Context, cfg *config.BatchProposerConfig, minC
152156
return p
153157
}
154158

159+
// SetReplayDB sets the replay database for the BatchProposer.
160+
// This is used for the proposer tool only, to change the l2_block data source.
161+
// This function is not thread-safe and should be called after initializing the BatchProposer and before starting to propose chunks.
162+
func (p *BatchProposer) SetReplayDB(replayDB *gorm.DB) {
163+
p.l2BlockOrm = orm.NewL2Block(replayDB)
164+
p.replayMode = true
165+
}
166+
155167
// TryProposeBatch tries to propose a new batches.
156168
func (p *BatchProposer) TryProposeBatch() {
157169
p.batchProposerCircleTotal.Inc()
@@ -226,6 +238,15 @@ func (p *BatchProposer) updateDBBatchInfo(batch *encoding.Batch, codecVersion en
226238
log.Warn("BatchProposer.UpdateBatchHashInRange update the chunk's batch hash failure", "hash", dbBatch.Hash, "error", dbErr)
227239
return dbErr
228240
}
241+
if p.replayMode {
242+
// If replayMode is true, meaning the batch was proposed by the proposer tool,
243+
// set batch status to types.RollupCommitted and assign a unique commit tx hash to enable new bundle proposals.
244+
if dbErr = p.batchOrm.UpdateCommitTxHashAndRollupStatus(p.ctx, dbBatch.Hash, dbBatch.Hash, types.RollupCommitted, dbTX); dbErr != nil {
245+
log.Warn("BatchProposer.UpdateCommitTxHashAndRollupStatus update the batch's commit tx hash failure", "hash", dbBatch.Hash, "error", dbErr)
246+
return dbErr
247+
}
248+
}
249+
229250
return nil
230251
})
231252
if err != nil {

rollup/internal/controller/watcher/bundle_proposer.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ func (p *BundleProposer) proposeBundle() error {
199199

200200
currentTimeSec := uint64(time.Now().Unix())
201201
if firstChunk.StartBlockTime+p.bundleTimeoutSec < currentTimeSec {
202-
log.Info("first block timeout", "batch count", len(batches), "start block number", firstChunk.StartBlockNumber, "start block timestamp", firstChunk.StartBlockTime, "current time", currentTimeSec)
202+
log.Info("first block timeout", "batch count", len(batches), "start block number", firstChunk.StartBlockNumber, "start block timestamp", firstChunk.StartBlockTime, "bundle timeout", p.bundleTimeoutSec, "current time", currentTimeSec)
203203

204204
batches, err = p.allBatchesCommittedInSameTXIncluded(batches)
205205
if err != nil {

rollup/internal/controller/watcher/chunk_proposer.go

+22-3
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ type ChunkProposer struct {
3636
gasCostIncreaseMultiplier float64
3737
maxUncompressedBatchBytesSize uint64
3838

39+
replayMode bool
3940
minCodecVersion encoding.CodecVersion
4041
chainCfg *params.ChainConfig
4142

@@ -91,6 +92,7 @@ func NewChunkProposer(ctx context.Context, cfg *config.ChunkProposerConfig, minC
9192
chunkTimeoutSec: cfg.ChunkTimeoutSec,
9293
gasCostIncreaseMultiplier: cfg.GasCostIncreaseMultiplier,
9394
maxUncompressedBatchBytesSize: cfg.MaxUncompressedBatchBytesSize,
95+
replayMode: false,
9496
minCodecVersion: minCodecVersion,
9597
chainCfg: chainCfg,
9698

@@ -175,6 +177,14 @@ func NewChunkProposer(ctx context.Context, cfg *config.ChunkProposerConfig, minC
175177
return p
176178
}
177179

180+
// SetReplayDB sets the replay database for the ChunkProposer.
181+
// This is used for the proposer tool only, to change the l2_block data source.
182+
// This function is not thread-safe and should be called after initializing the ChunkProposer and before starting to propose chunks.
183+
func (p *ChunkProposer) SetReplayDB(replayDB *gorm.DB) {
184+
p.l2BlockOrm = orm.NewL2Block(replayDB)
185+
p.replayMode = true
186+
}
187+
178188
// TryProposeChunk tries to propose a new chunk.
179189
func (p *ChunkProposer) TryProposeChunk() {
180190
p.chunkProposerCircleTotal.Inc()
@@ -241,9 +251,12 @@ func (p *ChunkProposer) updateDBChunkInfo(chunk *encoding.Chunk, codecVersion en
241251
log.Warn("ChunkProposer.InsertChunk failed", "codec version", codecVersion, "err", err)
242252
return err
243253
}
244-
if err := p.l2BlockOrm.UpdateChunkHashInRange(p.ctx, dbChunk.StartBlockNumber, dbChunk.EndBlockNumber, dbChunk.Hash, dbTX); err != nil {
245-
log.Error("failed to update chunk_hash for l2_blocks", "chunk hash", dbChunk.Hash, "start block", dbChunk.StartBlockNumber, "end block", dbChunk.EndBlockNumber, "err", err)
246-
return err
254+
// In replayMode we don't need to update chunk_hash in l2_block table.
255+
if !p.replayMode {
256+
if err := p.l2BlockOrm.UpdateChunkHashInRange(p.ctx, dbChunk.StartBlockNumber, dbChunk.EndBlockNumber, dbChunk.Hash, dbTX); err != nil {
257+
log.Error("failed to update chunk_hash for l2_block", "chunk hash", dbChunk.Hash, "start block", dbChunk.StartBlockNumber, "end block", dbChunk.EndBlockNumber, "err", err)
258+
return err
259+
}
247260
}
248261
return nil
249262
})
@@ -436,6 +449,12 @@ func (p *ChunkProposer) recordTimerChunkMetrics(metrics *utils.ChunkMetrics) {
436449
}
437450

438451
func (p *ChunkProposer) tryProposeEuclidTransitionChunk(blocks []*encoding.Block) (bool, error) {
452+
// If we are in replay mode, there is a corner case when StartL2Block is set as 0 in this check,
453+
// it needs to get genesis block, but in mainnet db there is no genesis block, so we need to bypass this check.
454+
if p.replayMode {
455+
return false, nil
456+
}
457+
439458
if !p.chainCfg.IsEuclid(blocks[0].Header.Time) {
440459
return false, nil
441460
}

0 commit comments

Comments
 (0)