Skip to content

Commit

Permalink
feat(rollup): sync finalized batches from L1 (ethereum#515)
Browse files Browse the repository at this point in the history
* feat(rollup): sync finalized batches from L1

* tweak

* address comments

* address comments

* tweak

* address comments

* Update rollup/eventwatcher/eventwatcher.go

Co-authored-by: Péter Garamvölgyi <peter@scroll.io>

* address comments

* address comments

* address comments

* add s.rollupSyncService.Stop()

* add block chunk batch tests

* fix goimports-lint

* address comments

* address comments

* remove

* add TestUnpackLog test

* add decodeChunkRanges tests

* add commit batch version check

* fix goimport & golint

* tweak

* fixes

* add TestRollupSyncServiceStartAndStop

* add TestGetChunkRanges

* address comments

* add TestValidateBatch

* add TestCalculateFinalizedBatchMeta

* address comments

* address comments

* fix

* address comments

* address comments

* fix

* tweak log

* fix

* tweak logs

* increase wait time

* add --rollup.verify flag

* add flag check in s.rollupSyncService.Stop()

* bump version

* refactor

* tweak configs

* tweak

* refactor getLocalInfoForBatch

* use syscall.Kill(os.Getpid(), syscall.SIGTERM) to shutdown

* refactor decodeChunkBlockRanges

* nit

* address comments

* address comments

* add WithdrawRoot and StateRoot in FinalizedBatchMeta

* address comments

* address comments

* add ctx.Err() check in retry

* add configs in gen_config.go

* bump version

* fix goimport & golint

* try to fix goimport

* revert change

* fix goimport & golint

* tweak

* bump version

* add l2 block -> batch index mapping

* bump version

* add mainnet config

* bump version

* address comments

* rename some vars

* feat: add more detailed logs in crash case (ethereum#547)

add more detailed logs in crash case

* trigger ci

* re-add batch finalization progress log

* remove block number -> batch index mapping

---------

Co-authored-by: Péter Garamvölgyi <peter@scroll.io>
Co-authored-by: HAOYUatHZ <37070449+HAOYUatHZ@users.noreply.github.com>
  • Loading branch information
3 people authored Oct 27, 2023
1 parent bbf4204 commit 4412122
Show file tree
Hide file tree
Showing 28 changed files with 19,845 additions and 22 deletions.
1 change: 1 addition & 0 deletions cmd/geth/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ var (
utils.L1ConfirmationsFlag,
utils.L1DeploymentBlockFlag,
utils.CircuitCapacityCheckEnabledFlag,
utils.RollupVerifyEnabledFlag,
}

rpcFlags = []cli.Flag{
Expand Down
13 changes: 13 additions & 0 deletions cmd/utils/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -836,6 +836,12 @@ var (
Usage: "Enable circuit capacity check during block validation",
}

// Rollup verify service settings
RollupVerifyEnabledFlag = cli.BoolFlag{
Name: "rollup.verify",
Usage: "Enable verification of batch consistency between L1 and L2 in rollup",
}

// Max block range for `eth_getLogs` method
MaxBlockRangeFlag = cli.Int64Flag{
Name: "rpc.getlogs.maxrange",
Expand Down Expand Up @@ -1546,6 +1552,12 @@ func setCircuitCapacityCheck(ctx *cli.Context, cfg *ethconfig.Config) {
}
}

func setEnableRollupVerify(ctx *cli.Context, cfg *ethconfig.Config) {
if ctx.GlobalIsSet(RollupVerifyEnabledFlag.Name) {
cfg.EnableRollupVerify = ctx.GlobalBool(RollupVerifyEnabledFlag.Name)
}
}

func setMaxBlockRange(ctx *cli.Context, cfg *ethconfig.Config) {
if ctx.GlobalIsSet(MaxBlockRangeFlag.Name) {
cfg.MaxBlockRange = ctx.GlobalInt64(MaxBlockRangeFlag.Name)
Expand Down Expand Up @@ -1620,6 +1632,7 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) {
setWhitelist(ctx, cfg)
setLes(ctx, cfg)
setCircuitCapacityCheck(ctx, cfg)
setEnableRollupVerify(ctx, cfg)
setMaxBlockRange(ctx, cfg)

// Cap the cache allowance and tune the garbage collector
Expand Down
146 changes: 146 additions & 0 deletions core/rawdb/accessors_rollup_event.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package rawdb

import (
"bytes"
"math/big"

"github.com/scroll-tech/go-ethereum/common"
"github.com/scroll-tech/go-ethereum/ethdb"
"github.com/scroll-tech/go-ethereum/log"
"github.com/scroll-tech/go-ethereum/rlp"
)

// ChunkBlockRange represents the range of blocks within a chunk.
type ChunkBlockRange struct {
StartBlockNumber uint64
EndBlockNumber uint64
}

// FinalizedBatchMeta holds metadata for finalized batches.
type FinalizedBatchMeta struct {
BatchHash common.Hash
TotalL1MessagePopped uint64 // total number of L1 messages popped before and in this batch.
StateRoot common.Hash
WithdrawRoot common.Hash
}

// WriteRollupEventSyncedL1BlockNumber stores the latest synced L1 block number related to rollup events in the database.
func WriteRollupEventSyncedL1BlockNumber(db ethdb.KeyValueWriter, l1BlockNumber uint64) {
value := big.NewInt(0).SetUint64(l1BlockNumber).Bytes()
if err := db.Put(rollupEventSyncedL1BlockNumberKey, value); err != nil {
log.Crit("failed to store rollup event synced L1 block number for rollup event", "err", err)
}
}

// ReadRollupEventSyncedL1BlockNumber fetches the highest synced L1 block number associated with rollup events from the database.
func ReadRollupEventSyncedL1BlockNumber(db ethdb.Reader) *uint64 {
data, err := db.Get(rollupEventSyncedL1BlockNumberKey)
if err != nil && isNotFoundErr(err) {
return nil
}
if err != nil {
log.Crit("failed to read rollup event synced L1 block number from database", "err", err)
}

number := new(big.Int).SetBytes(data)
if !number.IsUint64() {
log.Crit("unexpected rollup event synced L1 block number in database", "number", number)
}

rollupEventSyncedL1BlockNumber := number.Uint64()
return &rollupEventSyncedL1BlockNumber
}

// WriteBatchChunkRanges writes the block ranges for each chunk within a batch to the database.
// It serializes the chunk ranges using RLP and stores them under a key derived from the batch index.
func WriteBatchChunkRanges(db ethdb.KeyValueWriter, batchIndex uint64, chunkBlockRanges []*ChunkBlockRange) {
bytes, err := rlp.EncodeToBytes(chunkBlockRanges)
if err != nil {
log.Crit("failed to RLP encode batch chunk ranges", "batch index", batchIndex, "err", err)
}
if err := db.Put(batchChunkRangesKey(batchIndex), bytes); err != nil {
log.Crit("failed to store batch chunk ranges", "batch index", batchIndex, "err", err)
}
}

// DeleteBatchChunkRanges removes the block ranges of all chunks associated with a specific batch from the database.
// Note: Only non-finalized batches can be reverted.
func DeleteBatchChunkRanges(db ethdb.KeyValueWriter, batchIndex uint64) {
if err := db.Delete(batchChunkRangesKey(batchIndex)); err != nil {
log.Crit("failed to delete batch chunk ranges", "batch index", batchIndex, "err", err)
}
}

// ReadBatchChunkRanges retrieves the block ranges of all chunks associated with a specific batch from the database.
// It returns a list of ChunkBlockRange pointers, or nil if no chunk ranges are found for the given batch index.
func ReadBatchChunkRanges(db ethdb.Reader, batchIndex uint64) []*ChunkBlockRange {
data, err := db.Get(batchChunkRangesKey(batchIndex))
if err != nil && isNotFoundErr(err) {
return nil
}
if err != nil {
log.Crit("failed to read batch chunk ranges from database", "err", err)
}

cr := new([]*ChunkBlockRange)
if err := rlp.Decode(bytes.NewReader(data), cr); err != nil {
log.Crit("Invalid ChunkBlockRange RLP", "batch index", batchIndex, "data", data, "err", err)
}
return *cr
}

// WriteFinalizedBatchMeta stores the metadata of a finalized batch in the database.
func WriteFinalizedBatchMeta(db ethdb.KeyValueWriter, batchIndex uint64, finalizedBatchMeta *FinalizedBatchMeta) {
var err error
bytes, err := rlp.EncodeToBytes(finalizedBatchMeta)
if err != nil {
log.Crit("failed to RLP encode batch metadata", "batch index", batchIndex, "err", err)
}
if err := db.Put(batchMetaKey(batchIndex), bytes); err != nil {
log.Crit("failed to store batch metadata", "batch index", batchIndex, "err", err)
}
}

// ReadFinalizedBatchMeta fetches the metadata of a finalized batch from the database.
func ReadFinalizedBatchMeta(db ethdb.Reader, batchIndex uint64) *FinalizedBatchMeta {
data, err := db.Get(batchMetaKey(batchIndex))
if err != nil && isNotFoundErr(err) {
return nil
}
if err != nil {
log.Crit("failed to read finalized batch metadata from database", "err", err)
}

fbm := new(FinalizedBatchMeta)
if err := rlp.Decode(bytes.NewReader(data), fbm); err != nil {
log.Crit("Invalid FinalizedBatchMeta RLP", "batch index", batchIndex, "data", data, "err", err)
}
return fbm
}

// WriteFinalizedL2BlockNumber stores the highest finalized L2 block number in the database.
func WriteFinalizedL2BlockNumber(db ethdb.KeyValueWriter, l2BlockNumber uint64) {
value := big.NewInt(0).SetUint64(l2BlockNumber).Bytes()
if err := db.Put(finalizedL2BlockNumberKey, value); err != nil {
log.Crit("failed to store finalized L2 block number for rollup event", "err", err)
}
}

// ReadFinalizedL2BlockNumber fetches the highest finalized L2 block number from the database.
func ReadFinalizedL2BlockNumber(db ethdb.Reader) *uint64 {
data, err := db.Get(finalizedL2BlockNumberKey)
if err != nil && isNotFoundErr(err) {
return nil
}
if err != nil {
log.Crit("failed to read finalized L2 block number from database", "err", err)
}

number := new(big.Int).SetBytes(data)
if !number.IsUint64() {
log.Crit("unexpected finalized L2 block number in database", "number", number)
}

finalizedL2BlockNumber := number.Uint64()
return &finalizedL2BlockNumber
}
186 changes: 186 additions & 0 deletions core/rawdb/accessors_rollup_event_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package rawdb

import (
"testing"

"github.com/scroll-tech/go-ethereum/common"
)

func TestWriteRollupEventSyncedL1BlockNumber(t *testing.T) {
blockNumbers := []uint64{
1,
1 << 2,
1 << 8,
1 << 16,
1 << 32,
}

db := NewMemoryDatabase()

// read non-existing value
if got := ReadRollupEventSyncedL1BlockNumber(db); got != nil {
t.Fatal("Expected 0 for non-existing value", "got", *got)
}

for _, num := range blockNumbers {
WriteRollupEventSyncedL1BlockNumber(db, num)
got := ReadRollupEventSyncedL1BlockNumber(db)

if *got != num {
t.Fatal("Block number mismatch", "expected", num, "got", got)
}
}
}

func TestFinalizedL2BlockNumber(t *testing.T) {
blockNumbers := []uint64{
1,
1 << 2,
1 << 8,
1 << 16,
1 << 32,
}

db := NewMemoryDatabase()

// read non-existing value
if got := ReadFinalizedL2BlockNumber(db); got != nil {
t.Fatal("Expected 0 for non-existing value", "got", *got)
}

for _, num := range blockNumbers {
WriteFinalizedL2BlockNumber(db, num)
got := ReadFinalizedL2BlockNumber(db)

if *got != num {
t.Fatal("Block number mismatch", "expected", num, "got", got)
}
}
}

func TestFinalizedBatchMeta(t *testing.T) {
batches := []*FinalizedBatchMeta{
{
BatchHash: common.BytesToHash([]byte("batch1")),
TotalL1MessagePopped: 123,
StateRoot: common.BytesToHash([]byte("stateRoot1")),
WithdrawRoot: common.BytesToHash([]byte("withdrawRoot1")),
},
{
BatchHash: common.BytesToHash([]byte("batch2")),
TotalL1MessagePopped: 456,
StateRoot: common.BytesToHash([]byte("stateRoot2")),
WithdrawRoot: common.BytesToHash([]byte("withdrawRoot2")),
},
{
BatchHash: common.BytesToHash([]byte("batch3")),
TotalL1MessagePopped: 789,
StateRoot: common.BytesToHash([]byte("stateRoot3")),
WithdrawRoot: common.BytesToHash([]byte("withdrawRoot3")),
},
}

db := NewMemoryDatabase()

for i, batch := range batches {
batchIndex := uint64(i)
WriteFinalizedBatchMeta(db, batchIndex, batch)
}

for i, batch := range batches {
batchIndex := uint64(i)
readBatch := ReadFinalizedBatchMeta(db, batchIndex)
if readBatch == nil {
t.Fatal("Failed to read batch from database")
}
if readBatch.BatchHash != batch.BatchHash || readBatch.TotalL1MessagePopped != batch.TotalL1MessagePopped ||
readBatch.StateRoot != batch.StateRoot || readBatch.WithdrawRoot != batch.WithdrawRoot {
t.Fatal("Mismatch in read batch", "expected", batch, "got", readBatch)
}
}

// over-write
newBatch := &FinalizedBatchMeta{
BatchHash: common.BytesToHash([]byte("newBatch")),
TotalL1MessagePopped: 999,
StateRoot: common.BytesToHash([]byte("newStateRoot")),
WithdrawRoot: common.BytesToHash([]byte("newWithdrawRoot")),
}
WriteFinalizedBatchMeta(db, 0, newBatch) // over-writing the batch with index 0
readBatch := ReadFinalizedBatchMeta(db, 0)
if readBatch.BatchHash != newBatch.BatchHash || readBatch.TotalL1MessagePopped != newBatch.TotalL1MessagePopped ||
readBatch.StateRoot != newBatch.StateRoot || readBatch.WithdrawRoot != newBatch.WithdrawRoot {
t.Fatal("Mismatch after over-writing batch", "expected", newBatch, "got", readBatch)
}

// read non-existing value
nonExistingIndex := uint64(len(batches) + 1)
readBatch = ReadFinalizedBatchMeta(db, nonExistingIndex)
if readBatch != nil {
t.Fatal("Expected nil for non-existing value", "got", readBatch)
}
}

func TestBatchChunkRanges(t *testing.T) {
chunks := [][]*ChunkBlockRange{
{
{StartBlockNumber: 1, EndBlockNumber: 100},
{StartBlockNumber: 101, EndBlockNumber: 200},
},
{
{StartBlockNumber: 201, EndBlockNumber: 300},
{StartBlockNumber: 301, EndBlockNumber: 400},
},
{
{StartBlockNumber: 401, EndBlockNumber: 500},
},
}

db := NewMemoryDatabase()

for i, chunkRange := range chunks {
batchIndex := uint64(i)
WriteBatchChunkRanges(db, batchIndex, chunkRange)
}

for i, chunkRange := range chunks {
batchIndex := uint64(i)
readChunkRange := ReadBatchChunkRanges(db, batchIndex)
if len(readChunkRange) != len(chunkRange) {
t.Fatal("Mismatch in number of chunk ranges", "expected", len(chunkRange), "got", len(readChunkRange))
}

for j, cr := range readChunkRange {
if cr.StartBlockNumber != chunkRange[j].StartBlockNumber || cr.EndBlockNumber != chunkRange[j].EndBlockNumber {
t.Fatal("Mismatch in chunk range", "batch index", batchIndex, "expected", chunkRange[j], "got", cr)
}
}
}

// over-write
newRange := []*ChunkBlockRange{{StartBlockNumber: 1001, EndBlockNumber: 1100}}
WriteBatchChunkRanges(db, 0, newRange)
readChunkRange := ReadBatchChunkRanges(db, 0)
if len(readChunkRange) != 1 || readChunkRange[0].StartBlockNumber != 1001 || readChunkRange[0].EndBlockNumber != 1100 {
t.Fatal("Over-write failed for chunk range", "expected", newRange, "got", readChunkRange)
}

// read non-existing value
if readChunkRange = ReadBatchChunkRanges(db, uint64(len(chunks)+1)); readChunkRange != nil {
t.Fatal("Expected nil for non-existing value", "got", readChunkRange)
}

// delete: revert batch
for i := range chunks {
batchIndex := uint64(i)
DeleteBatchChunkRanges(db, batchIndex)

readChunkRange := ReadBatchChunkRanges(db, batchIndex)
if readChunkRange != nil {
t.Fatal("Chunk range was not deleted", "batch index", batchIndex)
}
}

// delete non-existing value: ensure the delete operation handles non-existing values without errors.
DeleteBatchChunkRanges(db, uint64(len(chunks)+1))
}
Loading

0 comments on commit 4412122

Please sign in to comment.