Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EIP-2935 Historical block hashes #9991

Merged
merged 18 commits into from
May 9, 2024
3 changes: 3 additions & 0 deletions consensus/merge/merge.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
42 changes: 42 additions & 0 deletions consensus/misc/eip2935.go
Original file line number Diff line number Diff line change
@@ -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 {
Copy link
Member

@yperbasis yperbasis May 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably equivalent, but I find the pseudo-code in the spec easier to read. What I mean that instead of this p < window branch you have a early break in the loop. Something like:

for i := 0; i < params.BlockHashHistoryServeWindow - 1; i++ {
  ancestorNumber := parent.Number.Uint64() - 1
  if ancestorNumber == 0 { // stop at genesis block
    break
  }
  storeHash(big.NewInt(ancestorNumber), parent.ParentHash, state)
  parent = headerReader.GetHeader(parent.ParentHash, ancestorNumber)
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's an optimization to not have to do big.Int to uint64(), or any big.Int operation that many times

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Between optimization and clarity I'd choose clarity until profiling confirms that the performance degradation is material, but OK.

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)
}
12 changes: 12 additions & 0 deletions core/vm/eips.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
)

var activators = map[int]func(*JumpTable){
2935: enable2935,
7516: enable7516,
6780: enable6780,
5656: enable5656,
Expand Down Expand Up @@ -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: 0,
somnathb1 marked this conversation as resolved.
Show resolved Hide resolved
numPush: 1,
}
}
49 changes: 41 additions & 8 deletions core/vm/instructions.go
Original file line number Diff line number Diff line change
Expand Up @@ -464,28 +464,61 @@ 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) {
yperbasis marked this conversation as resolved.
Show resolved Hide resolved
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()
refundAmt := params.ColdSloadCostEIP2929
arg64, overflow := arg.Uint64WithOverflow()
if overflow {
arg.Clear()
} else {
var upper uint64
upper = interpreter.evm.Context.BlockNumber
if arg64 >= upper || arg64+params.BlockHashHistoryServeWindow < upper {
arg.Clear()
} else {
storageSlot := libcommon.BytesToHash(uint256.NewInt(arg64 % params.BlockHashHistoryServeWindow).Bytes())
if _, slotMod := interpreter.evm.IntraBlockState().AddSlotToAccessList(params.HistoryStorageAddress, storageSlot); !slotMod {
refundAmt = params.ColdSloadCostEIP2929 - params.WarmStorageReadCostEIP2929
}
interpreter.evm.intraBlockState.GetState(
params.HistoryStorageAddress,
&storageSlot,
arg,
)
}
}
// The gas func for this charges max (ColdSloadCostEIP2929) gas, refunding the rest here
interpreter.evm.intraBlockState.AddRefund(refundAmt)
yperbasis marked this conversation as resolved.
Show resolved Hide resolved
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
Expand Down
1 change: 1 addition & 0 deletions core/vm/jump_table.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ func validateAndFillMaxStack(jt *JumpTable) {
// cancun, and prague instructions.
func newPragueInstructionSet() JumpTable {
instructionSet := newCancunInstructionSet()
enable2935(&instructionSet)
validateAndFillMaxStack(&instructionSet)
return instructionSet
}
Expand Down
5 changes: 5 additions & 0 deletions core/vm/operations_acl.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,3 +235,8 @@ func makeSelfdestructGasFn(refundsEnabled bool) gasFunc {
}
return gasFunc
}

// gasOpBlockhashEIP2935 returns the max possible gas here, and refund the rest at its execute function
somnathb1 marked this conversation as resolved.
Show resolved Hide resolved
func gasOpBlockhashEIP2935(evm *EVM, contract *Contract, stack *stack.Stack, mem *Memory, memorySize uint64) (uint64, error) {
return params.ColdSloadCostEIP2929, nil
yperbasis marked this conversation as resolved.
Show resolved Hide resolved
}
2 changes: 1 addition & 1 deletion core/vm/runtime/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
127 changes: 121 additions & 6 deletions core/vm/runtime/runtime_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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),
Expand All @@ -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
}
Expand Down Expand Up @@ -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)
}
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions params/protocol_params.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}

Expand Down
Loading