From 68595ab256cb204f7ecaa426b9fcef4468ec681c Mon Sep 17 00:00:00 2001 From: dbanks12 Date: Tue, 19 Dec 2023 21:12:41 -0500 Subject: [PATCH 1/2] yellowpaper avm nested call returns --- yellow-paper/docs/public-vm/avm.md | 46 +++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/yellow-paper/docs/public-vm/avm.md b/yellow-paper/docs/public-vm/avm.md index 0ac65539a7a9..1f86dbe5f024 100644 --- a/yellow-paper/docs/public-vm/avm.md +++ b/yellow-paper/docs/public-vm/avm.md @@ -156,6 +156,7 @@ INITIAL_MACHINE_STATE = MachineState { l2GasLeft: TxRequest.l2GasLimit, pc: 0, memory: uninitialized, + internalCallStack: empty, } INITIAL_MESSAGE_CALL_RESULTS = MessageCallResults { @@ -172,9 +173,11 @@ With an initialized context (and therefore an initial program counter of 0), the ### Program Counter and Control Flow 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. -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. +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. -`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. +The `INTERNALCALL` instruction jumps to the destination specified by its input (sets `pc` to that destination), but first it pushes the current program counter to `machineState.internalCallStack`. The `INTERNALRETURN` instruction pops a destination from `machineState.internalCallStack` and jumps to that destination. + +> 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. ### Gas limits and tracking 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 - [`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`. - 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. - 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. -- 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). +- 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)). - 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". > 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 ``` > 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. +> 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. + ### Exceptional halting An exceptional halt is not explicitly triggered by an instruction but instead occurs when one of the following halting conditions is met: 1. **Insufficient gas** @@ -302,9 +307,42 @@ nestedMachineState = MachineState { l2GasLeft: callingContext.machineState.memory[instr.args.gasOffset+1], pc: 0, memory: uninitialized, + internalCallStack: empty, } ``` +> 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`. + > 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). > `STATICCALL_OP` and `DELEGATECALL_OP` refer to the 8-bit opcode values for the `STATICCALL` and `DELEGATECALL` instructions respectively. -### Updating the calling context after nested call halts \ No newline at end of file +### Updating the calling context after nested call halts +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. + +The success or failure of the nested call is captured into memory at the offset specified by the call instruction's `successOffset` input: +``` +context.machineState.memory[instr.args.successOffset] = subContext.results.reverted +``` + +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`. + +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. + +When a nested call completes, any of its allocated gas that remains unused is refunded to the caller. +``` +context.l1GasLeft += subContext.machineState.l1GasLeft +context.l2GasLeft += subContext.machineState.l2GasLeft +``` + +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. +``` +if instr.args.retSize > 0: + context.memory[instr.args.retOffset:instr.args.retOffset+instr.args.retSize] = subContext.results.output +``` + +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. +``` +if !subContext.results.reverted and instr.opcode != STATICCALL_OP: + context.worldState = subContext.worldState + context.accruedSubstate.append(subContext.accruedSubstate) +``` +> Reminder: a nested call cannot make updates to the world state or accrued substate if it is a [`STATICCALL`](./InstructionSet/#isa-section-staticcall). \ No newline at end of file From 0962547fd5e5808e53a7280c45bc861317a3331b Mon Sep 17 00:00:00 2001 From: dbanks12 Date: Wed, 20 Dec 2023 15:53:17 -0500 Subject: [PATCH 2/2] cleanup after PR comments --- yellow-paper/docs/public-vm/avm.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/yellow-paper/docs/public-vm/avm.md b/yellow-paper/docs/public-vm/avm.md index 1f86dbe5f024..d79f5898cf94 100644 --- a/yellow-paper/docs/public-vm/avm.md +++ b/yellow-paper/docs/public-vm/avm.md @@ -175,7 +175,7 @@ The program counter (machine state's `pc`) determines which instruction to execu 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. -The `INTERNALCALL` instruction jumps to the destination specified by its input (sets `pc` to that destination), but first it pushes the current program counter to `machineState.internalCallStack`. The `INTERNALRETURN` instruction pops a destination from `machineState.internalCallStack` and jumps to that destination. +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. > 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. @@ -247,7 +247,7 @@ An exceptional halt is not explicitly triggered by an instruction but instead oc 1. **World state modification attempt during a static call** ``` assert !environment.isStaticCall - or environment.bytecode[machineState.pc].opcode not in WS_MODIFYING_OPS + OR environment.bytecode[machineState.pc].opcode not in WS_MODIFYING_OPS ``` > Definition: `WS_MODIFYING_OPS` represents the list of all opcodes corresponding to instructions that modify world state. @@ -320,7 +320,7 @@ When a message call's execution encounters an instruction that itself triggers a The success or failure of the nested call is captured into memory at the offset specified by the call instruction's `successOffset` input: ``` -context.machineState.memory[instr.args.successOffset] = subContext.results.reverted +context.machineState.memory[instr.args.successOffset] = !subContext.results.reverted ``` 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`. @@ -341,7 +341,7 @@ if instr.args.retSize > 0: 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. ``` -if !subContext.results.reverted and instr.opcode != STATICCALL_OP: +if !subContext.results.reverted AND instr.opcode != STATICCALL_OP: context.worldState = subContext.worldState context.accruedSubstate.append(subContext.accruedSubstate) ```