mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-27 23:26:44 +00:00
* fix: nil-check curRc * chore: auto version bump [bot] --------- Co-authored-by: omerfirmak <omerfirmak@users.noreply.github.com>
250 lines
8 KiB
Go
250 lines
8 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()
|
|
header.GasUsed = 0
|
|
gasPool := new(core.GasPool).AddGas(header.GasLimit)
|
|
ccc.Reset()
|
|
|
|
accRc := new(types.RowConsumption)
|
|
for txIdx, tx := range block.Transactions() {
|
|
if !isForkStillActive(forkCtx) {
|
|
return noopCb
|
|
}
|
|
|
|
var curRc *types.RowConsumption
|
|
curRc, err = c.checkTxAndApply(parent, header, statedb, gasPool, 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) checkTxAndApply(parent *types.Block, header *types.Header, state *state.StateDB, gasPool *core.GasPool, tx *types.Transaction, ccc *Checker) (*types.RowConsumption, error) {
|
|
// don't commit the state during tracing for circuit capacity checker, otherwise we cannot revert.
|
|
// and even if we don't commit the state, the `refund` value will still be correct, as explained in `CommitTransaction`
|
|
commitStateAfterApply := false
|
|
snap := state.Snapshot()
|
|
|
|
// 1. we have to check circuit capacity before `core.ApplyTransaction`,
|
|
// because if the tx can be successfully executed but circuit capacity overflows, it will be inconvenient to revert.
|
|
// 2. even if we don't commit to the state during the tracing (which means `clearJournalAndRefund` is not called during the tracing),
|
|
// the `refund` value will still be correct, because:
|
|
// 2.1 when starting handling the first tx, `state.refund` is 0 by default,
|
|
// 2.2 after tracing, the state is either committed in `core.ApplyTransaction`, or reverted, so the `state.refund` can be cleared,
|
|
// 2.3 when starting handling the following txs, `state.refund` comes as 0
|
|
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), commitStateAfterApply)
|
|
// `w.current.traceEnv.State` & `w.current.state` share a same pointer to the state, so only need to revert `w.current.state`
|
|
// revert to snapshot for calling `core.ApplyMessage` again, (both `traceEnv.GetBlockTrace` & `core.ApplyTransaction` will call `core.ApplyMessage`)
|
|
state.RevertToSnapshot(snap)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rc, err := ccc.ApplyTransaction(trace)
|
|
if err != nil {
|
|
return rc, err
|
|
}
|
|
|
|
_, err = core.ApplyTransaction(c.bc.Config(), c.bc, nil /* coinbase will default to chainConfig.Scroll.FeeVaultAddress */, gasPool,
|
|
state, header, tx, &header.GasUsed, *c.bc.GetVMConfig())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return rc, nil
|
|
}
|
|
|
|
// 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
|
|
}
|