go-ethereum/rollup/rollup_sync_service/rollup_sync_service.go
Jonas Theis 40ebbd6491
feat(L1 follower): adjust to recent CodecV7 and contract changes (#1120)
* port changes from #1013

* port changes from #1068

* go.mod tidy

* fix compile error

* fix goimports

* fix log

* address review comments

* upgrade golang.org/x/net to 0.23.0

* port changes from #1018

* fix tests and linter errors

* address review comments

* refactor rollup sync service / verifier to use CalldataBlobSource to retrieve data from L1

* add configuration and initialize blob clients

* fix unit tests

* remove unused code

* address review comments

* address more review comments

* implement first version of new da-codec and to handle multiple batches submitted in one transaction

* add CommitBatchDAV7 and handle multiple commit events submitted in a single transactions

* fix bug due to previous batch being empty when processing the first batch within a set of batches

* Allow using MPT

* update to latest da-codec

* add field to CommittedBatchMeta to store LastL1MessageQueueHash for CodecV7 batches

* adjust rollup verifier to support CodecV7 batches

* address review comments

* fix issues after merge

* go mod tidy

* fix unit tests

* update da-codec

* add test TestValidateBatchCodecV7

* go mod tidy

* do not log error on shutdown

* add sanity check for version to deserialization of committedBatchMetaV7

* port changes from #1073

* chore: auto version bump [bot]

* address review comments

* add more logs

* disable ENRUpdater if DA sync mode is enabled

* exit pipeline if context is cancelled

* correctly handle override by setting the head of the chain to the parent's height so that created blocks will always become part of canonical chain

* fix error with genesis event being nil

* chore: auto version bump [bot]

* chore: auto version bump [bot]

* adjust to renaming in CodecV7

* implement carrying forward of L1 MessageQueue index

* fix issue after upgrading from old storage to new format where batchIndex was 0 and all batches would be skipped

* add new RevertBatch event

* add commitBatches to be able to read calldata of CodecV7/EuclidV2 committed batches

* implement finding of L1 message queue height for initial batch in recovery mode

* add sanity checks for computed batches from events and batch hashes given via calldata from commit transaction

* update ScrollChain ABI

* chore: auto version bump [bot]

* remove initial batch form DAQueue

* go mod tidy

* fix underflow bug when l1DeploymentBlock==0

* fix bug with wrong parentBatchHash of first batch of batches submitted in a single tx

* update to latest da-codec

* address review comments

* address review comments

* fix bug when l1MessageV2StartIndex==0 serialized to [] (empty slice) which would always be decoded to non-existing instead of 0

* chore: auto version bump [bot]

* cache go dependencies in Dockerfile.mockccc

* chore: auto version bump [bot]

* add INFO log when reverting rollup events in L1 follower mode

* change LastProcessedMessageQueueIndex of finalize event to  totalL1MessagesPoppedOverall

* cleanup

* chore: auto version bump [bot]

---------

Co-authored-by: Ömer Faruk Irmak <omerfirmak@gmail.com>
Co-authored-by: Thegaram <Thegaram@users.noreply.github.com>
Co-authored-by: jonastheis <jonastheis@users.noreply.github.com>
2025-03-03 13:14:32 +01:00

607 lines
24 KiB
Go

package rollup_sync_service
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"sync"
"time"
"github.com/scroll-tech/da-codec/encoding"
"github.com/scroll-tech/go-ethereum/common"
"github.com/scroll-tech/go-ethereum/core"
"github.com/scroll-tech/go-ethereum/core/rawdb"
"github.com/scroll-tech/go-ethereum/ethdb"
"github.com/scroll-tech/go-ethereum/log"
"github.com/scroll-tech/go-ethereum/node"
"github.com/scroll-tech/go-ethereum/params"
"github.com/scroll-tech/go-ethereum/rollup/da_syncer"
"github.com/scroll-tech/go-ethereum/rollup/da_syncer/blob_client"
"github.com/scroll-tech/go-ethereum/rollup/da_syncer/da"
"github.com/scroll-tech/go-ethereum/rollup/l1"
"github.com/scroll-tech/go-ethereum/rollup/rcfg"
"github.com/scroll-tech/go-ethereum/rollup/withdrawtrie"
)
const (
// defaultSyncInterval is the frequency at which we query for new rollup event.
defaultSyncInterval = 30 * time.Second
// defaultMaxRetries is the maximum number of retries allowed when the local node is not synced up to the required block height.
defaultMaxRetries = 20
// defaultGetBlockInRangeRetryDelay is the time delay between retries when attempting to get blocks in range.
// The service will wait for this duration if it detects that the local node has not synced up to the block height
// of a specific L1 batch finalize event.
defaultGetBlockInRangeRetryDelay = 60 * time.Second
// defaultLogInterval is the frequency at which we print the latest processed block.
defaultLogInterval = 5 * time.Minute
)
// RollupSyncService collects ScrollChain batch commit/revert/finalize events and stores metadata into db.
type RollupSyncService struct {
ctx context.Context
cancel context.CancelFunc
db ethdb.Database
bc *core.BlockChain
stack *node.Node
stateMu sync.Mutex
callDataBlobSource *da.CalldataBlobSource
}
func NewRollupSyncService(ctx context.Context, genesisConfig *params.ChainConfig, db ethdb.Database, l1Client l1.Client, bc *core.BlockChain, stack *node.Node, config da_syncer.Config) (*RollupSyncService, error) {
if genesisConfig.Scroll.L1Config == nil {
return nil, fmt.Errorf("missing L1 config in genesis")
}
// Initialize the latestProcessedBlock with the block just before the L1 deployment block.
// This serves as a default value when there's no L1 rollup events synced in the database.
var latestProcessedBlock uint64
if stack.Config().L1DeploymentBlock > 0 {
latestProcessedBlock = stack.Config().L1DeploymentBlock - 1
}
block := rawdb.ReadRollupEventSyncedL1BlockNumber(db)
if block != nil {
// restart from latest synced block number
latestProcessedBlock = *block
}
var success bool
ctx, cancel := context.WithCancel(ctx)
defer func() {
if !success {
cancel()
}
}()
l1Reader, err := l1.NewReader(ctx, l1.Config{
ScrollChainAddress: genesisConfig.Scroll.L1Config.ScrollChainAddress,
L1MessageQueueAddress: genesisConfig.Scroll.L1Config.L1MessageQueueAddress,
}, l1Client)
if err != nil {
return nil, fmt.Errorf("failed to initialize l1.Reader, err = %w", err)
}
blobClientList := blob_client.NewBlobClients()
if config.BeaconNodeAPIEndpoint != "" {
beaconNodeClient, err := blob_client.NewBeaconNodeClient(config.BeaconNodeAPIEndpoint)
if err != nil {
log.Warn("failed to create BeaconNodeClient", "err", err)
} else {
blobClientList.AddBlobClient(beaconNodeClient)
}
}
if config.BlobScanAPIEndpoint != "" {
blobClientList.AddBlobClient(blob_client.NewBlobScanClient(config.BlobScanAPIEndpoint))
}
if config.BlockNativeAPIEndpoint != "" {
blobClientList.AddBlobClient(blob_client.NewBlockNativeClient(config.BlockNativeAPIEndpoint))
}
if blobClientList.Size() == 0 {
return nil, errors.New("no blob client is configured for rollup verifier. Please provide at least one blob client via command line flag")
}
calldataBlobSource, err := da.NewCalldataBlobSource(ctx, latestProcessedBlock, l1Reader, blobClientList, db)
if err != nil {
return nil, fmt.Errorf("failed to create calldata blob source: %w", err)
}
success = true
return &RollupSyncService{
ctx: ctx,
cancel: cancel,
db: db,
bc: bc,
stack: stack,
callDataBlobSource: calldataBlobSource,
}, nil
}
func (s *RollupSyncService) Start() {
if s == nil {
return
}
log.Info("Starting rollup event sync background service", "latest processed block", s.callDataBlobSource.L1Height())
go func() {
syncTicker := time.NewTicker(defaultSyncInterval)
defer syncTicker.Stop()
logTicker := time.NewTicker(defaultLogInterval)
defer logTicker.Stop()
for {
select {
case <-s.ctx.Done():
return
case <-syncTicker.C:
err := s.fetchRollupEvents()
if err != nil {
// Do not log the error if the context is canceled.
select {
case <-s.ctx.Done():
return
default:
}
log.Error("failed to fetch rollup events", "err", err)
}
case <-logTicker.C:
log.Info("Sync rollup events progress update", "latest processed block", s.callDataBlobSource.L1Height())
}
}
}()
}
func (s *RollupSyncService) Stop() {
if s == nil {
return
}
log.Info("Stopping rollup event sync background service")
if s.cancel != nil {
s.cancel()
}
}
// ResetStartSyncHeight resets the RollupSyncService to a specific L1 block height
func (s *RollupSyncService) ResetStartSyncHeight(height uint64) {
if s == nil {
return
}
s.stateMu.Lock()
defer s.stateMu.Unlock()
s.callDataBlobSource.SetL1Height(height)
log.Info("Reset sync service", "height", height)
}
func (s *RollupSyncService) fetchRollupEvents() error {
s.stateMu.Lock()
defer s.stateMu.Unlock()
for {
prevL1Height := s.callDataBlobSource.L1Height()
daEntries, err := s.callDataBlobSource.NextData()
if err != nil {
if errors.Is(err, da.ErrSourceExhausted) {
log.Trace("Sync service exhausted data source, waiting for next data")
return nil
}
return fmt.Errorf("failed to get next data: %w", err)
}
if err = s.updateRollupEvents(daEntries); err != nil {
// Reset the L1 height to the previous value to retry fetching the same data.
s.callDataBlobSource.SetL1Height(prevL1Height)
return fmt.Errorf("failed to parse and update rollup event logs: %w", err)
}
log.Trace("Sync service fetched rollup events", "latest processed L1 block", s.callDataBlobSource.L1Height(), "latest finalized L1 block", s.callDataBlobSource.L1Finalized())
// note: the batch updates in updateRollupEvents are idempotent, if we crash
// before this line and re-execute the previous steps, we will get the same result.
rawdb.WriteRollupEventSyncedL1BlockNumber(s.db, s.callDataBlobSource.L1Height())
}
}
func (s *RollupSyncService) updateRollupEvents(daEntries da.Entries) error {
for _, entry := range daEntries {
switch entry.Type() {
case da.CommitBatchV0Type, da.CommitBatchWithBlobType:
log.Trace("found new CommitBatch event", "batch index", entry.BatchIndex())
entryWithBlocks, ok := entry.(da.EntryWithBlocks)
if !ok {
return fmt.Errorf("failed to cast to EntryWithBlocks, batch index: %v", entry.BatchIndex())
}
committedBatchMeta, err := s.getCommittedBatchMeta(entryWithBlocks)
if err != nil {
return fmt.Errorf("failed to get committed batch meta, batch index: %v, err: %w", entry.BatchIndex(), err)
}
rawdb.WriteCommittedBatchMeta(s.db, entry.BatchIndex(), committedBatchMeta)
case da.RevertBatchType:
log.Trace("found new RevertBatch event", "batch index", entry.BatchIndex())
if err := s.handleRevertEvent(entry.Event()); err != nil {
return fmt.Errorf("failed to handle revert event, batch index: %v, err: %w", entry.BatchIndex(), err)
}
case da.FinalizeBatchType:
event, ok := entry.Event().(*l1.FinalizeBatchEvent)
// This should never happen because we just checked the batch type
if !ok {
return fmt.Errorf("failed to cast to FinalizeBatchEvent, batch index: %v", entry.BatchIndex())
}
batchIndex := entry.BatchIndex()
log.Trace("found new FinalizeBatch event", "batch index", batchIndex)
lastFinalizedBatchIndex := rawdb.ReadLastFinalizedBatchIndex(s.db)
// After Darwin, FinalizeBatch event emitted every bundle, which contains multiple batches.
// Therefore, there are a range of finalized batches need to be saved into db.
//
// The range logic also applies to the batches before Darwin when FinalizeBatch event emitted
// per single batch. In this situation, `batchIndex` just equals to `*lastFinalizedBatchIndex + 1`
// and only one batch is processed through the for loop.
startBatchIndex := batchIndex
if lastFinalizedBatchIndex != nil {
startBatchIndex = *lastFinalizedBatchIndex + 1
} else {
log.Warn("got nil when reading last finalized batch index. This should happen only once.")
}
parentFinalizedBatchMeta := &rawdb.FinalizedBatchMeta{}
if startBatchIndex > 0 {
parentFinalizedBatchMeta = rawdb.ReadFinalizedBatchMeta(s.db, startBatchIndex-1)
}
var highestFinalizedBlockNumber uint64
batchWriter := s.db.NewBatch()
for index := startBatchIndex; index <= batchIndex; index++ {
var parentCommittedBatchMeta *rawdb.CommittedBatchMeta
var err error
if index > 0 {
if parentCommittedBatchMeta, err = rawdb.ReadCommittedBatchMeta(s.db, index-1); err != nil {
return fmt.Errorf("failed to read parent committed batch meta, batch index: %v, err: %w", index-1, err)
}
}
committedBatchMeta, err := rawdb.ReadCommittedBatchMeta(s.db, index)
if err != nil {
return fmt.Errorf("failed to read committed batch meta, batch index: %v, err: %w", index, err)
}
chunks, err := s.getLocalChunksForBatch(committedBatchMeta.ChunkBlockRanges)
if err != nil {
return fmt.Errorf("failed to get local node info, batch index: %v, err: %w", index, err)
}
endBlock, finalizedBatchMeta, err := validateBatch(index, event, parentFinalizedBatchMeta, parentCommittedBatchMeta, committedBatchMeta, chunks, s.stack)
if err != nil {
return fmt.Errorf("fatal: validateBatch failed: finalize event: %v, err: %w", event, err)
}
rawdb.WriteFinalizedBatchMeta(batchWriter, index, finalizedBatchMeta)
highestFinalizedBlockNumber = endBlock
parentFinalizedBatchMeta = finalizedBatchMeta
if index%100 == 0 {
log.Info("finalized batch progress", "batch index", index, "finalized l2 block height", endBlock)
}
}
if err := batchWriter.Write(); err != nil {
log.Error("fatal: failed to batch write finalized batch meta to database", "startBatchIndex", startBatchIndex, "endBatchIndex", batchIndex,
"batchCount", batchIndex-startBatchIndex+1, "highestFinalizedBlockNumber", highestFinalizedBlockNumber, "err", err)
return fmt.Errorf("failed to batch write finalized batch meta to database: %w", err)
}
rawdb.WriteFinalizedL2BlockNumber(s.db, highestFinalizedBlockNumber)
rawdb.WriteLastFinalizedBatchIndex(s.db, batchIndex)
log.Debug("write finalized l2 block number", "batch index", batchIndex, "finalized l2 block height", highestFinalizedBlockNumber)
default:
return fmt.Errorf("unknown daEntry, type: %d, batch index: %d", entry.Type(), entry.BatchIndex())
}
}
return nil
}
func (s *RollupSyncService) handleRevertEvent(event l1.RollupEvent) error {
switch event.Type() {
case l1.RevertEventV0Type:
revertBatch, ok := event.(*l1.RevertBatchEventV0)
if !ok {
return fmt.Errorf("unexpected type of revert event: %T, expected RevertEventV0Type", event)
}
rawdb.DeleteCommittedBatchMeta(s.db, revertBatch.BatchIndex().Uint64())
case l1.RevertEventV7Type:
revertBatch, ok := event.(*l1.RevertBatchEventV7)
if !ok {
return fmt.Errorf("unexpected type of revert event: %T, expected RevertEventV7Type", event)
}
// delete all batches from revertBatch.StartBatchIndex (inclusive) to revertBatch.FinishBatchIndex (inclusive)
for i := revertBatch.StartBatchIndex().Uint64(); i <= revertBatch.FinishBatchIndex().Uint64(); i++ {
rawdb.DeleteCommittedBatchMeta(s.db, i)
}
default:
return fmt.Errorf("unexpected type of revert event: %T", event)
}
return nil
}
func (s *RollupSyncService) getLocalChunksForBatch(chunkBlockRanges []*rawdb.ChunkBlockRange) ([]*encoding.Chunk, error) {
if len(chunkBlockRanges) == 0 {
return nil, fmt.Errorf("chunkBlockRanges is empty")
}
endBlockNumber := chunkBlockRanges[len(chunkBlockRanges)-1].EndBlockNumber
for i := 0; i < defaultMaxRetries; i++ {
if s.ctx.Err() != nil {
log.Info("Context canceled", "reason", s.ctx.Err())
return nil, s.ctx.Err()
}
localSyncedBlockHeight := s.bc.CurrentBlock().Number().Uint64()
if localSyncedBlockHeight >= endBlockNumber {
break // ready to proceed, exit retry loop
}
log.Debug("local node is not synced up to the required block height, waiting for next retry",
"retries", i+1, "local synced block height", localSyncedBlockHeight, "required end block number", endBlockNumber)
time.Sleep(defaultGetBlockInRangeRetryDelay)
}
localSyncedBlockHeight := s.bc.CurrentBlock().Number().Uint64()
if localSyncedBlockHeight < endBlockNumber {
return nil, fmt.Errorf("local node is not synced up to the required block height: %v, local synced block height: %v", endBlockNumber, localSyncedBlockHeight)
}
chunks := make([]*encoding.Chunk, len(chunkBlockRanges))
for i, cr := range chunkBlockRanges {
chunks[i] = &encoding.Chunk{Blocks: make([]*encoding.Block, cr.EndBlockNumber-cr.StartBlockNumber+1)}
for j := cr.StartBlockNumber; j <= cr.EndBlockNumber; j++ {
block := s.bc.GetBlockByNumber(j)
if block == nil {
return nil, fmt.Errorf("failed to get block by number: %v", i)
}
txData := encoding.TxsToTxsData(block.Transactions())
state, err := s.bc.StateAt(block.Root())
if err != nil {
return nil, fmt.Errorf("failed to get block state, block: %v, err: %w", block.Hash().Hex(), err)
}
withdrawRoot := withdrawtrie.ReadWTRSlot(rcfg.L2MessageQueueAddress, state)
chunks[i].Blocks[j-cr.StartBlockNumber] = &encoding.Block{
Header: block.Header(),
Transactions: txData,
WithdrawRoot: withdrawRoot,
}
}
}
return chunks, nil
}
func (s *RollupSyncService) getCommittedBatchMeta(commitedBatch da.EntryWithBlocks) (*rawdb.CommittedBatchMeta, error) {
if commitedBatch.BatchIndex() == 0 {
return &rawdb.CommittedBatchMeta{
Version: 0,
ChunkBlockRanges: []*rawdb.ChunkBlockRange{{StartBlockNumber: 0, EndBlockNumber: 0}},
PostL1MessageQueueHash: common.Hash{},
}, nil
}
chunkRanges, err := blockRangesFromChunks(commitedBatch.Chunks())
if err != nil {
return nil, fmt.Errorf("failed to decode block ranges from chunks, batch index: %v, err: %w", commitedBatch.BatchIndex(), err)
}
// With CodecV7 the batch creation changed. We need to compute and store PostL1MessageQueueHash.
// PrevL1MessageQueueHash of a batch == PostL1MessageQueueHash of the previous batch.
// We need to do this for every committed batch (instead of finalized batch) because the L1MessageQueueHash
// is a continuous hash of all L1 messages over all batches. With bundles we only receive the finalize event
// for the last batch of the bundle.
var lastL1MessageQueueHash common.Hash
if commitedBatch.Version() == encoding.CodecV7 {
parentCommittedBatchMeta, err := rawdb.ReadCommittedBatchMeta(s.db, commitedBatch.BatchIndex()-1)
if err != nil {
return nil, fmt.Errorf("failed to read parent committed batch meta, batch index: %v, err: %w", commitedBatch.BatchIndex()-1, err)
}
// If parent batch has a lower version this means this is the first batch of CodecV7.
// In this case we need to compute the prevL1MessageQueueHash from the empty hash.
var prevL1MessageQueueHash common.Hash
if encoding.CodecVersion(parentCommittedBatchMeta.Version) < commitedBatch.Version() {
prevL1MessageQueueHash = common.Hash{}
} else {
prevL1MessageQueueHash = parentCommittedBatchMeta.PostL1MessageQueueHash
}
chunks, err := s.getLocalChunksForBatch(chunkRanges)
if err != nil {
return nil, fmt.Errorf("failed to get local node info, batch index: %v, err: %w", commitedBatch.BatchIndex(), err)
}
// There is no chunks encoded in a batch anymore with CodecV7.
// For compatibility reason here we still use a single chunk to store the block ranges of the batch.
// We make sure that there is really only one chunk which contains all blocks of the batch.
if len(chunks) != 1 {
return nil, fmt.Errorf("invalid argument: chunk count is not 1 for CodecV7, batch index: %v", commitedBatch.BatchIndex())
}
lastL1MessageQueueHash, err = encoding.MessageQueueV2ApplyL1MessagesFromBlocks(prevL1MessageQueueHash, chunks[0].Blocks)
if err != nil {
return nil, fmt.Errorf("failed to apply L1 messages from blocks, batch index: %v, err: %w", chunks[0], err)
}
}
return &rawdb.CommittedBatchMeta{
Version: uint8(commitedBatch.Version()),
ChunkBlockRanges: chunkRanges,
PostL1MessageQueueHash: lastL1MessageQueueHash,
}, nil
}
// validateBatch verifies the consistency between the L1 contract and L2 node data.
// It performs the following checks:
// 1. Recalculates the batch hash locally
// 2. Compares local state root, local withdraw root, and locally calculated batch hash with L1 data (for the last batch only when "finalize by bundle")
//
// The function will terminate the node and exit if any consistency check fails.
//
// Parameters:
// - batchIndex: batch index of the validated batch
// - event: L1 finalize batch event data
// - parentFinalizedBatchMeta: metadata of the finalized parent batch
// - committedBatchMeta: committed batch metadata stored in the database
// - chunks: slice of chunk data for the current batch
// - stack: node stack to terminate the node in case of inconsistency
//
// Returns:
// - uint64: the end block height of the batch
// - *rawdb.FinalizedBatchMeta: finalized batch metadata
// - error: any error encountered during validation
//
// Note: This function is compatible with both "finalize by batch" and "finalize by bundle" methods.
// In "finalize by bundle", only the last batch of each bundle is fully verified.
// This check still ensures the correctness of all batch hashes in the bundle due to the parent-child relationship between batch hashes.
func validateBatch(batchIndex uint64, event *l1.FinalizeBatchEvent, parentFinalizedBatchMeta *rawdb.FinalizedBatchMeta, parentCommittedBatchMeta *rawdb.CommittedBatchMeta, committedBatchMeta *rawdb.CommittedBatchMeta, chunks []*encoding.Chunk, stack *node.Node) (uint64, *rawdb.FinalizedBatchMeta, error) {
if len(chunks) == 0 {
return 0, nil, fmt.Errorf("invalid argument: length of chunks is 0, batch index: %v", batchIndex)
}
startChunk := chunks[0]
if len(startChunk.Blocks) == 0 {
return 0, nil, fmt.Errorf("invalid argument: block count of start chunk is 0, batch index: %v", batchIndex)
}
startBlock := startChunk.Blocks[0]
endChunk := chunks[len(chunks)-1]
if len(endChunk.Blocks) == 0 {
return 0, nil, fmt.Errorf("invalid argument: block count of end chunk is 0, batch index: %v", batchIndex)
}
endBlock := endChunk.Blocks[len(endChunk.Blocks)-1]
// Note: All params of batch are calculated locally based on the block data.
var batch *encoding.Batch
if encoding.CodecVersion(committedBatchMeta.Version) < encoding.CodecV7 {
batch = &encoding.Batch{
Index: batchIndex,
TotalL1MessagePoppedBefore: parentFinalizedBatchMeta.TotalL1MessagePopped,
ParentBatchHash: parentFinalizedBatchMeta.BatchHash,
Chunks: chunks,
}
} else {
// With CodecV7 the batch creation changed. There is no chunks encoded in a batch anymore.
// For compatibility reason here we still use a single chunk to store the block ranges of the batch.
// We make sure that there is really only one chunk which contains all blocks of the batch.
if len(chunks) != 1 {
return 0, nil, fmt.Errorf("invalid argument: chunk count is not 1 for CodecV7, batch index: %v", batchIndex)
}
batch = &encoding.Batch{
Index: batchIndex,
ParentBatchHash: parentFinalizedBatchMeta.BatchHash,
Blocks: startChunk.Blocks,
PrevL1MessageQueueHash: parentCommittedBatchMeta.PostL1MessageQueueHash,
PostL1MessageQueueHash: committedBatchMeta.PostL1MessageQueueHash,
}
}
codecVersion := encoding.CodecVersion(committedBatchMeta.Version)
codec, err := encoding.CodecFromVersion(codecVersion)
if err != nil {
return 0, nil, fmt.Errorf("unsupported codec version: %v, batch index: %v, err: %w", codecVersion, batchIndex, err)
}
daBatch, err := codec.NewDABatch(batch)
if err != nil {
return 0, nil, fmt.Errorf("failed to create DA batch, batch index: %v, codec version: %v, err: %w", batchIndex, codecVersion, err)
}
localBatchHash := daBatch.Hash()
localStateRoot := endBlock.Header.Root
localWithdrawRoot := endBlock.WithdrawRoot
// Note: If the state root, withdraw root, and batch headers match, this ensures the consistency of blocks and transactions
// (including skipped transactions) between L1 and L2.
//
// Only check when batch index matches the index of the event. This is compatible with both "finalize by batch" and "finalize by bundle":
// - finalize by batch: check all batches
// - finalize by bundle: check the last batch, because only one event (containing the info of the last batch) is emitted per bundle
if batchIndex == event.BatchIndex().Uint64() {
if localStateRoot != event.StateRoot() {
log.Error("State root mismatch", "batch index", event.BatchIndex().Uint64(), "start block", startBlock.Header.Number.Uint64(), "end block", endBlock.Header.Number.Uint64(), "parent batch hash", parentFinalizedBatchMeta.BatchHash.Hex(), "l1 finalized state root", event.StateRoot().Hex(), "l2 state root", localStateRoot.Hex())
stack.Close()
os.Exit(1)
}
if localWithdrawRoot != event.WithdrawRoot() {
log.Error("Withdraw root mismatch", "batch index", event.BatchIndex().Uint64(), "start block", startBlock.Header.Number.Uint64(), "end block", endBlock.Header.Number.Uint64(), "parent batch hash", parentFinalizedBatchMeta.BatchHash.Hex(), "l1 finalized withdraw root", event.WithdrawRoot().Hex(), "l2 withdraw root", localWithdrawRoot.Hex())
stack.Close()
os.Exit(1)
}
// Verify batch hash
// This check ensures the correctness of all batch hashes in the bundle
// due to the parent-child relationship between batch hashes
if localBatchHash != event.BatchHash() {
log.Error("Batch hash mismatch", "batch index", event.BatchIndex().Uint64(), "start block", startBlock.Header.Number.Uint64(), "end block", endBlock.Header.Number.Uint64(), "parent batch hash", parentFinalizedBatchMeta.BatchHash.Hex(), "parent TotalL1MessagePopped", parentFinalizedBatchMeta.TotalL1MessagePopped, "l1 finalized batch hash", event.BatchHash().Hex(), "l2 batch hash", localBatchHash.Hex())
chunksJson, err := json.Marshal(chunks)
if err != nil {
log.Error("marshal chunks failed", "err", err)
}
log.Error("Chunks", "chunks", string(chunksJson))
stack.Close()
os.Exit(1)
}
}
totalL1MessagePopped := parentFinalizedBatchMeta.TotalL1MessagePopped
for _, chunk := range chunks {
totalL1MessagePopped += chunk.NumL1Messages(totalL1MessagePopped)
}
finalizedBatchMeta := &rawdb.FinalizedBatchMeta{
BatchHash: localBatchHash,
TotalL1MessagePopped: totalL1MessagePopped,
StateRoot: localStateRoot,
WithdrawRoot: localWithdrawRoot,
}
return endBlock.Header.Number.Uint64(), finalizedBatchMeta, nil
}
// blockRangesFromChunks decodes the provided chunks into a list of block ranges.
func blockRangesFromChunks(chunks []*encoding.DAChunkRawTx) ([]*rawdb.ChunkBlockRange, error) {
var chunkBlockRanges []*rawdb.ChunkBlockRange
for _, daChunkRawTx := range chunks {
if len(daChunkRawTx.Blocks) == 0 {
return nil, fmt.Errorf("no blocks found in DA chunk, chunk: %+v", daChunkRawTx)
}
chunkBlockRanges = append(chunkBlockRanges, &rawdb.ChunkBlockRange{
StartBlockNumber: daChunkRawTx.Blocks[0].Number(),
EndBlockNumber: daChunkRawTx.Blocks[len(daChunkRawTx.Blocks)-1].Number(),
})
}
return chunkBlockRanges, nil
}