go-ethereum/rollup/da_syncer/syncing_pipeline.go
Jonas Theis ac8164f5a4
feat: follower node sync from DA (#1098)
* 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

* bump version

* remove unused flag

* update da-codec commit

---------

Co-authored-by: Péter Garamvölgyi <peter@scroll.io>
2024-12-18 17:27:51 +01:00

231 lines
7 KiB
Go

package da_syncer
import (
"context"
"errors"
"fmt"
"sync"
"time"
"github.com/scroll-tech/go-ethereum/common/backoff"
"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/params"
"github.com/scroll-tech/go-ethereum/rollup/da_syncer/blob_client"
"github.com/scroll-tech/go-ethereum/rollup/da_syncer/serrors"
"github.com/scroll-tech/go-ethereum/rollup/rollup_sync_service"
"github.com/scroll-tech/go-ethereum/rollup/sync_service"
)
// Config is the configuration parameters of data availability syncing.
type Config struct {
BlobScanAPIEndpoint string // BlobScan blob api endpoint
BlockNativeAPIEndpoint string // BlockNative blob api endpoint
BeaconNodeAPIEndpoint string // Beacon node api endpoint
}
// SyncingPipeline is a derivation pipeline for syncing data from L1 and DA and transform it into
// L2 blocks and chain.
type SyncingPipeline struct {
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
expBackoff *backoff.Exponential
l1DeploymentBlock uint64
db ethdb.Database
blockchain *core.BlockChain
blockQueue *BlockQueue
daSyncer *DASyncer
}
func NewSyncingPipeline(ctx context.Context, blockchain *core.BlockChain, genesisConfig *params.ChainConfig, db ethdb.Database, ethClient sync_service.EthClient, l1DeploymentBlock uint64, config Config) (*SyncingPipeline, error) {
scrollChainABI, err := rollup_sync_service.ScrollChainMetaData.GetAbi()
if err != nil {
return nil, fmt.Errorf("failed to get scroll chain abi: %w", err)
}
l1Client, err := rollup_sync_service.NewL1Client(ctx, ethClient, genesisConfig.Scroll.L1Config.L1ChainId, genesisConfig.Scroll.L1Config.ScrollChainAddress, scrollChainABI)
if err != nil {
return nil, err
}
blobClientList := blob_client.NewBlobClients()
if config.BeaconNodeAPIEndpoint != "" {
beaconNodeClient, err := blob_client.NewBeaconNodeClient(config.BeaconNodeAPIEndpoint, l1Client)
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("DA syncing is enabled but no blob client is configured. Please provide at least one blob client via command line flag")
}
dataSourceFactory := NewDataSourceFactory(blockchain, genesisConfig, config, l1Client, blobClientList, db)
syncedL1Height := l1DeploymentBlock - 1
from := rawdb.ReadDASyncedL1BlockNumber(db)
if from != nil {
syncedL1Height = *from
}
daQueue := NewDAQueue(syncedL1Height, dataSourceFactory)
batchQueue := NewBatchQueue(daQueue, db)
blockQueue := NewBlockQueue(batchQueue)
daSyncer := NewDASyncer(blockchain)
ctx, cancel := context.WithCancel(ctx)
return &SyncingPipeline{
ctx: ctx,
cancel: cancel,
expBackoff: backoff.NewExponential(100*time.Millisecond, 10*time.Second, 100*time.Millisecond),
wg: sync.WaitGroup{},
l1DeploymentBlock: l1DeploymentBlock,
db: db,
blockchain: blockchain,
blockQueue: blockQueue,
daSyncer: daSyncer,
}, nil
}
func (s *SyncingPipeline) Step() error {
block, err := s.blockQueue.NextBlock(s.ctx)
if err != nil {
return err
}
err = s.daSyncer.SyncOneBlock(block)
return err
}
func (s *SyncingPipeline) Start() {
log.Info("sync from DA: starting pipeline")
s.wg.Add(1)
go func() {
s.mainLoop()
s.wg.Done()
}()
}
func (s *SyncingPipeline) mainLoop() {
stepCh := make(chan struct{}, 1)
var delayedStepCh <-chan time.Time
var resetCounter int
var tempErrorCounter int
// reqStep is a helper function to request a step to be executed.
// If delay is true, it will request a delayed step with exponential backoff, otherwise it will request an immediate step.
reqStep := func(delay bool) {
if delay {
if delayedStepCh == nil {
delayDur := s.expBackoff.NextDuration()
delayedStepCh = time.After(delayDur)
log.Debug("requesting delayed step", "delay", delayDur, "attempt", s.expBackoff.Attempt())
} else {
log.Debug("ignoring step request because of ongoing delayed step", "attempt", s.expBackoff.Attempt())
}
} else {
select {
case stepCh <- struct{}{}:
default:
}
}
}
// start pipeline
reqStep(false)
for {
select {
case <-s.ctx.Done():
return
default:
}
select {
case <-s.ctx.Done():
return
case <-delayedStepCh:
delayedStepCh = nil
reqStep(false)
case <-stepCh:
err := s.Step()
if err == nil {
// step succeeded, reset exponential backoff and continue
reqStep(false)
s.expBackoff.Reset()
resetCounter = 0
tempErrorCounter = 0
continue
}
if errors.Is(err, serrors.EOFError) {
// pipeline is empty, request a delayed step
// TODO: eventually (with state manager) this should not trigger a delayed step because external events will trigger a new step anyway
reqStep(true)
tempErrorCounter = 0
continue
} else if errors.Is(err, serrors.TemporaryError) {
log.Warn("syncing pipeline step failed due to temporary error, retrying", "err", err)
if tempErrorCounter > 100 {
log.Warn("syncing pipeline step failed due to 100 consecutive temporary errors, stopping pipeline worker", "last err", err)
return
}
// temporary error, request a delayed step
reqStep(true)
tempErrorCounter++
continue
} else if errors.Is(err, ErrBlockTooLow) {
// block number returned by the block queue is too low,
// we skip the blocks until we reach the correct block number again.
reqStep(false)
tempErrorCounter = 0
continue
} else if errors.Is(err, ErrBlockTooHigh) {
// block number returned by the block queue is too high,
// reset the pipeline and move backwards from the last L1 block we read
s.reset(resetCounter)
resetCounter++
reqStep(false)
tempErrorCounter = 0
continue
} else if errors.Is(err, context.Canceled) {
log.Info("syncing pipeline stopped due to cancelled context", "err", err)
return
}
log.Warn("syncing pipeline step failed due to unrecoverable error, stopping pipeline worker", "err", err)
return
}
}
}
func (s *SyncingPipeline) Stop() {
log.Info("sync from DA: stopping pipeline...")
s.cancel()
s.wg.Wait()
log.Info("sync from DA: stopping pipeline... done")
}
func (s *SyncingPipeline) reset(resetCounter int) {
amount := 100 * uint64(resetCounter)
syncedL1Height := s.l1DeploymentBlock - 1
from := rawdb.ReadDASyncedL1BlockNumber(s.db)
if from != nil && *from+amount > syncedL1Height {
syncedL1Height = *from - amount
rawdb.WriteDASyncedL1BlockNumber(s.db, syncedL1Height)
}
log.Info("resetting syncing pipeline", "syncedL1Height", syncedL1Height)
s.blockQueue.Reset(syncedL1Height)
}