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/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/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/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/blockchain_test.go b/core/blockchain_test.go index 36bfa0752558..9a1f183156a9 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -17,6 +17,7 @@ package core import ( + "bytes" "errors" "fmt" "math/big" @@ -4332,3 +4333,300 @@ 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 ( + 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() + 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{ + addr: {Balance: funds}, + bb: {Code: createDeployer, Balance: big.NewInt(0)}, + cc: {Code: create2Deployer, Balance: big.NewInt(0)}, + aa: { + Code: (&vm.Container{ + Types: []*vm.FunctionMetadata{ + {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.CALLF), + byte(0), + byte(3), + byte(vm.RETF), + }, + { + 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) + + container := vm.Container{ + Types: []*vm.FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 0}}, + Code: [][]byte{{byte(vm.STOP)}}, + Data: nil, + } + 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) { + b.SetCoinbase(aa) + + // 0: execute flag contract + 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, key) + b.AddTx(tx) + + // 1: 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) + + // 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: 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), + GasTipCap: big.NewInt(2), + AccessList: nil, + Data: initCode, + } + tx = types.NewTx(txdata) + tx, _ = types.SignTx(tx, signer, key) + b.AddTx(tx) + + // 5: deploy eof contract from create2 contract + txdata = &types.DynamicFeeTx{ + ChainID: gspec.Config.ChainID, + Nonce: 5, + 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) + 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) + } + + // 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) + } + } + + 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) + } + 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)), deployed) { + t.Fatalf("failed to deploy EOF with EOA") + } + 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())), deployed) { + t.Fatalf("failed to deploy EOF with CREATE2") + } +} diff --git a/core/state_transition.go b/core/state_transition.go index 653c6b183618..9f84882eafda 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.ErrInvalidEOFInitcode) { + 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) diff --git a/core/vm/analysis.go b/core/vm/analysis.go index 4aa8cfe70f11..37a109d8e03f 100644 --- a/core/vm/analysis.go +++ b/core/vm/analysis.go @@ -116,3 +116,87 @@ 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 eofCodeBitmapInternal(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)); { + var ( + op = OpCode(code[pc]) + numbits uint8 + ) + pc++ + + switch { + case op >= PUSH1 && op <= PUSH32: + numbits = uint8(op - PUSH1 + 1) + 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. + 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) > end { + // Jump table is truncated, mark as many bits + // as possible. + numbits = uint8(end - pc) + } + default: + // Op had no immediate operand, continue. + 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/analysis_test.go b/core/vm/analysis_test.go index 398861f8ae7d..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" ) @@ -56,6 +57,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]) + } } } @@ -107,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) + }) +} 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..a0e22c163c81 100644 --- a/core/vm/eips.go +++ b/core/vm/eips.go @@ -20,6 +20,8 @@ import ( "fmt" "sort" + "encoding/binary" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/params" "github.com/holiman/uint256" @@ -241,3 +243,134 @@ 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), + undefined: true, + } + 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), + terminal: true, + } + 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), + terminal: true, + } +} + +// 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, overflow := idx.Uint64WithOverflow(); overflow || idx >= 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 = binary.BigEndian.Uint16(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 + + // If returning from top frame, exit cleanly. + if len(scope.ReturnStack) == 0 { + return nil, errStopToken + } + return nil, nil +} diff --git a/core/vm/eof.go b/core/vm/eof.go new file mode 100644 index 000000000000..23de82b5f663 --- /dev/null +++ b/core/vm/eof.go @@ -0,0 +1,315 @@ +// 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" + "encoding/binary" + "errors" + "fmt" + "io" +) + +const ( + offsetVersion = 2 + offsetTypesKind = 3 + offsetCodeKind = 6 + + kindTypes = 1 + kindCode = 2 + kindData = 3 + + eofFormatByte = 0xef + eof1Version = 1 + + maxInputItems = 127 + maxOutputItems = 127 + maxStackHeight = 1023 +) + +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("invalid container size") +) + +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 an EOF container object. +type Container struct { + Types []*FunctionMetadata + Code [][]byte + Data []byte +} + +// FunctionMetadata is an EOF function signature. +type FunctionMetadata struct { + Input uint8 + Output uint8 + MaxStackHeight uint16 +} + +// 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 = append(b, kindCode) + b = binary.BigEndian.AppendUint16(b, uint16(len(c.Code))) + for _, code := range c.Code { + b = binary.BigEndian.AppendUint16(b, uint16(len(code))) + } + b = append(b, kindData) + 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)}...) + } + 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 !hasEOFMagic(b) { + return fmt.Errorf("%w: want %x", ErrInvalidMagic, eofMagic) + } + if len(b) < 14 { + return io.ErrUnexpectedEOF + } + if !isEOFVersion1(b) { + return fmt.Errorf("%w: have %d, want %d", ErrInvalidVersion, b[2], eof1Version) + } + + var ( + kind, typesSize, dataSize int + codeSizes []int + err error + ) + + // Parse type section header. + kind, typesSize, err = parseSection(b, offsetTypesKind) + if err != nil { + return err + } + if kind != kindTypes { + return fmt.Errorf("%w: found section kind %x instead", ErrMissingTypeHeader, kind) + } + if typesSize < 4 || typesSize%4 != 0 { + return fmt.Errorf("%w: type section size must be divisible by 4, have %d", ErrInvalidTypeSize, typesSize) + } + if typesSize/4 > 1024 { + return fmt.Errorf("%w: type section must not exceed 4*1024, have %d", ErrInvalidTypeSize, typesSize) + } + + // Parse code section header. + kind, codeSizes, err = parseSectionList(b, offsetCodeKind) + if err != nil { + return err + } + if kind != kindCode { + return fmt.Errorf("%w: found section kind %x instead", ErrMissingCodeHeader, kind) + } + if len(codeSizes) != typesSize/4 { + 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. + offsetDataKind := offsetCodeKind + 2 + 2*len(codeSizes) + 1 + kind, dataSize, err = parseSection(b, offsetDataKind) + if err != nil { + return err + } + if kind != kindData { + return fmt.Errorf("%w: found section %x instead", ErrMissingDataHeader, kind) + } + + // Check for terminator. + offsetTerminator := offsetDataKind + 3 + if len(b) < offsetTerminator { + return io.ErrUnexpectedEOF + } + if b[offsetTerminator] != 0 { + 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 fmt.Errorf("%w: have %d, want %d", ErrInvalidContainerSize, len(b), expectedSize) + } + + // Parse types section. + idx := offsetTerminator + 1 + var types []*FunctionMetadata + for i := 0; i < typesSize/4; i++ { + sig := &FunctionMetadata{ + Input: b[idx+i*4], + Output: b[idx+i*4+1], + MaxStackHeight: binary.BigEndian.Uint16(b[idx+i*4+2:]), + } + if sig.Input > maxInputItems { + return fmt.Errorf("%w for section %d: have %d", ErrTooManyInputs, i, sig.Input) + } + if sig.Output > maxOutputItems { + return fmt.Errorf("%w for section %d: have %d", ErrTooManyOutputs, i, sig.Output) + } + if sig.MaxStackHeight > 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 fmt.Errorf("%w: have %d, %d", ErrInvalidSection0Type, types[0].Input, types[0].Output) + } + c.Types = types + + // Parse code sections. + idx += typesSize + code := make([][]byte, len(codeSizes)) + for i, size := range codeSizes { + if size == 0 { + return fmt.Errorf("%w for section %d: size must not be 0", ErrInvalidCodeSize, i) + } + code[i] = b[idx : idx+size] + idx += size + } + c.Code = code + + // Parse data section. + c.Data = b[idx : idx+dataSize] + + 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 { + return err + } + } + return nil +} + +// parseSection decodes a (kind, size) pair from an EOF header. +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, 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, error) { + if len(b) < idx+2 { + return nil, io.ErrUnexpectedEOF + } + 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++ { + list[i] = int(binary.BigEndian.Uint16(b[idx+2+2*i:])) + } + return list, nil +} + +// parseUint16 parses a 16 bit unsigned integer. +func parseUint16(b []byte) (int, error) { + if len(b) < 2 { + return 0, io.ErrUnexpectedEOF + } + return int(binary.BigEndian.Uint16(b)), nil +} + +// 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 a +} + +// sum computes the sum of a slice. +func sum(list []int) (s int) { + for _, n := range list { + s += n + } + return +} diff --git a/core/vm/eof_test.go b/core/vm/eof_test.go new file mode 100644 index 000000000000..e8638128c2ed --- /dev/null +++ b/core/vm/eof_test.go @@ -0,0 +1,66 @@ +// 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: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 1}}, + Code: [][]byte{common.Hex2Bytes("604200")}, + Data: []byte{0x01, 0x02, 0x03}, + }, + }, + { + want: Container{ + Types: []*FunctionMetadata{ + {Input: 0, Output: 0, MaxStackHeight: 1}, + {Input: 2, Output: 3, MaxStackHeight: 4}, + {Input: 1, Output: 1, MaxStackHeight: 1}, + }, + Code: [][]byte{ + common.Hex2Bytes("604200"), + common.Hex2Bytes("6042604200"), + common.Hex2Bytes("00"), + }, + Data: []byte{}, + }, + }, + } { + var ( + 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) + } + if !reflect.DeepEqual(got, test.want) { + t.Fatalf("test %d: got %+v, want %+v", i, got, test.want) + } + } +} diff --git a/core/vm/errors.go b/core/vm/errors.go index fbbf19e178bf..33eb70b4280c 100644 --- a/core/vm/errors.go +++ b/core/vm/errors.go @@ -35,7 +35,10 @@ 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") + ErrInvalidEOFInitcode = errors.New("invalid eof initcode") ErrNonceUintOverflow = errors.New("nonce uint64 overflow") // errStopToken is an internal token indicating interpreter loop termination, @@ -50,10 +53,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 { @@ -61,10 +68,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/evm.go b/core/vm/evm.go index 149e9f761be3..f17899193131 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -17,7 +17,9 @@ package vm import ( + "fmt" "math/big" + "strings" "sync/atomic" "github.com/ethereum/go-ethereum/common" @@ -223,7 +225,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.parseContainer(code)) ret, err = evm.interpreter.Run(contract, input, false) gas = contract.Gas } @@ -277,10 +279,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.parseContainer(code)) ret, err = evm.interpreter.Run(contract, input, false) gas = contract.Gas } @@ -318,9 +321,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.parseContainer(code)) ret, err = evm.interpreter.Run(contract, input, false) gas = contract.Gas } @@ -372,8 +376,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.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. @@ -402,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) { @@ -411,6 +416,32 @@ 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 } + + // 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. If + // the initcode is EOF, contract.Container will be set. + contract := NewContract(caller, AccountRef(address), value, gas) + contract.SetCodeOptionalHash(&address, codeAndHash) + + // Validate initcode per EOF rules. If caller is EOF and initcode is legacy, fail. + isInitcodeEOF := hasEOFMagic(codeAndHash.code) + if evm.chainRules.IsShanghai { + 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("%w: %v", ErrInvalidEOFInitcode, err) + } + if err := c.ValidateCode(evm.interpreter.cfg.JumpTableEOF); err != nil { + return nil, common.Address{}, gas, fmt.Errorf("%w: %v", ErrInvalidEOFInitcode, 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 @@ -426,6 +457,7 @@ 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 } + // Create a new account on the state snapshot := evm.StateDB.Snapshot() evm.StateDB.CreateAccount(address) @@ -434,11 +466,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) @@ -454,9 +481,24 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, err = ErrMaxCodeSizeExceeded } + // Reject legacy contract deployment from EOF. + 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 && evm.chainRules.IsLondon { - err = ErrInvalidCode + if err == nil && len(ret) >= 1 && HasEOFByte(ret) { + if evm.chainRules.IsShanghai { + var c Container + if err = c.UnmarshalBinary(ret); err == nil { + err = c.ValidateCode(evm.interpreter.cfg.JumpTableEOF) + } + if err != nil { + err = fmt.Errorf("%w: %v", ErrInvalidEOF, err) + } + } else if evm.chainRules.IsLondon { + err = ErrInvalidCode + } } // if the contract creation ran successfully and no errors were returned @@ -495,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 := hasEOFMagic(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. @@ -505,8 +548,24 @@ 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 := hasEOFMagic(evm.StateDB.GetCode(caller.Address())) + return evm.create(caller, codeAndHash, gas, endowment, contractAddr, CREATE2, isEOF) } // ChainConfig returns the environment's chain configuration func (evm *EVM) ChainConfig() *params.ChainConfig { return evm.chainConfig } + +// 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 && strings.HasPrefix(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\ncode: %s\n", err, common.Bytes2Hex(b))) + } + 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..69661221885f 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 ( @@ -56,6 +62,7 @@ var ( londonInstructionSet = newLondonInstructionSet() mergeInstructionSet = newMergeInstructionSet() shanghaiInstructionSet = newShanghaiInstructionSet() + shanghaiEOFInstructionSet = newShanghaiEOFInstructionSet() ) // JumpTable contains the EVM opcodes supported at a given fork. @@ -79,6 +86,10 @@ func validate(jt JumpTable) JumpTable { return jt } +func NewShanghaiEOFInstructionSetForTesting() JumpTable { + return newShanghaiEOFInstructionSet() +} + func newShanghaiInstructionSet() JumpTable { instructionSet := newMergeInstructionSet() enable3855(&instructionSet) // PUSH0 instruction @@ -86,6 +97,12 @@ func newShanghaiInstructionSet() JumpTable { return validate(instructionSet) } +func newShanghaiEOFInstructionSet() JumpTable { + instructionSet := newShanghaiInstructionSet() + enableEOF(&instructionSet) + return validate(instructionSet) +} + func newMergeInstructionSet() JumpTable { instructionSet := newLondonInstructionSet() instructionSet[PREVRANDAO] = &operation{ @@ -197,6 +214,7 @@ func newByzantiumInstructionSet() JumpTable { minStack: minStack(2, 0), maxStack: maxStack(2, 0), memorySize: memoryRevert, + terminal: true, } return validate(instructionSet) } @@ -245,6 +263,7 @@ func newFrontierInstructionSet() JumpTable { constantGas: 0, minStack: minStack(0, 0), maxStack: maxStack(0, 0), + terminal: true, }, ADD: { execute: opAdd, @@ -1033,6 +1052,7 @@ func newFrontierInstructionSet() JumpTable { minStack: minStack(2, 0), maxStack: maxStack(2, 0), memorySize: memoryReturn, + terminal: true, }, SELFDESTRUCT: { execute: opSelfdestruct, @@ -1040,12 +1060,18 @@ 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. 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/opcodes.go b/core/vm/opcodes.go index 9f199eb8f60a..9efc42ba7608 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,12 @@ const ( LOG4 ) +// 0xb0 range - control flow ops. +const ( + CALLF = 0xb0 + RETF = 0xb1 +) + // 0xf0 range - closures. const ( CREATE OpCode = 0xf0 @@ -304,6 +313,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 +392,8 @@ var opCodeToString = map[OpCode]string{ LOG4: "LOG4", // 0xb0 range. + CALLF: "CALLF", + RETF: "RETF", TLOAD: "TLOAD", TSTORE: "TSTORE", diff --git a/core/vm/validate.go b/core/vm/validate.go new file mode 100644 index 000000000000..fccdd6fa3eba --- /dev/null +++ b/core/vm/validate.go @@ -0,0 +1,229 @@ +// +// 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 ( + "errors" + "fmt" + "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 + // 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 + ) + // 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]) + if jt[op].undefined { + 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 fmt.Errorf("%w: op %s, pos %d", ErrTruncatedImmediate, op, i) + } + i += size + case op == RJUMP || op == RJUMPI: + if len(code) <= i+2 { + 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 + } + i += 2 + case op == RJUMPV: + if len(code) <= i+1 { + return fmt.Errorf("%w: jump table size missing, op %s, pos %d", ErrTruncatedImmediate, op, i) + } + count := int(code[i+1]) + if count == 0 { + return fmt.Errorf("%w: must not be 0, pos %d", ErrInvalidBranchCount, i) + } + if len(code) <= i+count { + 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 { + return err + } + } + i += 1 + 2*count + case op == CALLF: + if i+2 >= len(code) { + return fmt.Errorf("%w: op %s, pos %d", ErrTruncatedImmediate, op, i) + } + arg, _ := parseUint16(code[i+1:]) + if arg >= len(metadata) { + return fmt.Errorf("%w: arg %d, last %d, pos %d", ErrInvalidSectionArgument, arg, len(metadata), i) + } + i += 2 + } + 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("%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 ErrUnreachableCode + } + return nil +} + +// 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 io.ErrUnexpectedEOF + } + if analysis != nil && *analysis == nil { + *analysis = eofCodeBitmap(code) + } + offset := parseInt16(code[imm:]) + dest := from + offset + if dest < 0 || dest >= length { + return fmt.Errorf("%w: out-of-bounds offset: offset %d, dest %d, pos %d", ErrInvalidJumpDest, offset, dest, imm) + } + if !analysis.codeSegment(uint64(dest)) { + return fmt.Errorf("%w: offset into immediate: offset %d, dest %d, pos %d", ErrInvalidJumpDest, offset, dest, imm) + } + 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, error) { + type item struct { + pos int + height int + } + var ( + heights = make(map[int]int) + worklist = []item{{0, int(metadata[section].Input)}} + maxStackHeight = int(metadata[section].Input) + ) + for 0 < len(worklist) { + var ( + idx = len(worklist) - 1 + pos = worklist[idx].pos + height = worklist[idx].height + ) + worklist = worklist[:idx] + outer: + for pos < len(code) { + op := OpCode(code[pos]) + + // 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, fmt.Errorf("%w: have %d, want %d", ErrConflictingStack, height, want) + } + // Already visited this path and stack height + // matches. + break + } + heights[pos] = height + + // Validate height for current op and update as needed. + if want, have := jt[op].minStack, height; want > have { + return 0, fmt.Errorf("%w: at pos %d", ErrStackUnderflow{stackLen: have, required: want}, 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 want, have := int(metadata[arg].Input), height; want > have { + return 0, fmt.Errorf("%w: at pos %d", ErrStackUnderflow{stackLen: have, required: want}, pos) + } + 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 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: + arg := parseInt16(code[pos+1:]) + pos += 3 + arg + case op == RJUMPI: + arg := parseInt16(code[pos+1:]) + 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 + arg, height: height}) + } + pos += 2 + 2*count + default: + if op >= PUSH1 && op <= PUSH32 { + pos += 1 + int(op-PUSH0) + } else if jt[op].terminal { + break outer + } else { + // Simple op, no operand. + pos += 1 + } + } + maxStackHeight = max(maxStackHeight, height) + } + } + if maxStackHeight != int(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 +} diff --git a/core/vm/validate_test.go b/core/vm/validate_test.go new file mode 100644 index 000000000000..57114196e746 --- /dev/null +++ b/core/vm/validate_test.go @@ -0,0 +1,250 @@ +// 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 ( + "errors" + "testing" + + "github.com/ethereum/go-ethereum/common" +) + +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(CALLF), 0x00, 0x00, + byte(STOP), + }, + 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: 1}}, + }, + { + code: []byte{ + byte(CALLER), + byte(POP), + }, + section: 0, + metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 1}}, + err: ErrInvalidCodeTermination, + }, + { + code: []byte{ + byte(RJUMP), + byte(0x00), + byte(0x01), + byte(CALLER), + byte(STOP), + }, + section: 0, + metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 0}}, + err: ErrUnreachableCode, + }, + { + code: []byte{ + byte(PUSH1), + byte(0x42), + byte(ADD), + byte(STOP), + }, + section: 0, + metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 1}}, + err: ErrStackUnderflow{stackLen: 1, required: 2}, + }, + { + code: []byte{ + byte(PUSH1), + byte(0x42), + byte(POP), + byte(STOP), + }, + section: 0, + metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 2}}, + err: ErrInvalidMaxStackHeight, + }, + { + 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: ErrInvalidJumpDest, + }, + { + 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: ErrInvalidJumpDest, + }, + { + code: []byte{ + byte(PUSH0), + byte(RJUMPV), + byte(0x00), + byte(STOP), + }, + section: 0, + metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 1}}, + err: ErrInvalidBranchCount, + }, + { + 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}}, + }, + { + 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}}, + }, + { + 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}}, + }, + { + code: []byte{ + byte(STOP), + byte(STOP), + byte(INVALID), + }, + section: 0, + metadata: []*FunctionMetadata{{Input: 0, Output: 0, MaxStackHeight: 0}}, + err: ErrUnreachableCode, + }, + { + code: []byte{ + byte(RETF), + }, + section: 0, + metadata: []*FunctionMetadata{{Input: 0, Output: 1, MaxStackHeight: 0}}, + err: ErrInvalidOutputs, + }, + { + code: []byte{ + byte(RETF), + }, + 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}}, + }, + { + 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 !errors.Is(err, test.err) { + t.Errorf("test %d (%s): unexpected error (want: %v, got: %v)", i, common.Bytes2Hex(test.code), test.err, err) + } + } +}