go-ethereum/rollup/rollup_sync_service/rollup_sync_service.go
Jonas Theis 141a8df143
fix(rollup verifier): nil pointer due to missing CommittedBatchMeta (#1188)
* fix nil pointer due to CommittedBatchMeta not being found in the DB and add rewind of L1 sync height to recover from missing CommittedBatchMeta

* chore: auto version bump [bot]

* formatting

* increase rewindL1height to 100

* make sure no underflow
2025-05-21 10:36:14 +02:00

671 lines
27 KiB
Go

package rollup_sync_service
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"strings"
"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/metrics"
"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
// rewindL1Height is the number of blocks to rewind the L1 sync height when a missing batch event is detected.
rewindL1Height = 100
)
var (
finalizedBlockGauge = metrics.NewRegisteredGauge("chain/head/finalized", nil)
ErrShouldResetSyncHeight = errors.New("ErrShouldResetSyncHeight")
ErrMissingBatchEvent = errors.New("ErrMissingBatchEvent")
)
// 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())
finalizedBlockHeightPtr := rawdb.ReadFinalizedL2BlockNumber(s.db)
if finalizedBlockHeightPtr != nil {
finalizedBlockGauge.Update(int64(*finalizedBlockHeightPtr))
}
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 {
if errors.Is(err, ErrShouldResetSyncHeight) {
log.Warn("Resetting sync height to L1 block 7892668 to fix L1 message queue hash calculation")
s.callDataBlobSource.SetL1Height(7892668)
return nil
}
if errors.Is(err, ErrMissingBatchEvent) {
// make sure no underflow
var rewindTo uint64
if prevL1Height > rewindL1Height {
rewindTo = prevL1Height - rewindL1Height
}
// If there's a missing batch event, rewind the L1 sync height by some blocks to re-fetch from L1 RPC and
// replay creating corresponding CommittedBatchMeta in local DB.
// This happens recursively until the missing event has been recovered as we will call fetchRollupEvents again
// with the `L1Height = prevL1Height - rewindL1Height`.
s.callDataBlobSource.SetL1Height(rewindTo)
return fmt.Errorf("missing batch event, rewinding L1 sync height by %d blocks to %d: %w", rewindL1Height, rewindTo, err)
}
// 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, errors.Join(ErrMissingBatchEvent, err))
}
if parentCommittedBatchMeta == nil {
return fmt.Errorf("parent committed batch meta = nil, batch index: %v, err: %w", index-1, ErrMissingBatchEvent)
}
}
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, errors.Join(ErrMissingBatchEvent, err))
}
if committedBatchMeta == nil {
return fmt.Errorf("committed batch meta = nil, batch index: %v, err: %w", index, ErrMissingBatchEvent)
}
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)
finalizedBlockGauge.Update(int64(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())
chunks[i].Blocks[j-cr.StartBlockNumber] = &encoding.Block{
Header: block.Header(),
Transactions: txData,
}
// read withdraw root, if available
// note: historical state is not available on full nodes
state, err := s.bc.StateAt(block.Root())
if err != nil {
log.Trace("State is not available, skipping withdraw trie validation", "blockNumber", block.NumberU64(), "blockHash", block.Hash().Hex(), "err", err)
continue
}
withdrawRoot := withdrawtrie.ReadWTRSlot(rcfg.L2MessageQueueAddress, state)
chunks[i].Blocks[j-cr.StartBlockNumber].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, errors.Join(ErrMissingBatchEvent, err))
}
if parentCommittedBatchMeta == nil {
return nil, fmt.Errorf("parent committed batch meta = nil, batch index: %v, err: %w", commitedBatch.BatchIndex()-1, ErrMissingBatchEvent)
}
// 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 {
// This is hotfix for the L1 message hash mismatch issue which lead to wrong committedBatchMeta.PostL1MessageQueueHash hashes.
// These in turn lead to a wrongly computed batch hash locally. This happened after upgrading to EuclidV2
// where da-codec was not updated to the latest version in l2geth.
// If the error message due to mismatching PostL1MessageQueueHash contains the same hash as the hardcoded one,
// this means the node ran into this issue.
// We need to reset the sync height to 1 block before the L1 block in which the last batch in CodecV6 was committed.
// The node will overwrite the wrongly computed message queue hashes.
if strings.Contains(err.Error(), "0xaa16faf2a1685fe1d7e0f2810b1a0e98c2841aef96596d10456a6d0f00000000") {
return 0, nil, ErrShouldResetSyncHeight
}
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)
}
// note: this check is optional,
// withdraw root correctness is already implied by state root correctness.
if localWithdrawRoot != (common.Hash{}) && 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: event.WithdrawRoot(),
}
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
}