core/vm: refactor + speed up eof validation

core/vm: some clarifications in the eof code
core/vm: clarifications + minor speedup
core/vm: clarifications + lint + minor speedup
core/vm, core/asm: support eof in asm instruction iteration
core/vm: comment out unused
core/vm: remove gasfunctions
Martin Holst Swende 2 months ago
parent a3031cd407
commit 8783059c80
No known key found for this signature in database
GPG Key ID: 683B438C05A5DDF0
  1. 1
      cmd/eofdump/eofparser.go
  2. 5
      cmd/eofdump/parse_test.go
  3. 8
      core/asm/asm.go
  4. 9
      core/asm/asm_test.go
  5. 4
      core/vm/analysis_eof.go
  6. 7
      core/vm/eips.go
  7. 73
      core/vm/eof.go
  8. 6
      core/vm/eof_immediates.go
  9. 5
      core/vm/eof_instructions.go
  10. 28
      core/vm/eof_test.go
  11. 60
      core/vm/gas_table.go
  12. 2
      core/vm/opcodes.go
  13. 44
      core/vm/validate.go
  14. 111
      core/vm/validate_linear.go
  15. 40
      core/vm/validate_test.go

@ -215,7 +215,6 @@ func parseAndValidate(s string, isInitCode bool) (*vm.Container, error) {
}
func parse(b []byte, isInitCode bool) (*vm.Container, error) {
var c vm.Container
if err := c.UnmarshalBinary(b, isInitCode); err != nil {
return nil, err

@ -50,13 +50,13 @@ func FuzzEofParsing(f *testing.F) {
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!")
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) {
f.Fatal("Unmarshal-> Marshal failure!")
t.Fatal("Unmarshal-> Marshal failure!")
}
}
if !bytes.Equal(cpy, data) {
@ -131,7 +131,6 @@ func testEofParse(t *testing.T, isInitCode bool, wantFile string) {
}
}
line++
}
corpus.Close()
}

@ -65,11 +65,11 @@ func (it *instructionIterator) Next() bool {
}
it.op = vm.OpCode(it.code[it.pc])
if it.op.IsPush() {
a := uint64(it.op) - uint64(vm.PUSH0)
u := it.pc + 1 + a
if a := vm.Immediates(it.op); a > 0 {
u := it.pc + 1 + uint64(a)
if uint64(len(it.code)) <= it.pc || uint64(len(it.code)) < u {
it.error = fmt.Errorf("incomplete push instruction at %v", it.pc)
it.error = fmt.Errorf("incomplete instruction at %v", it.pc)
return false
}
it.arg = it.code[it.pc+1 : u]

@ -29,10 +29,11 @@ func TestInstructionIterator(t *testing.T) {
code string
wantErr string
}{
{2, "61000000", ""}, // valid code
{0, "6100", "incomplete push instruction at 0"}, // invalid code
{2, "5900", ""}, // push0
{0, "", ""}, // empty
{2, "61000000", ""}, // valid code
{0, "6100", "incomplete instruction at 0"}, // invalid code
{2, "5900", ""}, // push0
{0, "", ""}, // empty
{2, "d1aabb00", ""}, // DATALOADN(aabb),STOP
} {
var (

@ -36,7 +36,9 @@ func eofCodeBitmapInternal(code, bits bitvec) bitvec {
pc++
switch {
case op >= PUSH1 && op <= PUSH32:
case op < PUSH1:
continue
case op <= PUSH32:
numbits = uint16(op - PUSH1 + 1)
case op == RJUMP || op == RJUMPI || op == CALLF || op == JUMPF || op == DATALOADN:
numbits = 2

@ -535,6 +535,13 @@ func enable4762(jt *JumpTable) {
}
// enableEOF applies the EOF changes.
// OBS! For EOF, there are two changes:
// 1. Two separate jumptables are required. One, EOF-jumptable, is used by
// eof contracts. This one contains things like RJUMP.
// 2. The regular non-eof jumptable also needs to be modified, specifically to
// modify how EXTCODECOPY works under the hood.
//
// This method _only_ deals with case 1.
func enableEOF(jt *JumpTable) {
// Deprecate opcodes
undefined := &operation{

@ -66,12 +66,12 @@ func isEOFVersion1(code []byte) bool {
// 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)
types []*functionMetadata
codeSections [][]byte
subContainers []*Container
subContainerCodes [][]byte
data []byte
dataSize int // might be more than len(data)
}
// functionMetadata is an EOF function signature.
@ -92,15 +92,15 @@ func (c *Container) MarshalBinary() []byte {
b = append(b, kindTypes)
b = binary.BigEndian.AppendUint16(b, uint16(len(c.types)*4))
b = append(b, kindCode)
b = binary.BigEndian.AppendUint16(b, uint16(len(c.code)))
for _, code := range c.code {
b = binary.BigEndian.AppendUint16(b, uint16(len(code)))
b = 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.sections) != 0 {
if len(c.subContainers) != 0 {
b = append(b, kindContainer)
b = binary.BigEndian.AppendUint16(b, uint16(len(c.sections)))
for _, section := range c.sections {
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)
@ -114,7 +114,7 @@ func (c *Container) MarshalBinary() []byte {
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 {
for _, code := range c.codeSections {
b = append(b, code...)
}
for _, section := range encodedContainer {
@ -228,7 +228,7 @@ func (c *Container) unmarshalSubContainer(b []byte, isInitcode bool, topLevel bo
// Parse types section.
idx := offsetTerminator + 1
var types []*functionMetadata
var types = make([]*functionMetadata, 0, typesSize/4)
for i := 0; i < typesSize/4; i++ {
sig := &functionMetadata{
inputs: b[idx+i*4],
@ -253,45 +253,44 @@ func (c *Container) unmarshalSubContainer(b []byte, isInitcode bool, topLevel bo
// Parse code sections.
idx += typesSize
code := make([][]byte, len(codeSizes))
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)
}
code[i] = b[idx : idx+size]
codeSections[i] = b[idx : idx+size]
idx += size
}
c.code = code
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))
}
containerCode := make([][]byte, 0, len(containerSizes))
container := make([]*Container, 0, 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)
}
c := new(Container)
subC := new(Container)
end := min(idx+size, len(b))
if err := c.unmarshalSubContainer(b[idx:end], isInitcode, false); err != nil {
if err := subC.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])
subContainers = append(subContainers, subC)
subContainerCodes = append(subContainerCodes, b[idx:end])
idx += size
}
c.sections = container
c.containerCode = containerCode
c.subContainers = subContainers
c.subContainerCodes = subContainerCodes
}
// Parse data section.
//Parse data section.
end := len(b)
if !isInitcode {
end = min(idx+dataSize, len(b))
@ -327,7 +326,7 @@ func (c *Container) validateSubContainer(jt *JumpTable, refBy int) error {
// should not mean 2 and 3 should be visited twice
var (
index = toVisit[0]
code = c.code[index]
code = c.codeSections[index]
)
if _, ok := visited[index]; !ok {
res, err := validateCode(code, index, c, jt, refBy == refByEOFCreate)
@ -359,10 +358,10 @@ func (c *Container) validateSubContainer(jt *JumpTable, refBy int) error {
toVisit = toVisit[1:]
}
// Make sure every code section is visited at least once.
if len(visited) != len(c.code) {
if len(visited) != len(c.codeSections) {
return ErrUnreachableCode
}
for idx, container := range c.sections {
for idx, container := range c.subContainers {
reference, ok := subContainerVisited[idx]
if !ok {
return ErrOrphanedSubcontainer
@ -444,14 +443,14 @@ func (c *Container) String() string {
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("CodeSize: %04x\n", len(c.codeSections))
for i, code := range c.codeSections {
result += fmt.Sprintf("Code %v length: %04x\n", i, len(code))
}
if len(c.sections) != 0 {
if len(c.subContainers) != 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("ContainerSize: %04x\n", len(c.subContainers))
for i, section := range c.subContainers {
result += fmt.Sprintf("Container %v length: %04x\n", i, len(section.MarshalBinary()))
}
}
@ -464,10 +463,10 @@ func (c *Container) String() string {
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 {
for i, code := range c.codeSections {
result += fmt.Sprintf("Code %v: %v\n", i, hex.EncodeToString(code))
}
for i, section := range c.sections {
for i, section := range c.subContainers {
result += fmt.Sprintf("Section %v: %v\n", i, hex.EncodeToString(section.MarshalBinary()))
}
result += fmt.Sprintf("Data: %v\n", hex.EncodeToString(c.data))

@ -55,3 +55,9 @@ func init() {
terminals[REVERT] = true
terminals[INVALID] = true
}
// Immediates returns the number bytes of immediates (argument not from
// stack but from code) a given opcode has.
func Immediates(op OpCode) int {
return int(immediates[op])
}

@ -16,11 +16,6 @@
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")

@ -32,18 +32,18 @@ func TestEOFMarshaling(t *testing.T) {
}{
{
want: Container{
types: []*functionMetadata{{inputs: 0, outputs: 0x80, maxStackHeight: 1}},
code: [][]byte{common.Hex2Bytes("604200")},
data: []byte{0x01, 0x02, 0x03},
dataSize: 3,
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}},
code: [][]byte{common.Hex2Bytes("604200")},
data: []byte{0x01, 0x02, 0x03},
dataSize: 3,
types: []*functionMetadata{{inputs: 0, outputs: 0x80, maxStackHeight: 1}},
codeSections: [][]byte{common.Hex2Bytes("604200")},
data: []byte{0x01, 0x02, 0x03},
dataSize: 3,
},
},
{
@ -53,7 +53,7 @@ func TestEOFMarshaling(t *testing.T) {
{inputs: 2, outputs: 3, maxStackHeight: 4},
{inputs: 1, outputs: 1, maxStackHeight: 1},
},
code: [][]byte{
codeSections: [][]byte{
common.Hex2Bytes("604200"),
common.Hex2Bytes("6042604200"),
common.Hex2Bytes("00"),
@ -82,11 +82,11 @@ func TestEOFSubcontainer(t *testing.T) {
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,
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()

@ -23,7 +23,6 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/math"
"github.com/ethereum/go-ethereum/params"
"github.com/holiman/uint256"
)
// memoryGasCost calculates the quadratic gas for memory expansion. It does so
@ -481,52 +480,6 @@ func gasStaticCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memo
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) {
var gas uint64
// EIP150 homestead gas reprice fork:
@ -550,8 +503,19 @@ func gasSelfdestruct(evm *EVM, contract *Contract, stack *Stack, mem *Memory, me
return gas, nil
}
func gasExtCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
panic("not implemented")
}
func gasExtDelegateCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
panic("not implemented")
}
func gasExtStaticCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
panic("not implemented")
}
// 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)
panic("not implemented")
}

@ -24,6 +24,8 @@ import (
type OpCode byte
// IsPush specifies if an opcode is a PUSH opcode.
// @deprecated: this method is often used in order to know if there are immediates.
// Please use `vm.Immediates` instead.
func (op OpCode) IsPush() bool {
return PUSH0 <= op && op <= PUSH32
}

@ -82,8 +82,8 @@ func validateCode(code []byte, section int, container *Container, jt *JumpTable,
count = 0
op OpCode
analysis bitvec
visitedCode = make(map[int]struct{})
visitedSubcontainers = make(map[int]int)
visitedCode map[int]struct{}
visitedSubcontainers map[int]int
hasReturnContract bool
hasStop bool
)
@ -129,6 +129,9 @@ func validateCode(code []byte, section int, container *Container, jt *JumpTable,
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:])
@ -138,6 +141,9 @@ func validateCode(code []byte, section int, container *Container, jt *JumpTable,
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:])
@ -150,8 +156,11 @@ func validateCode(code []byte, section int, container *Container, jt *JumpTable,
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)
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 {
@ -164,12 +173,15 @@ func validateCode(code []byte, section int, container *Container, jt *JumpTable,
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 arg >= len(container.subContainers) {
return nil, fmt.Errorf("%w: arg %d, last %d, pos %d", ErrUnreachableCode, arg, len(container.subContainers), i)
}
if ct := container.sections[arg]; len(ct.data) != ct.dataSize {
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)
}
if _, ok := visitedSubcontainers[arg]; ok {
return nil, fmt.Errorf("section already referenced, arg :%d", arg)
}
@ -226,3 +238,21 @@ func checkDest(code []byte, analysis *bitvec, imm, from, length int) error {
}
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, ",")
//}

@ -6,22 +6,37 @@ import (
"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
visitCount = 0
next = make([]int, 0, 1)
)
setBounds := func(pos, min, maxi int) *bounds {
stackBounds[pos] = &bounds{min, maxi}
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)
return stackBounds[pos]
}
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))
@ -29,97 +44,89 @@ func validateControlFlow(code []byte, section int, metadata []*functionMetadata,
qualifiedExit := false
for pos := 0; pos < len(code); pos++ {
op := OpCode(code[pos])
currentBounds := stackBounds[pos]
if currentBounds == nil {
ok, currentStackMin, currentStackMax := getStackMaxMin(pos)
if !ok {
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)
fmt.Println(pos, op, maxStackHeight, currentStackMin, currentStackMax)
}
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 {
if want, have := int(newSection.inputs), currentStackMin; 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 {
if have, limit := currentStackMax+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)
if currentStackMax != currentStackMin {
return 0, fmt.Errorf("%w: max %d, min %d, at pos %d", ErrInvalidOutputs, currentStackMax, currentStackMin, 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 {
if want := currentStackMin; 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 {
if have, limit := currentStackMax+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 {
if want, have := int(newSection.inputs), currentStackMin; 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 currentStackMax != currentStackMin {
return 0, fmt.Errorf("%w: max %d, min %d, at pos %d", ErrInvalidOutputs, currentStackMax, currentStackMin, pos)
}
if have, want := currentBounds.max, int(metadata[section].outputs)+int(newSection.inputs)-int(newSection.outputs); have != want {
if have, want := currentStackMax, 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 {
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, currentBounds.min; want > have {
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, currentBounds.min; want > have {
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, currentBounds.min; want > have {
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
}
var next []int
next = next[:0]
switch op {
case RJUMP:
nextPos := pos + 2 + parseInt16(code[pos+1:])
@ -127,19 +134,20 @@ func validateControlFlow(code []byte, section int, metadata []*functionMetadata,
// 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]
ok, nextMin, nextMax := getStackMaxMin(nextPos + 1)
if !ok {
return 0, ErrInvalidBackwardJump
}
if nextBounds.max != currentStackMax || nextBounds.min != currentStackMin {
if nextMax != currentStackMax || nextMin != 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))
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:])
@ -172,23 +180,23 @@ func validateControlFlow(code []byte, section int, metadata []*functionMetadata,
}
if nextPC > pos {
// target reached via forward jump or seq flow
nextBounds, ok := stackBounds[nextPC]
ok, nextMin, nextMax := getStackMaxMin(nextPC)
if !ok {
setBounds(nextPC, currentStackMin, currentStackMax)
} else {
setBounds(nextPC, min(nextBounds.min, currentStackMin), max(nextBounds.max, currentStackMax))
setBounds(nextPC, min(nextMin, currentStackMin), max(nextMax, currentStackMax))
}
} else {
// target reached via backwards jump
nextBounds, ok := stackBounds[nextPC]
ok, nextMin, nextMax := getStackMaxMin(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 currentStackMax != nextMax {
return 0, fmt.Errorf("%w want %d as current max got %d at pos %d,", ErrInvalidBackwardJump, currentStackMax, nextMax, 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 currentStackMin != nextMin {
return 0, fmt.Errorf("%w want %d as current min got %d at pos %d,", ErrInvalidBackwardJump, currentStackMin, nextMin, pos)
}
}
}
@ -199,7 +207,6 @@ func validateControlFlow(code []byte, section int, metadata []*functionMetadata,
} else {
pos = next[0]
}
}
if qualifiedExit != (metadata[section].outputs < maxOutputItems) {
return 0, fmt.Errorf("%w no RETF or qualified JUMPF", ErrInvalidNonReturningFlag)
@ -213,5 +220,5 @@ func validateControlFlow(code []byte, section int, metadata []*functionMetadata,
}
return 0, fmt.Errorf("%w in code section %d: have %d, want %d", ErrInvalidMaxStackHeight, section, maxStackHeight, metadata[section].maxStackHeight)
}
return len(stackBounds), nil
return visitCount, nil
}

@ -247,9 +247,9 @@ func TestValidateCode(t *testing.T) {
},
} {
container := &Container{
types: test.metadata,
data: make([]byte, 0),
sections: make([]*Container, 0),
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) {
@ -269,9 +269,9 @@ func BenchmarkRJUMPI(b *testing.B) {
}
code = append(code, byte(STOP))
container := &Container{
types: []*functionMetadata{{inputs: 0, outputs: 0x80, maxStackHeight: 1}},
data: make([]byte, 0),
sections: make([]*Container, 0),
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++ {
@ -299,9 +299,9 @@ func BenchmarkRJUMPV(b *testing.B) {
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),
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++ {
@ -322,7 +322,7 @@ func BenchmarkEOFValidation(b *testing.B) {
}
code = append(code, byte(STOP))
// First container
container.code = append(container.code, code)
container.codeSections = append(container.codeSections, code)
container.types = append(container.types, &functionMetadata{inputs: 0, outputs: 0x80, maxStackHeight: 0})
inner := []byte{
@ -330,12 +330,12 @@ func BenchmarkEOFValidation(b *testing.B) {
}
for i := 0; i < 1023; i++ {
container.code = append(container.code, inner)
container.codeSections = append(container.codeSections, inner)
container.types = append(container.types, &functionMetadata{inputs: 0, outputs: 0x80, maxStackHeight: 0})
}
for i := 0; i < 12; i++ {
container.code[i+1] = code
container.codeSections[i+1] = code
}
bin := container.MarshalBinary()
@ -365,7 +365,7 @@ func BenchmarkEOFValidation2(b *testing.B) {
}
code = append(code, byte(STOP))
// First container
container.code = append(container.code, code)
container.codeSections = append(container.codeSections, code)
container.types = append(container.types, &functionMetadata{inputs: 0, outputs: 0x80, maxStackHeight: 0})
inner := []byte{
@ -384,7 +384,7 @@ func BenchmarkEOFValidation2(b *testing.B) {
}
for i := 0; i < 1023; i++ {
container.code = append(container.code, inner)
container.codeSections = append(container.codeSections, inner)
container.types = append(container.types, &functionMetadata{inputs: 0, outputs: 0x80, maxStackHeight: 0})
}
@ -425,15 +425,15 @@ func BenchmarkEOFValidation3(b *testing.B) {
}
code = append(code, byte(STOP))
// First container
container.code = append(container.code, code)
container.codeSections = append(container.codeSections, 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.codeSections = append(container.codeSections, []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.codeSections[i+1] = append(snippet, byte(STOP))
container.types[i+1] = &functionMetadata{inputs: 0, outputs: 0x80, maxStackHeight: 1}
}
bin := container.MarshalBinary()
@ -468,9 +468,9 @@ func BenchmarkRJUMPI_2(b *testing.B) {
}
code = append(code, byte(STOP))
container := &Container{
types: []*functionMetadata{{inputs: 0, outputs: 0x80, maxStackHeight: 1}},
data: make([]byte, 0),
sections: make([]*Container, 0),
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++ {

Loading…
Cancel
Save