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")