From 492843d1858e4a615faf71a5aa7e37e8621b211e Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Tue, 6 Dec 2022 14:23:27 -0700 Subject: [PATCH 01/53] core/vm: add eof container --- core/vm/eof.go | 236 ++++++++++++++++++++++++++++++++++++++++++++ core/vm/eof_test.go | 65 ++++++++++++ 2 files changed, 301 insertions(+) create mode 100644 core/vm/eof.go create mode 100644 core/vm/eof_test.go diff --git a/core/vm/eof.go b/core/vm/eof.go new file mode 100644 index 000000000000..dcf29d25adb6 --- /dev/null +++ b/core/vm/eof.go @@ -0,0 +1,236 @@ +// Copyright 2022 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 ( + "bytes" + "fmt" +) + +const ( + offsetVersion = 2 + offsetTypesKind = 3 + offsetCodeKind = 6 + + kindTypes = 1 + kindCode = 2 + kindData = 3 + + eofFormatByte = 0xef + eof1Version = 1 +) + +var eofMagic = []byte{0xef, 0x00} + +// hasEOFByte returns true if code starts with 0xEF byte +func hasEOFByte(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(eofMagic) <= len(code) && bytes.Equal(eofMagic, code[0:len(eofMagic)]) +} + +// isEOFVersion1 returns true if the code's version byte equals eof1Version. It +// does not verify the EOF magic is valid. +func isEOFVersion1(code []byte) bool { + return 2 < len(code) && code[2] == byte(eof1Version) +} + +// Container is and EOF container object. +type Container struct { + Types []TypeAnnotation + Code [][]byte + Data []byte +} + +// TypeAnnotation is an EOF function signature. +type TypeAnnotation struct { + input uint8 + output uint8 + maxStackHeight uint16 +} + +// MarshalBinary encodes an EOF container into binary format. +func (c *Container) MarshalBinary() []byte { + b := make([]byte, 2) + copy(b, eofMagic) + b = append(b, eof1Version) + b = append(b, kindTypes) + b = appendUint16(b, uint16(len(c.Types)*4)) + b = append(b, kindCode) + b = appendUint16(b, uint16(len(c.Code))) + for _, code := range c.Code { + b = appendUint16(b, uint16(len(code))) + } + b = append(b, kindData) + b = appendUint16(b, uint16(len(c.Data))) + b = append(b, 0) // terminator + + for _, ty := range c.Types { + b = append(b, []byte{ty.input, ty.output, byte(ty.maxStackHeight >> 8), byte(ty.maxStackHeight & 0x00ff)}...) + } + for _, code := range c.Code { + b = append(b, code...) + } + b = append(b, c.Data...) + return b +} + +// UnmarshalBinary decodes an EOF container. +func (c *Container) UnmarshalBinary(b []byte) error { + if len(b) < 9 { + return fmt.Errorf("container size less than minimum valid size") + } + if !bytes.Equal(b[0:2], eofMagic) { + return fmt.Errorf("invalid magic") + } + if check(b, offsetVersion, eof1Version) { + return fmt.Errorf("invalid eof version") + } + + var ( + typesSize, dataSize int + codeSizes []int + kind int + ) + + // Parse types size. + if kind, typesSize = parseSection(b, offsetTypesKind); kind != kindTypes { + return fmt.Errorf("expected kind types") + } + if typesSize > 4*1024 { + return fmt.Errorf("number of code sections must not exceed 1024 (got %d)", typesSize) + } + + // Parse code sizes. + if kind, codeSizes = parseSectionList(b, offsetCodeKind); kind != kindCode { + return fmt.Errorf("expected kind code") + } + if len(codeSizes) != typesSize/4 { + return fmt.Errorf("mismatch of code sections count and type signatures (types %d, code %d)", typesSize/3, len(codeSizes)) + } + + // Parse data size. + offsetDataKind := offsetCodeKind + 2 + 2*len(codeSizes) + 1 + if len(b) < offsetDataKind+2 { + return fmt.Errorf("container size invalid") + } + if kind, dataSize = parseSection(b, offsetDataKind); kind != kindData { + return fmt.Errorf("expected kind data") + } + offsetTerminator := offsetDataKind + 3 + if check(b, offsetTerminator, 0) { + return fmt.Errorf("expected terminator") + } + + // Check for terminator. + expectedSize := offsetTerminator + typesSize + sum(codeSizes) + dataSize + 1 + if len(b) != expectedSize { + fmt.Println(codeSizes) + fmt.Println(offsetTerminator, typesSize, sum(codeSizes), dataSize) + return fmt.Errorf("invalid container size (want %d, got %d)", expectedSize, len(b)) + } + + // Parse types section. + idx := offsetTerminator + 1 + var types []TypeAnnotation + for i := 0; i < typesSize/4; i++ { + sig := TypeAnnotation{ + input: b[idx+i*4], + output: b[idx+i*4+1], + maxStackHeight: uint16(parseUint16(b[idx+i*4+2:])), + } + if sig.maxStackHeight > 1024 { + return fmt.Errorf("type annotation %d max stack height must not exceed 1024", i) + } + types = append(types, sig) + } + if types[0].input != 0 || types[0].output != 0 { + return fmt.Errorf("input and output of first code section must be 0") + } + c.Types = types + + // Parse code sections. + idx += typesSize + code := make([][]byte, len(codeSizes)) + for i, size := range codeSizes { + if size == 0 { + return fmt.Errorf("code section %d size must not be 0", i) + } + code[i] = b[idx : idx+size] + idx += size + } + c.Code = code + + // Parse data section. + c.Data = b[idx : idx+dataSize] + + return nil +} + +// parseSection decodes a (kind, size) pair from an EOF header. +func parseSection(b []byte, idx int) (kind, size int) { + return int(b[idx]), parseUint16(b[idx+1:]) +} + +// parseSectionList decodes a (kind, len, []codeSize) section list from an EOF +// header. +func parseSectionList(b []byte, idx int) (kind int, list []int) { + return int(b[idx]), parseList(b, idx+1) +} + +// parseList decodes a list of uint16.. +func parseList(b []byte, idx int) []int { + count := parseUint16(b[idx:]) + list := make([]int, count) + for i := 0; i < count; i++ { + list[i] = parseUint16(b[idx+2*i+2:]) + } + return list +} + +// parseUint16 parses a 16 bit unsigned integer. +func parseUint16(b []byte) int { + size := uint16(b[0]) << 8 + size += uint16(b[1]) + return int(size) +} + +// check returns if b[idx] == want after performing a bounds check. +func check(b []byte, idx int, want byte) bool { + if len(b) < idx { + return false + } + return b[idx] != want +} + +func sum(list []int) (s int) { + for _, n := range list { + s += n + + } + return +} + +func appendUint16(b []byte, v uint16) []byte { + return append(b, + byte(v>>8), + byte(v), + ) +} diff --git a/core/vm/eof_test.go b/core/vm/eof_test.go new file mode 100644 index 000000000000..7b470d4dcf35 --- /dev/null +++ b/core/vm/eof_test.go @@ -0,0 +1,65 @@ +// Copyright 2022 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 ( + "reflect" + "testing" + + "github.com/ethereum/go-ethereum/common" +) + +func TestEOFMarshaling(t *testing.T) { + for i, test := range []struct { + want Container + err error + }{ + { + want: Container{ + Types: []TypeAnnotation{{input: 0, output: 0, maxStackHeight: 0}}, + Code: [][]byte{common.Hex2Bytes("604200")}, + Data: []byte{0x01, 0x02, 0x03}, + }, + }, + { + want: Container{ + Types: []TypeAnnotation{ + {input: 0, output: 0, maxStackHeight: 0}, + {input: 2, output: 3, maxStackHeight: 32}, + {input: 1, output: 1, maxStackHeight: 1023}, + }, + Code: [][]byte{ + common.Hex2Bytes("604200"), + common.Hex2Bytes("6042604200"), + common.Hex2Bytes("00"), + }, + Data: []byte{}, + }, + }, + } { + var ( + b = test.want.MarshalBinary() + got Container + ) + if err := got.UnmarshalBinary(b); err != nil && err != test.err { + t.Fatalf("test %d: got error \"%v\", want \"%v\"", i, err, test.err) + } + if !reflect.DeepEqual(got, test.want) { + t.Fatalf("test %d: got %+v, want %+v", i, got, test.want) + } + } +} From f2566783511d15f7343ce3b90b51da6b324be369 Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Thu, 8 Dec 2022 14:46:57 -0700 Subject: [PATCH 02/53] core/vm: add eof execution semantics --- core/blockchain_test.go | 145 ++++++++++++++++++++++++++++++++++ core/vm/contract.go | 31 ++++++-- core/vm/eips.go | 148 +++++++++++++++++++++++++++++++++++ core/vm/eof.go | 36 ++++++--- core/vm/eof_test.go | 8 +- core/vm/errors.go | 1 + core/vm/evm.go | 45 ++++++++--- core/vm/gas.go | 1 + core/vm/instructions.go | 10 ++- core/vm/instructions_test.go | 20 ++--- core/vm/interpreter.go | 32 ++++++-- core/vm/jump_table.go | 7 ++ core/vm/opcodes.go | 16 ++++ 13 files changed, 446 insertions(+), 54 deletions(-) diff --git a/core/blockchain_test.go b/core/blockchain_test.go index 36bfa0752558..f333f8a0c987 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -4332,3 +4332,148 @@ func TestEIP3651(t *testing.T) { t.Fatalf("sender balance incorrect: expected %d, got %d", expected, actual) } } + +func int8ToByte(n int8) uint8 { + return uint8(n) +} + +func TestEOF(t *testing.T) { + var ( + aa = common.HexToAddress("0x000000000000000000000000000000000000aaaa") + engine = ethash.NewFaker() + + // A sender who makes transactions, has some funds + key1, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + key2, _ = crypto.HexToECDSA("8a1f9a8f95be41cd7ccb6168179afb4504aefe388d1e14474d32c45c72ce7b7a") + addr1 = crypto.PubkeyToAddress(key1.PublicKey) + addr2 = crypto.PubkeyToAddress(key2.PublicKey) + funds = new(big.Int).Mul(common.Big1, big.NewInt(params.Ether)) + gspec = &Genesis{ + Config: params.AllEthashProtocolChanges, + Alloc: GenesisAlloc{ + addr1: {Balance: funds}, + addr2: {Balance: funds}, + // The address 0xAAAA sloads 0x00 and 0x01 + aa: { + Code: (&vm.Container{ + Types: []vm.TypeAnnotation{ + {Input: 0, Output: 0, MaxStackHeight: 0}, + {Input: 0, Output: 0, MaxStackHeight: 2}, + {Input: 0, Output: 0, MaxStackHeight: 0}, + {Input: 0, Output: 0, MaxStackHeight: 2}, + }, + Code: [][]byte{ + { + byte(vm.CALLF), + byte(0), + byte(1), + byte(vm.CALLF), + byte(0), + byte(2), + byte(vm.STOP), + }, + { + byte(vm.PUSH1), + byte(2), + byte(vm.RJUMP), // skip first flag + byte(0), + byte(5), + + byte(vm.PUSH1), + byte(1), + byte(vm.PUSH1), + byte(0), + byte(vm.SSTORE), // set first flag + + byte(vm.PUSH1), + byte(1), + byte(vm.SWAP1), + byte(vm.SUB), + byte(vm.DUP1), + byte(vm.RJUMPI), // jump to first flag, then don't branch + byte(0xff), + int8ToByte(-13), + + byte(vm.PUSH1), + byte(1), + byte(vm.PUSH1), + byte(1), + byte(vm.SSTORE), // set second flag + byte(vm.RETF), + }, + { + + byte(vm.PUSH1), + byte(1), + byte(vm.PUSH1), + byte(2), + byte(vm.SSTORE), // set third flag + + byte(vm.JUMPF), + byte(0), + byte(3), + }, + { + byte(vm.PUSH1), + byte(0), + byte(vm.RJUMPV), // jump over invalid op + byte(1), + byte(0), + byte(1), + + byte(vm.INVALID), + + byte(vm.PUSH1), + byte(1), + byte(vm.PUSH1), + byte(3), + byte(vm.SSTORE), // set forth flag + byte(vm.RETF), + }, + }, + Data: []byte{}, + }).MarshalBinary(), + Nonce: 0, + Balance: big.NewInt(0), + }, + }, + } + ) + + gspec.Config.BerlinBlock = common.Big0 + gspec.Config.LondonBlock = common.Big0 + gspec.Config.ShanghaiTime = common.Big0 + signer := types.LatestSigner(gspec.Config) + + _, blocks, _ := GenerateChainWithGenesis(gspec, engine, 1, func(i int, b *BlockGen) { + b.SetCoinbase(aa) + txdata := &types.DynamicFeeTx{ + ChainID: gspec.Config.ChainID, + Nonce: 0, + To: &aa, + Gas: 500000, + GasFeeCap: newGwei(5), + GasTipCap: big.NewInt(2), + AccessList: nil, + Data: []byte{}, + } + tx := types.NewTx(txdata) + tx, _ = types.SignTx(tx, signer, key1) + + b.AddTx(tx) + }) + chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), nil, gspec, nil, engine, vm.Config{Debug: true, Tracer: logger.NewMarkdownLogger(&logger.Config{}, os.Stderr)}, 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) + } + + state, _ := chain.State() + for i := 0; i < 4; i++ { + if state.GetState(aa, common.BigToHash(big.NewInt(int64(i)))).Big().Uint64() != 1 { + t.Fatalf("flag %d not set", i) + } + } +} diff --git a/core/vm/contract.go b/core/vm/contract.go index bb0902969ec7..b86c764daa0b 100644 --- a/core/vm/contract.go +++ b/core/vm/contract.go @@ -53,10 +53,11 @@ 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 + Code []byte + Container *Container + CodeHash common.Hash + CodeAddr *common.Address + Input []byte Gas uint64 value *big.Int @@ -142,11 +143,12 @@ 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)) { +func (c *Contract) GetOp(n uint64, s uint64) OpCode { + if c.IsEOF() && n < uint64(len(c.Container.Code[s])) { + return OpCode(c.Container.Code[s][n]) + } else if n < uint64(len(c.Code)) { return OpCode(c.Code[n]) } - return STOP } @@ -177,10 +179,23 @@ func (c *Contract) Value() *big.Int { return c.value } +// IsEOF returns whether the contract is EOF. +func (c *Contract) IsEOF() bool { + return c.Container != nil +} + +func (c *Contract) CodeAt(section uint64) []byte { + if c.Container == nil { + return c.Code + } + return c.Container.Code[section] +} + // 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, container *Container) { c.Code = code + c.Container = container c.CodeHash = hash c.CodeAddr = addr } diff --git a/core/vm/eips.go b/core/vm/eips.go index 29ff27c55268..2597b1d7b20b 100644 --- a/core/vm/eips.go +++ b/core/vm/eips.go @@ -241,3 +241,151 @@ func enable3860(jt *JumpTable) { jt[CREATE].dynamicGas = gasCreateEip3860 jt[CREATE2].dynamicGas = gasCreate2Eip3860 } + +// enableEOF applies the EOF changes. +func enableEOF(jt *JumpTable) { + // Deprecate opcodes + undefined := &operation{ + execute: opUndefined, + constantGas: 0, + minStack: minStack(0, 0), + maxStack: maxStack(0, 0), + } + jt[CALLCODE] = undefined + jt[SELFDESTRUCT] = undefined + jt[JUMP] = undefined + jt[JUMPI] = undefined + jt[PC] = undefined + + // New opcodes + jt[RJUMP] = &operation{ + execute: opRjump, + constantGas: GasQuickStep, + minStack: minStack(0, 0), + maxStack: maxStack(0, 0), + } + jt[RJUMPI] = &operation{ + execute: opRjumpi, + constantGas: GasFastishStep, + minStack: minStack(1, 0), + maxStack: maxStack(1, 0), + } + jt[RJUMPV] = &operation{ + execute: opRjumpv, + constantGas: GasFastishStep, + minStack: minStack(1, 0), + maxStack: maxStack(1, 0), + } + jt[CALLF] = &operation{ + execute: opCallf, + constantGas: GasFastStep, + minStack: minStack(0, 0), + maxStack: maxStack(0, 0), + } + jt[RETF] = &operation{ + execute: opRetf, + constantGas: GasFastishStep, + minStack: minStack(0, 0), + maxStack: maxStack(0, 0), + } + jt[JUMPF] = &operation{ + execute: opJumpf, + constantGas: GasFastestStep, + minStack: minStack(0, 0), + maxStack: maxStack(0, 0), + } +} + +// opRjump implements the rjump opcode. +func opRjump(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { + var ( + code = scope.Contract.CodeAt(scope.CodeSection) + offset = parseInt16(code[*pc+1:]) + ) + // move pc past op and operand (+3), add relative offset, subtract 1 to + // account for interpreter loop. + *pc = uint64(int64(*pc+3) + int64(offset) - 1) + return nil, nil +} + +// opRjumpi implements the RJUMPI opcode +func opRjumpi(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { + condition := scope.Stack.pop() + if condition.BitLen() == 0 { + // Not branching, just skip over immediate argument. + *pc += 2 + return nil, nil + } + return opRjump(pc, interpreter, scope) +} + +// opRjumpv implements the RJUMPV opcode +func opRjumpv(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { + var ( + code = scope.Contract.CodeAt(scope.CodeSection) + count = uint64(code[*pc+1]) + idx = scope.Stack.pop() + ) + if idx.Uint64() >= uint64(count) { + // Index out-of-bounds, don't branch, just skip over immediate + // argument. + *pc += 1 + count*2 + return nil, nil + } + offset := parseInt16(code[*pc+2+2*idx.Uint64():]) + *pc = uint64(int64(*pc+2+count*2) + int64(offset) - 1) + return nil, nil +} + +// opCallf implements the CALLF opcode +func opCallf(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { + var ( + code = scope.Contract.CodeAt(scope.CodeSection) + idx = parseUint16(code[*pc+1:]) + typ = scope.Contract.Container.Types[scope.CodeSection] + ) + if scope.Stack.len()+int(typ.MaxStackHeight) >= 1024 { + return nil, fmt.Errorf("stack overflow") + } + retCtx := &ReturnContext{ + Section: scope.CodeSection, + Pc: *pc + 3, + StackHeight: scope.Stack.len() - int(typ.Input), + } + scope.ReturnStack = append(scope.ReturnStack, retCtx) + scope.CodeSection = uint64(idx) + *pc = 0 + *pc -= 1 // hacks xD + return nil, nil +} + +// opRetf implements the RETF opcode +func opRetf(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { + var ( + last = len(scope.ReturnStack) - 1 + retCtx = scope.ReturnStack[last] + ) + scope.ReturnStack = scope.ReturnStack[:last] + scope.CodeSection = retCtx.Section + *pc = retCtx.Pc - 1 + return nil, nil +} + +// opJumpf implements the JUMPF opcode +func opJumpf(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { + var ( + code = scope.Contract.CodeAt(scope.CodeSection) + idx = parseUint16(code[*pc+1:]) + ) + scope.CodeSection = uint64(idx) + *pc = 0 + *pc -= 1 // hacks xD + return nil, nil +} + +// parseInt16 returns the int16 located at b[0:2]. +func parseInt16(b []byte) int16 { + n := uint16(b[0]) << 8 + n += uint16(b[1]) + return int16(n) +} diff --git a/core/vm/eof.go b/core/vm/eof.go index dcf29d25adb6..1bc12c6c6c23 100644 --- a/core/vm/eof.go +++ b/core/vm/eof.go @@ -61,9 +61,9 @@ type Container struct { // TypeAnnotation is an EOF function signature. type TypeAnnotation struct { - input uint8 - output uint8 - maxStackHeight uint16 + Input uint8 + Output uint8 + MaxStackHeight uint16 } // MarshalBinary encodes an EOF container into binary format. @@ -83,7 +83,7 @@ func (c *Container) MarshalBinary() []byte { b = append(b, 0) // terminator for _, ty := range c.Types { - b = append(b, []byte{ty.input, ty.output, byte(ty.maxStackHeight >> 8), byte(ty.maxStackHeight & 0x00ff)}...) + b = append(b, []byte{ty.Input, ty.Output, byte(ty.MaxStackHeight >> 8), byte(ty.MaxStackHeight & 0x00ff)}...) } for _, code := range c.Code { b = append(b, code...) @@ -94,16 +94,17 @@ func (c *Container) MarshalBinary() []byte { // UnmarshalBinary decodes an EOF container. func (c *Container) UnmarshalBinary(b []byte) error { - if len(b) < 9 { - return fmt.Errorf("container size less than minimum valid size") - } - if !bytes.Equal(b[0:2], eofMagic) { + if !hasEOFMagic(b) { return fmt.Errorf("invalid magic") } if check(b, offsetVersion, eof1Version) { return fmt.Errorf("invalid eof version") } + if len(b) < 9 { + return fmt.Errorf("container size less than minimum valid size") + } + var ( typesSize, dataSize int codeSizes []int @@ -152,16 +153,16 @@ func (c *Container) UnmarshalBinary(b []byte) error { var types []TypeAnnotation for i := 0; i < typesSize/4; i++ { sig := TypeAnnotation{ - input: b[idx+i*4], - output: b[idx+i*4+1], - maxStackHeight: uint16(parseUint16(b[idx+i*4+2:])), + Input: b[idx+i*4], + Output: b[idx+i*4+1], + MaxStackHeight: uint16(parseUint16(b[idx+i*4+2:])), } - if sig.maxStackHeight > 1024 { + if sig.MaxStackHeight > 1024 { return fmt.Errorf("type annotation %d max stack height must not exceed 1024", i) } types = append(types, sig) } - if types[0].input != 0 || types[0].output != 0 { + if types[0].Input != 0 || types[0].Output != 0 { return fmt.Errorf("input and output of first code section must be 0") } c.Types = types @@ -184,6 +185,15 @@ func (c *Container) UnmarshalBinary(b []byte) error { return nil } +func (c *Container) ValidateCode() error { + // for _, code := range c.Code { + // if err := validateCode(code); err != nil { + // return err + // } + // } + return nil +} + // parseSection decodes a (kind, size) pair from an EOF header. func parseSection(b []byte, idx int) (kind, size int) { return int(b[idx]), parseUint16(b[idx+1:]) diff --git a/core/vm/eof_test.go b/core/vm/eof_test.go index 7b470d4dcf35..61eddc8a5465 100644 --- a/core/vm/eof_test.go +++ b/core/vm/eof_test.go @@ -30,7 +30,7 @@ func TestEOFMarshaling(t *testing.T) { }{ { want: Container{ - Types: []TypeAnnotation{{input: 0, output: 0, maxStackHeight: 0}}, + Types: []TypeAnnotation{{Input: 0, Output: 0, MaxStackHeight: 0}}, Code: [][]byte{common.Hex2Bytes("604200")}, Data: []byte{0x01, 0x02, 0x03}, }, @@ -38,9 +38,9 @@ func TestEOFMarshaling(t *testing.T) { { want: Container{ Types: []TypeAnnotation{ - {input: 0, output: 0, maxStackHeight: 0}, - {input: 2, output: 3, maxStackHeight: 32}, - {input: 1, output: 1, maxStackHeight: 1023}, + {Input: 0, Output: 0, MaxStackHeight: 0}, + {Input: 2, Output: 3, MaxStackHeight: 32}, + {Input: 1, Output: 1, MaxStackHeight: 1023}, }, Code: [][]byte{ common.Hex2Bytes("604200"), diff --git a/core/vm/errors.go b/core/vm/errors.go index fbbf19e178bf..3909d9c3d342 100644 --- a/core/vm/errors.go +++ b/core/vm/errors.go @@ -36,6 +36,7 @@ var ( ErrReturnDataOutOfBounds = errors.New("return data out of bounds") ErrGasUintOverflow = errors.New("gas uint64 overflow") ErrInvalidCode = errors.New("invalid code: must not begin with 0xef") + ErrInvalidEOF = errors.New("invalid eof") ErrNonceUintOverflow = errors.New("nonce uint64 overflow") // errStopToken is an internal token indicating interpreter loop termination, diff --git a/core/vm/evm.go b/core/vm/evm.go index 149e9f761be3..4680066429b4 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -17,6 +17,7 @@ package vm import ( + "fmt" "math/big" "sync/atomic" @@ -223,7 +224,7 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas // 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, evm.maybeParseContainer(code)) ret, err = evm.interpreter.Run(contract, input, false) gas = contract.Gas } @@ -277,10 +278,11 @@ 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) // 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, evm.maybeParseContainer(code)) ret, err = evm.interpreter.Run(contract, input, false) gas = contract.Gas } @@ -318,9 +320,10 @@ 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) // 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, evm.maybeParseContainer(code)) ret, err = evm.interpreter.Run(contract, input, false) gas = contract.Gas } @@ -372,8 +375,9 @@ func (evm *EVM) StaticCall(caller ContractRef, addr common.Address, input []byte addrCopy := addr // 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. + code := evm.StateDB.GetCode(addrCopy) 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, evm.maybeParseContainer(code)) // 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. @@ -426,6 +430,21 @@ 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 } + + // 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) + + // If the initcode is EOF, verify it is well-formed. + if evm.chainRules.IsShanghai && hasEOFByte(codeAndHash.code) { + var c Container + if err := c.UnmarshalBinary(codeAndHash.code); err != nil { + return nil, common.Address{}, 0, fmt.Errorf("%v: %v", ErrInvalidEOF, err) + } + contract.Container = &c + } + // Create a new account on the state snapshot := evm.StateDB.Snapshot() evm.StateDB.CreateAccount(address) @@ -434,11 +453,6 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, } evm.Context.Transfer(evm.StateDB, caller.Address(), address, value) - // 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) - if evm.Config.Debug { if evm.depth == 0 { evm.Config.Tracer.CaptureStart(evm, caller.Address(), address, true, codeAndHash.code, gas, value) @@ -510,3 +524,16 @@ func (evm *EVM) Create2(caller ContractRef, code []byte, gas uint64, endowment * // ChainConfig returns the environment's chain configuration func (evm *EVM) ChainConfig() *params.ChainConfig { return evm.chainConfig } + +func (evm *EVM) maybeParseContainer(b []byte) *Container { + if evm.chainRules.IsShanghai { + var c Container + if err := c.UnmarshalBinary(b); err != nil && err.Error() == "invalid magic" { + return nil + } else if err != nil { + panic(fmt.Sprintf("unexpected error: %v", err)) + } + return &c + } + return nil +} diff --git a/core/vm/gas.go b/core/vm/gas.go index 5cf1d852d24a..5fe589bce696 100644 --- a/core/vm/gas.go +++ b/core/vm/gas.go @@ -24,6 +24,7 @@ import ( const ( GasQuickStep uint64 = 2 GasFastestStep uint64 = 3 + GasFastishStep uint64 = 4 GasFastStep uint64 = 5 GasMidStep uint64 = 8 GasSlowStep uint64 = 10 diff --git a/core/vm/instructions.go b/core/vm/instructions.go index 8fa2fc57e51f..945b323c67bd 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -865,12 +865,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)) + code = scope.Contract.CodeAt(scope.CodeSection) + codeLen = uint64(len(code)) integer = new(uint256.Int) ) *pc += 1 if *pc < codeLen { - scope.Stack.push(integer.SetUint64(uint64(scope.Contract.Code[*pc]))) + scope.Stack.push(integer.SetUint64(uint64(code[*pc]))) } else { scope.Stack.push(integer.Clear()) } @@ -880,7 +881,8 @@ 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) + code := scope.Contract.CodeAt(scope.CodeSection) + codeLen := len(code) startMin := codeLen if int(*pc+1) < startMin { @@ -894,7 +896,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))) + code[startMin:endMin], pushByteSize))) *pc += size return nil, nil diff --git a/core/vm/instructions_test.go b/core/vm/instructions_test.go index b4144a66fae9..503356a1e813 100644 --- a/core/vm/instructions_test.go +++ b/core/vm/instructions_test.go @@ -114,7 +114,7 @@ func testTwoOperandOp(t *testing.T, tests []TwoOperandTestcase, opFn executionFu expected := new(uint256.Int).SetBytes(common.Hex2Bytes(test.Expected)) stack.push(x) stack.push(y) - opFn(&pc, evmInterpreter, &ScopeContext{nil, stack, nil}) + opFn(&pc, evmInterpreter, &ScopeContext{nil, stack, nil, 0, nil}) if len(stack.data) != 1 { t.Errorf("Expected one item on stack after %v, got %d: ", name, len(stack.data)) } @@ -229,7 +229,7 @@ func TestAddMod(t *testing.T) { stack.push(z) stack.push(y) stack.push(x) - opAddmod(&pc, evmInterpreter, &ScopeContext{nil, stack, nil}) + opAddmod(&pc, evmInterpreter, &ScopeContext{nil, stack, nil, 0, nil}) actual := stack.pop() if actual.Cmp(expected) != 0 { t.Errorf("Testcase %d, expected %x, got %x", i, expected, actual) @@ -256,7 +256,7 @@ func TestWriteExpectedValues(t *testing.T) { y := new(uint256.Int).SetBytes(common.Hex2Bytes(param.y)) stack.push(x) stack.push(y) - opFn(&pc, interpreter, &ScopeContext{nil, stack, nil}) + opFn(&pc, interpreter, &ScopeContext{nil, stack, nil, 0, nil}) actual := stack.pop() result[i] = TwoOperandTestcase{param.x, param.y, fmt.Sprintf("%064x", actual)} } @@ -292,7 +292,7 @@ func opBenchmark(bench *testing.B, op executionFunc, args ...string) { var ( env = NewEVM(BlockContext{}, TxContext{}, nil, params.TestChainConfig, Config{}) stack = newstack() - scope = &ScopeContext{nil, stack, nil} + scope = &ScopeContext{nil, stack, nil, 0, nil} evmInterpreter = NewEVMInterpreter(env, env.Config) ) @@ -543,13 +543,13 @@ func TestOpMstore(t *testing.T) { v := "abcdef00000000000000abba000000000deaf000000c0de00100000000133700" stack.push(new(uint256.Int).SetBytes(common.Hex2Bytes(v))) stack.push(new(uint256.Int)) - opMstore(&pc, evmInterpreter, &ScopeContext{mem, stack, nil}) + opMstore(&pc, evmInterpreter, &ScopeContext{mem, stack, nil, 0, nil}) if got := common.Bytes2Hex(mem.GetCopy(0, 32)); got != v { t.Fatalf("Mstore fail, got %v, expected %v", got, v) } stack.push(new(uint256.Int).SetUint64(0x1)) stack.push(new(uint256.Int)) - opMstore(&pc, evmInterpreter, &ScopeContext{mem, stack, nil}) + opMstore(&pc, evmInterpreter, &ScopeContext{mem, stack, nil, 0, nil}) if common.Bytes2Hex(mem.GetCopy(0, 32)) != "0000000000000000000000000000000000000000000000000000000000000001" { t.Fatalf("Mstore failed to overwrite previous value") } @@ -573,7 +573,7 @@ func BenchmarkOpMstore(bench *testing.B) { for i := 0; i < bench.N; i++ { stack.push(value) stack.push(memStart) - opMstore(&pc, evmInterpreter, &ScopeContext{mem, stack, nil}) + opMstore(&pc, evmInterpreter, &ScopeContext{mem, stack, nil, 0, nil}) } } @@ -588,7 +588,7 @@ func TestOpTstore(t *testing.T) { to = common.Address{1} contractRef = contractRef{caller} contract = NewContract(contractRef, AccountRef(to), new(big.Int), 0) - scopeContext = ScopeContext{mem, stack, contract} + scopeContext = ScopeContext{mem, stack, contract, 0, nil} value = common.Hex2Bytes("abcdef00000000000000abba000000000deaf000000c0de00100000000133700") ) @@ -636,7 +636,7 @@ func BenchmarkOpKeccak256(bench *testing.B) { for i := 0; i < bench.N; i++ { stack.push(uint256.NewInt(32)) stack.push(start) - opKeccak256(&pc, evmInterpreter, &ScopeContext{mem, stack, nil}) + opKeccak256(&pc, evmInterpreter, &ScopeContext{mem, stack, nil, 0, nil}) } } @@ -731,7 +731,7 @@ func TestRandom(t *testing.T) { pc = uint64(0) evmInterpreter = env.interpreter ) - opRandom(&pc, evmInterpreter, &ScopeContext{nil, stack, nil}) + opRandom(&pc, evmInterpreter, &ScopeContext{nil, stack, nil, 0, nil}) if len(stack.data) != 1 { t.Errorf("Expected one item on stack after %v, got %d: ", tt.name, len(stack.data)) } diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go index 7b040aac9e11..d700dede544c 100644 --- a/core/vm/interpreter.go +++ b/core/vm/interpreter.go @@ -30,7 +30,8 @@ type Config struct { NoBaseFee bool // Forces the EIP-1559 baseFee to 0 (needed for 0 price calls) EnablePreimageRecording bool // Enables recording of SHA3/keccak preimages - JumpTable *JumpTable // EVM instruction table, automatically populated if unset + JumpTable *JumpTable // EVM instruction table, automatically populated if unset + JumpTableEOF *JumpTable // EVM instruction table, automatically populated if unset ExtraEips []int // Additional EIPS that are to be enabled } @@ -41,6 +42,15 @@ type ScopeContext struct { Memory *Memory Stack *Stack Contract *Contract + + CodeSection uint64 + ReturnStack []*ReturnContext +} + +type ReturnContext struct { + Section uint64 + Pc uint64 + StackHeight int } // EVMInterpreter represents an EVM interpreter @@ -62,6 +72,7 @@ func NewEVMInterpreter(evm *EVM, cfg Config) *EVMInterpreter { switch { case evm.chainRules.IsShanghai: cfg.JumpTable = &shanghaiInstructionSet + cfg.JumpTableEOF = &shanghaiEOFInstructionSet case evm.chainRules.IsMerge: cfg.JumpTable = &mergeInstructionSet case evm.chainRules.IsLondon: @@ -133,13 +144,16 @@ func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) ( } var ( + jt *JumpTable // current jump table op OpCode // current opcode mem = NewMemory() // bound memory stack = newstack() // local stack callContext = &ScopeContext{ - Memory: mem, - Stack: stack, - Contract: contract, + Memory: mem, + Stack: stack, + Contract: contract, + CodeSection: 0, + ReturnStack: []*ReturnContext{{Section: 0, Pc: 0, StackHeight: 0}}, } // For optimisation reason we're using uint64 as the program counter. // It's theoretically possible to go above 2^64. The YP defines the PC @@ -160,6 +174,12 @@ func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) ( }() contract.Input = input + if contract.IsEOF() { + jt = in.cfg.JumpTableEOF + } else { + jt = in.cfg.JumpTable + } + if in.cfg.Debug { defer func() { if err != nil { @@ -182,8 +202,8 @@ 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) - operation := in.cfg.JumpTable[op] + op = contract.GetOp(pc, callContext.CodeSection) + operation := jt[op] cost = operation.constantGas // For tracing // Validate stack if sLen := stack.len(); sLen < operation.minStack { diff --git a/core/vm/jump_table.go b/core/vm/jump_table.go index 91f1be669a40..1a9ccce572d4 100644 --- a/core/vm/jump_table.go +++ b/core/vm/jump_table.go @@ -56,6 +56,7 @@ var ( londonInstructionSet = newLondonInstructionSet() mergeInstructionSet = newMergeInstructionSet() shanghaiInstructionSet = newShanghaiInstructionSet() + shanghaiEOFInstructionSet = newShanghaiEOFInstructionSet() ) // JumpTable contains the EVM opcodes supported at a given fork. @@ -86,6 +87,12 @@ func newShanghaiInstructionSet() JumpTable { return validate(instructionSet) } +func newShanghaiEOFInstructionSet() JumpTable { + instructionSet := newMergeInstructionSet() + enableEOF(&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 9f199eb8f60a..c6ca0b55f181 100644 --- a/core/vm/opcodes.go +++ b/core/vm/opcodes.go @@ -116,6 +116,9 @@ const ( MSIZE OpCode = 0x59 GAS OpCode = 0x5a JUMPDEST OpCode = 0x5b + RJUMP OpCode = 0x5c + RJUMPI OpCode = 0x5d + RJUMPV OpCode = 0x5e PUSH0 OpCode = 0x5f ) @@ -204,6 +207,13 @@ const ( LOG4 ) +// 0xb0 range - control flow ops. +const ( + CALLF = 0xb0 + iota + RETF + JUMPF +) + // 0xf0 range - closures. const ( CREATE OpCode = 0xf0 @@ -304,6 +314,9 @@ var opCodeToString = map[OpCode]string{ MSIZE: "MSIZE", GAS: "GAS", JUMPDEST: "JUMPDEST", + RJUMP: "RJUMP", + RJUMPI: "RJUMPI", + RJUMPV: "RJUMPV", PUSH0: "PUSH0", // 0x60 range - push. @@ -380,6 +393,9 @@ var opCodeToString = map[OpCode]string{ LOG4: "LOG4", // 0xb0 range. + CALLF: "CALLF", + RETF: "RETF", + JUMPF: "JUMPF", TLOAD: "TLOAD", TSTORE: "TSTORE", From 2579e9e769f0e256c65e22f0fcf679c153cef032 Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Thu, 22 Dec 2022 16:13:49 -0700 Subject: [PATCH 03/53] tests: add shanghai testing defn From 07da06dab54ae9b66fb2e4eb69da3dcae96c6b29 Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Thu, 22 Dec 2022 16:37:27 -0700 Subject: [PATCH 04/53] core/vm: add eof code validation --- core/blockchain_test.go | 2 +- core/vm/analysis.go | 66 +++++++++++++ core/vm/eips.go | 4 + core/vm/eof.go | 22 ++--- core/vm/eof_test.go | 4 +- core/vm/jump_table.go | 13 ++- core/vm/validate.go | 203 +++++++++++++++++++++++++++++++++++++++ core/vm/validate_test.go | 88 +++++++++++++++++ 8 files changed, 386 insertions(+), 16 deletions(-) create mode 100644 core/vm/validate.go create mode 100644 core/vm/validate_test.go diff --git a/core/blockchain_test.go b/core/blockchain_test.go index f333f8a0c987..43ed99142fbb 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -4356,7 +4356,7 @@ func TestEOF(t *testing.T) { // The address 0xAAAA sloads 0x00 and 0x01 aa: { Code: (&vm.Container{ - Types: []vm.TypeAnnotation{ + Types: []*vm.FunctionMetadata{ {Input: 0, Output: 0, MaxStackHeight: 0}, {Input: 0, Output: 0, MaxStackHeight: 2}, {Input: 0, Output: 0, MaxStackHeight: 0}, diff --git a/core/vm/analysis.go b/core/vm/analysis.go index 4aa8cfe70f11..a8e326c55615 100644 --- a/core/vm/analysis.go +++ b/core/vm/analysis.go @@ -116,3 +116,69 @@ func codeBitmapInternal(code, bits bitvec) bitvec { } return bits } + +// eofCodeBitmap collects data locations in code. +func eofCodeBitmap(code []byte) 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) +} + +// eofCodeBitmapInternal is the internal implementation of codeBitmap for EOF +// code validation. +func eofCodeBitmapInternal(code, bits bitvec) bitvec { + for pc := uint64(0); pc < uint64(len(code)); { + op := OpCode(code[pc]) + pc++ + if int8(op) == int8(RJUMP) || int8(op) == int8(RJUMPI) { + bits.setN(set2BitsMask, pc) + pc += 2 + } + var numbits uint8 + if int8(op) >= int8(PUSH1) || int8(op) <= int8(PUSH32) { + numbits = uint8(code[pc]) + pc++ + } else if int8(op) == int8(RJUMPV) { + numbits = uint8(op - PUSH1 + 1) + } else { + // If not PUSH (the int8(op) > int(PUSH32) is always false). + continue + } + if numbits >= 8 { + for ; numbits >= 16; numbits -= 16 { + bits.set16(pc) + pc += 16 + } + for ; numbits >= 8; numbits -= 8 { + bits.set8(pc) + pc += 8 + } + } + switch numbits { + case 1: + bits.set1(pc) + pc += 1 + case 2: + bits.setN(set2BitsMask, pc) + pc += 2 + case 3: + bits.setN(set3BitsMask, pc) + pc += 3 + case 4: + bits.setN(set4BitsMask, pc) + pc += 4 + case 5: + bits.setN(set5BitsMask, pc) + pc += 5 + case 6: + bits.setN(set6BitsMask, pc) + pc += 6 + case 7: + bits.setN(set7BitsMask, pc) + pc += 7 + } + } + return bits +} diff --git a/core/vm/eips.go b/core/vm/eips.go index 2597b1d7b20b..f6ecd58a0b41 100644 --- a/core/vm/eips.go +++ b/core/vm/eips.go @@ -250,6 +250,7 @@ func enableEOF(jt *JumpTable) { constantGas: 0, minStack: minStack(0, 0), maxStack: maxStack(0, 0), + undefined: true, } jt[CALLCODE] = undefined jt[SELFDESTRUCT] = undefined @@ -263,6 +264,7 @@ func enableEOF(jt *JumpTable) { constantGas: GasQuickStep, minStack: minStack(0, 0), maxStack: maxStack(0, 0), + terminal: true, } jt[RJUMPI] = &operation{ execute: opRjumpi, @@ -287,12 +289,14 @@ func enableEOF(jt *JumpTable) { constantGas: GasFastishStep, minStack: minStack(0, 0), maxStack: maxStack(0, 0), + terminal: true, } jt[JUMPF] = &operation{ execute: opJumpf, constantGas: GasFastestStep, minStack: minStack(0, 0), maxStack: maxStack(0, 0), + terminal: true, } } diff --git a/core/vm/eof.go b/core/vm/eof.go index 1bc12c6c6c23..5ce9655a8501 100644 --- a/core/vm/eof.go +++ b/core/vm/eof.go @@ -54,13 +54,13 @@ func isEOFVersion1(code []byte) bool { // Container is and EOF container object. type Container struct { - Types []TypeAnnotation + Types []*FunctionMetadata Code [][]byte Data []byte } -// TypeAnnotation is an EOF function signature. -type TypeAnnotation struct { +// FunctionMetadata is an EOF function signature. +type FunctionMetadata struct { Input uint8 Output uint8 MaxStackHeight uint16 @@ -150,9 +150,9 @@ func (c *Container) UnmarshalBinary(b []byte) error { // Parse types section. idx := offsetTerminator + 1 - var types []TypeAnnotation + var types []*FunctionMetadata for i := 0; i < typesSize/4; i++ { - sig := TypeAnnotation{ + sig := &FunctionMetadata{ Input: b[idx+i*4], Output: b[idx+i*4+1], MaxStackHeight: uint16(parseUint16(b[idx+i*4+2:])), @@ -185,12 +185,12 @@ func (c *Container) UnmarshalBinary(b []byte) error { return nil } -func (c *Container) ValidateCode() error { - // for _, code := range c.Code { - // if err := validateCode(code); err != nil { - // return err - // } - // } +func (c *Container) ValidateCode(jt *JumpTable) error { + for i, code := range c.Code { + if err := validateCode(code, i, c.Types, jt); err != nil { + return err + } + } return nil } diff --git a/core/vm/eof_test.go b/core/vm/eof_test.go index 61eddc8a5465..2aaa52480668 100644 --- a/core/vm/eof_test.go +++ b/core/vm/eof_test.go @@ -30,14 +30,14 @@ func TestEOFMarshaling(t *testing.T) { }{ { want: Container{ - Types: []TypeAnnotation{{Input: 0, Output: 0, MaxStackHeight: 0}}, + Types: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 0}}, Code: [][]byte{common.Hex2Bytes("604200")}, Data: []byte{0x01, 0x02, 0x03}, }, }, { want: Container{ - Types: []TypeAnnotation{ + Types: []*FunctionMetadata{ {Input: 0, Output: 0, MaxStackHeight: 0}, {Input: 2, Output: 3, MaxStackHeight: 32}, {Input: 1, Output: 1, MaxStackHeight: 1023}, diff --git a/core/vm/jump_table.go b/core/vm/jump_table.go index 1a9ccce572d4..465232ef368c 100644 --- a/core/vm/jump_table.go +++ b/core/vm/jump_table.go @@ -42,6 +42,12 @@ type operation struct { // memorySize returns the memory size required for the operation memorySize memorySizeFunc + + // undefined denotes if the instruction is not officially defined in the jump table + undefined bool + + // terminal denotes if the instruction can be the final opcode in a code section + terminal bool } var ( @@ -88,7 +94,7 @@ func newShanghaiInstructionSet() JumpTable { } func newShanghaiEOFInstructionSet() JumpTable { - instructionSet := newMergeInstructionSet() + instructionSet := newShanghaiInstructionSet() enableEOF(&instructionSet) return validate(instructionSet) } @@ -204,6 +210,7 @@ func newByzantiumInstructionSet() JumpTable { minStack: minStack(2, 0), maxStack: maxStack(2, 0), memorySize: memoryRevert, + terminal: true, } return validate(instructionSet) } @@ -252,6 +259,7 @@ func newFrontierInstructionSet() JumpTable { constantGas: 0, minStack: minStack(0, 0), maxStack: maxStack(0, 0), + terminal: true, }, ADD: { execute: opAdd, @@ -1040,6 +1048,7 @@ func newFrontierInstructionSet() JumpTable { minStack: minStack(2, 0), maxStack: maxStack(2, 0), memorySize: memoryReturn, + terminal: true, }, SELFDESTRUCT: { execute: opSelfdestruct, @@ -1052,7 +1061,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} } } diff --git a/core/vm/validate.go b/core/vm/validate.go new file mode 100644 index 000000000000..b6a23c72b8dc --- /dev/null +++ b/core/vm/validate.go @@ -0,0 +1,203 @@ +// Copyright 2022 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 ( + "fmt" + + "github.com/ethereum/go-ethereum/params" +) + +func validateCode(code []byte, section int, metadata []*FunctionMetadata, jt *JumpTable) error { + var ( + i = 0 + count = 0 + op OpCode + analysis *bitvec + ) + for i < len(code) { + count += 1 + op = OpCode(code[i]) + if jt[op].undefined { + return fmt.Errorf("use of undefined opcode") + } + switch { + case op >= PUSH1 && op <= PUSH32: + // Verify that push data is not truncated. + size := int(op - PUSH0) + if i+size >= len(code) { + return fmt.Errorf("truncated operand") + } + i += size + case op == RJUMP || op == RJUMPI: + if analysis == nil { + tmp := eofCodeBitmap(code) + analysis = &tmp + } + // Verify that the relative jump offset points to a + // destination in-bounds. + if err := checkDest(code[i+1:], *analysis, i, len(code)); err != nil { + return err + } + i += 2 + case op == RJUMPV: + if analysis == nil { + tmp := eofCodeBitmap(code) + analysis = &tmp + } + // Verify each branch in the jump table points to a + // destination in-bounds. + if i+1 >= len(code) { + return fmt.Errorf("truncated jump table operand") + } + count := int(code[i]) + if count == 0 { + return fmt.Errorf("rjumpv branch count must not be 0") + } + for j := 0; j < count; j++ { + if err := checkDest(code[i+1+j*2:], *analysis, i, len(code)); err != nil { + return err + } + } + i += 1 + 2*count + case op == CALLF || op == JUMPF: + if i+2 >= len(code) { + return fmt.Errorf("truncated operand") + } + arg := parseUint16(code[i+1:]) + if arg >= len(metadata) { + return fmt.Errorf("code section out-of-bounds (want: %d, have: %d)", arg, len(metadata)) + } + if op == JUMPF { + if metadata[section].Output != metadata[arg].Output { + return fmt.Errorf("jumpf to section with different number of outputs") + } + } + i += 2 + } + i += 1 + } + if !jt[op].terminal { + return fmt.Errorf("code section ends with non-terminal instruction") + } + if max, paths, err := validateControlFlow(code, section, metadata, jt); err != nil { + return err + } else if paths != count { + return fmt.Errorf("unreachable code") + } else if max != int(metadata[section].MaxStackHeight) { + return fmt.Errorf("computed max stack height for code section %d does not match expect (want: %d, got: %d)", section, metadata[section].MaxStackHeight, max) + } + return nil +} + +func checkDest(code []byte, analysis bitvec, idx, length int) error { + if len(code) < 2 { + return fmt.Errorf("truncated operand") + } + offset := parseInt16(code) + dest := idx + int(offset) + if dest < 0 || dest >= len(code) { + return fmt.Errorf("relative offset out-of-bounds: %d", dest) + } + if analysis[dest] == 1 { + return fmt.Errorf("relative offset into immediate operand: %d", dest) + } + return nil +} + +func validateControlFlow(code []byte, section int, metadata []*FunctionMetadata, jt *JumpTable) (int, int, error) { + type item struct { + pos int + height int + } + var ( + heights = make(map[int]int) + worklist = []item{{0, int(metadata[section].Input)}} + maxStackHeight = 0 + ) + for 0 < len(worklist) { + var ( + idx = len(worklist) - 1 + pos = worklist[idx].pos + height = worklist[idx].height + ) + worklist = worklist[:idx] + for pos < len(code) { + op := OpCode(code[pos]) + fmt.Println(op) + + // Check if pos has already be visited; if so, the stack heights should be the same. + if exp, ok := heights[pos]; ok { + if height != exp { + return 0, 0, fmt.Errorf("stack height mismatch for different paths") + } + // Already visited this path and stack height + // matches. + break + } + heights[pos] = height + + switch { + case op == CALLF: + arg := parseUint16(code[pos+1:]) + if metadata[arg].Input < uint8(height) { + return 0, 0, fmt.Errorf("stack underflow") + } + if int(metadata[arg].Output)+height > int(params.StackLimit) { + return 0, 0, fmt.Errorf("stack overflow") + } + case op == RJUMP: + arg := parseUint16(code[pos+1:]) + pos += 3 + int(arg) + case op == RJUMPI: + arg := parseUint16(code[pos+1:]) + worklist = append(worklist, item{pos: pos + 3 + int(arg), height: height}) + pos += 3 + case op == RJUMPV: + count := int(code[pos+1]) + for i := 0; i < count; i++ { + arg := parseUint16(code[pos+2+2*i:]) + worklist = append(worklist, item{pos: pos + int(arg), height: height}) + } + pos += 2 + 2*count + default: + if jt[op].minStack > height { + return 0, 0, fmt.Errorf("stack underflow") + } + if jt[op].maxStack < height { + return 0, 0, fmt.Errorf("stack overflow") + } + height += int(params.StackLimit) - jt[op].maxStack + if op >= PUSH1 && op <= PUSH32 { + pos += 1 + int(op-PUSH0) + } else { + // No immediate. + pos += 1 + } + } + maxStackHeight = max(maxStackHeight, height) + } + } + return maxStackHeight, len(heights), nil +} + +func max(a, b int) int { + if a < b { + return b + } + return a +} diff --git a/core/vm/validate_test.go b/core/vm/validate_test.go new file mode 100644 index 000000000000..876b904958d1 --- /dev/null +++ b/core/vm/validate_test.go @@ -0,0 +1,88 @@ +// Copyright 2022 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 ( + "fmt" + "testing" +) + +func TestValidateCode(t *testing.T) { + for i, test := range []struct { + code []byte + section int + metadata []*FunctionMetadata + err error + }{ + { + code: []byte{ + byte(CALLER), + byte(POP), + byte(STOP), + }, + section: 0, + metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 1}}, + }, + { + code: []byte{ + byte(CALLER), + byte(POP), + }, + section: 0, + metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 1}}, + err: fmt.Errorf("code section ends with non-terminal instruction"), + }, + { + code: []byte{ + byte(RJUMP), + byte(0x00), + byte(0x01), + byte(CALLER), + byte(STOP), + }, + section: 0, + metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 1}}, + err: fmt.Errorf("unreachable code"), + }, + { + code: []byte{ + byte(PUSH1), + byte(0x42), + byte(ADD), + byte(STOP), + }, + section: 0, + metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 1}}, + err: fmt.Errorf("stack underflow"), + }, + { + code: []byte{ + byte(PUSH1), + byte(0x42), + byte(POP), + byte(STOP), + }, + section: 0, + metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 2}}, + err: fmt.Errorf("computed max stack height for code section 0 does not match expect (want: 2, got: 1)"), + }, + } { + if err := validateCode(test.code, test.section, test.metadata, &shanghaiEOFInstructionSet); test.err != nil && (err == nil || test.err.Error() != err.Error()) { + t.Fatalf("test %d: unexpected error (want: %v, got: %v)", i, test.err, err) + } + } +} From c90f24ef5cd26168cd8d30a6c1797868e854daae Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Thu, 29 Dec 2022 10:42:40 -0700 Subject: [PATCH 05/53] core/vm: properly validated deployed code if eof --- core/vm/eof.go | 4 +--- core/vm/evm.go | 11 +++++++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/core/vm/eof.go b/core/vm/eof.go index 5ce9655a8501..8573ea756995 100644 --- a/core/vm/eof.go +++ b/core/vm/eof.go @@ -101,7 +101,7 @@ func (c *Container) UnmarshalBinary(b []byte) error { return fmt.Errorf("invalid eof version") } - if len(b) < 9 { + if len(b) < 15 { return fmt.Errorf("container size less than minimum valid size") } @@ -143,8 +143,6 @@ func (c *Container) UnmarshalBinary(b []byte) error { // Check for terminator. expectedSize := offsetTerminator + typesSize + sum(codeSizes) + dataSize + 1 if len(b) != expectedSize { - fmt.Println(codeSizes) - fmt.Println(offsetTerminator, typesSize, sum(codeSizes), dataSize) return fmt.Errorf("invalid container size (want %d, got %d)", expectedSize, len(b)) } diff --git a/core/vm/evm.go b/core/vm/evm.go index 4680066429b4..0fde4ea930cd 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -469,8 +469,15 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, } // 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 && len(ret) >= 1 && ret[0] == 0xEF { + if evm.chainRules.IsShanghai { + var c Container + if err = c.UnmarshalBinary(ret); err != nil { + err = fmt.Errorf("invalid code: %v", err) + } + } else if evm.chainRules.IsLondon { + err = ErrInvalidCode + } } // if the contract creation ran successfully and no errors were returned From 068401107e04a38f45fe8eabf1017d78914187a4 Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Thu, 29 Dec 2022 11:41:53 -0700 Subject: [PATCH 06/53] core/vm: fix relative jump destination validation --- core/vm/analysis.go | 13 ++++++++----- core/vm/analysis_test.go | 22 ++++++++++++++++++++++ core/vm/validate.go | 11 +++++------ core/vm/validate_test.go | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 11 deletions(-) diff --git a/core/vm/analysis.go b/core/vm/analysis.go index a8e326c55615..72b12f902d7f 100644 --- a/core/vm/analysis.go +++ b/core/vm/analysis.go @@ -123,7 +123,7 @@ func eofCodeBitmap(code []byte) bitvec { // 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) + return eofCodeBitmapInternal(code, bits) } // eofCodeBitmapInternal is the internal implementation of codeBitmap for EOF @@ -132,16 +132,19 @@ func eofCodeBitmapInternal(code, bits bitvec) bitvec { for pc := uint64(0); pc < uint64(len(code)); { op := OpCode(code[pc]) pc++ + + // RJUMP and RJUMPI always have 2 byte operand. if int8(op) == int8(RJUMP) || int8(op) == int8(RJUMPI) { bits.setN(set2BitsMask, pc) pc += 2 + continue } var numbits uint8 - if int8(op) >= int8(PUSH1) || int8(op) <= int8(PUSH32) { - numbits = uint8(code[pc]) - pc++ - } else if int8(op) == int8(RJUMPV) { + if int8(op) >= int8(PUSH1) && int8(op) <= int8(PUSH32) { numbits = uint8(op - PUSH1 + 1) + } else if int8(op) == int8(RJUMPV) { + // RJUMPV has variable sized operand + numbits = uint8(code[pc]*2) + 1 } else { // 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..172148cccc1b 100644 --- a/core/vm/analysis_test.go +++ b/core/vm/analysis_test.go @@ -56,6 +56,28 @@ func TestJumpDestAnalysis(t *testing.T) { if ret[test.which] != test.exp { t.Fatalf("test %d: expected %x, got %02x", i, test.exp, ret[test.which]) } + ret = eofCodeBitmap(test.code) + if ret[test.which] != test.exp { + t.Fatalf("eof test %d: expected %x, got %02x", i, test.exp, ret[test.which]) + } + } +} + +func TestEOFAnalysis(t *testing.T) { + tests := []struct { + code []byte + exp byte + which int + }{ + {[]byte{byte(RJUMP), 0x01, 0x01, 0x01}, 0b0000_0110, 0}, + {[]byte{byte(RJUMPI), byte(RJUMP), byte(RJUMP), byte(RJUMPI)}, 0b0011_0110, 0}, + {[]byte{byte(RJUMPV), 0x02, byte(RJUMP), 0x00, byte(RJUMPI), 0x00}, 0b0011_1110, 0}, + } + for i, test := range tests { + ret := eofCodeBitmap(test.code) + if ret[test.which] != test.exp { + t.Fatalf("test %d: expected %x, got %02x", i, test.exp, ret[test.which]) + } } } diff --git a/core/vm/validate.go b/core/vm/validate.go index b6a23c72b8dc..8aa936c4b7f3 100644 --- a/core/vm/validate.go +++ b/core/vm/validate.go @@ -50,7 +50,7 @@ func validateCode(code []byte, section int, metadata []*FunctionMetadata, jt *Ju } // Verify that the relative jump offset points to a // destination in-bounds. - if err := checkDest(code[i+1:], *analysis, i, len(code)); err != nil { + if err := checkDest(code[i+1:], *analysis, i+3, len(code)); err != nil { return err } i += 2 @@ -64,12 +64,12 @@ func validateCode(code []byte, section int, metadata []*FunctionMetadata, jt *Ju if i+1 >= len(code) { return fmt.Errorf("truncated jump table operand") } - count := int(code[i]) + count := int(code[i+1]) if count == 0 { return fmt.Errorf("rjumpv branch count must not be 0") } for j := 0; j < count; j++ { - if err := checkDest(code[i+1+j*2:], *analysis, i, len(code)); err != nil { + if err := checkDest(code[i+2+j*2:], *analysis, i+2*count+2, len(code)); err != nil { return err } } @@ -110,10 +110,10 @@ func checkDest(code []byte, analysis bitvec, idx, length int) error { } offset := parseInt16(code) dest := idx + int(offset) - if dest < 0 || dest >= len(code) { + if dest < 0 || dest >= length { return fmt.Errorf("relative offset out-of-bounds: %d", dest) } - if analysis[dest] == 1 { + if !analysis.codeSegment(uint64(dest)) { return fmt.Errorf("relative offset into immediate operand: %d", dest) } return nil @@ -138,7 +138,6 @@ func validateControlFlow(code []byte, section int, metadata []*FunctionMetadata, worklist = worklist[:idx] for pos < len(code) { op := OpCode(code[pos]) - fmt.Println(op) // Check if pos has already be visited; if so, the stack heights should be the same. if exp, ok := heights[pos]; ok { diff --git a/core/vm/validate_test.go b/core/vm/validate_test.go index 876b904958d1..6a303178a9af 100644 --- a/core/vm/validate_test.go +++ b/core/vm/validate_test.go @@ -80,6 +80,39 @@ func TestValidateCode(t *testing.T) { metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 2}}, err: fmt.Errorf("computed max stack height for code section 0 does not match expect (want: 2, got: 1)"), }, + { + code: []byte{ + byte(PUSH0), + byte(RJUMPI), + byte(0x00), + byte(0x01), + byte(PUSH1), + byte(0x42), // jumps to here + byte(POP), + byte(STOP), + }, + section: 0, + metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 1}}, + err: fmt.Errorf("relative offset into immediate operand: 5"), + }, + { + code: []byte{ + byte(PUSH0), + byte(RJUMPV), + byte(0x02), + byte(0x00), + byte(0x01), + byte(0x00), + byte(0x02), + byte(PUSH1), + byte(0x42), // jumps to here + byte(POP), // and here + byte(STOP), + }, + section: 0, + metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 1}}, + err: fmt.Errorf("relative offset into immediate operand: 8"), + }, } { if err := validateCode(test.code, test.section, test.metadata, &shanghaiEOFInstructionSet); test.err != nil && (err == nil || test.err.Error() != err.Error()) { t.Fatalf("test %d: unexpected error (want: %v, got: %v)", i, test.err, err) From 3906b6423754dea5476381c8f89d5bee985a1c52 Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Thu, 29 Dec 2022 11:48:09 -0700 Subject: [PATCH 07/53] core/vm: do code validation on eof --- core/vm/evm.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/vm/evm.go b/core/vm/evm.go index 0fde4ea930cd..e1d5860db70e 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -442,6 +442,9 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, if err := c.UnmarshalBinary(codeAndHash.code); err != nil { return nil, common.Address{}, 0, fmt.Errorf("%v: %v", ErrInvalidEOF, err) } + if err := c.ValidateCode(evm.Config.JumpTableEOF); err != nil { + return nil, common.Address{}, 0, fmt.Errorf("%v: %v", ErrInvalidEOF, err) + } contract.Container = &c } @@ -472,7 +475,10 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, if err == nil && len(ret) >= 1 && ret[0] == 0xEF { if evm.chainRules.IsShanghai { var c Container - if err = c.UnmarshalBinary(ret); err != nil { + if err = c.UnmarshalBinary(ret); err == nil { + err = c.ValidateCode(evm.Config.JumpTableEOF) + } + if err != nil { err = fmt.Errorf("invalid code: %v", err) } } else if evm.chainRules.IsLondon { From 05cf350e3107555d1c579e1eff7d6ef2597cba6e Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Thu, 29 Dec 2022 13:07:14 -0700 Subject: [PATCH 08/53] core/vm: fix linter and account for overflowing rjumpv case --- core/blockchain_test.go | 3 --- core/vm/analysis.go | 2 +- core/vm/eips.go | 2 +- core/vm/eof.go | 3 +-- 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/core/blockchain_test.go b/core/blockchain_test.go index 43ed99142fbb..c86242d79e01 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -4344,15 +4344,12 @@ func TestEOF(t *testing.T) { // A sender who makes transactions, has some funds key1, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") - key2, _ = crypto.HexToECDSA("8a1f9a8f95be41cd7ccb6168179afb4504aefe388d1e14474d32c45c72ce7b7a") addr1 = crypto.PubkeyToAddress(key1.PublicKey) - addr2 = crypto.PubkeyToAddress(key2.PublicKey) funds = new(big.Int).Mul(common.Big1, big.NewInt(params.Ether)) gspec = &Genesis{ Config: params.AllEthashProtocolChanges, Alloc: GenesisAlloc{ addr1: {Balance: funds}, - addr2: {Balance: funds}, // The address 0xAAAA sloads 0x00 and 0x01 aa: { Code: (&vm.Container{ diff --git a/core/vm/analysis.go b/core/vm/analysis.go index 72b12f902d7f..b96a4935223d 100644 --- a/core/vm/analysis.go +++ b/core/vm/analysis.go @@ -144,7 +144,7 @@ func eofCodeBitmapInternal(code, bits bitvec) bitvec { numbits = uint8(op - PUSH1 + 1) } else if int8(op) == int8(RJUMPV) { // RJUMPV has variable sized operand - numbits = uint8(code[pc]*2) + 1 + numbits = code[pc]*2 + 1 } else { // If not PUSH (the int8(op) > int(PUSH32) is always false). continue diff --git a/core/vm/eips.go b/core/vm/eips.go index f6ecd58a0b41..817d359dc10c 100644 --- a/core/vm/eips.go +++ b/core/vm/eips.go @@ -330,7 +330,7 @@ func opRjumpv(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]b count = uint64(code[*pc+1]) idx = scope.Stack.pop() ) - if idx.Uint64() >= uint64(count) { + if idx, overflow := idx.Uint64WithOverflow(); overflow || idx >= count { // Index out-of-bounds, don't branch, just skip over immediate // argument. *pc += 1 + count*2 diff --git a/core/vm/eof.go b/core/vm/eof.go index 8573ea756995..e3587d0dbf1e 100644 --- a/core/vm/eof.go +++ b/core/vm/eof.go @@ -97,7 +97,7 @@ func (c *Container) UnmarshalBinary(b []byte) error { if !hasEOFMagic(b) { return fmt.Errorf("invalid magic") } - if check(b, offsetVersion, eof1Version) { + if !isEOFVersion1(b) { return fmt.Errorf("invalid eof version") } @@ -231,7 +231,6 @@ func check(b []byte, idx int, want byte) bool { func sum(list []int) (s int) { for _, n := range list { s += n - } return } From d75812ce21543db54e40b815b48b21d12d99ba5b Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Thu, 29 Dec 2022 15:24:11 -0700 Subject: [PATCH 09/53] core/vm: add tests for deploying eof containers --- core/blockchain_test.go | 121 ++++++++++++++++++++++++++++++++++++--- core/vm/evm.go | 4 +- core/vm/validate.go | 6 +- core/vm/validate_test.go | 21 +++++++ 4 files changed, 138 insertions(+), 14 deletions(-) diff --git a/core/blockchain_test.go b/core/blockchain_test.go index c86242d79e01..d22588895de2 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -17,6 +17,7 @@ package core import ( + "bytes" "errors" "fmt" "math/big" @@ -4339,18 +4340,41 @@ func int8ToByte(n int8) uint8 { func TestEOF(t *testing.T) { var ( + createDeployer = []byte{ + byte(vm.CALLDATASIZE), // size + byte(vm.PUSH1), 0x00, // offset + byte(vm.PUSH1), 0x00, // dst + byte(vm.CALLDATACOPY), + byte(vm.CALLDATASIZE), // len + byte(vm.PUSH1), 0x00, // offset + byte(vm.PUSH1), 0x00, // value + byte(vm.CREATE), + } + create2Deployer = []byte{ + byte(vm.CALLDATASIZE), // len + byte(vm.PUSH1), 0x00, // offset + byte(vm.PUSH1), 0x00, // dst + byte(vm.CALLDATACOPY), + byte(vm.PUSH1), 0x00, // salt + byte(vm.CALLDATASIZE), // len + byte(vm.PUSH1), 0x00, // offset + byte(vm.PUSH1), 0x00, // value + byte(vm.CREATE2), + } + aa = common.HexToAddress("0x000000000000000000000000000000000000aaaa") + bb = common.HexToAddress("0x000000000000000000000000000000000000bbbb") + cc = common.HexToAddress("0x000000000000000000000000000000000000cccc") engine = ethash.NewFaker() - - // A sender who makes transactions, has some funds - key1, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") - addr1 = crypto.PubkeyToAddress(key1.PublicKey) - funds = new(big.Int).Mul(common.Big1, big.NewInt(params.Ether)) - gspec = &Genesis{ + key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + addr = crypto.PubkeyToAddress(key.PublicKey) + funds = new(big.Int).Mul(common.Big1, big.NewInt(params.Ether)) + gspec = &Genesis{ Config: params.AllEthashProtocolChanges, Alloc: GenesisAlloc{ - addr1: {Balance: funds}, - // The address 0xAAAA sloads 0x00 and 0x01 + addr: {Balance: funds}, + bb: {Code: createDeployer, Balance: big.NewInt(0)}, + cc: {Code: create2Deployer, Balance: big.NewInt(0)}, aa: { Code: (&vm.Container{ Types: []*vm.FunctionMetadata{ @@ -4442,8 +4466,31 @@ func TestEOF(t *testing.T) { gspec.Config.ShanghaiTime = common.Big0 signer := types.LatestSigner(gspec.Config) + container := vm.Container{ + Types: []*vm.FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 0}}, + Code: [][]byte{{byte(vm.STOP)}}, + Data: nil, + } + deployCode := container.MarshalBinary() + + initCode := []byte{ + byte(vm.PUSH1), byte(len(deployCode)), // len + byte(vm.PUSH1), 0x0c, // offset + byte(vm.PUSH1), 0x00, // dst offset + byte(vm.CODECOPY), + + // code in memory + byte(vm.PUSH1), byte(len(deployCode)), // size + byte(vm.PUSH1), 0x00, // offset + byte(vm.RETURN), + } + initCode = append(initCode, deployCode...) + initHash := crypto.Keccak256Hash(initCode) + _, blocks, _ := GenerateChainWithGenesis(gspec, engine, 1, func(i int, b *BlockGen) { b.SetCoinbase(aa) + + // execute flag contract txdata := &types.DynamicFeeTx{ ChainID: gspec.Config.ChainID, Nonce: 0, @@ -4455,8 +4502,52 @@ func TestEOF(t *testing.T) { Data: []byte{}, } tx := types.NewTx(txdata) - tx, _ = types.SignTx(tx, signer, key1) + tx, _ = types.SignTx(tx, signer, key) + b.AddTx(tx) + + // deploy eof contract from eoa + txdata = &types.DynamicFeeTx{ + ChainID: gspec.Config.ChainID, + Nonce: 1, + To: nil, + Gas: 500000, + GasFeeCap: newGwei(5), + GasTipCap: big.NewInt(2), + AccessList: nil, + Data: initCode, + } + tx = types.NewTx(txdata) + tx, _ = types.SignTx(tx, signer, key) + b.AddTx(tx) + // deploy eof contract from create contract + txdata = &types.DynamicFeeTx{ + ChainID: gspec.Config.ChainID, + Nonce: 2, + To: &bb, + Gas: 500000, + GasFeeCap: newGwei(5), + GasTipCap: big.NewInt(2), + AccessList: nil, + Data: initCode, + } + tx = types.NewTx(txdata) + tx, _ = types.SignTx(tx, signer, key) + b.AddTx(tx) + + // deploy eof contract from create2 contract + txdata = &types.DynamicFeeTx{ + ChainID: gspec.Config.ChainID, + Nonce: 3, + To: &cc, + Gas: 500000, + GasFeeCap: newGwei(5), + GasTipCap: big.NewInt(2), + AccessList: nil, + Data: initCode, + } + tx = types.NewTx(txdata) + tx, _ = types.SignTx(tx, signer, key) b.AddTx(tx) }) chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), nil, gspec, nil, engine, vm.Config{Debug: true, Tracer: logger.NewMarkdownLogger(&logger.Config{}, os.Stderr)}, nil, nil) @@ -4467,10 +4558,22 @@ func TestEOF(t *testing.T) { t.Fatalf("block %d: failed to insert into chain: %v", n, err) } + // Check flags. state, _ := chain.State() for i := 0; i < 4; i++ { if state.GetState(aa, common.BigToHash(big.NewInt(int64(i)))).Big().Uint64() != 1 { t.Fatalf("flag %d not set", i) } } + + // Check various deployment mechanisms. + if bytes.Compare(state.GetCode(crypto.CreateAddress(addr, 1)), deployCode) != 0 { + t.Fatalf("failed to deploy EOF with EOA") + } + if bytes.Compare(state.GetCode(crypto.CreateAddress(bb, 0)), deployCode) != 0 { + t.Fatalf("failed to deploy EOF with CREATE") + } + if bytes.Compare(state.GetCode(crypto.CreateAddress2(cc, [32]byte{}, initHash.Bytes())), deployCode) != 0 { + t.Fatalf("failed to deploy EOF with CREATE2") + } } diff --git a/core/vm/evm.go b/core/vm/evm.go index e1d5860db70e..ff2235c970cd 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -442,7 +442,7 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, if err := c.UnmarshalBinary(codeAndHash.code); err != nil { return nil, common.Address{}, 0, fmt.Errorf("%v: %v", ErrInvalidEOF, err) } - if err := c.ValidateCode(evm.Config.JumpTableEOF); err != nil { + if err := c.ValidateCode(evm.interpreter.cfg.JumpTableEOF); err != nil { return nil, common.Address{}, 0, fmt.Errorf("%v: %v", ErrInvalidEOF, err) } contract.Container = &c @@ -476,7 +476,7 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, if evm.chainRules.IsShanghai { var c Container if err = c.UnmarshalBinary(ret); err == nil { - err = c.ValidateCode(evm.Config.JumpTableEOF) + err = c.ValidateCode(evm.interpreter.cfg.JumpTableEOF) } if err != nil { err = fmt.Errorf("invalid code: %v", err) diff --git a/core/vm/validate.go b/core/vm/validate.go index 8aa936c4b7f3..5be98c0b0478 100644 --- a/core/vm/validate.go +++ b/core/vm/validate.go @@ -33,7 +33,7 @@ func validateCode(code []byte, section int, metadata []*FunctionMetadata, jt *Ju count += 1 op = OpCode(code[i]) if jt[op].undefined { - return fmt.Errorf("use of undefined opcode") + return fmt.Errorf("use of undefined opcode %s", op) } switch { case op >= PUSH1 && op <= PUSH32: @@ -83,8 +83,8 @@ func validateCode(code []byte, section int, metadata []*FunctionMetadata, jt *Ju return fmt.Errorf("code section out-of-bounds (want: %d, have: %d)", arg, len(metadata)) } if op == JUMPF { - if metadata[section].Output != metadata[arg].Output { - return fmt.Errorf("jumpf to section with different number of outputs") + if metadata[section].Output < metadata[arg].Output { + return fmt.Errorf("jumpf to section with more outputs") } } i += 2 diff --git a/core/vm/validate_test.go b/core/vm/validate_test.go index 6a303178a9af..e9e44a735c14 100644 --- a/core/vm/validate_test.go +++ b/core/vm/validate_test.go @@ -113,6 +113,27 @@ func TestValidateCode(t *testing.T) { metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 1}}, err: fmt.Errorf("relative offset into immediate operand: 8"), }, + { + code: []byte{ + byte(PUSH0), + byte(RJUMPV), + byte(0x00), + byte(STOP), + }, + section: 0, + metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 1}}, + err: fmt.Errorf("rjumpv branch count must not be 0"), + }, + { + code: []byte{ + byte(JUMPF), + byte(0x00), + byte(0x01), + }, + section: 0, + metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 1}, {Input: 0, Output: 1, MaxStackHeight: 1}}, + err: fmt.Errorf("jumpf to section with more outputs"), + }, } { if err := validateCode(test.code, test.section, test.metadata, &shanghaiEOFInstructionSet); test.err != nil && (err == nil || test.err.Error() != err.Error()) { t.Fatalf("test %d: unexpected error (want: %v, got: %v)", i, test.err, err) From 4d9179d5921f681f3ce08e814902e2203dc0908c Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Thu, 29 Dec 2022 15:26:50 -0700 Subject: [PATCH 10/53] core/vm: fix lints --- core/vm/validate.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/vm/validate.go b/core/vm/validate.go index 5be98c0b0478..d12626e2f831 100644 --- a/core/vm/validate.go +++ b/core/vm/validate.go @@ -161,16 +161,16 @@ func validateControlFlow(code []byte, section int, metadata []*FunctionMetadata, } case op == RJUMP: arg := parseUint16(code[pos+1:]) - pos += 3 + int(arg) + pos += 3 + arg case op == RJUMPI: arg := parseUint16(code[pos+1:]) - worklist = append(worklist, item{pos: pos + 3 + int(arg), height: height}) + worklist = append(worklist, item{pos: pos + 3 + arg, height: height}) pos += 3 case op == RJUMPV: count := int(code[pos+1]) for i := 0; i < count; i++ { arg := parseUint16(code[pos+2+2*i:]) - worklist = append(worklist, item{pos: pos + int(arg), height: height}) + worklist = append(worklist, item{pos: pos + arg, height: height}) } pos += 2 + 2*count default: From 2411f4d9d57884621da099002bfccefec64cbd36 Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Thu, 29 Dec 2022 15:45:13 -0700 Subject: [PATCH 11/53] core/vm: disallow eof deploying legacy code --- core/vm/errors.go | 1 + core/vm/evm.go | 22 +++++++++++++++------- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/core/vm/errors.go b/core/vm/errors.go index 3909d9c3d342..275d7c1815ce 100644 --- a/core/vm/errors.go +++ b/core/vm/errors.go @@ -35,6 +35,7 @@ var ( ErrWriteProtection = errors.New("write protection") ErrReturnDataOutOfBounds = errors.New("return data out of bounds") ErrGasUintOverflow = errors.New("gas uint64 overflow") + ErrLegacyCode = errors.New("invalid code: EOF contract must not deploy legacy code") ErrInvalidCode = errors.New("invalid code: must not begin with 0xef") ErrInvalidEOF = errors.New("invalid eof") ErrNonceUintOverflow = errors.New("nonce uint64 overflow") diff --git a/core/vm/evm.go b/core/vm/evm.go index ff2235c970cd..9d35d9a8790e 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -224,7 +224,7 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas // 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, evm.maybeParseContainer(code)) + contract.SetCallCode(&addrCopy, evm.StateDB.GetCodeHash(addrCopy), code, evm.parseContainer(code)) ret, err = evm.interpreter.Run(contract, input, false) gas = contract.Gas } @@ -282,7 +282,7 @@ func (evm *EVM) CallCode(caller ContractRef, addr common.Address, input []byte, // 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), code, evm.maybeParseContainer(code)) + contract.SetCallCode(&addrCopy, evm.StateDB.GetCodeHash(addrCopy), code, evm.parseContainer(code)) ret, err = evm.interpreter.Run(contract, input, false) gas = contract.Gas } @@ -323,7 +323,7 @@ func (evm *EVM) DelegateCall(caller ContractRef, addr common.Address, input []by code := evm.StateDB.GetCode(addrCopy) // 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), code, evm.maybeParseContainer(code)) + contract.SetCallCode(&addrCopy, evm.StateDB.GetCodeHash(addrCopy), code, evm.parseContainer(code)) ret, err = evm.interpreter.Run(contract, input, false) gas = contract.Gas } @@ -377,7 +377,7 @@ func (evm *EVM) StaticCall(caller ContractRef, addr common.Address, input []byte // The contract is a scoped environment for this execution context only. code := evm.StateDB.GetCode(addrCopy) contract := NewContract(caller, AccountRef(addrCopy), new(big.Int), gas) - contract.SetCallCode(&addrCopy, evm.StateDB.GetCodeHash(addrCopy), code, evm.maybeParseContainer(code)) + contract.SetCallCode(&addrCopy, evm.StateDB.GetCodeHash(addrCopy), code, evm.parseContainer(code)) // 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. @@ -437,7 +437,8 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, contract.SetCodeOptionalHash(&address, codeAndHash) // If the initcode is EOF, verify it is well-formed. - if evm.chainRules.IsShanghai && hasEOFByte(codeAndHash.code) { + isInitcodeEOF := hasEOFByte(codeAndHash.code) + if evm.chainRules.IsShanghai && isInitcodeEOF { var c Container if err := c.UnmarshalBinary(codeAndHash.code); err != nil { return nil, common.Address{}, 0, fmt.Errorf("%v: %v", ErrInvalidEOF, err) @@ -471,6 +472,11 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, err = ErrMaxCodeSizeExceeded } + // Reject legacy contract deployment from EOF. + if err == nil && isInitcodeEOF && !hasEOFByte(ret) { + err = ErrLegacyCode + } + // Reject code starting with 0xEF if EIP-3541 is enabled. if err == nil && len(ret) >= 1 && ret[0] == 0xEF { if evm.chainRules.IsShanghai { @@ -479,7 +485,7 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, err = c.ValidateCode(evm.interpreter.cfg.JumpTableEOF) } if err != nil { - err = fmt.Errorf("invalid code: %v", err) + err = fmt.Errorf("%v: %v", ErrInvalidEOF, err) } } else if evm.chainRules.IsLondon { err = ErrInvalidCode @@ -538,12 +544,14 @@ func (evm *EVM) Create2(caller ContractRef, code []byte, gas uint64, endowment * // ChainConfig returns the environment's chain configuration func (evm *EVM) ChainConfig() *params.ChainConfig { return evm.chainConfig } -func (evm *EVM) maybeParseContainer(b []byte) *Container { +// parseContainer tries to parse an EOF container if the Shanghai fork is active. It expects the code to already be validated. +func (evm *EVM) parseContainer(b []byte) *Container { if evm.chainRules.IsShanghai { var c Container if err := c.UnmarshalBinary(b); err != nil && err.Error() == "invalid magic" { return nil } else if err != nil { + // Code was already validated, so no other errors should be possible. panic(fmt.Sprintf("unexpected error: %v", err)) } return &c From c1eae0ec13eaf5eb000b6d79df13faaff96f6d43 Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Fri, 30 Dec 2022 08:08:47 -0700 Subject: [PATCH 12/53] core/vm: parse relative args as ints in validation --- core/vm/validate.go | 12 ++++++------ core/vm/validate_test.go | 23 +++++++++++++++++++++-- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/core/vm/validate.go b/core/vm/validate.go index d12626e2f831..2f34a8cf9aa6 100644 --- a/core/vm/validate.go +++ b/core/vm/validate.go @@ -160,17 +160,17 @@ func validateControlFlow(code []byte, section int, metadata []*FunctionMetadata, return 0, 0, fmt.Errorf("stack overflow") } case op == RJUMP: - arg := parseUint16(code[pos+1:]) - pos += 3 + arg + arg := parseInt16(code[pos+1:]) + pos += 3 + int(arg) case op == RJUMPI: - arg := parseUint16(code[pos+1:]) - worklist = append(worklist, item{pos: pos + 3 + arg, height: height}) + arg := parseInt16(code[pos+1:]) + worklist = append(worklist, item{pos: pos + 3 + int(arg), height: height}) pos += 3 case op == RJUMPV: count := int(code[pos+1]) for i := 0; i < count; i++ { - arg := parseUint16(code[pos+2+2*i:]) - worklist = append(worklist, item{pos: pos + arg, height: height}) + arg := parseInt16(code[pos+2+2*i:]) + worklist = append(worklist, item{pos: pos + int(arg), height: height}) } pos += 2 + 2*count default: diff --git a/core/vm/validate_test.go b/core/vm/validate_test.go index e9e44a735c14..885acce8e406 100644 --- a/core/vm/validate_test.go +++ b/core/vm/validate_test.go @@ -134,9 +134,28 @@ func TestValidateCode(t *testing.T) { metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 1}, {Input: 0, Output: 1, MaxStackHeight: 1}}, err: fmt.Errorf("jumpf to section with more outputs"), }, + { + code: []byte{ + byte(RJUMP), 0x00, 0x03, + byte(JUMPDEST), + byte(JUMPDEST), + byte(RETURN), + byte(PUSH1), 20, + byte(PUSH1), 39, + byte(PUSH1), 0x00, + byte(CODECOPY), + byte(PUSH1), 20, + byte(PUSH1), 0x00, + byte(RJUMP), 0xff, 0xef, + }, + section: 0, + metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 3}}, + }, } { - if err := validateCode(test.code, test.section, test.metadata, &shanghaiEOFInstructionSet); test.err != nil && (err == nil || test.err.Error() != err.Error()) { - t.Fatalf("test %d: unexpected error (want: %v, got: %v)", i, test.err, err) + err := validateCode(test.code, test.section, test.metadata, &shanghaiEOFInstructionSet) + if (err != nil && test.err == nil) || (err == nil && test.err != nil) || (err != nil && test.err != nil && err.Error() != test.err.Error()) { + t.Errorf("test %d: unexpected error (want: %v, got: %v)", i, test.err, err) } } + } From 836a7f85fa304a2e7f1ca0dde68ff1d02ec4a2fe Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Fri, 30 Dec 2022 08:17:02 -0700 Subject: [PATCH 13/53] core/vm: add invalid to jump table --- core/vm/jump_table.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/vm/jump_table.go b/core/vm/jump_table.go index 465232ef368c..b0623e9e0262 100644 --- a/core/vm/jump_table.go +++ b/core/vm/jump_table.go @@ -1056,6 +1056,12 @@ func newFrontierInstructionSet() JumpTable { minStack: minStack(1, 0), maxStack: maxStack(1, 0), }, + INVALID: { + execute: opUndefined, + minStack: minStack(0, 0), + maxStack: maxStack(0, 0), + terminal: true, + }, } // Fill all unassigned slots with opUndefined. From afc52bfb55f7548ad5416e2f11ab1ec95cf8261d Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Fri, 30 Dec 2022 08:26:18 -0700 Subject: [PATCH 14/53] core/vm: reorder stack height checks in eof validation --- core/vm/validate.go | 16 +++++++++------- core/vm/validate_test.go | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/core/vm/validate.go b/core/vm/validate.go index 2f34a8cf9aa6..277075751eab 100644 --- a/core/vm/validate.go +++ b/core/vm/validate.go @@ -150,6 +150,15 @@ func validateControlFlow(code []byte, section int, metadata []*FunctionMetadata, } heights[pos] = height + // Validate height for current op and update as needed. + if jt[op].minStack > height { + return 0, 0, fmt.Errorf("stack underflow") + } + if jt[op].maxStack < height { + return 0, 0, fmt.Errorf("stack overflow") + } + height += int(params.StackLimit) - jt[op].maxStack + switch { case op == CALLF: arg := parseUint16(code[pos+1:]) @@ -174,13 +183,6 @@ func validateControlFlow(code []byte, section int, metadata []*FunctionMetadata, } pos += 2 + 2*count default: - if jt[op].minStack > height { - return 0, 0, fmt.Errorf("stack underflow") - } - if jt[op].maxStack < height { - return 0, 0, fmt.Errorf("stack overflow") - } - height += int(params.StackLimit) - jt[op].maxStack if op >= PUSH1 && op <= PUSH32 { pos += 1 + int(op-PUSH0) } else { diff --git a/core/vm/validate_test.go b/core/vm/validate_test.go index 885acce8e406..dc42a215ffeb 100644 --- a/core/vm/validate_test.go +++ b/core/vm/validate_test.go @@ -151,6 +151,24 @@ func TestValidateCode(t *testing.T) { section: 0, metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 3}}, }, + { + code: []byte{ + byte(PUSH1), 1, + byte(RJUMPI), 0x00, 0x03, + byte(JUMPDEST), + byte(JUMPDEST), + byte(STOP), + byte(PUSH1), 20, + byte(PUSH1), 39, + byte(PUSH1), 0x00, + byte(CODECOPY), + byte(PUSH1), 20, + byte(PUSH1), 0x00, + byte(RETURN), + }, + section: 0, + metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 3}}, + }, } { err := validateCode(test.code, test.section, test.metadata, &shanghaiEOFInstructionSet) if (err != nil && test.err == nil) || (err == nil && test.err != nil) || (err != nil && test.err != nil && err.Error() != test.err.Error()) { From 2cd597718d9b6e7d7813ce66636dd1e22439ccc2 Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Fri, 30 Dec 2022 08:57:30 -0700 Subject: [PATCH 15/53] core/vm: follow rjumpv branches correctly --- core/vm/validate.go | 2 +- core/vm/validate_test.go | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/core/vm/validate.go b/core/vm/validate.go index 277075751eab..151b8db3b88c 100644 --- a/core/vm/validate.go +++ b/core/vm/validate.go @@ -179,7 +179,7 @@ func validateControlFlow(code []byte, section int, metadata []*FunctionMetadata, count := int(code[pos+1]) for i := 0; i < count; i++ { arg := parseInt16(code[pos+2+2*i:]) - worklist = append(worklist, item{pos: pos + int(arg), height: height}) + worklist = append(worklist, item{pos: pos + 2 + 2*count + int(arg), height: height}) } pos += 2 + 2*count default: diff --git a/core/vm/validate_test.go b/core/vm/validate_test.go index dc42a215ffeb..1ccf68873270 100644 --- a/core/vm/validate_test.go +++ b/core/vm/validate_test.go @@ -169,6 +169,24 @@ func TestValidateCode(t *testing.T) { section: 0, metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 3}}, }, + { + code: []byte{ + byte(PUSH1), 1, + byte(RJUMPV), 0x02, 0x00, 0x03, 0xff, 0xf8, + byte(JUMPDEST), + byte(JUMPDEST), + byte(STOP), + byte(PUSH1), 20, + byte(PUSH1), 39, + byte(PUSH1), 0x00, + byte(CODECOPY), + byte(PUSH1), 20, + byte(PUSH1), 0x00, + byte(RETURN), + }, + section: 0, + metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 3}}, + }, } { err := validateCode(test.code, test.section, test.metadata, &shanghaiEOFInstructionSet) if (err != nil && test.err == nil) || (err == nil && test.err != nil) || (err != nil && test.err != nil && err.Error() != test.err.Error()) { From 944ba9f37d0c34153158e4438198997b3b798d7a Mon Sep 17 00:00:00 2001 From: Martin Holst Swende Date: Fri, 30 Dec 2022 08:23:59 +0100 Subject: [PATCH 16/53] core/vm: make eof parser more paranoid --- core/vm/eips.go | 5 +-- core/vm/eof.go | 80 ++++++++++++++++++++++++++++--------------- core/vm/eof_test.go | 1 + core/vm/jump_table.go | 4 +++ core/vm/validate.go | 4 +-- 5 files changed, 62 insertions(+), 32 deletions(-) diff --git a/core/vm/eips.go b/core/vm/eips.go index 817d359dc10c..4702bcd31754 100644 --- a/core/vm/eips.go +++ b/core/vm/eips.go @@ -20,6 +20,7 @@ import ( "fmt" "sort" + "encoding/binary" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/params" "github.com/holiman/uint256" @@ -345,7 +346,7 @@ func opRjumpv(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]b func opCallf(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { var ( code = scope.Contract.CodeAt(scope.CodeSection) - idx = parseUint16(code[*pc+1:]) + idx = binary.BigEndian.Uint16(code[*pc+1:]) typ = scope.Contract.Container.Types[scope.CodeSection] ) if scope.Stack.len()+int(typ.MaxStackHeight) >= 1024 { @@ -379,7 +380,7 @@ func opRetf(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byt func opJumpf(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { var ( code = scope.Contract.CodeAt(scope.CodeSection) - idx = parseUint16(code[*pc+1:]) + idx = binary.BigEndian.Uint16(code[*pc+1:]) ) scope.CodeSection = uint64(idx) *pc = 0 diff --git a/core/vm/eof.go b/core/vm/eof.go index e3587d0dbf1e..50851e091ef5 100644 --- a/core/vm/eof.go +++ b/core/vm/eof.go @@ -18,7 +18,9 @@ package vm import ( "bytes" + "encoding/binary" "fmt" + "io" ) const ( @@ -72,14 +74,15 @@ func (c *Container) MarshalBinary() []byte { copy(b, eofMagic) b = append(b, eof1Version) b = append(b, kindTypes) - b = appendUint16(b, uint16(len(c.Types)*4)) + b = binary.BigEndian.AppendUint16(b, uint16(len(c.Types)*4)) + //b = appendUint16(b, uint16(len(c.Types)*4)) b = append(b, kindCode) - b = appendUint16(b, uint16(len(c.Code))) + b = binary.BigEndian.AppendUint16(b, uint16(len(c.Code))) for _, code := range c.Code { - b = appendUint16(b, uint16(len(code))) + b = binary.BigEndian.AppendUint16(b, uint16(len(code))) } b = append(b, kindData) - b = appendUint16(b, uint16(len(c.Data))) + b = binary.BigEndian.AppendUint16(b, uint16(len(c.Data))) b = append(b, 0) // terminator for _, ty := range c.Types { @@ -109,10 +112,13 @@ func (c *Container) UnmarshalBinary(b []byte) error { typesSize, dataSize int codeSizes []int kind int + err error ) // Parse types size. - if kind, typesSize = parseSection(b, offsetTypesKind); kind != kindTypes { + if kind, typesSize, err = parseSection(b, offsetTypesKind); err != nil { + return err + } else if kind != kindTypes { return fmt.Errorf("expected kind types") } if typesSize > 4*1024 { @@ -120,7 +126,9 @@ func (c *Container) UnmarshalBinary(b []byte) error { } // Parse code sizes. - if kind, codeSizes = parseSectionList(b, offsetCodeKind); kind != kindCode { + if kind, codeSizes, err = parseSectionList(b, offsetCodeKind); err != nil { + return fmt.Errorf("failed to parse section list: %v", err) + } else if kind != kindCode { return fmt.Errorf("expected kind code") } if len(codeSizes) != typesSize/4 { @@ -132,7 +140,9 @@ func (c *Container) UnmarshalBinary(b []byte) error { if len(b) < offsetDataKind+2 { return fmt.Errorf("container size invalid") } - if kind, dataSize = parseSection(b, offsetDataKind); kind != kindData { + if kind, dataSize, err = parseSection(b, offsetDataKind); err != nil { + return err + } else if kind != kindData { return fmt.Errorf("expected kind data") } offsetTerminator := offsetDataKind + 3 @@ -153,7 +163,7 @@ func (c *Container) UnmarshalBinary(b []byte) error { sig := &FunctionMetadata{ Input: b[idx+i*4], Output: b[idx+i*4+1], - MaxStackHeight: uint16(parseUint16(b[idx+i*4+2:])), + MaxStackHeight: uint16(binary.BigEndian.Uint16(b[idx+i*4+2:])), } if sig.MaxStackHeight > 1024 { return fmt.Errorf("type annotation %d max stack height must not exceed 1024", i) @@ -193,31 +203,52 @@ func (c *Container) ValidateCode(jt *JumpTable) error { } // parseSection decodes a (kind, size) pair from an EOF header. -func parseSection(b []byte, idx int) (kind, size int) { - return int(b[idx]), parseUint16(b[idx+1:]) +func parseSection(b []byte, idx int) (kind, size int, err error) { + if idx+3 >= len(b) { + return 0, 0, io.ErrUnexpectedEOF + } + kind = int(b[idx]) + size = int(binary.BigEndian.Uint16(b[idx+1:])) + return kind, size, nil } // parseSectionList decodes a (kind, len, []codeSize) section list from an EOF // header. -func parseSectionList(b []byte, idx int) (kind int, list []int) { - return int(b[idx]), parseList(b, idx+1) +func parseSectionList(b []byte, idx int) (kind int, list []int, err error) { + if idx >= len(b) { + return 0, nil, io.ErrUnexpectedEOF + } + kind = int(b[idx]) + list, err = parseList(b, idx+1) + if err != nil { + return 0, nil, err + } + return kind, list, nil } // parseList decodes a list of uint16.. -func parseList(b []byte, idx int) []int { - count := parseUint16(b[idx:]) +func parseList(b []byte, idx int) ([]int, error) { + if len(b) < idx+2 { + return nil, io.ErrUnexpectedEOF + } + count := binary.BigEndian.Uint16(b[idx:]) + if len(b) < 2+int(count*2) { + return nil, io.ErrUnexpectedEOF + + } list := make([]int, count) - for i := 0; i < count; i++ { - list[i] = parseUint16(b[idx+2*i+2:]) + for i := 0; i < int(count); i++ { + list[i] = int(binary.BigEndian.Uint16(b[idx+2*i+2:])) } - return list + return list, nil } // parseUint16 parses a 16 bit unsigned integer. -func parseUint16(b []byte) int { - size := uint16(b[0]) << 8 - size += uint16(b[1]) - return int(size) +func parseUint16(b []byte) (int, error) { + if len(b) < 2 { + return 0, io.ErrUnexpectedEOF + } + return int(binary.BigEndian.Uint16(b)), nil } // check returns if b[idx] == want after performing a bounds check. @@ -234,10 +265,3 @@ func sum(list []int) (s int) { } return } - -func appendUint16(b []byte, v uint16) []byte { - return append(b, - byte(v>>8), - byte(v), - ) -} diff --git a/core/vm/eof_test.go b/core/vm/eof_test.go index 2aaa52480668..6c640aa36ffe 100644 --- a/core/vm/eof_test.go +++ b/core/vm/eof_test.go @@ -55,6 +55,7 @@ func TestEOFMarshaling(t *testing.T) { b = test.want.MarshalBinary() got Container ) + t.Logf("b: %#x", b) if err := got.UnmarshalBinary(b); err != nil && err != test.err { t.Fatalf("test %d: got error \"%v\", want \"%v\"", i, err, test.err) } diff --git a/core/vm/jump_table.go b/core/vm/jump_table.go index b0623e9e0262..69661221885f 100644 --- a/core/vm/jump_table.go +++ b/core/vm/jump_table.go @@ -86,6 +86,10 @@ func validate(jt JumpTable) JumpTable { return jt } +func NewShanghaiEOFInstructionSetForTesting() JumpTable { + return newShanghaiEOFInstructionSet() +} + func newShanghaiInstructionSet() JumpTable { instructionSet := newMergeInstructionSet() enable3855(&instructionSet) // PUSH0 instruction diff --git a/core/vm/validate.go b/core/vm/validate.go index 151b8db3b88c..f62750e2325c 100644 --- a/core/vm/validate.go +++ b/core/vm/validate.go @@ -78,7 +78,7 @@ func validateCode(code []byte, section int, metadata []*FunctionMetadata, jt *Ju if i+2 >= len(code) { return fmt.Errorf("truncated operand") } - arg := parseUint16(code[i+1:]) + arg, _ := parseUint16(code[i+1:]) if arg >= len(metadata) { return fmt.Errorf("code section out-of-bounds (want: %d, have: %d)", arg, len(metadata)) } @@ -161,7 +161,7 @@ func validateControlFlow(code []byte, section int, metadata []*FunctionMetadata, switch { case op == CALLF: - arg := parseUint16(code[pos+1:]) + arg, _ := parseUint16(code[pos+1:]) if metadata[arg].Input < uint8(height) { return 0, 0, fmt.Errorf("stack underflow") } From 3df0d42bff49816d9148a0727ef9ddab6192370e Mon Sep 17 00:00:00 2001 From: Martin Holst Swende Date: Fri, 30 Dec 2022 08:49:18 +0100 Subject: [PATCH 17/53] core/vm: fix on eof --- core/vm/eof.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/vm/eof.go b/core/vm/eof.go index 50851e091ef5..e233b11422f7 100644 --- a/core/vm/eof.go +++ b/core/vm/eof.go @@ -232,7 +232,7 @@ func parseList(b []byte, idx int) ([]int, error) { return nil, io.ErrUnexpectedEOF } count := binary.BigEndian.Uint16(b[idx:]) - if len(b) < 2+int(count*2) { + if len(b) < idx + 2+int(count*2) { return nil, io.ErrUnexpectedEOF } From 723e363a58adffa52419eae59037c55ab613f731 Mon Sep 17 00:00:00 2001 From: Martin Holst Swende Date: Fri, 30 Dec 2022 08:59:31 +0100 Subject: [PATCH 18/53] core/vm: fix uint16 overflow --- core/vm/eof.go | 4 ++-- core/vm/eof_test.go | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/core/vm/eof.go b/core/vm/eof.go index e233b11422f7..9bc4786da900 100644 --- a/core/vm/eof.go +++ b/core/vm/eof.go @@ -232,13 +232,13 @@ func parseList(b []byte, idx int) ([]int, error) { return nil, io.ErrUnexpectedEOF } count := binary.BigEndian.Uint16(b[idx:]) - if len(b) < idx + 2+int(count*2) { + if len(b) <= idx+2+int(count)*2 { return nil, io.ErrUnexpectedEOF } list := make([]int, count) for i := 0; i < int(count); i++ { - list[i] = int(binary.BigEndian.Uint16(b[idx+2*i+2:])) + list[i] = int(binary.BigEndian.Uint16(b[idx+2+2*i:])) } return list, nil } diff --git a/core/vm/eof_test.go b/core/vm/eof_test.go index 6c640aa36ffe..e8638128c2ed 100644 --- a/core/vm/eof_test.go +++ b/core/vm/eof_test.go @@ -30,7 +30,7 @@ func TestEOFMarshaling(t *testing.T) { }{ { want: Container{ - Types: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 0}}, + Types: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 1}}, Code: [][]byte{common.Hex2Bytes("604200")}, Data: []byte{0x01, 0x02, 0x03}, }, @@ -38,9 +38,9 @@ func TestEOFMarshaling(t *testing.T) { { want: Container{ Types: []*FunctionMetadata{ - {Input: 0, Output: 0, MaxStackHeight: 0}, - {Input: 2, Output: 3, MaxStackHeight: 32}, - {Input: 1, Output: 1, MaxStackHeight: 1023}, + {Input: 0, Output: 0, MaxStackHeight: 1}, + {Input: 2, Output: 3, MaxStackHeight: 4}, + {Input: 1, Output: 1, MaxStackHeight: 1}, }, Code: [][]byte{ common.Hex2Bytes("604200"), From f4188b22134045f4c04a04b500c30bdd3fabf404 Mon Sep 17 00:00:00 2001 From: Martin Holst Swende Date: Fri, 30 Dec 2022 14:32:26 +0100 Subject: [PATCH 19/53] core/vm: tests for codeBitmap/eofCodeBitmap --- core/vm/analysis_test.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/core/vm/analysis_test.go b/core/vm/analysis_test.go index 172148cccc1b..a50b360e5e4b 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" ) @@ -129,3 +130,20 @@ func BenchmarkJumpdestOpAnalysis(bench *testing.B) { op = STOP bench.Run(op.String(), bencher) } + +func TestCodeAnalysis(t *testing.T) { + for _, tc := range []string{ + "5e30303030", + } { + eofCodeBitmap(common.FromHex(tc)) + codeBitmap(common.FromHex(tc)) + } +} + +func FuzzCodeAnalysis(f *testing.F) { + f.Add(common.FromHex("5e30303030")) + f.Fuzz(func(t *testing.T, data []byte) { + eofCodeBitmap(data) + codeBitmap(data) + }) +} From b4c013663747b10f9e3434b0413f0db70bd39cb4 Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Sat, 31 Dec 2022 08:19:42 -0700 Subject: [PATCH 20/53] core/vm: bound check rjumpv before code analysis --- core/vm/validate.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/core/vm/validate.go b/core/vm/validate.go index f62750e2325c..3172e3f18865 100644 --- a/core/vm/validate.go +++ b/core/vm/validate.go @@ -55,10 +55,6 @@ func validateCode(code []byte, section int, metadata []*FunctionMetadata, jt *Ju } i += 2 case op == RJUMPV: - if analysis == nil { - tmp := eofCodeBitmap(code) - analysis = &tmp - } // Verify each branch in the jump table points to a // destination in-bounds. if i+1 >= len(code) { @@ -68,6 +64,13 @@ func validateCode(code []byte, section int, metadata []*FunctionMetadata, jt *Ju if count == 0 { return fmt.Errorf("rjumpv branch count must not be 0") } + if i+count+1 >= len(code) { + return fmt.Errorf("truncated jump table operand") + } + if analysis == nil { + tmp := eofCodeBitmap(code) + analysis = &tmp + } for j := 0; j < count; j++ { if err := checkDest(code[i+2+j*2:], *analysis, i+2*count+2, len(code)); err != nil { return err From 092750eb786e36755cc5e3de23de51a589a8f9ab Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Sat, 31 Dec 2022 09:18:47 -0700 Subject: [PATCH 21/53] core/vm: fix fuzzer crashes --- core/vm/analysis.go | 6 ++++++ core/vm/eof.go | 3 +++ core/vm/validate.go | 5 ++++- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/core/vm/analysis.go b/core/vm/analysis.go index b96a4935223d..5201c70f17af 100644 --- a/core/vm/analysis.go +++ b/core/vm/analysis.go @@ -144,7 +144,13 @@ func eofCodeBitmapInternal(code, bits bitvec) bitvec { numbits = uint8(op - PUSH1 + 1) } else if int8(op) == int8(RJUMPV) { // RJUMPV has variable sized operand + if pc >= uint64(len(code)) { + continue // skip over if rjumpv is truncated + } numbits = code[pc]*2 + 1 + if pc+uint64(numbits) >= uint64(len(code)) { + continue // skip over if rjumpv is truncated + } } else { // If not PUSH (the int8(op) > int(PUSH32) is always false). continue diff --git a/core/vm/eof.go b/core/vm/eof.go index 9bc4786da900..9837e08261a4 100644 --- a/core/vm/eof.go +++ b/core/vm/eof.go @@ -121,6 +121,9 @@ func (c *Container) UnmarshalBinary(b []byte) error { } else if kind != kindTypes { return fmt.Errorf("expected kind types") } + if typesSize < 4 { + return fmt.Errorf("type section size invalid") + } if typesSize > 4*1024 { return fmt.Errorf("number of code sections must not exceed 1024 (got %d)", typesSize) } diff --git a/core/vm/validate.go b/core/vm/validate.go index 3172e3f18865..c289ecb1eba3 100644 --- a/core/vm/validate.go +++ b/core/vm/validate.go @@ -44,6 +44,9 @@ func validateCode(code []byte, section int, metadata []*FunctionMetadata, jt *Ju } i += size case op == RJUMP || op == RJUMPI: + if i+2 >= len(code) { + return fmt.Errorf("truncated rjump* operand") + } if analysis == nil { tmp := eofCodeBitmap(code) analysis = &tmp @@ -64,7 +67,7 @@ func validateCode(code []byte, section int, metadata []*FunctionMetadata, jt *Ju if count == 0 { return fmt.Errorf("rjumpv branch count must not be 0") } - if i+count+1 >= len(code) { + if i+count >= len(code) { return fmt.Errorf("truncated jump table operand") } if analysis == nil { From 4d9b08f3f92953a82a84018c779f73308f622098 Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Sat, 31 Dec 2022 12:32:10 -0700 Subject: [PATCH 22/53] core/vm: more fixes --- core/vm/eof.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/vm/eof.go b/core/vm/eof.go index 9837e08261a4..feddcea3af78 100644 --- a/core/vm/eof.go +++ b/core/vm/eof.go @@ -121,7 +121,7 @@ func (c *Container) UnmarshalBinary(b []byte) error { } else if kind != kindTypes { return fmt.Errorf("expected kind types") } - if typesSize < 4 { + if typesSize%4 != 0 { return fmt.Errorf("type section size invalid") } if typesSize > 4*1024 { From e84268b45e64a187a9361b8e80bc1b294e991b57 Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Sat, 31 Dec 2022 16:23:09 -0700 Subject: [PATCH 23/53] core/vm: more fixes --- core/vm/eof.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/vm/eof.go b/core/vm/eof.go index feddcea3af78..f7bf88cb4bba 100644 --- a/core/vm/eof.go +++ b/core/vm/eof.go @@ -121,7 +121,7 @@ func (c *Container) UnmarshalBinary(b []byte) error { } else if kind != kindTypes { return fmt.Errorf("expected kind types") } - if typesSize%4 != 0 { + if typesSize < 4 || typesSize%4 != 0 { return fmt.Errorf("type section size invalid") } if typesSize > 4*1024 { From 6a6af8163bf7851213e8671229767326c5d26dbc Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Sun, 1 Jan 2023 16:46:19 -0700 Subject: [PATCH 24/53] core/vm: bound type section inputs and outputs to 127 --- core/vm/eof.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/vm/eof.go b/core/vm/eof.go index f7bf88cb4bba..bd78e9e39199 100644 --- a/core/vm/eof.go +++ b/core/vm/eof.go @@ -168,6 +168,9 @@ func (c *Container) UnmarshalBinary(b []byte) error { Output: b[idx+i*4+1], MaxStackHeight: uint16(binary.BigEndian.Uint16(b[idx+i*4+2:])), } + if sig.Output > 127 || sig.Input > 127 { + return fmt.Errorf("type annotation %d inputs and outputs must not exceed 127", i) + } if sig.MaxStackHeight > 1024 { return fmt.Errorf("type annotation %d max stack height must not exceed 1024", i) } From 349275839986187557ea38c39990d6348a42db66 Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Mon, 2 Jan 2023 06:36:49 -0700 Subject: [PATCH 25/53] core/vm: exit stack validation on terminal op --- core/vm/validate.go | 5 ++++- core/vm/validate_test.go | 16 ++++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/core/vm/validate.go b/core/vm/validate.go index c289ecb1eba3..72286a597bc8 100644 --- a/core/vm/validate.go +++ b/core/vm/validate.go @@ -105,7 +105,7 @@ func validateCode(code []byte, section int, metadata []*FunctionMetadata, jt *Ju } else if paths != count { return fmt.Errorf("unreachable code") } else if max != int(metadata[section].MaxStackHeight) { - return fmt.Errorf("computed max stack height for code section %d does not match expect (want: %d, got: %d)", section, metadata[section].MaxStackHeight, max) + return fmt.Errorf("computed max stack height for code section %d does not match expected (want: %d, got: %d)", section, metadata[section].MaxStackHeight, max) } return nil } @@ -142,6 +142,7 @@ func validateControlFlow(code []byte, section int, metadata []*FunctionMetadata, height = worklist[idx].height ) worklist = worklist[:idx] + outer: for pos < len(code) { op := OpCode(code[pos]) @@ -191,6 +192,8 @@ func validateControlFlow(code []byte, section int, metadata []*FunctionMetadata, default: if op >= PUSH1 && op <= PUSH32 { pos += 1 + int(op-PUSH0) + } else if jt[op].terminal { + break outer } else { // No immediate. pos += 1 diff --git a/core/vm/validate_test.go b/core/vm/validate_test.go index 1ccf68873270..01c0b575b4e1 100644 --- a/core/vm/validate_test.go +++ b/core/vm/validate_test.go @@ -19,6 +19,8 @@ package vm import ( "fmt" "testing" + + "github.com/ethereum/go-ethereum/common" ) func TestValidateCode(t *testing.T) { @@ -78,7 +80,7 @@ func TestValidateCode(t *testing.T) { }, section: 0, metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 2}}, - err: fmt.Errorf("computed max stack height for code section 0 does not match expect (want: 2, got: 1)"), + err: fmt.Errorf("computed max stack height for code section 0 does not match expected (want: 2, got: 1)"), }, { code: []byte{ @@ -187,10 +189,20 @@ func TestValidateCode(t *testing.T) { section: 0, metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 3}}, }, + { + code: []byte{ + byte(STOP), + byte(STOP), + byte(INVALID), + }, + section: 0, + metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 0}}, + err: fmt.Errorf("unreachable code"), + }, } { err := validateCode(test.code, test.section, test.metadata, &shanghaiEOFInstructionSet) if (err != nil && test.err == nil) || (err == nil && test.err != nil) || (err != nil && test.err != nil && err.Error() != test.err.Error()) { - t.Errorf("test %d: unexpected error (want: %v, got: %v)", i, test.err, err) + t.Errorf("test %d (%s): unexpected error (want: %v, got: %v)", i, common.Bytes2Hex(test.code), test.err, err) } } From 60f1ed3778c039e76e631b4827afa64bea91a71f Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Mon, 2 Jan 2023 06:40:59 -0700 Subject: [PATCH 26/53] core/vm: reduce max_stack_height limit to 1023 --- core/vm/eof.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/vm/eof.go b/core/vm/eof.go index bd78e9e39199..a43df5fd4c91 100644 --- a/core/vm/eof.go +++ b/core/vm/eof.go @@ -171,8 +171,8 @@ func (c *Container) UnmarshalBinary(b []byte) error { if sig.Output > 127 || sig.Input > 127 { return fmt.Errorf("type annotation %d inputs and outputs must not exceed 127", i) } - if sig.MaxStackHeight > 1024 { - return fmt.Errorf("type annotation %d max stack height must not exceed 1024", i) + if sig.MaxStackHeight > 1023 { + return fmt.Errorf("type annotation %d max stack height must not exceed 1023", i) } types = append(types, sig) } From c4f8993aa05f6ab7a2b047a35e33fec993dd1bf1 Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Mon, 2 Jan 2023 07:28:11 -0700 Subject: [PATCH 27/53] core/vm: validate retf outputs --- core/vm/validate.go | 5 +++++ core/vm/validate_test.go | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/core/vm/validate.go b/core/vm/validate.go index 72286a597bc8..e6e0884ab066 100644 --- a/core/vm/validate.go +++ b/core/vm/validate.go @@ -189,6 +189,11 @@ func validateControlFlow(code []byte, section int, metadata []*FunctionMetadata, worklist = append(worklist, item{pos: pos + 2 + 2*count + int(arg), height: height}) } pos += 2 + 2*count + case op == RETF: + if int(metadata[section].Output) != height { + return 0, 0, fmt.Errorf("wrong number of outputs (want: %d, got: %d)", metadata[section].Output, height) + } + break outer default: if op >= PUSH1 && op <= PUSH32 { pos += 1 + int(op-PUSH0) diff --git a/core/vm/validate_test.go b/core/vm/validate_test.go index 01c0b575b4e1..52e609ef1b8d 100644 --- a/core/vm/validate_test.go +++ b/core/vm/validate_test.go @@ -199,6 +199,14 @@ func TestValidateCode(t *testing.T) { metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 0}}, err: fmt.Errorf("unreachable code"), }, + { + code: []byte{ + byte(RETF), + }, + section: 0, + metadata: []*FunctionMetadata{{Input: 0, Output: 1, MaxStackHeight: 0}}, + err: fmt.Errorf("wrong number of outputs (want: 1, got: 0)"), + }, } { err := validateCode(test.code, test.section, test.metadata, &shanghaiEOFInstructionSet) if (err != nil && test.err == nil) || (err == nil && test.err != nil) || (err != nil && test.err != nil && err.Error() != test.err.Error()) { From 2c8a9ab528da974fb6ee00f8266fbb215d70551a Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Mon, 2 Jan 2023 07:53:30 -0700 Subject: [PATCH 28/53] core/vm: account for input stack height in max stack height calculation --- core/vm/validate.go | 2 +- core/vm/validate_test.go | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/core/vm/validate.go b/core/vm/validate.go index e6e0884ab066..ec3d18fd3e4e 100644 --- a/core/vm/validate.go +++ b/core/vm/validate.go @@ -133,7 +133,7 @@ func validateControlFlow(code []byte, section int, metadata []*FunctionMetadata, var ( heights = make(map[int]int) worklist = []item{{0, int(metadata[section].Input)}} - maxStackHeight = 0 + maxStackHeight = int(metadata[section].Input) ) for 0 < len(worklist) { var ( diff --git a/core/vm/validate_test.go b/core/vm/validate_test.go index 52e609ef1b8d..8503660f142d 100644 --- a/core/vm/validate_test.go +++ b/core/vm/validate_test.go @@ -207,6 +207,13 @@ func TestValidateCode(t *testing.T) { metadata: []*FunctionMetadata{{Input: 0, Output: 1, MaxStackHeight: 0}}, err: fmt.Errorf("wrong number of outputs (want: 1, got: 0)"), }, + { + code: []byte{ + byte(RETF), + }, + section: 0, + metadata: []*FunctionMetadata{{Input: 3, Output: 3, MaxStackHeight: 3}}, + }, } { err := validateCode(test.code, test.section, test.metadata, &shanghaiEOFInstructionSet) if (err != nil && test.err == nil) || (err == nil && test.err != nil) || (err != nil && test.err != nil && err.Error() != test.err.Error()) { From ae91086fb89397463bb074c705a7a047f0277731 Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Mon, 2 Jan 2023 10:05:06 -0700 Subject: [PATCH 29/53] core/vm: increment pos after callf in stack validation --- core/vm/validate.go | 1 + core/vm/validate_test.go | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/core/vm/validate.go b/core/vm/validate.go index ec3d18fd3e4e..f4115ca4f2e3 100644 --- a/core/vm/validate.go +++ b/core/vm/validate.go @@ -175,6 +175,7 @@ func validateControlFlow(code []byte, section int, metadata []*FunctionMetadata, if int(metadata[arg].Output)+height > int(params.StackLimit) { return 0, 0, fmt.Errorf("stack overflow") } + pos += 3 case op == RJUMP: arg := parseInt16(code[pos+1:]) pos += 3 + int(arg) diff --git a/core/vm/validate_test.go b/core/vm/validate_test.go index 8503660f142d..df0736d40de7 100644 --- a/core/vm/validate_test.go +++ b/core/vm/validate_test.go @@ -39,6 +39,14 @@ func TestValidateCode(t *testing.T) { section: 0, metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 1}}, }, + { + code: []byte{ + byte(CALLF), 0x00, 0x00, + byte(STOP), + }, + section: 0, + metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 0}}, + }, { code: []byte{ byte(CALLER), From 96e435d39ba4e417730e2e0bb6e302970c168fde Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Mon, 2 Jan 2023 10:14:54 -0700 Subject: [PATCH 30/53] core/vm: flip comparison operator in stack validation for callf --- core/vm/validate.go | 2 +- core/vm/validate_test.go | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/core/vm/validate.go b/core/vm/validate.go index f4115ca4f2e3..1d102eb30b0d 100644 --- a/core/vm/validate.go +++ b/core/vm/validate.go @@ -169,7 +169,7 @@ func validateControlFlow(code []byte, section int, metadata []*FunctionMetadata, switch { case op == CALLF: arg, _ := parseUint16(code[pos+1:]) - if metadata[arg].Input < uint8(height) { + if metadata[arg].Input > uint8(height) { return 0, 0, fmt.Errorf("stack underflow") } if int(metadata[arg].Output)+height > int(params.StackLimit) { diff --git a/core/vm/validate_test.go b/core/vm/validate_test.go index df0736d40de7..02f5f73c6e3c 100644 --- a/core/vm/validate_test.go +++ b/core/vm/validate_test.go @@ -47,6 +47,15 @@ func TestValidateCode(t *testing.T) { section: 0, metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 0}}, }, + { + code: []byte{ + byte(ADDRESS), + byte(CALLF), 0x00, 0x00, + byte(STOP), + }, + section: 0, + metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 0}}, + }, { code: []byte{ byte(CALLER), From 3ab52d53ffcec8a90281dd67904dd3c68e4704da Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Mon, 2 Jan 2023 12:18:34 -0700 Subject: [PATCH 31/53] core/vm: clean up eof unmarshaling errors plus a few other fixes --- core/vm/analysis.go | 2 +- core/vm/analysis_test.go | 4 +- core/vm/eof.go | 106 +++++++++++++++++++++++++-------------- core/vm/evm.go | 5 +- core/vm/validate.go | 10 ++-- core/vm/validate_test.go | 2 +- 6 files changed, 79 insertions(+), 50 deletions(-) diff --git a/core/vm/analysis.go b/core/vm/analysis.go index 5201c70f17af..8321da094c1a 100644 --- a/core/vm/analysis.go +++ b/core/vm/analysis.go @@ -148,7 +148,7 @@ func eofCodeBitmapInternal(code, bits bitvec) bitvec { continue // skip over if rjumpv is truncated } numbits = code[pc]*2 + 1 - if pc+uint64(numbits) >= uint64(len(code)) { + if pc+uint64(numbits) > uint64(len(code)) { continue // skip over if rjumpv is truncated } } else { diff --git a/core/vm/analysis_test.go b/core/vm/analysis_test.go index a50b360e5e4b..6bc99ca28fad 100644 --- a/core/vm/analysis_test.go +++ b/core/vm/analysis_test.go @@ -70,8 +70,8 @@ func TestEOFAnalysis(t *testing.T) { exp byte which int }{ - {[]byte{byte(RJUMP), 0x01, 0x01, 0x01}, 0b0000_0110, 0}, - {[]byte{byte(RJUMPI), byte(RJUMP), byte(RJUMP), byte(RJUMPI)}, 0b0011_0110, 0}, + // {[]byte{byte(RJUMP), 0x01, 0x01, 0x01}, 0b0000_0110, 0}, + // {[]byte{byte(RJUMPI), byte(RJUMP), byte(RJUMP), byte(RJUMPI)}, 0b0011_0110, 0}, {[]byte{byte(RJUMPV), 0x02, byte(RJUMP), 0x00, byte(RJUMPI), 0x00}, 0b0011_1110, 0}, } for i, test := range tests { diff --git a/core/vm/eof.go b/core/vm/eof.go index a43df5fd4c91..3c46ae31df0a 100644 --- a/core/vm/eof.go +++ b/core/vm/eof.go @@ -21,6 +21,8 @@ import ( "encoding/binary" "fmt" "io" + + "github.com/ethereum/go-ethereum/common" ) const ( @@ -34,6 +36,10 @@ const ( eofFormatByte = 0xef eof1Version = 1 + + maxInputItems = 127 + maxOutputItems = 127 + maxStackHeight = 1023 ) var eofMagic = []byte{0xef, 0x00} @@ -54,7 +60,7 @@ func isEOFVersion1(code []byte) bool { return 2 < len(code) && code[2] == byte(eof1Version) } -// Container is and EOF container object. +// Container is an EOF container object. type Container struct { Types []*FunctionMetadata Code [][]byte @@ -70,12 +76,14 @@ type FunctionMetadata struct { // MarshalBinary encodes an EOF container into binary format. func (c *Container) MarshalBinary() []byte { + // Build EOF prefix. b := make([]byte, 2) copy(b, eofMagic) b = append(b, eof1Version) + + // Write section headers. b = append(b, kindTypes) b = binary.BigEndian.AppendUint16(b, uint16(len(c.Types)*4)) - //b = appendUint16(b, uint16(len(c.Types)*4)) b = append(b, kindCode) b = binary.BigEndian.AppendUint16(b, uint16(len(c.Code))) for _, code := range c.Code { @@ -85,6 +93,7 @@ func (c *Container) MarshalBinary() []byte { b = binary.BigEndian.AppendUint16(b, uint16(len(c.Data))) b = append(b, 0) // terminator + // Write section contents. for _, ty := range c.Types { b = append(b, []byte{ty.Input, ty.Output, byte(ty.MaxStackHeight >> 8), byte(ty.MaxStackHeight & 0x00ff)}...) } @@ -92,71 +101,76 @@ func (c *Container) MarshalBinary() []byte { b = append(b, code...) } b = append(b, c.Data...) + return b } // UnmarshalBinary decodes an EOF container. func (c *Container) UnmarshalBinary(b []byte) error { if !hasEOFMagic(b) { - return fmt.Errorf("invalid magic") + return fmt.Errorf("invalid magic: have %s, want %s", common.Bytes2Hex(b[:min(2, len(b))]), common.Bytes2Hex(eofMagic)) } if !isEOFVersion1(b) { - return fmt.Errorf("invalid eof version") - } - - if len(b) < 15 { - return fmt.Errorf("container size less than minimum valid size") + have := "" + if len(b) > 3 { + have = fmt.Sprintf("%d", int(b[2])) + } + return fmt.Errorf("invalid eof version: have %s, want %d", have, eof1Version) } var ( - typesSize, dataSize int - codeSizes []int - kind int - err error + kind, typesSize, dataSize int + codeSizes []int + err error ) - // Parse types size. - if kind, typesSize, err = parseSection(b, offsetTypesKind); err != nil { + // Parse type section header. + kind, typesSize, err = parseSection(b, offsetTypesKind) + if err != nil { return err - } else if kind != kindTypes { - return fmt.Errorf("expected kind types") + } + if kind != kindTypes { + return fmt.Errorf("expected kind types 0x01: have %x", kind) } if typesSize < 4 || typesSize%4 != 0 { - return fmt.Errorf("type section size invalid") + return fmt.Errorf("type section size must be divisible by 4: have %d", typesSize) } - if typesSize > 4*1024 { - return fmt.Errorf("number of code sections must not exceed 1024 (got %d)", typesSize) + if typesSize/4 > 1024 { + return fmt.Errorf("number of code sections must not exceed 1024: got %d", typesSize/4) } - // Parse code sizes. - if kind, codeSizes, err = parseSectionList(b, offsetCodeKind); err != nil { + // Parse code section header. + kind, codeSizes, err = parseSectionList(b, offsetCodeKind) + if err != nil { return fmt.Errorf("failed to parse section list: %v", err) - } else if kind != kindCode { - return fmt.Errorf("expected kind code") + } + if kind != kindCode { + return fmt.Errorf("expected kind code 0x02: have %x", kind) } if len(codeSizes) != typesSize/4 { - return fmt.Errorf("mismatch of code sections count and type signatures (types %d, code %d)", typesSize/3, len(codeSizes)) + return fmt.Errorf("mismatch of code sections count and type signatures: types %d, code %d)", typesSize/4, len(codeSizes)) } - // Parse data size. + // Parse data section header. offsetDataKind := offsetCodeKind + 2 + 2*len(codeSizes) + 1 - if len(b) < offsetDataKind+2 { - return fmt.Errorf("container size invalid") - } - if kind, dataSize, err = parseSection(b, offsetDataKind); err != nil { + kind, dataSize, err = parseSection(b, offsetDataKind) + if err != nil { return err - } else if kind != kindData { - return fmt.Errorf("expected kind data") } + if kind != kindData { + return fmt.Errorf("expected kind data 0x03: have %x", kind) + } + + // Check for terminator. offsetTerminator := offsetDataKind + 3 if check(b, offsetTerminator, 0) { return fmt.Errorf("expected terminator") } - // Check for terminator. + // Verify overall container size. expectedSize := offsetTerminator + typesSize + sum(codeSizes) + dataSize + 1 if len(b) != expectedSize { - return fmt.Errorf("invalid container size (want %d, got %d)", expectedSize, len(b)) + return fmt.Errorf("invalid container size: have %d, want %d", len(b), expectedSize) } // Parse types section. @@ -168,16 +182,19 @@ func (c *Container) UnmarshalBinary(b []byte) error { Output: b[idx+i*4+1], MaxStackHeight: uint16(binary.BigEndian.Uint16(b[idx+i*4+2:])), } - if sig.Output > 127 || sig.Input > 127 { - return fmt.Errorf("type annotation %d inputs and outputs must not exceed 127", i) + if sig.Input > maxInputItems { + return fmt.Errorf("invalid type annotation at index %d: inputs must not exceed %d: have %d", i, maxInputItems, sig.Input) + } + if sig.Output > maxOutputItems { + return fmt.Errorf("invalid type annotation at index %d: inputs and outputs must not exceed %d: have %d", i, maxOutputItems, sig.Output) } - if sig.MaxStackHeight > 1023 { - return fmt.Errorf("type annotation %d max stack height must not exceed 1023", i) + if sig.MaxStackHeight > maxStackHeight { + return fmt.Errorf("invalid type annotation at index %d: max stack height must not exceed %d: have %d", i, maxStackHeight, sig.MaxStackHeight) } types = append(types, sig) } if types[0].Input != 0 || types[0].Output != 0 { - return fmt.Errorf("input and output of first code section must be 0") + return fmt.Errorf("invalid type annotation at index 0: input and output must be 0: have (%d, %d)", types[0].Input, types[0].Output) } c.Types = types @@ -186,7 +203,7 @@ func (c *Container) UnmarshalBinary(b []byte) error { code := make([][]byte, len(codeSizes)) for i, size := range codeSizes { if size == 0 { - return fmt.Errorf("code section %d size must not be 0", i) + return fmt.Errorf("invalid code section %d: size must not be 0", i) } code[i] = b[idx : idx+size] idx += size @@ -199,6 +216,8 @@ func (c *Container) UnmarshalBinary(b []byte) error { return nil } +// ValidateCode validates each code section of the container against the EOF v1 +// rule set. func (c *Container) ValidateCode(jt *JumpTable) error { for i, code := range c.Code { if err := validateCode(code, i, c.Types, jt); err != nil { @@ -265,6 +284,15 @@ func check(b []byte, idx int, want byte) bool { return b[idx] != want } +// min returns the minimum value between x and y. +func min(x, y int) int { + if x < y { + return x + } + return y +} + +// sum computes the sum of a slice. func sum(list []int) (s int) { for _, n := range list { s += n diff --git a/core/vm/evm.go b/core/vm/evm.go index 9d35d9a8790e..b21dce904f21 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -19,6 +19,7 @@ package vm import ( "fmt" "math/big" + "strings" "sync/atomic" "github.com/ethereum/go-ethereum/common" @@ -485,7 +486,7 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, err = c.ValidateCode(evm.interpreter.cfg.JumpTableEOF) } if err != nil { - err = fmt.Errorf("%v: %v", ErrInvalidEOF, err) + err = fmt.Errorf("%w: %v", ErrInvalidEOF, err) } } else if evm.chainRules.IsLondon { err = ErrInvalidCode @@ -548,7 +549,7 @@ func (evm *EVM) ChainConfig() *params.ChainConfig { return evm.chainConfig } func (evm *EVM) parseContainer(b []byte) *Container { if evm.chainRules.IsShanghai { var c Container - if err := c.UnmarshalBinary(b); err != nil && err.Error() == "invalid magic" { + if err := c.UnmarshalBinary(b); err != nil && strings.HasPrefix(err.Error(), "invalid magic") { return nil } else if err != nil { // Code was already validated, so no other errors should be possible. diff --git a/core/vm/validate.go b/core/vm/validate.go index 1d102eb30b0d..565692f33f76 100644 --- a/core/vm/validate.go +++ b/core/vm/validate.go @@ -176,6 +176,11 @@ func validateControlFlow(code []byte, section int, metadata []*FunctionMetadata, return 0, 0, fmt.Errorf("stack overflow") } pos += 3 + case op == RETF: + if int(metadata[section].Output) != height { + return 0, 0, fmt.Errorf("wrong number of outputs (want: %d, got: %d)", metadata[section].Output, height) + } + break outer case op == RJUMP: arg := parseInt16(code[pos+1:]) pos += 3 + int(arg) @@ -190,11 +195,6 @@ func validateControlFlow(code []byte, section int, metadata []*FunctionMetadata, worklist = append(worklist, item{pos: pos + 2 + 2*count + int(arg), height: height}) } pos += 2 + 2*count - case op == RETF: - if int(metadata[section].Output) != height { - return 0, 0, fmt.Errorf("wrong number of outputs (want: %d, got: %d)", metadata[section].Output, height) - } - break outer default: if op >= PUSH1 && op <= PUSH32 { pos += 1 + int(op-PUSH0) diff --git a/core/vm/validate_test.go b/core/vm/validate_test.go index 02f5f73c6e3c..2b271ce2a8af 100644 --- a/core/vm/validate_test.go +++ b/core/vm/validate_test.go @@ -54,7 +54,7 @@ func TestValidateCode(t *testing.T) { byte(STOP), }, section: 0, - metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 0}}, + metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 1}}, }, { code: []byte{ From c115b1a289472d4a301db70719ca3c81e2372e1b Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Mon, 2 Jan 2023 20:59:56 -0700 Subject: [PATCH 32/53] core/vm: account for callf return value in stack analysis and some other clean up --- core/vm/analysis.go | 14 +++++++++++--- core/vm/eips.go | 8 +------- core/vm/eof.go | 25 ++++++++++++++---------- core/vm/validate.go | 42 +++++++++++++++------------------------- core/vm/validate_test.go | 9 +++++++++ 5 files changed, 52 insertions(+), 46 deletions(-) diff --git a/core/vm/analysis.go b/core/vm/analysis.go index 8321da094c1a..8bc63bef824c 100644 --- a/core/vm/analysis.go +++ b/core/vm/analysis.go @@ -143,13 +143,21 @@ func eofCodeBitmapInternal(code, bits bitvec) bitvec { if int8(op) >= int8(PUSH1) && int8(op) <= int8(PUSH32) { numbits = uint8(op - PUSH1 + 1) } else if int8(op) == int8(RJUMPV) { - // RJUMPV has variable sized operand + // RJUMPV is unique as it has a variable sized operand. + // The total size is determined by the count byte which + // immediate proceeds RJUMPV. Truncation will be caught + // in other validation steps -- for now, just return a + // valid bitmap for as much of the code as is + // available. if pc >= uint64(len(code)) { - continue // skip over if rjumpv is truncated + // Count missing, no more bits to mark. + return bits } numbits = code[pc]*2 + 1 if pc+uint64(numbits) > uint64(len(code)) { - continue // skip over if rjumpv is truncated + // Jump table is truncated, mark as many bits + // as possible. + numbits = uint8(len(code) - int(pc)) } } else { // If not PUSH (the int8(op) > int(PUSH32) is always false). diff --git a/core/vm/eips.go b/core/vm/eips.go index 4702bcd31754..3200cab89ccf 100644 --- a/core/vm/eips.go +++ b/core/vm/eips.go @@ -21,6 +21,7 @@ import ( "sort" "encoding/binary" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/params" "github.com/holiman/uint256" @@ -387,10 +388,3 @@ func opJumpf(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]by *pc -= 1 // hacks xD return nil, nil } - -// parseInt16 returns the int16 located at b[0:2]. -func parseInt16(b []byte) int16 { - n := uint16(b[0]) << 8 - n += uint16(b[1]) - return int16(n) -} diff --git a/core/vm/eof.go b/core/vm/eof.go index 3c46ae31df0a..e1f64e8550a7 100644 --- a/core/vm/eof.go +++ b/core/vm/eof.go @@ -163,7 +163,7 @@ func (c *Container) UnmarshalBinary(b []byte) error { // Check for terminator. offsetTerminator := offsetDataKind + 3 - if check(b, offsetTerminator, 0) { + if len(b) < offsetTerminator || b[offsetTerminator] != 0 { return fmt.Errorf("expected terminator") } @@ -276,20 +276,25 @@ func parseUint16(b []byte) (int, error) { return int(binary.BigEndian.Uint16(b)), nil } -// check returns if b[idx] == want after performing a bounds check. -func check(b []byte, idx int, want byte) bool { - if len(b) < idx { - return false +// parseInt16 parses a 16 bit signed integer. +func parseInt16(b []byte) int { + return int(int16(b[1]) | int16(b[0])<<8) +} + +// max returns the maximum of a and b. +func max(a, b int) int { + if a < b { + return b } - return b[idx] != want + return a } // min returns the minimum value between x and y. -func min(x, y int) int { - if x < y { - return x +func min(a, b int) int { + if a < b { + return a } - return y + return b } // sum computes the sum of a slice. diff --git a/core/vm/validate.go b/core/vm/validate.go index 565692f33f76..08ce8868b286 100644 --- a/core/vm/validate.go +++ b/core/vm/validate.go @@ -22,12 +22,13 @@ import ( "github.com/ethereum/go-ethereum/params" ) +// validateCode validates the code parameter against the EOF v1 validity requirements. func validateCode(code []byte, section int, metadata []*FunctionMetadata, jt *JumpTable) error { var ( i = 0 count = 0 op OpCode - analysis *bitvec + analysis bitvec ) for i < len(code) { count += 1 @@ -37,7 +38,6 @@ func validateCode(code []byte, section int, metadata []*FunctionMetadata, jt *Ju } switch { case op >= PUSH1 && op <= PUSH32: - // Verify that push data is not truncated. size := int(op - PUSH0) if i+size >= len(code) { return fmt.Errorf("truncated operand") @@ -48,18 +48,13 @@ func validateCode(code []byte, section int, metadata []*FunctionMetadata, jt *Ju return fmt.Errorf("truncated rjump* operand") } if analysis == nil { - tmp := eofCodeBitmap(code) - analysis = &tmp + analysis = eofCodeBitmap(code) } - // Verify that the relative jump offset points to a - // destination in-bounds. - if err := checkDest(code[i+1:], *analysis, i+3, len(code)); err != nil { + if err := checkDest(code[i+1:], i+3, len(code), analysis); err != nil { return err } i += 2 case op == RJUMPV: - // Verify each branch in the jump table points to a - // destination in-bounds. if i+1 >= len(code) { return fmt.Errorf("truncated jump table operand") } @@ -71,11 +66,10 @@ func validateCode(code []byte, section int, metadata []*FunctionMetadata, jt *Ju return fmt.Errorf("truncated jump table operand") } if analysis == nil { - tmp := eofCodeBitmap(code) - analysis = &tmp + analysis = eofCodeBitmap(code) } for j := 0; j < count; j++ { - if err := checkDest(code[i+2+j*2:], *analysis, i+2*count+2, len(code)); err != nil { + if err := checkDest(code[i+2+j*2:], i+2*count+2, len(code), analysis); err != nil { return err } } @@ -110,12 +104,12 @@ func validateCode(code []byte, section int, metadata []*FunctionMetadata, jt *Ju return nil } -func checkDest(code []byte, analysis bitvec, idx, length int) error { +// checkDest parses a relative offset at code[0:2] and checks if it is a valid jump destination. +func checkDest(code []byte, from, length int, analysis bitvec) error { if len(code) < 2 { return fmt.Errorf("truncated operand") } - offset := parseInt16(code) - dest := idx + int(offset) + dest := from + parseInt16(code) if dest < 0 || dest >= length { return fmt.Errorf("relative offset out-of-bounds: %d", dest) } @@ -125,6 +119,8 @@ func checkDest(code []byte, analysis bitvec, idx, length int) error { return nil } +// validateControlFlow iterates through all possible branches the provided code +// value and determines if it is valid per EOF v1. func validateControlFlow(code []byte, section int, metadata []*FunctionMetadata, jt *JumpTable) (int, int, error) { type item struct { pos int @@ -175,6 +171,7 @@ func validateControlFlow(code []byte, section int, metadata []*FunctionMetadata, if int(metadata[arg].Output)+height > int(params.StackLimit) { return 0, 0, fmt.Errorf("stack overflow") } + height += int(metadata[arg].Output) pos += 3 case op == RETF: if int(metadata[section].Output) != height { @@ -183,16 +180,16 @@ func validateControlFlow(code []byte, section int, metadata []*FunctionMetadata, break outer case op == RJUMP: arg := parseInt16(code[pos+1:]) - pos += 3 + int(arg) + pos += 3 + arg case op == RJUMPI: arg := parseInt16(code[pos+1:]) - worklist = append(worklist, item{pos: pos + 3 + int(arg), height: height}) + worklist = append(worklist, item{pos: pos + 3 + arg, height: height}) pos += 3 case op == RJUMPV: count := int(code[pos+1]) for i := 0; i < count; i++ { arg := parseInt16(code[pos+2+2*i:]) - worklist = append(worklist, item{pos: pos + 2 + 2*count + int(arg), height: height}) + worklist = append(worklist, item{pos: pos + 2 + 2*count + arg, height: height}) } pos += 2 + 2*count default: @@ -201,7 +198,7 @@ func validateControlFlow(code []byte, section int, metadata []*FunctionMetadata, } else if jt[op].terminal { break outer } else { - // No immediate. + // Simple op, no operand. pos += 1 } } @@ -210,10 +207,3 @@ func validateControlFlow(code []byte, section int, metadata []*FunctionMetadata, } return maxStackHeight, len(heights), nil } - -func max(a, b int) int { - if a < b { - return b - } - return a -} diff --git a/core/vm/validate_test.go b/core/vm/validate_test.go index 2b271ce2a8af..009fd48eaf01 100644 --- a/core/vm/validate_test.go +++ b/core/vm/validate_test.go @@ -231,6 +231,15 @@ func TestValidateCode(t *testing.T) { section: 0, metadata: []*FunctionMetadata{{Input: 3, Output: 3, MaxStackHeight: 3}}, }, + { + code: []byte{ + byte(CALLF), 0x00, 0x01, + byte(POP), + byte(STOP), + }, + section: 0, + metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 1}, {Input: 0, Output: 1, MaxStackHeight: 0}}, + }, } { err := validateCode(test.code, test.section, test.metadata, &shanghaiEOFInstructionSet) if (err != nil && test.err == nil) || (err == nil && test.err != nil) || (err != nil && test.err != nil && err.Error() != test.err.Error()) { From 713eb5f2c0e08447da738892b1e57dd6e6dfce2e Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Tue, 3 Jan 2023 08:39:50 -0700 Subject: [PATCH 33/53] core/vm: fix lints --- core/blockchain_test.go | 6 +++--- core/vm/eof.go | 3 +-- core/vm/validate.go | 2 +- core/vm/validate_test.go | 1 - 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/core/blockchain_test.go b/core/blockchain_test.go index d22588895de2..dd9fef228a7f 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -4567,13 +4567,13 @@ func TestEOF(t *testing.T) { } // Check various deployment mechanisms. - if bytes.Compare(state.GetCode(crypto.CreateAddress(addr, 1)), deployCode) != 0 { + if !bytes.Equal(state.GetCode(crypto.CreateAddress(addr, 1)), deployCode) { t.Fatalf("failed to deploy EOF with EOA") } - if bytes.Compare(state.GetCode(crypto.CreateAddress(bb, 0)), deployCode) != 0 { + if !bytes.Equal(state.GetCode(crypto.CreateAddress(bb, 0)), deployCode) { t.Fatalf("failed to deploy EOF with CREATE") } - if bytes.Compare(state.GetCode(crypto.CreateAddress2(cc, [32]byte{}, initHash.Bytes())), deployCode) != 0 { + if !bytes.Equal(state.GetCode(crypto.CreateAddress2(cc, [32]byte{}, initHash.Bytes())), deployCode) { t.Fatalf("failed to deploy EOF with CREATE2") } } diff --git a/core/vm/eof.go b/core/vm/eof.go index e1f64e8550a7..8e860995dba4 100644 --- a/core/vm/eof.go +++ b/core/vm/eof.go @@ -180,7 +180,7 @@ func (c *Container) UnmarshalBinary(b []byte) error { sig := &FunctionMetadata{ Input: b[idx+i*4], Output: b[idx+i*4+1], - MaxStackHeight: uint16(binary.BigEndian.Uint16(b[idx+i*4+2:])), + MaxStackHeight: binary.BigEndian.Uint16(b[idx+i*4+2:]), } if sig.Input > maxInputItems { return fmt.Errorf("invalid type annotation at index %d: inputs must not exceed %d: have %d", i, maxInputItems, sig.Input) @@ -259,7 +259,6 @@ func parseList(b []byte, idx int) ([]int, error) { count := binary.BigEndian.Uint16(b[idx:]) if len(b) <= idx+2+int(count)*2 { return nil, io.ErrUnexpectedEOF - } list := make([]int, count) for i := 0; i < int(count); i++ { diff --git a/core/vm/validate.go b/core/vm/validate.go index 08ce8868b286..1f00dd97b4ee 100644 --- a/core/vm/validate.go +++ b/core/vm/validate.go @@ -171,7 +171,7 @@ func validateControlFlow(code []byte, section int, metadata []*FunctionMetadata, if int(metadata[arg].Output)+height > int(params.StackLimit) { return 0, 0, fmt.Errorf("stack overflow") } - height += int(metadata[arg].Output) + height += metadata[arg].Output pos += 3 case op == RETF: if int(metadata[section].Output) != height { diff --git a/core/vm/validate_test.go b/core/vm/validate_test.go index 009fd48eaf01..9d7c7872024c 100644 --- a/core/vm/validate_test.go +++ b/core/vm/validate_test.go @@ -246,5 +246,4 @@ func TestValidateCode(t *testing.T) { t.Errorf("test %d (%s): unexpected error (want: %v, got: %v)", i, common.Bytes2Hex(test.code), test.err, err) } } - } From cd29313743f690172446b64a42e885346fd49c9e Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Tue, 3 Jan 2023 11:09:38 -0700 Subject: [PATCH 34/53] core/vm: fix incorrect type --- core/vm/validate.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/vm/validate.go b/core/vm/validate.go index 1f00dd97b4ee..08ce8868b286 100644 --- a/core/vm/validate.go +++ b/core/vm/validate.go @@ -171,7 +171,7 @@ func validateControlFlow(code []byte, section int, metadata []*FunctionMetadata, if int(metadata[arg].Output)+height > int(params.StackLimit) { return 0, 0, fmt.Errorf("stack overflow") } - height += metadata[arg].Output + height += int(metadata[arg].Output) pos += 3 case op == RETF: if int(metadata[section].Output) != height { From dff09a9ac2fa230fe09d32fbfab34d0ce2cdac08 Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Tue, 3 Jan 2023 11:48:38 -0700 Subject: [PATCH 35/53] core/vm: clean up eof validation and remove jumpf --- core/blockchain_test.go | 3 ++- core/vm/eips.go | 19 ------------------- core/vm/opcodes.go | 2 -- core/vm/validate.go | 41 ++++++++++++++++++---------------------- core/vm/validate_test.go | 10 ---------- 5 files changed, 20 insertions(+), 55 deletions(-) diff --git a/core/blockchain_test.go b/core/blockchain_test.go index dd9fef228a7f..de2e942e1cc7 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -4430,9 +4430,10 @@ func TestEOF(t *testing.T) { byte(2), byte(vm.SSTORE), // set third flag - byte(vm.JUMPF), + byte(vm.CALLF), byte(0), byte(3), + byte(vm.RETF), }, { byte(vm.PUSH1), diff --git a/core/vm/eips.go b/core/vm/eips.go index 3200cab89ccf..a3a5eab84dcd 100644 --- a/core/vm/eips.go +++ b/core/vm/eips.go @@ -293,13 +293,6 @@ func enableEOF(jt *JumpTable) { maxStack: maxStack(0, 0), terminal: true, } - jt[JUMPF] = &operation{ - execute: opJumpf, - constantGas: GasFastestStep, - minStack: minStack(0, 0), - maxStack: maxStack(0, 0), - terminal: true, - } } // opRjump implements the rjump opcode. @@ -376,15 +369,3 @@ func opRetf(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byt *pc = retCtx.Pc - 1 return nil, nil } - -// opJumpf implements the JUMPF opcode -func opJumpf(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { - var ( - code = scope.Contract.CodeAt(scope.CodeSection) - idx = binary.BigEndian.Uint16(code[*pc+1:]) - ) - scope.CodeSection = uint64(idx) - *pc = 0 - *pc -= 1 // hacks xD - return nil, nil -} diff --git a/core/vm/opcodes.go b/core/vm/opcodes.go index c6ca0b55f181..695063e30411 100644 --- a/core/vm/opcodes.go +++ b/core/vm/opcodes.go @@ -211,7 +211,6 @@ const ( const ( CALLF = 0xb0 + iota RETF - JUMPF ) // 0xf0 range - closures. @@ -395,7 +394,6 @@ var opCodeToString = map[OpCode]string{ // 0xb0 range. CALLF: "CALLF", RETF: "RETF", - JUMPF: "JUMPF", TLOAD: "TLOAD", TSTORE: "TSTORE", diff --git a/core/vm/validate.go b/core/vm/validate.go index 08ce8868b286..26f60fcd0755 100644 --- a/core/vm/validate.go +++ b/core/vm/validate.go @@ -25,13 +25,16 @@ import ( // validateCode validates the code parameter against the EOF v1 validity requirements. func validateCode(code []byte, section int, metadata []*FunctionMetadata, jt *JumpTable) error { var ( - i = 0 + i = 0 + // Tracks the number of actual instructions in the code (e.g. + // non-immediate values). This is used at the end to determine + // if each instruction is reachable. count = 0 op OpCode analysis bitvec ) for i < len(code) { - count += 1 + count++ op = OpCode(code[i]) if jt[op].undefined { return fmt.Errorf("use of undefined opcode %s", op) @@ -39,42 +42,36 @@ func validateCode(code []byte, section int, metadata []*FunctionMetadata, jt *Ju switch { case op >= PUSH1 && op <= PUSH32: size := int(op - PUSH0) - if i+size >= len(code) { + if len(code) <= i+size { return fmt.Errorf("truncated operand") } i += size case op == RJUMP || op == RJUMPI: - if i+2 >= len(code) { + if len(code) <= i+2 { return fmt.Errorf("truncated rjump* operand") } - if analysis == nil { - analysis = eofCodeBitmap(code) - } - if err := checkDest(code[i+1:], i+3, len(code), analysis); err != nil { + if err := checkDest(code, &analysis, i+1, i+3, len(code)); err != nil { return err } i += 2 case op == RJUMPV: - if i+1 >= len(code) { + if len(code) <= i+1 { return fmt.Errorf("truncated jump table operand") } count := int(code[i+1]) if count == 0 { return fmt.Errorf("rjumpv branch count must not be 0") } - if i+count >= len(code) { + if len(code) <= i+count { return fmt.Errorf("truncated jump table operand") } - if analysis == nil { - analysis = eofCodeBitmap(code) - } for j := 0; j < count; j++ { - if err := checkDest(code[i+2+j*2:], i+2*count+2, len(code), analysis); err != nil { + if err := checkDest(code, &analysis, i+2+j*2, i+2*count+2, len(code)); err != nil { return err } } i += 1 + 2*count - case op == CALLF || op == JUMPF: + case op == CALLF: if i+2 >= len(code) { return fmt.Errorf("truncated operand") } @@ -82,11 +79,6 @@ func validateCode(code []byte, section int, metadata []*FunctionMetadata, jt *Ju if arg >= len(metadata) { return fmt.Errorf("code section out-of-bounds (want: %d, have: %d)", arg, len(metadata)) } - if op == JUMPF { - if metadata[section].Output < metadata[arg].Output { - return fmt.Errorf("jumpf to section with more outputs") - } - } i += 2 } i += 1 @@ -105,11 +97,14 @@ func validateCode(code []byte, section int, metadata []*FunctionMetadata, jt *Ju } // checkDest parses a relative offset at code[0:2] and checks if it is a valid jump destination. -func checkDest(code []byte, from, length int, analysis bitvec) error { - if len(code) < 2 { +func checkDest(code []byte, analysis *bitvec, imm, from, length int) error { + if len(code) < imm+2 { return fmt.Errorf("truncated operand") } - dest := from + parseInt16(code) + if analysis != nil && *analysis == nil { + *analysis = eofCodeBitmap(code) + } + dest := from + parseInt16(code[imm:]) if dest < 0 || dest >= length { return fmt.Errorf("relative offset out-of-bounds: %d", dest) } diff --git a/core/vm/validate_test.go b/core/vm/validate_test.go index 9d7c7872024c..b075cba06301 100644 --- a/core/vm/validate_test.go +++ b/core/vm/validate_test.go @@ -143,16 +143,6 @@ func TestValidateCode(t *testing.T) { metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 1}}, err: fmt.Errorf("rjumpv branch count must not be 0"), }, - { - code: []byte{ - byte(JUMPF), - byte(0x00), - byte(0x01), - }, - section: 0, - metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 1}, {Input: 0, Output: 1, MaxStackHeight: 1}}, - err: fmt.Errorf("jumpf to section with more outputs"), - }, { code: []byte{ byte(RJUMP), 0x00, 0x03, From 167e3b989d17bbb1b12c2932bb75484cc7ef3eb2 Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Tue, 3 Jan 2023 12:08:41 -0700 Subject: [PATCH 36/53] core/vm: account for callf inputs and add some comments --- core/vm/validate.go | 32 +++++++++++++++++++++----------- core/vm/validate_test.go | 13 ++++++++++++- 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/core/vm/validate.go b/core/vm/validate.go index 26f60fcd0755..a6ffa4d7dc84 100644 --- a/core/vm/validate.go +++ b/core/vm/validate.go @@ -33,6 +33,12 @@ func validateCode(code []byte, section int, metadata []*FunctionMetadata, jt *Ju op OpCode analysis bitvec ) + // This loop visits every single instruction and verifies: + // * if the instruction is valid for the given jump table. + // * if the instruction has an immediate value, it is not truncated. + // * if performing a relative jump, all jump destinations are valid. + // * if changing code sections, the new code section index is valid and + // will not cause a stack overflow. for i < len(code) { count++ op = OpCode(code[i]) @@ -83,15 +89,15 @@ func validateCode(code []byte, section int, metadata []*FunctionMetadata, jt *Ju } i += 1 } + // Code sections may not "fall through" and require proper termination. + // Therefore, the last instruction must be considered terminal. if !jt[op].terminal { return fmt.Errorf("code section ends with non-terminal instruction") } - if max, paths, err := validateControlFlow(code, section, metadata, jt); err != nil { + if paths, err := validateControlFlow(code, section, metadata, jt); err != nil { return err } else if paths != count { return fmt.Errorf("unreachable code") - } else if max != int(metadata[section].MaxStackHeight) { - return fmt.Errorf("computed max stack height for code section %d does not match expected (want: %d, got: %d)", section, metadata[section].MaxStackHeight, max) } return nil } @@ -116,7 +122,7 @@ func checkDest(code []byte, analysis *bitvec, imm, from, length int) error { // validateControlFlow iterates through all possible branches the provided code // value and determines if it is valid per EOF v1. -func validateControlFlow(code []byte, section int, metadata []*FunctionMetadata, jt *JumpTable) (int, int, error) { +func validateControlFlow(code []byte, section int, metadata []*FunctionMetadata, jt *JumpTable) (int, error) { type item struct { pos int height int @@ -140,7 +146,7 @@ func validateControlFlow(code []byte, section int, metadata []*FunctionMetadata, // Check if pos has already be visited; if so, the stack heights should be the same. if exp, ok := heights[pos]; ok { if height != exp { - return 0, 0, fmt.Errorf("stack height mismatch for different paths") + return 0, fmt.Errorf("stack height mismatch for different paths") } // Already visited this path and stack height // matches. @@ -150,10 +156,10 @@ func validateControlFlow(code []byte, section int, metadata []*FunctionMetadata, // Validate height for current op and update as needed. if jt[op].minStack > height { - return 0, 0, fmt.Errorf("stack underflow") + return 0, fmt.Errorf("stack underflow") } if jt[op].maxStack < height { - return 0, 0, fmt.Errorf("stack overflow") + return 0, fmt.Errorf("stack overflow") } height += int(params.StackLimit) - jt[op].maxStack @@ -161,16 +167,17 @@ func validateControlFlow(code []byte, section int, metadata []*FunctionMetadata, case op == CALLF: arg, _ := parseUint16(code[pos+1:]) if metadata[arg].Input > uint8(height) { - return 0, 0, fmt.Errorf("stack underflow") + return 0, fmt.Errorf("stack underflow") } if int(metadata[arg].Output)+height > int(params.StackLimit) { - return 0, 0, fmt.Errorf("stack overflow") + return 0, fmt.Errorf("stack overflow") } + height -= int(metadata[arg].Input) height += int(metadata[arg].Output) pos += 3 case op == RETF: if int(metadata[section].Output) != height { - return 0, 0, fmt.Errorf("wrong number of outputs (want: %d, got: %d)", metadata[section].Output, height) + return 0, fmt.Errorf("wrong number of outputs (want: %d, got: %d)", metadata[section].Output, height) } break outer case op == RJUMP: @@ -200,5 +207,8 @@ func validateControlFlow(code []byte, section int, metadata []*FunctionMetadata, maxStackHeight = max(maxStackHeight, height) } } - return maxStackHeight, len(heights), nil + if maxStackHeight != int(metadata[section].MaxStackHeight) { + return 0, fmt.Errorf("computed max stack height for code section %d does not match expected (want: %d, got: %d)", section, metadata[section].MaxStackHeight, maxStackHeight) + } + return len(heights), nil } diff --git a/core/vm/validate_test.go b/core/vm/validate_test.go index b075cba06301..138f97ff118d 100644 --- a/core/vm/validate_test.go +++ b/core/vm/validate_test.go @@ -74,7 +74,7 @@ func TestValidateCode(t *testing.T) { byte(STOP), }, section: 0, - metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 1}}, + metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 0}}, err: fmt.Errorf("unreachable code"), }, { @@ -230,6 +230,17 @@ func TestValidateCode(t *testing.T) { section: 0, metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 1}, {Input: 0, Output: 1, MaxStackHeight: 0}}, }, + { + code: []byte{ + byte(ORIGIN), + byte(ORIGIN), + byte(CALLF), 0x00, 0x01, + byte(POP), + byte(RETF), + }, + section: 0, + metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 2}, {Input: 2, Output: 1, MaxStackHeight: 2}}, + }, } { err := validateCode(test.code, test.section, test.metadata, &shanghaiEOFInstructionSet) if (err != nil && test.err == nil) || (err == nil && test.err != nil) || (err != nil && test.err != nil && err.Error() != test.err.Error()) { From 505db9d347f46c8fd21d7fef5a0328df7d63ea7d Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Wed, 4 Jan 2023 07:47:15 -0700 Subject: [PATCH 37/53] cmd/evm: validate pre-allocated eof code in t8n --- cmd/evm/internal/t8ntool/transition.go | 19 +++++++++++++++++++ cmd/evm/t8n_test.go | 8 ++++++++ cmd/evm/testdata/26/alloc.json | 14 ++++++++++++++ cmd/evm/testdata/26/env.json | 11 +++++++++++ cmd/evm/testdata/26/txs.json | 1 + core/vm/eof.go | 4 ++-- core/vm/evm.go | 4 ++-- 7 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 cmd/evm/testdata/26/alloc.json create mode 100644 cmd/evm/testdata/26/env.json create mode 100644 cmd/evm/testdata/26/txs.json diff --git a/cmd/evm/internal/t8ntool/transition.go b/cmd/evm/internal/t8ntool/transition.go index 8b05f1def9db..eca27c3fb5b8 100644 --- a/cmd/evm/internal/t8ntool/transition.go +++ b/cmd/evm/internal/t8ntool/transition.go @@ -262,6 +262,25 @@ func Transition(ctx *cli.Context) error { return NewError(ErrorConfig, errors.New("EIP-1559 config but missing 'currentBaseFee' in env section")) } } + // Sanity check pre-allocated EOF code to not panic in state transition. + if chainConfig.IsShanghai(big.NewInt(int64(prestate.Env.Number))) { + for addr, acc := range prestate.Pre { + if vm.HasEOFByte(acc.Code) { + var ( + c vm.Container + err error + ) + err = c.UnmarshalBinary(acc.Code) + if err == nil { + jt := vm.NewShanghaiEOFInstructionSetForTesting() + err = c.ValidateCode(&jt) + } + if err != nil { + return NewError(ErrorConfig, fmt.Errorf("code at %s considered invalid: %v", addr, err)) + } + } + } + } isMerged := chainConfig.TerminalTotalDifficulty != nil && chainConfig.TerminalTotalDifficulty.BitLen() == 0 env := prestate.Env if isMerged { diff --git a/cmd/evm/t8n_test.go b/cmd/evm/t8n_test.go index b7a0d9c2c3c7..411be6cf5e6a 100644 --- a/cmd/evm/t8n_test.go +++ b/cmd/evm/t8n_test.go @@ -251,6 +251,14 @@ func TestT8n(t *testing.T) { output: t8nOutput{alloc: true, result: true}, expOut: "exp.json", }, + { // Validate pre-allocated EOF code + base: "./testdata/26", + input: t8nInput{ + "alloc.json", "txs.json", "env.json", "Shanghai", "", + }, + output: t8nOutput{alloc: true, result: false}, + expExitCode: 3, + }, } { args := []string{"t8n"} args = append(args, tc.output.get()...) diff --git a/cmd/evm/testdata/26/alloc.json b/cmd/evm/testdata/26/alloc.json new file mode 100644 index 000000000000..43024b43d78a --- /dev/null +++ b/cmd/evm/testdata/26/alloc.json @@ -0,0 +1,14 @@ +{ + "a94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "balance": "0x0", + "code": "0xef01", + "nonce": "0x1", + "storage": {} + }, + "a94f5374fce5edbc8e2a8697c15331677e6ebf0c": { + "balance": "0x0", + "code": "0xef0001010008020002000700020300000000000002020100025959b0000250b101b1", + "nonce": "0x1", + "storage": {} + } +} diff --git a/cmd/evm/testdata/26/env.json b/cmd/evm/testdata/26/env.json new file mode 100644 index 000000000000..bb2c9e0d7d68 --- /dev/null +++ b/cmd/evm/testdata/26/env.json @@ -0,0 +1,11 @@ +{ + "currentCoinbase": "0xc94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "currentDifficulty": null, + "currentRandom": "0xdeadc0de", + "currentGasLimit": "0x750a163df65e8a", + "parentBaseFee": "0x500", + "parentGasUsed": "0x0", + "parentGasLimit": "0x750a163df65e8a", + "currentNumber": "1", + "currentTimestamp": "1000" +} diff --git a/cmd/evm/testdata/26/txs.json b/cmd/evm/testdata/26/txs.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/cmd/evm/testdata/26/txs.json @@ -0,0 +1 @@ +[] diff --git a/core/vm/eof.go b/core/vm/eof.go index 8e860995dba4..78a47698dd38 100644 --- a/core/vm/eof.go +++ b/core/vm/eof.go @@ -44,8 +44,8 @@ const ( var eofMagic = []byte{0xef, 0x00} -// hasEOFByte returns true if code starts with 0xEF byte -func hasEOFByte(code []byte) bool { +// HasEOFByte returns true if code starts with 0xEF byte +func HasEOFByte(code []byte) bool { return len(code) != 0 && code[0] == eofFormatByte } diff --git a/core/vm/evm.go b/core/vm/evm.go index b21dce904f21..3a12f0885a7e 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -438,7 +438,7 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, contract.SetCodeOptionalHash(&address, codeAndHash) // If the initcode is EOF, verify it is well-formed. - isInitcodeEOF := hasEOFByte(codeAndHash.code) + isInitcodeEOF := HasEOFByte(codeAndHash.code) if evm.chainRules.IsShanghai && isInitcodeEOF { var c Container if err := c.UnmarshalBinary(codeAndHash.code); err != nil { @@ -474,7 +474,7 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, } // Reject legacy contract deployment from EOF. - if err == nil && isInitcodeEOF && !hasEOFByte(ret) { + if err == nil && isInitcodeEOF && !HasEOFByte(ret) { err = ErrLegacyCode } From f7efe96f45c014f91accc6e8c394c18b3ced9e0a Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Wed, 4 Jan 2023 07:59:22 -0700 Subject: [PATCH 38/53] core/vm: ban eof code from running legacy initcode --- core/vm/evm.go | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/core/vm/evm.go b/core/vm/evm.go index 3a12f0885a7e..6695c9dddfa6 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -407,7 +407,7 @@ func (c *codeAndHash) Hash() common.Hash { } // 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) { +func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, value *big.Int, address common.Address, typ OpCode, fromEOF bool) ([]byte, common.Address, uint64, error) { // Depth check execution. Fail if we're trying to execute above the // limit. if evm.depth > int(params.CallCreateDepth) { @@ -437,17 +437,25 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, contract := NewContract(caller, AccountRef(address), value, gas) contract.SetCodeOptionalHash(&address, codeAndHash) - // If the initcode is EOF, verify it is well-formed. - isInitcodeEOF := HasEOFByte(codeAndHash.code) - if evm.chainRules.IsShanghai && isInitcodeEOF { - var c Container - if err := c.UnmarshalBinary(codeAndHash.code); err != nil { - return nil, common.Address{}, 0, fmt.Errorf("%v: %v", ErrInvalidEOF, err) - } - if err := c.ValidateCode(evm.interpreter.cfg.JumpTableEOF); err != nil { - return nil, common.Address{}, 0, fmt.Errorf("%v: %v", ErrInvalidEOF, err) + var ( + isCallerEOF = fromEOF + isInitcodeEOF = HasEOFByte(codeAndHash.code) + ) + if evm.chainRules.IsShanghai { + if isCallerEOF && !isInitcodeEOF { + // Don't allow EOF contract to run legacy initcode. + return nil, common.Address{}, 0, ErrLegacyCode + } else if isInitcodeEOF { + // If the initcode is EOF, verify it is well-formed. + var c Container + if err := c.UnmarshalBinary(codeAndHash.code); err != nil { + return nil, common.Address{}, 0, fmt.Errorf("%v: %v", ErrInvalidEOF, err) + } + if err := c.ValidateCode(evm.interpreter.cfg.JumpTableEOF); err != nil { + return nil, common.Address{}, 0, fmt.Errorf("%v: %v", ErrInvalidEOF, err) + } + contract.Container = &c } - contract.Container = &c } // Create a new account on the state @@ -529,7 +537,8 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, // Create creates a new contract using code as deployment code. func (evm *EVM) Create(caller ContractRef, code []byte, gas uint64, value *big.Int) (ret []byte, contractAddr common.Address, leftOverGas uint64, err error) { contractAddr = crypto.CreateAddress(caller.Address(), evm.StateDB.GetNonce(caller.Address())) - return evm.create(caller, &codeAndHash{code: code}, gas, value, contractAddr, CREATE) + isEOF := HasEOFByte(evm.StateDB.GetCode(caller.Address())) + return evm.create(caller, &codeAndHash{code: code}, gas, value, contractAddr, CREATE, isEOF) } // Create2 creates a new contract using code as deployment code. @@ -539,7 +548,8 @@ func (evm *EVM) Create(caller ContractRef, code []byte, gas uint64, value *big.I func (evm *EVM) Create2(caller ContractRef, code []byte, gas uint64, endowment *big.Int, salt *uint256.Int) (ret []byte, contractAddr common.Address, leftOverGas uint64, err error) { codeAndHash := &codeAndHash{code: code} contractAddr = crypto.CreateAddress2(caller.Address(), salt.Bytes32(), codeAndHash.Hash().Bytes()) - return evm.create(caller, codeAndHash, gas, endowment, contractAddr, CREATE2) + isEOF := HasEOFByte(evm.StateDB.GetCode(caller.Address())) + return evm.create(caller, codeAndHash, gas, endowment, contractAddr, CREATE2, isEOF) } // ChainConfig returns the environment's chain configuration From bd082f5381c462ba1a68536d8ac4b68bec538174 Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Wed, 4 Jan 2023 08:58:46 -0700 Subject: [PATCH 39/53] core/vm: uncomment analysis tests --- core/vm/analysis_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/vm/analysis_test.go b/core/vm/analysis_test.go index 6bc99ca28fad..a50b360e5e4b 100644 --- a/core/vm/analysis_test.go +++ b/core/vm/analysis_test.go @@ -70,8 +70,8 @@ func TestEOFAnalysis(t *testing.T) { exp byte which int }{ - // {[]byte{byte(RJUMP), 0x01, 0x01, 0x01}, 0b0000_0110, 0}, - // {[]byte{byte(RJUMPI), byte(RJUMP), byte(RJUMP), byte(RJUMPI)}, 0b0011_0110, 0}, + {[]byte{byte(RJUMP), 0x01, 0x01, 0x01}, 0b0000_0110, 0}, + {[]byte{byte(RJUMPI), byte(RJUMP), byte(RJUMP), byte(RJUMPI)}, 0b0011_0110, 0}, {[]byte{byte(RJUMPV), 0x02, byte(RJUMP), 0x00, byte(RJUMPI), 0x00}, 0b0011_1110, 0}, } for i, test := range tests { From 630be09d901be7fce1bedd25a1695295f9adf11f Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Wed, 4 Jan 2023 10:30:19 -0700 Subject: [PATCH 40/53] core/vm: don't consume all gas if eof initcode validation fails --- core/vm/evm.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/vm/evm.go b/core/vm/evm.go index 6695c9dddfa6..3304d3e6ff8c 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -444,15 +444,15 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, if evm.chainRules.IsShanghai { if isCallerEOF && !isInitcodeEOF { // Don't allow EOF contract to run legacy initcode. - return nil, common.Address{}, 0, ErrLegacyCode + return nil, common.Address{}, gas, ErrLegacyCode } else if isInitcodeEOF { // If the initcode is EOF, verify it is well-formed. var c Container if err := c.UnmarshalBinary(codeAndHash.code); err != nil { - return nil, common.Address{}, 0, fmt.Errorf("%v: %v", ErrInvalidEOF, err) + return nil, common.Address{}, gas, fmt.Errorf("%v: %v", ErrInvalidEOF, err) } if err := c.ValidateCode(evm.interpreter.cfg.JumpTableEOF); err != nil { - return nil, common.Address{}, 0, fmt.Errorf("%v: %v", ErrInvalidEOF, err) + return nil, common.Address{}, gas, fmt.Errorf("%v: %v", ErrInvalidEOF, err) } contract.Container = &c } From 2382f836a64176a12a8afdcccd01875fa1392919 Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Wed, 4 Jan 2023 11:15:45 -0700 Subject: [PATCH 41/53] core/vm: check for full magic in initcode --- core/vm/evm.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/core/vm/evm.go b/core/vm/evm.go index 3304d3e6ff8c..e27855402ea3 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -439,7 +439,7 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, var ( isCallerEOF = fromEOF - isInitcodeEOF = HasEOFByte(codeAndHash.code) + isInitcodeEOF = hasEOFMagic(codeAndHash.code) ) if evm.chainRules.IsShanghai { if isCallerEOF && !isInitcodeEOF { @@ -482,12 +482,12 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, } // Reject legacy contract deployment from EOF. - if err == nil && isInitcodeEOF && !HasEOFByte(ret) { + if err == nil && isInitcodeEOF && !hasEOFMagic(ret) { err = ErrLegacyCode } // Reject code starting with 0xEF if EIP-3541 is enabled. - if err == nil && len(ret) >= 1 && ret[0] == 0xEF { + if err == nil && len(ret) >= 1 && HasEOFByte(ret) { if evm.chainRules.IsShanghai { var c Container if err = c.UnmarshalBinary(ret); err == nil { @@ -537,7 +537,7 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, // Create creates a new contract using code as deployment code. func (evm *EVM) Create(caller ContractRef, code []byte, gas uint64, value *big.Int) (ret []byte, contractAddr common.Address, leftOverGas uint64, err error) { contractAddr = crypto.CreateAddress(caller.Address(), evm.StateDB.GetNonce(caller.Address())) - isEOF := HasEOFByte(evm.StateDB.GetCode(caller.Address())) + isEOF := hasEOFMagic(evm.StateDB.GetCode(caller.Address())) return evm.create(caller, &codeAndHash{code: code}, gas, value, contractAddr, CREATE, isEOF) } @@ -548,7 +548,7 @@ func (evm *EVM) Create(caller ContractRef, code []byte, gas uint64, value *big.I func (evm *EVM) Create2(caller ContractRef, code []byte, gas uint64, endowment *big.Int, salt *uint256.Int) (ret []byte, contractAddr common.Address, leftOverGas uint64, err error) { codeAndHash := &codeAndHash{code: code} contractAddr = crypto.CreateAddress2(caller.Address(), salt.Bytes32(), codeAndHash.Hash().Bytes()) - isEOF := HasEOFByte(evm.StateDB.GetCode(caller.Address())) + isEOF := hasEOFMagic(evm.StateDB.GetCode(caller.Address())) return evm.create(caller, codeAndHash, gas, endowment, contractAddr, CREATE2, isEOF) } From f4c5cd7aad91c706527634f9965a37e9de194bee Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Fri, 6 Jan 2023 11:51:44 -0700 Subject: [PATCH 42/53] core/vm: clean stop after retf --- core/vm/eips.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/vm/eips.go b/core/vm/eips.go index a3a5eab84dcd..a0e22c163c81 100644 --- a/core/vm/eips.go +++ b/core/vm/eips.go @@ -367,5 +367,10 @@ func opRetf(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byt scope.ReturnStack = scope.ReturnStack[:last] scope.CodeSection = retCtx.Section *pc = retCtx.Pc - 1 + + // If returning from top frame, exit cleanly. + if len(scope.ReturnStack) == 0 { + return nil, errStopToken + } return nil, nil } From 41cd50b1172eb9a44a8e1145192e439865f355cc Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Fri, 6 Jan 2023 12:57:16 -0700 Subject: [PATCH 43/53] core/vm: add custom eof errors --- core/vm/eof.go | 84 ++++++++++++++++++++++++++++++---------- core/vm/errors.go | 12 +++++- core/vm/validate.go | 68 +++++++++++++++++++------------- core/vm/validate_test.go | 22 +++++------ 4 files changed, 126 insertions(+), 60 deletions(-) diff --git a/core/vm/eof.go b/core/vm/eof.go index 78a47698dd38..0dbc42ff1ed9 100644 --- a/core/vm/eof.go +++ b/core/vm/eof.go @@ -19,6 +19,7 @@ package vm import ( "bytes" "encoding/binary" + "errors" "fmt" "io" @@ -42,6 +43,44 @@ const ( maxStackHeight = 1023 ) +type ParseError struct { + inner error + index int + msg string +} + +func NewParseError(err error, index int, format string, a ...any) ParseError { + return ParseError{inner: err, index: index, msg: fmt.Sprintf(format, a...)} +} + +func (p ParseError) Error() string { + if p.msg == "" { + return fmt.Sprintf("%s at position %d", p.inner, p.index) + } + return fmt.Sprintf("%s at position %d, %s", p.inner, p.index, p.msg) +} + +func (p ParseError) Unwrap() error { + return p.inner +} + +var ( + ErrInvalidMagic = errors.New("invalid magic") + ErrInvalidVersion = errors.New("invalid version") + ErrMissingTypeHeader = errors.New("missing type header") + ErrInvalidTypeSize = errors.New("invalid type section size") + ErrMissingCodeHeader = errors.New("missing code header") + ErrInvalidCodeHeader = errors.New("invalid code header") + ErrInvalidCodeSize = errors.New("invalid code size") + ErrMissingDataHeader = errors.New("missing data header") + ErrMissingTerminator = errors.New("missing header terminator") + ErrTooManyInputs = errors.New("invalid type content, too many inputs") + ErrTooManyOutputs = errors.New("invalid type content, too many inputs") + ErrInvalidSection0Type = errors.New("invalid section 0 type, input and output should be zero") + ErrTooLargeMaxStackHeight = errors.New("invalid type content, max stack height exceeds limit") + ErrInvalidContainerSize = errors.New("inavlid container size") +) + var eofMagic = []byte{0xef, 0x00} // HasEOFByte returns true if code starts with 0xEF byte @@ -107,15 +146,15 @@ func (c *Container) MarshalBinary() []byte { // UnmarshalBinary decodes an EOF container. func (c *Container) UnmarshalBinary(b []byte) error { + e := NewParseError + if len(b) < 14 { + return io.ErrUnexpectedEOF + } if !hasEOFMagic(b) { - return fmt.Errorf("invalid magic: have %s, want %s", common.Bytes2Hex(b[:min(2, len(b))]), common.Bytes2Hex(eofMagic)) + return e(ErrInvalidMagic, 0, "have %s, want %s", common.Bytes2Hex(b[:len(eofMagic)]), eofMagic) } if !isEOFVersion1(b) { - have := "" - if len(b) > 3 { - have = fmt.Sprintf("%d", int(b[2])) - } - return fmt.Errorf("invalid eof version: have %s, want %d", have, eof1Version) + return e(ErrInvalidVersion, 2, "have %d, want %d", b[2], eof1Version) } var ( @@ -130,25 +169,25 @@ func (c *Container) UnmarshalBinary(b []byte) error { return err } if kind != kindTypes { - return fmt.Errorf("expected kind types 0x01: have %x", kind) + return e(ErrMissingTypeHeader, offsetTypesKind, "found section kind %x instead", kind) } if typesSize < 4 || typesSize%4 != 0 { - return fmt.Errorf("type section size must be divisible by 4: have %d", typesSize) + return e(ErrInvalidTypeSize, offsetTypesKind+1, "type section size must be divisible by 4: have %d", typesSize) } if typesSize/4 > 1024 { - return fmt.Errorf("number of code sections must not exceed 1024: got %d", typesSize/4) + return e(ErrInvalidTypeSize, offsetTypesKind+1, "type section must not exceed 4*1024: have %d", typesSize/4) } // Parse code section header. kind, codeSizes, err = parseSectionList(b, offsetCodeKind) if err != nil { - return fmt.Errorf("failed to parse section list: %v", err) + return err } if kind != kindCode { - return fmt.Errorf("expected kind code 0x02: have %x", kind) + return e(ErrMissingCodeHeader, offsetCodeKind, "found section kind %x instead", kind) } if len(codeSizes) != typesSize/4 { - return fmt.Errorf("mismatch of code sections count and type signatures: types %d, code %d)", typesSize/4, len(codeSizes)) + return e(ErrInvalidCodeSize, offsetCodeKind+1, "mismatch of code sections count and type signatures: types %d, code %d)", typesSize/4, len(codeSizes)) } // Parse data section header. @@ -158,19 +197,22 @@ func (c *Container) UnmarshalBinary(b []byte) error { return err } if kind != kindData { - return fmt.Errorf("expected kind data 0x03: have %x", kind) + return e(ErrMissingDataHeader, offsetDataKind, "found section kind %x instead", kind) } // Check for terminator. offsetTerminator := offsetDataKind + 3 - if len(b) < offsetTerminator || b[offsetTerminator] != 0 { - return fmt.Errorf("expected terminator") + if len(b) < offsetTerminator { + return io.ErrUnexpectedEOF + } + if b[offsetTerminator] != 0 { + return e(ErrMissingTerminator, offsetTerminator, "have %x", b[offsetTerminator]) } // Verify overall container size. expectedSize := offsetTerminator + typesSize + sum(codeSizes) + dataSize + 1 if len(b) != expectedSize { - return fmt.Errorf("invalid container size: have %d, want %d", len(b), expectedSize) + return e(ErrInvalidContainerSize, 0, "have %d, want %d", len(b), expectedSize) } // Parse types section. @@ -183,18 +225,18 @@ func (c *Container) UnmarshalBinary(b []byte) error { MaxStackHeight: binary.BigEndian.Uint16(b[idx+i*4+2:]), } if sig.Input > maxInputItems { - return fmt.Errorf("invalid type annotation at index %d: inputs must not exceed %d: have %d", i, maxInputItems, sig.Input) + return e(ErrTooManyInputs, idx+i*4, "for section %d, have %d", i, sig.Input) } if sig.Output > maxOutputItems { - return fmt.Errorf("invalid type annotation at index %d: inputs and outputs must not exceed %d: have %d", i, maxOutputItems, sig.Output) + return e(ErrTooManyOutputs, idx+i*4+1, "for section %d, have %d", i, sig.Output) } if sig.MaxStackHeight > maxStackHeight { - return fmt.Errorf("invalid type annotation at index %d: max stack height must not exceed %d: have %d", i, maxStackHeight, sig.MaxStackHeight) + return e(ErrTooLargeMaxStackHeight, idx+i*4+2, "for section %d, have %d", i, sig.MaxStackHeight) } types = append(types, sig) } if types[0].Input != 0 || types[0].Output != 0 { - return fmt.Errorf("invalid type annotation at index 0: input and output must be 0: have (%d, %d)", types[0].Input, types[0].Output) + return e(ErrInvalidSection0Type, idx, "have %d, %d", types[0].Input, types[0].Output) } c.Types = types @@ -203,7 +245,7 @@ func (c *Container) UnmarshalBinary(b []byte) error { code := make([][]byte, len(codeSizes)) for i, size := range codeSizes { if size == 0 { - return fmt.Errorf("invalid code section %d: size must not be 0", i) + return e(ErrInvalidCodeSize, offsetCodeKind+2+i*2, "invalid code section %d: size must not be 0", i) } code[i] = b[idx : idx+size] idx += size diff --git a/core/vm/errors.go b/core/vm/errors.go index 275d7c1815ce..f1ae2a302c53 100644 --- a/core/vm/errors.go +++ b/core/vm/errors.go @@ -52,10 +52,14 @@ type ErrStackUnderflow struct { required int } -func (e *ErrStackUnderflow) Error() string { +func (e ErrStackUnderflow) Error() string { return fmt.Sprintf("stack underflow (%d <=> %d)", e.stackLen, e.required) } +func (e ErrStackUnderflow) Unwrap() error { + return fmt.Errorf("stack underflow") +} + // ErrStackOverflow wraps an evm error when the items on the stack exceeds // the maximum allowance. type ErrStackOverflow struct { @@ -63,10 +67,14 @@ type ErrStackOverflow struct { limit int } -func (e *ErrStackOverflow) Error() string { +func (e ErrStackOverflow) Error() string { return fmt.Sprintf("stack limit reached %d (%d)", e.stackLen, e.limit) } +func (e ErrStackOverflow) Unwrap() error { + return fmt.Errorf("stack overflow") +} + // ErrInvalidOpCode wraps an evm error when an invalid opcode is encountered. type ErrInvalidOpCode struct { opcode OpCode diff --git a/core/vm/validate.go b/core/vm/validate.go index a6ffa4d7dc84..d4b2f8faa7f7 100644 --- a/core/vm/validate.go +++ b/core/vm/validate.go @@ -1,5 +1,3 @@ -// Copyright 2022 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 @@ -17,15 +15,30 @@ package vm import ( - "fmt" + "errors" + "io" "github.com/ethereum/go-ethereum/params" ) +var ( + ErrUndefinedInstruction = errors.New("undefined instrustion") + ErrTruncatedImmediate = errors.New("truncated immediate") + ErrInvalidSectionArgument = errors.New("invalid section argument") + ErrInvalidJumpDest = errors.New("invalid jump destination") + ErrConflictingStack = errors.New("conflicting stack height") + ErrInvalidBranchCount = errors.New("invalid number of branches in jump table") + ErrInvalidOutputs = errors.New("invalid number of outputs") + ErrInvalidMaxStackHeight = errors.New("invalid max stack height") + ErrInvalidCodeTermination = errors.New("invalid code termination") + ErrUnreachableCode = errors.New("unreachable code") +) + // validateCode validates the code parameter against the EOF v1 validity requirements. func validateCode(code []byte, section int, metadata []*FunctionMetadata, jt *JumpTable) error { var ( i = 0 + e = NewParseError // Tracks the number of actual instructions in the code (e.g. // non-immediate values). This is used at the end to determine // if each instruction is reachable. @@ -43,18 +56,18 @@ func validateCode(code []byte, section int, metadata []*FunctionMetadata, jt *Ju count++ op = OpCode(code[i]) if jt[op].undefined { - return fmt.Errorf("use of undefined opcode %s", op) + return e(ErrUndefinedInstruction, i, "opcode=%s", op) } switch { case op >= PUSH1 && op <= PUSH32: size := int(op - PUSH0) if len(code) <= i+size { - return fmt.Errorf("truncated operand") + return e(ErrTruncatedImmediate, i, "op=%s", op) } i += size case op == RJUMP || op == RJUMPI: if len(code) <= i+2 { - return fmt.Errorf("truncated rjump* operand") + return e(ErrTruncatedImmediate, i, "op=%s", op) } if err := checkDest(code, &analysis, i+1, i+3, len(code)); err != nil { return err @@ -62,14 +75,14 @@ func validateCode(code []byte, section int, metadata []*FunctionMetadata, jt *Ju i += 2 case op == RJUMPV: if len(code) <= i+1 { - return fmt.Errorf("truncated jump table operand") + return e(ErrTruncatedImmediate, i, "jump table size missing") } count := int(code[i+1]) if count == 0 { - return fmt.Errorf("rjumpv branch count must not be 0") + return e(ErrInvalidBranchCount, i, "must not be 0") } if len(code) <= i+count { - return fmt.Errorf("truncated jump table operand") + return e(ErrTruncatedImmediate, i, "jump table truncated") } for j := 0; j < count; j++ { if err := checkDest(code, &analysis, i+2+j*2, i+2*count+2, len(code)); err != nil { @@ -79,11 +92,11 @@ func validateCode(code []byte, section int, metadata []*FunctionMetadata, jt *Ju i += 1 + 2*count case op == CALLF: if i+2 >= len(code) { - return fmt.Errorf("truncated operand") + return e(ErrTruncatedImmediate, i, "op=%s", op) } arg, _ := parseUint16(code[i+1:]) if arg >= len(metadata) { - return fmt.Errorf("code section out-of-bounds (want: %d, have: %d)", arg, len(metadata)) + return e(ErrInvalidSectionArgument, i, "arg %d, last section %d", arg, len(metadata)) } i += 2 } @@ -92,12 +105,13 @@ func validateCode(code []byte, section int, metadata []*FunctionMetadata, jt *Ju // Code sections may not "fall through" and require proper termination. // Therefore, the last instruction must be considered terminal. if !jt[op].terminal { - return fmt.Errorf("code section ends with non-terminal instruction") + return e(ErrInvalidCodeTermination, i, "ends with op %s", op) } if paths, err := validateControlFlow(code, section, metadata, jt); err != nil { return err } else if paths != count { - return fmt.Errorf("unreachable code") + // TODO(matt): return actual position of unreacable code + return e(ErrUnreachableCode, 0, "") } return nil } @@ -105,17 +119,18 @@ func validateCode(code []byte, section int, metadata []*FunctionMetadata, jt *Ju // checkDest parses a relative offset at code[0:2] and checks if it is a valid jump destination. func checkDest(code []byte, analysis *bitvec, imm, from, length int) error { if len(code) < imm+2 { - return fmt.Errorf("truncated operand") + return io.ErrUnexpectedEOF } if analysis != nil && *analysis == nil { *analysis = eofCodeBitmap(code) } - dest := from + parseInt16(code[imm:]) + offset := parseInt16(code[imm:]) + dest := from + offset if dest < 0 || dest >= length { - return fmt.Errorf("relative offset out-of-bounds: %d", dest) + return NewParseError(ErrInvalidJumpDest, imm, "relative offset out-of-bounds: offset %d, dest %d", offset, dest) } if !analysis.codeSegment(uint64(dest)) { - return fmt.Errorf("relative offset into immediate operand: %d", dest) + return NewParseError(ErrInvalidJumpDest, imm, "relative offset into immediate value: offset %d, dest %d", offset, dest) } return nil } @@ -128,6 +143,7 @@ func validateControlFlow(code []byte, section int, metadata []*FunctionMetadata, height int } var ( + e = NewParseError heights = make(map[int]int) worklist = []item{{0, int(metadata[section].Input)}} maxStackHeight = int(metadata[section].Input) @@ -144,9 +160,9 @@ func validateControlFlow(code []byte, section int, metadata []*FunctionMetadata, op := OpCode(code[pos]) // Check if pos has already be visited; if so, the stack heights should be the same. - if exp, ok := heights[pos]; ok { - if height != exp { - return 0, fmt.Errorf("stack height mismatch for different paths") + if want, ok := heights[pos]; ok { + if height != want { + return 0, e(ErrConflictingStack, pos, "have %d, want %d", height, want) } // Already visited this path and stack height // matches. @@ -156,10 +172,10 @@ func validateControlFlow(code []byte, section int, metadata []*FunctionMetadata, // Validate height for current op and update as needed. if jt[op].minStack > height { - return 0, fmt.Errorf("stack underflow") + return 0, e(ErrStackUnderflow{stackLen: height, required: jt[op].minStack}, pos, "") } if jt[op].maxStack < height { - return 0, fmt.Errorf("stack overflow") + return 0, e(ErrStackOverflow{stackLen: height, limit: jt[op].maxStack}, pos, "") } height += int(params.StackLimit) - jt[op].maxStack @@ -167,17 +183,17 @@ func validateControlFlow(code []byte, section int, metadata []*FunctionMetadata, case op == CALLF: arg, _ := parseUint16(code[pos+1:]) if metadata[arg].Input > uint8(height) { - return 0, fmt.Errorf("stack underflow") + return 0, e(ErrStackUnderflow{stackLen: height, required: int(metadata[arg].Input)}, pos, "CALLF underflow to section %d", arg) } if int(metadata[arg].Output)+height > int(params.StackLimit) { - return 0, fmt.Errorf("stack overflow") + return 0, e(ErrStackOverflow{stackLen: int(metadata[arg].Output) + height, limit: int(params.StackLimit)}, pos, "CALLF overflow to section %d") } height -= int(metadata[arg].Input) height += int(metadata[arg].Output) pos += 3 case op == RETF: if int(metadata[section].Output) != height { - return 0, fmt.Errorf("wrong number of outputs (want: %d, got: %d)", metadata[section].Output, height) + return 0, e(ErrInvalidOutputs, pos, "have %d, want %d", height, metadata[section].Output) } break outer case op == RJUMP: @@ -208,7 +224,7 @@ func validateControlFlow(code []byte, section int, metadata []*FunctionMetadata, } } if maxStackHeight != int(metadata[section].MaxStackHeight) { - return 0, fmt.Errorf("computed max stack height for code section %d does not match expected (want: %d, got: %d)", section, metadata[section].MaxStackHeight, maxStackHeight) + return 0, e(ErrInvalidMaxStackHeight, 0, "at code section %d, have %d, want %d", section, maxStackHeight, metadata[section].MaxStackHeight) } return len(heights), nil } diff --git a/core/vm/validate_test.go b/core/vm/validate_test.go index 138f97ff118d..57114196e746 100644 --- a/core/vm/validate_test.go +++ b/core/vm/validate_test.go @@ -17,7 +17,7 @@ package vm import ( - "fmt" + "errors" "testing" "github.com/ethereum/go-ethereum/common" @@ -63,7 +63,7 @@ func TestValidateCode(t *testing.T) { }, section: 0, metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 1}}, - err: fmt.Errorf("code section ends with non-terminal instruction"), + err: ErrInvalidCodeTermination, }, { code: []byte{ @@ -75,7 +75,7 @@ func TestValidateCode(t *testing.T) { }, section: 0, metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 0}}, - err: fmt.Errorf("unreachable code"), + err: ErrUnreachableCode, }, { code: []byte{ @@ -86,7 +86,7 @@ func TestValidateCode(t *testing.T) { }, section: 0, metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 1}}, - err: fmt.Errorf("stack underflow"), + err: ErrStackUnderflow{stackLen: 1, required: 2}, }, { code: []byte{ @@ -97,7 +97,7 @@ func TestValidateCode(t *testing.T) { }, section: 0, metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 2}}, - err: fmt.Errorf("computed max stack height for code section 0 does not match expected (want: 2, got: 1)"), + err: ErrInvalidMaxStackHeight, }, { code: []byte{ @@ -112,7 +112,7 @@ func TestValidateCode(t *testing.T) { }, section: 0, metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 1}}, - err: fmt.Errorf("relative offset into immediate operand: 5"), + err: ErrInvalidJumpDest, }, { code: []byte{ @@ -130,7 +130,7 @@ func TestValidateCode(t *testing.T) { }, section: 0, metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 1}}, - err: fmt.Errorf("relative offset into immediate operand: 8"), + err: ErrInvalidJumpDest, }, { code: []byte{ @@ -141,7 +141,7 @@ func TestValidateCode(t *testing.T) { }, section: 0, metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 1}}, - err: fmt.Errorf("rjumpv branch count must not be 0"), + err: ErrInvalidBranchCount, }, { code: []byte{ @@ -204,7 +204,7 @@ func TestValidateCode(t *testing.T) { }, section: 0, metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 0}}, - err: fmt.Errorf("unreachable code"), + err: ErrUnreachableCode, }, { code: []byte{ @@ -212,7 +212,7 @@ func TestValidateCode(t *testing.T) { }, section: 0, metadata: []*FunctionMetadata{{Input: 0, Output: 1, MaxStackHeight: 0}}, - err: fmt.Errorf("wrong number of outputs (want: 1, got: 0)"), + err: ErrInvalidOutputs, }, { code: []byte{ @@ -243,7 +243,7 @@ func TestValidateCode(t *testing.T) { }, } { err := validateCode(test.code, test.section, test.metadata, &shanghaiEOFInstructionSet) - if (err != nil && test.err == nil) || (err == nil && test.err != nil) || (err != nil && test.err != nil && err.Error() != test.err.Error()) { + if !errors.Is(err, test.err) { t.Errorf("test %d (%s): unexpected error (want: %v, got: %v)", i, common.Bytes2Hex(test.code), test.err, err) } } From 50110b58d51452d915263c45135b200310ec55a3 Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Mon, 9 Jan 2023 08:43:11 -0700 Subject: [PATCH 44/53] cmd/evm: add eofparser test tool to evm binary --- cmd/evm/eofparser.go | 168 +++++++++++++++++++++++++++++++++++++++++++ cmd/evm/main.go | 36 ++++++++++ core/vm/eof.go | 2 +- 3 files changed, 205 insertions(+), 1 deletion(-) create mode 100644 cmd/evm/eofparser.go diff --git a/cmd/evm/eofparser.go b/cmd/evm/eofparser.go new file mode 100644 index 000000000000..8603cfbbbc12 --- /dev/null +++ b/cmd/evm/eofparser.go @@ -0,0 +1,168 @@ +// Copyright 2023 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 main + +import ( + "bufio" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "strings" + + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/log" + "github.com/urfave/cli/v2" +) + +func init() { + jt = vm.NewShanghaiEOFInstructionSetForTesting() +} + +var ( + jt vm.JumpTable + errorMap = map[string]int{ + io.ErrUnexpectedEOF.Error(): 1, + vm.ErrInvalidMagic.Error(): 2, + vm.ErrInvalidVersion.Error(): 3, + vm.ErrMissingTypeHeader.Error(): 4, + vm.ErrInvalidTypeSize.Error(): 5, + vm.ErrMissingCodeHeader.Error(): 6, + vm.ErrInvalidCodeHeader.Error(): 7, + vm.ErrMissingDataHeader.Error(): 8, + vm.ErrMissingTerminator.Error(): 9, + vm.ErrTooManyInputs.Error(): 10, + vm.ErrTooManyOutputs.Error(): 11, + vm.ErrTooLargeMaxStackHeight.Error(): 12, + vm.ErrInvalidCodeSize.Error(): 13, + vm.ErrInvalidContainerSize.Error(): 14, + vm.ErrUndefinedInstruction.Error(): 15, + vm.ErrTruncatedImmediate.Error(): 16, + vm.ErrInvalidSectionArgument.Error(): 17, + vm.ErrInvalidJumpDest.Error(): 18, + vm.ErrConflictingStack.Error(): 19, + vm.ErrInvalidBranchCount.Error(): 20, + vm.ErrInvalidOutputs.Error(): 21, + vm.ErrInvalidMaxStackHeight.Error(): 22, + vm.ErrInvalidCodeTermination.Error(): 23, + vm.ErrUnreachableCode.Error(): 24, + } +) + +type EOFTest struct { + Code string `json:"code"` + Results map[string]etResult `json:"results"` +} + +type etResult struct { + Result bool `json:"result"` + Exception int `json:"exception,omitempty"` +} + +func eofParser(ctx *cli.Context) error { + glogger := log.NewGlogHandler(log.StreamHandler(os.Stderr, log.TerminalFormat(false))) + glogger.Verbosity(log.Lvl(ctx.Int(VerbosityFlag.Name))) + log.Root().SetHandler(glogger) + + // If `--hex` is set, parse and validate the hex string argument. + if ctx.IsSet(HexFlag.Name) { + if _, err := parseAndValidate(ctx.String(HexFlag.Name)); err != nil { + if err2 := errors.Unwrap(err); err2 != nil { + err = err2 + } + return fmt.Errorf("err(%d): %w", errorMap[err.Error()], err) + } + fmt.Println("ok.") + return nil + } + + // If `--test` is set, parse and validate the reference test at the provided path. + if ctx.IsSet(RefTestFlag.Name) { + src, err := os.ReadFile(ctx.String(RefTestFlag.Name)) + if err != nil { + return err + } + var tests map[string]EOFTest + if err = json.Unmarshal(src, &tests); err != nil { + return err + } + passed, total := 0, 0 + for name, tt := range tests { + for fork, r := range tt.Results { + total++ + // TODO(matt): all tests currently run against + // shanghai EOF, add support for custom forks. + _, err := parseAndValidate(tt.Code) + if err2 := errors.Unwrap(err); err2 != nil { + err = err2 + } + if r.Result && err != nil { + fmt.Fprintf(os.Stderr, "%s, %s: expected success, got %v\n", name, fork, err) + continue + } + if !r.Result && err == nil { + fmt.Fprintf(os.Stderr, "%s, %s: expected error %d, got %v\n", name, fork, r.Exception, err) + continue + } + if !r.Result && err != nil && r.Exception != errorMap[err.Error()] { + fmt.Fprintf(os.Stderr, "%s, %s: expected error %d, got: err(%d): %v\n", name, fork, r.Exception, errorMap[err.Error()], err) + continue + } + passed++ + } + } + fmt.Printf("%d/%d tests passed.\n", passed, total) + return nil + } + + // If neither are passed in, read input from stdin. + scanner := bufio.NewScanner(os.Stdin) + for scanner.Scan() { + t := strings.TrimSpace(scanner.Text()) + if len(t) == 0 || t[0] == '#' { + continue + } + if _, err := parseAndValidate(t); err != nil { + if err2 := errors.Unwrap(err); err2 != nil { + err = err2 + } + fmt.Fprintf(os.Stderr, "err(%d): %v\n", errorMap[err.Error()], err) + } + } + + return nil +} + +func parseAndValidate(s string) (*vm.Container, error) { + if len(s) >= 2 && strings.HasPrefix(s, "0x") { + s = s[2:] + } + b, err := hex.DecodeString(s) + if err != nil { + return nil, fmt.Errorf("unable to decode data: %w", err) + } + var c vm.Container + if err := c.UnmarshalBinary(b); err != nil { + return nil, err + } + if err := c.ValidateCode(&jt); err != nil { + return nil, err + } + return &c, nil +} diff --git a/cmd/evm/main.go b/cmd/evm/main.go index 5f9e75f48c6f..97e29cd6584a 100644 --- a/cmd/evm/main.go +++ b/cmd/evm/main.go @@ -21,9 +21,12 @@ import ( "fmt" "math/big" "os" + "strings" "github.com/ethereum/go-ethereum/cmd/evm/internal/t8ntool" + "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/internal/flags" + "github.com/ethereum/go-ethereum/tests" "github.com/urfave/cli/v2" ) @@ -125,6 +128,26 @@ var ( Value: true, Usage: "enable return data output", } + HexFlag = &cli.StringFlag{ + Name: "hex", + Usage: "single container data parse and validation", + } + ForknameFlag = &cli.StringFlag{ + Name: "state.fork", + Usage: fmt.Sprintf("Name of ruleset to use."+ + "\n\tAvailable forknames:"+ + "\n\t %v"+ + "\n\tAvailable extra eips:"+ + "\n\t %v"+ + "\n\tSyntax (+ExtraEip)", + strings.Join(tests.AvailableForks(), "\n\t "), + strings.Join(vm.ActivateableEips(), ", ")), + Value: "Shanghai", + } + RefTestFlag = &cli.StringFlag{ + Name: "test", + Usage: "Path to EOF validation reference test.", + } ) var stateTransitionCommand = &cli.Command{ @@ -185,6 +208,18 @@ var blockBuilderCommand = &cli.Command{ }, } +var eofParserCommand = &cli.Command{ + Name: "eofparser", + Aliases: []string{"eof"}, + Usage: "parses hex eof container and returns validation errors (if any)", + Action: eofParser, + Flags: []cli.Flag{ + VerbosityFlag, + HexFlag, + RefTestFlag, + }, +} + var app = flags.NewApp("the evm command line interface") func init() { @@ -221,6 +256,7 @@ func init() { stateTransitionCommand, transactionCommand, blockBuilderCommand, + eofParserCommand, } } diff --git a/core/vm/eof.go b/core/vm/eof.go index 0dbc42ff1ed9..0eb7c8b9143c 100644 --- a/core/vm/eof.go +++ b/core/vm/eof.go @@ -78,7 +78,7 @@ var ( ErrTooManyOutputs = errors.New("invalid type content, too many inputs") ErrInvalidSection0Type = errors.New("invalid section 0 type, input and output should be zero") ErrTooLargeMaxStackHeight = errors.New("invalid type content, max stack height exceeds limit") - ErrInvalidContainerSize = errors.New("inavlid container size") + ErrInvalidContainerSize = errors.New("invalid container size") ) var eofMagic = []byte{0xef, 0x00} From db10933dad112e2bda862d21acf11d4e3ab3816c Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Tue, 10 Jan 2023 11:11:48 -0700 Subject: [PATCH 45/53] core: fix eof blockchain test and reorder magic check --- core/vm/eof.go | 6 +++--- core/vm/evm.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/vm/eof.go b/core/vm/eof.go index 0eb7c8b9143c..cf2dfe286e75 100644 --- a/core/vm/eof.go +++ b/core/vm/eof.go @@ -147,12 +147,12 @@ func (c *Container) MarshalBinary() []byte { // UnmarshalBinary decodes an EOF container. func (c *Container) UnmarshalBinary(b []byte) error { e := NewParseError - if len(b) < 14 { - return io.ErrUnexpectedEOF - } if !hasEOFMagic(b) { return e(ErrInvalidMagic, 0, "have %s, want %s", common.Bytes2Hex(b[:len(eofMagic)]), eofMagic) } + if len(b) < 14 { + return io.ErrUnexpectedEOF + } if !isEOFVersion1(b) { return e(ErrInvalidVersion, 2, "have %d, want %d", b[2], eof1Version) } diff --git a/core/vm/evm.go b/core/vm/evm.go index e27855402ea3..402a0abd51df 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -563,7 +563,7 @@ func (evm *EVM) parseContainer(b []byte) *Container { return nil } else if err != nil { // Code was already validated, so no other errors should be possible. - panic(fmt.Sprintf("unexpected error: %v", err)) + panic(fmt.Sprintf("unexpected error: %v\ncode: %s\n", err, common.Bytes2Hex(b))) } return &c } From a229ad6f549c104bddec8ab4e76212eda79b868f Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Wed, 11 Jan 2023 11:40:16 -0700 Subject: [PATCH 46/53] core/vm: don't update caller nonce on creation if eof validation fails --- core/vm/evm.go | 52 +++++++++++++++++++++++++------------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/core/vm/evm.go b/core/vm/evm.go index 402a0abd51df..f70aeebf9e29 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -416,47 +416,47 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, if !evm.Context.CanTransfer(evm.StateDB, caller.Address(), value) { return nil, common.Address{}, gas, ErrInsufficientBalance } - nonce := evm.StateDB.GetNonce(caller.Address()) - if nonce+1 < nonce { - return nil, common.Address{}, gas, ErrNonceUintOverflow - } - evm.StateDB.SetNonce(caller.Address(), nonce+1) - // We add this to the access list _before_ taking a snapshot. Even if the creation fails, - // the access-list change should not be rolled back - if evm.chainRules.IsBerlin { - evm.StateDB.AddAddressToAccessList(address) - } - // Ensure there's no existing contract already at the designated address - contractHash := evm.StateDB.GetCodeHash(address) - if evm.StateDB.GetNonce(address) != 0 || (contractHash != (common.Hash{}) && contractHash != emptyCodeHash) { - return nil, common.Address{}, 0, ErrContractAddressCollision - } // 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. + // The contract is a scoped environment for this execution context only. If + // the initcode is EOF, contract.Container will be set. contract := NewContract(caller, AccountRef(address), value, gas) contract.SetCodeOptionalHash(&address, codeAndHash) - var ( - isCallerEOF = fromEOF - isInitcodeEOF = hasEOFMagic(codeAndHash.code) - ) + // Validate initcode per EOF rules. If caller is EOF and initcode is legacy, fail. + isInitcodeEOF := hasEOFMagic(codeAndHash.code) if evm.chainRules.IsShanghai { - if isCallerEOF && !isInitcodeEOF { - // Don't allow EOF contract to run legacy initcode. - return nil, common.Address{}, gas, ErrLegacyCode - } else if isInitcodeEOF { + if isInitcodeEOF { // If the initcode is EOF, verify it is well-formed. var c Container if err := c.UnmarshalBinary(codeAndHash.code); err != nil { - return nil, common.Address{}, gas, fmt.Errorf("%v: %v", ErrInvalidEOF, err) + return nil, common.Address{}, gas, fmt.Errorf("%w: %v", ErrInvalidEOF, err) } if err := c.ValidateCode(evm.interpreter.cfg.JumpTableEOF); err != nil { - return nil, common.Address{}, gas, fmt.Errorf("%v: %v", ErrInvalidEOF, err) + return nil, common.Address{}, gas, fmt.Errorf("%w: %v", ErrInvalidEOF, err) } contract.Container = &c + } else if fromEOF { + // Don't allow EOF contract to execute legacy initcode. + return nil, common.Address{}, gas, ErrLegacyCode } } + // Check for nonce overflow and then update caller nonce by 1. + nonce := evm.StateDB.GetNonce(caller.Address()) + if nonce+1 < nonce { + return nil, common.Address{}, gas, ErrNonceUintOverflow + } + evm.StateDB.SetNonce(caller.Address(), nonce+1) + // We add this to the access list _before_ taking a snapshot. Even if the creation fails, + // the access-list change should not be rolled back + if evm.chainRules.IsBerlin { + evm.StateDB.AddAddressToAccessList(address) + } + // Ensure there's no existing contract already at the designated address + contractHash := evm.StateDB.GetCodeHash(address) + if evm.StateDB.GetNonce(address) != 0 || (contractHash != (common.Hash{}) && contractHash != emptyCodeHash) { + return nil, common.Address{}, 0, ErrContractAddressCollision + } // Create a new account on the state snapshot := evm.StateDB.Snapshot() From d410d2a348453f6e4686bf270627920e53c603a0 Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Wed, 11 Jan 2023 11:51:58 -0700 Subject: [PATCH 47/53] core/vm: clean up eof bitmap analysis Co-authored-by: lightclient@protonmail.com Co-authored-by: Martin Holst Swende --- core/vm/analysis.go | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/core/vm/analysis.go b/core/vm/analysis.go index 8bc63bef824c..37a109d8e03f 100644 --- a/core/vm/analysis.go +++ b/core/vm/analysis.go @@ -130,39 +130,40 @@ func eofCodeBitmap(code []byte) bitvec { // code validation. func eofCodeBitmapInternal(code, bits bitvec) bitvec { for pc := uint64(0); pc < uint64(len(code)); { - op := OpCode(code[pc]) + var ( + op = OpCode(code[pc]) + numbits uint8 + ) pc++ - // RJUMP and RJUMPI always have 2 byte operand. - if int8(op) == int8(RJUMP) || int8(op) == int8(RJUMPI) { - bits.setN(set2BitsMask, pc) - pc += 2 - continue - } - var numbits uint8 - if int8(op) >= int8(PUSH1) && int8(op) <= int8(PUSH32) { + switch { + case op >= PUSH1 && op <= PUSH32: numbits = uint8(op - PUSH1 + 1) - } else if int8(op) == int8(RJUMPV) { + case op == RJUMP || op == RJUMPI || op == CALLF: + numbits = 2 + case op == RJUMPV: // RJUMPV is unique as it has a variable sized operand. // The total size is determined by the count byte which // immediate proceeds RJUMPV. Truncation will be caught // in other validation steps -- for now, just return a // valid bitmap for as much of the code as is // available. - if pc >= uint64(len(code)) { + end := uint64(len(code)) + if pc >= end { // Count missing, no more bits to mark. return bits } numbits = code[pc]*2 + 1 - if pc+uint64(numbits) > uint64(len(code)) { + if pc+uint64(numbits) > end { // Jump table is truncated, mark as many bits // as possible. - numbits = uint8(len(code) - int(pc)) + numbits = uint8(end - pc) } - } else { - // If not PUSH (the int8(op) > int(PUSH32) is always false). + default: + // Op had no immediate operand, continue. continue } + if numbits >= 8 { for ; numbits >= 16; numbits -= 16 { bits.set16(pc) From 8fb979f15dc55342b03b55b376a5e238a94b603b Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Wed, 11 Jan 2023 15:40:40 -0700 Subject: [PATCH 48/53] core/vm: remove custom parser error --- core/vm/eof.go | 54 +++++++++++++-------------------------------- core/vm/validate.go | 51 +++++++++++++++++++++--------------------- 2 files changed, 40 insertions(+), 65 deletions(-) diff --git a/core/vm/eof.go b/core/vm/eof.go index cf2dfe286e75..a185c7bb1adc 100644 --- a/core/vm/eof.go +++ b/core/vm/eof.go @@ -22,8 +22,6 @@ import ( "errors" "fmt" "io" - - "github.com/ethereum/go-ethereum/common" ) const ( @@ -43,27 +41,6 @@ const ( maxStackHeight = 1023 ) -type ParseError struct { - inner error - index int - msg string -} - -func NewParseError(err error, index int, format string, a ...any) ParseError { - return ParseError{inner: err, index: index, msg: fmt.Sprintf(format, a...)} -} - -func (p ParseError) Error() string { - if p.msg == "" { - return fmt.Sprintf("%s at position %d", p.inner, p.index) - } - return fmt.Sprintf("%s at position %d, %s", p.inner, p.index, p.msg) -} - -func (p ParseError) Unwrap() error { - return p.inner -} - var ( ErrInvalidMagic = errors.New("invalid magic") ErrInvalidVersion = errors.New("invalid version") @@ -146,15 +123,14 @@ func (c *Container) MarshalBinary() []byte { // UnmarshalBinary decodes an EOF container. func (c *Container) UnmarshalBinary(b []byte) error { - e := NewParseError if !hasEOFMagic(b) { - return e(ErrInvalidMagic, 0, "have %s, want %s", common.Bytes2Hex(b[:len(eofMagic)]), eofMagic) + return fmt.Errorf("%w: want %s", ErrInvalidMagic, eofMagic) } if len(b) < 14 { return io.ErrUnexpectedEOF } if !isEOFVersion1(b) { - return e(ErrInvalidVersion, 2, "have %d, want %d", b[2], eof1Version) + return fmt.Errorf("%w: have %d, want %d", ErrInvalidVersion, b[2], eof1Version) } var ( @@ -169,13 +145,13 @@ func (c *Container) UnmarshalBinary(b []byte) error { return err } if kind != kindTypes { - return e(ErrMissingTypeHeader, offsetTypesKind, "found section kind %x instead", kind) + return fmt.Errorf("%w: found section kind %x instead", ErrMissingTypeHeader, kind) } if typesSize < 4 || typesSize%4 != 0 { - return e(ErrInvalidTypeSize, offsetTypesKind+1, "type section size must be divisible by 4: have %d", typesSize) + return fmt.Errorf("%w: type section size must be divisible by 4, have %d", ErrInvalidTypeSize, typesSize) } if typesSize/4 > 1024 { - return e(ErrInvalidTypeSize, offsetTypesKind+1, "type section must not exceed 4*1024: have %d", typesSize/4) + return fmt.Errorf("%w: type section must not exceed 4*1024, have %d", ErrInvalidTypeSize, typesSize) } // Parse code section header. @@ -184,10 +160,10 @@ func (c *Container) UnmarshalBinary(b []byte) error { return err } if kind != kindCode { - return e(ErrMissingCodeHeader, offsetCodeKind, "found section kind %x instead", kind) + return fmt.Errorf("%w: found section kind %x instead", ErrMissingCodeHeader, kind) } if len(codeSizes) != typesSize/4 { - return e(ErrInvalidCodeSize, offsetCodeKind+1, "mismatch of code sections count and type signatures: types %d, code %d)", typesSize/4, len(codeSizes)) + return fmt.Errorf("%w: mismatch of code sections cound and type signatures, types %d, code %d", ErrInvalidCodeSize, typesSize/4, len(codeSizes)) } // Parse data section header. @@ -197,7 +173,7 @@ func (c *Container) UnmarshalBinary(b []byte) error { return err } if kind != kindData { - return e(ErrMissingDataHeader, offsetDataKind, "found section kind %x instead", kind) + return fmt.Errorf("%w: found section %x instead", ErrMissingDataHeader, kind) } // Check for terminator. @@ -206,13 +182,13 @@ func (c *Container) UnmarshalBinary(b []byte) error { return io.ErrUnexpectedEOF } if b[offsetTerminator] != 0 { - return e(ErrMissingTerminator, offsetTerminator, "have %x", b[offsetTerminator]) + return fmt.Errorf("%w: have %x", ErrMissingTerminator, b[offsetTerminator]) } // Verify overall container size. expectedSize := offsetTerminator + typesSize + sum(codeSizes) + dataSize + 1 if len(b) != expectedSize { - return e(ErrInvalidContainerSize, 0, "have %d, want %d", len(b), expectedSize) + return fmt.Errorf("%w: have %d, want %d", ErrInvalidContainerSize, len(b), expectedSize) } // Parse types section. @@ -225,18 +201,18 @@ func (c *Container) UnmarshalBinary(b []byte) error { MaxStackHeight: binary.BigEndian.Uint16(b[idx+i*4+2:]), } if sig.Input > maxInputItems { - return e(ErrTooManyInputs, idx+i*4, "for section %d, have %d", i, sig.Input) + return fmt.Errorf("%w for section %d: have %d", ErrTooManyInputs, i, sig.Input) } if sig.Output > maxOutputItems { - return e(ErrTooManyOutputs, idx+i*4+1, "for section %d, have %d", i, sig.Output) + return fmt.Errorf("%w for section %d: have %d", ErrTooManyOutputs, i, sig.Output) } if sig.MaxStackHeight > maxStackHeight { - return e(ErrTooLargeMaxStackHeight, idx+i*4+2, "for section %d, have %d", i, sig.MaxStackHeight) + return fmt.Errorf("%w for section %d: have %d", ErrTooLargeMaxStackHeight, i, sig.MaxStackHeight) } types = append(types, sig) } if types[0].Input != 0 || types[0].Output != 0 { - return e(ErrInvalidSection0Type, idx, "have %d, %d", types[0].Input, types[0].Output) + return fmt.Errorf("%w: have %d, %d", ErrInvalidSection0Type, types[0].Input, types[0].Output) } c.Types = types @@ -245,7 +221,7 @@ func (c *Container) UnmarshalBinary(b []byte) error { code := make([][]byte, len(codeSizes)) for i, size := range codeSizes { if size == 0 { - return e(ErrInvalidCodeSize, offsetCodeKind+2+i*2, "invalid code section %d: size must not be 0", i) + return fmt.Errorf("%w for section %d: size must not be 0", ErrInvalidCodeSize, i) } code[i] = b[idx : idx+size] idx += size diff --git a/core/vm/validate.go b/core/vm/validate.go index d4b2f8faa7f7..fccdd6fa3eba 100644 --- a/core/vm/validate.go +++ b/core/vm/validate.go @@ -16,6 +16,7 @@ package vm import ( "errors" + "fmt" "io" "github.com/ethereum/go-ethereum/params" @@ -38,7 +39,6 @@ var ( func validateCode(code []byte, section int, metadata []*FunctionMetadata, jt *JumpTable) error { var ( i = 0 - e = NewParseError // Tracks the number of actual instructions in the code (e.g. // non-immediate values). This is used at the end to determine // if each instruction is reachable. @@ -56,18 +56,18 @@ func validateCode(code []byte, section int, metadata []*FunctionMetadata, jt *Ju count++ op = OpCode(code[i]) if jt[op].undefined { - return e(ErrUndefinedInstruction, i, "opcode=%s", op) + return fmt.Errorf("%w: op %s, pos %d", ErrUndefinedInstruction, op, i) } switch { case op >= PUSH1 && op <= PUSH32: size := int(op - PUSH0) if len(code) <= i+size { - return e(ErrTruncatedImmediate, i, "op=%s", op) + return fmt.Errorf("%w: op %s, pos %d", ErrTruncatedImmediate, op, i) } i += size case op == RJUMP || op == RJUMPI: if len(code) <= i+2 { - return e(ErrTruncatedImmediate, i, "op=%s", op) + return fmt.Errorf("%w: op %s, pos %d", ErrTruncatedImmediate, op, i) } if err := checkDest(code, &analysis, i+1, i+3, len(code)); err != nil { return err @@ -75,14 +75,14 @@ func validateCode(code []byte, section int, metadata []*FunctionMetadata, jt *Ju i += 2 case op == RJUMPV: if len(code) <= i+1 { - return e(ErrTruncatedImmediate, i, "jump table size missing") + return fmt.Errorf("%w: jump table size missing, op %s, pos %d", ErrTruncatedImmediate, op, i) } count := int(code[i+1]) if count == 0 { - return e(ErrInvalidBranchCount, i, "must not be 0") + return fmt.Errorf("%w: must not be 0, pos %d", ErrInvalidBranchCount, i) } if len(code) <= i+count { - return e(ErrTruncatedImmediate, i, "jump table truncated") + return fmt.Errorf("%w: jump table truncated, op %s, pos %d", ErrTruncatedImmediate, op, i) } for j := 0; j < count; j++ { if err := checkDest(code, &analysis, i+2+j*2, i+2*count+2, len(code)); err != nil { @@ -92,11 +92,11 @@ func validateCode(code []byte, section int, metadata []*FunctionMetadata, jt *Ju i += 1 + 2*count case op == CALLF: if i+2 >= len(code) { - return e(ErrTruncatedImmediate, i, "op=%s", op) + return fmt.Errorf("%w: op %s, pos %d", ErrTruncatedImmediate, op, i) } arg, _ := parseUint16(code[i+1:]) if arg >= len(metadata) { - return e(ErrInvalidSectionArgument, i, "arg %d, last section %d", arg, len(metadata)) + return fmt.Errorf("%w: arg %d, last %d, pos %d", ErrInvalidSectionArgument, arg, len(metadata), i) } i += 2 } @@ -105,13 +105,13 @@ func validateCode(code []byte, section int, metadata []*FunctionMetadata, jt *Ju // Code sections may not "fall through" and require proper termination. // Therefore, the last instruction must be considered terminal. if !jt[op].terminal { - return e(ErrInvalidCodeTermination, i, "ends with op %s", op) + return fmt.Errorf("%w: end with %s, pos %d", ErrInvalidCodeTermination, op, i) } if paths, err := validateControlFlow(code, section, metadata, jt); err != nil { return err } else if paths != count { // TODO(matt): return actual position of unreacable code - return e(ErrUnreachableCode, 0, "") + return ErrUnreachableCode } return nil } @@ -127,10 +127,10 @@ func checkDest(code []byte, analysis *bitvec, imm, from, length int) error { offset := parseInt16(code[imm:]) dest := from + offset if dest < 0 || dest >= length { - return NewParseError(ErrInvalidJumpDest, imm, "relative offset out-of-bounds: offset %d, dest %d", offset, dest) + return fmt.Errorf("%w: out-of-bounds offset: offset %d, dest %d, pos %d", ErrInvalidJumpDest, offset, dest, imm) } if !analysis.codeSegment(uint64(dest)) { - return NewParseError(ErrInvalidJumpDest, imm, "relative offset into immediate value: offset %d, dest %d", offset, dest) + return fmt.Errorf("%w: offset into immediate: offset %d, dest %d, pos %d", ErrInvalidJumpDest, offset, dest, imm) } return nil } @@ -143,7 +143,6 @@ func validateControlFlow(code []byte, section int, metadata []*FunctionMetadata, height int } var ( - e = NewParseError heights = make(map[int]int) worklist = []item{{0, int(metadata[section].Input)}} maxStackHeight = int(metadata[section].Input) @@ -162,7 +161,7 @@ func validateControlFlow(code []byte, section int, metadata []*FunctionMetadata, // Check if pos has already be visited; if so, the stack heights should be the same. if want, ok := heights[pos]; ok { if height != want { - return 0, e(ErrConflictingStack, pos, "have %d, want %d", height, want) + return 0, fmt.Errorf("%w: have %d, want %d", ErrConflictingStack, height, want) } // Already visited this path and stack height // matches. @@ -171,29 +170,29 @@ func validateControlFlow(code []byte, section int, metadata []*FunctionMetadata, heights[pos] = height // Validate height for current op and update as needed. - if jt[op].minStack > height { - return 0, e(ErrStackUnderflow{stackLen: height, required: jt[op].minStack}, pos, "") + if want, have := jt[op].minStack, height; want > have { + return 0, fmt.Errorf("%w: at pos %d", ErrStackUnderflow{stackLen: have, required: want}, pos) } - if jt[op].maxStack < height { - return 0, e(ErrStackOverflow{stackLen: height, limit: jt[op].maxStack}, pos, "") + if want, have := jt[op].maxStack, height; want < have { + return 0, fmt.Errorf("%w: at pos %d", ErrStackOverflow{stackLen: have, limit: want}, pos) } height += int(params.StackLimit) - jt[op].maxStack switch { case op == CALLF: arg, _ := parseUint16(code[pos+1:]) - if metadata[arg].Input > uint8(height) { - return 0, e(ErrStackUnderflow{stackLen: height, required: int(metadata[arg].Input)}, pos, "CALLF underflow to section %d", arg) + if want, have := int(metadata[arg].Input), height; want > have { + return 0, fmt.Errorf("%w: at pos %d", ErrStackUnderflow{stackLen: have, required: want}, pos) } - if int(metadata[arg].Output)+height > int(params.StackLimit) { - return 0, e(ErrStackOverflow{stackLen: int(metadata[arg].Output) + height, limit: int(params.StackLimit)}, pos, "CALLF overflow to section %d") + if have, limit := int(metadata[arg].Output)+height, int(params.StackLimit); have > limit { + return 0, fmt.Errorf("%w: at pos %d", ErrStackOverflow{stackLen: have, limit: limit}, pos) } height -= int(metadata[arg].Input) height += int(metadata[arg].Output) pos += 3 case op == RETF: - if int(metadata[section].Output) != height { - return 0, e(ErrInvalidOutputs, pos, "have %d, want %d", height, metadata[section].Output) + if have, want := int(metadata[section].Output), height; have != want { + return 0, fmt.Errorf("%w: have %d, want %d, at pos %d", ErrInvalidOutputs, have, want, pos) } break outer case op == RJUMP: @@ -224,7 +223,7 @@ func validateControlFlow(code []byte, section int, metadata []*FunctionMetadata, } } if maxStackHeight != int(metadata[section].MaxStackHeight) { - return 0, e(ErrInvalidMaxStackHeight, 0, "at code section %d, have %d, want %d", section, maxStackHeight, metadata[section].MaxStackHeight) + return 0, fmt.Errorf("%w in code section %d: have %d, want %d", ErrInvalidMaxStackHeight, section, maxStackHeight, metadata[section].MaxStackHeight) } return len(heights), nil } From e60f7ec6e136a9d7d0276a2336f4e15d55f883f6 Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Fri, 13 Jan 2023 10:51:57 -0700 Subject: [PATCH 49/53] core/vm: don't use iota to figure out eip-4750 opcodes --- core/vm/opcodes.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/vm/opcodes.go b/core/vm/opcodes.go index 695063e30411..9efc42ba7608 100644 --- a/core/vm/opcodes.go +++ b/core/vm/opcodes.go @@ -209,8 +209,8 @@ const ( // 0xb0 range - control flow ops. const ( - CALLF = 0xb0 + iota - RETF + CALLF = 0xb0 + RETF = 0xb1 ) // 0xf0 range - closures. From 55bff2b5cc2b20794d95cebca71f1ce353096460 Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Fri, 13 Jan 2023 10:56:33 -0700 Subject: [PATCH 50/53] core/vm: remove unused function --- core/vm/eof.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/core/vm/eof.go b/core/vm/eof.go index a185c7bb1adc..249655d106d5 100644 --- a/core/vm/eof.go +++ b/core/vm/eof.go @@ -306,14 +306,6 @@ func max(a, b int) int { return a } -// min returns the minimum value between x and y. -func min(a, b int) int { - if a < b { - return a - } - return b -} - // sum computes the sum of a slice. func sum(list []int) (s int) { for _, n := range list { From a1f2a179dc010eb67c456eaa4a36b69bef6435f4 Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Tue, 17 Jan 2023 07:53:32 -0700 Subject: [PATCH 51/53] core/vm: change formatter in error msg for invalid magic --- core/vm/eof.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/vm/eof.go b/core/vm/eof.go index 249655d106d5..23de82b5f663 100644 --- a/core/vm/eof.go +++ b/core/vm/eof.go @@ -124,7 +124,7 @@ func (c *Container) MarshalBinary() []byte { // UnmarshalBinary decodes an EOF container. func (c *Container) UnmarshalBinary(b []byte) error { if !hasEOFMagic(b) { - return fmt.Errorf("%w: want %s", ErrInvalidMagic, eofMagic) + return fmt.Errorf("%w: want %x", ErrInvalidMagic, eofMagic) } if len(b) < 14 { return io.ErrUnexpectedEOF From e20edd6a1f5cdd1027db8913c3e3940592377c1e Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Tue, 17 Jan 2023 09:11:36 -0700 Subject: [PATCH 52/53] core/vm: bump nonce for create tx with invalid eof --- core/blockchain_test.go | 34 ++++++++++++++++++++++++++++++---- core/state_transition.go | 8 ++++++++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/core/blockchain_test.go b/core/blockchain_test.go index de2e942e1cc7..922c6242b694 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -4491,7 +4491,7 @@ func TestEOF(t *testing.T) { _, blocks, _ := GenerateChainWithGenesis(gspec, engine, 1, func(i int, b *BlockGen) { b.SetCoinbase(aa) - // execute flag contract + // 0: execute flag contract txdata := &types.DynamicFeeTx{ ChainID: gspec.Config.ChainID, Nonce: 0, @@ -4506,7 +4506,7 @@ func TestEOF(t *testing.T) { tx, _ = types.SignTx(tx, signer, key) b.AddTx(tx) - // deploy eof contract from eoa + // 1: deploy eof contract from eoa txdata = &types.DynamicFeeTx{ ChainID: gspec.Config.ChainID, Nonce: 1, @@ -4521,10 +4521,31 @@ func TestEOF(t *testing.T) { tx, _ = types.SignTx(tx, signer, key) b.AddTx(tx) - // deploy eof contract from create contract + // 2: invalid initcode in create tx, should be valid and use all gas + invalid := (&vm.Container{ + Types: []*vm.FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 1}}, + Code: [][]byte{common.Hex2Bytes("604200")}, + Data: []byte{0x01, 0x02, 0x03}, + }).MarshalBinary() + invalid[2] = 0x02 // make version invalid txdata = &types.DynamicFeeTx{ ChainID: gspec.Config.ChainID, Nonce: 2, + To: nil, + Gas: 500000, + GasFeeCap: newGwei(5), + GasTipCap: big.NewInt(2), + AccessList: nil, + Data: invalid, + } + tx = types.NewTx(txdata) + tx, _ = types.SignTx(tx, signer, key) + b.AddTx(tx) + + // 3: deploy eof contract from create contract + txdata = &types.DynamicFeeTx{ + ChainID: gspec.Config.ChainID, + Nonce: 3, To: &bb, Gas: 500000, GasFeeCap: newGwei(5), @@ -4539,7 +4560,7 @@ func TestEOF(t *testing.T) { // deploy eof contract from create2 contract txdata = &types.DynamicFeeTx{ ChainID: gspec.Config.ChainID, - Nonce: 3, + Nonce: 4, To: &cc, Gas: 500000, GasFeeCap: newGwei(5), @@ -4567,6 +4588,11 @@ func TestEOF(t *testing.T) { } } + r := chain.GetReceiptsByHash(blocks[0].Hash())[2] + if got, want := r.GasUsed, blocks[0].Transactions()[2].Gas(); r.Status == types.ReceiptStatusFailed && got != want { + t.Fatalf("gas accounting invalid for create tx with invalid initcode: gasUsed %d, gasLimit %d", got, want) + } + // Check various deployment mechanisms. if !bytes.Equal(state.GetCode(crypto.CreateAddress(addr, 1)), deployCode) { t.Fatalf("failed to deploy EOF with EOA") diff --git a/core/state_transition.go b/core/state_transition.go index 653c6b183618..22ba1084d55b 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -17,6 +17,7 @@ package core import ( + "errors" "fmt" "math" "math/big" @@ -353,6 +354,13 @@ func (st *StateTransition) TransitionDb() (*ExecutionResult, error) { ) if contractCreation { ret, _, st.gas, vmerr = st.evm.Create(sender, st.data, st.gas, st.value) + // Special case for EOF, if the initcode or deployed code is + // invalid, the tx is considered valid (so update nonce), but + // is to be treated as an exceptional abort (so burn all gas). + if errors.Is(vmerr, vm.ErrInvalidEOF) { + st.gas = 0 + st.state.SetNonce(msg.From(), st.state.GetNonce(sender.Address())+1) + } } else { // Increment the nonce for the next transaction st.state.SetNonce(msg.From(), st.state.GetNonce(sender.Address())+1) From df2388894ca0cf72acb28d66f235c51ce612a404 Mon Sep 17 00:00:00 2001 From: "lightclient@protonmail.com" Date: Thu, 19 Jan 2023 08:55:08 -0700 Subject: [PATCH 53/53] core/vm: don't double bump nonce when deployed eof is invalid in create tx --- core/blockchain_test.go | 66 ++++++++++++++++++++++++++++------------ core/state_transition.go | 2 +- core/vm/errors.go | 1 + core/vm/evm.go | 4 +-- 4 files changed, 50 insertions(+), 23 deletions(-) diff --git a/core/blockchain_test.go b/core/blockchain_test.go index 922c6242b694..9a1f183156a9 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -4472,20 +4472,22 @@ func TestEOF(t *testing.T) { Code: [][]byte{{byte(vm.STOP)}}, Data: nil, } - deployCode := container.MarshalBinary() - - initCode := []byte{ - byte(vm.PUSH1), byte(len(deployCode)), // len - byte(vm.PUSH1), 0x0c, // offset - byte(vm.PUSH1), 0x00, // dst offset - byte(vm.CODECOPY), - - // code in memory - byte(vm.PUSH1), byte(len(deployCode)), // size - byte(vm.PUSH1), 0x00, // offset - byte(vm.RETURN), - } - initCode = append(initCode, deployCode...) + deployed := container.MarshalBinary() + makeDeployCode := func(b []byte) []byte { + out := []byte{ + byte(vm.PUSH1), byte(len(b)), // len + byte(vm.PUSH1), 0x0c, // offset + byte(vm.PUSH1), 0x00, // dst offset + byte(vm.CODECOPY), + + // code in memory + byte(vm.PUSH1), byte(len(b)), // size + byte(vm.PUSH1), 0x00, // offset + byte(vm.RETURN), + } + return append(out, b...) + } + initCode := makeDeployCode(deployed) initHash := crypto.Keccak256Hash(initCode) _, blocks, _ := GenerateChainWithGenesis(gspec, engine, 1, func(i int, b *BlockGen) { @@ -4542,10 +4544,30 @@ func TestEOF(t *testing.T) { tx, _ = types.SignTx(tx, signer, key) b.AddTx(tx) - // 3: deploy eof contract from create contract + // 3: invalid deployed eof in create tx, should be valid and use all gas + inner := (&vm.Container{ + Types: []*vm.FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 0}}, + Code: [][]byte{common.Hex2Bytes("0000")}, + }).MarshalBinary() + invalid = makeDeployCode(inner) txdata = &types.DynamicFeeTx{ ChainID: gspec.Config.ChainID, Nonce: 3, + To: nil, + Gas: 500000, + GasFeeCap: newGwei(5), + GasTipCap: big.NewInt(2), + AccessList: nil, + Data: invalid, + } + tx = types.NewTx(txdata) + tx, _ = types.SignTx(tx, signer, key) + b.AddTx(tx) + + // 4: deploy eof contract from create contract + txdata = &types.DynamicFeeTx{ + ChainID: gspec.Config.ChainID, + Nonce: 4, To: &bb, Gas: 500000, GasFeeCap: newGwei(5), @@ -4557,10 +4579,10 @@ func TestEOF(t *testing.T) { tx, _ = types.SignTx(tx, signer, key) b.AddTx(tx) - // deploy eof contract from create2 contract + // 5: deploy eof contract from create2 contract txdata = &types.DynamicFeeTx{ ChainID: gspec.Config.ChainID, - Nonce: 4, + Nonce: 5, To: &cc, Gas: 500000, GasFeeCap: newGwei(5), @@ -4592,15 +4614,19 @@ func TestEOF(t *testing.T) { if got, want := r.GasUsed, blocks[0].Transactions()[2].Gas(); r.Status == types.ReceiptStatusFailed && got != want { t.Fatalf("gas accounting invalid for create tx with invalid initcode: gasUsed %d, gasLimit %d", got, want) } + r = chain.GetReceiptsByHash(blocks[0].Hash())[3] + if got, want := r.GasUsed, blocks[0].Transactions()[3].Gas(); r.Status == types.ReceiptStatusFailed && got != want { + t.Fatalf("gas accounting invalid for create tx with invalid deployed code: gasUsed %d, gasLimit %d", got, want) + } // Check various deployment mechanisms. - if !bytes.Equal(state.GetCode(crypto.CreateAddress(addr, 1)), deployCode) { + if !bytes.Equal(state.GetCode(crypto.CreateAddress(addr, 1)), deployed) { t.Fatalf("failed to deploy EOF with EOA") } - if !bytes.Equal(state.GetCode(crypto.CreateAddress(bb, 0)), deployCode) { + if !bytes.Equal(state.GetCode(crypto.CreateAddress(bb, 0)), deployed) { t.Fatalf("failed to deploy EOF with CREATE") } - if !bytes.Equal(state.GetCode(crypto.CreateAddress2(cc, [32]byte{}, initHash.Bytes())), deployCode) { + if !bytes.Equal(state.GetCode(crypto.CreateAddress2(cc, [32]byte{}, initHash.Bytes())), deployed) { t.Fatalf("failed to deploy EOF with CREATE2") } } diff --git a/core/state_transition.go b/core/state_transition.go index 22ba1084d55b..9f84882eafda 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -357,7 +357,7 @@ func (st *StateTransition) TransitionDb() (*ExecutionResult, error) { // Special case for EOF, if the initcode or deployed code is // invalid, the tx is considered valid (so update nonce), but // is to be treated as an exceptional abort (so burn all gas). - if errors.Is(vmerr, vm.ErrInvalidEOF) { + if errors.Is(vmerr, vm.ErrInvalidEOFInitcode) { st.gas = 0 st.state.SetNonce(msg.From(), st.state.GetNonce(sender.Address())+1) } diff --git a/core/vm/errors.go b/core/vm/errors.go index f1ae2a302c53..33eb70b4280c 100644 --- a/core/vm/errors.go +++ b/core/vm/errors.go @@ -38,6 +38,7 @@ var ( ErrLegacyCode = errors.New("invalid code: EOF contract must not deploy legacy code") ErrInvalidCode = errors.New("invalid code: must not begin with 0xef") ErrInvalidEOF = errors.New("invalid eof") + ErrInvalidEOFInitcode = errors.New("invalid eof initcode") ErrNonceUintOverflow = errors.New("nonce uint64 overflow") // errStopToken is an internal token indicating interpreter loop termination, diff --git a/core/vm/evm.go b/core/vm/evm.go index f70aeebf9e29..f17899193131 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -430,10 +430,10 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, // If the initcode is EOF, verify it is well-formed. var c Container if err := c.UnmarshalBinary(codeAndHash.code); err != nil { - return nil, common.Address{}, gas, fmt.Errorf("%w: %v", ErrInvalidEOF, err) + return nil, common.Address{}, gas, fmt.Errorf("%w: %v", ErrInvalidEOFInitcode, err) } if err := c.ValidateCode(evm.interpreter.cfg.JumpTableEOF); err != nil { - return nil, common.Address{}, gas, fmt.Errorf("%w: %v", ErrInvalidEOF, err) + return nil, common.Address{}, gas, fmt.Errorf("%w: %v", ErrInvalidEOFInitcode, err) } contract.Container = &c } else if fromEOF {