Skip to content
Merged
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
81 changes: 81 additions & 0 deletions core/blockchain_livetracer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// 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 <http://www.gnu.org/licenses/>.

package core

import (
"math/big"
"testing"

"github.com/XinFinOrg/XDPoSChain/common"
"github.com/XinFinOrg/XDPoSChain/consensus/ethash"
"github.com/XinFinOrg/XDPoSChain/core/rawdb"
"github.com/XinFinOrg/XDPoSChain/core/tracing"
"github.com/XinFinOrg/XDPoSChain/core/types"
"github.com/XinFinOrg/XDPoSChain/core/vm"
"github.com/XinFinOrg/XDPoSChain/params"
)

// TestBlockchainInitHook pins that a live tracer attached through vm.Config
// receives the chain configuration when the blockchain is created. The supply
// tracer decides from this hook whether SELFDESTRUCT still burns the destructed
// balance (EIP-8246); if the call disappears the tracer keeps the pre-fork
// accounting silently, so the delivery itself needs a guard.
func TestBlockchainInitHook(t *testing.T) {
gspec := &Genesis{
Config: &params.ChainConfig{
ChainID: big.NewInt(1338),
HomesteadBlock: new(big.Int),
Ethash: new(params.EthashConfig),
},
Alloc: types.GenesisAlloc{
common.Address{1}: {Balance: big.NewInt(1)},
},
Difficulty: big.NewInt(1),
}

var (
calls int
gotCfg *params.ChainConfig
)
hooks := &tracing.Hooks{
OnBlockchainInit: func(chainConfig *params.ChainConfig) {
calls++
gotCfg = chainConfig
},
}
chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), nil, gspec, ethash.NewFaker(), vm.Config{Tracer: hooks})
if err != nil {
t.Fatalf("failed to create the blockchain: %v", err)
}
defer chain.Stop()

if calls != 1 {
t.Fatalf("OnBlockchainInit called %d time(s), want 1", calls)
}
if gotCfg == nil {
t.Fatal("OnBlockchainInit received a nil chain config")
}
if gotCfg.ChainID.Cmp(gspec.Config.ChainID) != 0 {
t.Errorf("OnBlockchainInit chain id = %v, want %v", gotCfg.ChainID, gspec.Config.ChainID)
}
if gotCfg.HomesteadBlock.Cmp(gspec.Config.HomesteadBlock) != 0 {
t.Errorf("OnBlockchainInit homestead block = %v, want %v", gotCfg.HomesteadBlock, gspec.Config.HomesteadBlock)
}
if gotCfg.AmsterdamBlock != nil {
t.Errorf("OnBlockchainInit Amsterdam block = %v, want nil", gotCfg.AmsterdamBlock)
}
}
62 changes: 50 additions & 12 deletions eth/tracers/live/supply.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/XinFinOrg/XDPoSChain/core/vm"
"github.com/XinFinOrg/XDPoSChain/eth/tracers"
"github.com/XinFinOrg/XDPoSChain/log"
"github.com/XinFinOrg/XDPoSChain/params"
"gopkg.in/natefinch/lumberjack.v2"
)

Expand Down Expand Up @@ -66,6 +67,17 @@ type supply struct {
delta supplyInfo
txCallstack []supplyTxCallstack // Callstack for current transaction
logger *lumberjack.Logger
chainConfig *params.ChainConfig

// burnsBalance reports whether SELFDESTRUCT still burns the balance of the
// destructed account in the block being traced. It is false from Amsterdam
// onwards, where EIP-8246 preserves that balance.
burnsBalance bool

// warnedNoConfig reports whether a missing chain configuration has already
// been logged. Only core.NewBlockChain delivers it, so every other mount
// point keeps the pre-fork accounting; warn once instead of on every block.
warnedNoConfig bool
}

type supplyTracerConfig struct {
Expand Down Expand Up @@ -93,18 +105,20 @@ func newSupply(cfg json.RawMessage) (*tracing.Hooks, error) {
}

t := &supply{
delta: newSupplyInfo(),
logger: logger,
delta: newSupplyInfo(),
logger: logger,
burnsBalance: true, // until the chain config says otherwise
}
return &tracing.Hooks{
OnBlockStart: t.OnBlockStart,
OnBlockEnd: t.OnBlockEnd,
OnGenesisBlock: t.OnGenesisBlock,
OnTxStart: t.OnTxStart,
OnBalanceChange: t.OnBalanceChange,
OnEnter: t.OnEnter,
OnExit: t.OnExit,
OnClose: t.OnClose,
OnBlockchainInit: t.OnBlockchainInit,
OnBlockStart: t.OnBlockStart,
OnBlockEnd: t.OnBlockEnd,
OnGenesisBlock: t.OnGenesisBlock,
OnTxStart: t.OnTxStart,
OnBalanceChange: t.OnBalanceChange,
OnEnter: t.OnEnter,
OnExit: t.OnExit,
OnClose: t.OnClose,
}, nil
}

Expand All @@ -131,13 +145,36 @@ func (s *supply) resetDelta() {
s.delta = newSupplyInfo()
}

// OnBlockchainInit receives the chain configuration, which decides whether
// SELFDESTRUCT still burns the destructed balance in a given block. core.
// NewBlockChain invokes it for every live tracer attached to a chain; without
// it the tracer assumes a chain that predates the fork.
func (s *supply) OnBlockchainInit(chainConfig *params.ChainConfig) {
s.chainConfig = chainConfig
}

func (s *supply) OnBlockStart(ev tracing.BlockEvent) {
s.resetDelta()

s.delta.Number = ev.Block.NumberU64()
s.delta.Hash = ev.Block.Hash()
s.delta.ParentHash = ev.Block.ParentHash()

// EIP-8246 removes the SELFDESTRUCT burn from Amsterdam onwards, so the
// balance of a self-destructed account is no longer part of the burn for
// blocks at or after the fork. Treat a missing chain config as pre-fork to
// keep the previous accounting.
//
// The gate is only correct once the consensus side of EIP-8246 is in effect
// too: on a chain that schedules Amsterdam without it, the balance is still
// destroyed but no longer reported here, so the burn is under-reported. Ship
// the tracer change together with the consensus one.
if s.chainConfig == nil && !s.warnedNoConfig {
s.warnedNoConfig = true
log.Warn("Supply tracer has no chain config, accounting SELFDESTRUCT as burning the balance")
}
s.burnsBalance = s.chainConfig == nil || !s.chainConfig.IsAmsterdam(ev.Block.Number())

// Calculate Burn for this block
if ev.Block.BaseFee() != nil {
burn := new(big.Int).Mul(new(big.Int).SetUint64(ev.Block.GasUsed()), ev.Block.BaseFee())
Expand Down Expand Up @@ -209,8 +246,9 @@ func (s *supply) OnEnter(depth int, typ byte, from common.Address, to common.Add
}

// This is a special case of burned amount which has to be handled here
// which happens when type == selfdestruct and from == to.
if vm.OpCode(typ) == vm.SELFDESTRUCT && from == to && value.Cmp(common.Big0) == 1 {
// which happens when type == selfdestruct and from == to. EIP-8246
// (Amsterdam) keeps that balance, so it is not a burn there.
if s.burnsBalance && vm.OpCode(typ) == vm.SELFDESTRUCT && from == to && value.Cmp(common.Big0) == 1 {
call.burn = value
}

Expand Down
193 changes: 193 additions & 0 deletions eth/tracers/live/supply_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
// 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 <http://www.gnu.org/licenses/>.

package live

import (
"bytes"
"encoding/json"
"fmt"
"math/big"
"os"
"path/filepath"
"testing"

"github.com/XinFinOrg/XDPoSChain/common"
"github.com/XinFinOrg/XDPoSChain/core/tracing"
"github.com/XinFinOrg/XDPoSChain/core/types"
"github.com/XinFinOrg/XDPoSChain/core/vm"
"github.com/XinFinOrg/XDPoSChain/params"
)

// TestSupplySelfdestructForkGate pins that the supply tracer only accounts a
// selfdestruct-to-self as burnt while the fork still burns the balance, and
// that it takes the fork from the chain configuration.
func TestSupplySelfdestructForkGate(t *testing.T) {
var (
addr = common.HexToAddress("0x8246")
value = big.NewInt(100)
// The fork boundaries are checked at the block that starts the trace.
tests = []struct {
name string
amsterdam *big.Int
number int64
revertDepth int // frame depth whose exit reports the revert, -1 for none
innerFrame bool // selfdestruct inside a nested call instead of the top-level frame
skipInit bool
wantBurn *big.Int
}{
{"pre-amsterdam", nil, 1, -1, false, false, value},
{"amsterdam", big.NewInt(0), 1, -1, false, false, nil},
{"before-fork-block", big.NewInt(10), 9, -1, false, false, value},
{"at-fork-block", big.NewInt(10), 10, -1, false, false, nil},
{"reverted", nil, 1, 0, false, false, nil},
{"reverted-inner-frame", nil, 1, 1, true, false, nil},
{"no-chain-config", big.NewInt(0), 1, -1, false, true, value},
}
)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
config := *params.AllEthashProtocolChanges
config.AmsterdamBlock = tt.amsterdam

dir := t.TempDir()
hooks, err := newSupply(json.RawMessage(fmt.Sprintf(`{"path":%q}`, dir)))
if err != nil {
t.Fatalf("failed to create the supply tracer: %v", err)
}
// core.NewBlockChain invokes this hook when a live tracer is
// attached; without it the tracer has no chain configuration.
if !tt.skipInit {
if hooks.OnBlockchainInit == nil {
t.Fatal("supply tracer does not register OnBlockchainInit")
}
hooks.OnBlockchainInit(&config)
}
block := types.NewBlockWithHeader(&types.Header{Number: big.NewInt(tt.number)})
hooks.OnBlockStart(tracing.BlockEvent{Block: block})
hooks.OnTxStart(&tracing.VMContext{BlockNumber: block.Number()}, nil, addr)
hooks.OnEnter(0, byte(vm.CALL), addr, addr, nil, 0, new(big.Int))
if tt.innerFrame {
hooks.OnEnter(1, byte(vm.CALL), addr, addr, nil, 0, new(big.Int))
hooks.OnEnter(2, byte(vm.SELFDESTRUCT), addr, addr, nil, 0, value)
hooks.OnExit(2, nil, 0, nil, false)
hooks.OnExit(1, nil, 0, nil, tt.revertDepth == 1)
hooks.OnExit(0, nil, 0, nil, false)
} else {
// The selfdestruct runs in the top-level frame and reports its own
// exit one level deeper, which never carries a revert. Only the
// exit of the frame itself can mark the transaction as reverted.
hooks.OnEnter(1, byte(vm.SELFDESTRUCT), addr, addr, nil, 0, value)
hooks.OnExit(1, nil, 0, nil, false)
hooks.OnExit(0, nil, 0, nil, tt.revertDepth == 0)
}
hooks.OnBlockEnd(nil)
hooks.OnClose()

got := readSupplyBurn(t, dir)
if tt.wantBurn == nil {
if got.Sign() != 0 {
t.Errorf("reported burn = %v, want none", got)
}
return
}
if got.Cmp(tt.wantBurn) != 0 {
t.Errorf("reported burn = %v, want %v", got, tt.wantBurn)
}
})
}
}

// readSupplyBurn reads the produced supply log and returns the misc burn of
// its single block.
func readSupplyBurn(t *testing.T, dir string) *big.Int {
t.Helper()

raw, err := os.ReadFile(filepath.Join(dir, "supply.jsonl"))
if err != nil {
t.Fatalf("failed to read the supply log: %v", err)
}
var info supplyInfo
if err := json.Unmarshal(bytes.TrimSpace(raw), &info); err != nil {
t.Fatalf("failed to decode the supply log: %v", err)
}
if info.Burn == nil || info.Burn.Misc == nil {
return new(big.Int)
}
return info.Burn.Misc
}

// TestSupplyBalanceChangeBurn pins the state-layer half of the selfdestruct
// burn accounting: a BalanceDecreaseSelfdestructBurn reported by the state is
// added to burn.misc, the other selfdestruct reasons are ignored, and the
// accounting does not depend on the fork, because the state layer already
// decides whether a balance is burnt (EIP-8246 removes that decision from the
// SELFDESTRUCT inference in OnEnter, not from this hook).
func TestSupplyBalanceChangeBurn(t *testing.T) {
var (
addr = common.HexToAddress("0x8246")
tests = []struct {
name string
amsterdam *big.Int // nil keeps the fork out of the block
burns []int64 // amounts reported as BalanceDecreaseSelfdestructBurn
noise []int64 // amounts reported with non-burn selfdestruct reasons
wantBurn *big.Int
}{
{"single-burn", nil, []int64{100}, nil, big.NewInt(100)},
{"accumulates", nil, []int64{100, 50}, nil, big.NewInt(150)},
{"other-reasons", nil, nil, []int64{100, 7}, nil},
{"amsterdam-still-accounted", big.NewInt(0), []int64{100}, nil, big.NewInt(100)},
}
)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
config := *params.AllEthashProtocolChanges
config.AmsterdamBlock = tt.amsterdam

dir := t.TempDir()
hooks, err := newSupply(json.RawMessage(fmt.Sprintf(`{"path":%q}`, dir)))
if err != nil {
t.Fatalf("failed to create the supply tracer: %v", err)
}
// core.NewBlockChain invokes this hook when a live tracer is
// attached; deliver it for every case so the block start sees the
// same state as on a real chain.
hooks.OnBlockchainInit(&config)
block := types.NewBlockWithHeader(&types.Header{Number: big.NewInt(1)})
hooks.OnBlockStart(tracing.BlockEvent{Block: block})

for _, amount := range tt.burns {
hooks.OnBalanceChange(addr, big.NewInt(amount), new(big.Int), tracing.BalanceDecreaseSelfdestructBurn)
}
for _, amount := range tt.noise {
hooks.OnBalanceChange(addr, big.NewInt(amount), new(big.Int), tracing.BalanceDecreaseSelfdestruct)
}
hooks.OnBlockEnd(nil)
hooks.OnClose()

got := readSupplyBurn(t, dir)
if tt.wantBurn == nil {
if got.Sign() != 0 {
t.Errorf("reported burn = %v, want none", got)
}
return
}
if got.Cmp(tt.wantBurn) != 0 {
t.Errorf("reported burn = %v, want %v", got, tt.wantBurn)
}
})
}
}
Loading