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..0014dfef2da0
--- /dev/null
+++ b/core/log_index_builder.go
@@ -0,0 +1,37 @@
+// 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. Interim implementation: fixed-size entries and a flat keccak root
+func BuildLogIndexForBlock(blockNumber uint64, receipts types.Receipts) []TableWrite {
+ b0 := types.NewIndexBuilder()
+ b0.AddBlockEntries(blockNumber, receipts)
+ return []TableWrite{{FirstBlock: blockNumber, TableSize: 1, Root: b0.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..2d8914a42572 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(), 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..7219f400696b
--- /dev/null
+++ b/core/types/index_builder.go
@@ -0,0 +1,71 @@
+// 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 entries for all logs in a block's receipts.
+func (b *IndexBuilder) AddBlockEntries(blockNumber uint64, receipts Receipts) {
+ for txIdx, receipt := range receipts {
+ // Transaction entry
+ b.entries = append(b.entries, EncodeEntry(EntryTypeTransaction, receipt.TxHash, blockNumber, uint32(txIdx), 0))
+ for logIdx, log := range receipt.Logs {
+ // Address entry
+ addrHash := common.BytesToHash(log.Address.Bytes())
+ b.entries = append(b.entries, EncodeEntry(EntryTypeLogAddress, addrHash, blockNumber, uint32(txIdx), uint32(logIdx)))
+ // Topic entries
+ for topicIdx, topic := range log.Topics {
+ if topicIdx < 4 {
+ b.entries = append(b.entries, EncodeEntry(EntryType(int(EntryTypeLogTopic0)+topicIdx), topic, blockNumber, uint32(txIdx), uint32(logIdx)))
+ }
+ }
+ }
+ }
+}
+
+// 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/logindex_entry.go b/core/types/logindex_entry.go
new file mode 100644
index 000000000000..ee834a8e9022
--- /dev/null
+++ b/core/types/logindex_entry.go
@@ -0,0 +1,132 @@
+// 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"
+
+ "github.com/ethereum/go-ethereum/common"
+)
+
+// 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; tx/log indices are zero.
+// - EntryTypeTransaction: index value is a tx hash; log index is zero.
+// - EntryTypeLogAddress: index value is the log's contract address.
+// - EntryTypeLogTopic0-3: index value is the log's topic[i].
+type EntryType uint8
+
+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
+)
+
+// IndexEntrySize is the fixed size of each record in the sorted index table.
+const IndexEntrySize = 64
+
+// IndexEntry represents a single 64-byte record in the sorted index table.
+//
+// Layout (big-endian):
+//
+// [0] entry type (1 byte)
+// [1:33] index value (32 bytes)
+// [33:41] block number (8 bytes)
+// [41:45] transaction index (4 bytes)
+// [45:49] log index (4 bytes)
+// [49:64] zero-reserved (15 bytes)
+//
+// Sorting is lexicographical on the full 64 bytes, giving ordering:
+// entry_type, index_value, block_number, tx_index, log_index.
+type IndexEntry [IndexEntrySize]byte
+
+// EncodeEntry packs the components into a fixed-size 64-byte index entry.
+// Non-applicable position fields should be passed as zero.
+func EncodeEntry(typ EntryType, value common.Hash, blockNum uint64, txIdx uint32, logIdx uint32) IndexEntry {
+ var e IndexEntry
+ e[0] = byte(typ)
+ copy(e[1:33], value[:])
+ binary.BigEndian.PutUint64(e[33:41], blockNum)
+ binary.BigEndian.PutUint32(e[41:45], txIdx)
+ binary.BigEndian.PutUint32(e[45:49], logIdx)
+ // bytes 49..63 remain zero
+ return e
+}
+
+// EntryFields decodes an IndexEntry back to its components.
+func EntryFields(entry IndexEntry) (typ EntryType, value common.Hash, blockNum uint64, txIdx uint32, logIdx uint32) {
+ typ = EntryType(entry[0])
+ copy(value[:], entry[1:33])
+ blockNum = binary.BigEndian.Uint64(entry[33:41])
+ txIdx = binary.BigEndian.Uint32(entry[41:45])
+ logIdx = binary.BigEndian.Uint32(entry[45:49])
+ return
+}
+
+// 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
+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/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..a3efc98f45b4 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.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")