core/vm, cmd/eofdump: implement eof validation

cmd/eofdump: benchmarks of eof validation speeds
core/vm: move eof instructions to separate file
core/vm: unexport fields

Co-authored-by: lightclient <lightclient@protonmail.com>
Co-authored-by: Marius van der Wijden <m.vanderwijden@live.de>
Co-authored-by: Danno Ferrin <danno.ferrin@shemnon.com>
This commit is contained in:
Martin Holst Swende 2024-09-08 20:12:08 +02:00
parent db6ae7fa12
commit 77484b4cf6
No known key found for this signature in database
GPG key ID: 683B438C05A5DDF0
26 changed files with 9303 additions and 166 deletions

248
cmd/eofdump/eofparser.go Normal file
View file

@ -0,0 +1,248 @@
// 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"
"io/fs"
"os"
"path/filepath"
"strings"
"sync/atomic"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/urfave/cli/v2"
)
func init() {
jt = vm.NewPragueEOFInstructionSetForTesting()
}
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,
}
initcode = "INITCODE"
)
type RefTests struct {
Vectors map[string]EOFTest `json:"vectors"`
}
type EOFTest struct {
Code string `json:"code"`
Results map[string]etResult `json:"results"`
ContainerKind string `json:"containerKind"`
}
type etResult struct {
Result bool `json:"result"`
Exception string `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), false); 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) {
var (
file = ctx.String(RefTestFlag.Name)
executedTests atomic.Int32
passedTests atomic.Int32
)
if info, err := os.Stat(file); err != nil {
return err
} else if !info.IsDir() {
src, err := os.ReadFile(file)
if err != nil {
return err
}
_, _, err = ExecuteTest(src)
return err
} else {
err = filepath.Walk(file, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
fmt.Printf("Executing Tests: %v\n", info.Name())
src, err := os.ReadFile(path)
if err != nil {
return err
}
passed, total, err := ExecuteTest(src)
passedTests.Add(int32(passed))
executedTests.Add(int32(total))
return err
})
if err != nil {
return err
}
fmt.Printf("Passed %v tests out of %v\n", passedTests.Load(), executedTests.Load())
return nil
}
}
// If neither are passed in, read input from stdin.
reader := bufio.NewReaderSize(os.Stdin, 1024*1024)
t, err := reader.ReadString('\n')
for err == nil {
l := len(t)
if l == 0 || t[0] == '#' {
continue
}
if t[l-1] == '\n' {
t = t[:l-1] // remove newline
}
if _, err := parseAndValidate(t, false); err != nil {
if err2 := errors.Unwrap(err); err2 != nil {
err = err2
}
fmt.Printf("err(%d): %v\n", errorMap[err.Error()], err)
} else {
fmt.Println("OK")
}
t, err = reader.ReadString('\n')
}
println(err.Error())
return nil
}
func ExecuteTest(src []byte) (int, int, error) {
var testsByName map[string]RefTests
if err := json.Unmarshal(src, &testsByName); err != nil {
return 0, 0, err
}
passed, total := 0, 0
for testsName, tests := range testsByName {
for name, tt := range tests.Vectors {
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, tt.ContainerKind == initcode)
if err2 := errors.Unwrap(err); err2 != nil {
err = err2
}
if r.Result && err != nil {
fmt.Fprintf(os.Stderr, "%s %s, %s: expected success, got %v\n", testsName, name, fork, err)
continue
}
if !r.Result && err == nil {
fmt.Fprintf(os.Stderr, "%s %s, %s: expected error %s, got %v\n", testsName, name, fork, r.Exception, err)
continue
}
/*
// TODO (MariusVanDerWijden) reenable once tests have a decent error format
if !r.Result && err != nil && r.Exception != 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 passed, total, nil
}
func parseAndValidate(s string, isInitCode bool) (*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)
}
return parse(b, isInitCode)
}
func parse(b []byte, isInitCode bool) (*vm.Container, error) {
var c vm.Container
if err := c.UnmarshalBinary(b, isInitCode); err != nil {
return nil, err
}
if err := c.ValidateCode(&jt, isInitCode); err != nil {
return nil, err
}
return &c, nil
}
func eofDump(ctx *cli.Context) error {
// If `--hex` is set, parse and validate the hex string argument.
if ctx.IsSet(HexFlag.Name) {
s := ctx.String(HexFlag.Name)
if len(s) >= 2 && strings.HasPrefix(s, "0x") {
s = s[2:]
}
b, err := hex.DecodeString(s)
if err != nil {
return fmt.Errorf("unable to decode data: %w", err)
}
var c vm.Container
if err := c.UnmarshalBinary(b, false); err != nil {
return err
}
fmt.Print(c.String())
return nil
}
return nil
}

65
cmd/eofdump/main.go Normal file
View file

@ -0,0 +1,65 @@
package main
import (
"fmt"
"os"
"github.com/ethereum/go-ethereum/internal/debug"
"github.com/ethereum/go-ethereum/internal/flags"
"github.com/urfave/cli/v2"
)
var app = flags.NewApp("the evm command line interface")
var (
RefTestFlag = &cli.StringFlag{
Name: "test",
Usage: "Path to EOF validation reference test.",
}
HexFlag = &cli.StringFlag{
Name: "hex",
Usage: "single container data parse and validation",
}
)
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 eofDumpCommand = &cli.Command{
Name: "eofdump",
Usage: "parses hex eof container",
Action: eofDump,
Flags: []cli.Flag{
HexFlag,
},
}
func init() {
app.Commands = []*cli.Command{
eofParserCommand,
eofDumpCommand,
}
app.Before = func(ctx *cli.Context) error {
flags.MigrateGlobalFlags(ctx)
return debug.Setup(ctx)
}
app.After = func(ctx *cli.Context) error {
debug.Exit()
return nil
}
}
func main() {
if err := app.Run(os.Args); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}

167
cmd/eofdump/parse_test.go Normal file
View file

@ -0,0 +1,167 @@
package main
import (
"bufio"
"bytes"
"encoding/hex"
"fmt"
"os"
"strings"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/vm"
)
func FuzzEofParsing(f *testing.F) {
// Seed with corpus from execution-spec-tests
for i := 0; ; i++ {
fname := fmt.Sprintf("testdata/eof_corpus_%d.txt", i)
corpus, err := os.Open(fname)
if err != nil {
break
}
f.Logf("Reading seed data from %v", fname)
scanner := bufio.NewScanner(corpus)
scanner.Buffer(make([]byte, 1024), 10*1024*1024)
for scanner.Scan() {
s := scanner.Text()
if len(s) >= 2 && strings.HasPrefix(s, "0x") {
s = s[2:]
}
b, err := hex.DecodeString(s)
if err != nil {
panic(err) // rotten corpus
}
f.Add(b)
}
corpus.Close()
if err := scanner.Err(); err != nil {
panic(err) // rotten corpus
}
}
// And do the fuzzing
f.Fuzz(func(t *testing.T, data []byte) {
var (
jt = vm.NewPragueEOFInstructionSetForTesting()
c vm.Container
)
cpy := common.CopyBytes(data)
if err := c.UnmarshalBinary(data, true); err == nil {
c.ValidateCode(&jt, true)
if have := c.MarshalBinary(); !bytes.Equal(have, data) {
f.Fatal("Unmarshal-> Marshal failure!")
}
}
if err := c.UnmarshalBinary(data, false); err == nil {
c.ValidateCode(&jt, false)
if have := c.MarshalBinary(); !bytes.Equal(have, data) {
f.Fatal("Unmarshal-> Marshal failure!")
}
}
if !bytes.Equal(cpy, data) {
panic("data modified during unmarshalling")
}
})
}
func TestEofParseInitcode(t *testing.T) {
testEofParse(t, true, "testdata/results.initcode.txt")
}
func TestEofParseRegular(t *testing.T) {
testEofParse(t, false, "testdata/results.regular.txt")
}
func testEofParse(t *testing.T, isInitCode bool, wantFile string) {
var wantFn func() string
var wantLoc = 0
{ // Configure the want-reader
wants, err := os.Open(wantFile)
if err != nil {
t.Fatal(err)
}
scanner := bufio.NewScanner(wants)
scanner.Buffer(make([]byte, 1024), 10*1024*1024)
wantFn = func() string {
if scanner.Scan() {
wantLoc++
return scanner.Text()
}
return "end of file reached"
}
}
for i := 0; ; i++ {
fname := fmt.Sprintf("testdata/eof_corpus_%d.txt", i)
corpus, err := os.Open(fname)
if err != nil {
break
}
t.Logf("# Reading seed data from %v", fname)
scanner := bufio.NewScanner(corpus)
scanner.Buffer(make([]byte, 1024), 10*1024*1024)
line := 1
for scanner.Scan() {
s := scanner.Text()
if len(s) >= 2 && strings.HasPrefix(s, "0x") {
s = s[2:]
}
b, err := hex.DecodeString(s)
if err != nil {
panic(err) // rotten corpus
}
have := "OK"
if _, err := parse(b, isInitCode); err != nil {
have = fmt.Sprintf("ERR: %v", err)
}
if false { // Change this to generate the want-output
fmt.Printf("%v\n", have)
} else {
want := wantFn()
if have != want {
if len(want) > 100 {
want = want[:100]
}
if len(b) > 100 {
b = b[:100]
}
t.Fatalf("%v:%d\n%v\ninput %x\nisInit: %v\nhave: %q\nwant: %q\n",
fname, line, fmt.Sprintf("%v:%d", wantFile, wantLoc), b, isInitCode, have, want)
}
}
line++
}
corpus.Close()
}
}
func BenchmarkEofParse(b *testing.B) {
corpus, err := os.Open("testdata/eof_benches.txt")
if err != nil {
b.Fatal(err)
}
defer corpus.Close()
scanner := bufio.NewScanner(corpus)
scanner.Buffer(make([]byte, 1024), 10*1024*1024)
line := 1
for scanner.Scan() {
s := scanner.Text()
if len(s) >= 2 && strings.HasPrefix(s, "0x") {
s = s[2:]
}
data, err := hex.DecodeString(s)
if err != nil {
b.Fatal(err) // rotten corpus
}
b.Run(fmt.Sprintf("test-%d", line), func(b *testing.B) {
b.ReportAllocs()
b.SetBytes(int64(len(data)))
for i := 0; i < b.N; i++ {
_, _ = parse(data, false)
}
})
line++
}
}

19
cmd/eofdump/testdata/eof_benches.txt vendored Normal file

File diff suppressed because one or more lines are too long

1814
cmd/eofdump/testdata/eof_corpus_0.txt vendored Normal file

File diff suppressed because one or more lines are too long

350
cmd/eofdump/testdata/eof_corpus_1.txt vendored Normal file

File diff suppressed because one or more lines are too long

2164
cmd/eofdump/testdata/results.initcode.txt vendored Normal file

File diff suppressed because it is too large Load diff

2164
cmd/eofdump/testdata/results.regular.txt vendored Normal file

File diff suppressed because it is too large Load diff

103
core/vm/analysis_eof.go Normal file
View file

@ -0,0 +1,103 @@
// Copyright 2024 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
// 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 uint16
)
pc++
switch {
case op >= PUSH1 && op <= PUSH32:
numbits = uint16(op - PUSH1 + 1)
case op == RJUMP || op == RJUMPI || op == CALLF || op == JUMPF || op == DATALOADN:
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 = uint16(code[pc])*2 + 3
if pc+uint64(numbits) > end {
// Jump table is truncated, mark as many bits
// as possible.
numbits = uint16(end - pc)
}
case op == DUPN || op == SWAPN || op == EXCHANGE || op == EOFCREATE || op == RETURNCONTRACT:
numbits = 1
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

@ -533,3 +533,166 @@ func enable4762(jt *JumpTable) {
} }
} }
} }
// 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[CALL] = undefined
jt[CALLCODE] = undefined
jt[DELEGATECALL] = undefined
jt[STATICCALL] = undefined
jt[SELFDESTRUCT] = undefined
jt[JUMP] = undefined
jt[JUMPI] = undefined
jt[PC] = undefined
jt[CREATE] = undefined
jt[CREATE2] = undefined
jt[CODESIZE] = undefined
jt[CODECOPY] = undefined
jt[EXTCODESIZE] = undefined
jt[EXTCODECOPY] = undefined
jt[EXTCODEHASH] = undefined
jt[GAS] = undefined
// Allow 0xFE to terminate sections
jt[INVALID] = &operation{
execute: opUndefined,
constantGas: 0,
minStack: minStack(0, 0),
maxStack: maxStack(0, 0),
}
// New opcodes
jt[RJUMP] = &operation{
execute: opRjump,
constantGas: GasQuickStep,
minStack: minStack(0, 0),
maxStack: maxStack(0, 0),
}
jt[RJUMPI] = &operation{
execute: opRjumpi,
constantGas: GasFastishStep,
minStack: minStack(1, 0),
maxStack: maxStack(1, 0),
}
jt[RJUMPV] = &operation{
execute: opRjumpv,
constantGas: GasFastishStep,
minStack: minStack(1, 0),
maxStack: maxStack(1, 0),
}
jt[CALLF] = &operation{
execute: opCallf,
constantGas: GasFastStep,
minStack: minStack(0, 0),
maxStack: maxStack(0, 0),
}
jt[RETF] = &operation{
execute: opRetf,
constantGas: GasFastestStep,
minStack: minStack(0, 0),
maxStack: maxStack(0, 0),
}
jt[JUMPF] = &operation{
execute: opJumpf,
constantGas: GasFastStep,
minStack: minStack(0, 0),
maxStack: maxStack(0, 0),
}
jt[EOFCREATE] = &operation{
execute: opEOFCreate,
constantGas: params.Create2Gas,
dynamicGas: gasEOFCreate,
minStack: minStack(4, 1),
maxStack: maxStack(4, 1),
memorySize: memoryEOFCreate,
}
jt[RETURNCONTRACT] = &operation{
execute: opReturnContract,
// returncontract has zero constant gas cost
dynamicGas: pureMemoryGascost,
minStack: minStack(2, 0),
maxStack: maxStack(2, 0),
memorySize: memoryReturnContract,
}
jt[DATALOAD] = &operation{
execute: opDataLoad,
constantGas: GasFastishStep,
minStack: minStack(1, 1),
maxStack: maxStack(1, 1),
}
jt[DATALOADN] = &operation{
execute: opDataLoadN,
constantGas: GasFastestStep,
minStack: minStack(0, 1),
maxStack: maxStack(0, 1),
}
jt[DATASIZE] = &operation{
execute: opDataSize,
constantGas: GasQuickStep,
minStack: minStack(0, 1),
maxStack: maxStack(0, 1),
}
jt[DATACOPY] = &operation{
execute: opDataCopy,
constantGas: GasFastestStep,
dynamicGas: memoryCopierGas(2),
minStack: minStack(3, 0),
maxStack: maxStack(3, 0),
memorySize: memoryDataCopy,
}
jt[DUPN] = &operation{
execute: opDupN,
constantGas: GasFastestStep,
minStack: minStack(0, 1),
maxStack: maxStack(0, 1),
}
jt[SWAPN] = &operation{
execute: opSwapN,
constantGas: GasFastestStep,
minStack: minStack(0, 0),
maxStack: maxStack(0, 0),
}
jt[EXCHANGE] = &operation{
execute: opExchange,
constantGas: GasFastestStep,
minStack: minStack(0, 0),
maxStack: maxStack(0, 0),
}
jt[RETURNDATALOAD] = &operation{
execute: opReturnDataLoad,
constantGas: GasFastestStep,
minStack: minStack(1, 1),
maxStack: maxStack(1, 1),
}
jt[EXTCALL] = &operation{
execute: opExtCall,
constantGas: params.WarmStorageReadCostEIP2929,
dynamicGas: makeCallVariantGasCallEIP2929(gasExtCall, 0),
minStack: minStack(4, 1),
maxStack: maxStack(4, 1),
memorySize: memoryExtCall,
}
jt[EXTDELEGATECALL] = &operation{
execute: opExtDelegateCall,
dynamicGas: makeCallVariantGasCallEIP2929(gasExtDelegateCall, 0),
constantGas: params.WarmStorageReadCostEIP2929,
minStack: minStack(3, 1),
maxStack: maxStack(3, 1),
memorySize: memoryExtCall,
}
jt[EXTSTATICCALL] = &operation{
execute: opExtStaticCall,
constantGas: params.WarmStorageReadCostEIP2929,
dynamicGas: makeCallVariantGasCallEIP2929(gasExtStaticCall, 0),
minStack: minStack(3, 1),
maxStack: maxStack(3, 1),
memorySize: memoryExtCall,
}
}

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

@ -0,0 +1,475 @@
// 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"
"encoding/hex"
"errors"
"fmt"
"io"
"github.com/ethereum/go-ethereum/params"
)
const (
offsetVersion = 2
offsetTypesKind = 3
offsetCodeKind = 6
kindTypes = 1
kindCode = 2
kindContainer = 3
kindData = 4
eofFormatByte = 0xef
eof1Version = 1
maxInputItems = 127
maxOutputItems = 128
maxStackHeight = 1023
maxContainerSections = 256
)
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
sections []*Container
containerCode [][]byte
data []byte
dataSize int // might be more than len(data)
}
// functionMetadata is an EOF function signature.
type functionMetadata struct {
inputs uint8
outputs 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)))
}
var encodedContainer [][]byte
if len(c.sections) != 0 {
b = append(b, kindContainer)
b = binary.BigEndian.AppendUint16(b, uint16(len(c.sections)))
for _, section := range c.sections {
encoded := section.MarshalBinary()
b = binary.BigEndian.AppendUint16(b, uint16(len(encoded)))
encodedContainer = append(encodedContainer, encoded)
}
}
b = append(b, kindData)
b = binary.BigEndian.AppendUint16(b, uint16(c.dataSize))
b = append(b, 0) // terminator
// Write section contents.
for _, ty := range c.types {
b = append(b, []byte{ty.inputs, ty.outputs, byte(ty.maxStackHeight >> 8), byte(ty.maxStackHeight & 0x00ff)}...)
}
for _, code := range c.code {
b = append(b, code...)
}
for _, section := range encodedContainer {
b = append(b, section...)
}
b = append(b, c.data...)
return b
}
// UnmarshalBinary decodes an EOF container.
func (c *Container) UnmarshalBinary(b []byte, isInitcode bool) error {
return c.unmarshalSubContainer(b, isInitcode, true)
}
func (c *Container) unmarshalSubContainer(b []byte, isInitcode bool, topLevel bool) error {
if !hasEOFMagic(b) {
return fmt.Errorf("%w: want %x", ErrInvalidMagic, eofMagic)
}
if len(b) < 14 {
return io.ErrUnexpectedEOF
}
if len(b) > params.MaxInitCodeSize {
return ErrMaxInitCodeSizeExceeded
}
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 found and type signatures, types %d, code %d", ErrInvalidCodeSize, typesSize/4, len(codeSizes))
}
// Parse (optional) container section header.
var containerSizes []int
offset := offsetCodeKind + 2 + 2*len(codeSizes) + 1
if offset < len(b) && b[offset] == kindContainer {
kind, containerSizes, err = parseSectionList(b, offset)
if err != nil {
return err
}
if kind != kindContainer {
panic("somethings wrong")
}
if len(containerSizes) == 0 {
return fmt.Errorf("%w: total container count must not be zero", ErrInvalidContainerSectionSize)
}
offset = offset + 2 + 2*len(containerSizes) + 1
}
// Parse data section header.
kind, dataSize, err = parseSection(b, offset)
if err != nil {
return err
}
if kind != kindData {
return fmt.Errorf("%w: found section %x instead", ErrMissingDataHeader, kind)
}
c.dataSize = dataSize
// Check for terminator.
offsetTerminator := offset + 3
if len(b) < offsetTerminator {
return fmt.Errorf("%w: invalid offset terminator", 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(containerSizes) != 0 {
expectedSize += sum(containerSizes)
}
if len(b) < expectedSize-dataSize {
return fmt.Errorf("%w: have %d, want %d", ErrInvalidContainerSize, len(b), expectedSize)
}
// Only check that the expected size is not exceed on non-initcode
if !isInitcode && 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{
inputs: b[idx+i*4],
outputs: b[idx+i*4+1],
maxStackHeight: binary.BigEndian.Uint16(b[idx+i*4+2:]),
}
if sig.inputs > maxInputItems {
return fmt.Errorf("%w for section %d: have %d", ErrTooManyInputs, i, sig.inputs)
}
if sig.outputs > maxOutputItems {
return fmt.Errorf("%w for section %d: have %d", ErrTooManyOutputs, i, sig.outputs)
}
if sig.maxStackHeight > maxStackHeight {
return fmt.Errorf("%w for section %d: have %d", ErrTooLargeMaxStackHeight, i, sig.maxStackHeight)
}
types = append(types, sig)
}
if types[0].inputs != 0 || types[0].outputs != 0x80 {
return fmt.Errorf("%w: have %d, %d", ErrInvalidSection0Type, types[0].inputs, types[0].outputs)
}
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 the optional container sizes.
if len(containerSizes) != 0 {
if len(containerSizes) > maxContainerSections {
return fmt.Errorf("%w number of container section exceed: %v: have %v", ErrInvalidContainerSectionSize, maxContainerSections, len(containerSizes))
}
containerCode := make([][]byte, 0, len(containerSizes))
container := make([]*Container, 0, len(containerSizes))
for i, size := range containerSizes {
if size == 0 || idx+size > len(b) {
return fmt.Errorf("%w for section %d: size must not be 0", ErrInvalidContainerSectionSize, i)
}
c := new(Container)
end := min(idx+size, len(b))
if err := c.unmarshalSubContainer(b[idx:end], isInitcode, false); err != nil {
if topLevel {
return fmt.Errorf("%w in sub container %d", err, i)
}
return err
}
container = append(container, c)
containerCode = append(containerCode, b[idx:end])
idx += size
}
c.sections = container
c.containerCode = containerCode
}
// Parse data section.
end := len(b)
if !isInitcode {
end = min(idx+dataSize, len(b))
}
if topLevel && len(b) != idx+dataSize {
return ErrTruncatedTopLevelContainer
}
c.data = b[idx:end]
return nil
}
// ValidateCode validates each code section of the container against the EOF v1
// rule set.
func (c *Container) ValidateCode(jt *JumpTable, isInitCode bool) error {
refBy := notRefByEither
if isInitCode {
refBy = refByEOFCreate
}
return c.validateSubContainer(jt, refBy)
}
func (c *Container) validateSubContainer(jt *JumpTable, refBy int) error {
visited := make(map[int]struct{})
subContainerVisited := make(map[int]int)
toVisit := []int{0}
for len(toVisit) > 0 {
// TODO check if this can be used as a DOS
// Theres and edge case here where we mark something as visited that we visit before,
// This should not trigger a re-visit
// e.g. 0 -> 1, 2, 3
// 1 -> 2, 3
// should not mean 2 and 3 should be visited twice
var (
index = toVisit[0]
code = c.code[index]
)
if _, ok := visited[index]; !ok {
res, err := validateCode(code, index, c, jt, refBy == refByEOFCreate)
if err != nil {
return err
}
visited[index] = struct{}{}
// Mark all sections that can be visited from here.
for idx := range res.visitedCode {
if _, ok := visited[idx]; !ok {
toVisit = append(toVisit, idx)
}
}
// Mark all subcontainer that can be visited from here.
for idx, reference := range res.visitedSubContainers {
// Make sure subcontainers are only ever referenced by either EOFCreate or ReturnContract
if ref, ok := subContainerVisited[idx]; ok && ref != reference {
return errors.New("section referenced by both EOFCreate and ReturnContract")
}
subContainerVisited[idx] = reference
}
if refBy == refByReturnContract && res.isInitCode {
return ErrIncompatibleContainerKind
}
if refBy == refByEOFCreate && res.isRuntime {
return ErrIncompatibleContainerKind
}
}
toVisit = toVisit[1:]
}
// Make sure every code section is visited at least once.
if len(visited) != len(c.code) {
return ErrUnreachableCode
}
for idx, container := range c.sections {
reference, ok := subContainerVisited[idx]
if !ok {
return ErrOrphanedSubcontainer
}
if err := container.validateSubContainer(jt, reference); 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)
}
// sum computes the sum of a slice.
func sum(list []int) (s int) {
for _, n := range list {
s += n
}
return
}
func (c *Container) String() string {
var result string
result += "Header\n"
result += "-----------\n"
result += fmt.Sprintf("EOFMagic: %02x\n", eofMagic)
result += fmt.Sprintf("EOFVersion: %02x\n", eof1Version)
result += fmt.Sprintf("KindType: %02x\n", kindTypes)
result += fmt.Sprintf("TypesSize: %04x\n", len(c.types)*4)
result += fmt.Sprintf("KindCode: %02x\n", kindCode)
result += fmt.Sprintf("CodeSize: %04x\n", len(c.code))
for i, code := range c.code {
result += fmt.Sprintf("Code %v length: %04x\n", i, len(code))
}
if len(c.sections) != 0 {
result += fmt.Sprintf("KindContainer: %02x\n", kindContainer)
result += fmt.Sprintf("ContainerSize: %04x\n", len(c.sections))
for i, section := range c.sections {
result += fmt.Sprintf("Container %v length: %04x\n", i, len(section.MarshalBinary()))
}
}
result += fmt.Sprintf("KindData: %02x\n", kindData)
result += fmt.Sprintf("DataSize: %04x\n", len(c.data))
result += fmt.Sprintf("Terminator: %02x\n", 0x0)
result += "-----------\n"
result += "Body\n"
result += "-----------\n"
for i, typ := range c.types {
result += fmt.Sprintf("Type %v: %v\n", i, hex.EncodeToString([]byte{typ.inputs, typ.outputs, byte(typ.maxStackHeight >> 8), byte(typ.maxStackHeight & 0x00ff)}))
}
for i, code := range c.code {
result += fmt.Sprintf("Code %v: %v\n", i, hex.EncodeToString(code))
}
for i, section := range c.sections {
result += fmt.Sprintf("Section %v: %v\n", i, hex.EncodeToString(section.MarshalBinary()))
}
result += fmt.Sprintf("Data: %v\n", hex.EncodeToString(c.data))
return result
}

57
core/vm/eof_immediates.go Normal file
View file

@ -0,0 +1,57 @@
// Copyright 2024 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
// immediate denotes how many immediate bytes an operation uses. This information
// is not required during runtime, only during EOF-validation, so is not
// places into the op-struct in the instruction table.
// Note: the immediates is fork-agnostic, and assumes that validity of opcodes at
// the given time is performed elsewhere.
var immediates [256]uint8
// terminals denotes whether instructions can be the final opcode in a code section.
// Note: the terminals is fork-agnostic, and assumes that validity of opcodes at
// the given time is performed elsewhere.
var terminals [256]bool
func init() {
// The legacy pushes
for i := uint8(1); i < 33; i++ {
immediates[int(PUSH0)+int(i)] = i
}
// And new eof opcodes.
immediates[DATALOADN] = 2
immediates[RJUMP] = 2
immediates[RJUMPI] = 2
immediates[RJUMPV] = 3
immediates[CALLF] = 2
immediates[JUMPF] = 2
immediates[DUPN] = 1
immediates[SWAPN] = 1
immediates[EXCHANGE] = 1
immediates[EOFCREATE] = 1
immediates[RETURNCONTRACT] = 1
// Define the terminals.
terminals[STOP] = true
terminals[RETF] = true
terminals[JUMPF] = true
terminals[RETURNCONTRACT] = true
terminals[RETURN] = true
terminals[REVERT] = true
terminals[INVALID] = true
}

117
core/vm/eof_instructions.go Normal file
View file

@ -0,0 +1,117 @@
// Copyright 2024 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
// opExtCodeCopyEOF implements the EXTCODECOPY opcode for EOF-enabled forks.
func opExtCodeCopyEOF(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
panic("not implemented")
}
// opRjump implements the RJUMP opcode.
func opRjump(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
panic("not implemented")
}
// opRjumpi implements the RJUMPI opcode
func opRjumpi(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
panic("not implemented")
}
// opRjumpv implements the RJUMPV opcode
func opRjumpv(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
panic("not implemented")
}
// opCallf implements the CALLF opcode
func opCallf(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
panic("not implemented")
}
// opRetf implements the RETF opcode
func opRetf(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
panic("not implemented")
}
// opJumpf implements the JUMPF opcode
func opJumpf(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
panic("not implemented")
}
// opEOFCreate implements the EOFCREATE opcode
func opEOFCreate(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
panic("not implemented")
}
// opReturnContract implements the RETURNCONTRACT opcode
func opReturnContract(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
panic("not implemented")
}
// opDataLoad implements the DATALOAD opcode
func opDataLoad(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
panic("not implemented")
}
// opDataLoadN implements the DATALOADN opcode
func opDataLoadN(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
panic("not implemented")
}
// opDataSize implements the DATASIZE opcode
func opDataSize(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
panic("not implemented")
}
// opDataCopy implements the DATACOPY opcode
func opDataCopy(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
panic("not implemented")
}
// opDupN implements the DUPN opcode
func opDupN(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
panic("not implemented")
}
// opSwapN implements the SWAPN opcode
func opSwapN(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
panic("not implemented")
}
// opExchange implements the EXCHANGE opcode
func opExchange(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
panic("not implemented")
}
// opReturnDataLoad implements the RETURNDATALOAD opcode
func opReturnDataLoad(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
panic("not implemented")
}
// opExtCall implements the EOFCREATE opcode
func opExtCall(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
panic("not implemented")
}
// opExtDelegateCall implements the EXTDELEGATECALL opcode
func opExtDelegateCall(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
panic("not implemented")
}
// opExtStaticCall implements the EXTSTATICCALL opcode
func opExtStaticCall(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
panic("not implemented")
}

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

@ -0,0 +1,119 @@
// 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 (
"encoding/hex"
"fmt"
"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{{inputs: 0, outputs: 0x80, maxStackHeight: 1}},
code: [][]byte{common.Hex2Bytes("604200")},
data: []byte{0x01, 0x02, 0x03},
dataSize: 3,
},
},
{
want: Container{
types: []*functionMetadata{{inputs: 0, outputs: 0x80, maxStackHeight: 1}},
code: [][]byte{common.Hex2Bytes("604200")},
data: []byte{0x01, 0x02, 0x03},
dataSize: 3,
},
},
{
want: Container{
types: []*functionMetadata{
{inputs: 0, outputs: 0x80, maxStackHeight: 1},
{inputs: 2, outputs: 3, maxStackHeight: 4},
{inputs: 1, outputs: 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, true); 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)
}
}
}
func TestEOFSubcontainer(t *testing.T) {
var subcontainer = new(Container)
if err := subcontainer.UnmarshalBinary(common.Hex2Bytes("ef000101000402000100010400000000800000fe"), true); err != nil {
t.Fatal(err)
}
container := Container{
types: []*functionMetadata{{inputs: 0, outputs: 0x80, maxStackHeight: 1}},
code: [][]byte{common.Hex2Bytes("604200")},
sections: []*Container{subcontainer},
data: []byte{0x01, 0x02, 0x03},
dataSize: 3,
}
var (
b = container.MarshalBinary()
got Container
)
if err := got.UnmarshalBinary(b, true); err != nil {
t.Fatal(err)
}
fmt.Print(got)
if res := got.MarshalBinary(); !reflect.DeepEqual(res, b) {
t.Fatalf("invalid marshalling, want %v got %v", b, res)
}
}
func TestMarshaling(t *testing.T) {
tests := []string{
"EF000101000402000100040400000000800000E0000000",
"ef0001010004020001000d04000000008000025fe100055f5fe000035f600100",
}
for i, test := range tests {
s, err := hex.DecodeString(test)
if err != nil {
t.Fatalf("test %d: error decoding: %v", i, err)
}
var got Container
if err := got.UnmarshalBinary(s, true); err != nil {
t.Fatalf("test %d: got error %v", i, err)
}
}
}

View file

@ -51,10 +51,14 @@ type ErrStackUnderflow struct {
required int required int
} }
func (e *ErrStackUnderflow) Error() string { func (e ErrStackUnderflow) Error() string {
return fmt.Sprintf("stack underflow (%d <=> %d)", e.stackLen, e.required) 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 // ErrStackOverflow wraps an evm error when the items on the stack exceeds
// the maximum allowance. // the maximum allowance.
type ErrStackOverflow struct { type ErrStackOverflow struct {
@ -62,10 +66,14 @@ type ErrStackOverflow struct {
limit int limit int
} }
func (e *ErrStackOverflow) Error() string { func (e ErrStackOverflow) Error() string {
return fmt.Sprintf("stack limit reached %d (%d)", e.stackLen, e.limit) 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. // ErrInvalidOpCode wraps an evm error when an invalid opcode is encountered.
type ErrInvalidOpCode struct { type ErrInvalidOpCode struct {
opcode OpCode opcode OpCode

View file

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

View file

@ -23,6 +23,7 @@ import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/common/math"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
"github.com/holiman/uint256"
) )
// memoryGasCost calculates the quadratic gas for memory expansion. It does so // memoryGasCost calculates the quadratic gas for memory expansion. It does so
@ -480,6 +481,52 @@ func gasStaticCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memo
return gas, nil return gas, nil
} }
func gasExtCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
var (
gas uint64
transfersValue = !stack.Back(2).IsZero()
address = common.Address(stack.Back(1).Bytes20())
)
if transfersValue && evm.StateDB.Empty(address) {
gas += params.CallNewAccountGas
}
if transfersValue && !evm.chainRules.IsEIP4762 { // Non-verkle
gas += params.CallValueTransferGas
} else if transfersValue && evm.chainRules.IsEIP4762 { // Verkle
gas += evm.AccessEvents.ValueTransferGas(contract.Address(), address)
}
memoryGas, err := gasEOFMemCost(evm, contract, stack, mem, memorySize)
if err != nil {
return 0, err
}
var overflow bool
if gas, overflow = math.SafeAdd(gas, memoryGas); overflow {
return 0, ErrGasUintOverflow
}
return gas, nil
}
// gasEOFMemCost calculates the cost of an external call in EOF, but does not
// take value-transfer-related costs into account: only mem expansion costs.
func gasEOFMemCost(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
gas, err := memoryGasCost(mem, memorySize)
if err != nil {
return 0, err
}
evm.callGasTemp, err = callGas(true, contract.Gas, gas, new(uint256.Int).SetUint64(contract.Gas))
if err != nil {
return 0, err
}
var overflow bool
if gas, overflow = math.SafeAdd(gas, evm.callGasTemp); overflow {
return 0, ErrGasUintOverflow
}
return gas, nil
}
var gasExtDelegateCall = gasEOFMemCost
var gasExtStaticCall = gasEOFMemCost
func gasSelfdestruct(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { func gasSelfdestruct(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
var gas uint64 var gas uint64
// EIP150 homestead gas reprice fork: // EIP150 homestead gas reprice fork:
@ -502,3 +549,9 @@ func gasSelfdestruct(evm *EVM, contract *Contract, stack *Stack, mem *Memory, me
} }
return gas, nil return gas, nil
} }
// gasEOFCreate returns the gas-cost for EOF-Create. Hashing charge needs to be
// deducted in the opcode itself, since it depends on the immediate
func gasEOFCreate(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
return memoryGasCost(mem, memorySize)
}

View file

@ -42,6 +42,9 @@ type operation struct {
// memorySize returns the memory size required for the operation // memorySize returns the memory size required for the operation
memorySize memorySizeFunc memorySize memorySizeFunc
// undefined denotes if the instruction is not officially defined in the jump table
undefined bool
} }
var ( var (
@ -58,6 +61,7 @@ var (
shanghaiInstructionSet = newShanghaiInstructionSet() shanghaiInstructionSet = newShanghaiInstructionSet()
cancunInstructionSet = newCancunInstructionSet() cancunInstructionSet = newCancunInstructionSet()
verkleInstructionSet = newVerkleInstructionSet() verkleInstructionSet = newVerkleInstructionSet()
pragueEOFInstructionSet = newPragueEOFInstructionSet()
) )
// JumpTable contains the EVM opcodes supported at a given fork. // JumpTable contains the EVM opcodes supported at a given fork.
@ -87,6 +91,16 @@ func newVerkleInstructionSet() JumpTable {
return validate(instructionSet) return validate(instructionSet)
} }
func NewPragueEOFInstructionSetForTesting() JumpTable {
return newPragueEOFInstructionSet()
}
func newPragueEOFInstructionSet() JumpTable {
instructionSet := newCancunInstructionSet()
enableEOF(&instructionSet)
return validate(instructionSet)
}
func newCancunInstructionSet() JumpTable { func newCancunInstructionSet() JumpTable {
instructionSet := newShanghaiInstructionSet() instructionSet := newShanghaiInstructionSet()
enable4844(&instructionSet) // EIP-4844 (BLOBHASH opcode) enable4844(&instructionSet) // EIP-4844 (BLOBHASH opcode)
@ -1059,12 +1073,17 @@ func newFrontierInstructionSet() JumpTable {
minStack: minStack(1, 0), minStack: minStack(1, 0),
maxStack: maxStack(1, 0), maxStack: maxStack(1, 0),
}, },
INVALID: {
execute: opUndefined,
minStack: minStack(0, 0),
maxStack: maxStack(0, 0),
},
} }
// Fill all unassigned slots with opUndefined. // Fill all unassigned slots with opUndefined.
for i, entry := range tbl { for i, entry := range tbl {
if entry == nil { 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

@ -78,6 +78,7 @@ func memoryCall(stack *Stack) (uint64, bool) {
} }
return y, false return y, false
} }
func memoryDelegateCall(stack *Stack) (uint64, bool) { func memoryDelegateCall(stack *Stack) (uint64, bool) {
x, overflow := calcMemSize64(stack.Back(4), stack.Back(5)) x, overflow := calcMemSize64(stack.Back(4), stack.Back(5))
if overflow { if overflow {
@ -119,3 +120,19 @@ func memoryRevert(stack *Stack) (uint64, bool) {
func memoryLog(stack *Stack) (uint64, bool) { func memoryLog(stack *Stack) (uint64, bool) {
return calcMemSize64(stack.Back(0), stack.Back(1)) return calcMemSize64(stack.Back(0), stack.Back(1))
} }
func memoryExtCall(stack *Stack) (uint64, bool) {
return calcMemSize64(stack.Back(1), stack.Back(2))
}
func memoryDataCopy(stack *Stack) (uint64, bool) {
return calcMemSize64(stack.Back(0), stack.Back(2))
}
func memoryEOFCreate(stack *Stack) (uint64, bool) {
return calcMemSize64(stack.Back(2), stack.Back(3))
}
func memoryReturnContract(stack *Stack) (uint64, bool) {
return calcMemSize64(stack.Back(0), stack.Back(1))
}

View file

@ -209,6 +209,29 @@ const (
LOG4 LOG4
) )
// 0xd0 range - eof operations.
const (
DATALOAD OpCode = 0xd0
DATALOADN OpCode = 0xd1
DATASIZE OpCode = 0xd2
DATACOPY OpCode = 0xd3
)
// 0xe0 range - eof operations.
const (
RJUMP OpCode = 0xe0
RJUMPI OpCode = 0xe1
RJUMPV OpCode = 0xe2
CALLF OpCode = 0xe3
RETF OpCode = 0xe4
JUMPF OpCode = 0xe5
DUPN OpCode = 0xe6
SWAPN OpCode = 0xe7
EXCHANGE OpCode = 0xe8
EOFCREATE OpCode = 0xec
RETURNCONTRACT OpCode = 0xee
)
// 0xf0 range - closures. // 0xf0 range - closures.
const ( const (
CREATE OpCode = 0xf0 CREATE OpCode = 0xf0
@ -218,10 +241,15 @@ const (
DELEGATECALL OpCode = 0xf4 DELEGATECALL OpCode = 0xf4
CREATE2 OpCode = 0xf5 CREATE2 OpCode = 0xf5
STATICCALL OpCode = 0xfa RETURNDATALOAD OpCode = 0xf7
REVERT OpCode = 0xfd EXTCALL OpCode = 0xf8
INVALID OpCode = 0xfe EXTDELEGATECALL OpCode = 0xf9
SELFDESTRUCT OpCode = 0xff
STATICCALL OpCode = 0xfa
EXTSTATICCALL OpCode = 0xfb
REVERT OpCode = 0xfd
INVALID OpCode = 0xfe
SELFDESTRUCT OpCode = 0xff
) )
var opCodeToString = [256]string{ var opCodeToString = [256]string{
@ -384,6 +412,25 @@ var opCodeToString = [256]string{
LOG3: "LOG3", LOG3: "LOG3",
LOG4: "LOG4", LOG4: "LOG4",
// 0xd range - eof ops.
DATALOAD: "DATALOAD",
DATALOADN: "DATALOADN",
DATASIZE: "DATASIZE",
DATACOPY: "DATACOPY",
// 0xe0 range.
RJUMP: "RJUMP",
RJUMPI: "RJUMPI",
RJUMPV: "RJUMPV",
CALLF: "CALLF",
RETF: "RETF",
JUMPF: "JUMPF",
DUPN: "DUPN",
SWAPN: "SWAPN",
EXCHANGE: "EXCHANGE",
EOFCREATE: "EOFCREATE",
RETURNCONTRACT: "RETURNCONTRACT",
// 0xf0 range - closures. // 0xf0 range - closures.
CREATE: "CREATE", CREATE: "CREATE",
CALL: "CALL", CALL: "CALL",
@ -391,10 +438,16 @@ var opCodeToString = [256]string{
CALLCODE: "CALLCODE", CALLCODE: "CALLCODE",
DELEGATECALL: "DELEGATECALL", DELEGATECALL: "DELEGATECALL",
CREATE2: "CREATE2", CREATE2: "CREATE2",
STATICCALL: "STATICCALL",
REVERT: "REVERT", RETURNDATALOAD: "RETURNDATALOAD",
INVALID: "INVALID", EXTCALL: "EXTCALL",
SELFDESTRUCT: "SELFDESTRUCT", EXTDELEGATECALL: "EXTDELEGATECALL",
STATICCALL: "STATICCALL",
EXTSTATICCALL: "EXTSTATICCALL",
REVERT: "REVERT",
INVALID: "INVALID",
SELFDESTRUCT: "SELFDESTRUCT",
} }
func (op OpCode) String() string { func (op OpCode) String() string {
@ -405,155 +458,174 @@ func (op OpCode) String() string {
} }
var stringToOp = map[string]OpCode{ var stringToOp = map[string]OpCode{
"STOP": STOP, "STOP": STOP,
"ADD": ADD, "ADD": ADD,
"MUL": MUL, "MUL": MUL,
"SUB": SUB, "SUB": SUB,
"DIV": DIV, "DIV": DIV,
"SDIV": SDIV, "SDIV": SDIV,
"MOD": MOD, "MOD": MOD,
"SMOD": SMOD, "SMOD": SMOD,
"EXP": EXP, "EXP": EXP,
"NOT": NOT, "NOT": NOT,
"LT": LT, "LT": LT,
"GT": GT, "GT": GT,
"SLT": SLT, "SLT": SLT,
"SGT": SGT, "SGT": SGT,
"EQ": EQ, "EQ": EQ,
"ISZERO": ISZERO, "ISZERO": ISZERO,
"SIGNEXTEND": SIGNEXTEND, "SIGNEXTEND": SIGNEXTEND,
"AND": AND, "AND": AND,
"OR": OR, "OR": OR,
"XOR": XOR, "XOR": XOR,
"BYTE": BYTE, "BYTE": BYTE,
"SHL": SHL, "SHL": SHL,
"SHR": SHR, "SHR": SHR,
"SAR": SAR, "SAR": SAR,
"ADDMOD": ADDMOD, "ADDMOD": ADDMOD,
"MULMOD": MULMOD, "MULMOD": MULMOD,
"KECCAK256": KECCAK256, "KECCAK256": KECCAK256,
"ADDRESS": ADDRESS, "ADDRESS": ADDRESS,
"BALANCE": BALANCE, "BALANCE": BALANCE,
"ORIGIN": ORIGIN, "ORIGIN": ORIGIN,
"CALLER": CALLER, "CALLER": CALLER,
"CALLVALUE": CALLVALUE, "CALLVALUE": CALLVALUE,
"CALLDATALOAD": CALLDATALOAD, "CALLDATALOAD": CALLDATALOAD,
"CALLDATASIZE": CALLDATASIZE, "CALLDATASIZE": CALLDATASIZE,
"CALLDATACOPY": CALLDATACOPY, "CALLDATACOPY": CALLDATACOPY,
"CHAINID": CHAINID, "CHAINID": CHAINID,
"BASEFEE": BASEFEE, "BASEFEE": BASEFEE,
"BLOBHASH": BLOBHASH, "BLOBHASH": BLOBHASH,
"BLOBBASEFEE": BLOBBASEFEE, "BLOBBASEFEE": BLOBBASEFEE,
"DELEGATECALL": DELEGATECALL, "DELEGATECALL": DELEGATECALL,
"STATICCALL": STATICCALL, "STATICCALL": STATICCALL,
"CODESIZE": CODESIZE, "CODESIZE": CODESIZE,
"CODECOPY": CODECOPY, "CODECOPY": CODECOPY,
"GASPRICE": GASPRICE, "GASPRICE": GASPRICE,
"EXTCODESIZE": EXTCODESIZE, "EXTCODESIZE": EXTCODESIZE,
"EXTCODECOPY": EXTCODECOPY, "EXTCODECOPY": EXTCODECOPY,
"RETURNDATASIZE": RETURNDATASIZE, "RETURNDATASIZE": RETURNDATASIZE,
"RETURNDATACOPY": RETURNDATACOPY, "RETURNDATACOPY": RETURNDATACOPY,
"EXTCODEHASH": EXTCODEHASH, "EXTCODEHASH": EXTCODEHASH,
"BLOCKHASH": BLOCKHASH, "BLOCKHASH": BLOCKHASH,
"COINBASE": COINBASE, "COINBASE": COINBASE,
"TIMESTAMP": TIMESTAMP, "TIMESTAMP": TIMESTAMP,
"NUMBER": NUMBER, "NUMBER": NUMBER,
"DIFFICULTY": DIFFICULTY, "DIFFICULTY": DIFFICULTY,
"GASLIMIT": GASLIMIT, "GASLIMIT": GASLIMIT,
"SELFBALANCE": SELFBALANCE, "SELFBALANCE": SELFBALANCE,
"POP": POP, "POP": POP,
"MLOAD": MLOAD, "MLOAD": MLOAD,
"MSTORE": MSTORE, "MSTORE": MSTORE,
"MSTORE8": MSTORE8, "MSTORE8": MSTORE8,
"SLOAD": SLOAD, "SLOAD": SLOAD,
"SSTORE": SSTORE, "SSTORE": SSTORE,
"JUMP": JUMP, "JUMP": JUMP,
"JUMPI": JUMPI, "JUMPI": JUMPI,
"PC": PC, "PC": PC,
"MSIZE": MSIZE, "MSIZE": MSIZE,
"GAS": GAS, "GAS": GAS,
"JUMPDEST": JUMPDEST, "JUMPDEST": JUMPDEST,
"TLOAD": TLOAD, "TLOAD": TLOAD,
"TSTORE": TSTORE, "TSTORE": TSTORE,
"MCOPY": MCOPY, "MCOPY": MCOPY,
"PUSH0": PUSH0, "PUSH0": PUSH0,
"PUSH1": PUSH1, "PUSH1": PUSH1,
"PUSH2": PUSH2, "PUSH2": PUSH2,
"PUSH3": PUSH3, "PUSH3": PUSH3,
"PUSH4": PUSH4, "PUSH4": PUSH4,
"PUSH5": PUSH5, "PUSH5": PUSH5,
"PUSH6": PUSH6, "PUSH6": PUSH6,
"PUSH7": PUSH7, "PUSH7": PUSH7,
"PUSH8": PUSH8, "PUSH8": PUSH8,
"PUSH9": PUSH9, "PUSH9": PUSH9,
"PUSH10": PUSH10, "PUSH10": PUSH10,
"PUSH11": PUSH11, "PUSH11": PUSH11,
"PUSH12": PUSH12, "PUSH12": PUSH12,
"PUSH13": PUSH13, "PUSH13": PUSH13,
"PUSH14": PUSH14, "PUSH14": PUSH14,
"PUSH15": PUSH15, "PUSH15": PUSH15,
"PUSH16": PUSH16, "PUSH16": PUSH16,
"PUSH17": PUSH17, "PUSH17": PUSH17,
"PUSH18": PUSH18, "PUSH18": PUSH18,
"PUSH19": PUSH19, "PUSH19": PUSH19,
"PUSH20": PUSH20, "PUSH20": PUSH20,
"PUSH21": PUSH21, "PUSH21": PUSH21,
"PUSH22": PUSH22, "PUSH22": PUSH22,
"PUSH23": PUSH23, "PUSH23": PUSH23,
"PUSH24": PUSH24, "PUSH24": PUSH24,
"PUSH25": PUSH25, "PUSH25": PUSH25,
"PUSH26": PUSH26, "PUSH26": PUSH26,
"PUSH27": PUSH27, "PUSH27": PUSH27,
"PUSH28": PUSH28, "PUSH28": PUSH28,
"PUSH29": PUSH29, "PUSH29": PUSH29,
"PUSH30": PUSH30, "PUSH30": PUSH30,
"PUSH31": PUSH31, "PUSH31": PUSH31,
"PUSH32": PUSH32, "PUSH32": PUSH32,
"DUP1": DUP1, "DUP1": DUP1,
"DUP2": DUP2, "DUP2": DUP2,
"DUP3": DUP3, "DUP3": DUP3,
"DUP4": DUP4, "DUP4": DUP4,
"DUP5": DUP5, "DUP5": DUP5,
"DUP6": DUP6, "DUP6": DUP6,
"DUP7": DUP7, "DUP7": DUP7,
"DUP8": DUP8, "DUP8": DUP8,
"DUP9": DUP9, "DUP9": DUP9,
"DUP10": DUP10, "DUP10": DUP10,
"DUP11": DUP11, "DUP11": DUP11,
"DUP12": DUP12, "DUP12": DUP12,
"DUP13": DUP13, "DUP13": DUP13,
"DUP14": DUP14, "DUP14": DUP14,
"DUP15": DUP15, "DUP15": DUP15,
"DUP16": DUP16, "DUP16": DUP16,
"SWAP1": SWAP1, "SWAP1": SWAP1,
"SWAP2": SWAP2, "SWAP2": SWAP2,
"SWAP3": SWAP3, "SWAP3": SWAP3,
"SWAP4": SWAP4, "SWAP4": SWAP4,
"SWAP5": SWAP5, "SWAP5": SWAP5,
"SWAP6": SWAP6, "SWAP6": SWAP6,
"SWAP7": SWAP7, "SWAP7": SWAP7,
"SWAP8": SWAP8, "SWAP8": SWAP8,
"SWAP9": SWAP9, "SWAP9": SWAP9,
"SWAP10": SWAP10, "SWAP10": SWAP10,
"SWAP11": SWAP11, "SWAP11": SWAP11,
"SWAP12": SWAP12, "SWAP12": SWAP12,
"SWAP13": SWAP13, "SWAP13": SWAP13,
"SWAP14": SWAP14, "SWAP14": SWAP14,
"SWAP15": SWAP15, "SWAP15": SWAP15,
"SWAP16": SWAP16, "SWAP16": SWAP16,
"LOG0": LOG0, "LOG0": LOG0,
"LOG1": LOG1, "LOG1": LOG1,
"LOG2": LOG2, "LOG2": LOG2,
"LOG3": LOG3, "LOG3": LOG3,
"LOG4": LOG4, "LOG4": LOG4,
"CREATE": CREATE, "DATALOAD": DATALOAD,
"CREATE2": CREATE2, "DATALOADN": DATALOADN,
"CALL": CALL, "DATASIZE": DATASIZE,
"RETURN": RETURN, "DATACOPY": DATACOPY,
"CALLCODE": CALLCODE, "RJUMP": RJUMP,
"REVERT": REVERT, "RJUMPI": RJUMPI,
"INVALID": INVALID, "RJUMPV": RJUMPV,
"SELFDESTRUCT": SELFDESTRUCT, "CALLF": CALLF,
"RETF": RETF,
"JUMPF": JUMPF,
"DUPN": DUPN,
"SWAPN": SWAPN,
"EXCHANGE": EXCHANGE,
"EOFCREATE": EOFCREATE,
"RETURNCONTRACT": RETURNCONTRACT,
"CREATE": CREATE,
"CREATE2": CREATE2,
"RETURNDATALOAD": RETURNDATALOAD,
"EXTCALL": EXTCALL,
"EXTDELEGATECALL": EXTDELEGATECALL,
"EXTSTATICCALL": EXTSTATICCALL,
"CALL": CALL,
"RETURN": RETURN,
"CALLCODE": CALLCODE,
"REVERT": REVERT,
"INVALID": INVALID,
"SELFDESTRUCT": SELFDESTRUCT,
} }
// StringToOp finds the opcode whose name is stored in `str`. // StringToOp finds the opcode whose name is stored in `str`.

View file

@ -152,9 +152,9 @@ func gasEip2929AccountCheck(evm *EVM, contract *Contract, stack *Stack, mem *Mem
return 0, nil return 0, nil
} }
func makeCallVariantGasCallEIP2929(oldCalculator gasFunc) gasFunc { func makeCallVariantGasCallEIP2929(oldCalculator gasFunc, addressPosition int) gasFunc {
return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
addr := common.Address(stack.Back(1).Bytes20()) addr := common.Address(stack.Back(addressPosition).Bytes20())
// Check slot presence in the access list // Check slot presence in the access list
warmAccess := evm.StateDB.AddressInAccessList(addr) warmAccess := evm.StateDB.AddressInAccessList(addr)
// The WarmStorageReadCostEIP2929 (100) is already deducted in the form of a constant cost, so // The WarmStorageReadCostEIP2929 (100) is already deducted in the form of a constant cost, so
@ -192,10 +192,10 @@ func makeCallVariantGasCallEIP2929(oldCalculator gasFunc) gasFunc {
} }
var ( var (
gasCallEIP2929 = makeCallVariantGasCallEIP2929(gasCall) gasCallEIP2929 = makeCallVariantGasCallEIP2929(gasCall, 1)
gasDelegateCallEIP2929 = makeCallVariantGasCallEIP2929(gasDelegateCall) gasDelegateCallEIP2929 = makeCallVariantGasCallEIP2929(gasDelegateCall, 1)
gasStaticCallEIP2929 = makeCallVariantGasCallEIP2929(gasStaticCall) gasStaticCallEIP2929 = makeCallVariantGasCallEIP2929(gasStaticCall, 1)
gasCallCodeEIP2929 = makeCallVariantGasCallEIP2929(gasCallCode) gasCallCodeEIP2929 = makeCallVariantGasCallEIP2929(gasCallCode, 1)
gasSelfdestructEIP2929 = makeSelfdestructGasFn(true) gasSelfdestructEIP2929 = makeSelfdestructGasFn(true)
// gasSelfdestructEIP3529 implements the changes in EIP-3529 (no refunds) // gasSelfdestructEIP3529 implements the changes in EIP-3529 (no refunds)
gasSelfdestructEIP3529 = makeSelfdestructGasFn(false) gasSelfdestructEIP3529 = makeSelfdestructGasFn(false)

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

@ -0,0 +1,228 @@
//
// 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"
)
// Below are all possible errors that can occur during validation of
// EOF containers.
var (
ErrInvalidMagic = errors.New("invalid magic")
ErrUndefinedInstruction = errors.New("undefined instruction")
ErrTruncatedImmediate = errors.New("truncated immediate")
ErrInvalidSectionArgument = errors.New("invalid section argument")
ErrInvalidCallArgument = errors.New("callf into non-returning section")
ErrInvalidDataloadNArgument = errors.New("invalid dataloadN argument")
ErrInvalidJumpDest = errors.New("invalid jump destination")
ErrInvalidBackwardJump = errors.New("invalid backward jump")
//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")
ErrEOFCreateWithTruncatedSection = errors.New("eofcreate with truncated section")
ErrOrphanedSubcontainer = errors.New("subcontainer not referenced at all")
ErrIncompatibleContainerKind = errors.New("incompatible container kind")
ErrStopAndReturnContract = errors.New("Stop/Return and Returncontract in the same code section")
ErrStopInInitCode = errors.New("initcode contains a RETURN or STOP opcode")
ErrTruncatedTopLevelContainer = errors.New("truncated top level container")
ErrUnreachableCode = errors.New("unreachable code")
ErrInvalidNonReturningFlag = errors.New("invalid non-returning flag, bad RETF")
ErrInvalidVersion = errors.New("invalid version")
ErrMissingTypeHeader = errors.New("missing type header")
ErrInvalidTypeSize = errors.New("invalid type section size")
ErrMissingCodeHeader = errors.New("missing code header")
ErrInvalidCodeSize = errors.New("invalid code size")
ErrInvalidContainerSectionSize = errors.New("invalid container section 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 outputs")
ErrInvalidSection0Type = errors.New("invalid section 0 type, input and output should be zero and non-returning (0x80)")
ErrTooLargeMaxStackHeight = errors.New("invalid type content, max stack height exceeds limit")
ErrInvalidContainerSize = errors.New("invalid container size")
)
const (
notRefByEither = iota
refByReturnContract
refByEOFCreate
)
type validationResult struct {
visitedCode map[int]struct{}
visitedSubContainers map[int]int
isInitCode bool
isRuntime bool
}
// validateCode validates the code parameter against the EOF v1 validity requirements.
func validateCode(code []byte, section int, container *Container, jt *JumpTable, isInitCode bool) (*validationResult, 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
visitedCode = make(map[int]struct{})
visitedSubcontainers = make(map[int]int)
hasReturnContract bool
hasStop bool
)
// 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 nil, fmt.Errorf("%w: op %s, pos %d", ErrUndefinedInstruction, op, i)
}
size := int(immediates[op])
if size != 0 && len(code) <= i+size {
return nil, fmt.Errorf("%w: op %s, pos %d", ErrTruncatedImmediate, op, i)
}
switch op {
case RJUMP, RJUMPI:
if err := checkDest(code, &analysis, i+1, i+3, len(code)); err != nil {
return nil, err
}
case RJUMPV:
max_size := int(code[i+1])
length := max_size + 1
if len(code) <= i+length {
return nil, fmt.Errorf("%w: jump table truncated, op %s, pos %d", ErrTruncatedImmediate, op, i)
}
offset := i + 2
for j := 0; j < length; j++ {
if err := checkDest(code, &analysis, offset+j*2, offset+(length*2), len(code)); err != nil {
return nil, err
}
}
i += 2 * max_size
case CALLF:
arg, _ := parseUint16(code[i+1:])
if arg >= len(container.types) {
return nil, fmt.Errorf("%w: arg %d, last %d, pos %d", ErrInvalidSectionArgument, arg, len(container.types), i)
}
if container.types[arg].outputs == 0x80 {
return nil, fmt.Errorf("%w: section %v", ErrInvalidCallArgument, arg)
}
visitedCode[arg] = struct{}{}
case JUMPF:
arg, _ := parseUint16(code[i+1:])
if arg >= len(container.types) {
return nil, fmt.Errorf("%w: arg %d, last %d, pos %d", ErrInvalidSectionArgument, arg, len(container.types), i)
}
if container.types[arg].outputs != 0x80 && container.types[arg].outputs > container.types[section].outputs {
return nil, fmt.Errorf("%w: arg %d, last %d, pos %d", ErrInvalidOutputs, arg, len(container.types), i)
}
visitedCode[arg] = struct{}{}
case DATALOADN:
arg, _ := parseUint16(code[i+1:])
// TODO why are we checking this? We should just pad
if arg+32 > len(container.data) {
return nil, fmt.Errorf("%w: arg %d, last %d, pos %d", ErrInvalidDataloadNArgument, arg, len(container.data), i)
}
case RETURNCONTRACT:
if !isInitCode {
return nil, ErrIncompatibleContainerKind
}
arg := int(code[i+1])
if arg >= len(container.sections) {
return nil, fmt.Errorf("%w: arg %d, last %d, pos %d", ErrUnreachableCode, arg, len(container.sections), i)
}
// We need to store per subcontainer how it was referenced
if v, ok := visitedSubcontainers[arg]; ok && v != refByReturnContract {
return nil, fmt.Errorf("section already referenced, arg :%d", arg)
}
if hasStop {
return nil, ErrStopAndReturnContract
}
hasReturnContract = true
visitedSubcontainers[arg] = refByReturnContract
case EOFCREATE:
arg := int(code[i+1])
if arg >= len(container.sections) {
return nil, fmt.Errorf("%w: arg %d, last %d, pos %d", ErrUnreachableCode, arg, len(container.sections), i)
}
if ct := container.sections[arg]; len(ct.data) != ct.dataSize {
return nil, fmt.Errorf("%w: container %d, have %d, claimed %d, pos %d", ErrEOFCreateWithTruncatedSection, arg, len(ct.data), ct.dataSize, i)
}
if _, ok := visitedSubcontainers[arg]; ok {
return nil, fmt.Errorf("section already referenced, arg :%d", arg)
}
// We need to store per subcontainer how it was referenced
if v, ok := visitedSubcontainers[arg]; ok && v != refByEOFCreate {
return nil, fmt.Errorf("section already referenced, arg :%d", arg)
}
visitedSubcontainers[arg] = refByEOFCreate
case STOP, RETURN:
if isInitCode {
return nil, ErrStopInInitCode
}
if hasReturnContract {
return nil, ErrStopAndReturnContract
}
hasStop = true
}
i += size + 1
}
// Code sections may not "fall through" and require proper termination.
// Therefore, the last instruction must be considered terminal or RJUMP.
if !terminals[op] && op != RJUMP {
return nil, fmt.Errorf("%w: end with %s, pos %d", ErrInvalidCodeTermination, op, i)
}
if paths, err := validateControlFlow(code, section, container.types, jt); err != nil {
return nil, err
} else if paths != count {
// TODO(matt): return actual position of unreachable code
return nil, ErrUnreachableCode
}
return &validationResult{
visitedCode: visitedCode,
visitedSubContainers: visitedSubcontainers,
isInitCode: hasReturnContract,
isRuntime: hasStop,
}, 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
}

217
core/vm/validate_linear.go Normal file
View file

@ -0,0 +1,217 @@
package vm
import (
"fmt"
"github.com/ethereum/go-ethereum/params"
)
type bounds struct {
min int
max int
}
func validateControlFlow(code []byte, section int, metadata []*functionMetadata, jt *JumpTable) (int, error) {
var (
stackBounds = make(map[int]*bounds)
maxStackHeight = int(metadata[section].inputs)
debugging = !true
)
setBounds := func(pos, min, maxi int) *bounds {
stackBounds[pos] = &bounds{min, maxi}
maxStackHeight = max(maxStackHeight, maxi)
return stackBounds[pos]
}
// set the initial stack bounds
setBounds(0, int(metadata[section].inputs), int(metadata[section].inputs))
qualifiedExit := false
for pos := 0; pos < len(code); pos++ {
op := OpCode(code[pos])
currentBounds := stackBounds[pos]
if currentBounds == nil {
if debugging {
fmt.Printf("Stack bounds not set: %v at %v \n", op, pos)
}
return 0, ErrUnreachableCode
}
if debugging {
fmt.Println(pos, op, maxStackHeight, currentBounds)
}
var (
currentStackMax = currentBounds.max
currentStackMin = currentBounds.min
)
switch op {
case CALLF:
arg, _ := parseUint16(code[pos+1:])
newSection := metadata[arg]
if want, have := int(newSection.inputs), currentBounds.min; want > have {
return 0, fmt.Errorf("%w: at pos %d", ErrStackUnderflow{stackLen: have, required: want}, pos)
}
if have, limit := currentBounds.max+int(newSection.maxStackHeight)-int(newSection.inputs), int(params.StackLimit); have > limit {
return 0, fmt.Errorf("%w: at pos %d", ErrStackOverflow{stackLen: have, limit: limit}, pos)
}
change := int(newSection.outputs) - int(newSection.inputs)
currentStackMax += change
currentStackMin += change
case RETF:
if currentBounds.max != currentBounds.min {
return 0, fmt.Errorf("%w: max %d, min %d, at pos %d", ErrInvalidOutputs, currentBounds.max, currentBounds.min, pos)
}
have := int(metadata[section].outputs)
if have >= maxOutputItems {
return 0, fmt.Errorf("%w: at pos %d", ErrInvalidNonReturningFlag, pos)
}
if want := currentBounds.min; have != want {
return 0, fmt.Errorf("%w: have %d, want %d, at pos %d", ErrInvalidOutputs, have, want, pos)
}
qualifiedExit = true
case JUMPF:
arg, _ := parseUint16(code[pos+1:])
newSection := metadata[arg]
if have, limit := currentBounds.max+int(newSection.maxStackHeight)-int(newSection.inputs), int(params.StackLimit); have > limit {
return 0, fmt.Errorf("%w: at pos %d", ErrStackOverflow{stackLen: have, limit: limit}, pos)
}
if newSection.outputs == 0x80 {
if want, have := int(newSection.inputs), currentBounds.min; want > have {
return 0, fmt.Errorf("%w: at pos %d", ErrStackUnderflow{stackLen: have, required: want}, pos)
}
} else {
if currentBounds.max != currentBounds.min {
return 0, fmt.Errorf("%w: max %d, min %d, at pos %d", ErrInvalidOutputs, currentBounds.max, currentBounds.min, pos)
}
if have, want := currentBounds.max, int(metadata[section].outputs)+int(newSection.inputs)-int(newSection.outputs); have != want {
return 0, fmt.Errorf("%w: at pos %d", ErrInvalidOutputs, pos)
}
}
qualifiedExit = qualifiedExit || newSection.outputs < maxOutputItems
case DUPN:
arg := int(code[pos+1]) + 1
if want, have := arg, currentBounds.min; want > have {
return 0, fmt.Errorf("%w: at pos %d", ErrStackUnderflow{stackLen: have, required: want}, pos)
}
case SWAPN:
arg := int(code[pos+1]) + 1
if want, have := arg+1, currentBounds.min; want > have {
return 0, fmt.Errorf("%w: at pos %d", ErrStackUnderflow{stackLen: have, required: want}, pos)
}
case EXCHANGE:
arg := int(code[pos+1])
n := arg>>4 + 1
m := arg&0x0f + 1
if want, have := n+m+1, currentBounds.min; want > have {
return 0, fmt.Errorf("%w: at pos %d", ErrStackUnderflow{stackLen: have, required: want}, pos)
}
default:
if want, have := jt[op].minStack, currentBounds.min; want > have {
return 0, fmt.Errorf("%w: at pos %d", ErrStackUnderflow{stackLen: have, required: want}, pos)
}
}
if !terminals[op] && op != CALLF {
change := int(params.StackLimit) - jt[op].maxStack
currentStackMax += change
currentStackMin += change
}
var next []int
switch op {
case RJUMP:
nextPos := pos + 2 + parseInt16(code[pos+1:])
next = append(next, nextPos)
// We set the stack bounds of the destination
// and skip the argument, only for RJUMP, all other opcodes are handled later
if nextPos+1 < pos {
nextBounds, ok := stackBounds[nextPos+1]
if !ok {
return 0, ErrInvalidBackwardJump
}
if nextBounds.max != currentStackMax || nextBounds.min != currentStackMin {
return 0, ErrInvalidMaxStackHeight
}
}
nextBounds, ok := stackBounds[nextPos+1]
if !ok {
setBounds(nextPos+1, currentStackMin, currentStackMax)
} else {
setBounds(nextPos+1, min(nextBounds.min, currentStackMin), max(nextBounds.max, currentStackMax))
}
case RJUMPI:
arg := parseInt16(code[pos+1:])
next = append(next, pos+2)
next = append(next, pos+2+arg)
case RJUMPV:
count := int(code[pos+1]) + 1
next = append(next, pos+1+2*count)
for i := 0; i < count; i++ {
arg := parseInt16(code[pos+2+2*i:])
next = append(next, pos+1+2*count+arg)
}
default:
if imm := int(immediates[op]); imm != 0 {
next = append(next, pos+imm)
} else {
// Simple op, no operand.
next = append(next, pos)
}
}
if debugging {
fmt.Println(next)
}
if op != RJUMP && !terminals[op] {
for _, instr := range next {
nextPC := instr + 1
if nextPC >= len(code) {
return 0, fmt.Errorf("%w: end with %s, pos %d", ErrInvalidCodeTermination, op, pos)
}
if nextPC > pos {
// target reached via forward jump or seq flow
nextBounds, ok := stackBounds[nextPC]
if !ok {
setBounds(nextPC, currentStackMin, currentStackMax)
} else {
setBounds(nextPC, min(nextBounds.min, currentStackMin), max(nextBounds.max, currentStackMax))
}
} else {
// target reached via backwards jump
nextBounds, ok := stackBounds[nextPC]
if !ok {
return 0, ErrInvalidBackwardJump
}
if currentStackMax != nextBounds.max {
return 0, fmt.Errorf("%w want %d as current max got %d at pos %d,", ErrInvalidBackwardJump, currentStackMax, nextBounds.max, pos)
}
if currentStackMin != nextBounds.min {
return 0, fmt.Errorf("%w want %d as current min got %d at pos %d,", ErrInvalidBackwardJump, currentStackMin, nextBounds.min, pos)
}
}
}
}
if op == RJUMP {
pos += 2 // skip the immediate
} else {
pos = next[0]
}
}
if qualifiedExit != (metadata[section].outputs < maxOutputItems) {
return 0, fmt.Errorf("%w no RETF or qualified JUMPF", ErrInvalidNonReturningFlag)
}
if maxStackHeight >= int(params.StackLimit) {
return 0, ErrStackOverflow{maxStackHeight, int(params.StackLimit)}
}
if maxStackHeight != int(metadata[section].maxStackHeight) {
if debugging {
fmt.Print(maxStackHeight, metadata[section].maxStackHeight)
}
return 0, fmt.Errorf("%w in code section %d: have %d, want %d", ErrInvalidMaxStackHeight, section, maxStackHeight, metadata[section].maxStackHeight)
}
return len(stackBounds), nil
}

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

@ -0,0 +1,497 @@
// 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 (
"encoding/binary"
"errors"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/params"
)
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{{inputs: 0, outputs: 0x80, maxStackHeight: 1}},
},
{
code: []byte{
byte(CALLF), 0x00, 0x00,
byte(RETF),
},
section: 0,
metadata: []*functionMetadata{{inputs: 0, outputs: 0, maxStackHeight: 0}},
},
{
code: []byte{
byte(ADDRESS),
byte(CALLF), 0x00, 0x00,
byte(POP),
byte(RETF),
},
section: 0,
metadata: []*functionMetadata{{inputs: 0, outputs: 0, maxStackHeight: 1}},
},
{
code: []byte{
byte(CALLER),
byte(POP),
},
section: 0,
metadata: []*functionMetadata{{inputs: 0, outputs: 0x80, maxStackHeight: 1}},
err: ErrInvalidCodeTermination,
},
{
code: []byte{
byte(RJUMP),
byte(0x00),
byte(0x01),
byte(CALLER),
byte(STOP),
},
section: 0,
metadata: []*functionMetadata{{inputs: 0, outputs: 0x80, maxStackHeight: 0}},
err: ErrUnreachableCode,
},
{
code: []byte{
byte(PUSH1),
byte(0x42),
byte(ADD),
byte(STOP),
},
section: 0,
metadata: []*functionMetadata{{inputs: 0, outputs: 0x80, maxStackHeight: 1}},
err: ErrStackUnderflow{stackLen: 1, required: 2},
},
{
code: []byte{
byte(PUSH1),
byte(0x42),
byte(POP),
byte(STOP),
},
section: 0,
metadata: []*functionMetadata{{inputs: 0, outputs: 0x80, 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{{inputs: 0, outputs: 0x80, maxStackHeight: 1}},
err: ErrInvalidJumpDest,
},
{
code: []byte{
byte(PUSH0),
byte(RJUMPV),
byte(0x01),
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{{inputs: 0, outputs: 0x80, maxStackHeight: 1}},
err: ErrInvalidJumpDest,
},
{
code: []byte{
byte(PUSH0),
byte(RJUMPV),
byte(0x00),
byte(STOP),
},
section: 0,
metadata: []*functionMetadata{{inputs: 0, outputs: 0x80, maxStackHeight: 1}},
err: ErrTruncatedImmediate,
},
{
code: []byte{
byte(RJUMP), 0x00, 0x03,
byte(JUMPDEST), // this code is unreachable to forward jumps alone
byte(JUMPDEST),
byte(RETURN),
byte(PUSH1), 20,
byte(PUSH1), 39,
byte(PUSH1), 0x00,
byte(DATACOPY),
byte(PUSH1), 20,
byte(PUSH1), 0x00,
byte(RJUMP), 0xff, 0xef,
},
section: 0,
metadata: []*functionMetadata{{inputs: 0, outputs: 0x80, maxStackHeight: 3}},
err: ErrUnreachableCode,
},
{
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(DATACOPY),
byte(PUSH1), 20,
byte(PUSH1), 0x00,
byte(RETURN),
},
section: 0,
metadata: []*functionMetadata{{inputs: 0, outputs: 0x80, maxStackHeight: 3}},
},
{
code: []byte{
byte(PUSH1), 1,
byte(RJUMPV), 0x01, 0x00, 0x03, 0xff, 0xf8,
byte(JUMPDEST),
byte(JUMPDEST),
byte(STOP),
byte(PUSH1), 20,
byte(PUSH1), 39,
byte(PUSH1), 0x00,
byte(DATACOPY),
byte(PUSH1), 20,
byte(PUSH1), 0x00,
byte(RETURN),
},
section: 0,
metadata: []*functionMetadata{{inputs: 0, outputs: 0x80, maxStackHeight: 3}},
},
{
code: []byte{
byte(STOP),
byte(STOP),
byte(INVALID),
},
section: 0,
metadata: []*functionMetadata{{inputs: 0, outputs: 0x80, maxStackHeight: 0}},
err: ErrUnreachableCode,
},
{
code: []byte{
byte(RETF),
},
section: 0,
metadata: []*functionMetadata{{inputs: 0, outputs: 1, maxStackHeight: 0}},
err: ErrInvalidOutputs,
},
{
code: []byte{
byte(RETF),
},
section: 0,
metadata: []*functionMetadata{{inputs: 3, outputs: 3, maxStackHeight: 3}},
},
{
code: []byte{
byte(CALLF), 0x00, 0x01,
byte(POP),
byte(STOP),
},
section: 0,
metadata: []*functionMetadata{{inputs: 0, outputs: 0x80, maxStackHeight: 1}, {inputs: 0, outputs: 1, maxStackHeight: 0}},
},
{
code: []byte{
byte(ORIGIN),
byte(ORIGIN),
byte(CALLF), 0x00, 0x01,
byte(POP),
byte(RETF),
},
section: 0,
metadata: []*functionMetadata{{inputs: 0, outputs: 0, maxStackHeight: 2}, {inputs: 2, outputs: 1, maxStackHeight: 2}},
},
} {
container := &Container{
types: test.metadata,
data: make([]byte, 0),
sections: make([]*Container, 0),
}
_, err := validateCode(test.code, test.section, container, &pragueEOFInstructionSet, false)
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)
}
}
}
func BenchmarkRJUMPI(b *testing.B) {
snippet := []byte{
byte(PUSH0),
byte(RJUMPI), 0xFF, 0xFC,
}
code := []byte{}
for i := 0; i < params.MaxCodeSize/len(snippet)-1; i++ {
code = append(code, snippet...)
}
code = append(code, byte(STOP))
container := &Container{
types: []*functionMetadata{{inputs: 0, outputs: 0x80, maxStackHeight: 1}},
data: make([]byte, 0),
sections: make([]*Container, 0),
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := validateCode(code, 0, container, &pragueEOFInstructionSet, true)
if err != nil {
b.Fatal(err)
}
}
}
func BenchmarkRJUMPV(b *testing.B) {
snippet := []byte{
byte(PUSH0),
byte(RJUMPV),
0xff, // count
0x00, 0x00,
}
for i := 0; i < 255; i++ {
snippet = append(snippet, []byte{0x00, 0x00}...)
}
code := []byte{}
for i := 0; i < 24576/len(snippet)-1; i++ {
code = append(code, snippet...)
}
code = append(code, byte(PUSH0))
code = append(code, byte(STOP))
container := &Container{
types: []*functionMetadata{{inputs: 0, outputs: 0x80, maxStackHeight: 1}},
data: make([]byte, 0),
sections: make([]*Container, 0),
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := validateCode(code, 0, container, &pragueEOFInstructionSet, true)
if err != nil {
b.Fatal(err)
}
}
}
func BenchmarkEOFValidation(b *testing.B) {
var container Container
var code []byte
maxSections := 1024
for i := 0; i < maxSections; i++ {
code = append(code, byte(CALLF))
code = binary.BigEndian.AppendUint16(code, uint16(i%(maxSections-1))+1)
}
code = append(code, byte(STOP))
// First container
container.code = append(container.code, code)
container.types = append(container.types, &functionMetadata{inputs: 0, outputs: 0x80, maxStackHeight: 0})
inner := []byte{
byte(STOP),
}
for i := 0; i < 1023; i++ {
container.code = append(container.code, inner)
container.types = append(container.types, &functionMetadata{inputs: 0, outputs: 0x80, maxStackHeight: 0})
}
for i := 0; i < 12; i++ {
container.code[i+1] = code
}
bin := container.MarshalBinary()
if len(bin) > 48*1024 {
b.Fatal("Exceeds 48Kb")
}
var container2 Container
b.ResetTimer()
for i := 0; i < b.N; i++ {
if err := container2.UnmarshalBinary(bin, true); err != nil {
b.Fatal(err)
}
if err := container2.ValidateCode(&pragueEOFInstructionSet, true); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkEOFValidation2(b *testing.B) {
var container Container
var code []byte
maxSections := 1024
for i := 0; i < maxSections; i++ {
code = append(code, byte(CALLF))
code = binary.BigEndian.AppendUint16(code, uint16(i%(maxSections-1))+1)
}
code = append(code, byte(STOP))
// First container
container.code = append(container.code, code)
container.types = append(container.types, &functionMetadata{inputs: 0, outputs: 0x80, maxStackHeight: 0})
inner := []byte{
byte(CALLF), 0x03, 0xE8,
byte(CALLF), 0x03, 0xE9,
byte(CALLF), 0x03, 0xF0,
byte(CALLF), 0x03, 0xF1,
byte(CALLF), 0x03, 0xF2,
byte(CALLF), 0x03, 0xF3,
byte(CALLF), 0x03, 0xF4,
byte(CALLF), 0x03, 0xF5,
byte(CALLF), 0x03, 0xF6,
byte(CALLF), 0x03, 0xF7,
byte(CALLF), 0x03, 0xF8,
byte(JUMPF), 0x00, 0x00,
}
for i := 0; i < 1023; i++ {
container.code = append(container.code, inner)
container.types = append(container.types, &functionMetadata{inputs: 0, outputs: 0x80, maxStackHeight: 0})
}
bin := container.MarshalBinary()
if len(bin) > 48*1024 {
b.Fatal("Exceeds 48Kb")
}
var container2 Container
b.ResetTimer()
for i := 0; i < b.N; i++ {
if err := container2.UnmarshalBinary(bin, true); err != nil {
b.Fatal(err)
}
if err := container2.ValidateCode(&pragueEOFInstructionSet, true); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkEOFValidation3(b *testing.B) {
var container Container
var code []byte
snippet := []byte{
byte(PUSH0),
byte(RJUMPV),
0xff, // count
0x00, 0x00,
}
for i := 0; i < 255; i++ {
snippet = append(snippet, []byte{0x00, 0x00}...)
}
code = append(code, snippet...)
maxSections := 1024
for i := 0; i < maxSections; i++ {
code = append(code, byte(CALLF))
code = binary.BigEndian.AppendUint16(code, uint16(i%(maxSections-1))+1)
}
code = append(code, byte(STOP))
// First container
container.code = append(container.code, code)
container.types = append(container.types, &functionMetadata{inputs: 0, outputs: 0x80, maxStackHeight: 1})
for i := 0; i < 1023; i++ {
container.code = append(container.code, []byte{byte(RJUMP), 0x00, 0x00, byte(JUMPF), 0x00, 0x00})
container.types = append(container.types, &functionMetadata{inputs: 0, outputs: 0x80, maxStackHeight: 0})
}
for i := 0; i < 65; i++ {
container.code[i+1] = append(snippet, byte(STOP))
container.types[i+1] = &functionMetadata{inputs: 0, outputs: 0x80, maxStackHeight: 1}
}
bin := container.MarshalBinary()
if len(bin) > 48*1024 {
b.Fatal("Exceeds 48Kb")
}
b.ResetTimer()
b.ReportMetric(float64(len(bin)), "bytes")
for i := 0; i < b.N; i++ {
for k := 0; k < 40; k++ {
var container2 Container
if err := container2.UnmarshalBinary(bin, true); err != nil {
b.Fatal(err)
}
if err := container2.ValidateCode(&pragueEOFInstructionSet, true); err != nil {
b.Fatal(err)
}
}
}
}
func BenchmarkRJUMPI_2(b *testing.B) {
code := []byte{
byte(PUSH0),
byte(RJUMPI), 0xFF, 0xFC,
}
for i := 0; i < params.MaxCodeSize/4-1; i++ {
code = append(code, byte(PUSH0))
x := -4 * i
code = append(code, byte(RJUMPI))
code = binary.BigEndian.AppendUint16(code, uint16(x))
}
code = append(code, byte(STOP))
container := &Container{
types: []*functionMetadata{{inputs: 0, outputs: 0x80, maxStackHeight: 1}},
data: make([]byte, 0),
sections: make([]*Container, 0),
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := validateCode(code, 0, container, &pragueEOFInstructionSet, true)
if err != nil {
b.Fatal(err)
}
}
}
func FuzzUnmarshalBinary(f *testing.F) {
f.Fuzz(func(_ *testing.T, input []byte) {
var container Container
container.UnmarshalBinary(input, true)
})
}
func FuzzValidate(f *testing.F) {
f.Fuzz(func(_ *testing.T, code []byte, maxStack uint16) {
var container Container
container.types = append(container.types, &functionMetadata{inputs: 0, outputs: 0x80, maxStackHeight: maxStack})
validateCode(code, 0, &container, &pragueEOFInstructionSet, true)
})
}