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)
+ }
+ }
+}