From 2c846b5601e1f3461cc3a4212a976a261f531591 Mon Sep 17 00:00:00 2001 From: rayoo Date: Mon, 4 May 2026 21:35:14 +0800 Subject: [PATCH] eth/tracers: forward V2 state hooks through mux tracer The mux tracer never exposed OnNonceChangeV2 or OnCodeChangeV2 on the Hooks struct it returns. A child tracer that only implements the V2 variants (which include a reason parameter - CodeChangeReason / NonceChangeReason) silently received no events when wrapped behind the mux. Worse, OnCodeChangeV2 had a fanout method defined (added in #33148) but was never wired into the outer Hooks{} literal, so it was dead code. OnNonceChangeV2 had neither the fanout nor the wire. Mirror the precedence already used in core/state_processor.go and the OnSystemCall fix from #34862: expose only the V2 variants on the Hooks struct, and have the fanout prefer each child's V2 hook, falling back to V1 when only V1 is set. Exposing both V1 and V2 simultaneously would have tripped the "cannot have both" guard in WrapWithJournal and made statedb_hooked pick only V2 (so V1-only children would still lose events). Callers: a child tracer registered via muxTracer config that tracks nonce changes with NonceChangeReason (e.g. to detect EIP-7702 authorizations vs contract creation) or code changes with CodeChangeReason (e.g. to filter self-destruct from creation) was functionally broken - no events reached the child at all for OnNonceChangeV2, and OnCodeChangeV2 specifically became dead code after #33148. Add a regression test covering both V2-only and V1-only children. --- eth/tracers/native/mux.go | 26 +++++----- eth/tracers/native/mux_test.go | 87 ++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 12 deletions(-) create mode 100644 eth/tracers/native/mux_test.go diff --git a/eth/tracers/native/mux.go b/eth/tracers/native/mux.go index 362fb24297..bb2101deec 100644 --- a/eth/tracers/native/mux.go +++ b/eth/tracers/native/mux.go @@ -63,6 +63,12 @@ func newMuxTracerFromConfig(ctx *tracers.Context, cfg json.RawMessage, chainConf // // The names parameter associates a label with each tracer, used as keys in // the aggregated JSON result returned by GetResult. +// +// For hooks that have both a V1 and V2 form (OnCodeChange / OnCodeChangeV2, +// OnNonceChange / OnNonceChangeV2, OnSystemCallStart / OnSystemCallStartV2), +// the mux exposes only the V2 variant upward. The fanout then prefers each +// child's V2 hook and falls back to V1 if only V1 is set, mirroring the +// precedence already used in core/state_processor.go. func NewMuxTracer(names []string, objects []*tracers.Tracer) (*tracers.Tracer, error) { t := &muxTracer{names: names, tracers: objects} return &tracers.Tracer{ @@ -75,8 +81,8 @@ func NewMuxTracer(names []string, objects []*tracers.Tracer) (*tracers.Tracer, e OnFault: t.OnFault, OnGasChange: t.OnGasChange, OnBalanceChange: t.OnBalanceChange, - OnNonceChange: t.OnNonceChange, - OnCodeChange: t.OnCodeChange, + OnNonceChangeV2: t.OnNonceChangeV2, + OnCodeChangeV2: t.OnCodeChangeV2, OnStorageChange: t.OnStorageChange, OnLog: t.OnLog, OnSystemCallStartV2: t.OnSystemCallStart, @@ -151,26 +157,22 @@ func (t *muxTracer) OnBalanceChange(a common.Address, prev, new *big.Int, reason } } -func (t *muxTracer) OnNonceChange(a common.Address, prev, new uint64) { +func (t *muxTracer) OnNonceChangeV2(a common.Address, prev, new uint64, reason tracing.NonceChangeReason) { for _, t := range t.tracers { - if t.OnNonceChange != nil { + if t.OnNonceChangeV2 != nil { + t.OnNonceChangeV2(a, prev, new, reason) + } else if t.OnNonceChange != nil { t.OnNonceChange(a, prev, new) } } } -func (t *muxTracer) OnCodeChange(a common.Address, prevCodeHash common.Hash, prev []byte, codeHash common.Hash, code []byte) { - for _, t := range t.tracers { - if t.OnCodeChange != nil { - t.OnCodeChange(a, prevCodeHash, prev, codeHash, code) - } - } -} - func (t *muxTracer) OnCodeChangeV2(a common.Address, prevCodeHash common.Hash, prev []byte, codeHash common.Hash, code []byte, reason tracing.CodeChangeReason) { for _, t := range t.tracers { if t.OnCodeChangeV2 != nil { t.OnCodeChangeV2(a, prevCodeHash, prev, codeHash, code, reason) + } else if t.OnCodeChange != nil { + t.OnCodeChange(a, prevCodeHash, prev, codeHash, code) } } } diff --git a/eth/tracers/native/mux_test.go b/eth/tracers/native/mux_test.go new file mode 100644 index 0000000000..902b7a026a --- /dev/null +++ b/eth/tracers/native/mux_test.go @@ -0,0 +1,87 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package native + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/eth/tracers" +) + +// TestMuxForwardsV2StateHooks verifies that the mux tracer fans out the V2 +// variants of state-change hooks to child tracers. A child tracer that only +// implements OnCodeChangeV2 / OnNonceChangeV2 must still receive events when +// wrapped behind the mux. The mux must also fall back to the V1 hook when a +// child only implements V1, mirroring the precedence used in +// core/state_processor.go. +func TestMuxForwardsV2StateHooks(t *testing.T) { + var ( + codeV2Calls int + nonceV2Calls int + codeV1Calls int + nonceV1Calls int + ) + v2Child := &tracers.Tracer{ + Hooks: &tracing.Hooks{ + OnCodeChangeV2: func(addr common.Address, prevCodeHash common.Hash, prevCode []byte, codeHash common.Hash, code []byte, reason tracing.CodeChangeReason) { + codeV2Calls++ + }, + OnNonceChangeV2: func(addr common.Address, prev, new uint64, reason tracing.NonceChangeReason) { + nonceV2Calls++ + }, + }, + } + v1Child := &tracers.Tracer{ + Hooks: &tracing.Hooks{ + OnCodeChange: func(addr common.Address, prevCodeHash common.Hash, prevCode []byte, codeHash common.Hash, code []byte) { + codeV1Calls++ + }, + OnNonceChange: func(addr common.Address, prev, new uint64) { + nonceV1Calls++ + }, + }, + } + mux, err := NewMuxTracer([]string{"v2", "v1"}, []*tracers.Tracer{v2Child, v1Child}) + if err != nil { + t.Fatalf("NewMuxTracer: %v", err) + } + + if mux.Hooks.OnCodeChangeV2 == nil { + t.Fatal("mux does not expose OnCodeChangeV2; V2-only child tracers will miss code changes") + } + if mux.Hooks.OnNonceChangeV2 == nil { + t.Fatal("mux does not expose OnNonceChangeV2; V2-only child tracers will miss nonce changes") + } + + mux.Hooks.OnCodeChangeV2(common.Address{}, common.Hash{}, nil, common.Hash{}, nil, tracing.CodeChangeContractCreation) + mux.Hooks.OnNonceChangeV2(common.Address{}, 0, 1, tracing.NonceChangeEoACall) + + if codeV2Calls != 1 { + t.Fatalf("V2 child OnCodeChangeV2 got %d calls, want 1", codeV2Calls) + } + if nonceV2Calls != 1 { + t.Fatalf("V2 child OnNonceChangeV2 got %d calls, want 1", nonceV2Calls) + } + if codeV1Calls != 1 { + t.Fatalf("V1 child OnCodeChange got %d calls, want 1 (mux should fall back from V2 to V1)", codeV1Calls) + } + if nonceV1Calls != 1 { + t.Fatalf("V1 child OnNonceChange got %d calls, want 1 (mux should fall back from V2 to V1)", nonceV1Calls) + } +}