diff --git a/core/eip7708_transfer_logs_test.go b/core/eip7708_transfer_logs_test.go new file mode 100644 index 000000000000..102fdc07fd72 --- /dev/null +++ b/core/eip7708_transfer_logs_test.go @@ -0,0 +1,780 @@ +// 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 core + +import ( + "bytes" + "crypto/ecdsa" + "encoding/binary" + "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/state" + "github.com/XinFinOrg/XDPoSChain/core/tracing" + "github.com/XinFinOrg/XDPoSChain/core/types" + "github.com/XinFinOrg/XDPoSChain/crypto" + "github.com/XinFinOrg/XDPoSChain/params" + "github.com/holiman/uint256" +) + +// eip7708TestCode is the Solidity test contract from go-ethereum's EIP-7708 +// test (core/eth_transfer_logs_test.go). test() forwards half of the value it +// received to 0xDeaDBeef, whose test2() emits an event and self-destructs to +// 0x12345678: +// +// pragma solidity >=0.4.22 <0.6.0; +// +// contract TestLogs { +// address public constant target_contract = 0x00000000000000000000000000000000DeaDBeef; +// address payable constant selfdestruct_addr = 0x0000000000000000000000000000000012345678; +// +// event Response(bool success, bytes data); +// event TestEvent(); +// event TestEvent2(); +// +// function test() public payable { +// emit TestEvent(); +// target_contract.call.value(msg.value/2)(abi.encodeWithSignature("test2()")); +// } +// function test2() public payable { +// emit TestEvent2(); +// selfdestruct(selfdestruct_addr); +// } +// } +// +// The bytecode-level cases below mirror go-ethereum's core/eip7708_test.go +// boundary matrix (DELEGATECALL, CREATE2, failed CREATE, special recipients, +// fee exclusion and the fork gate), adapted to this tree's big.Int balances +// and GenerateChain harness. +var eip7708TestCode = common.FromHex("6080604052600436106100345760003560e01c8063574ffc311461003957806366e41cb714610090578063f8a8fd6d1461009a575b600080fd5b34801561004557600080fd5b5061004e6100a4565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b6100986100ac565b005b6100a26100f5565b005b63deadbeef81565b7f38e80b5c85ba49b7280ccc8f22548faa62ae30d5a008a1b168fba5f47f5d1ee560405160405180910390a1631234567873ffffffffffffffffffffffffffffffffffffffff16ff5b7f24ec1d3ff24c2f6ff210738839dbc339cd45a5294d85c79361016243157aae7b60405160405180910390a163deadbeef73ffffffffffffffffffffffffffffffffffffffff166002348161014657fe5b046040516024016040516020818303038152906040527f66e41cb7000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff83818316178352505050506040518082805190602001908083835b602083106101fd57805182526020820191506020810190506020830392506101da565b6001836020036101000a03801982511681845116808217855250505050505090500191505060006040518083038185875af1925050503d806000811461025f576040519150601f19603f3d011682016040523d82523d6000602084013e610264565b606091505b50505056fea265627a7a723158202cce817a434785d8560c200762f972d453ccd30694481be7545f9035a512826364736f6c63430005100032") + +var ( + eip7708Key = "b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291" + eip7708Callee = common.HexToAddress("cafebabe") + eip7708Target = common.HexToAddress("0x00000000000000000000000000000000DeaDBeef") + eip7708Benef = common.HexToAddress("0x0000000000000000000000000000000012345678") +) + +// eip7708TransferLog builds the EIP-7708 transfer log expected for a value +// move from -> to. The system log uses params.SystemAddress as emitter and the +// ERC-20 Transfer ABI with a 32-byte amount. +func eip7708TransferLog(from, to common.Address, amount uint64) *types.Log { + data := make([]byte, 32) + binary.BigEndian.PutUint64(data[24:], amount) + return &types.Log{ + Address: params.SystemAddress, + Topics: []common.Hash{ + params.EthTransferLogEvent, + common.BytesToHash(from.Bytes()), + common.BytesToHash(to.Bytes()), + }, + Data: data, + } +} + +// runEIP7708Transfer executes the CALL/SELFDESTRUCT scenario of the upstream +// EIP-7708 test against the given chain config and returns the receipt logs. +func runEIP7708Transfer(t *testing.T, config *params.ChainConfig, value int64) (*types.Block, *types.Receipt, *types.Transaction, common.Address) { + t.Helper() + + key, sender := eip7708KeyPair(t) + gspec := &Genesis{ + Config: config, + Alloc: types.GenesisAlloc{ + sender: {Balance: big.NewInt(params.Ether)}, + eip7708Callee: {Code: eip7708TestCode}, + eip7708Target: {Code: eip7708TestCode}, + }, + } + db := rawdb.NewMemoryDatabase() + genesis := gspec.MustCommit(db) + + signer := types.LatestSigner(config) + tx, err := types.SignTx(types.NewTransaction( + 0, eip7708Callee, big.NewInt(value), 500_000, eip7708GasPrice(), common.FromHex("f8a8fd6d"), + ), signer, key) + if err != nil { + t.Fatalf("failed to sign the transfer transaction: %v", err) + } + blocks, receipts := GenerateChain(config, genesis, ethash.NewFaker(), db, 1, func(i int, b *BlockGen) { + b.AddTx(tx) + }) + if status := receipts[0][0].Status; status != types.ReceiptStatusSuccessful { + t.Fatalf("transfer transaction failed with status %d", status) + } + return blocks[0], receipts[0][0], tx, sender +} + +// TestEIP7708TransferLogs drives the Amsterdam transfer logs through block +// processing: the transaction value, the CALL value and the SELFDESTRUCT value +// each produce one system log, interleaved with the contract's own events. +func TestEIP7708TransferLogs(t *testing.T) { + config := *params.AllEthashProtocolChanges // Amsterdam active from block 0 + block, receipt, tx, sender := runEIP7708Transfer(t, &config, 1_000_000_000) + + var ( + testEvent = crypto.Keccak256Hash([]byte("TestEvent()")) + testEvent2 = crypto.Keccak256Hash([]byte("TestEvent2()")) + want = []*types.Log{ + eip7708TransferLog(sender, eip7708Callee, 1_000_000_000), + {Address: eip7708Callee, Topics: []common.Hash{testEvent}}, + eip7708TransferLog(eip7708Callee, eip7708Target, 500_000_000), + {Address: eip7708Target, Topics: []common.Hash{testEvent2}}, + eip7708TransferLog(eip7708Target, eip7708Benef, 500_000_000), + } + ) + if len(receipt.Logs) != len(want) { + t.Fatalf("log count = %d, want %d", len(receipt.Logs), len(want)) + } + for i, log := range receipt.Logs { + eip7708CheckRawLog(t, log, want[i]) + if log.BlockNumber != block.NumberU64() { + t.Errorf("log %d block number = %d, want %d", i, log.BlockNumber, block.NumberU64()) + } + if log.TxHash != tx.Hash() || log.TxIndex != 0 || log.Index != uint(i) { + t.Errorf("log %d metadata = (%v, %d, %d), want (%v, 0, %d)", i, log.TxHash, log.TxIndex, log.Index, tx.Hash(), i) + } + } +} + +// TestEIP7708ZeroValue pins that a transaction moving no value produces no +// transfer logs, only the contract's own events. +func TestEIP7708ZeroValue(t *testing.T) { + config := *params.AllEthashProtocolChanges + _, receipt, _, _ := runEIP7708Transfer(t, &config, 0) + + eip7708NoTransferLog(t, receipt.Logs, 2) +} + +// TestEIP7708PreAmsterdam pins the fork gate: the same scenario before +// Amsterdam emits the contract events only. +func TestEIP7708PreAmsterdam(t *testing.T) { + config := *params.AllEthashProtocolChanges + config.AmsterdamBlock = nil + + _, receipt, _, _ := runEIP7708Transfer(t, &config, 1_000_000_000) + + eip7708NoTransferLog(t, receipt.Logs, 2) +} + +// TestEIP7708SelfTransfer pins that a transfer to self moves no value and +// therefore emits no transfer log. +func TestEIP7708SelfTransfer(t *testing.T) { + config := *params.AllEthashProtocolChanges + key, sender := eip7708KeyPair(t) + gspec := &Genesis{Config: &config, Alloc: types.GenesisAlloc{sender: {Balance: big.NewInt(params.Ether)}}} + db := rawdb.NewMemoryDatabase() + genesis := gspec.MustCommit(db) + tx, err := types.SignTx(types.NewTransaction( + 0, sender, big.NewInt(params.Ether/2), params.TxGas, eip7708GasPrice(), nil, + ), types.LatestSigner(&config), key) + if err != nil { + t.Fatalf("failed to sign the self transfer: %v", err) + } + _, receipts := GenerateChain(&config, genesis, ethash.NewFaker(), db, 1, func(i int, b *BlockGen) { + b.AddTx(tx) + }) + if status := receipts[0][0].Status; status != types.ReceiptStatusSuccessful { + t.Fatalf("self transfer failed with status %d", status) + } + if logs := receipts[0][0].Logs; len(logs) != 0 { + t.Fatalf("self transfer emitted %d log(s), want none", len(logs)) + } +} + +// eip7708CallCode returns runtime code that CALLs target with one wei and then +// either reverts (revert=true) or stops. The caller must be funded with one wei. +func eip7708CallCode(target common.Address, revert bool) []byte { + code := []byte{0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x01, 0x73} + code = append(code, target.Bytes()...) + code = append(code, 0x5a, 0xf1, 0x50, 0x60, 0x00, 0x60, 0x00) + if revert { + code = append(code, 0xfd) + } else { + code = append(code, 0x00) + } + return code +} + +// TestEIP7708RevertedTransferLog pins that the transfer log of a reverted call +// frame is discarded with the rest of the frame's state changes. +func TestEIP7708RevertedTransferLog(t *testing.T) { + var ( + config = *params.AllEthashProtocolChanges + contract = common.HexToAddress("0x00000000000000000000000000000000000000aa") + target = common.HexToAddress("0x00000000000000000000000000000000000000bb") + ) + key, sender := eip7708KeyPair(t) + tests := []struct { + name string + revert bool + wantLog int + }{ + {name: "reverted frame", revert: true, wantLog: 0}, + {name: "committed frame", revert: false, wantLog: 1}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gspec := &Genesis{ + Config: &config, + Alloc: types.GenesisAlloc{ + sender: {Balance: big.NewInt(params.Ether)}, + contract: {Balance: big.NewInt(1), Code: eip7708CallCode(target, tt.revert)}, + }, + } + db := rawdb.NewMemoryDatabase() + genesis := gspec.MustCommit(db) + tx, err := types.SignTx(types.NewTransaction( + 0, contract, nil, 100_000, eip7708GasPrice(), nil, + ), types.LatestSigner(&config), key) + if err != nil { + t.Fatalf("failed to sign the call transaction: %v", err) + } + _, receipts := GenerateChain(&config, genesis, ethash.NewFaker(), db, 1, func(i int, b *BlockGen) { + b.AddTx(tx) + }) + logs := receipts[0][0].Logs + if len(logs) != tt.wantLog { + t.Fatalf("log count = %d, want %d", len(logs), tt.wantLog) + } + if tt.wantLog == 1 { + want := eip7708TransferLog(contract, target, 1) + if logs[0].Address != want.Address || !bytes.Equal(logs[0].Data, want.Data) { + t.Fatalf("unexpected transfer log: %+v", logs[0]) + } + } + }) + } +} + +// eip7708CallLogged returns runtime code that emits a LOG0 and then CALLs target +// with one wei, either reverting (revert=true) or stopping. The caller must be +// funded with one wei. The LOG0 pins that a reverted inner frame only drops its +// own logs, not the caller's. +func eip7708CallLogged(target common.Address, revert bool) []byte { + code := []byte{0x60, 0x00, 0x60, 0x00, 0xa0, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x01, 0x73} + code = append(code, target.Bytes()...) + code = append(code, 0x5a, 0xf1, 0x50, 0x60, 0x00, 0x60, 0x00) + if revert { + code = append(code, 0xfd) + } else { + code = append(code, 0x00) + } + return code +} + +// TestEIP7708OuterRevertDiscardsAllLogs pins that a reverted top-level call +// frame takes every log with it, including the transaction's own transfer log. +func TestEIP7708OuterRevertDiscardsAllLogs(t *testing.T) { + var ( + config = *params.AllEthashProtocolChanges + caller = common.HexToAddress("0x00000000000000000000000000000000000000d1") + callee = common.HexToAddress("0x00000000000000000000000000000000000000d2") + ) + receipt, _ := eip7708RunTx(t, &config, types.GenesisAlloc{ + caller: {Balance: big.NewInt(1), Code: eip7708CallCode(callee, true)}, + callee: {Code: []byte{0x00}}, + }, &caller, big.NewInt(1), 200_000, nil, common.Address{}) + if receipt.Status != types.ReceiptStatusFailed { + t.Fatalf("reverting call status = %d, want failed", receipt.Status) + } + if len(receipt.Logs) != 0 { + t.Fatalf("reverted transaction kept %d log(s), want none", len(receipt.Logs)) + } +} + +// TestEIP7708InnerRevertKeepsOuterLog pins that a reverted inner call frame only +// drops its own transfer log: the transaction transfer and the caller's LOG0 +// survive. +func TestEIP7708InnerRevertKeepsOuterLog(t *testing.T) { + var ( + config = *params.AllEthashProtocolChanges + caller = common.HexToAddress("0x00000000000000000000000000000000000000d3") + reverter = common.HexToAddress("0x00000000000000000000000000000000000000d4") + ) + receipt, sender := eip7708RunTx(t, &config, types.GenesisAlloc{ + caller: {Balance: big.NewInt(1), Code: eip7708CallLogged(reverter, false)}, + reverter: {Code: []byte{0x60, 0x00, 0x60, 0x00, 0xfd}}, + }, &caller, big.NewInt(1), 200_000, nil, common.Address{}) + if receipt.Status != types.ReceiptStatusSuccessful { + t.Fatalf("call transaction failed with status %d", receipt.Status) + } + if len(receipt.Logs) != 2 { + t.Fatalf("log count = %d, want the transaction transfer and the caller LOG0", len(receipt.Logs)) + } + eip7708CheckLog(t, receipt.Logs[0], eip7708TransferLog(sender, caller, 1)) + if receipt.Logs[1].Address != caller || len(receipt.Logs[1].Topics) != 0 || len(receipt.Logs[1].Data) != 0 { + t.Fatalf("second log = %+v, want the caller LOG0", receipt.Logs[1]) + } +} + +// TestEIP7708DelegateCallAttribution pins that a transfer made by code running +// under DELEGATECALL is attributed to the executing context, not to the +// implementation the code was taken from. +func TestEIP7708DelegateCallAttribution(t *testing.T) { + var ( + config = *params.AllEthashProtocolChanges + caller = common.HexToAddress("0x00000000000000000000000000000000000000d5") + impl = common.HexToAddress("0x00000000000000000000000000000000000000d6") + callee = common.HexToAddress("0x00000000000000000000000000000000000000d7") + ) + receipt, sender := eip7708RunTx(t, &config, types.GenesisAlloc{ + caller: {Balance: big.NewInt(10), Code: eip7708DelegateCallCode(impl)}, + impl: {Code: eip7708CallLogged(callee, false)}, + callee: {Code: []byte{0x00}}, + }, &caller, big.NewInt(10), 300_000, nil, common.Address{}) + if receipt.Status != types.ReceiptStatusSuccessful { + t.Fatalf("delegate call transaction failed with status %d", receipt.Status) + } + if len(receipt.Logs) != 3 { + t.Fatalf("log count = %d, want the transaction transfer, the delegated LOG0 and the inner transfer", len(receipt.Logs)) + } + eip7708CheckLog(t, receipt.Logs[0], eip7708TransferLog(sender, caller, 10)) + if receipt.Logs[1].Address != caller || len(receipt.Logs[1].Topics) != 0 || len(receipt.Logs[1].Data) != 0 { + t.Fatalf("delegated LOG0 = %+v, want the executing context %v as emitter", receipt.Logs[1], caller) + } + eip7708CheckLog(t, receipt.Logs[2], eip7708TransferLog(caller, callee, 1)) +} + +// eip7708GasPrice returns the gas price used by the cases below. +func eip7708GasPrice() *big.Int { + return new(big.Int).Mul(big.NewInt(20), big.NewInt(params.GWei)) +} + +// eip7708KeyPair loads the sender key shared by the cases below. +func eip7708KeyPair(t *testing.T) (*ecdsa.PrivateKey, common.Address) { + t.Helper() + + key, err := crypto.HexToECDSA(eip7708Key) + if err != nil { + t.Fatalf("failed to load the sender key: %v", err) + } + return key, crypto.PubkeyToAddress(key.PublicKey) +} + +// eip7708RunTx runs a single transaction in its own block against a genesis +// that funds the sender with one ether, and returns the receipt together with +// the sender. A nil to creates a contract from data; a zero coinbase keeps the +// genesis default. +func eip7708RunTx(t *testing.T, config *params.ChainConfig, alloc types.GenesisAlloc, to *common.Address, value *big.Int, gas uint64, data []byte, coinbase common.Address) (*types.Receipt, common.Address) { + t.Helper() + + key, sender := eip7708KeyPair(t) + funded := types.GenesisAlloc{sender: {Balance: big.NewInt(params.Ether)}} + for addr, account := range alloc { + funded[addr] = account + } + db := rawdb.NewMemoryDatabase() + genesis := (&Genesis{Config: config, Alloc: funded}).MustCommit(db) + + var ( + tx *types.Transaction + err error + ) + if to == nil { + tx, err = types.SignTx(types.NewContractCreation(0, value, gas, eip7708GasPrice(), data), types.LatestSigner(config), key) + } else { + tx, err = types.SignTx(types.NewTransaction(0, *to, value, gas, eip7708GasPrice(), data), types.LatestSigner(config), key) + } + if err != nil { + t.Fatalf("failed to sign the transaction: %v", err) + } + _, receipts := GenerateChain(config, genesis, ethash.NewFaker(), db, 1, func(i int, b *BlockGen) { + if coinbase != (common.Address{}) { + b.SetCoinbase(coinbase) + } + b.AddTx(tx) + }) + return receipts[0][0], sender +} + +// eip7708CheckRawLog compares an emitted log against the expected one: emitter, +// topics and data. +func eip7708CheckRawLog(t *testing.T, got, want *types.Log) { + t.Helper() + + if got.Address != want.Address { + t.Errorf("log address = %v, want %v", got.Address, want.Address) + } + if len(got.Topics) != len(want.Topics) { + t.Fatalf("topic count = %d, want %d", len(got.Topics), len(want.Topics)) + } + for i := range want.Topics { + if got.Topics[i] != want.Topics[i] { + t.Errorf("topic %d = %v, want %v", i, got.Topics[i], want.Topics[i]) + } + } + if !bytes.Equal(got.Data, want.Data) { + t.Errorf("data = %x, want %x", got.Data, want.Data) + } +} + +// eip7708CheckLog compares an emitted log against the expected transfer log and +// pins the 32-byte amount encoding on its own. +func eip7708CheckLog(t *testing.T, got, want *types.Log) { + t.Helper() + + eip7708CheckRawLog(t, got, want) + if len(got.Data) != 32 { + t.Errorf("data length = %d, want 32", len(got.Data)) + } +} + +// eip7708NoTransferLog pins that a receipt carries exactly want logs and that +// none of them is an EIP-7708 system transfer log. +func eip7708NoTransferLog(t *testing.T, logs []*types.Log, want int) { + t.Helper() + + if len(logs) != want { + t.Fatalf("log count = %d, want %d contract events only", len(logs), want) + } + for i, log := range logs { + if log.Address == params.SystemAddress { + t.Errorf("log %d is an ETH transfer log, want contract events only", i) + } + } +} + +// eip7708DelegateCallCode returns code that DELEGATECALLs target and stops. +// DELEGATECALL runs the callee's code in the caller's context and moves no +// value, so it must not produce a transfer log. +func eip7708DelegateCallCode(target common.Address) []byte { + code := []byte{0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x73} + code = append(code, target.Bytes()...) + return append(code, 0x5a, 0xf4, 0x50, 0x00) +} + +// eip7708Create2Code returns code that writes the one-byte init code 0x00 to +// memory and CREATE2s a contract endowed with value. +func eip7708Create2Code(value byte) []byte { + return []byte{ + 0x60, 0x00, 0x60, 0x00, 0x53, // MSTORE8(0, 0x00): init code is STOP + 0x60, 0x00, // salt + 0x60, 0x01, // size + 0x60, 0x00, // offset + 0x60, value, // endowment + 0xf5, // CREATE2 + 0x50, // POP + 0x00, // STOP + } +} + +// TestEIP7708DelegateCallDoesNotLog pins that a DELEGATECALL frame moves no +// value and therefore adds no transfer log. +func TestEIP7708DelegateCallDoesNotLog(t *testing.T) { + var ( + config = *params.AllEthashProtocolChanges + caller = common.HexToAddress("0x00000000000000000000000000000000000000c1") + target = common.HexToAddress("0x00000000000000000000000000000000000000c2") + ) + receipt, sender := eip7708RunTx(t, &config, types.GenesisAlloc{ + caller: {Code: eip7708DelegateCallCode(target)}, + target: {Code: []byte{0x00}}, + }, &caller, big.NewInt(1), 200_000, nil, common.Address{}) + if receipt.Status != types.ReceiptStatusSuccessful { + t.Fatalf("delegate call transaction failed with status %d", receipt.Status) + } + if len(receipt.Logs) != 1 { + t.Fatalf("log count = %d, want the transaction transfer only", len(receipt.Logs)) + } + eip7708CheckLog(t, receipt.Logs[0], eip7708TransferLog(sender, caller, 1)) +} + +// TestEIP7708Create2Endowment pins that a CREATE2 endowment is logged with the +// created address as recipient. +func TestEIP7708Create2Endowment(t *testing.T) { + var ( + config = *params.AllEthashProtocolChanges + factory = common.HexToAddress("0x00000000000000000000000000000000000000c3") + ) + receipt, _ := eip7708RunTx(t, &config, types.GenesisAlloc{ + factory: {Balance: big.NewInt(5), Code: eip7708Create2Code(5)}, + }, &factory, big.NewInt(0), 300_000, nil, common.Address{}) + if receipt.Status != types.ReceiptStatusSuccessful { + t.Fatalf("create2 transaction failed with status %d", receipt.Status) + } + if len(receipt.Logs) != 1 { + t.Fatalf("log count = %d, want the create2 endowment only", len(receipt.Logs)) + } + created := crypto.CreateAddress2(factory, [32]byte{}, crypto.Keccak256([]byte{0x00})) + eip7708CheckLog(t, receipt.Logs[0], eip7708TransferLog(factory, created, 5)) +} + +// TestEIP7708FailedCreateRollsBackLog pins that the endowment log of a failed +// contract creation is rolled back with the rest of the frame. +func TestEIP7708FailedCreateRollsBackLog(t *testing.T) { + config := *params.AllEthashProtocolChanges + // PUSH1 0x00; PUSH1 0x00; REVERT + receipt, _ := eip7708RunTx(t, &config, nil, nil, big.NewInt(5), 500_000, []byte{0x60, 0x00, 0x60, 0x00, 0xfd}, common.Address{}) + if receipt.Status != types.ReceiptStatusFailed { + t.Fatalf("reverting create status = %d, want failed", receipt.Status) + } + if len(receipt.Logs) != 0 { + t.Fatalf("failed create kept %d log(s), want none", len(receipt.Logs)) + } +} + +// TestEIP7708SpecialRecipients pins that value sent to a precompile, to the +// system address or to the coinbase address is logged like any other transfer. +func TestEIP7708SpecialRecipients(t *testing.T) { + coinbase := common.HexToAddress("0x00000000000000000000000000000000000000c9") + for _, tc := range []struct { + name string + to common.Address + coinbase common.Address + }{ + {name: "precompile", to: common.BytesToAddress([]byte{4})}, + {name: "system", to: params.SystemAddress}, + {name: "coinbase", to: coinbase, coinbase: coinbase}, + } { + t.Run(tc.name, func(t *testing.T) { + config := *params.AllEthashProtocolChanges + to := tc.to + receipt, sender := eip7708RunTx(t, &config, nil, &to, big.NewInt(1), 100_000, nil, tc.coinbase) + if receipt.Status != types.ReceiptStatusSuccessful { + t.Fatalf("transfer failed with status %d", receipt.Status) + } + if len(receipt.Logs) != 1 { + t.Fatalf("log count = %d, want 1", len(receipt.Logs)) + } + eip7708CheckLog(t, receipt.Logs[0], eip7708TransferLog(sender, to, 1)) + }) + } +} + +// TestEIP7708FeesNotLogged pins that the priority-fee credit to the coinbase +// stays outside EIP-7708: the transaction value is the receipt's only log. +func TestEIP7708FeesNotLogged(t *testing.T) { + var ( + config = *params.AllEthashProtocolChanges + recipient = common.HexToAddress("0x00000000000000000000000000000000000000c4") + coinbase = common.HexToAddress("0x00000000000000000000000000000000000000c5") + ) + receipt, sender := eip7708RunTx(t, &config, nil, &recipient, big.NewInt(1), params.TxGas, nil, coinbase) + if receipt.Status != types.ReceiptStatusSuccessful { + t.Fatalf("transfer failed with status %d", receipt.Status) + } + if len(receipt.Logs) != 1 { + t.Fatalf("log count = %d, want the transaction transfer only", len(receipt.Logs)) + } + eip7708CheckLog(t, receipt.Logs[0], eip7708TransferLog(sender, recipient, 1)) +} + +// TestEIP7708TransferForkGate drives core.Transfer directly to pin both the +// fork gate and the 32-byte log data. +func TestEIP7708TransferForkGate(t *testing.T) { + var ( + from = common.HexToAddress("0x00000000000000000000000000000000000000c6") + to = common.HexToAddress("0x00000000000000000000000000000000000000c7") + txHash = common.HexToHash("0x01") + ) + sdb, err := state.New(types.EmptyRootHash, state.NewDatabase(rawdb.NewMemoryDatabase())) + if err != nil { + t.Fatalf("failed to create the state: %v", err) + } + sdb.SetTxContext(txHash, 0) + sdb.SetBalance(from, big.NewInt(4), tracing.BalanceChangeUnspecified) + + Transfer(sdb, from, to, uint256.NewInt(1), ¶ms.Rules{}) + if logs := sdb.GetLogs(txHash, 1, common.Hash{}); len(logs) != 0 { + t.Fatalf("pre-Amsterdam transfer logged %d entry, want none", len(logs)) + } + Transfer(sdb, from, to, uint256.NewInt(1), ¶ms.Rules{IsAmsterdam: true}) + logs := sdb.GetLogs(txHash, 1, common.Hash{}) + if len(logs) != 1 { + t.Fatalf("Amsterdam transfer logged %d entry, want 1", len(logs)) + } + eip7708CheckLog(t, logs[0], eip7708TransferLog(from, to, 1)) +} + +// eip7708RunRawTx runs a pre-built transaction in its own block against a +// genesis that funds the sender with one ether, and returns the receipt. A +// zero coinbase keeps the genesis default. +func eip7708RunRawTx(t *testing.T, config *params.ChainConfig, alloc types.GenesisAlloc, tx *types.Transaction, coinbase common.Address) *types.Receipt { + t.Helper() + + _, sender := eip7708KeyPair(t) + funded := types.GenesisAlloc{sender: {Balance: big.NewInt(params.Ether)}} + for addr, account := range alloc { + funded[addr] = account + } + db := rawdb.NewMemoryDatabase() + genesis := (&Genesis{Config: config, Alloc: funded}).MustCommit(db) + + _, receipts := GenerateChain(config, genesis, ethash.NewFaker(), db, 1, func(i int, b *BlockGen) { + if coinbase != (common.Address{}) { + b.SetCoinbase(coinbase) + } + b.AddTx(tx) + }) + return receipts[0][0] +} + +// TestEIP7708SelfdestructToSelf pins that a SELFDESTRUCT to the executing +// account moves no value and therefore emits no transfer log; only the +// transaction value is logged. +func TestEIP7708SelfdestructToSelf(t *testing.T) { + var ( + config = *params.AllEthashProtocolChanges + contract = common.HexToAddress("0x00000000000000000000000000000000000000ca") + ) + code := append([]byte{0x73}, contract.Bytes()...) + code = append(code, 0xff) // PUSH20 self; SELFDESTRUCT + receipt, sender := eip7708RunTx(t, &config, types.GenesisAlloc{ + contract: {Code: code}, + }, &contract, big.NewInt(1), 200_000, nil, common.Address{}) + if receipt.Status != types.ReceiptStatusSuccessful { + t.Fatalf("selfdestruct transaction failed with status %d", receipt.Status) + } + if len(receipt.Logs) != 1 { + t.Fatalf("log count = %d, want the transaction transfer only", len(receipt.Logs)) + } + eip7708CheckLog(t, receipt.Logs[0], eip7708TransferLog(sender, contract, 1)) +} + +// TestEIP7708TransactionTypes pins that the transaction value is logged for +// every transaction type, including a delegated EOA that receives the value +// (the log names the delegated account, never its implementation). +func TestEIP7708TransactionTypes(t *testing.T) { + var ( + config = *params.AllEthashProtocolChanges + key, sender = eip7708KeyPair(t) + recipient = common.HexToAddress("0x00000000000000000000000000000000000000cb") + signer = types.LatestSigner(&config) + gasPrice = eip7708GasPrice() + ) + authKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020") + auth, err := types.SignSetCode(authKey, types.SetCodeAuthorization{ + ChainID: *uint256.MustFromBig(config.ChainID), + Address: common.HexToAddress("0x00000000000000000000000000000000000000cc"), + }) + if err != nil { + t.Fatalf("failed to sign the set-code authorization: %v", err) + } + build := func(inner types.TxData) *types.Transaction { + t.Helper() + + tx, err := types.SignTx(types.NewTx(inner), signer, key) + if err != nil { + t.Fatalf("failed to sign the transaction: %v", err) + } + return tx + } + cases := []struct { + name string + tx *types.Transaction + }{ + {"legacy", build(&types.LegacyTx{ + Nonce: 0, To: &recipient, Value: big.NewInt(1), Gas: 100_000, GasPrice: gasPrice, + })}, + {"access-list", build(&types.AccessListTx{ + ChainID: config.ChainID, Nonce: 0, GasPrice: gasPrice, Gas: 100_000, + To: &recipient, Value: big.NewInt(1), + })}, + {"dynamic", build(&types.DynamicFeeTx{ + ChainID: config.ChainID, Nonce: 0, GasTipCap: gasPrice, GasFeeCap: gasPrice, + Gas: 100_000, To: &recipient, Value: big.NewInt(1), + })}, + {"set-code", build(&types.SetCodeTx{ + ChainID: uint256.MustFromBig(config.ChainID), Nonce: 0, + GasTipCap: uint256.MustFromBig(gasPrice), GasFeeCap: uint256.MustFromBig(gasPrice), + Gas: 300_000, To: recipient, Value: uint256.NewInt(1), + AuthList: []types.SetCodeAuthorization{auth}, + })}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + receipt := eip7708RunRawTx(t, &config, nil, tc.tx, common.Address{}) + if receipt.Status != types.ReceiptStatusSuccessful { + t.Fatalf("transfer failed with status %d", receipt.Status) + } + if len(receipt.Logs) != 1 { + t.Fatalf("log count = %d, want the transaction transfer only", len(receipt.Logs)) + } + eip7708CheckLog(t, receipt.Logs[0], eip7708TransferLog(sender, recipient, 1)) + }) + } + + t.Run("delegated", func(t *testing.T) { + var ( + delegated = common.HexToAddress("0x00000000000000000000000000000000000000cd") + impl = common.HexToAddress("0x00000000000000000000000000000000000000ce") + ) + tx := build(&types.LegacyTx{ + Nonce: 0, To: &delegated, Value: big.NewInt(1), Gas: 100_000, GasPrice: gasPrice, + }) + receipt := eip7708RunRawTx(t, &config, types.GenesisAlloc{ + delegated: {Code: types.AddressToDelegation(impl)}, + impl: {Code: []byte{0x00}}, + }, tx, common.Address{}) + if receipt.Status != types.ReceiptStatusSuccessful { + t.Fatalf("delegated transfer failed with status %d", receipt.Status) + } + if len(receipt.Logs) != 1 { + t.Fatalf("log count = %d, want the transaction transfer only", len(receipt.Logs)) + } + eip7708CheckLog(t, receipt.Logs[0], eip7708TransferLog(sender, delegated, 1)) + }) +} + +// eip7708CallCodeOpcode returns runtime code that CALLCODEs target with the +// given value. CALLCODE runs the callee's code in the caller's context and +// moves no value, so it must not produce a transfer log. +func eip7708CallCodeOpcode(target common.Address, value byte) []byte { + code := []byte{0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, value, 0x73} + code = append(code, target.Bytes()...) + return append(code, 0x5a, 0xf2, 0x50, 0x00) +} + +// TestEIP7708CallCodeDoesNotLog pins that a CALLCODE frame moves no value and +// therefore adds no transfer log. +func TestEIP7708CallCodeDoesNotLog(t *testing.T) { + var ( + config = *params.AllEthashProtocolChanges + caller = common.HexToAddress("0x00000000000000000000000000000000000000cf") + target = common.HexToAddress("0x00000000000000000000000000000000000000d0") + ) + receipt, sender := eip7708RunTx(t, &config, types.GenesisAlloc{ + caller: {Balance: big.NewInt(1), Code: eip7708CallCodeOpcode(target, 1)}, + target: {Code: []byte{0x00}}, + }, &caller, big.NewInt(1), 200_000, nil, common.Address{}) + if receipt.Status != types.ReceiptStatusSuccessful { + t.Fatalf("callcode transaction failed with status %d", receipt.Status) + } + if len(receipt.Logs) != 1 { + t.Fatalf("log count = %d, want the transaction transfer only", len(receipt.Logs)) + } + eip7708CheckLog(t, receipt.Logs[0], eip7708TransferLog(sender, caller, 1)) +} + +// TestEIP7708CreateEndowment pins that the endowment of a successful +// top-level CREATE is logged with the created address as recipient. +func TestEIP7708CreateEndowment(t *testing.T) { + config := *params.AllEthashProtocolChanges + // Init code 0x00 is STOP: the creation succeeds without deploying code. + receipt, sender := eip7708RunTx(t, &config, nil, nil, big.NewInt(5), 500_000, []byte{0x00}, common.Address{}) + if receipt.Status != types.ReceiptStatusSuccessful { + t.Fatalf("create transaction failed with status %d", receipt.Status) + } + if len(receipt.Logs) != 1 { + t.Fatalf("log count = %d, want the create endowment only", len(receipt.Logs)) + } + eip7708CheckLog(t, receipt.Logs[0], eip7708TransferLog(sender, crypto.CreateAddress(sender, 0), 5)) +} diff --git a/core/evm.go b/core/evm.go index 4be2906f2728..dd30390d6f07 100644 --- a/core/evm.go +++ b/core/evm.go @@ -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" ) @@ -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)) + } } diff --git a/core/state/statedb_log_test.go b/core/state/statedb_log_test.go new file mode 100644 index 000000000000..f178b20421b7 --- /dev/null +++ b/core/state/statedb_log_test.go @@ -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 . + +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) + } +} diff --git a/core/types/log.go b/core/types/log.go index 5a2f388c4700..71d8a3c8edbb 100644 --- a/core/types/log.go +++ b/core/types/log.go @@ -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" ) @@ -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 diff --git a/core/vm/contracts_kzg_stub_test.go b/core/vm/contracts_kzg_stub_test.go index 33e5d788d8e6..f3d3c7eca326 100644 --- a/core/vm/contracts_kzg_stub_test.go +++ b/core/vm/contracts_kzg_stub_test.go @@ -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{}, diff --git a/core/vm/eip8246_test.go b/core/vm/eip8246_test.go index 9835e1b59a74..09ecc249a7e6 100644 --- a/core/vm/eip8246_test.go +++ b/core/vm/eip8246_test.go @@ -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 } diff --git a/core/vm/evm.go b/core/vm/evm.go index 3183b0b8750e..7c08ccd058ac 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -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 @@ -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) @@ -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. diff --git a/core/vm/gas_table_test.go b/core/vm/gas_table_test.go index da709fc6bd66..7bd329f42a71 100644 --- a/core/vm/gas_table_test.go +++ b/core/vm/gas_table_test.go @@ -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}}) @@ -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}}) @@ -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{} diff --git a/core/vm/instructions.go b/core/vm/instructions.go index 4d6ebbac4ae4..46b600bc6a77 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -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) diff --git a/core/vm/interpreter_test.go b/core/vm/interpreter_test.go index c6fc242f1b10..fa9db9dd43a2 100644 --- a/core/vm/interpreter_test.go +++ b/core/vm/interpreter_test.go @@ -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 { diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go index 19a6b72c3f0a..325d123e23d0 100644 --- a/internal/ethapi/api_test.go +++ b/internal/ethapi/api_test.go @@ -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. diff --git a/internal/ethapi/logtracer.go b/internal/ethapi/logtracer.go index 867a178def6c..9b387a377962 100644 --- a/internal/ethapi/logtracer.go +++ b/internal/ethapi/logtracer.go @@ -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. diff --git a/internal/ethapi/simulate.go b/internal/ethapi/simulate.go index 091084a48c78..8544dccf5b0e 100644 --- a/internal/ethapi/simulate.go +++ b/internal/ethapi/simulate.go @@ -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(), diff --git a/params/protocol_params.go b/params/protocol_params.go index c958c5a3b161..4490eeb80939 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -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)') +) diff --git a/tests/vm_test_util.go b/tests/vm_test_util.go index 1986fd134d64..e0b5be88d09e 100644 --- a/tests/vm_test_util.go +++ b/tests/vm_test_util.go @@ -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,