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
780 changes: 780 additions & 0 deletions core/eip7708_transfer_logs_test.go

Large diffs are not rendered by default.

9 changes: 7 additions & 2 deletions core/evm.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/XinFinOrg/XDPoSChain/core/types"
"github.com/XinFinOrg/XDPoSChain/core/vm"
"github.com/XinFinOrg/XDPoSChain/crypto"
"github.com/XinFinOrg/XDPoSChain/params"
"github.com/holiman/uint256"
)

Expand Down Expand Up @@ -124,9 +125,13 @@ func CanTransfer(db vm.StateDB, addr common.Address, amount *uint256.Int) bool {
return amount.CmpBig(db.GetBalance(addr)) <= 0
}

// Transfer subtracts amount from sender and adds amount to recipient using the given Db
func Transfer(db vm.StateDB, sender, recipient common.Address, amount *uint256.Int) {
// Transfer subtracts amount from sender and adds amount to recipient using the given Db,
// emitting an EIP-7708 transfer log for the moved amount from Amsterdam onwards.
func Transfer(db vm.StateDB, sender, recipient common.Address, amount *uint256.Int, rules *params.Rules) {
amtBig := amount.ToBig()
db.SubBalance(sender, amtBig, tracing.BalanceChangeTransfer)
db.AddBalance(recipient, amtBig, tracing.BalanceChangeTransfer)
if rules.IsAmsterdam && !amount.IsZero() && sender != recipient {
db.AddLog(types.EthTransferLog(sender, recipient, amtBig))
}
}
61 changes: 61 additions & 0 deletions core/state/statedb_log_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// 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 state

import (
"testing"

"github.com/XinFinOrg/XDPoSChain/common"
"github.com/XinFinOrg/XDPoSChain/core/rawdb"
"github.com/XinFinOrg/XDPoSChain/core/types"
)

// TestAddLogSnapshotRevert pins that a log emitted inside a call frame is
// journaled like any other state change: reverting the snapshot removes it and
// rolls the log index back, so the next log reuses the index. EIP-7708 transfer
// logs rely on this to disappear together with a reverted frame.
func TestAddLogSnapshotRevert(t *testing.T) {
sdb, err := New(types.EmptyRootHash, NewDatabase(rawdb.NewMemoryDatabase()))
if err != nil {
t.Fatalf("failed to create state: %v", err)
}
txHash := common.HexToHash("0x01")
sdb.SetTxContext(txHash, 0)

snap := sdb.Snapshot()
sdb.AddLog(&types.Log{Address: common.HexToAddress("0xaa")})
if logs := sdb.GetLogs(txHash, 1, common.Hash{}); len(logs) != 1 {
t.Fatalf("log count before revert = %d, want 1", len(logs))
}
sdb.RevertToSnapshot(snap)
if logs := sdb.GetLogs(txHash, 1, common.Hash{}); len(logs) != 0 {
t.Fatalf("log count after revert = %d, want 0", len(logs))
}

// The block-wide log counter must have been rolled back as well.
sdb.AddLog(&types.Log{Address: common.HexToAddress("0xbb")})
logs := sdb.GetLogs(txHash, 1, common.Hash{})
if len(logs) != 1 {
t.Fatalf("log count after second add = %d, want 1", len(logs))
}
if logs[0].Index != 0 {
t.Errorf("log index after revert = %d, want 0", logs[0].Index)
}
if logs[0].Address != common.HexToAddress("0xbb") {
t.Errorf("log address = %v, want 0xbb", logs[0].Address)
}
}
16 changes: 16 additions & 0 deletions core/types/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ package types
import (
"fmt"
"io"
"math/big"

"github.com/XinFinOrg/XDPoSChain/common"
"github.com/XinFinOrg/XDPoSChain/common/hexutil"
"github.com/XinFinOrg/XDPoSChain/params"
"github.com/XinFinOrg/XDPoSChain/rlp"
)

Expand Down Expand Up @@ -63,6 +65,20 @@ type logMarshaling struct {
Index hexutil.Uint
}

// EthTransferLog creates an ETH transfer log according to EIP-7708.
// Specification: https://eips.ethereum.org/EIPS/eip-7708
func EthTransferLog(from, to common.Address, amount *big.Int) *Log {
return &Log{
Address: params.SystemAddress,
Topics: []common.Hash{
params.EthTransferLogEvent,
common.BytesToHash(from.Bytes()),
common.BytesToHash(to.Bytes()),
},
Data: common.BigToHash(amount).Bytes(),
}
}

type rlpLog struct {
Address common.Address
Topics []common.Hash
Expand Down
2 changes: 1 addition & 1 deletion core/vm/contracts_kzg_stub_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ func TestKZGPointEvaluationStubFails(t *testing.T) {
}
evm := NewEVM(BlockContext{
CanTransfer: func(StateDB, common.Address, *uint256.Int) bool { return true },
Transfer: func(StateDB, common.Address, common.Address, *uint256.Int) {},
Transfer: func(StateDB, common.Address, common.Address, *uint256.Int, *params.Rules) {},
BlockNumber: big.NewInt(1),
Time: 1,
Random: &common.Hash{},
Expand Down
2 changes: 1 addition & 1 deletion core/vm/eip8246_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ func TestSelfdestruct6780EventForkGate(t *testing.T) {
CanTransfer: func(db StateDB, addr common.Address, amount *uint256.Int) bool {
return db.GetBalance(addr).Cmp(amount.ToBig()) >= 0
},
Transfer: func(db StateDB, from, to common.Address, amount *uint256.Int) {
Transfer: func(db StateDB, from, to common.Address, amount *uint256.Int, _ *params.Rules) {
if amount.Sign() == 0 {
return
}
Expand Down
6 changes: 3 additions & 3 deletions core/vm/evm.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ type (
// CanTransferFunc is the signature of a transfer guard function
CanTransferFunc func(StateDB, common.Address, *uint256.Int) bool
// TransferFunc is the signature of a transfer function
TransferFunc func(StateDB, common.Address, common.Address, *uint256.Int)
TransferFunc func(StateDB, common.Address, common.Address, *uint256.Int, *params.Rules)
// GetHashFunc returns the n'th block hash in the blockchain
// and is used by the BLOCKHASH EVM op code.
GetHashFunc func(uint64) common.Hash
Expand Down Expand Up @@ -255,7 +255,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g
}
evm.StateDB.CreateAccount(addr)
}
evm.Context.Transfer(evm.StateDB, caller, addr, value)
evm.Context.Transfer(evm.StateDB, caller, addr, value, &evm.chainRules)

if isPrecompile {
ret, gas, err = RunPrecompiledContract(evm, p, input, gas, evm.Config.Tracer)
Expand Down Expand Up @@ -503,7 +503,7 @@ func (evm *EVM) create(caller common.Address, code []byte, gas uint64, value *ui
if evm.chainRules.IsEIP158 {
evm.StateDB.SetNonce(address, 1, tracing.NonceChangeNewContract)
}
evm.Context.Transfer(evm.StateDB, caller, address, value)
evm.Context.Transfer(evm.StateDB, caller, address, value, &evm.chainRules)

// Initialise a new contract and set the code that is to be used by the EVM.
// The contract is a scoped environment for this execution context only.
Expand Down
6 changes: 3 additions & 3 deletions core/vm/gas_table_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ func TestEIP2200(t *testing.T) {

vmctx := BlockContext{
CanTransfer: func(StateDB, common.Address, *uint256.Int) bool { return true },
Transfer: func(StateDB, common.Address, common.Address, *uint256.Int) {},
Transfer: func(StateDB, common.Address, common.Address, *uint256.Int, *params.Rules) {},
}
evm := NewEVM(vmctx, statedb, nil, params.AllEthashProtocolChanges, Config{ExtraEips: []int{2200}})

Expand Down Expand Up @@ -147,7 +147,7 @@ func TestEIP2200UsesCombinedStateGetter(t *testing.T) {
countingDB := &countingStateDB{StateDB: statedb}
vmctx := BlockContext{
CanTransfer: func(StateDB, common.Address, *uint256.Int) bool { return true },
Transfer: func(StateDB, common.Address, common.Address, *uint256.Int) {},
Transfer: func(StateDB, common.Address, common.Address, *uint256.Int, *params.Rules) {},
}
evm := NewEVM(vmctx, countingDB, nil, params.AllEthashProtocolChanges, Config{ExtraEips: []int{2200}})

Expand Down Expand Up @@ -209,7 +209,7 @@ func TestCreateGas(t *testing.T) {
statedb.Finalise(true)
vmctx := BlockContext{
CanTransfer: func(StateDB, common.Address, *uint256.Int) bool { return true },
Transfer: func(StateDB, common.Address, common.Address, *uint256.Int) {},
Transfer: func(StateDB, common.Address, common.Address, *uint256.Int, *params.Rules) {},
BlockNumber: big.NewInt(0),
}
config := Config{}
Expand Down
8 changes: 8 additions & 0 deletions core/vm/instructions.go
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,14 @@ func opSelfdestruct6780(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, erro
evm.StateDB.AddBalance(beneficiary, balance, tracing.BalanceIncreaseSelfdestruct)
}
evm.StateDB.SelfDestruct6780(this, preserveBalance)
// EIP-7708: emit a transfer log for the moved balance. No burn log is
// emitted here: EIP-8246 (go-ethereum #35219) removes the SELFDESTRUCT burn entirely.
// From Amsterdam onwards the balance is only moved when the beneficiary
// differs from the destroyed account, so a self-destruct to self moves
// nothing and therefore logs nothing.
if evm.chainRules.IsAmsterdam && balance.Sign() != 0 && this != beneficiary {
evm.StateDB.AddLog(types.EthTransferLog(this, beneficiary, balance))
}
if tracer := evm.Config.Tracer; tracer != nil {
if tracer.OnEnter != nil {
tracer.OnEnter(evm.depth, byte(SELFDESTRUCT), this, beneficiary, []byte{}, 0, balance)
Expand Down
2 changes: 1 addition & 1 deletion core/vm/interpreter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ var loopInterruptTests = []string{
func TestLoopInterrupt(t *testing.T) {
address := common.BytesToAddress([]byte("contract"))
vmctx := BlockContext{
Transfer: func(StateDB, common.Address, common.Address, *uint256.Int) {},
Transfer: func(StateDB, common.Address, common.Address, *uint256.Int, *params.Rules) {},
}

for i, tt := range loopInterruptTests {
Expand Down
87 changes: 87 additions & 0 deletions internal/ethapi/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1353,6 +1353,93 @@ func TestSimulateV1(t *testing.T) {
require.ErrorContains(t, err, "too many blocks")
}

// TestSimulateV1TransferLogs verifies that traceTransfers reports each ether
// transfer exactly once across the Amsterdam boundary: before the fork the
// tracer synthesizes an ERC-20 style log from the ERC-7528 sentinel address,
// after it only the protocol-emitted EIP-7708 log remains.
func TestSimulateV1TransferLogs(t *testing.T) {
t.Parallel()

var (
sender = common.HexToAddress("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1")
recipient = common.HexToAddress("0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")
value = big.NewInt(1000)
)
run := func(t *testing.T, amsterdam bool, wantAddr common.Address) {
t.Helper()

// The genesis config only has to be valid; the simulator takes the fork
// rules from the backend config, so Amsterdam is scheduled there.
genesis := &core.Genesis{
Config: params.MergedTestChainConfig,
Alloc: types.GenesisAlloc{sender: {Balance: big.NewInt(params.Ether)}},
}
db := rawdb.NewMemoryDatabase()
block := genesis.MustCommit(db)
stateDB, err := state.New(block.Root(), state.NewDatabase(db))
require.NoError(t, err)

backend := &simulateBackendMock{
estimateBackendMock: &estimateBackendMock{
backendMock: newBackendMock(),
stateDB: stateDB,
header: block.Header(),
engine: ethash.NewFaker(),
},
gasCap: 30_000_000,
}
if amsterdam {
backend.config.AmsterdamBlock = big.NewInt(0)
} else {
backend.config.AmsterdamBlock = nil
}
api := NewBlockChainAPI(backend, nil)
result, err := api.SimulateV1(context.Background(), simOpts{
TraceTransfers: true,
BlockStateCalls: []simBlock{{Calls: []TransactionArgs{{
From: &sender,
To: &recipient,
Value: (*hexutil.Big)(value),
}}}},
}, nil)
require.NoError(t, err)

type transferLog struct {
Address common.Address `json:"address"`
Topics []common.Hash `json:"topics"`
Data hexutil.Bytes `json:"data"`
}
type callSummary struct {
Logs []transferLog `json:"logs"`
}
type blockSummary struct {
Calls []callSummary `json:"calls"`
}
encoded, err := json.Marshal(result)
require.NoError(t, err)
var summary []blockSummary
require.NoError(t, json.Unmarshal(encoded, &summary))
require.Len(t, summary, 1)
require.Len(t, summary[0].Calls, 1)
require.Len(t, summary[0].Calls[0].Logs, 1, "transfer must be reported exactly once")

log := summary[0].Calls[0].Logs[0]
require.Equal(t, wantAddr, log.Address)
require.Equal(t, []common.Hash{
transferTopic,
common.BytesToHash(sender.Bytes()),
common.BytesToHash(recipient.Bytes()),
}, log.Topics)
require.Equal(t, common.BigToHash(value).Bytes(), []byte(log.Data))
}
t.Run("pre-amsterdam", func(t *testing.T) {
run(t, false, transferAddress)
})
t.Run("post-amsterdam", func(t *testing.T) {
run(t, true, params.SystemAddress)
})
}

// TestSimulateV1Multicall3StateOverride covers the account the Prague install owns. The
// install runs after the caller's state overrides, so an override of its code would either
// trip the consensus panic that guards a diverging contract or be undone by the install.
Expand Down
11 changes: 9 additions & 2 deletions internal/ethapi/logtracer.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,20 @@ var (
// - call value
// - self destructs
//
// The log format for a transfer is:
// - address: 0x0000000000000000000000000000000000000000
// From Amsterdam onwards the protocol emits these itself (EIP-7708), so the
// caller turns the synthesis off and the transfer logs arrive through the OnLog
// hook instead; the list above only describes the pre-fork behaviour.
//
// The log format for a synthesized transfer is:
// - address: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE (ERC-7528)
// - data: Value
// - topics:
// - Transfer(address,address,uint256)
// - Sender address
// - Recipient address
//
// The protocol logs that replace the synthesis from Amsterdam onwards carry
// params.SystemAddress as their emitter instead.
type tracer struct {
// logs keeps logs for all open call frames.
// This lets us clear logs for failed calls.
Expand Down
6 changes: 5 additions & 1 deletion internal/ethapi/simulate.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,13 +179,17 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header,
if err := block.StateOverrides.Apply(sim.state, precompiles); err != nil {
return nil, nil, err
}
// Post-Amsterdam the protocol emits an EIP-7708 transfer log for every
// ether transfer, so synthesizing them would report each transfer twice.
traceTransfers := sim.traceTransfers && !sim.chainConfig.IsAmsterdam(header.Number)

var (
gasUsed uint64
txes = make([]*types.Transaction, len(block.Calls))
callResults = make([]simCallResult, len(block.Calls))
receipts = make([]*types.Receipt, len(block.Calls))
// Block hash will be repaired after execution.
tracer = newTracer(sim.traceTransfers, blockContext.BlockNumber.Uint64(), common.Hash{}, common.Hash{}, 0)
tracer = newTracer(traceTransfers, blockContext.BlockNumber.Uint64(), common.Hash{}, common.Hash{}, 0)
vmConfig = &vm.Config{
NoBaseFee: !sim.validate,
Tracer: tracer.Hooks(),
Expand Down
6 changes: 6 additions & 0 deletions params/protocol_params.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,3 +213,9 @@ var (
HistoryStorageAddress = common.HexToAddress("0x0000F90827F1C53a10cb7A02335B175320002935")
HistoryStorageCode = common.FromHex("3373fffffffffffffffffffffffffffffffffffffffe14604657602036036042575f35600143038111604257611fff81430311604257611fff9006545f5260205ff35b5f5ffd5b5f35611fff60014303065500")
)

// System log events.
var (
// EIP-7708 - System log emitted for ETH transfers
EthTransferLogEvent = common.HexToHash("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef") // keccak256('Transfer(address,address,uint256)')
)
2 changes: 1 addition & 1 deletion tests/vm_test_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ func (t *VMTest) newEVM(statedb *state.StateDB, vmconfig vm.Config) *vm.EVM {
}
return core.CanTransfer(db, address, amount)
}
transfer := func(db vm.StateDB, sender, recipient common.Address, amount *uint256.Int) {}
transfer := func(db vm.StateDB, sender, recipient common.Address, amount *uint256.Int, rules *params.Rules) {}
txContext := vm.TxContext{
Origin: t.json.Exec.Origin,
GasPrice: t.json.Exec.GasPrice,
Expand Down
Loading