go-ethereum/rollup/ccc/async_checker.go

223 lines
6.3 KiB
Go

package ccc
import (
"context"
"fmt"
"time"
"github.com/sourcegraph/conc/stream"
"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/core/state"
"github.com/scroll-tech/go-ethereum/core/types"
"github.com/scroll-tech/go-ethereum/core/vm"
"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/params"
"github.com/scroll-tech/go-ethereum/rollup/tracing"
)
var (
failCounter = metrics.NewRegisteredCounter("ccc/async/fail", nil)
checkTimer = metrics.NewRegisteredTimer("ccc/async/check", nil)
activeWorkersGauge = metrics.NewRegisteredGauge("ccc/async/active_workers", nil)
)
type Blockchain interface {
Database() ethdb.Database
GetBlock(hash common.Hash, number uint64) *types.Block
StateAt(root common.Hash) (*state.StateDB, error)
Config() *params.ChainConfig
GetVMConfig() *vm.Config
CurrentHeader() *types.Header
core.ChainContext
}
// AsyncChecker allows a caller to spawn CCC verification tasks
type AsyncChecker struct {
bc Blockchain
onFailingBlock func(*types.Block, error)
workers *stream.Stream
freeCheckers chan *Checker
// local state to keep track of the chain progressing and terminate tasks early if needed
currentHead *types.Header
forkCtx context.Context
forkCtxCancelFunc context.CancelFunc
// tests
blockNumberToFail uint64
txnIdxToFail uint64
}
type ErrorWithTxnIdx struct {
TxIdx uint
err error
ShouldSkip bool
AccRc *types.RowConsumption
}
func (e *ErrorWithTxnIdx) Error() string {
return fmt.Sprintf("txn at index %d failed with %s (rc = %s)", e.TxIdx, e.err, fmt.Sprint(e.AccRc))
}
func (e *ErrorWithTxnIdx) Unwrap() error {
return e.err
}
func NewAsyncChecker(bc Blockchain, numWorkers int, lightMode bool) *AsyncChecker {
forkCtx, forkCtxCancelFunc := context.WithCancel(context.Background())
return &AsyncChecker{
bc: bc,
freeCheckers: func(count int) chan *Checker {
checkers := make(chan *Checker, count)
for i := 0; i < count; i++ {
checkers <- NewChecker(lightMode)
}
return checkers
}(numWorkers),
workers: stream.New().WithMaxGoroutines(numWorkers),
currentHead: bc.CurrentHeader(),
forkCtx: forkCtx,
forkCtxCancelFunc: forkCtxCancelFunc,
}
}
func (c *AsyncChecker) WithOnFailingBlock(onFailingBlock func(*types.Block, error)) *AsyncChecker {
c.onFailingBlock = onFailingBlock
return c
}
func (c *AsyncChecker) Wait() {
c.workers.Wait()
}
// Check spawns an async CCC verification task.
func (c *AsyncChecker) Check(block *types.Block) error {
if block.NumberU64() > c.currentHead.Number.Uint64()+1 {
log.Warn("non continuous chain observed in AsyncChecker", "prev", c.currentHead, "got", block.Header())
}
if block.ParentHash() != c.currentHead.Hash() {
// seems like there is a fork happening, a block from the canonical chain must have failed CCC check
// assume the incoming block is the new tip in the fork
c.forkCtx, c.forkCtxCancelFunc = context.WithCancel(context.Background())
}
c.currentHead = block.Header()
checker := <-c.freeCheckers
// all blocks in the same fork share the same context to allow terminating them all at once if needed
ctx, ctxCancelFunc := c.forkCtx, c.forkCtxCancelFunc
c.workers.Go(func() stream.Callback {
taskCb := c.checkerTask(block, checker, ctx, ctxCancelFunc)
return func() {
taskCb()
c.freeCheckers <- checker
}
})
return nil
}
func isForkStillActive(forkCtx context.Context) bool {
select {
case <-forkCtx.Done():
// an ancestor block of this block failed CCC check, this fork is not active anymore
return false
default:
}
return true
}
func (c *AsyncChecker) checkerTask(block *types.Block, ccc *Checker, forkCtx context.Context, forkCtxCancelFunc context.CancelFunc) stream.Callback {
activeWorkersGauge.Inc(1)
checkStart := time.Now()
defer func() {
checkTimer.UpdateSince(checkStart)
activeWorkersGauge.Dec(1)
}()
noopCb := func() {}
parent := c.bc.GetBlock(block.ParentHash(), block.NumberU64()-1)
if parent == nil {
return noopCb // not part of a chain
}
var err error
failingCallback := func() {
failCounter.Inc(1)
if isForkStillActive(forkCtx) {
// we failed the CCC check, cancel the context to signal all tasks preceding this one to terminate early
forkCtxCancelFunc()
if c.onFailingBlock != nil {
c.onFailingBlock(block, err)
}
}
}
if c.blockNumberToFail == block.NumberU64() {
err = &ErrorWithTxnIdx{
TxIdx: uint(c.txnIdxToFail),
err: err,
}
c.blockNumberToFail = 0
return failingCallback
}
statedb, err := c.bc.StateAt(parent.Root())
if err != nil {
return failingCallback
}
header := block.Header()
ccc.Reset()
accRc := new(types.RowConsumption)
for txIdx, tx := range block.Transactions() {
if !isForkStillActive(forkCtx) {
return noopCb
}
var curRc *types.RowConsumption
curRc, err = c.checkTx(parent, header, statedb, tx, ccc)
if err != nil {
err = &ErrorWithTxnIdx{
TxIdx: uint(txIdx),
err: err,
// if the txn is the first in block or the additional resource utilization caused
// by this txn alone is enough to overflow the circuit, skip
ShouldSkip: txIdx == 0 || curRc == nil || curRc.Difference(*accRc).IsOverflown(),
AccRc: curRc,
}
return failingCallback
}
accRc = curRc
}
return func() {
if isForkStillActive(forkCtx) {
// all good, write the row consumption
log.Debug("CCC passed", "blockhash", block.Hash(), "height", block.NumberU64())
rawdb.WriteBlockRowConsumption(c.bc.Database(), block.Hash(), accRc)
}
}
}
func (c *AsyncChecker) checkTx(parent *types.Block, header *types.Header, state *state.StateDB, tx *types.Transaction, ccc *Checker) (*types.RowConsumption, error) {
trace, err := tracing.NewTracerWrapper().CreateTraceEnvAndGetBlockTrace(c.bc.Config(), c.bc, c.bc.Engine(), c.bc.Database(),
state, parent, types.NewBlockWithHeader(header).WithBody([]*types.Transaction{tx}, nil), true)
if err != nil {
return nil, err
}
return ccc.ApplyTransaction(trace)
}
// ScheduleError forces a block to error on a given transaction index
func (c *AsyncChecker) ScheduleError(blockNumber uint64, txnIndx uint64) {
c.blockNumberToFail = blockNumber
c.txnIdxToFail = txnIndx
}