Skip to content

Commit a1c701d

Browse files
authored
docs(yellowpaper): avm nested call returns, updating calling context (AztecProtocol#3749)
1 parent f1eb6d5 commit a1c701d

File tree

1 file changed

+43
-5
lines changed
  • yellow-paper/docs/public-vm

1 file changed

+43
-5
lines changed

yellow-paper/docs/public-vm/avm.md

+43-5
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ INITIAL_MACHINE_STATE = MachineState {
156156
l2GasLeft: TxRequest.l2GasLimit,
157157
pc: 0,
158158
memory: uninitialized,
159+
internalCallStack: empty,
159160
}
160161
161162
INITIAL_MESSAGE_CALL_RESULTS = MessageCallResults {
@@ -172,9 +173,11 @@ With an initialized context (and therefore an initial program counter of 0), the
172173
### Program Counter and Control Flow
173174
The program counter (machine state's `pc`) determines which instruction to execute (`instr = environment.bytecode[pc]`). Each instruction's state transition function updates the program counter in some way, which allows the VM to progress to the next instruction at each step.
174175

175-
Most instructions simply increment the program counter by 1. This allows VM execution to flow naturally from instruction to instruction. Some instructions ([`JUMP`](./InstructionSet#isa-section-jump), [`JUMPI`](./InstructionSet#isa-section-jumpi), `INTERNALCALL`, `INTERNALRETURN`) modify the program counter based on inputs.
176+
Most instructions simply increment the program counter by 1. This allows VM execution to flow naturally from instruction to instruction. Some instructions ([`JUMP`](./InstructionSet#isa-section-jump), [`JUMPI`](./InstructionSet#isa-section-jumpi), `INTERNALCALL`) modify the program counter based on inputs.
176177

177-
`JUMP`, `JUMPI`, and `INTERNALCALL` assign a new value to program counter from a constant present in the bytecode. These instructions never assign a value from memory to program counter. Before jumping, the `INTERNALCALL` instruction pushes the current program counter to an internal call-stack that is maintained in a reserved region of memory. `INTERNALRETURN` pops a destination from that internal call-stack and jumps there. Thus, jump destinations, can be either constants from the contract bytecode, or destinations popped from the internal call-stack.
178+
The `INTERNALCALL` instruction jumps to the destination specified by its input (sets `pc` to that destination), but first it pushes the current `pc+1` to `machineState.internalCallStack`. The `INTERNALRETURN` instruction pops a destination from `machineState.internalCallStack` and jumps there.
179+
180+
> Jump destinations can only be constants from the contract bytecode, or destinations popped from `machineState.internalCallStack`. A jump destination will never originate from main memory.
178181
179182
### Gas limits and tracking
180183
Each instruction has an associated `l1GasCost` and `l2GasCost`. Before an instruction is executed, the VM enforces that there is sufficient gas remaining via the following assertions:
@@ -202,7 +205,7 @@ A instruction's gas cost is loosely derived from its complexity. Execution compl
202205
- [`JUMP`](./InstructionSet/#isa-section-jump) is an example of an instruction with constant gas cost. Regardless of its inputs, the instruction always incurs the same `l1GasCost` and `l2GasCost`.
203206
- The [`SET`](./InstructionSet/#isa-section-set) instruction operates on a different sized constant (based on its `dst-type`). Therefore, this instruction's gas cost increases with the size of its input.
204207
- Instructions that operate on a data range of a specified "size" scale in cost with that size. An example of this is the [`CALLDATACOPY`](./InstructionSet/#isa-section-calldatacopy) argument which copies `copySize` words from `environment.calldata` to memory.
205-
- The [`CALL`](./InstructionSet/#isa-section-call)/[`STATICCALL`](./InstructionSet/#isa-section-call)/`DELEGATECALL` instruction's gas cost is determined by its `l*Gas` arguments, but any gas unused by the triggered message call is refunded after its completion (more on this later).
208+
- The [`CALL`](./InstructionSet/#isa-section-call)/[`STATICCALL`](./InstructionSet/#isa-section-call)/`DELEGATECALL` instruction's gas cost is determined by its `l*Gas` arguments, but any gas unused by the triggered message call is refunded after its completion ([more on this later](#updating-the-calling-context-after-nested-call-halts)).
206209
- An instruction with "offset" arguments (like [`ADD`](./InstructionSet/#isa-section-add) and many others), has increased cost for each offset argument that is flagged as "indirect".
207210

208211
> Implementation detail: an instruction's gas cost will roughly align with the number of rows it corresponds to in the SNARK execution trace including rows in the sub-operation table, memory table, chiplet tables, etc.
@@ -222,6 +225,8 @@ results.output = machineState.memory[instr.args.retOffset:instr.args.retOffset+i
222225
```
223226
> Definitions: `retOffset` and `retSize` here are arguments to the [`RETURN`](./InstructionSet/#isa-section-return) and [`REVERT`](./InstructionSet/#isa-section-revert) instructions. If `retSize` is 0, the context will have no output. Otherwise, these arguments point to a region of memory to output.
224227
228+
> Note: `results.output` is only relevant when the caller is a message call itself. When a public execution request's initial message call halts normally, its `results.output` is ignored.
229+
225230
### Exceptional halting
226231
An exceptional halt is not explicitly triggered by an instruction but instead occurs when one of the following halting conditions is met:
227232
1. **Insufficient gas**
@@ -242,7 +247,7 @@ An exceptional halt is not explicitly triggered by an instruction but instead oc
242247
1. **World state modification attempt during a static call**
243248
```
244249
assert !environment.isStaticCall
245-
or environment.bytecode[machineState.pc].opcode not in WS_MODIFYING_OPS
250+
OR environment.bytecode[machineState.pc].opcode not in WS_MODIFYING_OPS
246251
```
247252
> Definition: `WS_MODIFYING_OPS` represents the list of all opcodes corresponding to instructions that modify world state.
248253
@@ -302,9 +307,42 @@ nestedMachineState = MachineState {
302307
l2GasLeft: callingContext.machineState.memory[instr.args.gasOffset+1],
303308
pc: 0,
304309
memory: uninitialized,
310+
internalCallStack: empty,
305311
}
306312
```
313+
> Note: the sub-context machine state's `l*GasLeft` is initialized based on the call instruction's `gasOffset` argument. The caller allocates some amount of L1 and L2 gas to the nested call. It does so using the instruction's `gasOffset` argument. In particular, prior to the message call instruction, the caller populates `M[gasOffset]` with the sub-context's initial `l1GasLeft`. Likewise it populates `M[gasOffset+1]` with `l2GasLeft`.
314+
307315
> Note: recall that `INITIAL_MESSAGE_CALL_RESULTS` is the same initial value used during [context initialization for a public execution request's initial message call](#context-initialization-for-initial-call).
308316
> `STATICCALL_OP` and `DELEGATECALL_OP` refer to the 8-bit opcode values for the `STATICCALL` and `DELEGATECALL` instructions respectively.
309317
310-
### Updating the calling context after nested call halts
318+
### Updating the calling context after nested call halts
319+
When a message call's execution encounters an instruction that itself triggers a message call, the nested call executes until it reaches a halt. At that point, control returns to the caller, and the calling context is updated based on the sub-context and the message call instruction's transition function. The components of that transition function are defined below.
320+
321+
The success or failure of the nested call is captured into memory at the offset specified by the call instruction's `successOffset` input:
322+
```
323+
context.machineState.memory[instr.args.successOffset] = !subContext.results.reverted
324+
```
325+
326+
Recall that a nested call is allocated some gas. In particular, the call instruction's `gasOffset` input points to an L1 and L2 gas allocation for the nested call. As shown in the [section above](#context-initialization-for-a-nested-call), a nested call's `subContext.machineState.l1GasLeft` is initialized to `context.machineState.memory[instr.args.gasOffset]`. Likewise, `l2GasLeft` is initialized from `gasOfffset+1`.
327+
328+
As detailed in [the gas section above](#gas-cost-notes-and-examples), every instruction has an associated `instr.l1GasCost` and `instr.l2GasCost`. A nested call instruction's cost is the same as its initial `l*GasLeft` and `l2GasLeft`. Prior to the nested call's execution, this cost is subtracted from the calling context's remaining gas.
329+
330+
When a nested call completes, any of its allocated gas that remains unused is refunded to the caller.
331+
```
332+
context.l1GasLeft += subContext.machineState.l1GasLeft
333+
context.l2GasLeft += subContext.machineState.l2GasLeft
334+
```
335+
336+
If a nested call halts normally with a [`RETURN`](./InstructionSet/#isa-section-return) or [`REVERT`](./InstructionSet/#isa-section-revert), it may have some output data (`subContext.results.output`). The caller's `retOffset` and `retSize` arguments to the nested call instruction specify a region in memory to place output data when the nested call returns.
337+
```
338+
if instr.args.retSize > 0:
339+
context.memory[instr.args.retOffset:instr.args.retOffset+instr.args.retSize] = subContext.results.output
340+
```
341+
342+
As long as a nested call has not reverted, its updates to the world state and accrued substate will be absorbed into the calling context.
343+
```
344+
if !subContext.results.reverted AND instr.opcode != STATICCALL_OP:
345+
context.worldState = subContext.worldState
346+
context.accruedSubstate.append(subContext.accruedSubstate)
347+
```
348+
> Reminder: a nested call cannot make updates to the world state or accrued substate if it is a [`STATICCALL`](./InstructionSet/#isa-section-staticcall).

0 commit comments

Comments
 (0)