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
This commit is contained in:
Martin Holst Swende 2024-09-11 15:00:06 +02:00
parent 77484b4cf6
commit d3f736faf0
No known key found for this signature in database
GPG key ID: 683B438C05A5DDF0
15 changed files with 207 additions and 196 deletions

View file

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

View file

@ -50,13 +50,13 @@ func FuzzEofParsing(f *testing.F) {
if err := c.UnmarshalBinary(data, true); err == nil { if err := c.UnmarshalBinary(data, true); err == nil {
c.ValidateCode(&jt, true) c.ValidateCode(&jt, true)
if have := c.MarshalBinary(); !bytes.Equal(have, data) { 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 { if err := c.UnmarshalBinary(data, false); err == nil {
c.ValidateCode(&jt, false) c.ValidateCode(&jt, false)
if have := c.MarshalBinary(); !bytes.Equal(have, data) { if have := c.MarshalBinary(); !bytes.Equal(have, data) {
f.Fatal("Unmarshal-> Marshal failure!") t.Fatal("Unmarshal-> Marshal failure!")
} }
} }
if !bytes.Equal(cpy, data) { if !bytes.Equal(cpy, data) {
@ -131,7 +131,6 @@ func testEofParse(t *testing.T, isInitCode bool, wantFile string) {
} }
} }
line++ line++
} }
corpus.Close() corpus.Close()
} }

View file

@ -65,11 +65,11 @@ func (it *instructionIterator) Next() bool {
} }
it.op = vm.OpCode(it.code[it.pc]) it.op = vm.OpCode(it.code[it.pc])
if it.op.IsPush() {
a := uint64(it.op) - uint64(vm.PUSH0) if a := vm.Immediates(it.op); a > 0 {
u := it.pc + 1 + a u := it.pc + 1 + uint64(a)
if uint64(len(it.code)) <= it.pc || uint64(len(it.code)) < u { 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 return false
} }
it.arg = it.code[it.pc+1 : u] it.arg = it.code[it.pc+1 : u]

View file

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

View file

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

View file

@ -535,6 +535,13 @@ func enable4762(jt *JumpTable) {
} }
// enableEOF applies the EOF changes. // 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) { func enableEOF(jt *JumpTable) {
// Deprecate opcodes // Deprecate opcodes
undefined := &operation{ undefined := &operation{

View file

@ -66,12 +66,12 @@ func isEOFVersion1(code []byte) bool {
// Container is an EOF container object. // Container is an EOF container object.
type Container struct { type Container struct {
types []*functionMetadata types []*functionMetadata
code [][]byte codeSections [][]byte
sections []*Container subContainers []*Container
containerCode [][]byte subContainerCodes [][]byte
data []byte data []byte
dataSize int // might be more than len(data) dataSize int // might be more than len(data)
} }
// functionMetadata is an EOF function signature. // functionMetadata is an EOF function signature.
@ -92,15 +92,15 @@ func (c *Container) MarshalBinary() []byte {
b = append(b, kindTypes) b = append(b, kindTypes)
b = binary.BigEndian.AppendUint16(b, uint16(len(c.types)*4)) b = binary.BigEndian.AppendUint16(b, uint16(len(c.types)*4))
b = append(b, kindCode) b = append(b, kindCode)
b = binary.BigEndian.AppendUint16(b, uint16(len(c.code))) b = binary.BigEndian.AppendUint16(b, uint16(len(c.codeSections)))
for _, code := range c.code { for _, codeSection := range c.codeSections {
b = binary.BigEndian.AppendUint16(b, uint16(len(code))) b = binary.BigEndian.AppendUint16(b, uint16(len(codeSection)))
} }
var encodedContainer [][]byte var encodedContainer [][]byte
if len(c.sections) != 0 { if len(c.subContainers) != 0 {
b = append(b, kindContainer) b = append(b, kindContainer)
b = binary.BigEndian.AppendUint16(b, uint16(len(c.sections))) b = binary.BigEndian.AppendUint16(b, uint16(len(c.subContainers)))
for _, section := range c.sections { for _, section := range c.subContainers {
encoded := section.MarshalBinary() encoded := section.MarshalBinary()
b = binary.BigEndian.AppendUint16(b, uint16(len(encoded))) b = binary.BigEndian.AppendUint16(b, uint16(len(encoded)))
encodedContainer = append(encodedContainer, encoded) encodedContainer = append(encodedContainer, encoded)
@ -114,7 +114,7 @@ func (c *Container) MarshalBinary() []byte {
for _, ty := range c.types { for _, ty := range c.types {
b = append(b, []byte{ty.inputs, ty.outputs, byte(ty.maxStackHeight >> 8), byte(ty.maxStackHeight & 0x00ff)}...) 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...) b = append(b, code...)
} }
for _, section := range encodedContainer { for _, section := range encodedContainer {
@ -228,7 +228,7 @@ func (c *Container) unmarshalSubContainer(b []byte, isInitcode bool, topLevel bo
// Parse types section. // Parse types section.
idx := offsetTerminator + 1 idx := offsetTerminator + 1
var types []*functionMetadata var types = make([]*functionMetadata, 0, typesSize/4)
for i := 0; i < typesSize/4; i++ { for i := 0; i < typesSize/4; i++ {
sig := &functionMetadata{ sig := &functionMetadata{
inputs: b[idx+i*4], inputs: b[idx+i*4],
@ -253,45 +253,44 @@ func (c *Container) unmarshalSubContainer(b []byte, isInitcode bool, topLevel bo
// Parse code sections. // Parse code sections.
idx += typesSize idx += typesSize
code := make([][]byte, len(codeSizes)) codeSections := make([][]byte, len(codeSizes))
for i, size := range codeSizes { for i, size := range codeSizes {
if size == 0 { if size == 0 {
return fmt.Errorf("%w for section %d: size must not be 0", ErrInvalidCodeSize, i) 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 idx += size
} }
c.code = code c.codeSections = codeSections
// Parse the optional container sizes. // Parse the optional container sizes.
if len(containerSizes) != 0 { if len(containerSizes) != 0 {
if len(containerSizes) > maxContainerSections { if len(containerSizes) > maxContainerSections {
return fmt.Errorf("%w number of container section exceed: %v: have %v", ErrInvalidContainerSectionSize, maxContainerSections, len(containerSizes)) return fmt.Errorf("%w number of container section exceed: %v: have %v", ErrInvalidContainerSectionSize, maxContainerSections, len(containerSizes))
} }
containerCode := make([][]byte, 0, len(containerSizes)) subContainerCodes := make([][]byte, 0, len(containerSizes))
container := make([]*Container, 0, len(containerSizes)) subContainers := make([]*Container, 0, len(containerSizes))
for i, size := range containerSizes { for i, size := range containerSizes {
if size == 0 || idx+size > len(b) { if size == 0 || idx+size > len(b) {
return fmt.Errorf("%w for section %d: size must not be 0", ErrInvalidContainerSectionSize, i) 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)) 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 { if topLevel {
return fmt.Errorf("%w in sub container %d", err, i) return fmt.Errorf("%w in sub container %d", err, i)
} }
return err return err
} }
container = append(container, c) subContainers = append(subContainers, subC)
containerCode = append(containerCode, b[idx:end]) subContainerCodes = append(subContainerCodes, b[idx:end])
idx += size idx += size
} }
c.sections = container c.subContainers = subContainers
c.containerCode = containerCode c.subContainerCodes = subContainerCodes
} }
// Parse data section. //Parse data section.
end := len(b) end := len(b)
if !isInitcode { if !isInitcode {
end = min(idx+dataSize, len(b)) 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 // should not mean 2 and 3 should be visited twice
var ( var (
index = toVisit[0] index = toVisit[0]
code = c.code[index] code = c.codeSections[index]
) )
if _, ok := visited[index]; !ok { if _, ok := visited[index]; !ok {
res, err := validateCode(code, index, c, jt, refBy == refByEOFCreate) 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:] toVisit = toVisit[1:]
} }
// Make sure every code section is visited at least once. // Make sure every code section is visited at least once.
if len(visited) != len(c.code) { if len(visited) != len(c.codeSections) {
return ErrUnreachableCode return ErrUnreachableCode
} }
for idx, container := range c.sections { for idx, container := range c.subContainers {
reference, ok := subContainerVisited[idx] reference, ok := subContainerVisited[idx]
if !ok { if !ok {
return ErrOrphanedSubcontainer return ErrOrphanedSubcontainer
@ -444,14 +443,14 @@ func (c *Container) String() string {
result += fmt.Sprintf("KindType: %02x\n", kindTypes) result += fmt.Sprintf("KindType: %02x\n", kindTypes)
result += fmt.Sprintf("TypesSize: %04x\n", len(c.types)*4) result += fmt.Sprintf("TypesSize: %04x\n", len(c.types)*4)
result += fmt.Sprintf("KindCode: %02x\n", kindCode) result += fmt.Sprintf("KindCode: %02x\n", kindCode)
result += fmt.Sprintf("CodeSize: %04x\n", len(c.code)) result += fmt.Sprintf("CodeSize: %04x\n", len(c.codeSections))
for i, code := range c.code { for i, code := range c.codeSections {
result += fmt.Sprintf("Code %v length: %04x\n", i, len(code)) 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("KindContainer: %02x\n", kindContainer)
result += fmt.Sprintf("ContainerSize: %04x\n", len(c.sections)) result += fmt.Sprintf("ContainerSize: %04x\n", len(c.subContainers))
for i, section := range c.sections { for i, section := range c.subContainers {
result += fmt.Sprintf("Container %v length: %04x\n", i, len(section.MarshalBinary())) 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 { 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)})) 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)) 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("Section %v: %v\n", i, hex.EncodeToString(section.MarshalBinary()))
} }
result += fmt.Sprintf("Data: %v\n", hex.EncodeToString(c.data)) result += fmt.Sprintf("Data: %v\n", hex.EncodeToString(c.data))

View file

@ -55,3 +55,9 @@ func init() {
terminals[REVERT] = true terminals[REVERT] = true
terminals[INVALID] = 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])
}

View file

@ -16,11 +16,6 @@
package vm 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. // opRjump implements the RJUMP opcode.
func opRjump(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { func opRjump(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
panic("not implemented") panic("not implemented")

View file

@ -32,18 +32,18 @@ func TestEOFMarshaling(t *testing.T) {
}{ }{
{ {
want: Container{ want: Container{
types: []*functionMetadata{{inputs: 0, outputs: 0x80, maxStackHeight: 1}}, types: []*functionMetadata{{inputs: 0, outputs: 0x80, maxStackHeight: 1}},
code: [][]byte{common.Hex2Bytes("604200")}, codeSections: [][]byte{common.Hex2Bytes("604200")},
data: []byte{0x01, 0x02, 0x03}, data: []byte{0x01, 0x02, 0x03},
dataSize: 3, dataSize: 3,
}, },
}, },
{ {
want: Container{ want: Container{
types: []*functionMetadata{{inputs: 0, outputs: 0x80, maxStackHeight: 1}}, types: []*functionMetadata{{inputs: 0, outputs: 0x80, maxStackHeight: 1}},
code: [][]byte{common.Hex2Bytes("604200")}, codeSections: [][]byte{common.Hex2Bytes("604200")},
data: []byte{0x01, 0x02, 0x03}, data: []byte{0x01, 0x02, 0x03},
dataSize: 3, dataSize: 3,
}, },
}, },
{ {
@ -53,7 +53,7 @@ func TestEOFMarshaling(t *testing.T) {
{inputs: 2, outputs: 3, maxStackHeight: 4}, {inputs: 2, outputs: 3, maxStackHeight: 4},
{inputs: 1, outputs: 1, maxStackHeight: 1}, {inputs: 1, outputs: 1, maxStackHeight: 1},
}, },
code: [][]byte{ codeSections: [][]byte{
common.Hex2Bytes("604200"), common.Hex2Bytes("604200"),
common.Hex2Bytes("6042604200"), common.Hex2Bytes("6042604200"),
common.Hex2Bytes("00"), common.Hex2Bytes("00"),
@ -82,11 +82,11 @@ func TestEOFSubcontainer(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
container := Container{ container := Container{
types: []*functionMetadata{{inputs: 0, outputs: 0x80, maxStackHeight: 1}}, types: []*functionMetadata{{inputs: 0, outputs: 0x80, maxStackHeight: 1}},
code: [][]byte{common.Hex2Bytes("604200")}, codeSections: [][]byte{common.Hex2Bytes("604200")},
sections: []*Container{subcontainer}, subContainers: []*Container{subcontainer},
data: []byte{0x01, 0x02, 0x03}, data: []byte{0x01, 0x02, 0x03},
dataSize: 3, dataSize: 3,
} }
var ( var (
b = container.MarshalBinary() b = container.MarshalBinary()

View file

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

View file

@ -24,6 +24,8 @@ import (
type OpCode byte type OpCode byte
// IsPush specifies if an opcode is a PUSH opcode. // 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 { func (op OpCode) IsPush() bool {
return PUSH0 <= op && op <= PUSH32 return PUSH0 <= op && op <= PUSH32
} }

View file

@ -82,8 +82,8 @@ func validateCode(code []byte, section int, container *Container, jt *JumpTable,
count = 0 count = 0
op OpCode op OpCode
analysis bitvec analysis bitvec
visitedCode = make(map[int]struct{}) visitedCode map[int]struct{}
visitedSubcontainers = make(map[int]int) visitedSubcontainers map[int]int
hasReturnContract bool hasReturnContract bool
hasStop bool hasStop bool
) )
@ -129,6 +129,9 @@ func validateCode(code []byte, section int, container *Container, jt *JumpTable,
if container.types[arg].outputs == 0x80 { if container.types[arg].outputs == 0x80 {
return nil, fmt.Errorf("%w: section %v", ErrInvalidCallArgument, arg) return nil, fmt.Errorf("%w: section %v", ErrInvalidCallArgument, arg)
} }
if visitedCode == nil {
visitedCode = make(map[int]struct{})
}
visitedCode[arg] = struct{}{} visitedCode[arg] = struct{}{}
case JUMPF: case JUMPF:
arg, _ := parseUint16(code[i+1:]) 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 { 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) 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{}{} visitedCode[arg] = struct{}{}
case DATALOADN: case DATALOADN:
arg, _ := parseUint16(code[i+1:]) arg, _ := parseUint16(code[i+1:])
@ -150,8 +156,11 @@ func validateCode(code []byte, section int, container *Container, jt *JumpTable,
return nil, ErrIncompatibleContainerKind return nil, ErrIncompatibleContainerKind
} }
arg := int(code[i+1]) arg := int(code[i+1])
if arg >= len(container.sections) { if arg >= len(container.subContainers) {
return nil, fmt.Errorf("%w: arg %d, last %d, pos %d", ErrUnreachableCode, arg, len(container.sections), i) 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 // We need to store per subcontainer how it was referenced
if v, ok := visitedSubcontainers[arg]; ok && v != refByReturnContract { 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 visitedSubcontainers[arg] = refByReturnContract
case EOFCREATE: case EOFCREATE:
arg := int(code[i+1]) arg := int(code[i+1])
if arg >= len(container.sections) { if arg >= len(container.subContainers) {
return nil, fmt.Errorf("%w: arg %d, last %d, pos %d", ErrUnreachableCode, arg, len(container.sections), i) 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) 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 { if _, ok := visitedSubcontainers[arg]; ok {
return nil, fmt.Errorf("section already referenced, arg :%d", arg) 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 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, ",")
//}

View file

@ -6,22 +6,37 @@ import (
"github.com/ethereum/go-ethereum/params" "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) { func validateControlFlow(code []byte, section int, metadata []*functionMetadata, jt *JumpTable) (int, error) {
var ( var (
stackBounds = make(map[int]*bounds)
maxStackHeight = int(metadata[section].inputs) maxStackHeight = int(metadata[section].inputs)
debugging = !true debugging = !true
visitCount = 0
next = make([]int, 0, 1)
) )
var (
setBounds := func(pos, min, maxi int) *bounds { stackBoundsMax = make([]uint16, len(code))
stackBounds[pos] = &bounds{min, maxi} 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) 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 // set the initial stack bounds
setBounds(0, int(metadata[section].inputs), int(metadata[section].inputs)) setBounds(0, int(metadata[section].inputs), int(metadata[section].inputs))
@ -29,97 +44,89 @@ func validateControlFlow(code []byte, section int, metadata []*functionMetadata,
qualifiedExit := false qualifiedExit := false
for pos := 0; pos < len(code); pos++ { for pos := 0; pos < len(code); pos++ {
op := OpCode(code[pos]) op := OpCode(code[pos])
currentBounds := stackBounds[pos] ok, currentStackMin, currentStackMax := getStackMaxMin(pos)
if currentBounds == nil { if !ok {
if debugging { if debugging {
fmt.Printf("Stack bounds not set: %v at %v \n", op, pos) fmt.Printf("Stack bounds not set: %v at %v \n", op, pos)
} }
return 0, ErrUnreachableCode return 0, ErrUnreachableCode
} }
if debugging { if debugging {
fmt.Println(pos, op, maxStackHeight, currentBounds) fmt.Println(pos, op, maxStackHeight, currentStackMin, currentStackMax)
} }
var (
currentStackMax = currentBounds.max
currentStackMin = currentBounds.min
)
switch op { switch op {
case CALLF: case CALLF:
arg, _ := parseUint16(code[pos+1:]) arg, _ := parseUint16(code[pos+1:])
newSection := metadata[arg] 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) 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) return 0, fmt.Errorf("%w: at pos %d", ErrStackOverflow{stackLen: have, limit: limit}, pos)
} }
change := int(newSection.outputs) - int(newSection.inputs) change := int(newSection.outputs) - int(newSection.inputs)
currentStackMax += change currentStackMax += change
currentStackMin += change currentStackMin += change
case RETF: case RETF:
if currentBounds.max != currentBounds.min { if currentStackMax != currentStackMin {
return 0, fmt.Errorf("%w: max %d, min %d, at pos %d", ErrInvalidOutputs, currentBounds.max, currentBounds.min, pos) return 0, fmt.Errorf("%w: max %d, min %d, at pos %d", ErrInvalidOutputs, currentStackMax, currentStackMin, pos)
} }
have := int(metadata[section].outputs) have := int(metadata[section].outputs)
if have >= maxOutputItems { if have >= maxOutputItems {
return 0, fmt.Errorf("%w: at pos %d", ErrInvalidNonReturningFlag, pos) 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) return 0, fmt.Errorf("%w: have %d, want %d, at pos %d", ErrInvalidOutputs, have, want, pos)
} }
qualifiedExit = true qualifiedExit = true
case JUMPF: case JUMPF:
arg, _ := parseUint16(code[pos+1:]) arg, _ := parseUint16(code[pos+1:])
newSection := metadata[arg] 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) return 0, fmt.Errorf("%w: at pos %d", ErrStackOverflow{stackLen: have, limit: limit}, pos)
} }
if newSection.outputs == 0x80 { 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) return 0, fmt.Errorf("%w: at pos %d", ErrStackUnderflow{stackLen: have, required: want}, pos)
} }
} else { } else {
if currentBounds.max != currentBounds.min { if currentStackMax != currentStackMin {
return 0, fmt.Errorf("%w: max %d, min %d, at pos %d", ErrInvalidOutputs, currentBounds.max, currentBounds.min, pos) 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) return 0, fmt.Errorf("%w: at pos %d", ErrInvalidOutputs, pos)
} }
} }
qualifiedExit = qualifiedExit || newSection.outputs < maxOutputItems qualifiedExit = qualifiedExit || newSection.outputs < maxOutputItems
case DUPN: case DUPN:
arg := int(code[pos+1]) + 1 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) return 0, fmt.Errorf("%w: at pos %d", ErrStackUnderflow{stackLen: have, required: want}, pos)
} }
case SWAPN: case SWAPN:
arg := int(code[pos+1]) + 1 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) return 0, fmt.Errorf("%w: at pos %d", ErrStackUnderflow{stackLen: have, required: want}, pos)
} }
case EXCHANGE: case EXCHANGE:
arg := int(code[pos+1]) arg := int(code[pos+1])
n := arg>>4 + 1 n := arg>>4 + 1
m := arg&0x0f + 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) return 0, fmt.Errorf("%w: at pos %d", ErrStackUnderflow{stackLen: have, required: want}, pos)
} }
default: 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) return 0, fmt.Errorf("%w: at pos %d", ErrStackUnderflow{stackLen: have, required: want}, pos)
} }
} }
if !terminals[op] && op != CALLF { if !terminals[op] && op != CALLF {
change := int(params.StackLimit) - jt[op].maxStack change := int(params.StackLimit) - jt[op].maxStack
currentStackMax += change currentStackMax += change
currentStackMin += change currentStackMin += change
} }
next = next[:0]
var next []int
switch op { switch op {
case RJUMP: case RJUMP:
nextPos := pos + 2 + parseInt16(code[pos+1:]) 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 // We set the stack bounds of the destination
// and skip the argument, only for RJUMP, all other opcodes are handled later // and skip the argument, only for RJUMP, all other opcodes are handled later
if nextPos+1 < pos { if nextPos+1 < pos {
nextBounds, ok := stackBounds[nextPos+1] ok, nextMin, nextMax := getStackMaxMin(nextPos + 1)
if !ok { if !ok {
return 0, ErrInvalidBackwardJump return 0, ErrInvalidBackwardJump
} }
if nextBounds.max != currentStackMax || nextBounds.min != currentStackMin { if nextMax != currentStackMax || nextMin != currentStackMin {
return 0, ErrInvalidMaxStackHeight return 0, ErrInvalidMaxStackHeight
} }
}
nextBounds, ok := stackBounds[nextPos+1]
if !ok {
setBounds(nextPos+1, currentStackMin, currentStackMax)
} else { } 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: case RJUMPI:
arg := parseInt16(code[pos+1:]) arg := parseInt16(code[pos+1:])
@ -172,23 +180,23 @@ func validateControlFlow(code []byte, section int, metadata []*functionMetadata,
} }
if nextPC > pos { if nextPC > pos {
// target reached via forward jump or seq flow // target reached via forward jump or seq flow
nextBounds, ok := stackBounds[nextPC] ok, nextMin, nextMax := getStackMaxMin(nextPC)
if !ok { if !ok {
setBounds(nextPC, currentStackMin, currentStackMax) setBounds(nextPC, currentStackMin, currentStackMax)
} else { } else {
setBounds(nextPC, min(nextBounds.min, currentStackMin), max(nextBounds.max, currentStackMax)) setBounds(nextPC, min(nextMin, currentStackMin), max(nextMax, currentStackMax))
} }
} else { } else {
// target reached via backwards jump // target reached via backwards jump
nextBounds, ok := stackBounds[nextPC] ok, nextMin, nextMax := getStackMaxMin(nextPC)
if !ok { if !ok {
return 0, ErrInvalidBackwardJump return 0, ErrInvalidBackwardJump
} }
if currentStackMax != nextBounds.max { if currentStackMax != nextMax {
return 0, fmt.Errorf("%w want %d as current max got %d at pos %d,", ErrInvalidBackwardJump, currentStackMax, nextBounds.max, pos) return 0, fmt.Errorf("%w want %d as current max got %d at pos %d,", ErrInvalidBackwardJump, currentStackMax, nextMax, pos)
} }
if currentStackMin != nextBounds.min { if currentStackMin != nextMin {
return 0, fmt.Errorf("%w want %d as current min got %d at pos %d,", ErrInvalidBackwardJump, currentStackMin, nextBounds.min, pos) 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 { } else {
pos = next[0] pos = next[0]
} }
} }
if qualifiedExit != (metadata[section].outputs < maxOutputItems) { if qualifiedExit != (metadata[section].outputs < maxOutputItems) {
return 0, fmt.Errorf("%w no RETF or qualified JUMPF", ErrInvalidNonReturningFlag) 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 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
} }

View file

@ -247,9 +247,9 @@ func TestValidateCode(t *testing.T) {
}, },
} { } {
container := &Container{ container := &Container{
types: test.metadata, types: test.metadata,
data: make([]byte, 0), data: make([]byte, 0),
sections: make([]*Container, 0), subContainers: make([]*Container, 0),
} }
_, err := validateCode(test.code, test.section, container, &pragueEOFInstructionSet, false) _, err := validateCode(test.code, test.section, container, &pragueEOFInstructionSet, false)
if !errors.Is(err, test.err) { if !errors.Is(err, test.err) {
@ -269,9 +269,9 @@ func BenchmarkRJUMPI(b *testing.B) {
} }
code = append(code, byte(STOP)) code = append(code, byte(STOP))
container := &Container{ container := &Container{
types: []*functionMetadata{{inputs: 0, outputs: 0x80, maxStackHeight: 1}}, types: []*functionMetadata{{inputs: 0, outputs: 0x80, maxStackHeight: 1}},
data: make([]byte, 0), data: make([]byte, 0),
sections: make([]*Container, 0), subContainers: make([]*Container, 0),
} }
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { 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(PUSH0))
code = append(code, byte(STOP)) code = append(code, byte(STOP))
container := &Container{ container := &Container{
types: []*functionMetadata{{inputs: 0, outputs: 0x80, maxStackHeight: 1}}, types: []*functionMetadata{{inputs: 0, outputs: 0x80, maxStackHeight: 1}},
data: make([]byte, 0), data: make([]byte, 0),
sections: make([]*Container, 0), subContainers: make([]*Container, 0),
} }
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
@ -322,7 +322,7 @@ func BenchmarkEOFValidation(b *testing.B) {
} }
code = append(code, byte(STOP)) code = append(code, byte(STOP))
// First container // 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}) container.types = append(container.types, &functionMetadata{inputs: 0, outputs: 0x80, maxStackHeight: 0})
inner := []byte{ inner := []byte{
@ -330,12 +330,12 @@ func BenchmarkEOFValidation(b *testing.B) {
} }
for i := 0; i < 1023; i++ { 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}) container.types = append(container.types, &functionMetadata{inputs: 0, outputs: 0x80, maxStackHeight: 0})
} }
for i := 0; i < 12; i++ { for i := 0; i < 12; i++ {
container.code[i+1] = code container.codeSections[i+1] = code
} }
bin := container.MarshalBinary() bin := container.MarshalBinary()
@ -365,7 +365,7 @@ func BenchmarkEOFValidation2(b *testing.B) {
} }
code = append(code, byte(STOP)) code = append(code, byte(STOP))
// First container // 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}) container.types = append(container.types, &functionMetadata{inputs: 0, outputs: 0x80, maxStackHeight: 0})
inner := []byte{ inner := []byte{
@ -384,7 +384,7 @@ func BenchmarkEOFValidation2(b *testing.B) {
} }
for i := 0; i < 1023; i++ { 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}) 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)) code = append(code, byte(STOP))
// First container // 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}) container.types = append(container.types, &functionMetadata{inputs: 0, outputs: 0x80, maxStackHeight: 1})
for i := 0; i < 1023; i++ { 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}) container.types = append(container.types, &functionMetadata{inputs: 0, outputs: 0x80, maxStackHeight: 0})
} }
for i := 0; i < 65; i++ { 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} container.types[i+1] = &functionMetadata{inputs: 0, outputs: 0x80, maxStackHeight: 1}
} }
bin := container.MarshalBinary() bin := container.MarshalBinary()
@ -468,9 +468,9 @@ func BenchmarkRJUMPI_2(b *testing.B) {
} }
code = append(code, byte(STOP)) code = append(code, byte(STOP))
container := &Container{ container := &Container{
types: []*functionMetadata{{inputs: 0, outputs: 0x80, maxStackHeight: 1}}, types: []*functionMetadata{{inputs: 0, outputs: 0x80, maxStackHeight: 1}},
data: make([]byte, 0), data: make([]byte, 0),
sections: make([]*Container, 0), subContainers: make([]*Container, 0),
} }
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {