Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions consensus/bor/bor.go
Original file line number Diff line number Diff line change
Expand Up @@ -1631,6 +1631,34 @@ func (c *Bor) runMilestoneFetcher() {
}
}

// stateTracingHooks is implemented by state wrappers (state.NewHookedState)
// that emit tracing hooks for the state they wrap.
type stateTracingHooks interface {
Hooks() *tracing.Hooks
}

// systemTxVMConfig returns the vm.Config to use when applying bor system
// transactions (span commits and state-sync events) over the given state.
//
// The tracer is derived from the state itself rather than taken from
// c.vmConfig: canonical block import passes a hooked state when a live tracer
// is configured, so system transactions keep being traced there. Every other
// caller — the miner and eth_simulateV1 via FinalizeAndAssemble, or historical
// state regeneration via Finalize — passes a plain state and must not fire the
// node-wide live tracer: those run outside the import goroutine, and invoking
// the singleton live tracer concurrently corrupts its state and can crash the
// node.
func (c *Bor) systemTxVMConfig(state vm.StateDB) vm.Config {
cfg := c.vmConfig
if hooked, ok := state.(stateTracingHooks); ok {
cfg.Tracer = hooked.Hooks()
} else {
cfg.Tracer = nil
}

return cfg
}

func (c *Bor) checkAndCommitSpan(
state vm.StateDB,
header *types.Header,
Expand Down Expand Up @@ -1753,7 +1781,7 @@ func (c *Bor) FetchAndCommitSpan(
)
}

return c.spanner.CommitSpan(ctx, minSpan, validators, producers, state, header, chain, c.vmConfig)
return c.spanner.CommitSpan(ctx, minSpan, validators, producers, state, header, chain, c.systemTxVMConfig(state))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

systemTxVMConfig sets cfg.Tracer = nil whenever the passed-in state does not implement stateTracingHooks, and the parallel state processor passes an unwrapped *state.StateDB to Finalize. With --vmtrace enabled, bor span-commit and state-sync system transactions therefore execute with no tracer on the parallel execution path.

Impact: Observability only — no state, receipt, or consensus output changes — but live-tracer streams lose bor system transactions on the parallel-processor path with no error or warning, making the gap data-dependent and hard to detect downstream by consumers building external indexes/state feeds.

Evidence — the helper drops the tracer rather than falling back to the engine config:

// consensus/bor/bor.go:1651-1660
func (c *Bor) systemTxVMConfig(state vm.StateDB) vm.Config {
	cfg := c.vmConfig
	if hooked, ok := state.(stateTracingHooks); ok {
 cfg.Tracer = hooked.Hooks()
	} else {
 cfg.Tracer = nil
	}

	return cfg
}

Only *hookedStateDB satisfies stateTracingHooks (core/state/statedb_hooked.go:314), and the two import paths differ:

// core/state_processor.go — serial path wraps the state, tracing preserved
var tracingStateDB = vm.StateDB(statedb)
if hooks := cfg.Tracer; hooks != nil {
	tracingStateDB = state.NewHookedState(statedb, hooks)
}
receipts, err = p.chain.Engine().Finalize(p.chain, header, tracingStateDB, block.Body(), receipts)
// core/parallel_state_processor.go:431 — plain statedb, else-branch taken, tracer nil
receipts, err = p.chain.Engine().Finalize(p.bc.hc, header, statedb, block.Body(), receipts)

The PR and commit bodies declare an intentional change away from c.vmConfig for system transactions, so the contract change itself is deliberate; what needs confirmation is whether dropping tracing on the parallel import path is also intended, since canonical import was meant to keep tracer coverage.

Suggested fix: either wrap the state in core/parallel_state_processor.go before calling Finalize, mirroring the serial path's state.NewHookedState(statedb, hooks), or fall back to the engine-configured tracer instead of nil:

cfg := c.vmConfig
if hooked, ok := state.(stateTracingHooks); ok {
	cfg.Tracer = hooked.Hooks()
}
// otherwise keep cfg.Tracer from c.vmConfig
return cfg

Suggested test: import a block through the parallel state processor with a live tracer configured and assert that OnTxStart/OnEnter fire for the bor span-commit and state-sync system transactions.

Relatedly, the added consensus/bor/vmconfig_test.go (TestSystemTxVMConfig) only covers the helper in isolation; consider adding coverage asserting that system-transaction tracing is preserved on canonical import and suppressed for FinalizeAndAssemble/eth_simulateV1.

}

// CommitStates commit states
Expand Down Expand Up @@ -1855,6 +1883,8 @@ func (c *Bor) CommitStates(

var gasUsed uint64

vmConfig := c.systemTxVMConfig(state)

for _, eventRecord := range eventRecords {
if eventRecord.ID <= lastStateID {
continue
Expand Down Expand Up @@ -1894,7 +1924,7 @@ func (c *Bor) CommitStates(
// we expect that this call MUST emit an event, otherwise we wouldn't make a receipt
// if the receiver address is not a contract then we'll skip the most of the execution and emitting an event as well
// https://github.com/0xPolygon/genesis-contracts/blob/master/contracts/StateReceiver.sol#L27
gasUsed, err = c.GenesisContractsClient.CommitState(eventRecord, state, header, chain, c.vmConfig)
gasUsed, err = c.GenesisContractsClient.CommitState(eventRecord, state, header, chain, vmConfig)
if err != nil {
return nil, err
}
Expand Down
47 changes: 47 additions & 0 deletions consensus/bor/vmconfig_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package bor

import (
"math/big"
"testing"

"github.com/stretchr/testify/require"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
)

// TestSystemTxVMConfig verifies that system transactions (span commits and
// state-sync events) are traced only when the caller traces the state they
// are applied to. The node-wide live tracer stored in c.vmConfig must never
// leak into contexts that pass a plain state (miner and eth_simulateV1 via
// FinalizeAndAssemble, historical state regeneration via Finalize): those run
// outside the import goroutine, and invoking the singleton live tracer
// concurrently corrupts it.
func TestSystemTxVMConfig(t *testing.T) {
t.Parallel()

liveTracer := &tracing.Hooks{
OnEnter: func(depth int, typ byte, from, to common.Address, input []byte, gas uint64, value *big.Int) {
t.Error("live tracer must not be invoked for untraced states")
},
}
c := &Bor{vmConfig: vm.Config{Tracer: liveTracer, NoBaseFee: true}}

plainState, err := state.New(types.EmptyRootHash, state.NewDatabaseForTesting())
require.NoError(t, err)

// A plain state (regeneration, miner, eth_simulateV1) must not get the
// live tracer, but keeps the rest of the config.
cfg := c.systemTxVMConfig(plainState)
require.Nil(t, cfg.Tracer)
require.True(t, cfg.NoBaseFee)

// A hooked state (canonical import with a live tracer) keeps being traced
// with the hooks that trace the state itself.
importHooks := &tracing.Hooks{}
cfg = c.systemTxVMConfig(state.NewHookedState(plainState, importHooks))
require.Same(t, importHooks, cfg.Tracer)
}
5 changes: 5 additions & 0 deletions core/state/statedb_hooked.go
Original file line number Diff line number Diff line change
Expand Up @@ -309,3 +309,8 @@ func (s *hookedStateDB) Logs() []*types.Log {
func (s *hookedStateDB) Inner() *StateDB {
return s.inner
}

// Hooks returns the tracing hooks this state emits to.
func (s *hookedStateDB) Hooks() *tracing.Hooks {
return s.hooks
}