diff --git a/cmd/evm/internal/t8ntool/execution.go b/cmd/evm/internal/t8ntool/execution.go index 5178c0d4df5f..40322f73cab1 100644 --- a/cmd/evm/internal/t8ntool/execution.go +++ b/cmd/evm/internal/t8ntool/execution.go @@ -363,7 +363,8 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, for _, receipt := range receipts { allLogs = append(allLogs, receipt.Logs...) } - requests, bal, err := core.PostExecution(context.Background(), chainConfig, vmContext.BlockNumber, vmContext.Time, allLogs, evm, uint32(len(receipts)+1)) + // EIP-8304: log index tables are not built by t8n, wired in a later PR + requests, bal, err := core.PostExecution(context.Background(), chainConfig, vmContext.BlockNumber, vmContext.Time, allLogs, nil, evm, uint32(len(receipts)+1)) if err != nil { return nil, nil, nil, NewError(ErrorEVM, fmt.Errorf("failed to process post-execution: %v", err)) } diff --git a/core/chain_makers.go b/core/chain_makers.go index 99c36b536607..3709b53eb125 100644 --- a/core/chain_makers.go +++ b/core/chain_makers.go @@ -326,7 +326,8 @@ func (b *BlockGen) collectRequests(readonly bool) (requests [][]byte, bal *bal.C blockContext := NewEVMBlockContext(b.header, b.cm, &b.header.Coinbase) evm := vm.NewEVM(blockContext, statedb, b.cm.config, vm.Config{}) - requests, bal, err := PostExecution(context.Background(), b.cm.config, b.header.Number, b.header.Time, blockLogs, evm, uint32(len(b.txs)+1)) + // EIP-8304: log index tables are not built for test chains, wired in a later PR + requests, bal, err := PostExecution(context.Background(), b.cm.config, b.header.Number, b.header.Time, blockLogs, nil, evm, uint32(len(b.txs)+1)) if err != nil { panic(fmt.Sprintf("failed to run post-execution: %v", err)) } diff --git a/core/genesis.go b/core/genesis.go index c0c526ccf4d8..f170a81dbf2e 100644 --- a/core/genesis.go +++ b/core/genesis.go @@ -747,6 +747,9 @@ func DeveloperGenesisBlock(gasLimit uint64, faucet *common.Address) *Genesis { } // Pre-deploy the system contracts the enabled forks call into maps.Copy(genesis.Alloc, SystemContractAllocs()) + // EIP-8304: pre-deploy the index contract. Kept out of SystemContractAllocs + // so test-chain genesis matches upstream until the fork's own genesis. + genesis.Alloc[params.IndexContractAddress] = types.Account{Nonce: 1, Code: params.IndexContractCode, Balance: common.Big0} if faucet != nil { genesis.Alloc[*faucet] = types.Account{Balance: new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 256), big.NewInt(9))} } diff --git a/core/log_index_builder.go b/core/log_index_builder.go new file mode 100644 index 000000000000..6b559a8e26e4 --- /dev/null +++ b/core/log_index_builder.go @@ -0,0 +1,39 @@ +// 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 ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +// TableWrite describes a table root that should be written to the index contract. +type TableWrite struct { + FirstBlock uint64 + TableSize uint64 + Root common.Hash +} + +// BuildLogIndexForBlock builds the EIP-8304 level-0 log index table for the given +// block. parentHash is the hash of the parent block, whose block entry is added +// with the spec's one-block delay (none for the genesis block). Interim +// implementation: variable-length entries and a flat keccak root. +func BuildLogIndexForBlock(blockNumber uint64, parentHash common.Hash, receipts types.Receipts) []TableWrite { + b0 := types.NewIndexBuilder() + b0.AddBlockEntries(parentHash, blockNumber, receipts) + return []TableWrite{{FirstBlock: blockNumber, TableSize: 1, Root: b0.Build()}} +} diff --git a/core/log_index_builder_test.go b/core/log_index_builder_test.go new file mode 100644 index 000000000000..43788c4a7c7d --- /dev/null +++ b/core/log_index_builder_test.go @@ -0,0 +1,50 @@ +// 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 ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +// TestBuildLogIndexForBlock pins the level-0 wrapper: one table per block, +// carrying the builder root of the block's entries including the parent's +// block entry (one-block delay). +func TestBuildLogIndexForBlock(t *testing.T) { + parentHash := common.HexToHash("0x978ce0036b6d1c62d716045505587d15cc85a1def92f9f450937b6467295e517") + receipts := types.Receipts{ + {TxHash: common.HexToHash("0xca2d12d1b8132de09d0d668cc87349dc70134bee3010e03ddb2d83f7160bd6e3"), Logs: []*types.Log{ + {Address: common.HexToAddress("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"), Topics: []common.Hash{ + common.HexToHash("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"), + }}, + }}, + } + wantRoot := types.NewIndexBuilder() + wantRoot.AddBlockEntries(parentHash, 42, receipts) + tables := BuildLogIndexForBlock(42, parentHash, receipts) + if len(tables) != 1 { + t.Fatalf("table count = %d, want 1", len(tables)) + } + if tables[0].FirstBlock != 42 || tables[0].TableSize != 1 { + t.Errorf("table = {first %d, size %d}, want {42, 1}", tables[0].FirstBlock, tables[0].TableSize) + } + if tables[0].Root != wantRoot.Build() { + t.Errorf("root %x, want %x", tables[0].Root, wantRoot.Build()) + } +} diff --git a/core/logindex_contract_test.go b/core/logindex_contract_test.go new file mode 100644 index 000000000000..0ebc0ae299dd --- /dev/null +++ b/core/logindex_contract_test.go @@ -0,0 +1,225 @@ +// 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" + "encoding/binary" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus" + "github.com/ethereum/go-ethereum/consensus/beacon" + "github.com/ethereum/go-ethereum/consensus/ethash" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/triedb" +) + +// eip8304ChainContext is a minimal ChainContext for exercising the EIP-8304 +// index contract in isolation. +type eip8304ChainContext struct{ cfg *params.ChainConfig } + +func (eip8304ChainContext) Engine() consensus.Engine { return beacon.New(ethash.NewFaker()) } +func (c eip8304ChainContext) Config() *params.ChainConfig { return c.cfg } +func (eip8304ChainContext) GetHeader(common.Hash, uint64) *types.Header { return nil } +func (eip8304ChainContext) CurrentHeader() *types.Header { return nil } +func (eip8304ChainContext) GetHeaderByHash(common.Hash) *types.Header { return nil } +func (eip8304ChainContext) GetHeaderByNumber(uint64) *types.Header { return nil } + +// newEIP8304ContractState creates a fresh state for exercising the index +// contract in isolation. +func newEIP8304ContractState(t *testing.T) *state.StateDB { + t.Helper() + diskdb := rawdb.NewMemoryDatabase() + tdb := triedb.NewDatabase(diskdb, nil) + statedb, err := state.New(common.Hash{}, state.NewMPTDatabase(tdb, state.NewCodeDB(diskdb))) + if err != nil { + t.Fatal(err) + } + return statedb +} + +// newEIP8304ContractEVM creates an EVM over the given state with the given +// block number, which drives the contract's NUMBER-based freshness checks. +func newEIP8304ContractEVM(t *testing.T, statedb *state.StateDB, blockNumber uint64) *vm.EVM { + t.Helper() + header := &types.Header{ + Number: new(big.Int).SetUint64(blockNumber), + Time: blockNumber, + GasLimit: 30_000_000, + Coinbase: common.Address{}, + BaseFee: big.NewInt(0), + Difficulty: big.NewInt(0), + } + ctx := NewEVMBlockContext(header, eip8304ChainContext{params.MergedTestChainConfig}, &header.Coinbase) + return vm.NewEVM(ctx, statedb, params.MergedTestChainConfig, vm.Config{}) +} + +// deployEIP8304IndexContract deploys the EIP-8304 index contract from its init +// code and returns the resulting address. +func deployEIP8304IndexContract(t *testing.T, evm *vm.EVM) common.Address { + t.Helper() + _, addr, _, err := evm.Create(crypto.PubkeyToAddress(eip8304TestKey.PublicKey), params.IndexContractInitCode, vm.NewGasBudget(5_000_000, 0), common.U2560) + if err != nil { + t.Fatalf("deploy failed: %v", err) + } + return addr +} + +// setCalldata builds the 96-byte set payload: first_block, table_size, +// table_root (all big-endian 32-byte words). +func setCalldata(firstBlock, tableSize uint64, root common.Hash) []byte { + calldata := make([]byte, 96) + binary.BigEndian.PutUint64(calldata[24:32], firstBlock) + binary.BigEndian.PutUint64(calldata[56:64], tableSize) + copy(calldata[64:96], root[:]) + return calldata +} + +// getCalldata builds the 64-byte get payload: first_block, table_size. +func getCalldata(firstBlock, tableSize uint64) []byte { + calldata := make([]byte, 64) + binary.BigEndian.PutUint64(calldata[24:32], firstBlock) + binary.BigEndian.PutUint64(calldata[56:64], tableSize) + return calldata +} + +// indexSlot is the storage slot for a table root per the EIP-8304 spec: +// table_size * TABLES_PER_LEVEL + (first_block / table_size) % TABLES_PER_LEVEL. +func indexSlot(firstBlock, tableSize uint64) common.Hash { + return common.BigToHash(new(big.Int).SetUint64(tableSize*0x400 + (firstBlock/tableSize)%0x400)) +} + +var eip8304TestKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + +// TestEIP8304IndexContract pins the behavior of the EIP-8304 index contract +// against the spec: +// +// - the deployed code must be exactly params.IndexContractCode (guards the +// init-code/runtime consistency, which has regressed before) +// - set: only the system address may write; the root lands at +// table_size*1024 + (first_block/table_size)%1024 +// - get: 64-byte calldata returns the stored root; reverts on wrong size or +// a first_block that is not a multiple of table_size +func TestEIP8304IndexContract(t *testing.T) { + t.Run("deployment", func(t *testing.T) { + statedb := newEIP8304ContractState(t) + evm := newEIP8304ContractEVM(t, statedb, 1) + addr := deployEIP8304IndexContract(t, evm) + if got := statedb.GetCode(addr); !bytes.Equal(got, params.IndexContractCode) { + t.Fatalf("deployed code mismatch: got %d bytes, want %d", len(got), len(params.IndexContractCode)) + } + }) + + t.Run("set", func(t *testing.T) { + root := common.HexToHash("0x00000000000000000000000000000000000000000000000000000000deadbeef") + cases := []struct{ fb, ts uint64 }{ + {2, 1}, {5, 2}, {8, 4}, {1030, 4}, {1024, 256}, + } + for _, c := range cases { + statedb := newEIP8304ContractState(t) + evm := newEIP8304ContractEVM(t, statedb, 1) + addr := deployEIP8304IndexContract(t, evm) + if _, _, err := evm.Call(params.SystemAddress, addr, setCalldata(c.fb, c.ts, root), vm.NewGasBudget(5_000_000, 0), common.U2560); err != nil { + t.Fatalf("set(%d, %d) failed: %v", c.fb, c.ts, err) + } + if got := statedb.GetState(addr, indexSlot(c.fb, c.ts)); got != root { + t.Fatalf("set(%d, %d): slot %s = %x, want %x", c.fb, c.ts, indexSlot(c.fb, c.ts), got, root) + } + } + }) + + t.Run("set-caller-guard", func(t *testing.T) { + statedb := newEIP8304ContractState(t) + evm := newEIP8304ContractEVM(t, statedb, 1) + addr := deployEIP8304IndexContract(t, evm) + root := common.HexToHash("0xdeadbeef") + sender := crypto.PubkeyToAddress(eip8304TestKey.PublicKey) + if _, _, err := evm.Call(sender, addr, setCalldata(2, 1, root), vm.NewGasBudget(5_000_000, 0), common.U2560); err == nil { + t.Fatal("non-system caller with set calldata: expected revert") + } + if got := statedb.GetState(addr, indexSlot(2, 1)); got != (common.Hash{}) { + t.Fatalf("non-system caller wrote slot %s = %x", indexSlot(2, 1), got) + } + }) + + t.Run("get", func(t *testing.T) { + root := common.HexToHash("0x00000000000000000000000000000000000000000000000000000000cafebabe") + sender := crypto.PubkeyToAddress(eip8304TestKey.PublicKey) + // The get path's freshness check is one-sided: table (fb, ts) remains + // readable while num < fb + 1025*ts + ts/4 (the ring-buffer overwrite + // window plus the table_size/4 publication delay, with truncating + // division). There is no lower age bound. Write at block 1, then read + // at the boundaries. + for _, c := range []struct { + fb, ts, num uint64 + wantErr bool + }{ + {fb: 2, ts: 1, num: 100, wantErr: false}, // no lower age bound + {fb: 2, ts: 1, num: 1026, wantErr: false}, // last readable block + {fb: 2, ts: 1, num: 1027, wantErr: true}, // ring window expired + {fb: 8, ts: 4, num: 4108, wantErr: false}, // ts=4 window + {fb: 8, ts: 4, num: 4109, wantErr: true}, + } { + statedb := newEIP8304ContractState(t) + evm := newEIP8304ContractEVM(t, statedb, 1) + addr := deployEIP8304IndexContract(t, evm) + if _, _, err := evm.Call(params.SystemAddress, addr, setCalldata(c.fb, c.ts, root), vm.NewGasBudget(5_000_000, 0), common.U2560); err != nil { + t.Fatalf("set(%d, %d) failed: %v", c.fb, c.ts, err) + } + evm = newEIP8304ContractEVM(t, statedb, c.num) + ret, _, err := evm.Call(sender, addr, getCalldata(c.fb, c.ts), vm.NewGasBudget(5_000_000, 0), common.U2560) + if c.wantErr { + if err == nil { + t.Fatalf("get(%d, %d) at block %d: expected revert", c.fb, c.ts, c.num) + } + continue + } + if err != nil { + t.Fatalf("get(%d, %d) at block %d failed: %v", c.fb, c.ts, c.num, err) + } + if got := common.BytesToHash(ret); got != root { + t.Fatalf("get(%d, %d) at block %d returned %x, want %x", c.fb, c.ts, c.num, got, root) + } + } + }) + + t.Run("get-guards", func(t *testing.T) { + root := common.HexToHash("0xcafebabe") + statedb := newEIP8304ContractState(t) + evm := newEIP8304ContractEVM(t, statedb, 259) + addr := deployEIP8304IndexContract(t, evm) + if _, _, err := evm.Call(params.SystemAddress, addr, setCalldata(2, 1, root), vm.NewGasBudget(5_000_000, 0), common.U2560); err != nil { + t.Fatalf("set failed: %v", err) + } + sender := crypto.PubkeyToAddress(eip8304TestKey.PublicKey) + // Wrong calldata size (32 bytes instead of 64). + if _, _, err := evm.Call(sender, addr, make([]byte, 32), vm.NewGasBudget(5_000_000, 0), common.U2560); err == nil { + t.Fatal("get with 32-byte calldata: expected revert") + } + // first_block not a multiple of table_size. + if _, _, err := evm.Call(sender, addr, getCalldata(3, 2), vm.NewGasBudget(5_000_000, 0), common.U2560); err == nil { + t.Fatal("get(3, 2): expected revert (3 is not a multiple of 2)") + } + }) +} diff --git a/core/state_processor.go b/core/state_processor.go index 05f4bee052ea..6363797fe5ec 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -18,6 +18,7 @@ package core import ( "context" + "encoding/binary" "fmt" "math/big" "sync/atomic" @@ -131,7 +132,13 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated blockAccessList.Merge(bal) spanEnd(nil) } - requests, bal, err := PostExecution(ctx, config, block.Number(), block.Time(), allLogs, evm, uint32(len(block.Transactions())+1)) + // EIP-8304: build the level-0 log index table root. No-op before the fork + // activates or when the contract is not deployed (non-dev chains). + var tablesToWrite []TableWrite + if config.IsAmsterdam(block.Number(), block.Time()) && evm.StateDB.GetCodeSize(params.IndexContractAddress) > 0 { + tablesToWrite = BuildLogIndexForBlock(block.Number().Uint64(), block.ParentHash(), receipts) + } + requests, bal, err := PostExecution(ctx, config, block.Number(), block.Time(), allLogs, tablesToWrite, evm, uint32(len(block.Transactions())+1)) if err != nil { return nil, err } @@ -175,7 +182,7 @@ func PreExecution(ctx context.Context, beaconRoot *common.Hash, parent *types.He // PostExecution processes post-execution system calls when Prague is enabled. // If Prague is not activated, it returns null requests to differentiate from // empty requests. -func PostExecution(ctx context.Context, config *params.ChainConfig, number *big.Int, time uint64, allLogs []*types.Log, evm *vm.EVM, blockAccessIndex uint32) (requests [][]byte, blockAccessList *bal.ConstructionBlockAccessList, err error) { +func PostExecution(ctx context.Context, config *params.ChainConfig, number *big.Int, time uint64, allLogs []*types.Log, tablesToWrite []TableWrite, evm *vm.EVM, blockAccessIndex uint32) (requests [][]byte, blockAccessList *bal.ConstructionBlockAccessList, err error) { _, _, spanEnd := telemetry.StartSpan(ctx, "core.postExecution") defer spanEnd(&err) @@ -209,6 +216,18 @@ func PostExecution(ctx context.Context, config *params.ChainConfig, number *big. return nil, nil, fmt.Errorf("failed to process builder exit queue: %w", err) } } + + // EIP-8304: write log index table roots to the on-chain contract. + if config.IsAmsterdam(number, time) { + if len(tablesToWrite) > 0 && blockAccessList == nil { + blockAccessList = bal.NewConstructionBlockAccessList() + } + for _, tw := range tablesToWrite { + if err := ProcessLogIndexWrites(tw, rules, evm, blockAccessIndex, blockAccessList); err != nil { + return nil, nil, fmt.Errorf("failed to process log index write: %w", err) + } + } + } return requests, blockAccessList, nil } @@ -379,6 +398,47 @@ func ProcessWithdrawalQueue(requests *[][]byte, rules params.Rules, evm *vm.EVM, return processRequestsSystemCall(requests, rules, evm, 0x01, params.WithdrawalQueueAddress, blockAccessIndex, blockAccessList) } +// ProcessLogIndexWrites performs the EIP-8304 system call to write a table root +// to the on-chain index contract. It follows the same pattern as processRequestsSystemCall +// (EIP-7685) to ensure state changes persist correctly. +func ProcessLogIndexWrites(tw TableWrite, rules params.Rules, evm *vm.EVM, blockAccessIndex uint32, blockAccessList *bal.ConstructionBlockAccessList) error { + if tracer := evm.Config.Tracer; tracer != nil { + onSystemCallStart(tracer, evm.GetVMContext()) + if tracer.OnSystemCallEnd != nil { + defer tracer.OnSystemCallEnd() + } + } + gasLimit, gasBudget := systemCallGasBudget(evm) + // Build 96-byte calldata: first_block(32) + table_size(32) + table_root(32) + calldata := make([]byte, 96) + binary.BigEndian.PutUint64(calldata[24:32], tw.FirstBlock) + binary.BigEndian.PutUint64(calldata[56:64], tw.TableSize) + copy(calldata[64:96], tw.Root[:]) + msg := &Message{ + From: params.SystemAddress, + GasLimit: gasLimit, + GasPrice: uint256.NewInt(0), + GasFeeCap: uint256.NewInt(0), + GasTipCap: uint256.NewInt(0), + To: ¶ms.IndexContractAddress, + Data: calldata, + } + evm.SetTxContext(NewEVMTxContext(msg)) + evm.StateDB.Prepare(rules, common.Address{}, common.Address{}, nil, nil, nil) + evm.StateDB.SetTxContext(common.Hash{}, 0, blockAccessIndex) + evm.StateDB.AddAddressToAccessList(params.IndexContractAddress) + _, _, err := evm.Call(msg.From, *msg.To, msg.Data, gasBudget, common.U2560) + if evm.StateDB.AccessEvents() != nil { + evm.StateDB.AccessEvents().Merge(evm.AccessEvents) + } + bal := evm.StateDB.Finalise(evm.GetRules()) + if err != nil { + return fmt.Errorf("EIP-8304 system call failed: %v", err) + } + blockAccessList.Merge(bal) + return nil +} + // ProcessConsolidationQueue calls the EIP-7251 consolidation queue contract. // It returns the opaque request data returned by the contract. func ProcessConsolidationQueue(requests *[][]byte, rules params.Rules, evm *vm.EVM, blockAccessIndex uint32, blockAccessList *bal.ConstructionBlockAccessList) error { diff --git a/core/state_processor_parallel.go b/core/state_processor_parallel.go index 6b4df1af85e0..619cf7409931 100644 --- a/core/state_processor_parallel.go +++ b/core/state_processor_parallel.go @@ -219,7 +219,8 @@ func (p *StateProcessor) processParallel(ctx context.Context, block *types.Block if precompileCache != nil { postEVM.SetPrecompileCache(precompileCache) } - requests, postBAL, err := PostExecution(ctx, config, header.Number, header.Time, allLogs, postEVM, postIndex) + // EIP-8304: log index tables are not built on the parallel path, wired in a later PR + requests, postBAL, err := PostExecution(ctx, config, header.Number, header.Time, allLogs, nil, postEVM, postIndex) postEVM.Release() if err != nil { return nil, err diff --git a/core/types/index_builder.go b/core/types/index_builder.go new file mode 100644 index 000000000000..09bcb9949905 --- /dev/null +++ b/core/types/index_builder.go @@ -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 . + +package types + +import ( + "sort" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" +) + +// IndexBuilder accumulates IndexEntry values and computes a Merkle root. +type IndexBuilder struct { + entries []IndexEntry +} + +func NewIndexBuilder() *IndexBuilder { + return &IndexBuilder{} +} + +// AddBlockEntries adds the EIP-8304 entries of one block: the parent block's +// block entry (one-block delay per the spec; none for the genesis block), one +// transaction entry per receipt carrying the cumulative log count of the block +// before that transaction, and per-log address and topic entries with log +// indices relative to their transaction. +func (b *IndexBuilder) AddBlockEntries(parentHash common.Hash, blockNumber uint64, receipts Receipts) { + if blockNumber > 0 { + // The table for block N contains the block entry of block N-1. + b.entries = append(b.entries, EncodeEntry(EntryTypeBlock, parentHash[:], blockNumber-1, 0, 0)) + } + var cumLogs uint32 + for txIdx, receipt := range receipts { + // Transaction entry: the trailing field is the cumulative log count + // of the block before this transaction. + b.entries = append(b.entries, EncodeEntry(EntryTypeTransaction, receipt.TxHash[:], blockNumber, uint32(txIdx), cumLogs)) + for logIdx, log := range receipt.Logs { + // Log indices are relative to the transaction. + b.entries = append(b.entries, EncodeEntry(EntryTypeLogAddress, log.Address[:], blockNumber, uint32(txIdx), uint32(logIdx))) + // Topic entries + for topicIdx, topic := range log.Topics { + if topicIdx < 4 { + b.entries = append(b.entries, EncodeEntry(EntryTypeLogTopic0+EntryType(topicIdx), topic[:], blockNumber, uint32(txIdx), uint32(logIdx))) + } + } + } + cumLogs += uint32(len(receipt.Logs)) + } +} + +// Entries returns the accumulated entries slice (unsorted). Callers that need +// sorted entries should call Build() or sort the returned slice themselves. +func (b *IndexBuilder) Entries() []IndexEntry { + return b.entries +} + +// Build sorts entries and returns a simple hash. +func (b *IndexBuilder) Build() common.Hash { + sort.Slice(b.entries, func(i, j int) bool { + return CompareEntries(b.entries[i], b.entries[j]) < 0 + }) + // Simple hash: Keccak256 of all concatenated entries + var buf []byte + for _, e := range b.entries { + buf = append(buf, e...) + } + return crypto.Keccak256Hash(buf) +} diff --git a/core/types/index_builder_test.go b/core/types/index_builder_test.go new file mode 100644 index 000000000000..60e62a7a6828 --- /dev/null +++ b/core/types/index_builder_test.go @@ -0,0 +1,208 @@ +// 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 types + +import ( + "bytes" + "encoding/hex" + "testing" + + "github.com/ethereum/go-ethereum/common" +) + +// Spec worked example (EIP-8304, pinned at +// ethereum/EIPs@a9b074cffb5bea63b201ee5fabf19553dbfe483a): blocks 40-43 of a +// chain whose 42-43 contain WETH/USDT transfers. +// +// Note on the spec's internal inconsistency: the chronological table lists the +// 42tx1 hash as 0xa75590c9... while the binary table encodes 0x75590c9c... +// This test pins the binary table (which is the authoritative encoding). +var ( + specHash39 = common.HexToHash("0xbf98e6cb26f6ff312586968d1f343a3d3c439a8c5c86233aff2a82f1a68263df") + specHash40 = common.HexToHash("0x42f66a2e9f9c68e223e8d826145d7cfacb00520dba6a9555803121de29790b65") + specHash41 = common.HexToHash("0x978ce0036b6d1c62d716045505587d15cc85a1def92f9f450937b6467295e517") + specHash42 = common.HexToHash("0x66f42ef12b140e8004ad39a760191457f485204ee6c9f990c33f14014e521f20") + + specTx42_0 = common.HexToHash("0xca2d12d1b8132de09d0d668cc87349dc70134bee3010e03ddb2d83f7160bd6e3") + specTx42_1 = common.HexToHash("0x75590c9ced72898d6207c917e11b452949043404a1802b893f7717a9c1c8f45d") + specTx43_0 = common.HexToHash("0x7046035b326ab22a3142e4416ed4300a28a483a31a1e04cf62bec575b2e7cf09") + + specWETH = common.HexToAddress("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2") + specUSDT = common.HexToAddress("0xdac17f958d2ee523a2206206994597c13d831ec7") + + specTransfer = common.HexToHash("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef") + specWithdrawal = common.HexToHash("0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65") + specAlice = common.HexToHash("0x0000000000000000000000004a5e9a3bf35df5e0ea4b4cd3289e0111f1deadf7") + specBob = common.HexToHash("0x000000000000000000000000f2a5fd4d5bb24b651d1198bc014941a16f6edde5") + specCarol = common.HexToHash("0x000000000000000000000000ac8cbe20039c2e9547da9107456179bd4f39a734") +) + +// TestIndexBuilderSpecWorkedExample reproduces the spec's 22-entry worked +// example byte-for-byte: four single-block tables (blocks 40-43) whose entries +// are the lexicographically sorted binary encodings listed in the spec. +func TestIndexBuilderSpecWorkedExample(t *testing.T) { + receipts42 := Receipts{ + {TxHash: specTx42_0, Logs: []*Log{ + {Address: specWETH, Topics: []common.Hash{specTransfer, specAlice, specBob}}, + {Address: specUSDT, Topics: []common.Hash{specTransfer, specBob, specAlice}}, + }}, + {TxHash: specTx42_1, Logs: []*Log{ + {Address: specWETH, Topics: []common.Hash{specWithdrawal, specBob}}, + }}, + } + receipts43 := Receipts{ + {TxHash: specTx43_0, Logs: []*Log{ + {Address: specUSDT, Topics: []common.Hash{specTransfer, specAlice, specCarol}}, + }}, + } + // The sorted binary table from the spec, verbatim (22 entries). + want := []string{ + "000042f66a2e9f9c68e223e8d826145d7cfacb00520dba6a9555803121de29790b650000000000000028", + "000066f42ef12b140e8004ad39a760191457f485204ee6c9f990c33f14014e521f20000000000000002a", + "0000978ce0036b6d1c62d716045505587d15cc85a1def92f9f450937b6467295e5170000000000000029", + "0000bf98e6cb26f6ff312586968d1f343a3d3c439a8c5c86233aff2a82f1a68263df0000000000000027", + "00017046035b326ab22a3142e4416ed4300a28a483a31a1e04cf62bec575b2e7cf09000000000000002b0000000000000000", + "000175590c9ced72898d6207c917e11b452949043404a1802b893f7717a9c1c8f45d000000000000002a0000000100000002", + "0001ca2d12d1b8132de09d0d668cc87349dc70134bee3010e03ddb2d83f7160bd6e3000000000000002a0000000000000000", + "0002c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000002a0000000000000000", + "0002c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000002a0000000100000000", + "0002dac17f958d2ee523a2206206994597c13d831ec7000000000000002a0000000000000001", + "0002dac17f958d2ee523a2206206994597c13d831ec7000000000000002b0000000000000000", + "00037fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65000000000000002a0000000100000000", + "0003ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef000000000000002a0000000000000000", + "0003ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef000000000000002a0000000000000001", + "0003ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef000000000000002b0000000000000000", + "00040000000000000000000000004a5e9a3bf35df5e0ea4b4cd3289e0111f1deadf7000000000000002a0000000000000000", + "00040000000000000000000000004a5e9a3bf35df5e0ea4b4cd3289e0111f1deadf7000000000000002b0000000000000000", + "0004000000000000000000000000f2a5fd4d5bb24b651d1198bc014941a16f6edde5000000000000002a0000000000000001", + "0004000000000000000000000000f2a5fd4d5bb24b651d1198bc014941a16f6edde5000000000000002a0000000100000000", + "00050000000000000000000000004a5e9a3bf35df5e0ea4b4cd3289e0111f1deadf7000000000000002a0000000000000001", + "0005000000000000000000000000ac8cbe20039c2e9547da9107456179bd4f39a734000000000000002b0000000000000000", + "0005000000000000000000000000f2a5fd4d5bb24b651d1198bc014941a16f6edde5000000000000002a0000000000000000", + } + + b := NewIndexBuilder() + b.AddBlockEntries(specHash39, 40, nil) + b.AddBlockEntries(specHash40, 41, nil) + b.AddBlockEntries(specHash41, 42, receipts42) + b.AddBlockEntries(specHash42, 43, receipts43) + if got := len(b.Entries()); got != 22 { + t.Fatalf("entry count = %d, want 22", got) + } + b.Build() // sorts in place + for i, e := range b.Entries() { + if got := hex.EncodeToString(e); got != want[i] { + t.Errorf("entry %d: encoded %s, want %s", i, got, want[i]) + } + } + + // Semantics the prose describes, decoded from the sorted entries: + // cumulative log counts, per-transaction log indices, one-block delay. + checks := []struct { + index int + typ EntryType + value []byte + block uint64 + tx uint32 + log uint32 + }{ + // 42tx1's transaction entry carries cumulative log count 2. + {5, EntryTypeTransaction, specTx42_1[:], 42, 1, 2}, + // 42tx0's transaction entry has cumulative log count 0. + {6, EntryTypeTransaction, specTx42_0[:], 42, 0, 0}, + // WETH log of 42tx1 has log index 0, not cumulative 2. + {8, EntryTypeLogAddress, specWETH[:], 42, 1, 0}, + // The table for block 43 holds block 42's entry (one-block delay). + {1, EntryTypeBlock, specHash42[:], 42, 0, 0}, + } + for _, c := range checks { + typ, content, block, tx, log, err := EntryFields(b.Entries()[c.index]) + if err != nil { + t.Fatalf("entry %d: unexpected error: %v", c.index, err) + } + if typ != c.typ || !bytes.Equal(content, c.value) || block != c.block || tx != c.tx || log != c.log { + t.Errorf("entry %d: decoded (type %d, %x, %d, %d, %d), want (%d, %x, %d, %d, %d)", + c.index, typ, content, block, tx, log, c.typ, c.value, c.block, c.tx, c.log) + } + } +} + +// TestIndexBuilderPerTableCounts pins the entry distribution across the four +// tables of the worked example: 1, 1, 14 and 6 entries. +func TestIndexBuilderPerTableCounts(t *testing.T) { + receipts42 := Receipts{ + {TxHash: specTx42_0, Logs: []*Log{ + {Address: specWETH, Topics: []common.Hash{specTransfer, specAlice, specBob}}, + {Address: specUSDT, Topics: []common.Hash{specTransfer, specBob, specAlice}}, + }}, + {TxHash: specTx42_1, Logs: []*Log{ + {Address: specWETH, Topics: []common.Hash{specWithdrawal, specBob}}, + }}, + } + receipts43 := Receipts{ + {TxHash: specTx43_0, Logs: []*Log{ + {Address: specUSDT, Topics: []common.Hash{specTransfer, specAlice, specCarol}}, + }}, + } + tests := []struct { + block uint64 + parent common.Hash + receipts Receipts + want int + }{ + {40, specHash39, nil, 1}, + {41, specHash40, nil, 1}, + {42, specHash41, receipts42, 14}, + {43, specHash42, receipts43, 6}, + } + for _, tt := range tests { + b := NewIndexBuilder() + b.AddBlockEntries(tt.parent, tt.block, tt.receipts) + if got := len(b.Entries()); got != tt.want { + t.Errorf("block %d: entry count = %d, want %d", tt.block, got, tt.want) + } + } +} + +// TestAddBlockEntriesGenesis covers the genesis edge cases: no block entry for +// block 0 and exactly the parent's entry for block 1. +func TestAddBlockEntriesGenesis(t *testing.T) { + b := NewIndexBuilder() + b.AddBlockEntries(specHash39, 0, nil) + if got := len(b.Entries()); got != 0 { + t.Fatalf("block 0: entry count = %d, want 0", got) + } + + b = NewIndexBuilder() + b.AddBlockEntries(specHash39, 1, nil) + if got := len(b.Entries()); got != 1 { + t.Fatalf("block 1: entry count = %d, want 1", got) + } + typ, content, block, tx, log, err := EntryFields(b.Entries()[0]) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if typ != EntryTypeBlock || !bytes.Equal(content, specHash39[:]) || block != 0 || tx != 0 || log != 0 { + t.Errorf("block 1 entry: decoded (type %d, %x, %d, %d, %d), want block entry of block 0", typ, content, block, tx, log) + } + + // An empty builder still hashes (the genesis block never gets a table). + if root := NewIndexBuilder().Build(); root == (common.Hash{}) { + t.Errorf("empty builder root should be the keccak of an empty table") + } +} + diff --git a/core/types/logindex_entry.go b/core/types/logindex_entry.go new file mode 100644 index 000000000000..1ba8c49ed343 --- /dev/null +++ b/core/types/logindex_entry.go @@ -0,0 +1,228 @@ +// 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 types + +import ( + "bytes" + "encoding/binary" + "fmt" +) + +// EntryType discriminates the kind of lookup a single index entry provides. +// +// The entry type determines how clients interpret the index value and which +// position fields are meaningful: +// +// - EntryTypeBlock: index value is a block hash; no position tail. +// - EntryTypeTransaction: index value is a tx hash; the trailing position +// field holds the cumulative log count of the block before the tx. +// - EntryTypeLogAddress: index value is the log's contract address. +// - EntryTypeLogTopic0-3: index value is the log's topic[i]. +// +// On the wire the type id is a 2-byte big-endian field. +type EntryType uint16 + +const ( + EntryTypeBlock EntryType = 0 // block hash lookup + EntryTypeTransaction EntryType = 1 // transaction hash lookup + EntryTypeLogAddress EntryType = 2 // log address lookup + EntryTypeLogTopic0 EntryType = 3 // log topic[0] lookup + EntryTypeLogTopic1 EntryType = 4 // log topic[1] lookup + EntryTypeLogTopic2 EntryType = 5 // log topic[2] lookup + EntryTypeLogTopic3 EntryType = 6 // log topic[3] lookup +) + +// Field sizes of the EIP-8304 entry encodings. All numbers are fixed-size +// big-endian. The layouts are parameterized here on purpose: if the spec +// changes a field size, edit this block and the offset arithmetic in +// EncodeEntry and EntryFields stays correct by construction. +const ( + EntryTypeSize = 2 // type id + BlockHashSize = 32 + TxHashSize = 32 + AddressSize = 20 + TopicSize = 32 + BlockNumberSize = 8 + TxIndexSize = 4 + LogIndexSize = 4 // log index within a transaction + CumulativeLogSize = 4 // logs before the transaction in its block +) + +// Total sizes of the four entry kinds, derived at compile time from the field +// sizes above: type || content || block number || tx index || log index. +const ( + BlockEntrySize = EntryTypeSize + BlockHashSize + BlockNumberSize // 42 + TransactionEntrySize = EntryTypeSize + TxHashSize + BlockNumberSize + TxIndexSize + CumulativeLogSize // 50 + LogAddressEntrySize = EntryTypeSize + AddressSize + BlockNumberSize + TxIndexSize + LogIndexSize // 38 + LogTopicEntrySize = EntryTypeSize + TopicSize + BlockNumberSize + TxIndexSize + LogIndexSize // 50 +) + +// IndexEntry is a single variable-length record of a sorted EIP-8304 index +// table. +// +// Layout (big-endian): +// +// type id (2) || content || block number (8) [|| tx index (4) || log index (4)] +// +// The content field is 32 bytes for block, transaction and topic entries and +// 20 bytes for address entries, giving the total sizes BlockEntrySize, +// TransactionEntrySize, LogAddressEntrySize and LogTopicEntrySize. Block +// entries stop after the block number; the other entry kinds carry the tx +// index and log index tail, which for transaction entries is the cumulative +// log count of the block before the transaction. +// +// Sorting is lexicographical on the full encoding, giving the ordering: +// type id, content, block number, tx index, log index. +type IndexEntry []byte + +// contentSize returns the encoded length of the searchable content field for +// the given entry type. +func contentSize(typ EntryType) int { + switch typ { + case EntryTypeBlock: + return BlockHashSize + case EntryTypeTransaction: + return TxHashSize + case EntryTypeLogAddress: + return AddressSize + case EntryTypeLogTopic0, EntryTypeLogTopic1, EntryTypeLogTopic2, EntryTypeLogTopic3: + return TopicSize + default: + panic(fmt.Sprintf("types: unknown entry type %d", typ)) + } +} + +// entrySize returns the total encoded length of an entry of the given type. +func entrySize(typ EntryType) int { + switch typ { + case EntryTypeBlock: + return BlockEntrySize + case EntryTypeTransaction: + return TransactionEntrySize + case EntryTypeLogAddress: + return LogAddressEntrySize + case EntryTypeLogTopic0, EntryTypeLogTopic1, EntryTypeLogTopic2, EntryTypeLogTopic3: + return LogTopicEntrySize + default: + panic(fmt.Sprintf("types: unknown entry type %d", typ)) + } +} + +// EncodeEntry packs the components into a variable-length index entry. content +// must be exactly AddressSize (20) bytes for EntryTypeLogAddress and 32 bytes +// otherwise. For EntryTypeTransaction, logIdx is the cumulative log count of +// the block before the transaction; for all other types it is the log index +// within the transaction. Block entries have no position tail, so txIdx and +// logIdx are ignored for them. +func EncodeEntry(typ EntryType, content []byte, blockNum uint64, txIdx uint32, logIdx uint32) IndexEntry { + if want := contentSize(typ); len(content) != want { + panic(fmt.Sprintf("types: entry content length %d, want %d for type %d", len(content), want, typ)) + } + e := make(IndexEntry, entrySize(typ)) + off := 0 + binary.BigEndian.PutUint16(e[off:off+EntryTypeSize], uint16(typ)) + off += EntryTypeSize + copy(e[off:], content) + off += len(content) + binary.BigEndian.PutUint64(e[off:off+BlockNumberSize], blockNum) + off += BlockNumberSize + if typ == EntryTypeBlock { + return e + } + binary.BigEndian.PutUint32(e[off:off+TxIndexSize], txIdx) + off += TxIndexSize + binary.BigEndian.PutUint32(e[off:off+LogIndexSize], logIdx) + return e +} + +// EntryFields decodes an entry back into its components. content is a fresh +// copy and does not alias the entry. It returns an error when the entry is +// shorter than the type id, carries an unknown type id, or its length does not +// match the type's fixed layout. +func EntryFields(entry IndexEntry) (typ EntryType, content []byte, blockNum uint64, txIdx uint32, logIdx uint32, err error) { + if len(entry) < EntryTypeSize { + return 0, nil, 0, 0, 0, fmt.Errorf("types: entry too short: %d bytes", len(entry)) + } + typ = EntryType(binary.BigEndian.Uint16(entry[:EntryTypeSize])) + if typ > EntryTypeLogTopic3 { + return 0, nil, 0, 0, 0, fmt.Errorf("types: unknown entry type %d", typ) + } + if len(entry) != entrySize(typ) { + return 0, nil, 0, 0, 0, fmt.Errorf("types: entry length %d, want %d for type %d", len(entry), entrySize(typ), typ) + } + content = make([]byte, contentSize(typ)) + copy(content, entry[EntryTypeSize:EntryTypeSize+len(content)]) + off := EntryTypeSize + len(content) + blockNum = binary.BigEndian.Uint64(entry[off : off+BlockNumberSize]) + if typ == EntryTypeBlock { + return typ, content, blockNum, 0, 0, nil + } + off += BlockNumberSize + txIdx = binary.BigEndian.Uint32(entry[off : off+TxIndexSize]) + off += TxIndexSize + logIdx = binary.BigEndian.Uint32(entry[off : off+LogIndexSize]) + return typ, content, blockNum, txIdx, logIdx, nil +} + +// CompareEntries returns the result of a lexicographic byte comparison +// between two index entries. +// +// Return values follow bytes.Compare: +// +// -1 if a < b +// 0 if a == b +// +1 if a > b +// +// Since every field is fixed-size big-endian, this equals the field-wise +// ordering: type id, content, block number, tx index, log index. +func CompareEntries(a, b IndexEntry) int { + return bytes.Compare(a, b) +} + +// EntryTypeString returns a human-readable name for the entry type, +// useful for debugging and log output. +func EntryTypeString(typ EntryType) string { + switch typ { + case EntryTypeBlock: + return "block" + case EntryTypeTransaction: + return "transaction" + case EntryTypeLogAddress: + return "log_address" + case EntryTypeLogTopic0: + return "log_topic0" + case EntryTypeLogTopic1: + return "log_topic1" + case EntryTypeLogTopic2: + return "log_topic2" + case EntryTypeLogTopic3: + return "log_topic3" + default: + return "unknown" + } +} + +// TableLevelCount is the number of chained-table levels in the index hierarchy. +// Level i covers a block range of TableSizes[i] blocks with staggered update +// schedules, forming a provable history of the full log index. +const TableLevelCount = 5 + +// TableSizes gives the block-range size for each level of the chained-table +// hierarchy. Level 0 uses single-block tables (updated every block), level 1 +// uses 4-block tables, level 2 uses 16-block tables, level 3 uses 64-block +// tables, and level 4 uses 256-block tables. +var TableSizes = [TableLevelCount]int{1, 4, 16, 64, 256} diff --git a/core/types/logindex_entry_test.go b/core/types/logindex_entry_test.go new file mode 100644 index 000000000000..19cceadc91b2 --- /dev/null +++ b/core/types/logindex_entry_test.go @@ -0,0 +1,189 @@ +// 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 types + +import ( + "bytes" + "encoding/hex" + "strings" + "testing" +) + +// TestEncodeEntry pins the exact wire layout of every entry type: 2-byte +// big-endian type id, content (32 bytes, 20 for addresses), then the +// position fields. Non-trivial values prove the big-endian byte order. +func TestEncodeEntry(t *testing.T) { + var ( + blockNum = uint64(0x0102030405060708) + txIdx = uint32(0x01020304) + logIdx = uint32(0x0a0b0c0d) + hashCtx = bytes.Repeat([]byte{0x11}, 32) + addrCtx = bytes.Repeat([]byte{0x22}, 20) + hashHex = strings.Repeat("11", 32) + addrHex = strings.Repeat("22", 20) + tailHex = "0102030405060708" + "01020304" + "0a0b0c0d" + ) + tests := []struct { + typ EntryType + content []byte + want string + }{ + {EntryTypeBlock, hashCtx, "0000" + hashHex + "0102030405060708"}, + {EntryTypeTransaction, hashCtx, "0001" + hashHex + tailHex}, + {EntryTypeLogAddress, addrCtx, "0002" + addrHex + tailHex}, + {EntryTypeLogTopic0, hashCtx, "0003" + hashHex + tailHex}, + {EntryTypeLogTopic1, hashCtx, "0004" + hashHex + tailHex}, + {EntryTypeLogTopic2, hashCtx, "0005" + hashHex + tailHex}, + {EntryTypeLogTopic3, hashCtx, "0006" + hashHex + tailHex}, + } + for _, tt := range tests { + e := EncodeEntry(tt.typ, tt.content, blockNum, txIdx, logIdx) + if got := hex.EncodeToString(e); got != tt.want { + t.Errorf("type %d: encoded %s, want %s", tt.typ, got, tt.want) + } + if len(e) != entrySize(tt.typ) { + t.Errorf("type %d: length %d, want %d", tt.typ, len(e), entrySize(tt.typ)) + } + } +} + +// TestEncodeEntryPanics asserts the encoder rejects wrong content sizes and +// unknown types instead of silently mis-encoding. +func TestEncodeEntryPanics(t *testing.T) { + mustPanic := func(name string, f func()) { + defer func() { + if recover() == nil { + t.Errorf("%s: expected panic", name) + } + }() + f() + } + mustPanic("block with 20-byte content", func() { EncodeEntry(EntryTypeBlock, make([]byte, 20), 0, 0, 0) }) + mustPanic("address with 32-byte content", func() { EncodeEntry(EntryTypeLogAddress, make([]byte, 32), 0, 0, 0) }) + mustPanic("topic with 20-byte content", func() { EncodeEntry(EntryTypeLogTopic2, make([]byte, 20), 0, 0, 0) }) + mustPanic("unknown type", func() { EncodeEntry(EntryType(7), make([]byte, 32), 0, 0, 0) }) +} + +// TestEntryFields round-trips every entry type and asserts the decoder +// rejects malformed and unknown-type entries without panicking. +func TestEntryFields(t *testing.T) { + var ( + blockNum = uint64(0x0102030405060708) + txIdx = uint32(0x01020304) + logIdx = uint32(0x0a0b0c0d) + ) + tests := []EntryType{ + EntryTypeBlock, EntryTypeTransaction, EntryTypeLogAddress, + EntryTypeLogTopic0, EntryTypeLogTopic1, EntryTypeLogTopic2, EntryTypeLogTopic3, + } + for _, typ := range tests { + content := make([]byte, contentSize(typ)) + for i := range content { + content[i] = byte(i) + } + e := EncodeEntry(typ, content, blockNum, txIdx, logIdx) + gotTyp, gotContent, gotBlock, gotTx, gotLog, err := EntryFields(e) + if err != nil { + t.Fatalf("type %d: unexpected error: %v", typ, err) + } + // Block entries have no position tail; txIdx and logIdx are ignored + // for them, so skip the tail comparison. + wantTx, wantLog := txIdx, logIdx + if typ == EntryTypeBlock { + wantTx, wantLog = 0, 0 + } + if gotTyp != typ || !bytes.Equal(gotContent, content) || gotBlock != blockNum || gotTx != wantTx || gotLog != wantLog { + t.Errorf("type %d: round-trip mismatch: typ %d content %x block %d tx %d log %d", typ, gotTyp, gotContent, gotBlock, gotTx, gotLog) + } + // The decoded content must not alias the entry. + gotContent[0] ^= 0xff + if bytes.Equal(gotContent, content) { + t.Errorf("type %d: decoded content aliases the entry", typ) + } + } + + // Malformed entries: too short, truncated, oversized, unknown type id. + valid := EncodeEntry(EntryTypeTransaction, make([]byte, TxHashSize), 0, 0, 0) + unknownID := EncodeEntry(EntryTypeLogTopic3, make([]byte, TopicSize), 0, 0, 0) + unknownID[0], unknownID[1] = 0x00, 0x07 + bad := []IndexEntry{ + nil, + valid[:1], + valid[:len(valid)-1], + append(IndexEntry{}, append(valid, 0)...), + unknownID, + {0xff, 0xff, 0x00}, + } + for _, e := range bad { + if _, _, _, _, _, err := EntryFields(e); err == nil { + t.Errorf("entry %x: expected error", e) + } + } +} + +// TestCompareEntries asserts the ordering implied by the spec's +// lexicographical sort: type id, then content, then block number, then tx +// index, then log index. +func TestCompareEntries(t *testing.T) { + hashOf := func(v byte) []byte { return bytes.Repeat([]byte{v}, 32) } + addrOf := func(v byte) []byte { return bytes.Repeat([]byte{v}, 20) } + tests := []struct { + name string + a, b IndexEntry + want int + }{ + {"type ordering", EncodeEntry(EntryTypeBlock, hashOf(0), 0, 0, 0), EncodeEntry(EntryTypeTransaction, hashOf(0), 0, 0, 0), -1}, + {"type ordering 2", EncodeEntry(EntryTypeTransaction, hashOf(0), 0, 0, 0), EncodeEntry(EntryTypeLogAddress, addrOf(0), 0, 0, 0), -1}, + {"type ordering 3", EncodeEntry(EntryTypeLogAddress, addrOf(0), 0, 0, 0), EncodeEntry(EntryTypeLogTopic0, hashOf(0), 0, 0, 0), -1}, + {"topic ordering", EncodeEntry(EntryTypeLogTopic1, hashOf(0), 0, 0, 0), EncodeEntry(EntryTypeLogTopic2, hashOf(0), 0, 0, 0), -1}, + {"content ordering", EncodeEntry(EntryTypeBlock, hashOf(0), 0, 0, 0), EncodeEntry(EntryTypeBlock, hashOf(1), 0, 0, 0), -1}, + {"block ordering", EncodeEntry(EntryTypeBlock, hashOf(0), 1, 0, 0), EncodeEntry(EntryTypeBlock, hashOf(0), 2, 0, 0), -1}, + {"tx ordering", EncodeEntry(EntryTypeTransaction, hashOf(0), 0, 1, 0), EncodeEntry(EntryTypeTransaction, hashOf(0), 0, 2, 0), -1}, + {"log ordering", EncodeEntry(EntryTypeLogAddress, addrOf(0), 0, 0, 1), EncodeEntry(EntryTypeLogAddress, addrOf(0), 0, 0, 2), -1}, + {"equal", EncodeEntry(EntryTypeBlock, hashOf(0), 0, 0, 0), EncodeEntry(EntryTypeBlock, hashOf(0), 0, 0, 0), 0}, + } + for _, tt := range tests { + if got := CompareEntries(tt.a, tt.b); got != tt.want { + t.Errorf("%s: CompareEntries = %d, want %d", tt.name, got, tt.want) + } + if got := CompareEntries(tt.b, tt.a); got != -tt.want { + t.Errorf("%s: comparison not antisymmetric", tt.name) + } + } +} + +// TestEntryTypeString covers the human-readable names used in logs. +func TestEntryTypeString(t *testing.T) { + want := map[EntryType]string{ + EntryTypeBlock: "block", EntryTypeTransaction: "transaction", + EntryTypeLogAddress: "log_address", EntryTypeLogTopic0: "log_topic0", + EntryTypeLogTopic1: "log_topic1", EntryTypeLogTopic2: "log_topic2", + EntryTypeLogTopic3: "log_topic3", EntryType(7): "unknown", + } + for typ, wantStr := range want { + if got := EntryTypeString(typ); got != wantStr { + t.Errorf("type %d: got %q, want %q", typ, got, wantStr) + } + } +} + +// TestEntrySizes guards the derived totals against the spec's fixed sizes. +func TestEntrySizes(t *testing.T) { + if BlockEntrySize != 42 || TransactionEntrySize != 50 || LogAddressEntrySize != 38 || LogTopicEntrySize != 50 { + t.Errorf("derived sizes: block %d, tx %d, address %d, topic %d; want 42, 50, 38, 50", BlockEntrySize, TransactionEntrySize, LogAddressEntrySize, LogTopicEntrySize) + } +} diff --git a/eth/api_debug_replay.go b/eth/api_debug_replay.go index 74b649eaaf84..ee18694f46d1 100644 --- a/eth/api_debug_replay.go +++ b/eth/api_debug_replay.go @@ -183,7 +183,8 @@ func (api *DebugAPI) replayBuild(ctx context.Context, block *types.Block, stated for _, r := range receipts { allLogs = append(allLogs, r.Logs...) } - _, postBal, err := core.PostExecution(ctx, config, header.Number, header.Time, allLogs, evm, uint32(tcount+1)) + // EIP-8304: log index tables are not built for replay, wired in a later PR + _, postBal, err := core.PostExecution(ctx, config, header.Number, header.Time, allLogs, nil, evm, uint32(tcount+1)) if err != nil { return nil, nil, 0, common.Hash{}, err } diff --git a/internal/ethapi/simulate.go b/internal/ethapi/simulate.go index 7e174551befc..7b6b0a379a61 100644 --- a/internal/ethapi/simulate.go +++ b/internal/ethapi/simulate.go @@ -393,8 +393,8 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, header.BlobGasUsed = &blobGasUsed } - // Process EIP-7685 requests - requests, bal, err := core.PostExecution(ctx, sim.chainConfig, header.Number, header.Time, allLogs, evm, uint32(len(block.Calls)+1)) + // Process EIP-7685 requests; EIP-8304 tables are nil here, wired in a later PR + requests, bal, err := core.PostExecution(ctx, sim.chainConfig, header.Number, header.Time, allLogs, nil, evm, uint32(len(block.Calls)+1)) if err != nil { return nil, nil, nil, err } diff --git a/miner/worker.go b/miner/worker.go index c690fb7446d1..a55f52441079 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -223,8 +223,16 @@ func (miner *Miner) generateWork(ctx context.Context, genParam *generateParams, allLogs = append(allLogs, r.Logs...) } + // EIP-8304: build log index tables for the system call inside PostExecution. + // No-op before the fork activates or when the contract is not deployed + // (non-dev chains). + var tablesToWrite []core.TableWrite + if miner.chainConfig.IsAmsterdam(work.header.Number, work.header.Time) && work.evm.StateDB.GetCodeSize(params.IndexContractAddress) > 0 { + tablesToWrite = core.BuildLogIndexForBlock(work.header.Number.Uint64(), work.header.ParentHash, work.receipts) + } + // Collect consensus-layer requests if Prague is enabled. - requests, bal, err := core.PostExecution(ctx, miner.chainConfig, work.header.Number, work.header.Time, allLogs, work.evm, uint32(work.tcount+1)) + requests, bal, err := core.PostExecution(ctx, miner.chainConfig, work.header.Number, work.header.Time, allLogs, tablesToWrite, work.evm, uint32(work.tcount+1)) if err != nil { return &newPayloadResult{err: err} } diff --git a/params/protocol_params.go b/params/protocol_params.go index 3cc8e2320c9c..efa4a10f52a6 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -249,6 +249,11 @@ var ( BeaconRootsAddress = common.HexToAddress("0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02") BeaconRootsCode = common.FromHex("3373fffffffffffffffffffffffffffffffffffffffe14604d57602036146024575f5ffd5b5f35801560495762001fff810690815414603c575f5ffd5b62001fff01545f5260205ff35b5f5ffd5b62001fff42064281555f359062001fff015500") + // EIP-8304 - Trustless Log and Transaction Index + IndexContractAddress = common.HexToAddress("0x0000000000000000000000000000000000008304") // TBD: per-spec address + IndexContractInitCode = common.FromHex("60758060095f395ff33373fffffffffffffffffffffffffffffffffffffffe1460605760403603605c576020358060801c605c576104008160048104430304828202925f35818106605c5704908103196103ff10605c570601548015605c575f5260205ff35b5f5ffd5b604035602035610400818102915f350406015500") + IndexContractCode = common.FromHex("3373fffffffffffffffffffffffffffffffffffffffe1460605760403603605c576020358060801c605c576104008160048104430304828202925f35818106605c5704908103196103ff10605c570601548015605c575f5260205ff35b5f5ffd5b604035602035610400818102915f350406015500") + // EIP-2935 - Serve historical block hashes from state HistoryStorageAddress = common.HexToAddress("0x0000F90827F1C53a10cb7A02335B175320002935") HistoryStorageCode = common.FromHex("3373fffffffffffffffffffffffffffffffffffffffe14604657602036036042575f35600143038111604257611fff81430311604257611fff9006545f5260205ff35b5f5ffd5b5f35611fff60014303065500")