mirror of https://github.com/ethereum/go-ethereum
core/vm, cmd/evm: implement eof validation (#30418)
The bulk of this PR is authored by @lightclient , in the original EOF-work. More recently, the code has been picked up and reworked for the new EOF specification, by @MariusVanDerWijden , in https://github.com/ethereum/go-ethereum/pull/29518, and also @shemnon has contributed with fixes. This PR is an attempt to start eating the elephant one small bite at a time, by selecting only the eof-validation as a standalone piece which can be merged without interfering too much in the core stuff. In this PR: - [x] Validation of eof containers, lifted from #29518, along with test-vectors from consensus-tests and fuzzing, to ensure that the move did not lose any functionality. - [x] Definition of eof opcodes, which is a prerequisite for validation - [x] Addition of `undefined` to a jumptable entry item. I'm not super-happy with this, but for the moment it seems the least invasive way to do it. A better way might be to go back and allowing nil-items or nil execute-functions to denote "undefined". - [x] benchmarks of eof validation speed --------- 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>pull/30544/head
parent
6416813cbe
commit
56c4f2bfd4
@ -0,0 +1,200 @@ |
|||||||
|
// 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" |
||||||
|
"fmt" |
||||||
|
"io/fs" |
||||||
|
"os" |
||||||
|
"path/filepath" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/core/vm" |
||||||
|
"github.com/ethereum/go-ethereum/log" |
||||||
|
"github.com/urfave/cli/v2" |
||||||
|
) |
||||||
|
|
||||||
|
func init() { |
||||||
|
jt = vm.NewPragueEOFInstructionSetForTesting() |
||||||
|
} |
||||||
|
|
||||||
|
var ( |
||||||
|
jt vm.JumpTable |
||||||
|
initcode = "INITCODE" |
||||||
|
) |
||||||
|
|
||||||
|
func eofParseAction(ctx *cli.Context) error { |
||||||
|
// 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 int |
||||||
|
passedTests int |
||||||
|
) |
||||||
|
err := filepath.Walk(file, func(path string, info fs.FileInfo, err error) error { |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if info.IsDir() { |
||||||
|
return nil |
||||||
|
} |
||||||
|
log.Debug("Executing test", "name", info.Name()) |
||||||
|
passed, tot, err := executeTest(path) |
||||||
|
passedTests += passed |
||||||
|
executedTests += tot |
||||||
|
return err |
||||||
|
}) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
log.Info("Executed tests", "passed", passedTests, "total executed", executedTests) |
||||||
|
return nil |
||||||
|
} |
||||||
|
// 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 { |
||||||
|
return fmt.Errorf("err: %w", err) |
||||||
|
} |
||||||
|
fmt.Println("OK") |
||||||
|
return nil |
||||||
|
} |
||||||
|
// If neither are passed in, read input from stdin.
|
||||||
|
scanner := bufio.NewScanner(os.Stdin) |
||||||
|
scanner.Buffer(make([]byte, 1024*1024), 10*1024*1024) |
||||||
|
for scanner.Scan() { |
||||||
|
l := strings.TrimSpace(scanner.Text()) |
||||||
|
if strings.HasPrefix(l, "#") || l == "" { |
||||||
|
continue |
||||||
|
} |
||||||
|
if _, err := parseAndValidate(l, false); err != nil { |
||||||
|
fmt.Printf("err: %v\n", err) |
||||||
|
} else { |
||||||
|
fmt.Println("OK") |
||||||
|
} |
||||||
|
} |
||||||
|
if err := scanner.Err(); err != nil { |
||||||
|
fmt.Println(err.Error()) |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
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 executeTest(path string) (int, int, error) { |
||||||
|
src, err := os.ReadFile(path) |
||||||
|
if err != nil { |
||||||
|
return 0, 0, err |
||||||
|
} |
||||||
|
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++ |
||||||
|
_, err := parseAndValidate(tt.Code, tt.ContainerKind == initcode) |
||||||
|
if r.Result && err != nil { |
||||||
|
log.Error("Test failure, expected validation success", "name", testsName, "idx", name, "fork", fork, "err", err) |
||||||
|
continue |
||||||
|
} |
||||||
|
if !r.Result && err == nil { |
||||||
|
log.Error("Test failure, expected validation error", "name", testsName, "idx", name, "fork", fork, "have err", r.Exception, "err", err) |
||||||
|
continue |
||||||
|
} |
||||||
|
passed++ |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
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 eofDumpAction(ctx *cli.Context) error { |
||||||
|
// If `--hex` is set, parse and validate the hex string argument.
|
||||||
|
if ctx.IsSet(hexFlag.Name) { |
||||||
|
return eofDump(ctx.String(hexFlag.Name)) |
||||||
|
} |
||||||
|
// Otherwise read from stdin
|
||||||
|
scanner := bufio.NewScanner(os.Stdin) |
||||||
|
scanner.Buffer(make([]byte, 1024*1024), 10*1024*1024) |
||||||
|
for scanner.Scan() { |
||||||
|
l := strings.TrimSpace(scanner.Text()) |
||||||
|
if strings.HasPrefix(l, "#") || l == "" { |
||||||
|
continue |
||||||
|
} |
||||||
|
if err := eofDump(l); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
fmt.Println("") |
||||||
|
} |
||||||
|
return scanner.Err() |
||||||
|
} |
||||||
|
|
||||||
|
func eofDump(hexdata string) error { |
||||||
|
if len(hexdata) >= 2 && strings.HasPrefix(hexdata, "0x") { |
||||||
|
hexdata = hexdata[2:] |
||||||
|
} |
||||||
|
b, err := hex.DecodeString(hexdata) |
||||||
|
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.Println(c.String()) |
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,166 @@ |
|||||||
|
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/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) { |
||||||
|
t.Fatal("Unmarshal-> Marshal failure!") |
||||||
|
} |
||||||
|
} |
||||||
|
if err := c.UnmarshalBinary(data, false); err == nil { |
||||||
|
c.ValidateCode(&jt, false) |
||||||
|
if have := c.MarshalBinary(); !bytes.Equal(have, data) { |
||||||
|
t.Fatal("Unmarshal-> Marshal failure!") |
||||||
|
} |
||||||
|
} |
||||||
|
if !bytes.Equal(cpy, data) { |
||||||
|
panic("data modified during unmarshalling") |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func TestEofParseInitcode(t *testing.T) { |
||||||
|
testEofParse(t, true, "testdata/eof/results.initcode.txt") |
||||||
|
} |
||||||
|
|
||||||
|
func TestEofParseRegular(t *testing.T) { |
||||||
|
testEofParse(t, false, "testdata/eof/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/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.Errorf("%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/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++ |
||||||
|
} |
||||||
|
} |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,98 @@ |
|||||||
|
// 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++ |
||||||
|
|
||||||
|
if op == RJUMPV { |
||||||
|
// RJUMPV is unique as it has a variable sized operand.
|
||||||
|
// The total size is determined by the count byte which
|
||||||
|
// immediate follows 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) |
||||||
|
} |
||||||
|
} else { |
||||||
|
numbits = uint16(Immediates(op)) |
||||||
|
if numbits == 0 { |
||||||
|
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 |
||||||
|
} |
@ -0,0 +1,501 @@ |
|||||||
|
// 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 |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"encoding/binary" |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"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 |
||||||
|
codeSections [][]byte |
||||||
|
subContainers []*Container |
||||||
|
subContainerCodes [][]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 |
||||||
|
} |
||||||
|
|
||||||
|
// stackDelta returns the #outputs - #inputs
|
||||||
|
func (meta *functionMetadata) stackDelta() int { |
||||||
|
return int(meta.outputs) - int(meta.inputs) |
||||||
|
} |
||||||
|
|
||||||
|
// checkInputs checks the current minimum stack (stackMin) against the required inputs
|
||||||
|
// of the metadata, and returns an error if the stack is too shallow.
|
||||||
|
func (meta *functionMetadata) checkInputs(stackMin int) error { |
||||||
|
if int(meta.inputs) > stackMin { |
||||||
|
return ErrStackUnderflow{stackLen: stackMin, required: int(meta.inputs)} |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// checkStackMax checks the if current maximum stack combined with the
|
||||||
|
// functin max stack will result in a stack overflow, and if so returns an error.
|
||||||
|
func (meta *functionMetadata) checkStackMax(stackMax int) error { |
||||||
|
newMaxStack := stackMax + int(meta.maxStackHeight) - int(meta.inputs) |
||||||
|
if newMaxStack > int(params.StackLimit) { |
||||||
|
return ErrStackOverflow{stackLen: newMaxStack, limit: int(params.StackLimit)} |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// 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.codeSections))) |
||||||
|
for _, codeSection := range c.codeSections { |
||||||
|
b = binary.BigEndian.AppendUint16(b, uint16(len(codeSection))) |
||||||
|
} |
||||||
|
var encodedContainer [][]byte |
||||||
|
if len(c.subContainers) != 0 { |
||||||
|
b = append(b, kindContainer) |
||||||
|
b = binary.BigEndian.AppendUint16(b, uint16(len(c.subContainers))) |
||||||
|
for _, section := range c.subContainers { |
||||||
|
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.codeSections { |
||||||
|
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.unmarshalContainer(b, isInitcode, true) |
||||||
|
} |
||||||
|
|
||||||
|
// UnmarshalSubContainer decodes an EOF container that is inside another container.
|
||||||
|
func (c *Container) UnmarshalSubContainer(b []byte, isInitcode bool) error { |
||||||
|
return c.unmarshalContainer(b, isInitcode, false) |
||||||
|
} |
||||||
|
|
||||||
|
func (c *Container) unmarshalContainer(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 (!topLevel || !isInitcode) && len(b) > expectedSize { |
||||||
|
return fmt.Errorf("%w: have %d, want %d", errInvalidContainerSize, len(b), expectedSize) |
||||||
|
} |
||||||
|
|
||||||
|
// Parse types section.
|
||||||
|
idx := offsetTerminator + 1 |
||||||
|
var types = make([]*functionMetadata, 0, typesSize/4) |
||||||
|
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 |
||||||
|
codeSections := 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) |
||||||
|
} |
||||||
|
codeSections[i] = b[idx : idx+size] |
||||||
|
idx += size |
||||||
|
} |
||||||
|
c.codeSections = codeSections |
||||||
|
// 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)) |
||||||
|
} |
||||||
|
subContainerCodes := make([][]byte, 0, len(containerSizes)) |
||||||
|
subContainers := 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) |
||||||
|
} |
||||||
|
subC := new(Container) |
||||||
|
end := min(idx+size, len(b)) |
||||||
|
if err := subC.unmarshalContainer(b[idx:end], isInitcode, false); err != nil { |
||||||
|
if topLevel { |
||||||
|
return fmt.Errorf("%w in sub container %d", err, i) |
||||||
|
} |
||||||
|
return err |
||||||
|
} |
||||||
|
subContainers = append(subContainers, subC) |
||||||
|
subContainerCodes = append(subContainerCodes, b[idx:end]) |
||||||
|
|
||||||
|
idx += size |
||||||
|
} |
||||||
|
c.subContainers = subContainers |
||||||
|
c.subContainerCodes = subContainerCodes |
||||||
|
} |
||||||
|
|
||||||
|
//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.codeSections[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.codeSections) { |
||||||
|
return errUnreachableCode |
||||||
|
} |
||||||
|
for idx, container := range c.subContainers { |
||||||
|
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 output = []string{ |
||||||
|
"Header", |
||||||
|
fmt.Sprintf(" - EOFMagic: %02x", eofMagic), |
||||||
|
fmt.Sprintf(" - EOFVersion: %02x", eof1Version), |
||||||
|
fmt.Sprintf(" - KindType: %02x", kindTypes), |
||||||
|
fmt.Sprintf(" - TypesSize: %04x", len(c.types)*4), |
||||||
|
fmt.Sprintf(" - KindCode: %02x", kindCode), |
||||||
|
fmt.Sprintf(" - KindData: %02x", kindData), |
||||||
|
fmt.Sprintf(" - DataSize: %04x", len(c.data)), |
||||||
|
fmt.Sprintf(" - Number of code sections: %d", len(c.codeSections)), |
||||||
|
} |
||||||
|
for i, code := range c.codeSections { |
||||||
|
output = append(output, fmt.Sprintf(" - Code section %d length: %04x", i, len(code))) |
||||||
|
} |
||||||
|
|
||||||
|
output = append(output, fmt.Sprintf(" - Number of subcontainers: %d", len(c.subContainers))) |
||||||
|
if len(c.subContainers) > 0 { |
||||||
|
for i, section := range c.subContainers { |
||||||
|
output = append(output, fmt.Sprintf(" - subcontainer %d length: %04x\n", i, len(section.MarshalBinary()))) |
||||||
|
} |
||||||
|
} |
||||||
|
output = append(output, "Body") |
||||||
|
for i, typ := range c.types { |
||||||
|
output = append(output, fmt.Sprintf(" - Type %v: %x", i, |
||||||
|
[]byte{typ.inputs, typ.outputs, byte(typ.maxStackHeight >> 8), byte(typ.maxStackHeight & 0x00ff)})) |
||||||
|
} |
||||||
|
for i, code := range c.codeSections { |
||||||
|
output = append(output, fmt.Sprintf(" - Code section %d: %#x", i, code)) |
||||||
|
} |
||||||
|
for i, section := range c.subContainers { |
||||||
|
output = append(output, fmt.Sprintf(" - Subcontainer %d: %x", i, section.MarshalBinary())) |
||||||
|
} |
||||||
|
output = append(output, fmt.Sprintf(" - Data: %#x", c.data)) |
||||||
|
return strings.Join(output, "\n") |
||||||
|
} |
@ -0,0 +1,235 @@ |
|||||||
|
// Copyright 2024 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 vm |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/params" |
||||||
|
) |
||||||
|
|
||||||
|
func validateControlFlow(code []byte, section int, metadata []*functionMetadata, jt *JumpTable) (int, error) { |
||||||
|
var ( |
||||||
|
maxStackHeight = int(metadata[section].inputs) |
||||||
|
visitCount = 0 |
||||||
|
next = make([]int, 0, 1) |
||||||
|
) |
||||||
|
var ( |
||||||
|
stackBoundsMax = make([]uint16, len(code)) |
||||||
|
stackBoundsMin = make([]uint16, len(code)) |
||||||
|
) |
||||||
|
setBounds := func(pos, min, maxi int) { |
||||||
|
// The stackboundMax slice is a bit peculiar. We use `0` to denote
|
||||||
|
// not set. Therefore, we use `1` to represent the value `0`, and so on.
|
||||||
|
// So if the caller wants to store `1` as max bound, we internally store it as
|
||||||
|
// `2`.
|
||||||
|
if stackBoundsMax[pos] == 0 { // Not yet set
|
||||||
|
visitCount++ |
||||||
|
} |
||||||
|
if maxi < 65535 { |
||||||
|
stackBoundsMax[pos] = uint16(maxi + 1) |
||||||
|
} |
||||||
|
stackBoundsMin[pos] = uint16(min) |
||||||
|
maxStackHeight = max(maxStackHeight, maxi) |
||||||
|
} |
||||||
|
getStackMaxMin := func(pos int) (ok bool, min, max int) { |
||||||
|
maxi := stackBoundsMax[pos] |
||||||
|
if maxi == 0 { // Not yet set
|
||||||
|
return false, 0, 0 |
||||||
|
} |
||||||
|
return true, int(stackBoundsMin[pos]), int(maxi - 1) |
||||||
|
} |
||||||
|
// 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]) |
||||||
|
ok, currentStackMin, currentStackMax := getStackMaxMin(pos) |
||||||
|
if !ok { |
||||||
|
return 0, errUnreachableCode |
||||||
|
} |
||||||
|
|
||||||
|
switch op { |
||||||
|
case CALLF: |
||||||
|
arg, _ := parseUint16(code[pos+1:]) |
||||||
|
newSection := metadata[arg] |
||||||
|
if err := newSection.checkInputs(currentStackMin); err != nil { |
||||||
|
return 0, fmt.Errorf("%w: at pos %d", err, pos) |
||||||
|
} |
||||||
|
if err := newSection.checkStackMax(currentStackMax); err != nil { |
||||||
|
return 0, fmt.Errorf("%w: at pos %d", err, pos) |
||||||
|
} |
||||||
|
delta := newSection.stackDelta() |
||||||
|
currentStackMax += delta |
||||||
|
currentStackMin += delta |
||||||
|
case RETF: |
||||||
|
/* From the spec: |
||||||
|
> for RETF the following must hold: stack_height_max == stack_height_min == types[current_code_index].outputs, |
||||||
|
|
||||||
|
In other words: RETF must unambiguously return all items remaining on the stack. |
||||||
|
*/ |
||||||
|
if currentStackMax != currentStackMin { |
||||||
|
return 0, fmt.Errorf("%w: max %d, min %d, at pos %d", errInvalidOutputs, currentStackMax, currentStackMin, pos) |
||||||
|
} |
||||||
|
numOutputs := int(metadata[section].outputs) |
||||||
|
if numOutputs >= maxOutputItems { |
||||||
|
return 0, fmt.Errorf("%w: at pos %d", errInvalidNonReturningFlag, pos) |
||||||
|
} |
||||||
|
if numOutputs != currentStackMin { |
||||||
|
return 0, fmt.Errorf("%w: have %d, want %d, at pos %d", errInvalidOutputs, numOutputs, currentStackMin, pos) |
||||||
|
} |
||||||
|
qualifiedExit = true |
||||||
|
case JUMPF: |
||||||
|
arg, _ := parseUint16(code[pos+1:]) |
||||||
|
newSection := metadata[arg] |
||||||
|
|
||||||
|
if err := newSection.checkStackMax(currentStackMax); err != nil { |
||||||
|
return 0, fmt.Errorf("%w: at pos %d", err, pos) |
||||||
|
} |
||||||
|
|
||||||
|
if newSection.outputs == 0x80 { |
||||||
|
if err := newSection.checkInputs(currentStackMin); err != nil { |
||||||
|
return 0, fmt.Errorf("%w: at pos %d", err, pos) |
||||||
|
} |
||||||
|
} else { |
||||||
|
if currentStackMax != currentStackMin { |
||||||
|
return 0, fmt.Errorf("%w: max %d, min %d, at pos %d", errInvalidOutputs, currentStackMax, currentStackMin, pos) |
||||||
|
} |
||||||
|
wantStack := int(metadata[section].outputs) - newSection.stackDelta() |
||||||
|
if currentStackMax != wantStack { |
||||||
|
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, currentStackMin; 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, currentStackMin; 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, currentStackMin; want > have { |
||||||
|
return 0, fmt.Errorf("%w: at pos %d", ErrStackUnderflow{stackLen: have, required: want}, pos) |
||||||
|
} |
||||||
|
default: |
||||||
|
if want, have := jt[op].minStack, currentStackMin; 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 |
||||||
|
} |
||||||
|
next = next[:0] |
||||||
|
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 { |
||||||
|
ok, nextMin, nextMax := getStackMaxMin(nextPos + 1) |
||||||
|
if !ok { |
||||||
|
return 0, errInvalidBackwardJump |
||||||
|
} |
||||||
|
if nextMax != currentStackMax || nextMin != currentStackMin { |
||||||
|
return 0, errInvalidMaxStackHeight |
||||||
|
} |
||||||
|
} else { |
||||||
|
ok, nextMin, nextMax := getStackMaxMin(nextPos + 1) |
||||||
|
if !ok { |
||||||
|
setBounds(nextPos+1, currentStackMin, currentStackMax) |
||||||
|
} else { |
||||||
|
setBounds(nextPos+1, min(nextMin, currentStackMin), max(nextMax, 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 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
|
||||||
|
ok, nextMin, nextMax := getStackMaxMin(nextPC) |
||||||
|
if !ok { |
||||||
|
setBounds(nextPC, currentStackMin, currentStackMax) |
||||||
|
} else { |
||||||
|
setBounds(nextPC, min(nextMin, currentStackMin), max(nextMax, currentStackMax)) |
||||||
|
} |
||||||
|
} else { |
||||||
|
// target reached via backwards jump
|
||||||
|
ok, nextMin, nextMax := getStackMaxMin(nextPC) |
||||||
|
if !ok { |
||||||
|
return 0, errInvalidBackwardJump |
||||||
|
} |
||||||
|
if currentStackMax != nextMax { |
||||||
|
return 0, fmt.Errorf("%w want %d as current max got %d at pos %d,", errInvalidBackwardJump, currentStackMax, nextMax, pos) |
||||||
|
} |
||||||
|
if currentStackMin != nextMin { |
||||||
|
return 0, fmt.Errorf("%w want %d as current min got %d at pos %d,", errInvalidBackwardJump, currentStackMin, nextMin, 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) { |
||||||
|
return 0, fmt.Errorf("%w in code section %d: have %d, want %d", errInvalidMaxStackHeight, section, maxStackHeight, metadata[section].maxStackHeight) |
||||||
|
} |
||||||
|
return visitCount, nil |
||||||
|
} |
@ -0,0 +1,70 @@ |
|||||||
|
// 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 |
||||||
|
} |
||||||
|
|
||||||
|
// Immediates returns the number bytes of immediates (argument not from
|
||||||
|
// stack but from code) a given opcode has.
|
||||||
|
// OBS:
|
||||||
|
// - This function assumes EOF instruction-set. It cannot be upon in
|
||||||
|
// a. pre-EOF code
|
||||||
|
// b. post-EOF but legacy code
|
||||||
|
// - RJUMPV is unique as it has a variable sized operand. The total size is
|
||||||
|
// determined by the count byte which immediately follows RJUMPV. This method
|
||||||
|
// will return '3' for RJUMPV, which is the minimum.
|
||||||
|
func Immediates(op OpCode) int { |
||||||
|
return int(immediates[op]) |
||||||
|
} |
@ -0,0 +1,112 @@ |
|||||||
|
// 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 |
||||||
|
|
||||||
|
// 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") |
||||||
|
} |
@ -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}}, |
||||||
|
codeSections: [][]byte{common.Hex2Bytes("604200")}, |
||||||
|
data: []byte{0x01, 0x02, 0x03}, |
||||||
|
dataSize: 3, |
||||||
|
}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
want: Container{ |
||||||
|
types: []*functionMetadata{{inputs: 0, outputs: 0x80, maxStackHeight: 1}}, |
||||||
|
codeSections: [][]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}, |
||||||
|
}, |
||||||
|
codeSections: [][]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}}, |
||||||
|
codeSections: [][]byte{common.Hex2Bytes("604200")}, |
||||||
|
subContainers: []*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) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,255 @@ |
|||||||
|
// 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 |
||||||
|
|
||||||
|
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") |
||||||
|
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 map[int]struct{} |
||||||
|
visitedSubcontainers 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) |
||||||
|
} |
||||||
|
if visitedCode == nil { |
||||||
|
visitedCode = make(map[int]struct{}) |
||||||
|
} |
||||||
|
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) |
||||||
|
} |
||||||
|
if visitedCode == nil { |
||||||
|
visitedCode = make(map[int]struct{}) |
||||||
|
} |
||||||
|
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.subContainers) { |
||||||
|
return nil, fmt.Errorf("%w: arg %d, last %d, pos %d", errUnreachableCode, arg, len(container.subContainers), i) |
||||||
|
} |
||||||
|
if visitedSubcontainers == nil { |
||||||
|
visitedSubcontainers = make(map[int]int) |
||||||
|
} |
||||||
|
// 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.subContainers) { |
||||||
|
return nil, fmt.Errorf("%w: arg %d, last %d, pos %d", errUnreachableCode, arg, len(container.subContainers), i) |
||||||
|
} |
||||||
|
if ct := container.subContainers[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 visitedSubcontainers == nil { |
||||||
|
visitedSubcontainers = make(map[int]int) |
||||||
|
} |
||||||
|
// 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 |
||||||
|
} |
||||||
|
|
||||||
|
//// disasm is a helper utility to show a sequence of comma-separated operations,
|
||||||
|
//// with immediates shown inline,
|
||||||
|
//// e.g: PUSH1(0x00),EOFCREATE(0x00),
|
||||||
|
//func disasm(code []byte) string {
|
||||||
|
// var ops []string
|
||||||
|
// for i := 0; i < len(code); i++ {
|
||||||
|
// var op string
|
||||||
|
// if args := immediates[code[i]]; args > 0 {
|
||||||
|
// op = fmt.Sprintf("%v(%#x)", OpCode(code[i]).String(), code[i+1:i+1+int(args)])
|
||||||
|
// i += int(args)
|
||||||
|
// } else {
|
||||||
|
// op = OpCode(code[i]).String()
|
||||||
|
// }
|
||||||
|
// ops = append(ops, op)
|
||||||
|
// }
|
||||||
|
// return strings.Join(ops, ",")
|
||||||
|
//}
|
@ -0,0 +1,517 @@ |
|||||||
|
// 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 |
||||||
|
|
||||||
|
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), |
||||||
|
subContainers: 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) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// BenchmarkRJUMPI tries to benchmark the RJUMPI opcode validation
|
||||||
|
// For this we do a bunch of RJUMPIs that jump backwards (in a potential infinite loop).
|
||||||
|
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), |
||||||
|
subContainers: make([]*Container, 0), |
||||||
|
} |
||||||
|
b.ResetTimer() |
||||||
|
for i := 0; i < b.N; i++ { |
||||||
|
_, err := validateCode(code, 0, container, &pragueEOFInstructionSet, false) |
||||||
|
if err != nil { |
||||||
|
b.Fatal(err) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// BenchmarkRJUMPV tries to benchmark the validation of the RJUMPV opcode
|
||||||
|
// for this we set up as many RJUMPV opcodes with a full jumptable (containing 0s) as possible.
|
||||||
|
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), |
||||||
|
subContainers: make([]*Container, 0), |
||||||
|
} |
||||||
|
b.ResetTimer() |
||||||
|
for i := 0; i < b.N; i++ { |
||||||
|
_, err := validateCode(code, 0, container, &pragueEOFInstructionSet, false) |
||||||
|
if err != nil { |
||||||
|
b.Fatal(err) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// BenchmarkEOFValidation tries to benchmark the code validation for the CALLF/RETF operation.
|
||||||
|
// For this we set up code that calls into 1024 code sections which can either
|
||||||
|
// - just contain a RETF opcode
|
||||||
|
// - or code to again call into 1024 code sections.
|
||||||
|
// We can't have all code sections calling each other, otherwise we would exceed 48KB.
|
||||||
|
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) |
||||||
|
} |
||||||
|
// First container
|
||||||
|
container.codeSections = append(container.codeSections, append(code, byte(STOP))) |
||||||
|
container.types = append(container.types, &functionMetadata{inputs: 0, outputs: 0x80, maxStackHeight: 0}) |
||||||
|
|
||||||
|
inner := []byte{ |
||||||
|
byte(RETF), |
||||||
|
} |
||||||
|
|
||||||
|
for i := 0; i < 1023; i++ { |
||||||
|
container.codeSections = append(container.codeSections, inner) |
||||||
|
container.types = append(container.types, &functionMetadata{inputs: 0, outputs: 0, maxStackHeight: 0}) |
||||||
|
} |
||||||
|
|
||||||
|
for i := 0; i < 12; i++ { |
||||||
|
container.codeSections[i+1] = append(code, byte(RETF)) |
||||||
|
} |
||||||
|
|
||||||
|
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, false); err != nil { |
||||||
|
b.Fatal(err) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// BenchmarkEOFValidation tries to benchmark the code validation for the CALLF/RETF operation.
|
||||||
|
// For this we set up code that calls into 1024 code sections which
|
||||||
|
// - contain calls to some other code sections.
|
||||||
|
// We can't have all code sections calling each other, otherwise we would exceed 48KB.
|
||||||
|
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.codeSections = append(container.codeSections, 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(CALLF), 0x03, 0xF, |
||||||
|
byte(RETF), |
||||||
|
} |
||||||
|
|
||||||
|
for i := 0; i < 1023; i++ { |
||||||
|
container.codeSections = append(container.codeSections, inner) |
||||||
|
container.types = append(container.types, &functionMetadata{inputs: 0, outputs: 0, 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, false); err != nil { |
||||||
|
b.Fatal(err) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// BenchmarkEOFValidation3 tries to benchmark the code validation for the CALLF/RETF and RJUMPI/V operations.
|
||||||
|
// For this we set up code that calls into 1024 code sections which either
|
||||||
|
// - contain an RJUMP opcode
|
||||||
|
// - contain calls to other code sections
|
||||||
|
// We can't have all code sections calling each other, otherwise we would exceed 48KB.
|
||||||
|
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...) |
||||||
|
// First container, calls into all other containers
|
||||||
|
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)) |
||||||
|
container.codeSections = append(container.codeSections, code) |
||||||
|
container.types = append(container.types, &functionMetadata{inputs: 0, outputs: 0x80, maxStackHeight: 1}) |
||||||
|
|
||||||
|
// Other containers
|
||||||
|
for i := 0; i < 1023; i++ { |
||||||
|
container.codeSections = append(container.codeSections, []byte{byte(RJUMP), 0x00, 0x00, byte(RETF)}) |
||||||
|
container.types = append(container.types, &functionMetadata{inputs: 0, outputs: 0, maxStackHeight: 0}) |
||||||
|
} |
||||||
|
// Other containers
|
||||||
|
for i := 0; i < 68; i++ { |
||||||
|
container.codeSections[i+1] = append(snippet, byte(RETF)) |
||||||
|
container.types[i+1] = &functionMetadata{inputs: 0, outputs: 0, 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, false); 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), |
||||||
|
subContainers: make([]*Container, 0), |
||||||
|
} |
||||||
|
b.ResetTimer() |
||||||
|
for i := 0; i < b.N; i++ { |
||||||
|
_, err := validateCode(code, 0, container, &pragueEOFInstructionSet, false) |
||||||
|
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) |
||||||
|
}) |
||||||
|
} |
Loading…
Reference in new issue