diff --git a/consensus/merge/merge.go b/consensus/merge/merge.go index c5544ef33bd..823be69a219 100644 --- a/consensus/merge/merge.go +++ b/consensus/merge/merge.go @@ -282,6 +282,9 @@ func (s *Merge) Initialize(config *chain.Config, chain consensus.ChainHeaderRead return syscall(addr, data, state, header, false /* constCall */) }) } + if chain.Config().IsPrague(header.Time) { + misc.StoreBlockHashesEip2935(header, state, config, chain) + } } func (s *Merge) APIs(chain consensus.ChainHeaderReader) []rpc.API { diff --git a/consensus/misc/eip2935.go b/consensus/misc/eip2935.go new file mode 100644 index 00000000000..64d4bef1586 --- /dev/null +++ b/consensus/misc/eip2935.go @@ -0,0 +1,42 @@ +package misc + +import ( + "github.com/holiman/uint256" + + "github.com/ledgerwatch/erigon-lib/chain" + libcommon "github.com/ledgerwatch/erigon-lib/common" + + "github.com/ledgerwatch/erigon/consensus" + "github.com/ledgerwatch/erigon/core/state" + "github.com/ledgerwatch/erigon/core/types" + "github.com/ledgerwatch/erigon/params" +) + +func StoreBlockHashesEip2935(header *types.Header, state *state.IntraBlockState, config *chain.Config, headerReader consensus.ChainHeaderReader) { + headerNum := header.Number.Uint64() + if headerNum == 0 { // Activation of fork at Genesis + return + } + storeHash(headerNum-1, header.ParentHash, state) + // If this is the fork block, add the parent's direct `HISTORY_SERVE_WINDOW - 1` ancestors as well + parent := headerReader.GetHeader(header.ParentHash, headerNum-1) + if parent.Time < config.PragueTime.Uint64() { + p := headerNum - 1 + window := params.BlockHashHistoryServeWindow - 1 + if p < window { + window = p + } + for i := window; i > 0; i-- { + p = p - 1 + storeHash(p, parent.ParentHash, state) + parent = headerReader.GetHeader(parent.ParentHash, p) + } + } +} + +func storeHash(num uint64, hash libcommon.Hash, state *state.IntraBlockState) { + slotNum := num % params.BlockHashHistoryServeWindow + storageSlot := libcommon.BytesToHash(uint256.NewInt(slotNum).Bytes()) + parentHashInt := uint256.NewInt(0).SetBytes32(hash.Bytes()) + state.SetState(params.HistoryStorageAddress, &storageSlot, *parentHashInt) +} diff --git a/core/vm/eips.go b/core/vm/eips.go index 8d48f1a7b33..c05c41006fb 100644 --- a/core/vm/eips.go +++ b/core/vm/eips.go @@ -29,6 +29,7 @@ import ( ) var activators = map[int]func(*JumpTable){ + 2935: enable2935, 7516: enable7516, 6780: enable6780, 5656: enable5656, @@ -327,3 +328,14 @@ func enable7516(jt *JumpTable) { numPush: 1, } } + +// enable2935 applies EIP-2935 (Historical block hashes in state) +func enable2935(jt *JumpTable) { + jt[BLOCKHASH] = &operation{ + execute: opBlockhash2935, + constantGas: GasExtStep, + dynamicGas: gasOpBlockhashEIP2935, + numPop: 1, + numPush: 1, + } +} diff --git a/core/vm/instructions.go b/core/vm/instructions.go index b35de6adee6..21f9bf24d15 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -464,28 +464,60 @@ func opGasprice(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([ return nil, nil } +// opBlockhash executes the BLOCKHASH opcode pre-EIP-2935 func opBlockhash(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { - num := scope.Stack.Peek() - num64, overflow := num.Uint64WithOverflow() + arg := scope.Stack.Peek() + arg64, overflow := arg.Uint64WithOverflow() if overflow { - num.Clear() + arg.Clear() return nil, nil } var upper, lower uint64 upper = interpreter.evm.Context.BlockNumber - if upper < 257 { + if upper <= params.BlockHashOldWindow { lower = 0 } else { - lower = upper - 256 + lower = upper - params.BlockHashOldWindow } - if num64 >= lower && num64 < upper { - num.SetBytes(interpreter.evm.Context.GetHash(num64).Bytes()) + if arg64 >= lower && arg64 < upper { + arg.SetBytes(interpreter.evm.Context.GetHash(arg64).Bytes()) } else { - num.Clear() + arg.Clear() } return nil, nil } +// opBlockhash2935 executes for the BLOCKHASH opcode post EIP-2935 by returning the +// corresponding hash for the blocknumber from the state, if within range. +// The range is defined by [head - params.BlockHashHistoryServeWindow - 1, head - 1] +// This should not be used without activating EIP-2935 +func opBlockhash2935(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { + arg := scope.Stack.Peek() + arg64, overflow := arg.Uint64WithOverflow() + if overflow { + arg.Clear() + return nil, nil + } + + // Check if arg is within allowed window + var upper uint64 + upper = interpreter.evm.Context.BlockNumber + if arg64 >= upper || arg64+params.BlockHashHistoryServeWindow < upper { + arg.Clear() + return nil, nil + } + + // Return state read value from the slot + storageSlot := libcommon.BytesToHash(uint256.NewInt(arg64 % params.BlockHashHistoryServeWindow).Bytes()) + interpreter.evm.intraBlockState.GetState( + params.HistoryStorageAddress, + &storageSlot, + arg, + ) + + return nil, nil +} + func opCoinbase(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { scope.Stack.Push(new(uint256.Int).SetBytes(interpreter.evm.Context.Coinbase.Bytes())) return nil, nil diff --git a/core/vm/jump_table.go b/core/vm/jump_table.go index 806ae494133..82c43dd3167 100644 --- a/core/vm/jump_table.go +++ b/core/vm/jump_table.go @@ -92,6 +92,7 @@ func validateAndFillMaxStack(jt *JumpTable) { // cancun, and prague instructions. func newPragueInstructionSet() JumpTable { instructionSet := newCancunInstructionSet() + enable2935(&instructionSet) validateAndFillMaxStack(&instructionSet) return instructionSet } diff --git a/core/vm/operations_acl.go b/core/vm/operations_acl.go index 6256ae5740b..1e1b68c6995 100644 --- a/core/vm/operations_acl.go +++ b/core/vm/operations_acl.go @@ -235,3 +235,22 @@ func makeSelfdestructGasFn(refundsEnabled bool) gasFunc { } return gasFunc } + +// gasOpBlockhashEIP2935 returns the gas for the new BLOCKHASH operation post EIP-2935 +// If arg is outside of the params.BlockHashHistoryServeWindow, zero dynamic gas is returned +// EIP-2929 Cold/Warm storage read cost is applicable here similar to SLOAD +func gasOpBlockhashEIP2935(evm *EVM, contract *Contract, stack *stack.Stack, mem *Memory, memorySize uint64) (uint64, error) { + arg := stack.Peek() + arg64, overflow := arg.Uint64WithOverflow() + if overflow { + return 0, nil + } + if arg64 >= evm.Context.BlockNumber || arg64+params.BlockHashHistoryServeWindow < evm.Context.BlockNumber { + return 0, nil + } + storageSlot := libcommon.BytesToHash(uint256.NewInt(arg64 % params.BlockHashHistoryServeWindow).Bytes()) + if _, slotMod := evm.IntraBlockState().AddSlotToAccessList(params.HistoryStorageAddress, storageSlot); slotMod { + return params.ColdSloadCostEIP2929, nil + } + return params.WarmStorageReadCostEIP2929, nil +} diff --git a/core/vm/runtime/runtime.go b/core/vm/runtime/runtime.go index 032d1b2e4d9..cec1e7078b1 100644 --- a/core/vm/runtime/runtime.go +++ b/core/vm/runtime/runtime.go @@ -111,8 +111,8 @@ func setDefaults(cfg *Config) { func Execute(code, input []byte, cfg *Config, bn uint64) ([]byte, *state.IntraBlockState, error) { if cfg == nil { cfg = new(Config) + setDefaults(cfg) } - setDefaults(cfg) externalState := cfg.State != nil var tx kv.RwTx diff --git a/core/vm/runtime/runtime_test.go b/core/vm/runtime/runtime_test.go index 1e326eea237..8553064707c 100644 --- a/core/vm/runtime/runtime_test.go +++ b/core/vm/runtime/runtime_test.go @@ -30,12 +30,15 @@ import ( "github.com/ledgerwatch/erigon/accounts/abi" "github.com/ledgerwatch/erigon/common" "github.com/ledgerwatch/erigon/consensus" + "github.com/ledgerwatch/erigon/consensus/misc" "github.com/ledgerwatch/erigon/core" "github.com/ledgerwatch/erigon/core/asm" "github.com/ledgerwatch/erigon/core/state" "github.com/ledgerwatch/erigon/core/types" "github.com/ledgerwatch/erigon/core/vm" "github.com/ledgerwatch/erigon/eth/tracers/logger" + "github.com/ledgerwatch/erigon/params" + "github.com/ledgerwatch/erigon/rlp" ) func TestDefaults(t *testing.T) { @@ -235,7 +238,7 @@ func fakeHeader(n uint64, parentHash libcommon.Hash) *types.Header { Coinbase: libcommon.HexToAddress("0x00000000000000000000000000000000deadbeef"), Number: big.NewInt(int64(n)), ParentHash: parentHash, - Time: 1000, + Time: n, Nonce: types.BlockNonce{0x1}, Extra: []byte{}, Difficulty: big.NewInt(0), @@ -244,6 +247,45 @@ func fakeHeader(n uint64, parentHash libcommon.Hash) *types.Header { return &header } +// FakeChainHeaderReader implements consensus.ChainHeaderReader interface +type FakeChainHeaderReader struct{} + +func (cr *FakeChainHeaderReader) GetHeaderByHash(hash libcommon.Hash) *types.Header { + return nil +} +func (cr *FakeChainHeaderReader) GetHeaderByNumber(number uint64) *types.Header { + return cr.GetHeaderByHash(libcommon.BigToHash(big.NewInt(int64(number)))) +} +func (cr *FakeChainHeaderReader) Config() *chain.Config { return nil } +func (cr *FakeChainHeaderReader) CurrentHeader() *types.Header { return nil } + +// GetHeader returns a fake header with the parentHash equal to the number - 1 +func (cr *FakeChainHeaderReader) GetHeader(hash libcommon.Hash, number uint64) *types.Header { + return &types.Header{ + Coinbase: libcommon.HexToAddress("0x00000000000000000000000000000000deadbeef"), + Number: big.NewInt(int64(number)), + ParentHash: libcommon.BigToHash(big.NewInt(int64(number - 1))), + Time: number, + Nonce: types.BlockNonce{0x1}, + Extra: []byte{}, + Difficulty: big.NewInt(0), + GasLimit: 100000, + } +} +func (cr *FakeChainHeaderReader) GetBlock(hash libcommon.Hash, number uint64) *types.Block { + return nil +} +func (cr *FakeChainHeaderReader) HasBlock(hash libcommon.Hash, number uint64) bool { return false } +func (cr *FakeChainHeaderReader) GetTd(hash libcommon.Hash, number uint64) *big.Int { return nil } +func (cr *FakeChainHeaderReader) FrozenBlocks() uint64 { return 0 } +func (cr *FakeChainHeaderReader) BorEventsByBlock(hash libcommon.Hash, number uint64) []rlp.RawValue { + return nil +} +func (cr *FakeChainHeaderReader) BorStartEventID(hash libcommon.Hash, number uint64) uint64 { + return 0 +} +func (cr *FakeChainHeaderReader) BorSpan(spanId uint64) []byte { return nil } + type dummyChain struct { counter int } @@ -313,10 +355,14 @@ func TestBlockhash(t *testing.T) { // The method call to 'test()' input := libcommon.Hex2Bytes("f8a8fd6d") chain := &dummyChain{} - ret, _, err := Execute(data, input, &Config{ + cfg := &Config{ GetHashFn: core.GetHashFn(header, chain.GetHeader), BlockNumber: new(big.Int).Set(header.Number), - }, header.Number.Uint64()) + Time: new(big.Int), + } + setDefaults(cfg) + cfg.ChainConfig.PragueTime = big.NewInt(1) + ret, _, err := Execute(data, input, cfg, header.Number.Uint64()) if err != nil { t.Fatalf("expected no error, got %v", err) } @@ -341,6 +387,73 @@ func TestBlockhash(t *testing.T) { } } +func TestBlockHashEip2935(t *testing.T) { + t.Parallel() + + // This is the contract we're using. It requests the blockhash for current num (should be all zeroes), We are fetching BlockHash for current block (should be zer0), parent block, last block which is supposed to be there (head - HISTORY_SERVE_WINDOW) and also one block before that (should be zero) + + /* + pragma solidity ^0.8.25; + contract BlockHashTestPrague{ + function test() public view returns (bytes32, bytes32, bytes32, bytes32){ + uint256 head = block.number; + bytes32 zero = blockhash(head); + bytes32 first = blockhash(head-1); + bytes32 last = blockhash(head - 8192); + bytes32 beyond = blockhash(head - 8193); + return (zero, first, last, beyond); + } + } + */ + // The contract above + data := libcommon.Hex2Bytes("608060405234801561000f575f80fd5b5060043610610029575f3560e01c8063f8a8fd6d1461002d575b5f80fd5b61003561004e565b60405161004594939291906100bf565b60405180910390f35b5f805f805f4390505f814090505f6001836100699190610138565b4090505f6120008461007b9190610138565b4090505f6120018561008d9190610138565b409050838383839850985098509850505050505090919293565b5f819050919050565b6100b9816100a7565b82525050565b5f6080820190506100d25f8301876100b0565b6100df60208301866100b0565b6100ec60408301856100b0565b6100f960608301846100b0565b95945050505050565b5f819050919050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f61014282610102565b915061014d83610102565b92508282039050818111156101655761016461010b565b5b9291505056fea2646970667358221220bac67d00c05154c1dca13fe3c1493172d44692d312cb3fd72a3d7457874d595464736f6c63430008190033") + // The method call to 'test()' + input := libcommon.Hex2Bytes("f8a8fd6d") + + // Current head + n := uint64(10000) + parentHash := libcommon.Hash{} + s := common.LeftPadBytes(big.NewInt(int64(n-1)).Bytes(), 32) + copy(parentHash[:], s) + fakeHeaderReader := &FakeChainHeaderReader{} + header := fakeHeaderReader.GetHeader(libcommon.BigToHash(big.NewInt(int64(n))), n) + + chain := &dummyChain{} + cfg := &Config{ + GetHashFn: core.GetHashFn(header, chain.GetHeader), + BlockNumber: new(big.Int).Set(header.Number), + Time: big.NewInt(10000), + } + setDefaults(cfg) + cfg.ChainConfig.PragueTime = big.NewInt(10000) + _, tx := memdb.NewTestTx(t) + cfg.State = state.New(state.NewPlainStateReader(tx)) + cfg.State.CreateAccount(params.HistoryStorageAddress, true) + misc.StoreBlockHashesEip2935(header, cfg.State, cfg.ChainConfig, &FakeChainHeaderReader{}) + + ret, _, err := Execute(data, input, cfg, header.Number.Uint64()) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if len(ret) != 128 { + t.Fatalf("expected returndata to be 128 bytes, got %d", len(ret)) + } + + zero := new(big.Int).SetBytes(ret[0:32]) + first := new(big.Int).SetBytes(ret[32:64]) + last := new(big.Int).SetBytes(ret[64:96]) + beyond := new(big.Int).SetBytes(ret[96:128]) + if zero.Sign() != 0 || beyond.Sign() != 0 { + t.Fatalf("expected zeroes, got %x %x", ret[0:32], ret[96:128]) + } + if first.Uint64() != 9999 { + t.Fatalf("first block should be 9999, got %d (%x)", first, ret[32:64]) + } + if last.Uint64() != 1808 { + t.Fatalf("last block should be 1808, got %d (%x)", last, ret[64:96]) + } +} + // benchmarkNonModifyingCode benchmarks code, but if the code modifies the // state, this should not be used, since it does not reset the state between runs. func benchmarkNonModifyingCode(b *testing.B, gas uint64, code []byte, name string) { //nolint:unparam @@ -521,14 +634,16 @@ func TestEip2929Cases(t *testing.T) { fmt.Printf("%v\n\nBytecode: \n```\n0x%x\n```\nOperations: \n```\n%v\n```\n\n", comment, code, ops) - //nolint:errcheck - Execute(code, nil, &Config{ + cfg := &Config{ EVMConfig: vm.Config{ Debug: true, Tracer: logger.NewMarkdownLogger(nil, os.Stdout), ExtraEips: []int{2929}, }, - }, 0) + } + setDefaults(cfg) + //nolint:errcheck + Execute(code, nil, cfg, 0) } { // First eip testcase diff --git a/params/protocol_params.go b/params/protocol_params.go index d760de8658d..05e4fe52d9f 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -171,11 +171,18 @@ const ( // PIP-27: secp256r1 elliptic curve signature verifier gas price P256VerifyGas uint64 = 3450 + + // EIP-2935: Historical block hashes in state + BlockHashHistoryServeWindow uint64 = 8192 + BlockHashOldWindow uint64 = 256 ) // EIP-4788: Beacon block root in the EVM var BeaconRootsAddress = common.HexToAddress("0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02") +// EIP-2935: Historical block hashes in state +var HistoryStorageAddress = common.HexToAddress("0x25a219378dad9b3503c8268c9ca836a52427a4fb") + // Gas discount table for BLS12-381 G1 and G2 multi exponentiation operations var Bls12381MultiExpDiscountTable = [128]uint64{1200, 888, 764, 641, 594, 547, 500, 453, 438, 423, 408, 394, 379, 364, 349, 334, 330, 326, 322, 318, 314, 310, 306, 302, 298, 294, 289, 285, 281, 277, 273, 269, 268, 266, 265, 263, 262, 260, 259, 257, 256, 254, 253, 251, 250, 248, 247, 245, 244, 242, 241, 239, 238, 236, 235, 233, 232, 231, 229, 228, 226, 225, 223, 222, 221, 220, 219, 219, 218, 217, 216, 216, 215, 214, 213, 213, 212, 211, 211, 210, 209, 208, 208, 207, 206, 205, 205, 204, 203, 202, 202, 201, 200, 199, 199, 198, 197, 196, 196, 195, 194, 193, 193, 192, 191, 191, 190, 189, 188, 188, 187, 186, 185, 185, 184, 183, 182, 182, 181, 180, 179, 179, 178, 177, 176, 176, 175, 174}