feat: async ccc checker (#996)

This commit is contained in:
Ömer Faruk Irmak 2024-08-23 11:04:12 +03:00 committed by GitHub
parent 972fe8afc4
commit 233a6adc15
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 371 additions and 121 deletions

View file

@ -168,6 +168,7 @@ var (
utils.L1ConfirmationsFlag,
utils.L1DeploymentBlockFlag,
utils.CircuitCapacityCheckEnabledFlag,
utils.CircuitCapacityCheckWorkersFlag,
utils.RollupVerifyEnabledFlag,
utils.ShadowforkPeersFlag,
}

View file

@ -27,6 +27,7 @@ import (
"math/big"
"os"
"path/filepath"
"runtime"
godebug "runtime/debug"
"strconv"
"strings"
@ -847,6 +848,12 @@ var (
Usage: "Enable circuit capacity check during block validation",
}
CircuitCapacityCheckWorkersFlag = cli.UintFlag{
Name: "ccc.numworkers",
Usage: "Set the number of workers that will be used for background CCC tasks",
Value: uint(runtime.GOMAXPROCS(0)),
}
// Rollup verify service settings
RollupVerifyEnabledFlag = cli.BoolFlag{
Name: "rollup.verify",
@ -1572,6 +1579,10 @@ func setWhitelist(ctx *cli.Context, cfg *ethconfig.Config) {
func setCircuitCapacityCheck(ctx *cli.Context, cfg *ethconfig.Config) {
if ctx.GlobalIsSet(CircuitCapacityCheckEnabledFlag.Name) {
cfg.CheckCircuitCapacity = ctx.GlobalBool(CircuitCapacityCheckEnabledFlag.Name)
cfg.CCCMaxWorkers = runtime.GOMAXPROCS(0)
if ctx.GlobalIsSet(CircuitCapacityCheckWorkersFlag.Name) {
cfg.CCCMaxWorkers = int(ctx.GlobalUint(CircuitCapacityCheckWorkersFlag.Name))
}
}
}

View file

@ -17,29 +17,22 @@
package core
import (
"errors"
"fmt"
"sync"
"time"
"github.com/scroll-tech/go-ethereum/consensus"
"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/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/ccc"
"github.com/scroll-tech/go-ethereum/trie"
)
var (
validateL1MessagesTimer = metrics.NewRegisteredTimer("validator/l1msg", nil)
validateRowConsumptionTimer = metrics.NewRegisteredTimer("validator/rowconsumption", nil)
validateTraceTimer = metrics.NewRegisteredTimer("validator/trace", nil)
validateLockTimer = metrics.NewRegisteredTimer("validator/lock", nil)
validateCccTimer = metrics.NewRegisteredTimer("validator/ccc", nil)
asyncValidatorTimer = metrics.NewRegisteredTimer("validator/async", nil)
)
// BlockValidator is responsible for validating block headers, uncles and
@ -50,12 +43,7 @@ type BlockValidator struct {
config *params.ChainConfig // Chain configuration options
bc *BlockChain // Canonical block chain
engine consensus.Engine // Consensus engine used for validating
// circuit capacity checker related fields
checkCircuitCapacity bool // whether enable circuit capacity check
cMu sync.Mutex // mutex for circuit capacity checker
tracer tracerWrapper // scroll tracer wrapper
circuitCapacityChecker *ccc.Checker // circuit capacity checker instance
asyncValidator func(*types.Block) error // Asynchronously run a validation task
}
// NewBlockValidator returns a new block validator which is safe for re-use
@ -68,15 +56,10 @@ func NewBlockValidator(config *params.ChainConfig, blockchain *BlockChain, engin
return validator
}
type tracerWrapper interface {
CreateTraceEnvAndGetBlockTrace(*params.ChainConfig, ChainContext, consensus.Engine, ethdb.Database, *state.StateDB, *types.Block, *types.Block, bool) (*types.BlockTrace, error)
}
func (v *BlockValidator) SetupTracerAndCircuitCapacityChecker(tracer tracerWrapper) {
v.checkCircuitCapacity = true
v.tracer = tracer
v.circuitCapacityChecker = ccc.NewChecker(true)
log.Info("new CircuitCapacityChecker in BlockValidator", "ID", v.circuitCapacityChecker.ID)
// WithAsyncValidator sets up an async validator to be triggered on each new block
func (v *BlockValidator) WithAsyncValidator(asyncValidator func(*types.Block) error) Validator {
v.asyncValidator = asyncValidator
return v
}
// ValidateBody validates the given block's uncles and verifies the block
@ -114,25 +97,13 @@ func (v *BlockValidator) ValidateBody(block *types.Block) error {
if err := v.ValidateL1Messages(block); err != nil {
return err
}
if v.checkCircuitCapacity {
// if a block's RowConsumption has been stored, which means it has been processed before,
// (e.g., in miner/worker.go or in insertChain),
// we simply skip its calculation and validation
if rawdb.ReadBlockRowConsumption(v.bc.db, block.Hash()) != nil {
return nil
}
rowConsumption, err := v.validateCircuitRowConsumption(block)
if err != nil {
if v.asyncValidator != nil {
asyncStart := time.Now()
if err := v.asyncValidator(block); err != nil {
return err
}
log.Trace(
"Validator write block row consumption",
"id", v.circuitCapacityChecker.ID,
"number", block.NumberU64(),
"hash", block.Hash().String(),
"rowConsumption", rowConsumption,
)
rawdb.WriteBlockRowConsumption(v.bc.db, block.Hash(), rowConsumption)
asyncValidatorTimer.UpdateSince(asyncStart)
}
return nil
}
@ -286,61 +257,3 @@ func CalcGasLimit(parentGasLimit, desiredLimit uint64) uint64 {
}
return limit
}
func (v *BlockValidator) createTraceEnvAndGetBlockTrace(block *types.Block) (*types.BlockTrace, error) {
parent := v.bc.GetBlock(block.ParentHash(), block.NumberU64()-1)
if parent == nil {
return nil, errors.New("validateCircuitRowConsumption: no parent block found")
}
statedb, err := v.bc.StateAt(parent.Root())
if err != nil {
return nil, err
}
return v.tracer.CreateTraceEnvAndGetBlockTrace(v.config, v.bc, v.engine, v.bc.db, statedb, parent, block, true)
}
func (v *BlockValidator) validateCircuitRowConsumption(block *types.Block) (*types.RowConsumption, error) {
defer func(t0 time.Time) {
validateRowConsumptionTimer.Update(time.Since(t0))
}(time.Now())
log.Trace(
"Validator apply ccc for block",
"id", v.circuitCapacityChecker.ID,
"number", block.NumberU64(),
"hash", block.Hash().String(),
"len(txs)", block.Transactions().Len(),
)
traceStartTime := time.Now()
traces, err := v.createTraceEnvAndGetBlockTrace(block)
if err != nil {
return nil, err
}
validateTraceTimer.Update(time.Since(traceStartTime))
lockStartTime := time.Now()
v.cMu.Lock()
defer v.cMu.Unlock()
validateLockTimer.Update(time.Since(lockStartTime))
cccStartTime := time.Now()
v.circuitCapacityChecker.Reset()
log.Trace("Validator reset ccc", "id", v.circuitCapacityChecker.ID)
rc, err := v.circuitCapacityChecker.ApplyBlock(traces)
validateCccTimer.Update(time.Since(cccStartTime))
log.Trace(
"Validator apply ccc for block result",
"id", v.circuitCapacityChecker.ID,
"number", block.NumberU64(),
"hash", block.Hash().String(),
"len(txs)", block.Transactions().Len(),
"rc", rc,
"err", err,
)
return rc, err
}

View file

@ -33,9 +33,8 @@ type Validator interface {
// gas used.
ValidateState(block *types.Block, state *state.StateDB, receipts types.Receipts, usedGas uint64) error
// SetupTracerAndCircuitCapacityChecker sets up ScrollTracerWrapper and CircuitCapacityChecker for validator,
// to get scroll-related traces and to validate the circuit row consumption
SetupTracerAndCircuitCapacityChecker(tracer tracerWrapper)
// WithAsyncValidator sets up an async validator to be triggered on each new block
WithAsyncValidator(asyncValidator func(*types.Block) error) Validator
}
// Prefetcher is an interface for pre-caching transaction signatures and state.

View file

@ -2,12 +2,6 @@ package types
import "slices"
type RowUsage struct {
IsOk bool `json:"is_ok"`
RowNumber uint64 `json:"row_number"`
RowUsageDetails []SubCircuitRowUsage `json:"row_usage_details"`
}
//go:generate gencodec -type SubCircuitRowUsage -out gen_row_consumption_json.go
type SubCircuitRowUsage struct {
Name string `json:"name" gencodec:"required"`
@ -15,7 +9,8 @@ type SubCircuitRowUsage struct {
}
// RowConsumptionLimit is the max number of row we support per subcircuit
const RowConsumptionLimit = 1_000_000
// the actual limit is 1M but for safety we go with 950k
const RowConsumptionLimit = 950_000
type RowConsumption []SubCircuitRowUsage

View file

@ -55,9 +55,9 @@ import (
"github.com/scroll-tech/go-ethereum/p2p/enode"
"github.com/scroll-tech/go-ethereum/params"
"github.com/scroll-tech/go-ethereum/rlp"
"github.com/scroll-tech/go-ethereum/rollup/ccc"
"github.com/scroll-tech/go-ethereum/rollup/rollup_sync_service"
"github.com/scroll-tech/go-ethereum/rollup/sync_service"
"github.com/scroll-tech/go-ethereum/rollup/tracing"
"github.com/scroll-tech/go-ethereum/rpc"
)
@ -73,6 +73,7 @@ type Ethereum struct {
txPool *core.TxPool
syncService *sync_service.SyncService
rollupSyncService *rollup_sync_service.RollupSyncService
asyncChecker *ccc.AsyncChecker
blockchain *core.BlockChain
handler *handler
ethDialCandidates enode.Iterator
@ -199,8 +200,11 @@ func New(stack *node.Node, config *ethconfig.Config, l1Client sync_service.EthCl
return nil, err
}
if config.CheckCircuitCapacity {
tracer := tracing.NewTracerWrapper()
eth.blockchain.Validator().SetupTracerAndCircuitCapacityChecker(tracer)
eth.asyncChecker = ccc.NewAsyncChecker(eth.blockchain, config.CCCMaxWorkers, true)
eth.asyncChecker.WithOnFailingBlock(func(b *types.Block, err error) {
log.Warn("block failed CCC check, it will be reorged by the sequencer", "hash", b.Hash(), "err", err)
})
eth.blockchain.Validator().WithAsyncValidator(eth.asyncChecker.Check)
}
// Rewind the chain in case of an incompatible config upgrade.
@ -594,6 +598,9 @@ func (s *Ethereum) Stop() error {
s.rollupSyncService.Stop()
}
s.miner.Close()
if s.config.CheckCircuitCapacity {
s.asyncChecker.Wait()
}
s.blockchain.Stop()
s.engine.Close()
rawdb.PopUncleanShutdownMarker(s.chainDb)

View file

@ -208,6 +208,7 @@ type Config struct {
// Check circuit capacity in block validator
CheckCircuitCapacity bool
CCCMaxWorkers int
// Enable verification of batch consistency between L1 and L2 in rollup
EnableRollupVerify bool

3
go.mod
View file

@ -53,6 +53,7 @@ require (
github.com/scroll-tech/da-codec v0.1.1-0.20240822151711-9e32313056ac
github.com/scroll-tech/zktrie v0.8.4
github.com/shirou/gopsutil v3.21.11+incompatible
github.com/sourcegraph/conc v0.3.0
github.com/status-im/keycard-go v0.0.0-20190316090335-8537d3370df4
github.com/stretchr/testify v1.9.0
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7
@ -99,6 +100,8 @@ require (
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/net v0.16.0 // indirect
golang.org/x/term v0.15.0 // indirect
google.golang.org/protobuf v1.23.0 // indirect

6
go.sum
View file

@ -405,6 +405,8 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
@ -450,7 +452,11 @@ go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=

View file

@ -24,7 +24,7 @@ import (
const (
VersionMajor = 5 // Major version component of the current release
VersionMinor = 6 // Minor version component of the current release
VersionPatch = 3 // Patch version component of the current release
VersionPatch = 4 // Patch version component of the current release
VersionMeta = "mainnet" // Version metadata to append to the version string
)

224
rollup/ccc/async_checker.go Normal file
View file

@ -0,0 +1,224 @@
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
}
type ErrorWithTxnIdx struct {
txIdx uint
err error
shouldSkip bool
}
func (e *ErrorWithTxnIdx) Error() string {
return fmt.Sprintf("txn at index %d failed with %s", e.txIdx, e.err)
}
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.Error("non continuous chain observed in AsyncChecker", "prev", c.currentHead, "got", block.Header())
} else 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 {
return c.checkerTask(block, checker, ctx, ctxCancelFunc)
})
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)
c.freeCheckers <- ccc
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)
}
}
}
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()
var accRc *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.Difference(*accRc).IsOverflown(),
}
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
}

View file

@ -0,0 +1,81 @@
package ccc
import (
"math/big"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/scroll-tech/go-ethereum/common"
"github.com/scroll-tech/go-ethereum/consensus/ethash"
"github.com/scroll-tech/go-ethereum/core"
"github.com/scroll-tech/go-ethereum/core/rawdb"
"github.com/scroll-tech/go-ethereum/core/types"
"github.com/scroll-tech/go-ethereum/core/vm"
"github.com/scroll-tech/go-ethereum/crypto"
"github.com/scroll-tech/go-ethereum/params"
)
func TestAsyncChecker(t *testing.T) {
// testKey is a private key to use for funding a tester account.
testKey, _ := crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
// testAddr is the Ethereum address of the tester account.
testAddr := crypto.PubkeyToAddress(testKey.PublicKey)
// Create a database pre-initialize with a genesis block
db := rawdb.NewMemoryDatabase()
(&core.Genesis{
Config: params.TestChainConfig,
Alloc: core.GenesisAlloc{testAddr: {Balance: new(big.Int).Mul(big.NewInt(1000), big.NewInt(params.Ether))}},
}).MustCommit(db)
chain, _ := core.NewBlockChain(db, nil, params.TestChainConfig, ethash.NewFaker(), vm.Config{}, nil, nil)
asyncChecker := NewAsyncChecker(chain, 1, false)
chain.Validator().WithAsyncValidator(asyncChecker.Check)
bs, _ := core.GenerateChain(params.TestChainConfig, chain.Genesis(), ethash.NewFaker(), db, 100, func(i int, block *core.BlockGen) {
for i := 0; i < 10; i++ {
signer := types.MakeSigner(params.TestChainConfig, block.Number())
tx, err := types.SignTx(types.NewTransaction(block.TxNonce(testAddr), testAddr, big.NewInt(1000), params.TxGas, block.BaseFee(), nil), signer, testKey)
if err != nil {
panic(err)
}
block.AddTx(tx)
}
})
noReorgBlocks := bs[:len(bs)/2]
if _, err := chain.InsertChain(noReorgBlocks); err != nil {
panic(err)
}
time.Sleep(time.Second)
for _, block := range noReorgBlocks {
require.NotNil(t, rawdb.ReadBlockRowConsumption(db, block.Hash()))
}
reorgBlocks := bs[len(bs)/2:]
skippedTxn := reorgBlocks[3].Transactions()[3]
checker := <-asyncChecker.freeCheckers
checker.Skip(skippedTxn.Hash(), ErrBlockRowConsumptionOverflow)
// trigger an error on some later height, we shouldn't get a notification for this
checker.ScheduleError(50, ErrBlockRowConsumptionOverflow)
asyncChecker.freeCheckers <- checker
var failingBlockHash common.Hash
var errWithIdx *ErrorWithTxnIdx
asyncChecker.WithOnFailingBlock(func(b *types.Block, err error) {
failingBlockHash = b.Hash()
require.ErrorAs(t, err, &errWithIdx)
})
if _, err := chain.InsertChain(reorgBlocks); err != nil {
panic(err)
}
time.Sleep(3 * time.Second)
require.Equal(t, reorgBlocks[3].Hash(), failingBlockHash)
require.Equal(t, uint(3), errWithIdx.txIdx)
require.True(t, errWithIdx.shouldSkip)
}

View file

@ -113,7 +113,7 @@ func (ccc *Checker) applyTransactionRustTrace(rustTrace unsafe.Pointer) (*types.
return nil, ErrUnknown
}
if !result.AccRowUsage.IsOk {
return nil, ErrBlockRowConsumptionOverflow
return (*types.RowConsumption)(&result.AccRowUsage.RowUsageDetails), ErrBlockRowConsumptionOverflow
}
return (*types.RowConsumption)(&result.AccRowUsage.RowUsageDetails), nil
}
@ -153,7 +153,7 @@ func (ccc *Checker) ApplyBlock(traces *types.BlockTrace) (*types.RowConsumption,
return nil, ErrUnknown
}
if !result.AccRowUsage.IsOk {
return nil, ErrBlockRowConsumptionOverflow
return (*types.RowConsumption)(&result.AccRowUsage.RowUsageDetails), ErrBlockRowConsumptionOverflow
}
return (*types.RowConsumption)(&result.AccRowUsage.RowUsageDetails), nil
}

View file

@ -44,7 +44,10 @@ func (ccc *Checker) ApplyTransaction(traces *types.BlockTrace) (*types.RowConsum
}
if ccc.skipError != nil {
if traces.Transactions[0].TxHash == ccc.skipHash {
return nil, ccc.skipError
return &types.RowConsumption{types.SubCircuitRowUsage{
Name: "mock",
RowNumber: 1_000_001,
}}, ccc.skipError
}
}
return &types.RowConsumption{types.SubCircuitRowUsage{

View file

@ -15,8 +15,14 @@ type WrappedCommonResult struct {
Error string `json:"error,omitempty"`
}
type RowUsage struct {
IsOk bool `json:"is_ok"`
RowNumber uint64 `json:"row_number"`
RowUsageDetails []types.SubCircuitRowUsage `json:"row_usage_details"`
}
type WrappedRowUsage struct {
AccRowUsage *types.RowUsage `json:"acc_row_usage,omitempty"`
AccRowUsage *RowUsage `json:"acc_row_usage,omitempty"`
Error string `json:"error,omitempty"`
}