Skip to content

Commit

Permalink
Update eip-2315.md (ethereum#4211)
Browse files Browse the repository at this point in the history
  • Loading branch information
gcolvin authored and PhABC committed Jan 25, 2022
1 parent 6f57901 commit f954f6f
Showing 1 changed file with 102 additions and 139 deletions.
241 changes: 102 additions & 139 deletions EIPS/eip-2315.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ category: Core
author: Greg Colvin <greg@colvin.org>, Martin Holst Swende (@holiman), Brooklyn Zelenka (@expede)
discussions-to: https://ethereum-magicians.org/t/eip-2315-simple-subroutines-for-the-evm/3941
created: 2019-10-17
requires: 3540, 3670
---

## Abstract

This proposal introduces four opcodes to support simple subroutines and relative jumps: `JUMPSUB`, `RETURNSUB`, `RJUMP` and `RJUMPI`.

This change supports substantial reductions in the complexity and the gas costs of calling and optimizing simple subroutines – from %33 to as much as 52% savings in gas..
This change supports substantial reductions in the complexity and the gas costs of calling and optimizing simple subroutines – from %33 to as much as 52% savings in gas.

## Motivation

Expand Down Expand Up @@ -41,7 +42,7 @@ We introduce one more stack into the EVM in addition to the existing `data stack

### Instructions

#### `JUMPSUB (0x5d) location`
#### `JUMPSUB (0x5e) location`

> Transfers control to a subroutine.
>
Expand All @@ -56,7 +57,7 @@ We introduce one more stack into the EVM in addition to the existing `data stack
> * _pops one item off the `data stack`_
> * _pushes one item on the `return stack`_
#### `RETURNSUB (0x5e)`
#### `RETURNSUB (0x5f)`

> Returns control to the caller of a subroutine.
>
Expand All @@ -69,7 +70,7 @@ We introduce one more stack into the EVM in addition to the existing `data stack
To provide a complete set of control structures, and to take full advantage of the performance benefits of simple subroutines we also provide two static, relative jump functions that take their arguments as immediate data rather then off the stack.

#### `RJUMP (0x??) offset`
#### `RJUMP (0x5c) offset`

> Transfers control to the address `PC + offset`, where offset is a three-byte, MSB first, twos-complement integer.
>
Expand All @@ -79,7 +80,7 @@ To provide a complete set of control structures, and to take full advantage of t
>
> The cost is _low_.
#### `RJUMPI (0x??) offset`
#### `RJUMPI (0x5d) offset`

> Conditionally transfers control to the address `PC + offset`, where offset is a three-byte, MSB first, twos-complement integer.
> 1. Decode the `offset` from the immediate data. The data is encoded as three bytes, MSB first, twos-complement.
Expand All @@ -96,31 +97,19 @@ _Notes:_
* _The description above lays out the semantics of this feature in terms of a `return stack`. But the actual state of the `return stack` is not observable by EVM code or consensus-critical to the protocol. (For example, a node implementer may code `JUMPSUB` to unobservably push `PC` on the `return stack` rather than `PC + 1`, which is allowed so long as `RETURNSUB` observably returns control to the `PC + 1` location.)_
* _The `return stack` is the functional equivalent of Turing's "delay line"._

The _low_ cost of `JUMPSUB` is justified by needing only about six Go operations to push the return address on the return stack and decode the immediate two byte destination to the `PC`. The _verylow_ cost of `RETURNSUB` is justified by needing only about three Go operations to pop the return stack into the `PC`. No 256-bit arithmetic or checking for valid destinations is needed. Also, `JUMP` is assigned _mid_, and `JUMPSUB` and RJUMP should be more efficient, as decoding immediate bytes should be cheaper than than converting 32-byte stack items, and the destination address will not need to be checked for either `JUMPSUB` or `RETURNSUB`. Benchmarking will be needed to tell if the costs are well-balanced.
`JUMP` and `JUMPI` are assigned _mid_ and _high_ gas fees -- they require operations on 256-bit stack items and checking for valid destinations. None of these operations require checking, and only `RJUMPI` requires 256-bit arithmetic. So the _low_ cost of `JUMPSUB` versus is justified by needing only about six Go operations to push the return address on the return stack and decode the immediate two byte destination to the `PC` and the _verylow_ cost of `RETURNSUB` is justified by needing only about three Go operations to pop the return stack into the `PC`. Similarly, the _low_ cost of `RJUMP` is justified by needing even less work than `JUMPSUB`, and the cost `RJUMPI` is `mid` because of the extra work to test the conditional. Benchmarking will be needed to tell if the costs are well-balanced.

### Validity

This EIP specifies validity rules for some important safety properties, including
Attempts to create contracts that can be proven to be invalid will fail.

valid instructions,
valid jump destinations,
no stack underflows, and
no stack overflows without recursion.
Invalid contracts will have one or more
* invalid instructions,
* invalid jump destinations,
* stack underflows, or
* stack overflows without recursion.

Valid contracts will not halt with an exception unless they either run out of gas or overflow stack during a recursive subroutine call.

Because of the dynamic JUMP and JUMPI instructions contracts these rules are necessary but not sufficient conditions for validity. So long as we have dynamic jums we cannot prove contracts to be valid, but only ensure that attempts to create contracts that can be proven to be invalid will fail.

#### Exceptional Halting States

_Execution_ is as defined in the [Yellow Paper](https://ethereum.github.io/yellowpaper/paper.pdf) a sequence of changes to the EVM state. The conditions on valid _code_ are preserved by state changes. At runtime, if execution of an instruction would violate a condition the execution is in an exceptional halting state. The Yellow Paper defines five such states.
1. Insufficient gas
2. More than 1024 stack items
3. Insufficient stack items
4. Invalid jump destination
5. Invalid instruction

We would like to consider EVM _code_ valid iff no execution of the program can lead to an exceptional halting state, but we must be able to validate _code_ in linear time to avoid denial of service attacks. So in practice, we can only partially meet these requirements. Our validation rules do not consider the _code's_ data and computations, only its control flow and stack use. This means we will reject programs with any invalid _code_ paths, even if those paths are not reachable at runtime.
Because of dynamic JUMP and JUMPI instructions the rules below give necessary but not sufficient conditions for validity. So long as we have unrestricted dynamic jumps we cannot prove contracts to be valid, only invalid. (See [EIP-3779](./eip-3779.md) for further discussion of validation and a means of restricting dynamic jumps.)

### Validation Rules

Expand All @@ -131,19 +120,9 @@ We would like to consider EVM _code_ valid iff no execution of the program can l
* the same on every path through an opcode.
3. The `stack pointer` is always positive and at most 1024.

We need to define `stack depth`. The Yellow Paper has the `stack pointer` (`SP`) pointing just past the top item on the `data stack`. We define the `stack base` (`BP`)as the element that the `SP` addressed at the entry to the current _basic block_, or `0` on program entry. So we can define the `stack depth` as the number of stack elements between the current `SP` and the current `BP`.

Taken together, these rules allow for code to be validated by traversing the control-flow graph, following each edge only once.

### Dependencies
The Yellow Paper has the `stack pointer` (`SP`) pointing just past the top item on the `data stack`. We define the `stack base` (`BP`)as the element that the `SP` addressed at the entry to the current _basic block_, or `0` on program entry, and the `stack depth` as the number of stack elements between the current `SP` and the current `BP`.

We need [EIP-3540: EVM Object Format (EOF)](./eip-3540.md) to support immediate arguments and [EIP-3670: EOF - Code Validation](./eip-3670.md) to support validation of instructions.

## Backwards and Forwards Compatibility

These changes do not affect the semantics of existing EVM code.

These changes are compatible with using [EIP-3337](https://eips.ethereum.org/EIPS/eip-3337) to provide stack frames, by associating a frame with each subroutine.
Taken together, these rules allow for code to be (in-)validated by traversing the control-flow graph, following each edge only once.

## Rationale

Expand All @@ -157,8 +136,6 @@ We prefer the separate return stack because it maintains a clear separation betw

We show here how these opcodes can be used to reduce the gas costs of both ordinary subroutine calls and low-level optimizations. The savings reported here will of course be less relevant to programs that use a few large subroutines rather than being a factored than into smaller ones. The choice of gas costs for the new opcodes above does not make a large difference in this analysis, as much of the improvement is due to PUSH and SWAP operations that are no longer needed. Even if `JUMPSUB` cost the same as `JUMP` – 8 gas rather than 5 - a simple subroutine call would still be 48% less costly versus 52%.

**_Note**: the **JUMP** versions of the examples below are all **valid code**._

#### **Simple Subroutine Call**

Consider this example of calling a fairly minimal subroutine
Expand Down Expand Up @@ -200,7 +177,7 @@ SQUARE:
swap1 ; 3 gas
jump ; 8 gas
Total: 53 gas
Total: 50 gas
```
Using `JUMPSUB` saves **_50 - 24 = 26_** gas versus using `JUMP` – a 52% performance improvement.

Expand Down Expand Up @@ -295,7 +272,91 @@ SQUARE:
```
Total 31 gas, compared to 24 gas for the return stack version.

##Validation Algorithm
## Backwards Compatibility

These changes do not affect the semantics of existing EVM code. These changes are compatible with the restricted forms of `JUMP` and `JUMPI` specified by [EIP-3779](./eip-3779.md) -- contracts following all of the rules given there and here will be valid.

## Test Cases

### Simple routine

This should jump into a subroutine, back out and stop.

Bytecode: `0x60045e005b5d` (`PUSH1 0x04, JUMPSUB, STOP, JUMPDEST, RETURNSUB`)

| Pc | Op | Cost | Stack | RStack |
|-------|-------------|------|-----------|-----------|
| 0 | JUMPSUB | 5 | [] | [] |
| 3 | RETURNSUB | 5 | [] | [0] |
| 4 | STOP | 0 | [] | [] |

Output: 0x
Consumed gas: `10`

### Two levels of subroutines

This should execute fine, going into one two depths of subroutines

Bytecode: `0x6800000000000000000c5e005b60115e5d5b5d` (`PUSH9 0x00000000000000000c, JUMPSUB, STOP, JUMPDEST, PUSH1 0x11, JUMPSUB, RETURNSUB, JUMPDEST, RETURNSUB`)

| Pc | Op | Cost | Stack | RStack |
|-------|-------------|------|-----------|-----------|
| 0 | JUMPSUB | 5 | [] | [] |
| 3 | JUMPSUB | 5 | [] | [0] |
| 4 | RETURNSUB | 5 | [] | [0,3] |
| 5 | RETURNSUB | 5 | [] | [3] |
| 6 | STOP | 0 | [] | [] |

Consumed gas: `20`

### Failure 1: invalid jump

This should fail, since the given location is outside of the code-range. The code is the same as previous example,
except that the pushed location is `0x01000000000000000c` instead of `0x0c`.

Bytecode: (`PUSH9 0x01000000000000000c, JUMPSUB, `0x6801000000000000000c5e005b60115e5d5b5d`, STOP, JUMPDEST, PUSH1 0x11, JUMPSUB, RETURNSUB, JUMPDEST, RETURNSUB`)

| Pc | Op | Cost | Stack | RStack |
|-------|-------------|------|-----------|-----------|
| 0 | JUMPSUB | 10 |[18446744073709551628] | [] |

```
Error: at pc=10, op=JUMPSUB: invalid jump destination
```

### Failure 2: shallow `return stack`

This should fail at first opcode, due to shallow `return_stack`

Bytecode: `0x5d5858` (`RETURNSUB, PC, PC`)

| Pc | Op | Cost | Stack | RStack |
|-------|-------------|------|-----------|-----------|
| 0 | RETURNSUB | 5 | [] | [] |

```
Error: at pc=0, op=RETURNSUB: invalid retsub
```

### Subroutine at end of code

In this example. the JUMPSUB is on the last byte of code. When the subroutine returns, it should hit the 'virtual stop' _after_ the bytecode, and not exit with error

Bytecode: `0x6005565b5d5b60035e` (`PUSH1 0x05, JUMP, JUMPDEST, RETURNSUB, JUMPDEST, PUSH1 0x03, JUMPSUB`)

| Pc | Op | Cost | Stack | RStack |
|-------|-------------|------|-----------|-----------|
| 0 | PUSH1 | 3 | [] | [] |
| 2 | JUMP | 8 | [5] | [] |
| 5 | JUMPDEST | 1 | [] | [] |
| 6 | JUMPSUB | 5 | [] | [] |
| 2 | RETURNSUB | 5 | [] | [2] |
| 7 | STOP | 0 | [] | [] |

Consumed gas: `30`


## Validation Algorithm

> This section specifies an algorithm for checking the above the rules. Equivalent code must be run at creation time. We assume that the validation defined in EIP-3540 and EIP-3670 has already run, although in practice the algorithms can be merged.
Expand All @@ -313,7 +374,6 @@ For simplicity's sake we assume a few helper functions.
```
var code [code_len]byte
var depth [code_len]unsigned
var stack [1024]int256 = { -1 } // stack grows down
var sp := 1023
var bp := 1023
Expand Down Expand Up @@ -341,23 +401,6 @@ func validate(pc := 0, depth := 0) boolean {
}
depth[pc] = depth
if (PUSH1 <= instruction && instruction <= PUSH16) {
stack[sp++] = imm_data(pc)
continue
}
if (DUP1 <= instruction && instruction <= DUP16) {
n := instruction - DUP1 + 1
stack[sp + 1] = stack[n + 1]
continue
}
if (SWAP1 <= instruction && instruction <= SWAP16) {
n := instruction - SWAP1 + 1
swap := stack[n]
stack[n] = stack[sp + 1]
stack[sp + 1] = swap
continue
}
if (instruction == RJUMP) {
// check for valid destination
Expand Down Expand Up @@ -404,86 +447,6 @@ func validate(pc := 0, depth := 0) boolean {
}
```

## Test Cases

### Simple routine

This should jump into a subroutine, back out and stop.

Bytecode: `0x60045e005b5d` (`PUSH1 0x04, JUMPSUB, STOP, JUMPDEST, RETURNSUB`)

| Pc | Op | Cost | Stack | RStack |
|-------|-------------|------|-----------|-----------|
| 0 | JUMPSUB | 5 | [] | [] |
| 3 | RETURNSUB | 5 | [] | [0] |
| 4 | STOP | 0 | [] | [] |

Output: 0x
Consumed gas: `10`

### Two levels of subroutines

This should execute fine, going into one two depths of subroutines

Bytecode: `0x6800000000000000000c5e005b60115e5d5b5d` (`PUSH9 0x00000000000000000c, JUMPSUB, STOP, JUMPDEST, PUSH1 0x11, JUMPSUB, RETURNSUB, JUMPDEST, RETURNSUB`)

| Pc | Op | Cost | Stack | RStack |
|-------|-------------|------|-----------|-----------|
| 0 | JUMPSUB | 5 | [] | [] |
| 3 | JUMPSUB | 5 | [] | [0] |
| 4 | RETURNSUB | 5 | [] | [0,3] |
| 5 | RETURNSUB | 5 | [] | [3] |
| 6 | STOP | 0 | [] | [] |

Consumed gas: `20`

### Failure 1: invalid jump

This should fail, since the given location is outside of the code-range. The code is the same as previous example,
except that the pushed location is `0x01000000000000000c` instead of `0x0c`.

Bytecode: (`PUSH9 0x01000000000000000c, JUMPSUB, `0x6801000000000000000c5e005b60115e5d5b5d`, STOP, JUMPDEST, PUSH1 0x11, JUMPSUB, RETURNSUB, JUMPDEST, RETURNSUB`)

| Pc | Op | Cost | Stack | RStack |
|-------|-------------|------|-----------|-----------|
| 0 | JUMPSUB | 10 |[18446744073709551628] | [] |

```
Error: at pc=10, op=JUMPSUB: invalid jump destination
```

### Failure 2: shallow `return stack`

This should fail at first opcode, due to shallow `return_stack`

Bytecode: `0x5d5858` (`RETURNSUB, PC, PC`)

| Pc | Op | Cost | Stack | RStack |
|-------|-------------|------|-----------|-----------|
| 0 | RETURNSUB | 5 | [] | [] |

```
Error: at pc=0, op=RETURNSUB: invalid retsub
```

### Subroutine at end of code

In this example. the JUMPSUB is on the last byte of code. When the subroutine returns, it should hit the 'virtual stop' _after_ the bytecode, and not exit with error

Bytecode: `0x6005565b5d5b60035e` (`PUSH1 0x05, JUMP, JUMPDEST, RETURNSUB, JUMPDEST, PUSH1 0x03, JUMPSUB`)

| Pc | Op | Cost | Stack | RStack |
|-------|-------------|------|-----------|-----------|
| 0 | PUSH1 | 3 | [] | [] |
| 2 | JUMP | 8 | [5] | [] |
| 5 | JUMPDEST | 1 | [] | [] |
| 6 | JUMPSUB | 5 | [] | [] |
| 2 | RETURNSUB | 5 | [] | [2] |
| 7 | STOP | 0 | [] | [] |

Consumed gas: `30`


## Security Considerations

These changes do introduce new flow control instructions, so any software which does static/dynamic analysis of EVM code needs to be modified accordingly. The `JUMPSUB` semantics are similar to `JUMP` whereas the `RETURNSUB` instruction is different, since it can 'land' on any opcode (but the possible destinations can be statically inferred).
Expand Down

0 comments on commit f954f6f

Please sign in to comment.