From 1fa49ea932f476ca0a6d3a1b8a5e5653bb124db0 Mon Sep 17 00:00:00 2001 From: Andrei Maiboroda Date: Tue, 11 May 2021 19:32:52 +0200 Subject: [PATCH 01/16] core/vm: Add EIP-3540 to supported extra EIPs --- core/vm/eips.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/vm/eips.go b/core/vm/eips.go index 93f5c399a668..248a4daab75b 100644 --- a/core/vm/eips.go +++ b/core/vm/eips.go @@ -32,6 +32,7 @@ var activators = map[int]func(*JumpTable){ 2200: enable2200, 1884: enable1884, 1344: enable1344, + 3540: enable3540, } // EnableEIP enables the given EIP on the config. @@ -192,3 +193,7 @@ func opPush0(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]by scope.Stack.push(new(uint256.Int)) return nil, nil } + +func enable3540(jt *JumpTable) { + // Do nothing. +} From 00b1256794e9381adc3afd2739d7df011b8674c2 Mon Sep 17 00:00:00 2001 From: Andrei Maiboroda Date: Tue, 17 May 2022 15:39:01 +0200 Subject: [PATCH 02/16] core/vm: Add validation of deployed code format according to EIP-3540 --- core/vm/eof.go | 124 ++++++++++++++++++++++++++++++++++++++++++++++ core/vm/errors.go | 18 ++++++- core/vm/evm.go | 27 ++++++++-- 3 files changed, 165 insertions(+), 4 deletions(-) create mode 100644 core/vm/eof.go diff --git a/core/vm/eof.go b/core/vm/eof.go new file mode 100644 index 000000000000..fa4d3a3edd35 --- /dev/null +++ b/core/vm/eof.go @@ -0,0 +1,124 @@ +// Copyright 2021 The go-ethereum Authors +// This file is part of go-ethereum. +// +// go-ethereum is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// go-ethereum is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with go-ethereum. If not, see . + +package vm + +import ( + "bytes" + "encoding/binary" +) + +const ( + eofFormatByte byte = 0xEF + eofMagicLen int = 2 + eof1Version byte = 1 + sectionKindTerminator byte = 0 + sectionKindCode byte = 1 + sectionKindData byte = 2 +) + +type EOF1Header struct { + codeSize uint16 // Size of code section. Cannot be 0 for EOF1 code. Equals 0 for legacy code. + dataSize uint16 // Size of data section. Equals 0 if data section is absent in EOF1 code. Equals 0 for legacy code. +} + +func getEofMagic() []byte { + return []byte{0xEF, 0x00} +} + +// hasFormatByte returns true if code starts with 0xEF byte +func hasFormatByte(code []byte) bool { + return len(code) != 0 && code[0] == eofFormatByte +} + +// hasEOFMagic returns true if code starts with magic defined by EIP-3540 +func hasEOFMagic(code []byte) bool { + return len(code) >= eofMagicLen && bytes.Equal(getEofMagic(), code[0:eofMagicLen]) +} + +// readEOF1Header parses EOF1-formatted code header +func readEOF1Header(code []byte) (EOF1Header, error) { + codeLen := len(code) + + i := eofMagicLen + if i >= codeLen || code[i] != eof1Version { + return EOF1Header{}, ErrEOF1InvalidVersion + } + i += 1 + + var header EOF1Header +sectionLoop: + for i < codeLen { + sectionKind := code[i] + i += 1 + switch sectionKind { + case sectionKindTerminator: + break sectionLoop + case sectionKindCode: + // Only 1 code section is allowed. + if header.codeSize != 0 { + return EOF1Header{}, ErrEOF1MultipleCodeSections + } + // Code section size must be present. + if i+2 > codeLen { + return EOF1Header{}, ErrEOF1CodeSectionSizeMissing + } + header.codeSize = binary.BigEndian.Uint16(code[i : i+2]) + // Code section size must not be 0. + if header.codeSize == 0 { + return EOF1Header{}, ErrEOF1EmptyCodeSection + } + i += 2 + case sectionKindData: + // Data section is allowed only after code section. + if header.codeSize == 0 { + return EOF1Header{}, ErrEOF1DataSectionBeforeCodeSection + } + // Only 1 data section is allowed. + if header.dataSize != 0 { + return EOF1Header{}, ErrEOF1MultipleDataSections + } + // Data section size must be present. + if i+2 > codeLen { + return EOF1Header{}, ErrEOF1DataSectionSizeMissing + } + header.dataSize = binary.BigEndian.Uint16(code[i : i+2]) + // Data section size must not be 0. + if header.dataSize == 0 { + return EOF1Header{}, ErrEOF1EmptyDataSection + } + i += 2 + default: + return EOF1Header{}, ErrEOF1UnknownSection + } + } + // 1 code section is required. + if header.codeSize == 0 { + return EOF1Header{}, ErrEOF1CodeSectionMissing + } + // Declared section sizes must correspond to real size (trailing bytes are not allowed.) + if i+int(header.codeSize)+int(header.dataSize) != codeLen { + return EOF1Header{}, ErrEOF1InvalidTotalSize + } + + return header, nil +} + +// validateEOF returns true if code has valid format +func validateEOF(code []byte) bool { + _, err := readEOF1Header(code) + return err == nil +} diff --git a/core/vm/errors.go b/core/vm/errors.go index 004f8ef1c83c..54d8e4f1f306 100644 --- a/core/vm/errors.go +++ b/core/vm/errors.go @@ -34,14 +34,30 @@ var ( ErrWriteProtection = errors.New("write protection") ErrReturnDataOutOfBounds = errors.New("return data out of bounds") ErrGasUintOverflow = errors.New("gas uint64 overflow") - ErrInvalidCode = errors.New("invalid code: must not begin with 0xef") ErrNonceUintOverflow = errors.New("nonce uint64 overflow") + ErrInvalidCode = errors.New("invalid code: must not begin with 0xef") + ErrInvalidCodeFormat = errors.New("invalid code: format validation failed") // errStopToken is an internal token indicating interpreter loop termination, // never returned to outside callers. errStopToken = errors.New("stop token") ) +// EOF1 validation errors +var ( + ErrEOF1InvalidVersion = errors.New("invalid version byte") + ErrEOF1MultipleCodeSections = errors.New("multiple code sections") + ErrEOF1CodeSectionSizeMissing = errors.New("can't read code section size") + ErrEOF1EmptyCodeSection = errors.New("code section size is 0") + ErrEOF1DataSectionBeforeCodeSection = errors.New("data section before code section") + ErrEOF1MultipleDataSections = errors.New("multiple data sections") + ErrEOF1DataSectionSizeMissing = errors.New("can't read data section size") + ErrEOF1EmptyDataSection = errors.New("data section size is 0") + ErrEOF1UnknownSection = errors.New("unknown section id") + ErrEOF1CodeSectionMissing = errors.New("no code section") + ErrEOF1InvalidTotalSize = errors.New("invalid total size") +) + // ErrStackUnderflow wraps an evm error when the items on the stack less // than the minimal requirement. type ErrStackUnderflow struct { diff --git a/core/vm/evm.go b/core/vm/evm.go index 888f4812a590..6eb003d02814 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -402,6 +402,15 @@ func (c *codeAndHash) Hash() common.Hash { return c.hash } +func hasEIP3540(vmConfig *Config) bool { + for _, eip := range vmConfig.ExtraEips { + if eip == 3540 { + return true + } + } + return false +} + // create creates a new contract using code as deployment code. func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, value *big.Int, address common.Address, typ OpCode) ([]byte, common.Address, uint64, error) { // Depth check execution. Fail if we're trying to execute above the @@ -457,9 +466,21 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, err = ErrMaxCodeSizeExceeded } - // Reject code starting with 0xEF if EIP-3541 is enabled. - if err == nil && len(ret) >= 1 && ret[0] == 0xEF && evm.chainRules.IsLondon { - err = ErrInvalidCode + if err == nil && hasFormatByte(ret) { + if hasEIP3540(&evm.Config) { + // Allow only valid EOF1 if EIP-3540 is enabled. + if hasEOFMagic(ret) { + if !validateEOF(ret) { + err = ErrInvalidCodeFormat + } + } else { + // Reject non-EOF code starting with 0xEF. + err = ErrInvalidCode + } + } else if evm.chainRules.IsLondon { + // Reject code starting with 0xEF in London. + err = ErrInvalidCode + } } // if the contract creation ran successfully and no errors were returned From 8710a780dbe184cba4123948df65f78a9f5744d5 Mon Sep 17 00:00:00 2001 From: Andrei Maiboroda Date: Mon, 16 May 2022 12:36:00 +0200 Subject: [PATCH 03/16] core/vm: Add validation of init code according to EIP-3540 --- core/vm/evm.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/vm/evm.go b/core/vm/evm.go index 6eb003d02814..fc9caf7518ea 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -436,6 +436,10 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, if evm.StateDB.GetNonce(address) != 0 || (contractHash != (common.Hash{}) && contractHash != emptyCodeHash) { return nil, common.Address{}, 0, ErrContractAddressCollision } + // Validate code if it claims to be EOF-formatted. + if hasEIP3540(&evm.Config) && hasEOFMagic(codeAndHash.code) && !validateEOF(codeAndHash.code) { + return nil, common.Address{}, gas, ErrInvalidCodeFormat + } // Create a new account on the state snapshot := evm.StateDB.Snapshot() evm.StateDB.CreateAccount(address) From bbe63e3db6ec1f0aa38ba15f514f47a48c77e78b Mon Sep 17 00:00:00 2001 From: Andrei Maiboroda Date: Thu, 29 Sep 2022 12:21:39 +0200 Subject: [PATCH 04/16] core/vm: EOF1 validation unit tests --- core/vm/eof_test.go | 144 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 core/vm/eof_test.go diff --git a/core/vm/eof_test.go b/core/vm/eof_test.go new file mode 100644 index 000000000000..311fb2594296 --- /dev/null +++ b/core/vm/eof_test.go @@ -0,0 +1,144 @@ +// Copyright 2021 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package vm + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" +) + +type eof1Test struct { + code string + codeSize uint16 + dataSize uint16 +} + +var eof1ValidTests = []eof1Test{ + {"EF00010100010000", 1, 0}, + {"EF0001010002006000", 2, 0}, + {"EF0001010002020001006000AA", 2, 1}, + {"EF0001010002020004006000AABBCCDD", 2, 4}, + {"EF00010100040200020060006001AABB", 4, 2}, + {"EF000101000602000400600060016002AABBCCDD", 6, 4}, +} + +type eof1InvalidTest struct { + code string + error string +} + +// Codes starting with something else other than magic +var notEOFTests = []string{ + // valid: "EF0001010002020004006000AABBCCDD", + "", + "FE", // invalid first byte + "FE0001010002020004006000AABBCCDD", // valid except first byte of magic + "EF", // incomplete magic + "EF01", // not correct magic + "EF0101010002020004006000AABBCCDD", // valid except second byte of magic +} + +// Codes starting with magic, but the rest is invalid +var eof1InvalidTests = []eof1InvalidTest{ + // valid: {"EF0001010002020004006000AABBCCDD", nil}, + {"EF00", ErrEOF1InvalidVersion.Error()}, // no version + {"EF0000", ErrEOF1InvalidVersion.Error()}, // invalid version + {"EF0002", ErrEOF1InvalidVersion.Error()}, // invalid version + {"EF0000010002020004006000AABBCCDD", ErrEOF1InvalidVersion.Error()}, // valid except version + {"EF0001", ErrEOF1CodeSectionMissing.Error()}, // no header + {"EF000100", ErrEOF1CodeSectionMissing.Error()}, // no code section + {"EF000101", ErrEOF1CodeSectionSizeMissing.Error()}, // no code section size + {"EF00010100", ErrEOF1CodeSectionSizeMissing.Error()}, // code section size incomplete + {"EF0001010002", ErrEOF1InvalidTotalSize.Error()}, // no section terminator + {"EF000101000200", ErrEOF1InvalidTotalSize.Error()}, // no code section contents + {"EF00010100020060", ErrEOF1InvalidTotalSize.Error()}, // not complete code section contents + {"EF0001010002006000DEADBEEF", ErrEOF1InvalidTotalSize.Error()}, // trailing bytes after code + {"EF00010100020100020060006000", ErrEOF1MultipleCodeSections.Error()}, // two code sections + {"EF000101000000", ErrEOF1EmptyCodeSection.Error()}, // 0 size code section + {"EF000101000002000200AABB", ErrEOF1EmptyCodeSection.Error()}, // 0 size code section, with non-0 data section + {"EF000102000401000200AABBCCDD6000", ErrEOF1DataSectionBeforeCodeSection.Error()}, // data section before code section + {"EF0001020004AABBCCDD", ErrEOF1DataSectionBeforeCodeSection.Error()}, // data section without code section + {"EF000101000202", ErrEOF1DataSectionSizeMissing.Error()}, // no data section size + {"EF00010100020200", ErrEOF1DataSectionSizeMissing.Error()}, // data section size incomplete + {"EF0001010002020004", ErrEOF1InvalidTotalSize.Error()}, // no section terminator + {"EF0001010002020004006000", ErrEOF1InvalidTotalSize.Error()}, // no data section contents + {"EF0001010002020004006000AABBCC", ErrEOF1InvalidTotalSize.Error()}, // not complete data section contents + {"EF0001010002020004006000AABBCCDDEE", ErrEOF1InvalidTotalSize.Error()}, // trailing bytes after data + {"EF0001010002020000006000", ErrEOF1EmptyDataSection.Error()}, // 0 size data section + {"EF0001010002020004020004006000AABBCCDDAABBCCDD", ErrEOF1MultipleDataSections.Error()}, // two data sections + {"EF0001010002030004006000AABBCCDD", ErrEOF1UnknownSection.Error()}, // section id = 3 +} + +func TestHasEOFMagic(t *testing.T) { + for _, test := range notEOFTests { + if hasEOFMagic(common.Hex2Bytes(test)) { + t.Errorf("code %v expected to be not EOF", test) + } + } + + for _, test := range eof1ValidTests { + if !hasEOFMagic(common.Hex2Bytes(test.code)) { + t.Errorf("code %v expected to be EOF", test.code) + } + } + + // invalid but still EOF + for _, test := range eof1InvalidTests { + if !hasEOFMagic(common.Hex2Bytes(test.code)) { + t.Errorf("code %v expected to be EOF", test.code) + } + } +} + +func TestReadEOF1Header(t *testing.T) { + for _, test := range eof1ValidTests { + header, err := readEOF1Header(common.Hex2Bytes(test.code)) + if err != nil { + t.Errorf("code %v validation failure, error: %v", test.code, err) + } + if header.codeSize != test.codeSize { + t.Errorf("code %v codeSize expected %v, got %v", test.code, test.codeSize, header.codeSize) + } + if header.dataSize != test.dataSize { + t.Errorf("code %v dataSize expected %v, got %v", test.code, test.dataSize, header.dataSize) + } + } + + for _, test := range eof1InvalidTests { + _, err := readEOF1Header(common.Hex2Bytes(test.code)) + if err == nil { + t.Errorf("code %v expected to be invalid", test.code) + } else if err.Error() != test.error { + t.Errorf("code %v expected error: \"%v\" got error: \"%v\"", test.code, test.error, err.Error()) + } + } +} + +func TestValidateEOF(t *testing.T) { + for _, test := range eof1ValidTests { + if !validateEOF(common.Hex2Bytes(test.code)) { + t.Errorf("code %v expected to be valid", test.code) + } + } + + for _, test := range eof1InvalidTests { + if validateEOF(common.Hex2Bytes(test.code)) { + t.Errorf("code %v expected to be invalid", test.code) + } + } +} From 0ed7b044fabf798d1c75913c02d39f32e2961aa1 Mon Sep 17 00:00:00 2001 From: Andrei Maiboroda Date: Thu, 29 Sep 2022 12:22:50 +0200 Subject: [PATCH 05/16] core/vm: Make PC relative to code section for EOF1 code --- core/vm/contract.go | 36 ++++++++++++++++++++++++++--- core/vm/eof.go | 27 ++++++++++++++++++++++ core/vm/eof_test.go | 12 ++++++++++ core/vm/evm.go | 51 ++++++++++++++++++++++++++++++++++------- core/vm/instructions.go | 18 ++++++++------- core/vm/interpreter.go | 2 +- 6 files changed, 126 insertions(+), 20 deletions(-) diff --git a/core/vm/contract.go b/core/vm/contract.go index bb0902969ec7..5fd9885db88f 100644 --- a/core/vm/contract.go +++ b/core/vm/contract.go @@ -50,6 +50,8 @@ type Contract struct { caller ContractRef self ContractRef + header EOF1Header + jumpdests map[common.Hash]bitvec // Aggregated result of JUMPDEST analysis. analysis bitvec // Locally cached result of JUMPDEST analysis @@ -143,7 +145,7 @@ func (c *Contract) AsDelegate() *Contract { // GetOp returns the n'th element in the contract's byte array func (c *Contract) GetOp(n uint64) OpCode { - if n < uint64(len(c.Code)) { + if n < c.CodeEndOffset() { return OpCode(c.Code[n]) } @@ -177,18 +179,46 @@ func (c *Contract) Value() *big.Int { return c.value } +// IsLegacy returns true if contract is not EOF +func (c *Contract) IsLegacy() bool { + // EOF1 doesn't allow contracts without code section + return c.header.codeSize == 0 +} + +// CodeBeginOffset returns starting offset of the code section +func (c *Contract) CodeBeginOffset() uint64 { + if c.IsLegacy() { + return 0 + } + return c.header.CodeBeginOffset() +} + +// CodeEndOffset returns offset of the code section end +func (c *Contract) CodeEndOffset() uint64 { + if c.IsLegacy() { + return uint64(len(c.Code)) + } + return c.header.CodeEndOffset() +} + // SetCallCode sets the code of the contract and address of the backing data // object -func (c *Contract) SetCallCode(addr *common.Address, hash common.Hash, code []byte) { +func (c *Contract) SetCallCode(addr *common.Address, hash common.Hash, code []byte, header *EOF1Header) { c.Code = code c.CodeHash = hash c.CodeAddr = addr + + c.header.codeSize = header.codeSize + c.header.dataSize = header.dataSize } // SetCodeOptionalHash can be used to provide code, but it's optional to provide hash. // In case hash is not provided, the jumpdest analysis will not be saved to the parent context -func (c *Contract) SetCodeOptionalHash(addr *common.Address, codeAndHash *codeAndHash) { +func (c *Contract) SetCodeOptionalHash(addr *common.Address, codeAndHash *codeAndHash, header *EOF1Header) { c.Code = codeAndHash.code c.CodeHash = codeAndHash.hash c.CodeAddr = addr + + c.header.codeSize = header.codeSize + c.header.dataSize = header.dataSize } diff --git a/core/vm/eof.go b/core/vm/eof.go index fa4d3a3edd35..d76188fc7f4a 100644 --- a/core/vm/eof.go +++ b/core/vm/eof.go @@ -122,3 +122,30 @@ func validateEOF(code []byte) bool { _, err := readEOF1Header(code) return err == nil } + +// readValidEOF1Header parses EOF1-formatted code header, assuming that it is already validated +func readValidEOF1Header(code []byte) EOF1Header { + var header EOF1Header + codeSizeOffset := 2 + eofMagicLen + header.codeSize = binary.BigEndian.Uint16(code[codeSizeOffset : codeSizeOffset+2]) + if code[codeSizeOffset+2] == 2 { + dataSizeOffset := codeSizeOffset + 3 + header.dataSize = binary.BigEndian.Uint16(code[dataSizeOffset : dataSizeOffset+2]) + } + return header +} + +// CodeBeginOffset returns starting offset of the code section +func (header *EOF1Header) CodeBeginOffset() uint64 { + if header.dataSize == 0 { + // len(magic) + version + code_section_id + code_section_size + terminator + return uint64(5 + eofMagicLen) + } + // len(magic) + version + code_section_id + code_section_size + data_section_id + data_section_size + terminator + return uint64(8 + eofMagicLen) +} + +// CodeEndOffset returns offset of the code section end +func (header *EOF1Header) CodeEndOffset() uint64 { + return header.CodeBeginOffset() + uint64(header.codeSize) +} diff --git a/core/vm/eof_test.go b/core/vm/eof_test.go index 311fb2594296..c13970f6ac1a 100644 --- a/core/vm/eof_test.go +++ b/core/vm/eof_test.go @@ -142,3 +142,15 @@ func TestValidateEOF(t *testing.T) { } } } + +func TestReadValidEOF1Header(t *testing.T) { + for _, test := range eof1ValidTests { + header := readValidEOF1Header(common.Hex2Bytes(test.code)) + if header.codeSize != test.codeSize { + t.Errorf("code %v codeSize expected %v, got %v", test.code, test.codeSize, header.codeSize) + } + if header.dataSize != test.dataSize { + t.Errorf("code %v dataSize expected %v, got %v", test.code, test.dataSize, header.dataSize) + } + } +} diff --git a/core/vm/evm.go b/core/vm/evm.go index fc9caf7518ea..26e830ed4c8a 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -221,10 +221,16 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas ret, err = nil, nil // gas is unchanged } else { addrCopy := addr + + var header EOF1Header + if hasEIP3540(&evm.Config) && hasEOFMagic(code) { + header = readValidEOF1Header(code) + } + // If the account has no code, we can abort here // 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) + contract.SetCallCode(&addrCopy, evm.StateDB.GetCodeHash(addrCopy), code, &header) ret, err = evm.interpreter.Run(contract, input, false) gas = contract.Gas } @@ -278,10 +284,18 @@ func (evm *EVM) CallCode(caller ContractRef, addr common.Address, input []byte, ret, gas, err = RunPrecompiledContract(p, input, gas) } else { addrCopy := addr + + code := evm.StateDB.GetCode(addrCopy) + + var header EOF1Header + if hasEIP3540(&evm.Config) && hasEOFMagic(code) { + header = readValidEOF1Header(code) + } + // Initialise a new contract and set the code that is to be used by the EVM. // 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)) + contract.SetCallCode(&addrCopy, evm.StateDB.GetCodeHash(addrCopy), code, &header) ret, err = evm.interpreter.Run(contract, input, false) gas = contract.Gas } @@ -319,9 +333,17 @@ func (evm *EVM) DelegateCall(caller ContractRef, addr common.Address, input []by ret, gas, err = RunPrecompiledContract(p, input, gas) } else { addrCopy := addr + + code := evm.StateDB.GetCode(addrCopy) + + var header EOF1Header + if hasEIP3540(&evm.Config) && hasEOFMagic(code) { + header = readValidEOF1Header(code) + } + // 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)) + contract.SetCallCode(&addrCopy, evm.StateDB.GetCodeHash(addrCopy), code, &header) ret, err = evm.interpreter.Run(contract, input, false) gas = contract.Gas } @@ -371,10 +393,18 @@ func (evm *EVM) StaticCall(caller ContractRef, addr common.Address, input []byte // leak the 'contract' to the outer scope, and make allocation for 'contract' // even if the actual execution ends on RunPrecompiled above. addrCopy := addr + + code := evm.StateDB.GetCode(addrCopy) + + var header EOF1Header + if hasEIP3540(&evm.Config) && hasEOFMagic(code) { + header = readValidEOF1Header(code) + } + // Initialise a new contract and set the code that is to be used by the EVM. // The contract is a scoped environment for this execution context only. contract := NewContract(caller, AccountRef(addrCopy), new(big.Int), gas) - contract.SetCallCode(&addrCopy, evm.StateDB.GetCodeHash(addrCopy), evm.StateDB.GetCode(addrCopy)) + contract.SetCallCode(&addrCopy, evm.StateDB.GetCodeHash(addrCopy), code, &header) // 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. @@ -436,9 +466,14 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, if evm.StateDB.GetNonce(address) != 0 || (contractHash != (common.Hash{}) && contractHash != emptyCodeHash) { return nil, common.Address{}, 0, ErrContractAddressCollision } - // Validate code if it claims to be EOF-formatted. - if hasEIP3540(&evm.Config) && hasEOFMagic(codeAndHash.code) && !validateEOF(codeAndHash.code) { - return nil, common.Address{}, gas, ErrInvalidCodeFormat + // Try to read code header if it claims to be EOF-formatted. + var header EOF1Header + if hasEIP3540(&evm.Config) && hasEOFMagic(codeAndHash.code) { + var err error + header, err = readEOF1Header(codeAndHash.code) + if err != nil { + return nil, common.Address{}, gas, ErrInvalidCodeFormat + } } // Create a new account on the state snapshot := evm.StateDB.Snapshot() @@ -451,7 +486,7 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, // Initialise a new contract and set the code that is to be used by the EVM. // The contract is a scoped environment for this execution context only. contract := NewContract(caller, AccountRef(address), value, gas) - contract.SetCodeOptionalHash(&address, codeAndHash) + contract.SetCodeOptionalHash(&address, codeAndHash, &header) if evm.Config.Debug { if evm.depth == 0 { diff --git a/core/vm/instructions.go b/core/vm/instructions.go index 22d459233b3d..38b0b05a16ad 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -867,12 +867,13 @@ func makeLog(size int) executionFunc { // opPush1 is a specialized version of pushN func opPush1(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { var ( - codeLen = uint64(len(scope.Contract.Code)) + codeEnd = scope.Contract.CodeEndOffset() integer = new(uint256.Int) ) *pc += 1 - if *pc < codeLen { - scope.Stack.push(integer.SetUint64(uint64(scope.Contract.Code[*pc]))) + dataPos := scope.Contract.CodeBeginOffset() + *pc + if dataPos < codeEnd { + scope.Stack.push(integer.SetUint64(uint64(scope.Contract.Code[dataPos]))) } else { scope.Stack.push(integer.Clear()) } @@ -882,14 +883,15 @@ func opPush1(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]by // make push instruction function func makePush(size uint64, pushByteSize int) executionFunc { return func(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { - codeLen := len(scope.Contract.Code) + codeEnd := int(scope.Contract.CodeEndOffset()) - startMin := codeLen - if int(*pc+1) < startMin { - startMin = int(*pc + 1) + startMin := codeEnd + pcAbsolute := scope.Contract.CodeBeginOffset() + *pc + if int(pcAbsolute+1) < startMin { + startMin = int(pcAbsolute + 1) } - endMin := codeLen + endMin := codeEnd if startMin+pushByteSize < endMin { endMin = startMin + pushByteSize } diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go index 312977b75588..10cd7111d1f5 100644 --- a/core/vm/interpreter.go +++ b/core/vm/interpreter.go @@ -178,7 +178,7 @@ func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) ( } // Get the operation from the jump table and validate the stack to ensure there are // enough stack items available to perform the operation. - op = contract.GetOp(pc) + op = contract.GetOp(contract.CodeBeginOffset() + pc) operation := in.cfg.JumpTable[op] cost = operation.constantGas // For tracing // Validate stack From 422b0eac17e4d7ae104bb4cfdc984ec07271ee68 Mon Sep 17 00:00:00 2001 From: Andrei Maiboroda Date: Tue, 11 May 2021 19:32:52 +0200 Subject: [PATCH 06/16] core/vm: Limit JUMPDEST analysis only to code section of EOF --- core/vm/analysis.go | 13 +++++++------ core/vm/analysis_test.go | 33 ++++++++++++++++++++++++++++----- core/vm/contract.go | 15 +++++++++++---- 3 files changed, 46 insertions(+), 15 deletions(-) diff --git a/core/vm/analysis.go b/core/vm/analysis.go index 4aa8cfe70f11..a22d81a50474 100644 --- a/core/vm/analysis.go +++ b/core/vm/analysis.go @@ -61,20 +61,21 @@ func (bits *bitvec) codeSegment(pos uint64) bool { } // codeBitmap collects data locations in code. -func codeBitmap(code []byte) bitvec { +func codeBitmap(c *Contract) bitvec { // The bitmap is 4 bytes longer than necessary, in case the code // ends with a PUSH32, the algorithm will push zeroes onto the // bitvector outside the bounds of the actual code. - bits := make(bitvec, len(code)/8+1+4) - return codeBitmapInternal(code, bits) + bits := make(bitvec, c.CodeSize()/8+1+4) + return codeBitmapInternal(c, bits) } // codeBitmapInternal is the internal implementation of codeBitmap. // It exists for the purpose of being able to run benchmark tests // without dynamic allocations affecting the results. -func codeBitmapInternal(code, bits bitvec) bitvec { - for pc := uint64(0); pc < uint64(len(code)); { - op := OpCode(code[pc]) +func codeBitmapInternal(c *Contract, bits bitvec) bitvec { + codeBegin := c.CodeBeginOffset() + for pc := uint64(0); pc < c.CodeSize(); { + op := OpCode(c.Code[codeBegin+pc]) pc++ if int8(op) < int8(PUSH1) { // If not PUSH (the int8(op) > int(PUSH32) is always false). continue diff --git a/core/vm/analysis_test.go b/core/vm/analysis_test.go index 398861f8ae7d..547f5ca296e4 100644 --- a/core/vm/analysis_test.go +++ b/core/vm/analysis_test.go @@ -20,6 +20,7 @@ import ( "math/bits" "testing" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" ) @@ -52,7 +53,15 @@ func TestJumpDestAnalysis(t *testing.T) { {[]byte{byte(PUSH32)}, 0b0000_0001, 4}, } for i, test := range tests { - ret := codeBitmap(test.code) + var ( + contract Contract + addr common.Address + hash common.Hash + header EOF1Header + ) + contract.SetCallCode(&addr, hash, test.code, &header) + + ret := codeBitmap(&contract) if ret[test.which] != test.exp { t.Fatalf("test %d: expected %x, got %02x", i, test.exp, ret[test.which]) } @@ -64,10 +73,17 @@ const analysisCodeSize = 1200 * 1024 func BenchmarkJumpdestAnalysis_1200k(bench *testing.B) { // 1.4 ms code := make([]byte, analysisCodeSize) - bench.SetBytes(analysisCodeSize) + var ( + contract Contract + addr common.Address + hash common.Hash + header EOF1Header + ) + contract.SetCallCode(&addr, hash, code, &header) bench.ResetTimer() + bench.SetBytes(analysisCodeSize) for i := 0; i < bench.N; i++ { - codeBitmap(code) + codeBitmap(&contract) } bench.StopTimer() } @@ -83,7 +99,13 @@ func BenchmarkJumpdestHashing_1200k(bench *testing.B) { } func BenchmarkJumpdestOpAnalysis(bench *testing.B) { - var op OpCode + var ( + contract Contract + addr common.Address + hash common.Hash + header EOF1Header + op OpCode + ) bencher := func(b *testing.B) { code := make([]byte, analysisCodeSize) b.SetBytes(analysisCodeSize) @@ -91,12 +113,13 @@ func BenchmarkJumpdestOpAnalysis(bench *testing.B) { code[i] = byte(op) } bits := make(bitvec, len(code)/8+1+4) + contract.SetCallCode(&addr, hash, code, &header) b.ResetTimer() for i := 0; i < b.N; i++ { for j := range bits { bits[j] = 0 } - codeBitmapInternal(code, bits) + codeBitmapInternal(&contract, bits) } } for op = PUSH1; op <= PUSH32; op++ { diff --git a/core/vm/contract.go b/core/vm/contract.go index 5fd9885db88f..3aa5565d46d9 100644 --- a/core/vm/contract.go +++ b/core/vm/contract.go @@ -88,11 +88,11 @@ func (c *Contract) validJumpdest(dest *uint256.Int) bool { udest, overflow := dest.Uint64WithOverflow() // PC cannot go beyond len(code) and certainly can't be bigger than 63bits. // Don't bother checking for JUMPDEST in that case. - if overflow || udest >= uint64(len(c.Code)) { + if overflow || udest >= c.CodeSize() { return false } // Only JUMPDESTs allowed for destinations - if OpCode(c.Code[udest]) != JUMPDEST { + if OpCode(c.Code[c.CodeBeginOffset()+udest]) != JUMPDEST { return false } return c.isCode(udest) @@ -114,7 +114,7 @@ func (c *Contract) isCode(udest uint64) bool { if !exist { // Do the analysis and save in parent context // We do not need to store it in c.analysis - analysis = codeBitmap(c.Code) + analysis = codeBitmap(c) c.jumpdests[c.CodeHash] = analysis } // Also stash it in current contract for faster access @@ -126,7 +126,7 @@ func (c *Contract) isCode(udest uint64) bool { // we don't have to recalculate it for every JUMP instruction in the execution // However, we don't save it within the parent context if c.analysis == nil { - c.analysis = codeBitmap(c.Code) + c.analysis = codeBitmap(c) } return c.analysis.codeSegment(udest) } @@ -201,6 +201,13 @@ func (c *Contract) CodeEndOffset() uint64 { return c.header.CodeEndOffset() } +func (c *Contract) CodeSize() uint64 { + if c.IsLegacy() { + return uint64(len(c.Code)) + } + return uint64(c.header.codeSize) +} + // SetCallCode sets the code of the contract and address of the backing data // object func (c *Contract) SetCallCode(addr *common.Address, hash common.Hash, code []byte, header *EOF1Header) { From 933c30493195f41d03b13a37a6e645de63b6bfe2 Mon Sep 17 00:00:00 2001 From: Alex Beregszaszi Date: Mon, 31 Oct 2022 17:41:10 +0100 Subject: [PATCH 07/16] Add docstring to CodeSize --- core/vm/contract.go | 1 + 1 file changed, 1 insertion(+) diff --git a/core/vm/contract.go b/core/vm/contract.go index 3aa5565d46d9..b907be54a793 100644 --- a/core/vm/contract.go +++ b/core/vm/contract.go @@ -201,6 +201,7 @@ func (c *Contract) CodeEndOffset() uint64 { return c.header.CodeEndOffset() } +// CodeSize returns the size of the code (if legacy) or code section (if EOF) func (c *Contract) CodeSize() uint64 { if c.IsLegacy() { return uint64(len(c.Code)) From 4913c1df656b90e0f27a59f100b708f04fafa0fd Mon Sep 17 00:00:00 2001 From: Andrei Maiboroda Date: Mon, 7 Nov 2022 19:48:31 -0600 Subject: [PATCH 08/16] Make GetOp take offset inside code and use it for validJumpdest --- core/vm/contract.go | 9 ++++++--- core/vm/interpreter.go | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/core/vm/contract.go b/core/vm/contract.go index b907be54a793..071bb7e8b653 100644 --- a/core/vm/contract.go +++ b/core/vm/contract.go @@ -84,6 +84,8 @@ func NewContract(caller ContractRef, object ContractRef, value *big.Int, gas uin return c } +// validJumpdest returns true if destination offset is inside code bounds and is JUMPDEST opcode +// dest is offset inside code section in case of EOF contract func (c *Contract) validJumpdest(dest *uint256.Int) bool { udest, overflow := dest.Uint64WithOverflow() // PC cannot go beyond len(code) and certainly can't be bigger than 63bits. @@ -92,7 +94,7 @@ func (c *Contract) validJumpdest(dest *uint256.Int) bool { return false } // Only JUMPDESTs allowed for destinations - if OpCode(c.Code[c.CodeBeginOffset()+udest]) != JUMPDEST { + if c.GetOp(udest) != JUMPDEST { return false } return c.isCode(udest) @@ -144,9 +146,10 @@ func (c *Contract) AsDelegate() *Contract { } // GetOp returns the n'th element in the contract's byte array +// n is offset inside code section in case of EOF contract func (c *Contract) GetOp(n uint64) OpCode { - if n < c.CodeEndOffset() { - return OpCode(c.Code[n]) + if n < c.CodeSize() { + return OpCode(c.Code[c.CodeBeginOffset()+n]) } return STOP diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go index 10cd7111d1f5..312977b75588 100644 --- a/core/vm/interpreter.go +++ b/core/vm/interpreter.go @@ -178,7 +178,7 @@ func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) ( } // Get the operation from the jump table and validate the stack to ensure there are // enough stack items available to perform the operation. - op = contract.GetOp(contract.CodeBeginOffset() + pc) + op = contract.GetOp(pc) operation := in.cfg.JumpTable[op] cost = operation.constantGas // For tracing // Validate stack From d5cf7a2cfdfeed8eb5ddcec998784d729e847be9 Mon Sep 17 00:00:00 2001 From: Andrei Maiboroda Date: Tue, 8 Nov 2022 15:49:21 -0600 Subject: [PATCH 09/16] Save code section bounds in Contract instead of EOF header --- core/vm/analysis.go | 6 ++-- core/vm/contract.go | 52 +++++++++++-------------------- core/vm/instructions.go | 8 ++--- eth/tracers/js/tracer_test.go | 1 + eth/tracers/logger/logger_test.go | 1 + 5 files changed, 27 insertions(+), 41 deletions(-) diff --git a/core/vm/analysis.go b/core/vm/analysis.go index a22d81a50474..35337a3a69c1 100644 --- a/core/vm/analysis.go +++ b/core/vm/analysis.go @@ -65,7 +65,7 @@ func codeBitmap(c *Contract) bitvec { // The bitmap is 4 bytes longer than necessary, in case the code // ends with a PUSH32, the algorithm will push zeroes onto the // bitvector outside the bounds of the actual code. - bits := make(bitvec, c.CodeSize()/8+1+4) + bits := make(bitvec, c.CodeSize/8+1+4) return codeBitmapInternal(c, bits) } @@ -73,8 +73,8 @@ func codeBitmap(c *Contract) bitvec { // It exists for the purpose of being able to run benchmark tests // without dynamic allocations affecting the results. func codeBitmapInternal(c *Contract, bits bitvec) bitvec { - codeBegin := c.CodeBeginOffset() - for pc := uint64(0); pc < c.CodeSize(); { + codeBegin := c.CodeBeginOffset + for pc := uint64(0); pc < c.CodeSize; { op := OpCode(c.Code[codeBegin+pc]) pc++ if int8(op) < int8(PUSH1) { // If not PUSH (the int8(op) > int(PUSH32) is always false). diff --git a/core/vm/contract.go b/core/vm/contract.go index 071bb7e8b653..3d0595c4c7b4 100644 --- a/core/vm/contract.go +++ b/core/vm/contract.go @@ -50,7 +50,8 @@ type Contract struct { caller ContractRef self ContractRef - header EOF1Header + CodeBeginOffset uint64 // starting offset of the code section if EOF or 0 if legacy + CodeSize uint64 // size of the code section if EOF or bytecode size if legacy jumpdests map[common.Hash]bitvec // Aggregated result of JUMPDEST analysis. analysis bitvec // Locally cached result of JUMPDEST analysis @@ -90,7 +91,7 @@ func (c *Contract) validJumpdest(dest *uint256.Int) bool { udest, overflow := dest.Uint64WithOverflow() // PC cannot go beyond len(code) and certainly can't be bigger than 63bits. // Don't bother checking for JUMPDEST in that case. - if overflow || udest >= c.CodeSize() { + if overflow || udest >= c.CodeSize { return false } // Only JUMPDESTs allowed for destinations @@ -148,8 +149,8 @@ func (c *Contract) AsDelegate() *Contract { // GetOp returns the n'th element in the contract's byte array // n is offset inside code section in case of EOF contract func (c *Contract) GetOp(n uint64) OpCode { - if n < c.CodeSize() { - return OpCode(c.Code[c.CodeBeginOffset()+n]) + if n < c.CodeSize { + return OpCode(c.Code[c.CodeBeginOffset+n]) } return STOP @@ -182,34 +183,19 @@ func (c *Contract) Value() *big.Int { return c.value } -// IsLegacy returns true if contract is not EOF -func (c *Contract) IsLegacy() bool { +// getCodeBounds returns beginning offset and size of the code section if EOF +// or 0 and bytecode length if legacy +func getCodeBounds(container []byte, header *EOF1Header) (begin uint64, size uint64) { // EOF1 doesn't allow contracts without code section - return c.header.codeSize == 0 -} - -// CodeBeginOffset returns starting offset of the code section -func (c *Contract) CodeBeginOffset() uint64 { - if c.IsLegacy() { - return 0 - } - return c.header.CodeBeginOffset() -} - -// CodeEndOffset returns offset of the code section end -func (c *Contract) CodeEndOffset() uint64 { - if c.IsLegacy() { - return uint64(len(c.Code)) - } - return c.header.CodeEndOffset() -} - -// CodeSize returns the size of the code (if legacy) or code section (if EOF) -func (c *Contract) CodeSize() uint64 { - if c.IsLegacy() { - return uint64(len(c.Code)) + isLegacy := (header.codeSize == 0) + if isLegacy { + begin = 0 + size = uint64(len(container)) + } else { + begin = header.CodeBeginOffset() + size = uint64(header.codeSize) } - return uint64(c.header.codeSize) + return } // SetCallCode sets the code of the contract and address of the backing data @@ -219,8 +205,7 @@ func (c *Contract) SetCallCode(addr *common.Address, hash common.Hash, code []by c.CodeHash = hash c.CodeAddr = addr - c.header.codeSize = header.codeSize - c.header.dataSize = header.dataSize + c.CodeBeginOffset, c.CodeSize = getCodeBounds(code, header) } // SetCodeOptionalHash can be used to provide code, but it's optional to provide hash. @@ -230,6 +215,5 @@ func (c *Contract) SetCodeOptionalHash(addr *common.Address, codeAndHash *codeAn c.CodeHash = codeAndHash.hash c.CodeAddr = addr - c.header.codeSize = header.codeSize - c.header.dataSize = header.dataSize + c.CodeBeginOffset, c.CodeSize = getCodeBounds(codeAndHash.code, header) } diff --git a/core/vm/instructions.go b/core/vm/instructions.go index 38b0b05a16ad..c7ac09bc7ded 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -867,11 +867,11 @@ func makeLog(size int) executionFunc { // opPush1 is a specialized version of pushN func opPush1(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { var ( - codeEnd = scope.Contract.CodeEndOffset() + codeEnd = scope.Contract.CodeBeginOffset + scope.Contract.CodeSize integer = new(uint256.Int) ) *pc += 1 - dataPos := scope.Contract.CodeBeginOffset() + *pc + dataPos := scope.Contract.CodeBeginOffset + *pc if dataPos < codeEnd { scope.Stack.push(integer.SetUint64(uint64(scope.Contract.Code[dataPos]))) } else { @@ -883,10 +883,10 @@ func opPush1(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]by // make push instruction function func makePush(size uint64, pushByteSize int) executionFunc { return func(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { - codeEnd := int(scope.Contract.CodeEndOffset()) + codeEnd := int(scope.Contract.CodeBeginOffset + scope.Contract.CodeSize) startMin := codeEnd - pcAbsolute := scope.Contract.CodeBeginOffset() + *pc + pcAbsolute := scope.Contract.CodeBeginOffset + *pc if int(pcAbsolute+1) < startMin { startMin = int(pcAbsolute + 1) } diff --git a/eth/tracers/js/tracer_test.go b/eth/tracers/js/tracer_test.go index 6a916f55b2a7..5644523af784 100644 --- a/eth/tracers/js/tracer_test.go +++ b/eth/tracers/js/tracer_test.go @@ -72,6 +72,7 @@ func runTrace(tracer tracers.Tracer, vmctx *vmContext, chaincfg *params.ChainCon if contractCode != nil { contract.Code = contractCode } + contract.CodeSize = uint64(len(contract.Code)) tracer.CaptureTxStart(gasLimit) tracer.CaptureStart(env, contract.Caller(), contract.Address(), false, []byte{}, startGas, value) diff --git a/eth/tracers/logger/logger_test.go b/eth/tracers/logger/logger_test.go index 1bc7456d31c6..3af858d1859e 100644 --- a/eth/tracers/logger/logger_test.go +++ b/eth/tracers/logger/logger_test.go @@ -59,6 +59,7 @@ func TestStoreCapture(t *testing.T) { contract = vm.NewContract(&dummyContractRef{}, &dummyContractRef{}, new(big.Int), 100000) ) contract.Code = []byte{byte(vm.PUSH1), 0x1, byte(vm.PUSH1), 0x0, byte(vm.SSTORE)} + contract.CodeSize = uint64(len(contract.Code)) var index common.Hash logger.CaptureStart(env, common.Address{}, contract.Address(), false, nil, 0, nil) _, err := env.Interpreter().Run(contract, []byte{}, false) From 8adc30cb735ba61a09ddb09c6cd6c099761a02b6 Mon Sep 17 00:00:00 2001 From: Andrei Maiboroda Date: Tue, 8 Nov 2022 12:34:24 -0600 Subject: [PATCH 10/16] core/vm: Rename Contract.Code => Contract.Container --- core/vm/analysis.go | 2 +- core/vm/contract.go | 33 ++++++++++++++----------------- core/vm/instructions.go | 20 +++++++++---------- core/vm/interpreter.go | 2 +- eth/tracers/js/tracer_test.go | 6 +++--- eth/tracers/logger/logger_test.go | 4 ++-- 6 files changed, 32 insertions(+), 35 deletions(-) diff --git a/core/vm/analysis.go b/core/vm/analysis.go index 35337a3a69c1..9abd4ccb915a 100644 --- a/core/vm/analysis.go +++ b/core/vm/analysis.go @@ -75,7 +75,7 @@ func codeBitmap(c *Contract) bitvec { func codeBitmapInternal(c *Contract, bits bitvec) bitvec { codeBegin := c.CodeBeginOffset for pc := uint64(0); pc < c.CodeSize; { - op := OpCode(c.Code[codeBegin+pc]) + op := OpCode(c.Container[codeBegin+pc]) pc++ if int8(op) < int8(PUSH1) { // If not PUSH (the int8(op) > int(PUSH32) is always false). continue diff --git a/core/vm/contract.go b/core/vm/contract.go index 3d0595c4c7b4..de5a3d8d357c 100644 --- a/core/vm/contract.go +++ b/core/vm/contract.go @@ -56,10 +56,9 @@ type Contract struct { jumpdests map[common.Hash]bitvec // Aggregated result of JUMPDEST analysis. analysis bitvec // Locally cached result of JUMPDEST analysis - Code []byte - CodeHash common.Hash - CodeAddr *common.Address - Input []byte + Container []byte + ContainerHash common.Hash + Input []byte Gas uint64 value *big.Int @@ -111,14 +110,14 @@ func (c *Contract) isCode(udest uint64) bool { // Do we have a contract hash already? // If we do have a hash, that means it's a 'regular' contract. For regular // contracts ( not temporary initcode), we store the analysis in a map - if c.CodeHash != (common.Hash{}) { + if c.ContainerHash != (common.Hash{}) { // Does parent context have the analysis? - analysis, exist := c.jumpdests[c.CodeHash] + analysis, exist := c.jumpdests[c.ContainerHash] if !exist { // Do the analysis and save in parent context // We do not need to store it in c.analysis analysis = codeBitmap(c) - c.jumpdests[c.CodeHash] = analysis + c.jumpdests[c.ContainerHash] = analysis } // Also stash it in current contract for faster access c.analysis = analysis @@ -150,7 +149,7 @@ func (c *Contract) AsDelegate() *Contract { // n is offset inside code section in case of EOF contract func (c *Contract) GetOp(n uint64) OpCode { if n < c.CodeSize { - return OpCode(c.Code[c.CodeBeginOffset+n]) + return OpCode(c.Container[c.CodeBeginOffset+n]) } return STOP @@ -200,20 +199,18 @@ func getCodeBounds(container []byte, header *EOF1Header) (begin uint64, size uin // SetCallCode sets the code of the contract and address of the backing data // object -func (c *Contract) SetCallCode(addr *common.Address, hash common.Hash, code []byte, header *EOF1Header) { - c.Code = code - c.CodeHash = hash - c.CodeAddr = addr +func (c *Contract) SetCallCode(addr *common.Address, hash common.Hash, container []byte, header *EOF1Header) { + c.Container = container + c.ContainerHash = hash - c.CodeBeginOffset, c.CodeSize = getCodeBounds(code, header) + c.CodeBeginOffset, c.CodeSize = getCodeBounds(container, header) } // SetCodeOptionalHash can be used to provide code, but it's optional to provide hash. // In case hash is not provided, the jumpdest analysis will not be saved to the parent context -func (c *Contract) SetCodeOptionalHash(addr *common.Address, codeAndHash *codeAndHash, header *EOF1Header) { - c.Code = codeAndHash.code - c.CodeHash = codeAndHash.hash - c.CodeAddr = addr +func (c *Contract) SetCodeOptionalHash(addr *common.Address, containerAndHash *codeAndHash, header *EOF1Header) { + c.Container = containerAndHash.code + c.ContainerHash = containerAndHash.hash - c.CodeBeginOffset, c.CodeSize = getCodeBounds(codeAndHash.code, header) + c.CodeBeginOffset, c.CodeSize = getCodeBounds(containerAndHash.code, header) } diff --git a/core/vm/instructions.go b/core/vm/instructions.go index c7ac09bc7ded..cb31970454d9 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -349,22 +349,22 @@ func opExtCodeSize(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) func opCodeSize(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { l := new(uint256.Int) - l.SetUint64(uint64(len(scope.Contract.Code))) + l.SetUint64(uint64(len(scope.Contract.Container))) scope.Stack.push(l) return nil, nil } func opCodeCopy(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { var ( - memOffset = scope.Stack.pop() - codeOffset = scope.Stack.pop() - length = scope.Stack.pop() + memOffset = scope.Stack.pop() + containerOffset = scope.Stack.pop() + length = scope.Stack.pop() ) - uint64CodeOffset, overflow := codeOffset.Uint64WithOverflow() + uint64ContainerOffset, overflow := containerOffset.Uint64WithOverflow() if overflow { - uint64CodeOffset = 0xffffffffffffffff + uint64ContainerOffset = 0xffffffffffffffff } - codeCopy := getData(scope.Contract.Code, uint64CodeOffset, length.Uint64()) + codeCopy := getData(scope.Contract.Container, uint64ContainerOffset, length.Uint64()) scope.Memory.Set(memOffset.Uint64(), length.Uint64(), codeCopy) return nil, nil @@ -812,7 +812,7 @@ func opRevert(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]b } func opUndefined(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { - return nil, &ErrInvalidOpCode{opcode: OpCode(scope.Contract.Code[*pc])} + return nil, &ErrInvalidOpCode{opcode: OpCode(scope.Contract.Container[*pc])} } func opStop(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { @@ -873,7 +873,7 @@ func opPush1(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]by *pc += 1 dataPos := scope.Contract.CodeBeginOffset + *pc if dataPos < codeEnd { - scope.Stack.push(integer.SetUint64(uint64(scope.Contract.Code[dataPos]))) + scope.Stack.push(integer.SetUint64(uint64(scope.Contract.Container[dataPos]))) } else { scope.Stack.push(integer.Clear()) } @@ -898,7 +898,7 @@ func makePush(size uint64, pushByteSize int) executionFunc { integer := new(uint256.Int) scope.Stack.push(integer.SetBytes(common.RightPadBytes( - scope.Contract.Code[startMin:endMin], pushByteSize))) + scope.Contract.Container[startMin:endMin], pushByteSize))) *pc += size return nil, nil diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go index 312977b75588..0faefe16110a 100644 --- a/core/vm/interpreter.go +++ b/core/vm/interpreter.go @@ -124,7 +124,7 @@ func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) ( in.returnData = nil // Don't bother with the execution if there's no code. - if len(contract.Code) == 0 { + if len(contract.Container) == 0 { return nil, nil } diff --git a/eth/tracers/js/tracer_test.go b/eth/tracers/js/tracer_test.go index 5644523af784..9e07f33d9a5d 100644 --- a/eth/tracers/js/tracer_test.go +++ b/eth/tracers/js/tracer_test.go @@ -68,11 +68,11 @@ func runTrace(tracer tracers.Tracer, vmctx *vmContext, chaincfg *params.ChainCon value = big.NewInt(0) contract = vm.NewContract(account{}, account{}, value, startGas) ) - contract.Code = []byte{byte(vm.PUSH1), 0x1, byte(vm.PUSH1), 0x1, 0x0} + contract.Container = []byte{byte(vm.PUSH1), 0x1, byte(vm.PUSH1), 0x1, 0x0} if contractCode != nil { - contract.Code = contractCode + contract.Container = contractCode } - contract.CodeSize = uint64(len(contract.Code)) + contract.CodeSize = uint64(len(contract.Container)) tracer.CaptureTxStart(gasLimit) tracer.CaptureStart(env, contract.Caller(), contract.Address(), false, []byte{}, startGas, value) diff --git a/eth/tracers/logger/logger_test.go b/eth/tracers/logger/logger_test.go index 3af858d1859e..e7b9cd26f264 100644 --- a/eth/tracers/logger/logger_test.go +++ b/eth/tracers/logger/logger_test.go @@ -58,8 +58,8 @@ func TestStoreCapture(t *testing.T) { env = vm.NewEVM(vm.BlockContext{}, vm.TxContext{}, &dummyStatedb{}, params.TestChainConfig, vm.Config{Debug: true, Tracer: logger}) contract = vm.NewContract(&dummyContractRef{}, &dummyContractRef{}, new(big.Int), 100000) ) - contract.Code = []byte{byte(vm.PUSH1), 0x1, byte(vm.PUSH1), 0x0, byte(vm.SSTORE)} - contract.CodeSize = uint64(len(contract.Code)) + contract.Container = []byte{byte(vm.PUSH1), 0x1, byte(vm.PUSH1), 0x0, byte(vm.SSTORE)} + contract.CodeSize = uint64(len(contract.Container)) var index common.Hash logger.CaptureStart(env, common.Address{}, contract.Address(), false, nil, 0, nil) _, err := env.Interpreter().Run(contract, []byte{}, false) From 4241a4009a87edee67d660a4deae7810d6e58b9d Mon Sep 17 00:00:00 2001 From: Andrei Maiboroda Date: Fri, 10 Dec 2021 17:32:07 +0100 Subject: [PATCH 11/16] core/vm: Add EIP-3670 to supported extra EIPs --- core/vm/eips.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/vm/eips.go b/core/vm/eips.go index 248a4daab75b..7467eab78719 100644 --- a/core/vm/eips.go +++ b/core/vm/eips.go @@ -33,6 +33,7 @@ var activators = map[int]func(*JumpTable){ 1884: enable1884, 1344: enable1344, 3540: enable3540, + 3670: enable3670, } // EnableEIP enables the given EIP on the config. @@ -197,3 +198,7 @@ func opPush0(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]by func enable3540(jt *JumpTable) { // Do nothing. } + +func enable3670(jt *JumpTable) { + // Do nothing. +} From 0b87043979dd0cc0fa2c3ee1549ca89c4538df91 Mon Sep 17 00:00:00 2001 From: Andrei Maiboroda Date: Tue, 17 May 2022 16:26:50 +0200 Subject: [PATCH 12/16] core/vm: Implement EIP-3670 EOF - Code Validation --- core/vm/eof.go | 38 ++++++++++++++++++++++++++++++++++---- core/vm/eof_test.go | 17 ++++++++++------- core/vm/errors.go | 26 ++++++++++++++------------ core/vm/evm.go | 33 +++++++++++++++++++-------------- core/vm/opcodes.go | 9 +++++++++ 5 files changed, 86 insertions(+), 37 deletions(-) diff --git a/core/vm/eof.go b/core/vm/eof.go index d76188fc7f4a..585f4e3c2e03 100644 --- a/core/vm/eof.go +++ b/core/vm/eof.go @@ -19,6 +19,7 @@ package vm import ( "bytes" "encoding/binary" + "reflect" ) const ( @@ -117,10 +118,39 @@ sectionLoop: return header, nil } -// validateEOF returns true if code has valid format -func validateEOF(code []byte) bool { - _, err := readEOF1Header(code) - return err == nil +// validateInstructions checks that there're no undefined instructions and code ends with a terminating instruction +func validateInstructions(code []byte, header *EOF1Header, jumpTable *JumpTable) error { + i := header.CodeBeginOffset() + var opcode OpCode + for i < header.CodeEndOffset() { + opcode = OpCode(code[i]) + if reflect.ValueOf(jumpTable[opcode].execute).Pointer() == reflect.ValueOf(opUndefined).Pointer() { + return ErrEOF1UndefinedInstruction + } + if opcode >= PUSH1 && opcode <= PUSH32 { + i += uint64(opcode) - uint64(PUSH1) + 1 + } + i += 1 + } + + if !opcode.isTerminating() { + return ErrEOF1TerminatingInstructionMissing + } + + return nil +} + +// validateEOF returns true if code has valid format and code section +func validateEOF(code []byte, jumpTable *JumpTable) (EOF1Header, error) { + header, err := readEOF1Header(code) + if err != nil { + return EOF1Header{}, err + } + err = validateInstructions(code, &header, jumpTable) + if err != nil { + return EOF1Header{}, err + } + return header, nil } // readValidEOF1Header parses EOF1-formatted code header, assuming that it is already validated diff --git a/core/vm/eof_test.go b/core/vm/eof_test.go index c13970f6ac1a..103d666cc6c7 100644 --- a/core/vm/eof_test.go +++ b/core/vm/eof_test.go @@ -30,11 +30,11 @@ type eof1Test struct { var eof1ValidTests = []eof1Test{ {"EF00010100010000", 1, 0}, - {"EF0001010002006000", 2, 0}, - {"EF0001010002020001006000AA", 2, 1}, - {"EF0001010002020004006000AABBCCDD", 2, 4}, - {"EF00010100040200020060006001AABB", 4, 2}, - {"EF000101000602000400600060016002AABBCCDD", 6, 4}, + {"EF0001010002000000", 2, 0}, + {"EF0001010002020001000000AA", 2, 1}, + {"EF0001010002020004000000AABBCCDD", 2, 4}, + {"EF0001010005020002006000600100AABB", 5, 2}, + {"EF00010100070200040060006001600200AABBCCDD", 7, 4}, } type eof1InvalidTest struct { @@ -130,14 +130,17 @@ func TestReadEOF1Header(t *testing.T) { } func TestValidateEOF(t *testing.T) { + jt := &mergeInstructionSet for _, test := range eof1ValidTests { - if !validateEOF(common.Hex2Bytes(test.code)) { + _, err := validateEOF(common.Hex2Bytes(test.code), jt) + if err != nil { t.Errorf("code %v expected to be valid", test.code) } } for _, test := range eof1InvalidTests { - if validateEOF(common.Hex2Bytes(test.code)) { + _, err := validateEOF(common.Hex2Bytes(test.code), jt) + if err == nil { t.Errorf("code %v expected to be invalid", test.code) } } diff --git a/core/vm/errors.go b/core/vm/errors.go index 54d8e4f1f306..b0e76803d6cb 100644 --- a/core/vm/errors.go +++ b/core/vm/errors.go @@ -36,7 +36,7 @@ var ( ErrGasUintOverflow = errors.New("gas uint64 overflow") ErrNonceUintOverflow = errors.New("nonce uint64 overflow") ErrInvalidCode = errors.New("invalid code: must not begin with 0xef") - ErrInvalidCodeFormat = errors.New("invalid code: format validation failed") + ErrInvalidEOFCode = errors.New("invalid code: EOF validation failed") // errStopToken is an internal token indicating interpreter loop termination, // never returned to outside callers. @@ -45,17 +45,19 @@ var ( // EOF1 validation errors var ( - ErrEOF1InvalidVersion = errors.New("invalid version byte") - ErrEOF1MultipleCodeSections = errors.New("multiple code sections") - ErrEOF1CodeSectionSizeMissing = errors.New("can't read code section size") - ErrEOF1EmptyCodeSection = errors.New("code section size is 0") - ErrEOF1DataSectionBeforeCodeSection = errors.New("data section before code section") - ErrEOF1MultipleDataSections = errors.New("multiple data sections") - ErrEOF1DataSectionSizeMissing = errors.New("can't read data section size") - ErrEOF1EmptyDataSection = errors.New("data section size is 0") - ErrEOF1UnknownSection = errors.New("unknown section id") - ErrEOF1CodeSectionMissing = errors.New("no code section") - ErrEOF1InvalidTotalSize = errors.New("invalid total size") + ErrEOF1InvalidVersion = errors.New("invalid version byte") + ErrEOF1MultipleCodeSections = errors.New("multiple code sections") + ErrEOF1CodeSectionSizeMissing = errors.New("can't read code section size") + ErrEOF1EmptyCodeSection = errors.New("code section size is 0") + ErrEOF1DataSectionBeforeCodeSection = errors.New("data section before code section") + ErrEOF1MultipleDataSections = errors.New("multiple data sections") + ErrEOF1DataSectionSizeMissing = errors.New("can't read data section size") + ErrEOF1EmptyDataSection = errors.New("data section size is 0") + ErrEOF1UnknownSection = errors.New("unknown section id") + ErrEOF1CodeSectionMissing = errors.New("no code section") + ErrEOF1InvalidTotalSize = errors.New("invalid total size") + ErrEOF1UndefinedInstruction = errors.New("undefined instruction") + ErrEOF1TerminatingInstructionMissing = errors.New("code section doesn't end with terminating instruction") ) // ErrStackUnderflow wraps an evm error when the items on the stack less diff --git a/core/vm/evm.go b/core/vm/evm.go index 26e830ed4c8a..551f1aab2de0 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -223,7 +223,7 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas addrCopy := addr var header EOF1Header - if hasEIP3540(&evm.Config) && hasEOFMagic(code) { + if hasEIP3540And3670(&evm.Config) && hasEOFMagic(code) { header = readValidEOF1Header(code) } @@ -288,7 +288,7 @@ func (evm *EVM) CallCode(caller ContractRef, addr common.Address, input []byte, code := evm.StateDB.GetCode(addrCopy) var header EOF1Header - if hasEIP3540(&evm.Config) && hasEOFMagic(code) { + if hasEIP3540And3670(&evm.Config) && hasEOFMagic(code) { header = readValidEOF1Header(code) } @@ -337,7 +337,7 @@ func (evm *EVM) DelegateCall(caller ContractRef, addr common.Address, input []by code := evm.StateDB.GetCode(addrCopy) var header EOF1Header - if hasEIP3540(&evm.Config) && hasEOFMagic(code) { + if hasEIP3540And3670(&evm.Config) && hasEOFMagic(code) { header = readValidEOF1Header(code) } @@ -397,7 +397,7 @@ func (evm *EVM) StaticCall(caller ContractRef, addr common.Address, input []byte code := evm.StateDB.GetCode(addrCopy) var header EOF1Header - if hasEIP3540(&evm.Config) && hasEOFMagic(code) { + if hasEIP3540And3670(&evm.Config) && hasEOFMagic(code) { header = readValidEOF1Header(code) } @@ -432,13 +432,17 @@ func (c *codeAndHash) Hash() common.Hash { return c.hash } -func hasEIP3540(vmConfig *Config) bool { +func hasEIP3540And3670(vmConfig *Config) bool { + eip3540 := false + eip3670 := false for _, eip := range vmConfig.ExtraEips { if eip == 3540 { - return true + eip3540 = true + } else if eip == 3670 { + eip3670 = true } } - return false + return eip3540 && eip3670 } // create creates a new contract using code as deployment code. @@ -468,11 +472,11 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, } // Try to read code header if it claims to be EOF-formatted. var header EOF1Header - if hasEIP3540(&evm.Config) && hasEOFMagic(codeAndHash.code) { + if hasEIP3540And3670(&evm.Config) && hasEOFMagic(codeAndHash.code) { var err error - header, err = readEOF1Header(codeAndHash.code) + header, err = validateEOF(codeAndHash.code, evm.interpreter.cfg.JumpTable) if err != nil { - return nil, common.Address{}, gas, ErrInvalidCodeFormat + return nil, common.Address{}, gas, ErrInvalidEOFCode } } // Create a new account on the state @@ -506,11 +510,12 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, } if err == nil && hasFormatByte(ret) { - if hasEIP3540(&evm.Config) { - // Allow only valid EOF1 if EIP-3540 is enabled. + if hasEIP3540And3670(&evm.Config) { + // Allow only valid EOF1 if EIP-3540 and EIP-3670 are enabled. if hasEOFMagic(ret) { - if !validateEOF(ret) { - err = ErrInvalidCodeFormat + _, err = validateEOF(ret, evm.interpreter.cfg.JumpTable) + if err != nil { + err = ErrInvalidEOFCode } } else { // Reject non-EOF code starting with 0xEF. diff --git a/core/vm/opcodes.go b/core/vm/opcodes.go index fa7de5049ace..1a35e89f473e 100644 --- a/core/vm/opcodes.go +++ b/core/vm/opcodes.go @@ -546,3 +546,12 @@ var stringToOp = map[string]OpCode{ func StringToOp(str string) OpCode { return stringToOp[str] } + +// IsTerminating specifies if an opcode is a valid terminating instruction in EOF. +func (op OpCode) isTerminating() bool { + switch op { + case STOP, RETURN, REVERT, INVALID, SELFDESTRUCT: + return true + } + return false +} From 62591d6683816564a0a2252b407e2a3f834057f3 Mon Sep 17 00:00:00 2001 From: Andrei Maiboroda Date: Fri, 10 Dec 2021 19:52:35 +0100 Subject: [PATCH 13/16] core/vm: Use flag to distinguish undefined operations --- core/vm/eof.go | 3 +-- core/vm/jump_table.go | 4 +++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/core/vm/eof.go b/core/vm/eof.go index 585f4e3c2e03..b4ccf771591e 100644 --- a/core/vm/eof.go +++ b/core/vm/eof.go @@ -19,7 +19,6 @@ package vm import ( "bytes" "encoding/binary" - "reflect" ) const ( @@ -124,7 +123,7 @@ func validateInstructions(code []byte, header *EOF1Header, jumpTable *JumpTable) var opcode OpCode for i < header.CodeEndOffset() { opcode = OpCode(code[i]) - if reflect.ValueOf(jumpTable[opcode].execute).Pointer() == reflect.ValueOf(opUndefined).Pointer() { + if jumpTable[opcode].undefined { return ErrEOF1UndefinedInstruction } if opcode >= PUSH1 && opcode <= PUSH32 { diff --git a/core/vm/jump_table.go b/core/vm/jump_table.go index 94229436d23c..25024c17aef9 100644 --- a/core/vm/jump_table.go +++ b/core/vm/jump_table.go @@ -42,6 +42,8 @@ type operation struct { // memorySize returns the memory size required for the operation memorySize memorySizeFunc + + undefined bool } var ( @@ -1037,7 +1039,7 @@ func newFrontierInstructionSet() JumpTable { // Fill all unassigned slots with opUndefined. for i, entry := range tbl { if entry == nil { - tbl[i] = &operation{execute: opUndefined, maxStack: maxStack(0, 0)} + tbl[i] = &operation{execute: opUndefined, maxStack: maxStack(0, 0), undefined: true} } } From dbe14071c6a85c2df2dbdaac4fb001b26cb5b730 Mon Sep 17 00:00:00 2001 From: Andrei Maiboroda Date: Thu, 29 Sep 2022 15:16:48 +0200 Subject: [PATCH 14/16] core/vm: Unit tests for code EOP-3670 code validation --- core/vm/eof_test.go | 171 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) diff --git a/core/vm/eof_test.go b/core/vm/eof_test.go index 103d666cc6c7..681e999da695 100644 --- a/core/vm/eof_test.go +++ b/core/vm/eof_test.go @@ -35,6 +35,13 @@ var eof1ValidTests = []eof1Test{ {"EF0001010002020004000000AABBCCDD", 2, 4}, {"EF0001010005020002006000600100AABB", 5, 2}, {"EF00010100070200040060006001600200AABBCCDD", 7, 4}, + {"EF000101000100FE", 1, 0}, // INVALID is defined and can be terminating + {"EF00010100050060006000F3", 5, 0}, // terminating with RETURN + {"EF00010100050060006000FD", 5, 0}, // terminating with REVERT + {"EF0001010003006000FF", 3, 0}, // terminating with SELFDESTRUCT + {"EF0001010022007F000000000000000000000000000000000000000000000000000000000000000000", 34, 0}, // PUSH32 + {"EF0001010022007F0C0D0E0F1E1F2122232425262728292A2B2C2D2E2F494A4B4C4D4E4F5C5D5E5F00", 34, 0}, // undefined instructions inside push data + {"EF000101000102002000000C0D0E0F1E1F2122232425262728292A2B2C2D2E2F494A4B4C4D4E4F5C5D5E5F", 1, 32}, // undefined instructions inside data section } type eof1InvalidTest struct { @@ -84,6 +91,21 @@ var eof1InvalidTests = []eof1InvalidTest{ {"EF0001010002030004006000AABBCCDD", ErrEOF1UnknownSection.Error()}, // section id = 3 } +var eof1InvalidInstructionsTests = []eof1InvalidTest{ + // 0C is undefined instruction + {"EF0001010001000C", ErrEOF1UndefinedInstruction.Error()}, + // EF is undefined instruction + {"EF000101000100EF", ErrEOF1UndefinedInstruction.Error()}, + // ADDRESS is not a terminating instruction + {"EF00010100010030", ErrEOF1TerminatingInstructionMissing.Error()}, + // PUSH1 without data + {"EF00010100010060", ErrEOF1TerminatingInstructionMissing.Error()}, + // PUSH32 with 31 bytes of data + {"EF0001010020007F00000000000000000000000000000000000000000000000000000000000000", ErrEOF1TerminatingInstructionMissing.Error()}, + // PUSH32 with 32 bytes of data and no terminating instruction + {"EF0001010021007F0000000000000000000000000000000000000000000000000000000000000000", ErrEOF1TerminatingInstructionMissing.Error()}, +} + func TestHasEOFMagic(t *testing.T) { for _, test := range notEOFTests { if hasEOFMagic(common.Hex2Bytes(test)) { @@ -157,3 +179,152 @@ func TestReadValidEOF1Header(t *testing.T) { } } } + +func TestValidateInstructions(t *testing.T) { + jt := &londonInstructionSet + for _, test := range eof1ValidTests { + code := common.Hex2Bytes(test.code) + header, err := readEOF1Header(code) + if err != nil { + t.Errorf("code %v header validation failure, error: %v", test.code, err) + } + + err = validateInstructions(code, &header, jt) + if err != nil { + t.Errorf("code %v instruction validation failure, error: %v", test.code, err) + } + } + + for _, test := range eof1InvalidInstructionsTests { + code := common.Hex2Bytes(test.code) + header, err := readEOF1Header(code) + if err != nil { + t.Errorf("code %v header validation failure, error: %v", test.code, err) + } + + err = validateInstructions(code, &header, jt) + if err == nil { + t.Errorf("code %v expected to be invalid", test.code) + } else if err.Error() != test.error { + t.Errorf("code %v expected error: \"%v\" got error: \"%v\"", test.code, test.error, err.Error()) + } + } +} + +func TestValidateUndefinedInstructions(t *testing.T) { + jt := &londonInstructionSet + code := common.Hex2Bytes("EF0001010002000C00") + instrByte := &code[7] + for opcode := uint16(0); opcode <= 0xff; opcode++ { + if OpCode(opcode) >= PUSH1 && OpCode(opcode) <= PUSH32 { + continue + } + + *instrByte = byte(opcode) + header, err := readEOF1Header(code) + if err != nil { + t.Errorf("code %v header validation failure, error: %v", common.Bytes2Hex(code), err) + } + + err = validateInstructions(code, &header, jt) + if jt[opcode].undefined { + if err == nil { + t.Errorf("opcode %v expected to be invalid", opcode) + } else if err != ErrEOF1UndefinedInstruction { + t.Errorf("opcode %v unxpected error: \"%v\"", opcode, err.Error()) + } + } else { + if err != nil { + t.Errorf("code %v instruction validation failure, error: %v", common.Bytes2Hex(code), err) + } + } + } +} + +func TestValidateTerminatingInstructions(t *testing.T) { + jt := &londonInstructionSet + code := common.Hex2Bytes("EF0001010001000C") + instrByte := &code[7] + for opcodeValue := uint16(0); opcodeValue <= 0xff; opcodeValue++ { + opcode := OpCode(opcodeValue) + if opcode >= PUSH1 && opcode <= PUSH32 { + continue + } + if jt[opcode].undefined { + continue + } + *instrByte = byte(opcode) + header, err := readEOF1Header(code) + if err != nil { + t.Errorf("code %v header validation failure, error: %v", common.Bytes2Hex(code), err) + } + err = validateInstructions(code, &header, jt) + + if opcode == STOP || opcode == RETURN || opcode == REVERT || opcode == INVALID || opcode == SELFDESTRUCT { + if err != nil { + t.Errorf("opcode %v expected to be valid terminating instruction", opcode) + } + } else { + if err == nil { + t.Errorf("opcode %v expected to be invalid terminating instruction", opcode) + } else if err != ErrEOF1TerminatingInstructionMissing { + t.Errorf("opcode %v unexpected error: \"%v\"", opcode, err.Error()) + } + } + } +} + +func TestValidateTruncatedPush(t *testing.T) { + jt := &londonInstructionSet + zeroes := [33]byte{} + code := common.Hex2Bytes("EF0001010001000C") + for opcode := PUSH1; opcode <= PUSH32; opcode++ { + requiredBytes := opcode - PUSH1 + 1 + + // make code with truncated PUSH data + codeTruncatedPush := append(code, zeroes[:requiredBytes-1]...) + codeTruncatedPush[5] = byte(len(codeTruncatedPush) - 7) + codeTruncatedPush[7] = byte(opcode) + + header, err := readEOF1Header(codeTruncatedPush) + if err != nil { + t.Errorf("code %v header validation failure, error: %v", common.Bytes2Hex(code), err) + } + err = validateInstructions(codeTruncatedPush, &header, jt) + if err == nil { + t.Errorf("code %v has truncated PUSH, expected to be invalid", common.Bytes2Hex(codeTruncatedPush)) + } else if err != ErrEOF1TerminatingInstructionMissing { + t.Errorf("code %v unexpected validation error: %v", common.Bytes2Hex(codeTruncatedPush), err) + } + + // make code with full PUSH data but no terminating instruction in the end + codeNotTerminated := append(code, zeroes[:requiredBytes]...) + codeNotTerminated[5] = byte(len(codeNotTerminated) - 7) + codeNotTerminated[7] = byte(opcode) + + header, err = readEOF1Header(codeNotTerminated) + if err != nil { + t.Errorf("code %v header validation failure, error: %v", common.Bytes2Hex(codeNotTerminated), err) + } + err = validateInstructions(codeTruncatedPush, &header, jt) + if err == nil { + t.Errorf("code %v does not have terminating instruction, expected to be invalid", common.Bytes2Hex(codeNotTerminated)) + } else if err != ErrEOF1TerminatingInstructionMissing { + t.Errorf("code %v unexpected validation error: %v", common.Bytes2Hex(codeNotTerminated), err) + } + + // make valid code + codeValid := append(code, zeroes[:requiredBytes+1]...) // + 1 for terminating STOP + codeValid[5] = byte(len(codeValid) - 7) + codeValid[7] = byte(opcode) + + header, err = readEOF1Header(codeValid) + if err != nil { + t.Errorf("code %v header validation failure, error: %v", common.Bytes2Hex(code), err) + } + err = validateInstructions(codeValid, &header, jt) + if err != nil { + t.Errorf("code %v instruction validation failure, error: %v", common.Bytes2Hex(code), err) + } + } +} From e2fa55a48f352e8fdb3ec84aa80e6f49baeb7878 Mon Sep 17 00:00:00 2001 From: Andrei Maiboroda Date: Mon, 29 Nov 2021 13:18:27 +0100 Subject: [PATCH 15/16] core/vm: Define an operation for INVALID(0xfe) It is required to distinguish INVALID in the jump table from undefined opcodes. --- core/vm/instructions.go | 4 ++++ core/vm/jump_table.go | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/core/vm/instructions.go b/core/vm/instructions.go index cb31970454d9..3de9d51dbc4f 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -819,6 +819,10 @@ func opStop(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byt return nil, errStopToken } +func opInvalid(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { + return nil, &ErrInvalidOpCode{opcode: INVALID} +} + func opSelfdestruct(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { if interpreter.readOnly { return nil, ErrWriteProtection diff --git a/core/vm/jump_table.go b/core/vm/jump_table.go index 25024c17aef9..5e55f18e2d0d 100644 --- a/core/vm/jump_table.go +++ b/core/vm/jump_table.go @@ -1028,6 +1028,12 @@ func newFrontierInstructionSet() JumpTable { maxStack: maxStack(2, 0), memorySize: memoryReturn, }, + INVALID: { + execute: opInvalid, + constantGas: 0, + minStack: minStack(0, 0), + maxStack: maxStack(0, 0), + }, SELFDESTRUCT: { execute: opSelfdestruct, dynamicGas: gasSelfdestruct, From 4363b033a32393e91d5d25ac878e1e37869d94c5 Mon Sep 17 00:00:00 2001 From: Hugo De la cruz Date: Thu, 3 Nov 2022 19:08:04 -0700 Subject: [PATCH 16/16] Implement EIP-4200 --- core/vm/eips.go | 57 +++++++++++++++++++++++++++++++++++++++ core/vm/eof.go | 41 +++++++++++++++++++++++++++- core/vm/eof_test.go | 36 +++++++++++++++++++++++-- core/vm/errors.go | 2 ++ core/vm/jump_table.go | 7 +++++ core/vm/opcodes.go | 6 +++++ params/protocol_params.go | 2 ++ 7 files changed, 148 insertions(+), 3 deletions(-) diff --git a/core/vm/eips.go b/core/vm/eips.go index 7467eab78719..80b9eb01d716 100644 --- a/core/vm/eips.go +++ b/core/vm/eips.go @@ -17,6 +17,7 @@ package vm import ( + "encoding/binary" "fmt" "sort" @@ -34,6 +35,7 @@ var activators = map[int]func(*JumpTable){ 1344: enable1344, 3540: enable3540, 3670: enable3670, + 4200: enable4200, } // EnableEIP enables the given EIP on the config. @@ -202,3 +204,58 @@ func enable3540(jt *JumpTable) { func enable3670(jt *JumpTable) { // Do nothing. } + +func enable4200(jt *JumpTable) { + // New opcodes + jt[RJUMP] = &operation{ + execute: opRjump, + constantGas: params.RjumpGas, + minStack: minStack(0, 0), + maxStack: maxStack(0, 0), + } + + jt[RJUMPI] = &operation{ + execute: opRjumpi, + constantGas: params.RjumpiGas, + minStack: minStack(0, 0), + maxStack: maxStack(0, 0), + } +} + +func opRjump(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { + immediateSize := 2 + codeEnd := int(len(scope.Contract.Container)) + start := codeEnd + pcAbsolute := scope.Contract.CodeBeginOffset + *pc + if int(pcAbsolute+1) < start { + start = int(pcAbsolute + 1) + } + + end := start + immediateSize + + if end >= codeEnd { + return nil, ErrEOF1TerminatingInstructionMissing + } + + *pc = *pc + uint64(immediateSize) + bytes := scope.Contract.Container[start:end] + pos := int16(binary.BigEndian.Uint16(bytes)) + + if pos < 0 { + *pc = *pc - uint64(pos*-1) + } else { + *pc = *pc + uint64(pos) + } + return nil, nil +} + +func opRjumpi(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { + cond := scope.Stack.pop() + if !cond.IsZero() { + return opRjump(pc, interpreter, scope) + } else { + *pc += 2 + } + + return nil, nil +} diff --git a/core/vm/eof.go b/core/vm/eof.go index b4ccf771591e..467cb602fcb6 100644 --- a/core/vm/eof.go +++ b/core/vm/eof.go @@ -121,17 +121,56 @@ sectionLoop: func validateInstructions(code []byte, header *EOF1Header, jumpTable *JumpTable) error { i := header.CodeBeginOffset() var opcode OpCode + jumpdests := map[uint64]bool{} + immediates := map[uint64]bool{} + for i < header.CodeEndOffset() { opcode = OpCode(code[i]) if jumpTable[opcode].undefined { return ErrEOF1UndefinedInstruction } if opcode >= PUSH1 && opcode <= PUSH32 { - i += uint64(opcode) - uint64(PUSH1) + 1 + pushSize := uint64(opcode) - uint64(PUSH1) + 1 + for j := i + 1; j <= i+pushSize; j++ { + immediates[j] = true + } + i += pushSize + } + + if opcode == RJUMP || opcode == RJUMPI { + i += 1 + if i+1 >= header.CodeEndOffset() { + break + } + + rjdBytes := code[i : i+2] + offset := int16(binary.BigEndian.Uint16(rjdBytes)) + + var rjumpdest uint64 + if offset < 0 { + rjumpdest = i + 2 - uint64(offset*-1) + } else { + rjumpdest = i + 2 + uint64(offset) + } + + if rjumpdest < header.CodeBeginOffset() || rjumpdest >= header.CodeEndOffset() { + return ErrEOF1RJumpDestinationOutOfBounds + } + + jumpdests[rjumpdest] = true + immediates[i] = true + immediates[i+1] = true + i += 1 } i += 1 } + for k := range jumpdests { + if immediates[k] { + return ErrEOF1RJumpDestTargetsImmediate + } + } + if !opcode.isTerminating() { return ErrEOF1TerminatingInstructionMissing } diff --git a/core/vm/eof_test.go b/core/vm/eof_test.go index 681e999da695..78cc71a37b88 100644 --- a/core/vm/eof_test.go +++ b/core/vm/eof_test.go @@ -42,6 +42,10 @@ var eof1ValidTests = []eof1Test{ {"EF0001010022007F000000000000000000000000000000000000000000000000000000000000000000", 34, 0}, // PUSH32 {"EF0001010022007F0C0D0E0F1E1F2122232425262728292A2B2C2D2E2F494A4B4C4D4E4F5C5D5E5F00", 34, 0}, // undefined instructions inside push data {"EF000101000102002000000C0D0E0F1E1F2122232425262728292A2B2C2D2E2F494A4B4C4D4E4F5C5D5E5F", 1, 32}, // undefined instructions inside data section + {"EF000101000E005C00056002600255600160015500", 14, 0}, // RJUMP + {"EF000101000F005C000560026002555B600160015500", 15, 0}, // RJUMP (to JUMPDEST) + {"EF00010100100060015D00056002600255600160015500", 16, 0}, // RJUMPI + {"EF00010100110060015D000560026002555B600160015500", 17, 0}, // RJUMPI (to JUMPDEST) } type eof1InvalidTest struct { @@ -104,6 +108,34 @@ var eof1InvalidInstructionsTests = []eof1InvalidTest{ {"EF0001010020007F00000000000000000000000000000000000000000000000000000000000000", ErrEOF1TerminatingInstructionMissing.Error()}, // PUSH32 with 32 bytes of data and no terminating instruction {"EF0001010021007F0000000000000000000000000000000000000000000000000000000000000000", ErrEOF1TerminatingInstructionMissing.Error()}, + // RJUMP truncated offset (final instruction in code section) + {"EF00010100060060016001555C", ErrEOF1TerminatingInstructionMissing.Error()}, + // RJUMP truncated offset + {"EF00010100070060016001555C00", ErrEOF1TerminatingInstructionMissing.Error()}, + // RJUMP to RJUMP's immediate + {"EF0001010011005C000660026002555C0000600160015500", ErrEOF1RJumpDestTargetsImmediate.Error()}, + // RJUMP to PUSH's immediate + {"EF000101000A006400000000005CFFFC00", ErrEOF1RJumpDestTargetsImmediate.Error()}, + // RJUMP to PUSH's immediate + {"EF000101000A006400000000005CFFFB00", ErrEOF1RJumpDestTargetsImmediate.Error()}, + // RJUMP to RJUMPI's immediate + {"EF000101000D005C00086001600155005DFFF700", ErrEOF1RJumpDestTargetsImmediate.Error()}, + // RJUMP target outside of code section bounds + {"EF0001010004020004005C000300AABBCCDD", ErrEOF1RJumpDestinationOutOfBounds.Error()}, + // RJUMPI truncated offset (final instruction in code section) + {"EF000101000800600160015560015d", ErrEOF1TerminatingInstructionMissing.Error()}, + // RJUMPI truncated offset + {"EF000101000900600160015560015d00", ErrEOF1TerminatingInstructionMissing.Error()}, + // RJUMPI to RJUMP's immediate + {"EF00010100130060015D000660026002555C0000600160015500", ErrEOF1RJumpDestTargetsImmediate.Error()}, + // RJUMPI to PUSH's immediate + {"EF000101000C0064000000000060015DFFFA00", ErrEOF1RJumpDestTargetsImmediate.Error()}, + // RJUMPI to PUSHs immediate + {"EF000101000C0064000000000060015CFFF900", ErrEOF1RJumpDestTargetsImmediate.Error()}, + // RJUMPI to RJUMPI's immediate + {"EF000101000F0060015D00086001600155005DFFF700", ErrEOF1RJumpDestTargetsImmediate.Error()}, + // RJUMPI target outside of code section bounds + {"EF00010100060200040060015D000300AABBCCDD", ErrEOF1RJumpDestinationOutOfBounds.Error()}, } func TestHasEOFMagic(t *testing.T) { @@ -152,7 +184,7 @@ func TestReadEOF1Header(t *testing.T) { } func TestValidateEOF(t *testing.T) { - jt := &mergeInstructionSet + jt := &eip4200InstructionSet for _, test := range eof1ValidTests { _, err := validateEOF(common.Hex2Bytes(test.code), jt) if err != nil { @@ -181,7 +213,7 @@ func TestReadValidEOF1Header(t *testing.T) { } func TestValidateInstructions(t *testing.T) { - jt := &londonInstructionSet + jt := &eip4200InstructionSet for _, test := range eof1ValidTests { code := common.Hex2Bytes(test.code) header, err := readEOF1Header(code) diff --git a/core/vm/errors.go b/core/vm/errors.go index b0e76803d6cb..94a426adc6cb 100644 --- a/core/vm/errors.go +++ b/core/vm/errors.go @@ -58,6 +58,8 @@ var ( ErrEOF1InvalidTotalSize = errors.New("invalid total size") ErrEOF1UndefinedInstruction = errors.New("undefined instruction") ErrEOF1TerminatingInstructionMissing = errors.New("code section doesn't end with terminating instruction") + ErrEOF1RJumpDestinationOutOfBounds = errors.New("relative jump destination out of bounds") + ErrEOF1RJumpDestTargetsImmediate = errors.New("relative jump destination targets immediate") ) // ErrStackUnderflow wraps an evm error when the items on the stack less diff --git a/core/vm/jump_table.go b/core/vm/jump_table.go index 5e55f18e2d0d..6bf00e9b2ae5 100644 --- a/core/vm/jump_table.go +++ b/core/vm/jump_table.go @@ -57,6 +57,7 @@ var ( berlinInstructionSet = newBerlinInstructionSet() londonInstructionSet = newLondonInstructionSet() mergeInstructionSet = newMergeInstructionSet() + eip4200InstructionSet = newEIP4200InstructionSet() ) // JumpTable contains the EVM opcodes supported at a given fork. @@ -80,6 +81,12 @@ func validate(jt JumpTable) JumpTable { return jt } +func newEIP4200InstructionSet() JumpTable { + instructionSet := newMergeInstructionSet() + enable4200(&instructionSet) + return validate(instructionSet) +} + func newMergeInstructionSet() JumpTable { instructionSet := newLondonInstructionSet() instructionSet[PREVRANDAO] = &operation{ diff --git a/core/vm/opcodes.go b/core/vm/opcodes.go index 1a35e89f473e..339cf88ae764 100644 --- a/core/vm/opcodes.go +++ b/core/vm/opcodes.go @@ -116,6 +116,8 @@ const ( MSIZE OpCode = 0x59 GAS OpCode = 0x5a JUMPDEST OpCode = 0x5b + RJUMP OpCode = 0x5c + RJUMPI OpCode = 0x5d PUSH0 OpCode = 0x5f ) @@ -298,6 +300,8 @@ var opCodeToString = map[OpCode]string{ MSIZE: "MSIZE", GAS: "GAS", JUMPDEST: "JUMPDEST", + RJUMP: "RJUMP", + RJUMPI: "RJUMPI", PUSH0: "PUSH0", // 0x60 range - push. @@ -462,6 +466,8 @@ var stringToOp = map[string]OpCode{ "MSIZE": MSIZE, "GAS": GAS, "JUMPDEST": JUMPDEST, + "RJUMP": RJUMP, + "RJUMPI": RJUMPI, "PUSH0": PUSH0, "PUSH1": PUSH1, "PUSH2": PUSH2, diff --git a/params/protocol_params.go b/params/protocol_params.go index b0037fd471c4..9f4c2d8b29f2 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -104,6 +104,8 @@ const ( ExtcodeHashGasConstantinople uint64 = 400 // Cost of EXTCODEHASH (introduced in Constantinople) ExtcodeHashGasEIP1884 uint64 = 700 // Cost of EXTCODEHASH after EIP 1884 (part in Istanbul) SelfdestructGasEIP150 uint64 = 5000 // Cost of SELFDESTRUCT post EIP 150 (Tangerine) + RjumpGas uint64 = 5 // Cost of RJUMP defined in EIP-4200 + RjumpiGas uint64 = 7 // Cost of RJUMPI defined in EIP-4200 // EXP has a dynamic portion depending on the size of the exponent ExpByteFrontier uint64 = 10 // was set to 10 in Frontier