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