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

Implement EIP-2937 #7

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 161 additions & 0 deletions core/blockchain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2437,6 +2437,167 @@ func TestSideImportPrunedBlocks(t *testing.T) {
}
}

// TestEIP2937Indestructible tests if a contract that is set as
// indestructible can self destruct.
//
// Contract 0xAAAA is set as indestructible, and then tries to self destruct.
// Contract 0xAAAA is unable to self destruct after being set as indestructible.
//
// Contract 0xBBBB is set as indestructible when PC!=0.
// Contract 0xBBBB is able to self destruct since the SETINDESTRUCTIBLE opcode
// must be the first byte of code to be put into effect.
func TestEIP2937Indestructible(t *testing.T) {
var (
aa = common.HexToAddress("0x000000000000000000000000000000000000aaaa")
bb = common.HexToAddress("0x000000000000000000000000000000000000bbbb")

// Generate a canonical chain to act as the main dataset
engine = ethash.NewFaker()
db = rawdb.NewMemoryDatabase()

// A sender who makes transactions, has some funds
key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
address = crypto.PubkeyToAddress(key.PublicKey)
funds = big.NewInt(1000000000)
gspec = &Genesis{
Config: params.TestChainConfig,
Alloc: GenesisAlloc{
address: {Balance: funds},
// The address 0xAAAAA is set as indestructible and tries to self destruct if called
aa: {
// Code calls set indestructible opcode then self destruct opcode
Code: []byte{byte(vm.PC), byte(vm.SETINDESTRUCTIBLE), byte(vm.SELFDESTRUCT)},
Nonce: 1,
Balance: big.NewInt(0),
},
// The address 0xBBBB calls SETINDESTRUCTIBLE when PC!=0 and then tries to self destruct
bb: {
Code: []byte{
byte(vm.PC), // [0]
byte(vm.PUSH1), 0x01, // [0,1] (value)
byte(vm.SETINDESTRUCTIBLE),
byte(vm.SELFDESTRUCT),
},
Balance: big.NewInt(1),
},
},
}
genesis = gspec.MustCommit(db)
)

blocks, _ := GenerateChain(params.TestChainConfig, genesis, engine, db, 1, func(i int, b *BlockGen) {
b.SetCoinbase(common.Address{1})
// One transaction to AAAA
tx, _ := types.SignTx(types.NewTransaction(0, aa,
big.NewInt(0), 50000, big.NewInt(1), nil), types.HomesteadSigner{}, key)
b.AddTx(tx)
})
// Import the canonical chain
diskdb := rawdb.NewMemoryDatabase()
gspec.MustCommit(diskdb)

chain, err := NewBlockChain(diskdb, nil, params.TestChainConfig, engine, vm.Config{}, nil, nil)

if err != nil {
t.Fatalf("failed to create tester chain: %v", err)
}
if n, err := chain.InsertChain(blocks); err != nil {
t.Fatalf("block %d: failed to insert into chain: %v", n, err)
}
st, _ := chain.State()

if !st.Exist(common.HexToAddress("0x000000000000000000000000000000000000aaaa")) {
t.Fatalf("Contract self destructed.")
}

if !st.Exist(common.HexToAddress("0x000000000000000000000000000000000000bbbb")) {
t.Fatalf("Contract is unable to self destruct when SETINDESTRUCTIBLE was not the first byte of code")
}
}

// TestEIP2937IndestructibleDelegateCall tests the delegate call corner case
// where contracts set as indestructible could be destroyed during by the
// contract it delegate calls into.
//
// Contract 0xBBBB is set as indestructible and calls 0xAAAA which
// self destructs. 0xBBBB does not self destruct when delegate calling
// to a contract which does self destruct.
func TestEIP2937IndestructibleDelegatecall(t *testing.T) {
var (
aa = common.HexToAddress("0x000000000000000000000000000000000000aaaa")
bb = common.HexToAddress("0x000000000000000000000000000000000000bbbb")

// Generate a canonical chain to act as the main dataset
engine = ethash.NewFaker()
db = rawdb.NewMemoryDatabase()

// A sender who makes transactions, has some funds
key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
address = crypto.PubkeyToAddress(key.PublicKey)
funds = big.NewInt(1000000000)
gspec = &Genesis{
Config: params.TestChainConfig,
Alloc: GenesisAlloc{
address: {Balance: funds},
// The address 0xAAAAA self destructs if called
aa: {
// Code calls self destruct opcode
Code: []byte{byte(vm.PC), byte(vm.SELFDESTRUCT)},
Nonce: 1,
Balance: big.NewInt(0),
},
// The address 0xBBBB is set as indestructible and uses delegate call
// to contract 0xAAAA
bb: {
Code: []byte{
byte(vm.PC), // [0]
byte(vm.SETINDESTRUCTIBLE),
byte(vm.DUP1), // [0,0]
byte(vm.DUP1), // [0,0,0]
byte(vm.DUP1), // [0,0,0,0]
byte(vm.PUSH1), 0x01, // [0,0,0,0,1] (value)
byte(vm.PUSH2), 0xaa, 0xaa, // [0,0,0,0,1, 0xaaaa]
byte(vm.GAS),
byte(vm.DELEGATECALL),
byte(vm.REVERT),
},
Balance: big.NewInt(1),
},
},
}
genesis = gspec.MustCommit(db)
)

blocks, _ := GenerateChain(params.TestChainConfig, genesis, engine, db, 1, func(i int, b *BlockGen) {
b.SetCoinbase(common.Address{1})
// One transaction to AAAA
tx, _ := types.SignTx(types.NewTransaction(0, aa,
big.NewInt(0), 50000, big.NewInt(1), nil), types.HomesteadSigner{}, key)
b.AddTx(tx)
// One transaction to BBBB
tx, _ = types.SignTx(types.NewTransaction(1, bb,
big.NewInt(0), 100000, big.NewInt(1), nil), types.HomesteadSigner{}, key)
b.AddTx(tx)
})
// Import the canonical chain
diskdb := rawdb.NewMemoryDatabase()
gspec.MustCommit(diskdb)

chain, err := NewBlockChain(diskdb, nil, params.TestChainConfig, engine, vm.Config{}, nil, nil)

if err != nil {
t.Fatalf("failed to create tester chain: %v", err)
}
if n, err := chain.InsertChain(blocks); err != nil {
t.Fatalf("block %d: failed to insert into chain: %v", n, err)
}
st, _ := chain.State()

if !st.Exist(common.HexToAddress("0x000000000000000000000000000000000000bbbb")) {
t.Fatalf("Contract self destructs even with set_indestructible set.")
}
}

// TestDeleteCreateRevert tests a weird state transition corner case that we hit
// while changing the internals of statedb. The workflow is that a contract is
// self destructed, then in a followup transaction (but same block) it's created
Expand Down
29 changes: 27 additions & 2 deletions core/vm/eips.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ var activators = map[int]func(*JumpTable){
1884: enable1884,
1344: enable1344,
2315: enable2315,
2937: enable2937,
}

// EnableEIP enables the given EIP on the config.
Expand All @@ -44,10 +45,13 @@ func EnableEIP(eipNum int, jt *JumpTable) error {
return nil
}

// ValidEip checks if an eip is in the activators table
func ValidEip(eipNum int) bool {
_, ok := activators[eipNum]
return ok
}

// ActivateableEips returns the available activatble eips
func ActivateableEips() []string {
var nums []string
for k := range activators {
Expand Down Expand Up @@ -83,6 +87,18 @@ func opSelfBalance(pc *uint64, interpreter *EVMInterpreter, callContext *callCtx
return nil, nil
}

// enable2937 applies EIP-2937 (SET_INDESTRUCTIBLE Opcode)
// - Adds an opcode that prevents contract from calling SELFDESTRUCT (0xFF)
func enable2937(jt *JumpTable) {
// New opcode
jt[SETINDESTRUCTIBLE] = &operation{
execute: opSetIndestructible,
constantGas: GasQuickStep,
minStack: minStack(0, 1),
maxStack: maxStack(0, 1),
}
}

// enable1344 applies EIP-1344 (ChainID Opcode)
// - Adds an opcode that returns the current chain’s EIP-155 unique identifier
func enable1344(jt *JumpTable) {
Expand All @@ -95,10 +111,19 @@ func enable1344(jt *JumpTable) {
}
}

// opSetIndestructible prevents a contract from calling SELFDESTRUCT
func opSetIndestructible(pc *uint64, interpreter *EVMInterpreter, callContext *callCtx) ([]byte, error) {
// SetIndestructible opcode must be the first byte of code
if *pc == 0 {
interpreter.indestructible = true
}
return nil, nil
}

// opChainID implements CHAINID opcode
func opChainID(pc *uint64, interpreter *EVMInterpreter, callContext *callCtx) ([]byte, error) {
chainId, _ := uint256.FromBig(interpreter.evm.chainConfig.ChainID)
callContext.stack.push(chainId)
chainID, _ := uint256.FromBig(interpreter.evm.chainConfig.ChainID)
callContext.stack.push(chainID)
return nil, nil
}

Expand Down
1 change: 1 addition & 0 deletions core/vm/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ var (
ErrGasUintOverflow = errors.New("gas uint64 overflow")
ErrInvalidRetsub = errors.New("invalid retsub")
ErrReturnStackExceeded = errors.New("return stack limit reached")
ErrContractIndestructible = errors.New("contract is indestructible")
)

// ErrStackUnderflow wraps an evm error when the items on the stack less
Expand Down
14 changes: 7 additions & 7 deletions core/vm/evm.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func (evm *EVM) precompile(addr common.Address) (PrecompiledContract, bool) {
}

// run runs the given contract and takes care of running precompiles with a fallback to the byte code interpreter.
func run(evm *EVM, contract *Contract, input []byte, readOnly bool) ([]byte, error) {
func run(evm *EVM, contract *Contract, input []byte, readOnly bool, indestructible bool) ([]byte, error) {
for _, interpreter := range evm.interpreters {
if interpreter.CanRun(contract.Code) {
if evm.interpreter != interpreter {
Expand All @@ -85,7 +85,7 @@ func run(evm *EVM, contract *Contract, input []byte, readOnly bool) ([]byte, err
}(evm.interpreter)
evm.interpreter = interpreter
}
return interpreter.Run(contract, input, readOnly)
return interpreter.Run(contract, input, readOnly, indestructible)
}
}
return nil, errors.New("no compatible interpreter")
Expand Down Expand Up @@ -270,7 +270,7 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas
// The depth-check is already done, and precompiles handled above
contract := NewContract(caller, AccountRef(addrCopy), value, gas)
contract.SetCallCode(&addrCopy, evm.StateDB.GetCodeHash(addrCopy), code)
ret, err = run(evm, contract, input, false)
ret, err = run(evm, contract, input, false, false)
Copy link

Choose a reason for hiding this comment

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

I think this highlights the commens I made on FEM about the EIP: it's far too vaguely specified. In this case, it appears that executing this sequence:

  • A calls B.
  • B does set-indesructible as the first op.
  • B calls on C
  • C does selfdestruct, but is prevented, because the flag was stickified from B.

This implementation seems based on the static mode, which is meant to be 'sticky' -- following along any child scopes.
I don't interpret the EIP to have the same semantics, though

gas = contract.Gas
}
}
Expand Down Expand Up @@ -322,7 +322,7 @@ func (evm *EVM) CallCode(caller ContractRef, addr common.Address, input []byte,
// The contract is a scoped environment for this execution context only.
contract := NewContract(caller, AccountRef(caller.Address()), value, gas)
contract.SetCallCode(&addrCopy, evm.StateDB.GetCodeHash(addrCopy), evm.StateDB.GetCode(addrCopy))
ret, err = run(evm, contract, input, false)
ret, err = run(evm, contract, input, false, false)
gas = contract.Gas
}
if err != nil {
Expand Down Expand Up @@ -357,7 +357,7 @@ func (evm *EVM) DelegateCall(caller ContractRef, addr common.Address, input []by
// Initialise a new contract and make initialise the delegate values
contract := NewContract(caller, AccountRef(caller.Address()), nil, gas).AsDelegate()
contract.SetCallCode(&addrCopy, evm.StateDB.GetCodeHash(addrCopy), evm.StateDB.GetCode(addrCopy))
ret, err = run(evm, contract, input, false)
ret, err = run(evm, contract, input, false, false)
gas = contract.Gas
}
if err != nil {
Expand Down Expand Up @@ -408,7 +408,7 @@ func (evm *EVM) StaticCall(caller ContractRef, addr common.Address, input []byte
// When an error was returned by the EVM or when setting the creation code
// above we revert to the snapshot and consume any gas remaining. Additionally
// when we're in Homestead this also counts for code storage gas errors.
ret, err = run(evm, contract, input, true)
ret, err = run(evm, contract, input, true, false)
gas = contract.Gas
}
if err != nil {
Expand Down Expand Up @@ -476,7 +476,7 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64,
}
start := time.Now()

ret, err := run(evm, contract, nil, false)
ret, err := run(evm, contract, nil, false, false)

// check whether the max code size has been exceeded
maxCodeSizeExceeded := evm.chainRules.IsEIP158 && len(ret) > params.MaxCodeSize
Expand Down
7 changes: 5 additions & 2 deletions core/vm/instructions.go
Original file line number Diff line number Diff line change
Expand Up @@ -815,9 +815,12 @@ func opStop(pc *uint64, interpreter *EVMInterpreter, callContext *callCtx) ([]by
}

func opSuicide(pc *uint64, interpreter *EVMInterpreter, callContext *callCtx) ([]byte, error) {
if interpreter.indestructible {
return nil, ErrContractIndestructible
}
beneficiary := callContext.stack.pop()
balance := interpreter.evm.StateDB.GetBalance(callContext.contract.Address())
interpreter.evm.StateDB.AddBalance(beneficiary.Bytes20(), balance)
interpreter.evm.StateDB.AddBalance(common.Address(beneficiary.Bytes20()), balance)
interpreter.evm.StateDB.Suicide(callContext.contract.Address())
return nil, nil
}
Expand Down Expand Up @@ -855,7 +858,7 @@ func opPush1(pc *uint64, interpreter *EVMInterpreter, callContext *callCtx) ([]b
codeLen = uint64(len(callContext.contract.Code))
integer = new(uint256.Int)
)
*pc += 1
*pc++
if *pc < codeLen {
callContext.stack.push(integer.SetUint64(uint64(callContext.contract.Code[*pc])))
} else {
Expand Down
17 changes: 13 additions & 4 deletions core/vm/interpreter.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ type Config struct {
type Interpreter interface {
// Run loops and evaluates the contract's code with the given input data and returns
// the return byte-slice and an error if one occurred.
Run(contract *Contract, input []byte, static bool) ([]byte, error)
Run(contract *Contract, input []byte, static bool, indestructible bool) ([]byte, error)
// CanRun tells if the contract, passed as an argument, can be
// run by the current interpreter. This is meant so that the
// caller can do something like:
Expand Down Expand Up @@ -87,8 +87,9 @@ type EVMInterpreter struct {
hasher keccakState // Keccak256 hasher instance shared across opcodes
hasherBuf common.Hash // Keccak256 hasher result array shared aross opcodes

readOnly bool // Whether to throw on stateful modifications
returnData []byte // Last CALL's return data for subsequent reuse
readOnly bool // Whether to throw on stateful modifications
indestructible bool // Whether contract can call opcode SELFDESTRUCT (0xFF)
returnData []byte // Last CALL's return data for subsequent reuse
}

// NewEVMInterpreter returns a new instance of the Interpreter.
Expand Down Expand Up @@ -138,7 +139,7 @@ func NewEVMInterpreter(evm *EVM, cfg Config) *EVMInterpreter {
// It's important to note that any errors returned by the interpreter should be
// considered a revert-and-consume-all-gas operation except for
// ErrExecutionReverted which means revert-and-keep-gas-left.
func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) (ret []byte, err error) {
func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool, indestructible bool) (ret []byte, err error) {

// Increment the call depth which is restricted to 1024
in.evm.depth++
Expand All @@ -151,6 +152,13 @@ func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) (
defer func() { in.readOnly = false }()
}

// Make sure indestructible is only set if we aren't in indestructible yet.
// This makes also sure that the indestructible flag isn't removed for child calls.
if indestructible && !in.indestructible {
in.indestructible = true
defer func() { in.indestructible = false }()
}

// Reset the previous call's return data. It's unimportant to preserve the old buffer
// as every returning call will return new data anyway.
in.returnData = nil
Expand Down Expand Up @@ -241,6 +249,7 @@ func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) (
return nil, ErrWriteProtection
}
}

// Static portion of gas
cost = operation.constantGas // For tracing
if !contract.UseGas(operation.constantGas) {
Expand Down
3 changes: 2 additions & 1 deletion core/vm/jump_table.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ type JumpTable [256]*operation
func newYoloV2InstructionSet() JumpTable {
instructionSet := newIstanbulInstructionSet()
enable2315(&instructionSet) // Subroutines - https://eips.ethereum.org/EIPS/eip-2315
enable2929(&instructionSet) // Access lists for trie accesses https://eips.ethereum.org/EIPS/eip-2929
enable2929(&instructionSet) // Access lists for trie accesses - https://eips.ethereum.org/EIPS/eip-2929
enable2937(&instructionSet) // SET_INDESTRUCTIBLE opcode - https://eips.ethereum.org/EIPS/eip-2937
return instructionSet
}

Expand Down
Loading