core/vm: add eof container

This commit is contained in:
lightclient@protonmail.com 2022-12-06 14:23:27 -07:00 committed by Marius van der Wijden
parent 183e7b702a
commit 9354ef082b
22 changed files with 1942 additions and 98 deletions

163
cmd/evm/eofparser.go Normal file
View file

@ -0,0 +1,163 @@
// 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 <http://www.gnu.org/licenses/>.
package main
import (
"bufio"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"strings"
"github.com/ethereum/go-ethereum/core/vm"
"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 {
// 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
}

View file

@ -196,6 +196,9 @@ func Transition(ctx *cli.Context) error {
if err := applyCancunChecks(&prestate.Env, chainConfig); err != nil {
return err
}
if err := applyEOFChecks(&prestate, chainConfig); err != nil {
return err
}
// Run the test and aggregate the result
s, result, body, err := prestate.Apply(vmConfig, chainConfig, txIt, ctx.Int64(RewardFlag.Name), getTracer)
if err != nil {
@ -285,6 +288,29 @@ func applyCancunChecks(env *stEnv, chainConfig *params.ChainConfig) error {
return nil
}
func applyEOFChecks(prestate *Prestate, chainConfig *params.ChainConfig) error {
// Sanity check pre-allocated EOF code to not panic in state transition.
if chainConfig.IsShanghai(big.NewInt(int64(prestate.Env.Number)), prestate.Env.Timestamp) {
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))
}
}
}
}
return nil
}
type Alloc map[common.Address]types.Account
func (g Alloc) OnRoot(common.Hash) {}

View file

@ -21,10 +21,13 @@ 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/debug"
"github.com/ethereum/go-ethereum/internal/flags"
"github.com/ethereum/go-ethereum/tests"
"github.com/urfave/cli/v2"
// Force-load the tracer engines to trigger registration
@ -138,6 +141,26 @@ var (
Usage: "enable return data output",
Category: flags.VMCategory,
}
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 <forkname>(+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{
@ -222,6 +245,17 @@ var traceFlags = []cli.Flag{
DisableReturnDataFlag,
}
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{
HexFlag,
RefTestFlag,
},
}
var app = flags.NewApp("the evm command line interface")
func init() {
@ -235,6 +269,7 @@ func init() {
stateTransitionCommand,
transactionCommand,
blockBuilderCommand,
eofParserCommand,
}
app.Before = func(ctx *cli.Context) error {
flags.MigrateGlobalFlags(ctx)

View file

@ -284,8 +284,6 @@ func TestT8n(t *testing.T) {
input: t8nInput{
"alloc.json", "txs_more.rlp", "env.json", "Cancun", "",
},
output: t8nOutput{alloc: true, result: true},
expOut: "exp.json",
},
} {
args := []string{"t8n"}

View file

@ -18,6 +18,7 @@ package core
import (
"encoding/binary"
"bytes"
"errors"
"fmt"
"math/big"
@ -4422,9 +4423,6 @@ func TestRequests(t *testing.T) {
t.Fatalf("failed to create tester chain: %v", err)
}
defer chain.Stop()
if n, err := chain.InsertChain(blocks); err != nil {
t.Fatalf("block %d: failed to insert into chain: %v", n, err)
}
// Verify the withdrawal requests match.
block := chain.GetBlockByNumber(1)
@ -4476,6 +4474,352 @@ func TestRequests(t *testing.T) {
}
if want.TargetPublicKey != got.TargetPublicKey {
t.Fatalf("wrong target public key: want %s, got %s", common.Bytes2Hex(want.TargetPublicKey[:]), common.Bytes2Hex(got.TargetPublicKey[:]))
}// Verify the withdrawal requests match.
block := chain.GetBlockByNumber(1)
if block == nil {
t.Fatalf("failed to retrieve block 1")
}
// Verify the withdrawal requests match.
got := block.Requests()
if len(got) != 2 {
t.Fatalf("wrong number of withdrawal requests: wanted 2, got %d", len(got))
}
for i, want := range wxs {
got, ok := got[i].Inner().(*types.WithdrawalRequest)
if !ok {
t.Fatalf("expected withdrawal request")
}
if want.Source != got.Source {
t.Fatalf("wrong source address: want %s, got %s", want.Source, got.Source)
}
if want.PublicKey != got.PublicKey {
t.Fatalf("wrong public key: want %s, got %s", common.Bytes2Hex(want.PublicKey[:]), common.Bytes2Hex(got.PublicKey[:]))
}
if want.Amount != got.Amount {
t.Fatalf("wrong amount: want %d, got %d", want.Amount, got.Amount)
}
}
// Verify the consolidation requests match. Even though both requests are sent
// in block two, only one is dequeued at a time.
for i, want := range cxs {
block := chain.GetBlockByNumber(uint64(i + 2))
if block == nil {
t.Fatalf("failed to retrieve block")
}
requests := block.Requests()
if len(requests) != 1 {
t.Fatalf("wrong number of consolidation requests: wanted 1, got %d", len(got))
}
got, ok := requests[0].Inner().(*types.ConsolidationRequest)
if !ok {
t.Fatalf("expected consolidation request")
}
if want.Source != got.Source {
t.Fatalf("wrong source address: want %s, got %s", want.Source, got.Source)
}
if want.SourcePublicKey != got.SourcePublicKey {
t.Fatalf("wrong source public key: want %s, got %s", common.Bytes2Hex(want.SourcePublicKey[:]), common.Bytes2Hex(got.SourcePublicKey[:]))
}
if want.TargetPublicKey != got.TargetPublicKey {
t.Fatalf("wrong target public key: want %s, got %s", common.Bytes2Hex(want.TargetPublicKey[:]), common.Bytes2Hex(got.TargetPublicKey[:]))
}
}
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 = beacon.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.AllDevChainProtocolChanges,
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: 0x80, 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 = u64(0)
signer := types.LatestSigner(gspec.Config)
container := vm.Container{
Types: []*vm.FunctionMetadata{{Input: 0, Output: 0x80, 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: 0x80, 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: 0x80, 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{}, 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")
}
}

View file

@ -18,6 +18,7 @@ package core
import (
"bytes"
"errors"
"fmt"
"math"
"math/big"
@ -512,6 +513,13 @@ func (st *StateTransition) TransitionDb() (*ExecutionResult, error) {
)
if contractCreation {
ret, _, st.gasRemaining, vmerr = st.evm.Create(sender, msg.Data, st.gasRemaining, 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.gasRemaining = 0
st.state.SetNonce(msg.From, st.state.GetNonce(sender.Address())+1)
}
} else {
ret, st.gasRemaining, vmerr = st.evm.Call(sender, st.to(), msg.Data, st.gasRemaining, value)
}

View file

@ -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
}

View file

@ -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])
}
}
}
@ -105,3 +128,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)
})
}

View file

@ -52,10 +52,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
// is the execution frame represented by this object a contract deployment
IsDeployment bool
@ -144,11 +145,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
}
@ -193,10 +195,23 @@ func (c *Contract) Value() *uint256.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
}

View file

@ -21,6 +21,8 @@ import (
"math"
"sort"
"encoding/binary"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/params"
@ -557,3 +559,135 @@ func enable7702(jt *JumpTable) {
jt[DELEGATECALL].constantGas = params.WarmStorageReadCostEIP2929
jt[DELEGATECALL].dynamicGas = gasDelegateCallEIP7702
}
// 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
>>>>>>> 3532f16eb7 (core/vm: add eof container)
}

315
core/vm/eof.go Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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
}

66
core/vm/eof_test.go Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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)
}
}
}

View file

@ -36,7 +36,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,
@ -51,10 +54,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 {
@ -62,10 +69,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

View file

@ -18,7 +18,9 @@ package vm
import (
"errors"
"fmt"
"math/big"
"strings"
"sync/atomic"
"github.com/ethereum/go-ethereum/common"
@ -242,7 +244,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.ResolveCodeHash(addrCopy), code)
contract.SetCallCode(&addrCopy, evm.StateDB.ResolveCodeHash(addrCopy), code, evm.parseContainer(code))
ret, err = evm.interpreter.Run(contract, input, false)
gas = contract.Gas
}
@ -299,6 +301,7 @@ func (evm *EVM) CallCode(caller ContractRef, addr common.Address, input []byte,
ret, gas, err = RunPrecompiledContract(p, input, gas, evm.Config.Tracer)
} 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)
@ -306,7 +309,7 @@ func (evm *EVM) CallCode(caller ContractRef, addr common.Address, input []byte,
witness.AddCode(evm.StateDB.GetCode(addrCopy))
witness.AddCode(evm.StateDB.ResolveCode(addrCopy))
}
contract.SetCallCode(&addrCopy, evm.StateDB.ResolveCodeHash(addrCopy), evm.StateDB.ResolveCode(addrCopy))
contract.SetCallCode(&addrCopy, evm.StateDB.ResolveCodeHash(addrCopy), code, evm.parseContainer(code))
ret, err = evm.interpreter.Run(contract, input, false)
gas = contract.Gas
}
@ -351,13 +354,14 @@ func (evm *EVM) DelegateCall(caller ContractRef, addr common.Address, input []by
ret, gas, err = RunPrecompiledContract(p, input, gas, evm.Config.Tracer)
} 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()
if witness := evm.StateDB.Witness(); witness != nil {
witness.AddCode(evm.StateDB.GetCode(addrCopy))
witness.AddCode(evm.StateDB.ResolveCode(addrCopy))
}
contract.SetCallCode(&addrCopy, evm.StateDB.ResolveCodeHash(addrCopy), evm.StateDB.ResolveCode(addrCopy))
contract.SetCallCode(&addrCopy, evm.StateDB.ResolveCodeHash(addrCopy), code, evm.parseContainer(code))
ret, err = evm.interpreter.Run(contract, input, false)
gas = contract.Gas
}
@ -411,12 +415,13 @@ 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(uint256.Int), gas)
if witness := evm.StateDB.Witness(); witness != nil {
witness.AddCode(evm.StateDB.GetCode(addrCopy))
witness.AddCode(evm.StateDB.ResolveCode(addrCopy))
}
contract.SetCallCode(&addrCopy, evm.StateDB.ResolveCodeHash(addrCopy), evm.StateDB.ResolveCode(addrCopy))
contract.SetCallCode(&addrCopy, evm.StateDB.ResolveCodeHash(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.
@ -449,7 +454,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 *uint256.Int, address common.Address, typ OpCode) (ret []byte, createAddress common.Address, leftOverGas uint64, err error) {
func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, value *uint256.Int, address common.Address, typ OpCode, fromEOF bool) (ret []byte, createAddress common.Address, leftOverGas uint64, err error) {
if evm.Config.Tracer != nil {
evm.captureBegin(evm.depth, typ, caller.Address(), address, codeAndHash.code, gas, value.ToBig())
defer func(startGas uint64) {
@ -464,6 +469,33 @@ 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)
contract.IsDeployment = true
// 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.tableEOF); 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
@ -508,13 +540,71 @@ 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)
contract.IsDeployment = true
// Charge the contract creation init gas in verkle mode
if evm.chainRules.IsEIP4762 {
if !contract.UseGas(evm.AccessEvents.ContractCreateInitGas(address, value.Sign() != 0), evm.Config.Tracer, tracing.GasChangeWitnessContractInit) {
err = ErrOutOfGas
}
}
ret, err = evm.initNewContract(contract, address, value)
if err == nil {
ret, err = evm.interpreter.Run(contract, nil, false)
}
// Check whether the max code size has been exceeded, assign err if the case.
if err == nil && evm.chainRules.IsEIP158 && len(ret) > params.MaxCodeSize {
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 && HasEOFByte(ret) {
if evm.chainRules.IsShanghai {
var c Container
if err = c.UnmarshalBinary(ret); err == nil {
err = c.ValidateCode(evm.interpreter.tableEOF)
}
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
// calculate the gas required to store the code. If the code could not
// be stored due to not enough gas set an error and let it be handled
// by the error checking condition below.
if err == nil {
if !evm.chainRules.IsEIP4762 {
createDataGas := uint64(len(ret)) * params.CreateDataGas
if !contract.UseGas(createDataGas, evm.Config.Tracer, tracing.GasChangeCallCodeStorage) {
err = ErrCodeStoreOutOfGas
}
} else {
// Contract creation completed, touch the missing fields in the contract
if !contract.UseGas(evm.AccessEvents.AddAccount(address, true), evm.Config.Tracer, tracing.GasChangeWitnessContractCreation) {
err = ErrCodeStoreOutOfGas
}
if err == nil && len(ret) > 0 && !contract.UseGas(evm.AccessEvents.CodeChunksRangeGas(address, 0, uint64(len(ret)), uint64(len(ret)), true), evm.Config.Tracer, tracing.GasChangeWitnessCodeChunk) {
err = ErrCodeStoreOutOfGas
}
}
if err == nil {
evm.StateDB.SetCode(address, ret)
}
}
// 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.
if err != nil && (evm.chainRules.IsHomestead || err != ErrCodeStoreOutOfGas) {
evm.StateDB.RevertToSnapshot(snapshot)
if err != ErrExecutionReverted {
@ -524,55 +614,11 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64,
return ret, address, contract.Gas, err
}
// initNewContract runs a new contract's creation code, performs checks on the
// resulting code that is to be deployed, and consumes necessary gas.
func (evm *EVM) initNewContract(contract *Contract, address common.Address, value *uint256.Int) ([]byte, error) {
// Charge the contract creation init gas in verkle mode
if evm.chainRules.IsEIP4762 {
if !contract.UseGas(evm.AccessEvents.ContractCreateInitGas(address, value.Sign() != 0), evm.Config.Tracer, tracing.GasChangeWitnessContractInit) {
return nil, ErrOutOfGas
}
}
ret, err := evm.interpreter.Run(contract, nil, false)
if err != nil {
return ret, err
}
// Check whether the max code size has been exceeded, assign err if the case.
if evm.chainRules.IsEIP158 && len(ret) > params.MaxCodeSize {
return ret, ErrMaxCodeSizeExceeded
}
// Reject code starting with 0xEF if EIP-3541 is enabled.
if len(ret) >= 1 && ret[0] == 0xEF && evm.chainRules.IsLondon {
return ret, ErrInvalidCode
}
if !evm.chainRules.IsEIP4762 {
createDataGas := uint64(len(ret)) * params.CreateDataGas
if !contract.UseGas(createDataGas, evm.Config.Tracer, tracing.GasChangeCallCodeStorage) {
return ret, ErrCodeStoreOutOfGas
}
} else {
// Contract creation completed, touch the missing fields in the contract
if !contract.UseGas(evm.AccessEvents.AddAccount(address, true), evm.Config.Tracer, tracing.GasChangeWitnessContractCreation) {
return ret, ErrCodeStoreOutOfGas
}
if len(ret) > 0 && !contract.UseGas(evm.AccessEvents.CodeChunksRangeGas(address, 0, uint64(len(ret)), uint64(len(ret)), true), evm.Config.Tracer, tracing.GasChangeWitnessCodeChunk) {
return ret, ErrCodeStoreOutOfGas
}
}
evm.StateDB.SetCode(address, ret)
return ret, nil
}
// Create creates a new contract using code as deployment code.
func (evm *EVM) Create(caller ContractRef, code []byte, gas uint64, value *uint256.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.
@ -582,7 +628,8 @@ func (evm *EVM) Create(caller ContractRef, code []byte, gas uint64, value *uint2
func (evm *EVM) Create2(caller ContractRef, code []byte, gas uint64, endowment *uint256.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
@ -628,3 +675,18 @@ func (evm *EVM) GetVMContext() *tracing.VMContext {
StateDB: evm.StateDB,
}
}
// 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
}

View file

@ -24,6 +24,7 @@ import (
const (
GasQuickStep uint64 = 2
GasFastestStep uint64 = 3
GasFastishStep uint64 = 4
GasFastStep uint64 = 5
GasMidStep uint64 = 8
GasSlowStep uint64 = 10

View file

@ -969,12 +969,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())
}

View file

@ -117,7 +117,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))
}
@ -232,7 +232,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)
@ -259,7 +259,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)}
}
@ -295,7 +295,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)
)
@ -546,13 +546,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")
}
@ -576,7 +576,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})
}
}
@ -591,7 +591,7 @@ func TestOpTstore(t *testing.T) {
to = common.Address{1}
contractRef = contractRef{caller}
contract = NewContract(contractRef, AccountRef(to), new(uint256.Int), 0)
scopeContext = ScopeContext{mem, stack, contract}
scopeContext = ScopeContext{mem, stack, contract, 0, nil}
value = common.Hex2Bytes("abcdef00000000000000abba000000000deaf000000c0de00100000000133700")
)
@ -639,7 +639,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})
}
}
@ -734,7 +734,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))
}
@ -776,7 +776,7 @@ func TestBlobHash(t *testing.T) {
evmInterpreter = env.interpreter
)
stack.push(uint256.NewInt(tt.idx))
opBlobHash(&pc, evmInterpreter, &ScopeContext{nil, stack, nil})
opBlobHash(&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))
}
@ -917,7 +917,7 @@ func TestOpMCopy(t *testing.T) {
mem.Resize(memorySize)
}
// Do the copy
opMcopy(&pc, evmInterpreter, &ScopeContext{mem, stack, nil})
opMcopy(&pc, evmInterpreter, &ScopeContext{mem, stack, nil, 0, nil})
want := common.FromHex(strings.ReplaceAll(tc.want, " ", ""))
if have := mem.store; !bytes.Equal(want, have) {
t.Errorf("case %d: \nwant: %#x\nhave: %#x\n", i, want, have)

View file

@ -31,7 +31,7 @@ import (
type Config struct {
Tracer *tracing.Hooks
NoBaseFee bool // Forces the EIP-1559 baseFee to 0 (needed for 0 price calls)
EnablePreimageRecording bool // Enables recording of SHA3/keccak preimages
EnablePreimageRecording bool // Enables recording of SHA3/keccak preimage
ExtraEips []int // Additional EIPS that are to be enabled
EnableWitnessCollection bool // true if witness collection is enabled
}
@ -42,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
}
// MemoryData returns the underlying memory slice. Callers must not modify the contents
@ -85,8 +94,9 @@ func (ctx *ScopeContext) CallInput() []byte {
// EVMInterpreter represents an EVM interpreter
type EVMInterpreter struct {
evm *EVM
table *JumpTable
evm *EVM
table *JumpTable
tableEOF *JumpTable
hasher crypto.KeccakState // Keccak256 hasher instance shared across opcodes
hasherBuf common.Hash // Keccak256 hasher result array shared across opcodes
@ -142,7 +152,7 @@ func NewEVMInterpreter(evm *EVM) *EVMInterpreter {
}
}
evm.Config.ExtraEips = extraEips
return &EVMInterpreter{evm: evm, table: table}
return &EVMInterpreter{evm: evm, table: table, tableEOF: &shanghaiEOFInstructionSet}
}
// Run loops and evaluates the contract's code with the given input data and returns
@ -173,13 +183,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
@ -202,6 +215,12 @@ func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) (
}()
contract.Input = input
if contract.IsEOF() {
jt = in.tableEOF
} else {
jt = in.table
}
if debug {
defer func() { // this deferred method handles exit-with-error
if err == nil {
@ -234,8 +253,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.table[op]
op = contract.GetOp(pc, callContext.CodeSection)
operation := jt[op]
cost = operation.constantGas // For tracing
// Validate stack
if sLen := stack.len(); sLen < operation.minStack {

View file

@ -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 (
@ -58,6 +64,7 @@ var (
shanghaiInstructionSet = newShanghaiInstructionSet()
cancunInstructionSet = newCancunInstructionSet()
verkleInstructionSet = newVerkleInstructionSet()
shanghaiEOFInstructionSet = newShanghaiEOFInstructionSet()
)
// JumpTable contains the EVM opcodes supported at a given fork.
@ -98,6 +105,10 @@ func newCancunInstructionSet() JumpTable {
return validate(instructionSet)
}
func NewShanghaiEOFInstructionSetForTesting() JumpTable {
return newShanghaiEOFInstructionSet()
}
func newShanghaiInstructionSet() JumpTable {
instructionSet := newMergeInstructionSet()
enable3855(&instructionSet) // PUSH0 instruction
@ -106,6 +117,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{
@ -217,6 +234,7 @@ func newByzantiumInstructionSet() JumpTable {
minStack: minStack(2, 0),
maxStack: maxStack(2, 0),
memorySize: memoryRevert,
terminal: true,
}
return validate(instructionSet)
}
@ -265,6 +283,7 @@ func newFrontierInstructionSet() JumpTable {
constantGas: 0,
minStack: minStack(0, 0),
maxStack: maxStack(0, 0),
terminal: true,
},
ADD: {
execute: opAdd,
@ -1053,6 +1072,7 @@ func newFrontierInstructionSet() JumpTable {
minStack: minStack(2, 0),
maxStack: maxStack(2, 0),
memorySize: memoryReturn,
terminal: true,
},
SELFDESTRUCT: {
execute: opSelfdestruct,
@ -1060,12 +1080,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}
}
}

View file

@ -121,6 +121,9 @@ const (
TLOAD OpCode = 0x5c
TSTORE OpCode = 0x5d
MCOPY OpCode = 0x5e
RJUMP OpCode = 0x5c
RJUMPI OpCode = 0x5d
RJUMPV OpCode = 0x5e
PUSH0 OpCode = 0x5f
)
@ -209,6 +212,12 @@ const (
LOG4
)
// 0xb0 range - control flow ops.
const (
CALLF = 0xb0
RETF = 0xb1
)
// 0xf0 range - closures.
const (
CREATE OpCode = 0xf0
@ -305,7 +314,11 @@ var opCodeToString = [256]string{
TLOAD: "TLOAD",
TSTORE: "TSTORE",
MCOPY: "MCOPY",
PUSH0: "PUSH0",
// TODO (MariusVanDerWijden) reenable once moved
//RJUMP: "RJUMP",
//RJUMPI: "RJUMPI",
//RJUMPV: "RJUMPV",
PUSH0: "PUSH0",
// 0x60 range - pushes.
PUSH1: "PUSH1",
@ -384,6 +397,10 @@ var opCodeToString = [256]string{
LOG3: "LOG3",
LOG4: "LOG4",
// 0xb0 range.
CALLF: "CALLF",
RETF: "RETF",
// 0xf0 range - closures.
CREATE: "CREATE",
CALL: "CALL",

229
core/vm/validate.go Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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
}

250
core/vm/validate_test.go Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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)
}
}
}