Skip to content

Commit ac4d047

Browse files
committed
core: add EIP-8304 index contract golden test
1 parent 926f67a commit ac4d047

1 file changed

Lines changed: 225 additions & 0 deletions

File tree

core/logindex_contract_test.go

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
// Copyright 2026 The go-ethereum Authors
2+
// This file is part of the go-ethereum library.
3+
//
4+
// The go-ethereum library is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Lesser General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// The go-ethereum library is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Lesser General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Lesser General Public License
15+
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package core
18+
19+
import (
20+
"bytes"
21+
"encoding/binary"
22+
"math/big"
23+
"testing"
24+
25+
"github.com/ethereum/go-ethereum/common"
26+
"github.com/ethereum/go-ethereum/consensus"
27+
"github.com/ethereum/go-ethereum/consensus/beacon"
28+
"github.com/ethereum/go-ethereum/consensus/ethash"
29+
"github.com/ethereum/go-ethereum/core/rawdb"
30+
"github.com/ethereum/go-ethereum/core/state"
31+
"github.com/ethereum/go-ethereum/core/types"
32+
"github.com/ethereum/go-ethereum/core/vm"
33+
"github.com/ethereum/go-ethereum/crypto"
34+
"github.com/ethereum/go-ethereum/params"
35+
"github.com/ethereum/go-ethereum/triedb"
36+
)
37+
38+
// eip8304ChainContext is a minimal ChainContext for exercising the EIP-8304
39+
// index contract in isolation.
40+
type eip8304ChainContext struct{ cfg *params.ChainConfig }
41+
42+
func (eip8304ChainContext) Engine() consensus.Engine { return beacon.New(ethash.NewFaker()) }
43+
func (c eip8304ChainContext) Config() *params.ChainConfig { return c.cfg }
44+
func (eip8304ChainContext) GetHeader(common.Hash, uint64) *types.Header { return nil }
45+
func (eip8304ChainContext) CurrentHeader() *types.Header { return nil }
46+
func (eip8304ChainContext) GetHeaderByHash(common.Hash) *types.Header { return nil }
47+
func (eip8304ChainContext) GetHeaderByNumber(uint64) *types.Header { return nil }
48+
49+
// newEIP8304ContractState creates a fresh state for exercising the index
50+
// contract in isolation.
51+
func newEIP8304ContractState(t *testing.T) *state.StateDB {
52+
t.Helper()
53+
diskdb := rawdb.NewMemoryDatabase()
54+
tdb := triedb.NewDatabase(diskdb, nil)
55+
statedb, err := state.New(common.Hash{}, state.NewMPTDatabase(tdb, state.NewCodeDB(diskdb)))
56+
if err != nil {
57+
t.Fatal(err)
58+
}
59+
return statedb
60+
}
61+
62+
// newEIP8304ContractEVM creates an EVM over the given state with the given
63+
// block number, which drives the contract's NUMBER-based freshness checks.
64+
func newEIP8304ContractEVM(t *testing.T, statedb *state.StateDB, blockNumber uint64) *vm.EVM {
65+
t.Helper()
66+
header := &types.Header{
67+
Number: new(big.Int).SetUint64(blockNumber),
68+
Time: blockNumber,
69+
GasLimit: 30_000_000,
70+
Coinbase: common.Address{},
71+
BaseFee: big.NewInt(0),
72+
Difficulty: big.NewInt(0),
73+
}
74+
ctx := NewEVMBlockContext(header, eip8304ChainContext{params.MergedTestChainConfig}, &header.Coinbase)
75+
return vm.NewEVM(ctx, statedb, params.MergedTestChainConfig, vm.Config{})
76+
}
77+
78+
// deployEIP8304IndexContract deploys the EIP-8304 index contract from its init
79+
// code and returns the resulting address.
80+
func deployEIP8304IndexContract(t *testing.T, evm *vm.EVM) common.Address {
81+
t.Helper()
82+
_, addr, _, err := evm.Create(crypto.PubkeyToAddress(eip8304TestKey.PublicKey), params.IndexContractInitCode, vm.NewGasBudget(5_000_000, 0), common.U2560)
83+
if err != nil {
84+
t.Fatalf("deploy failed: %v", err)
85+
}
86+
return addr
87+
}
88+
89+
// setCalldata builds the 96-byte set payload: first_block, table_size,
90+
// table_root (all big-endian 32-byte words).
91+
func setCalldata(firstBlock, tableSize uint64, root common.Hash) []byte {
92+
calldata := make([]byte, 96)
93+
binary.BigEndian.PutUint64(calldata[24:32], firstBlock)
94+
binary.BigEndian.PutUint64(calldata[56:64], tableSize)
95+
copy(calldata[64:96], root[:])
96+
return calldata
97+
}
98+
99+
// getCalldata builds the 64-byte get payload: first_block, table_size.
100+
func getCalldata(firstBlock, tableSize uint64) []byte {
101+
calldata := make([]byte, 64)
102+
binary.BigEndian.PutUint64(calldata[24:32], firstBlock)
103+
binary.BigEndian.PutUint64(calldata[56:64], tableSize)
104+
return calldata
105+
}
106+
107+
// indexSlot is the storage slot for a table root per the EIP-8304 spec:
108+
// table_size * TABLES_PER_LEVEL + (first_block / table_size) % TABLES_PER_LEVEL.
109+
func indexSlot(firstBlock, tableSize uint64) common.Hash {
110+
return common.BigToHash(new(big.Int).SetUint64(tableSize*0x400 + (firstBlock/tableSize)%0x400))
111+
}
112+
113+
var eip8304TestKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
114+
115+
// TestEIP8304IndexContract pins the behavior of the EIP-8304 index contract
116+
// against the spec:
117+
//
118+
// - the deployed code must be exactly params.IndexContractCode (guards the
119+
// init-code/runtime consistency, which has regressed before)
120+
// - set: only the system address may write; the root lands at
121+
// table_size*1024 + (first_block/table_size)%1024
122+
// - get: 64-byte calldata returns the stored root; reverts on wrong size or
123+
// a first_block that is not a multiple of table_size
124+
func TestEIP8304IndexContract(t *testing.T) {
125+
t.Run("deployment", func(t *testing.T) {
126+
statedb := newEIP8304ContractState(t)
127+
evm := newEIP8304ContractEVM(t, statedb, 1)
128+
addr := deployEIP8304IndexContract(t, evm)
129+
if got := statedb.GetCode(addr); !bytes.Equal(got, params.IndexContractCode) {
130+
t.Fatalf("deployed code mismatch: got %d bytes, want %d", len(got), len(params.IndexContractCode))
131+
}
132+
})
133+
134+
t.Run("set", func(t *testing.T) {
135+
root := common.HexToHash("0x00000000000000000000000000000000000000000000000000000000deadbeef")
136+
cases := []struct{ fb, ts uint64 }{
137+
{2, 1}, {5, 2}, {8, 4}, {1030, 4}, {1024, 256},
138+
}
139+
for _, c := range cases {
140+
statedb := newEIP8304ContractState(t)
141+
evm := newEIP8304ContractEVM(t, statedb, 1)
142+
addr := deployEIP8304IndexContract(t, evm)
143+
if _, _, err := evm.Call(params.SystemAddress, addr, setCalldata(c.fb, c.ts, root), vm.NewGasBudget(5_000_000, 0), common.U2560); err != nil {
144+
t.Fatalf("set(%d, %d) failed: %v", c.fb, c.ts, err)
145+
}
146+
if got := statedb.GetState(addr, indexSlot(c.fb, c.ts)); got != root {
147+
t.Fatalf("set(%d, %d): slot %s = %x, want %x", c.fb, c.ts, indexSlot(c.fb, c.ts), got, root)
148+
}
149+
}
150+
})
151+
152+
t.Run("set-caller-guard", func(t *testing.T) {
153+
statedb := newEIP8304ContractState(t)
154+
evm := newEIP8304ContractEVM(t, statedb, 1)
155+
addr := deployEIP8304IndexContract(t, evm)
156+
root := common.HexToHash("0xdeadbeef")
157+
sender := crypto.PubkeyToAddress(eip8304TestKey.PublicKey)
158+
if _, _, err := evm.Call(sender, addr, setCalldata(2, 1, root), vm.NewGasBudget(5_000_000, 0), common.U2560); err == nil {
159+
t.Fatal("non-system caller with set calldata: expected revert")
160+
}
161+
if got := statedb.GetState(addr, indexSlot(2, 1)); got != (common.Hash{}) {
162+
t.Fatalf("non-system caller wrote slot %s = %x", indexSlot(2, 1), got)
163+
}
164+
})
165+
166+
t.Run("get", func(t *testing.T) {
167+
root := common.HexToHash("0x00000000000000000000000000000000000000000000000000000000cafebabe")
168+
sender := crypto.PubkeyToAddress(eip8304TestKey.PublicKey)
169+
// The get path's freshness check is one-sided: table (fb, ts) remains
170+
// readable while num < fb + 1025*ts + ts/4 (the ring-buffer overwrite
171+
// window plus the table_size/4 publication delay, with truncating
172+
// division). There is no lower age bound. Write at block 1, then read
173+
// at the boundaries.
174+
for _, c := range []struct {
175+
fb, ts, num uint64
176+
wantErr bool
177+
}{
178+
{fb: 2, ts: 1, num: 100, wantErr: false}, // no lower age bound
179+
{fb: 2, ts: 1, num: 1026, wantErr: false}, // last readable block
180+
{fb: 2, ts: 1, num: 1027, wantErr: true}, // ring window expired
181+
{fb: 8, ts: 4, num: 4108, wantErr: false}, // ts=4 window
182+
{fb: 8, ts: 4, num: 4109, wantErr: true},
183+
} {
184+
statedb := newEIP8304ContractState(t)
185+
evm := newEIP8304ContractEVM(t, statedb, 1)
186+
addr := deployEIP8304IndexContract(t, evm)
187+
if _, _, err := evm.Call(params.SystemAddress, addr, setCalldata(c.fb, c.ts, root), vm.NewGasBudget(5_000_000, 0), common.U2560); err != nil {
188+
t.Fatalf("set(%d, %d) failed: %v", c.fb, c.ts, err)
189+
}
190+
evm = newEIP8304ContractEVM(t, statedb, c.num)
191+
ret, _, err := evm.Call(sender, addr, getCalldata(c.fb, c.ts), vm.NewGasBudget(5_000_000, 0), common.U2560)
192+
if c.wantErr {
193+
if err == nil {
194+
t.Fatalf("get(%d, %d) at block %d: expected revert", c.fb, c.ts, c.num)
195+
}
196+
continue
197+
}
198+
if err != nil {
199+
t.Fatalf("get(%d, %d) at block %d failed: %v", c.fb, c.ts, c.num, err)
200+
}
201+
if got := common.BytesToHash(ret); got != root {
202+
t.Fatalf("get(%d, %d) at block %d returned %x, want %x", c.fb, c.ts, c.num, got, root)
203+
}
204+
}
205+
})
206+
207+
t.Run("get-guards", func(t *testing.T) {
208+
root := common.HexToHash("0xcafebabe")
209+
statedb := newEIP8304ContractState(t)
210+
evm := newEIP8304ContractEVM(t, statedb, 259)
211+
addr := deployEIP8304IndexContract(t, evm)
212+
if _, _, err := evm.Call(params.SystemAddress, addr, setCalldata(2, 1, root), vm.NewGasBudget(5_000_000, 0), common.U2560); err != nil {
213+
t.Fatalf("set failed: %v", err)
214+
}
215+
sender := crypto.PubkeyToAddress(eip8304TestKey.PublicKey)
216+
// Wrong calldata size (32 bytes instead of 64).
217+
if _, _, err := evm.Call(sender, addr, make([]byte, 32), vm.NewGasBudget(5_000_000, 0), common.U2560); err == nil {
218+
t.Fatal("get with 32-byte calldata: expected revert")
219+
}
220+
// first_block not a multiple of table_size.
221+
if _, _, err := evm.Call(sender, addr, getCalldata(3, 2), vm.NewGasBudget(5_000_000, 0), common.U2560); err == nil {
222+
t.Fatal("get(3, 2): expected revert (3 is not a multiple of 2)")
223+
}
224+
})
225+
}

0 commit comments

Comments
 (0)