fix crash when attempting to call debug_getAccessList on block that doesn't contain one. remove the new pre/post-tx tracer hooks and use the system contract start/end hooks instead in the bal tracer

This commit is contained in:
Jared Wasinger 2025-12-17 14:29:50 -08:00
parent abaef48d54
commit 630a79e9d4
6 changed files with 80 additions and 16 deletions

View file

@ -17,6 +17,18 @@ type BlockAccessListTracer struct {
// the access list index that changes are currently being recorded into // the access list index that changes are currently being recorded into
balIdx uint16 balIdx uint16
// the number of system calls that have been invoked, used when building
// an access list to determine if the system calls being executed are
// before/after the block transactions.
sysCallCount int
// true if the tracer is processing post-tx state changes. in this case
// we won't record the final index after the end of the second post-tx
// system contract but after the finalization of the block.
// This is because we have EIP-4895 withdrawals which are processed after the
// last system contracts execute and must be included in the BAL.
isPostTx bool
} }
// NewBlockAccessListTracer returns an BlockAccessListTracer and a set of hooks // NewBlockAccessListTracer returns an BlockAccessListTracer and a set of hooks
@ -26,7 +38,6 @@ func NewBlockAccessListTracer() (*BlockAccessListTracer, *tracing.Hooks) {
} }
hooks := &tracing.Hooks{ hooks := &tracing.Hooks{
OnBlockFinalization: balTracer.OnBlockFinalization, OnBlockFinalization: balTracer.OnBlockFinalization,
OnPreTxExecutionDone: balTracer.OnPreTxExecutionDone,
OnTxEnd: balTracer.TxEndHook, OnTxEnd: balTracer.TxEndHook,
OnTxStart: balTracer.TxStartHook, OnTxStart: balTracer.TxStartHook,
OnEnter: balTracer.OnEnter, OnEnter: balTracer.OnEnter,
@ -38,11 +49,16 @@ func NewBlockAccessListTracer() (*BlockAccessListTracer, *tracing.Hooks) {
OnStorageRead: balTracer.OnStorageRead, OnStorageRead: balTracer.OnStorageRead,
OnAccountRead: balTracer.OnAcountRead, OnAccountRead: balTracer.OnAcountRead,
OnSelfDestructChange: balTracer.OnSelfDestruct, OnSelfDestructChange: balTracer.OnSelfDestruct,
OnSystemCallEnd: balTracer.OnSystemCallEnd,
} }
wrappedHooks, _ := tracing.WrapWithJournal(hooks) wrappedHooks, _ := tracing.WrapWithJournal(hooks)
return balTracer, wrappedHooks return balTracer, wrappedHooks
} }
func (a *BlockAccessListTracer) SetPostTx() {
a.isPostTx = true
}
// AccessList returns the constructed access list. // AccessList returns the constructed access list.
// It is assumed that this is only called after all the block state changes // It is assumed that this is only called after all the block state changes
// have been executed and the block has been finalized. // have been executed and the block has been finalized.
@ -50,9 +66,15 @@ func (a *BlockAccessListTracer) AccessList() *bal.AccessListBuilder {
return a.builder return a.builder
} }
func (a *BlockAccessListTracer) OnPreTxExecutionDone() { func (a *BlockAccessListTracer) OnSystemCallEnd() {
a.builder.FinaliseIdxChanges(0) if a.isPostTx {
a.balIdx++ return
}
a.sysCallCount++
if a.sysCallCount == 2 {
a.builder.FinaliseIdxChanges(a.balIdx)
a.balIdx++
}
} }
func (a *BlockAccessListTracer) TxStartHook(vm *tracing.VMContext, tx *types.Transaction, from common.Address) { func (a *BlockAccessListTracer) TxStartHook(vm *tracing.VMContext, tx *types.Transaction, from common.Address) {

View file

@ -2231,7 +2231,6 @@ func (bc *BlockChain) ProcessBlock(parentRoot common.Hash, block *types.Block, s
defer func() { defer func() {
bc.cfg.VmConfig.Tracer = nil bc.cfg.VmConfig.Tracer = nil
}() }()
} }
// Process block using the parent state as reference point // Process block using the parent state as reference point
pstart := time.Now() pstart := time.Now()

View file

@ -52,9 +52,12 @@ func (p *ParallelStateProcessor) prepareExecResult(block *types.Block, allStateR
header := block.Header() header := block.Header()
balTracer, hooks := NewBlockAccessListTracer() balTracer, hooks := NewBlockAccessListTracer()
balTracer.SetPostTx()
tracingStateDB := state.NewHookedState(postTxState, hooks) tracingStateDB := state.NewHookedState(postTxState, hooks)
context := NewEVMBlockContext(header, p.chain, nil) context := NewEVMBlockContext(header, p.chain, nil)
postTxState.SetAccessListIndex(len(block.Transactions()) + 1) lastBALIdx := len(block.Transactions()) + 1
postTxState.SetAccessListIndex(lastBALIdx)
cfg := vm.Config{ cfg := vm.Config{
Tracer: hooks, Tracer: hooks,
@ -122,6 +125,8 @@ func (p *ParallelStateProcessor) prepareExecResult(block *types.Block, allStateR
diff, stateReads := balTracer.builder.FinalizedIdxChanges() diff, stateReads := balTracer.builder.FinalizedIdxChanges()
allStateReads.Merge(stateReads) allStateReads.Merge(stateReads)
// TODO: if there is a failure, we need to print out the detailed logs explaining why the BAL validation failed
// but logs are disabled when we are running tests to prevent a ton of output
balIdx := len(block.Transactions()) + 1 balIdx := len(block.Transactions()) + 1
if !postTxState.BlockAccessList().ValidateStateDiff(balIdx, diff) { if !postTxState.BlockAccessList().ValidateStateDiff(balIdx, diff) {
return &ProcessResultWithMetrics{ return &ProcessResultWithMetrics{
@ -129,9 +134,9 @@ func (p *ParallelStateProcessor) prepareExecResult(block *types.Block, allStateR
} }
} }
if err := postTxState.BlockAccessList().ValidateStateReads(*allStateReads); err != nil { if !postTxState.BlockAccessList().ValidateStateReads(lastBALIdx, *allStateReads) {
return &ProcessResultWithMetrics{ return &ProcessResultWithMetrics{
ProcessResult: &ProcessResult{Error: err}, ProcessResult: &ProcessResult{Error: fmt.Errorf("BAL validation failure")},
} }
} }
@ -315,8 +320,6 @@ func (p *ParallelStateProcessor) Process(block *types.Block, stateTransition *st
ProcessParentBlockHash(block.ParentHash(), evm) ProcessParentBlockHash(block.ParentHash(), evm)
} }
balTracer.OnPreTxExecutionDone()
diff, stateReads := balTracer.builder.FinalizedIdxChanges() diff, stateReads := balTracer.builder.FinalizedIdxChanges()
if !statedb.BlockAccessList().ValidateStateDiff(0, diff) { if !statedb.BlockAccessList().ValidateStateDiff(0, diff) {
return nil, fmt.Errorf("BAL validation failure") return nil, fmt.Errorf("BAL validation failure")

View file

@ -143,7 +143,42 @@ func (r *BALReader) ModifiedAccounts() (res []common.Address) {
return res return res
} }
func (r *BALReader) ValidateStateReads(computedReads bal.StateAccesses) error { func logReadsDiff(idx int, address common.Address, computedReads map[common.Hash]struct{}, expectedReads []*bal.EncodedStorage) {
expectedReadsMap := make(map[common.Hash]struct{})
for _, er := range expectedReads {
expectedReadsMap[er.ToHash()] = struct{}{}
}
allReads := make(map[common.Hash]struct{})
for er := range expectedReadsMap {
allReads[er] = struct{}{}
}
for cr := range computedReads {
allReads[cr] = struct{}{}
}
var missingExpected, missingComputed []common.Hash
for storage := range allReads {
_, hasComputed := computedReads[storage]
_, hasExpected := expectedReadsMap[storage]
if hasComputed && !hasExpected {
missingExpected = append(missingExpected, storage)
}
if !hasComputed && hasExpected {
missingComputed = append(missingComputed, storage)
}
}
if len(missingExpected) > 0 {
log.Error("read storage slots which were not reported in the BAL", "index", idx, "address", address, missingExpected)
}
if len(missingComputed) > 0 {
log.Error("did not read storage slots which were reported in the BAL", "index", idx, "address", address, missingComputed)
}
}
func (r *BALReader) ValidateStateReads(idx int, computedReads bal.StateAccesses) bool {
// 1. remove any slots from 'allReads' which were written // 1. remove any slots from 'allReads' which were written
// 2. validate that the read set in the BAL matches 'allReads' exactly // 2. validate that the read set in the BAL matches 'allReads' exactly
for addr, reads := range computedReads { for addr, reads := range computedReads {
@ -154,22 +189,25 @@ func (r *BALReader) ValidateStateReads(computedReads bal.StateAccesses) error {
} }
} }
if _, ok := r.accesses[addr]; !ok { if _, ok := r.accesses[addr]; !ok {
return fmt.Errorf("account %x was accessed during execution but is not present in the access list", addr) log.Error(fmt.Sprintf("account %x was accessed during execution but is not present in the access list", addr))
return false
} }
expectedReads := r.accesses[addr].StorageReads expectedReads := r.accesses[addr].StorageReads
if len(reads) != len(expectedReads) { if len(reads) != len(expectedReads) {
return fmt.Errorf("mismatch between the number of computed reads and number of expected reads") logReadsDiff(idx, addr, reads, expectedReads)
return false
} }
for _, slot := range expectedReads { for _, slot := range expectedReads {
if _, ok := reads[slot.ToHash()]; !ok { if _, ok := reads[slot.ToHash()]; !ok {
return fmt.Errorf("expected read is missing from BAL") log.Error("expected read is missing from BAL")
return false
} }
} }
} }
return nil return true
} }
// changesAt returns all state changes occurring at the given index. // changesAt returns all state changes occurring at the given index.

View file

@ -546,5 +546,8 @@ func (api *DebugAPI) GetBlockAccessList(number rpc.BlockNumberOrHash) (interface
if block == nil { if block == nil {
return nil, fmt.Errorf("block not found") return nil, fmt.Errorf("block not found")
} }
if block.Body().AccessList == nil {
return nil, nil
}
return block.Body().AccessList.StringableRepresentation(), nil return block.Body().AccessList.StringableRepresentation(), nil
} }

View file

@ -214,7 +214,6 @@ func execBlockTest(t *testing.T, bt *testMatcher, test *BlockTest, buildAndVerif
for _, snapshot := range snapshotConf { for _, snapshot := range snapshotConf {
for _, dbscheme := range dbschemeConf { for _, dbscheme := range dbschemeConf {
//tracer := logger.NewJSONLogger(&logger.Config{}, os.Stdout)
if err := bt.checkFailure(t, test.Run(snapshot, dbscheme, false, buildAndVerifyBAL, nil, nil)); err != nil { if err := bt.checkFailure(t, test.Run(snapshot, dbscheme, false, buildAndVerifyBAL, nil, nil)); err != nil {
t.Errorf("test with config {snapshotter:%v, scheme:%v} failed: %v", snapshot, dbscheme, err) t.Errorf("test with config {snapshotter:%v, scheme:%v} failed: %v", snapshot, dbscheme, err)
return return