diff --git a/avm-transpiler/src/opcodes.rs b/avm-transpiler/src/opcodes.rs index 031bee76d2e..ef91f2b368d 100644 --- a/avm-transpiler/src/opcodes.rs +++ b/avm-transpiler/src/opcodes.rs @@ -76,6 +76,7 @@ pub enum AvmOpcode { // Control Flow - Contract Calls CALL, STATICCALL, + DELEGATECALL, RETURN, REVERT, @@ -163,6 +164,7 @@ impl AvmOpcode { // Control Flow - Contract Calls AvmOpcode::CALL => "CALL", AvmOpcode::STATICCALL => "STATICCALL", + AvmOpcode::DELEGATECALL => "DELEGATECALL", AvmOpcode::RETURN => "RETURN", AvmOpcode::REVERT => "REVERT", diff --git a/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_opcode.cpp b/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_opcode.cpp index d1202fd932c..b5e16c06a24 100644 --- a/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_opcode.cpp +++ b/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_opcode.cpp @@ -82,6 +82,7 @@ const std::unordered_map Bytecode::OPERANDS_NUM = { //// Control Flow - Contract Calls //{ OpCode::CALL, }, //{ OpCode::STATICCALL, }, + //{ OpCode::DELEGATECALL, }, { OpCode::RETURN, 2 }, // { OpCode::REVERT, }, diff --git a/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_opcode.hpp b/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_opcode.hpp index 634ba203205..e2520ba5448 100644 --- a/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_opcode.hpp +++ b/barretenberg/cpp/src/barretenberg/vm/avm_trace/avm_opcode.hpp @@ -90,6 +90,7 @@ enum class OpCode : uint8_t { // Control Flow - Contract Calls CALL, STATICCALL, + DELEGATECALL, RETURN, REVERT, diff --git a/yarn-project/simulator/src/avm/serialization/bytecode_serialization.ts b/yarn-project/simulator/src/avm/serialization/bytecode_serialization.ts index dcacc5dbf01..472de699af5 100644 --- a/yarn-project/simulator/src/avm/serialization/bytecode_serialization.ts +++ b/yarn-project/simulator/src/avm/serialization/bytecode_serialization.ts @@ -125,6 +125,7 @@ const INSTRUCTION_SET = () => // Control Flow - Contract Calls [Call.opcode, Call], [StaticCall.opcode, StaticCall], + //[DelegateCall.opcode, DelegateCall], [Return.opcode, Return], [Revert.opcode, Revert], diff --git a/yarn-project/simulator/src/avm/serialization/instruction_serialization.ts b/yarn-project/simulator/src/avm/serialization/instruction_serialization.ts index d9e07f50f04..22eb3f366dc 100644 --- a/yarn-project/simulator/src/avm/serialization/instruction_serialization.ts +++ b/yarn-project/simulator/src/avm/serialization/instruction_serialization.ts @@ -61,6 +61,7 @@ export enum Opcode { SENDL2TOL1MSG, CALL, STATICCALL, + DELEGATECALL, RETURN, REVERT, KECCAK, diff --git a/yellow-paper/docs/contract-deployment/instances.md b/yellow-paper/docs/contract-deployment/instances.md index eb31836761f..7c19895dcdc 100644 --- a/yellow-paper/docs/contract-deployment/instances.md +++ b/yellow-paper/docs/contract-deployment/instances.md @@ -145,7 +145,7 @@ Specific to private functions: Specific to public functions: -- The bytecode loaded by the [AVM](../public-vm/avm.md) for the contract matches the `bytecode_commitment` in the contract class, verified using the [bytecode validation circuit](../public-vm/bytecode-validation-circuit.md). +- The bytecode loaded by the [AVM](../public-vm/intro) for the contract matches the `bytecode_commitment` in the contract class, verified using the [bytecode validation circuit](../public-vm/bytecode-validation-circuit). - The contract Deployment Nullifier has been emitted, or prove that it hasn't, in which case the transaction is expected to revert. This check is done via a merkle (non-)membership proof of the Deployment Nullifier. Note that a public function should be callable in the same transaction in which its contract Deployment Nullifier was emitted. Note that, since constructors are handled at the application level, the kernel circuit is not required to check the Initialization Nullifier before executing code. diff --git a/yellow-paper/docs/public-vm/_nested-context.md b/yellow-paper/docs/public-vm/_nested-context.md new file mode 100644 index 00000000000..5de8badab2f --- /dev/null +++ b/yellow-paper/docs/public-vm/_nested-context.md @@ -0,0 +1,55 @@ +The nested call's execution context is derived from the caller's context and the call instruction's arguments. + +The following shorthand syntax is used to refer to nested context derivation in the ["Instruction Set"](./instruction-set) and other sections: + +```jsx +// instr.args are { gasOffset, addrOffset, argsOffset, retOffset, retSize } + +isStaticCall = instr.opcode == STATICCALL +isDelegateCall = instr.opcode == DELEGATECALL + +nestedContext = deriveContext(context, instr.args, isStaticCall, isDelegateCall) +``` + +Nested context derivation is defined as follows: +```jsx +nestedExecutionEnvironment = ExecutionEnvironment { + origin: context.origin, + sender: isDelegateCall ? context.sender : context.address, + address: M[addrOffset], + storageAddress: isDelegateCall ? context.storageAddress : M[addrOffset], + portal: callingContext.worldState.contracts[M[addrOffset]].portal, + feePerL1Gas: context.environment.feePerL1Gas, + feePerL2Gas: context.environment.feePerL2Gas, + feePerDaGas: context.environment.feePerDaGas, + contractCallDepth: context.contractCallDepth + 1, + contractCallPointer: context.worldStateAccessTrace.contractCalls.length + 1, + globals: context.globals, + isStaticCall: isStaticCall, + isDelegateCall: isDelegateCall, + calldata: context.memory[M[argsOffset]:M[argsOffset]+argsSize], +} + +nestedMachineState = MachineState { + l1GasLeft: context.machineState.memory[M[gasOffset]], + l2GasLeft: context.machineState.memory[M[gasOffset+1]], + daGasLeft: context.machineState.memory[M[gasOffset+2]], + pc = 0, + internalCallStack = [], // initialized as empty + memory = [0, ..., 0], // all 2^32 entries are initialized to zero +} +``` + + +```jsx +nestedContext = AvmContext { + environment: nestedExecutionEnvironment, + machineState: nestedMachineState, + worldState: context.worldState, + worldStateAccessTrace: context.worldStateAccessTrace, + accruedSubstate: { [], ... [], }, // all empty + results: {reverted: false, output: []}, +} +``` + +> `M[offset]` notation is shorthand for `context.machineState.memory[offset]` \ No newline at end of file diff --git a/yellow-paper/docs/public-vm/avm-circuit.md b/yellow-paper/docs/public-vm/avm-circuit.md index d4c3c17e653..6ab5a2f437d 100644 --- a/yellow-paper/docs/public-vm/avm-circuit.md +++ b/yellow-paper/docs/public-vm/avm-circuit.md @@ -24,7 +24,7 @@ Prior to the VM circuit's execution, a vector is assembled to contain the byteco Each entry in the bytecode vector will be paired with a call pointer and program counter. This **Bytecode Table** maps a call pointer and program counter to an instruction, and is used by the Instruction Controller to fetch instructions. > Note: "call pointer" is expanded on in a later section. -Each contract's public bytecode is committed to during contract deployment. As part of the AVM circuit verification algorithm, the bytecode vector (as a concatenation of all relevant contract bytecodes) is verified against the corresponding bytecode commitments. This is expanded on in ["Bytecode Validation Circuit"](./bytecode-validation-circuit.md). While the AVM circuit enforces that the correct instructions are executed according to its bytecode table, the verifier checks that bytecode table against the previously validated bytecode commitments. +Each contract's public bytecode is committed to during contract deployment. As part of the AVM circuit verification algorithm, the bytecode vector (as a concatenation of all relevant contract bytecodes) is verified against the corresponding bytecode commitments. This is expanded on in ["Bytecode Validation Circuit"](./bytecode-validation-circuit). While the AVM circuit enforces that the correct instructions are executed according to its bytecode table, the verifier checks that bytecode table against the previously validated bytecode commitments. ## Instruction Controller The Instruction Controller's responsibilities include instruction fetching and decoding. @@ -32,7 +32,7 @@ The Instruction Controller's responsibilities include instruction fetching and d ### Instruction fetching The Instruction Controller's **instruction fetch** mechanism makes use of the bytecode table to determine which instruction to execute based on the call pointer and program counter. Each instruction fetch corresponds to a circuit lookup to enforce that the correct instruction is processed for a given contract and program counter. -The combination of the instruction fetch circuitry, the bytecode table, and the ["Bytecode Validation Circuit"](./bytecode-validation-circuit.md) ensure that VM circuit processes the proper sequence of instructions. +The combination of the instruction fetch circuitry, the bytecode table, and the ["Bytecode Validation Circuit"](./bytecode-validation-circuit) ensure that VM circuit processes the proper sequence of instructions. ### Instruction decoding and sub-operations An instruction (its opcode, flags, and arguments) represents some high-level VM operation. For example, an `ADD` instruction says "add two items from memory and store the result in memory". The Instruction Controller **instruction decode** mechanism decodes instructions into sub-operations. While an instruction likely requires many circuit components, a **sub-operation** is a smaller task that can be fed to just one VM circuit component for processing. By decoding an instruction into sub-operations, the VM circuit translates high-level instructions into smaller achievable tasks. To continue with the `ADD` example, it would translate "add two items from memory and store the result in memory" to "load an item from memory, load another item from memory, add them, and store the result to memory." @@ -192,7 +192,6 @@ AvmSessionPublicInputs { sessionResults: AvmSessionResults, } ``` -> The `ExecutionEnvironment` structure is defined in [the AVM's high level specification](./avm.md). `initialEnvironment` here omits `calldata` and `bytecode`. > The `WorldStateAccessTrace` and `AccruedSubstate` types are defined in ["State"](./state). Their vectors are assigned constant/maximum lengths when used as circuit inputs. diff --git a/yellow-paper/docs/public-vm/avm.md b/yellow-paper/docs/public-vm/avm.md deleted file mode 100644 index 6227fef9a59..00000000000 --- a/yellow-paper/docs/public-vm/avm.md +++ /dev/null @@ -1,449 +0,0 @@ -# Aztec Virtual Machine - -:::note reference -Many terms and definitions here are borrowed from the [Ethereum Yellow Paper](https://ethereum.github.io/yellowpaper/paper.pdf). -::: - -## Introduction - -An Aztec transaction may include one or more **public execution requests**. A public execution request is a request to execute a specified contract's public bytecode given some arguments. Execution of a contract's public bytecode is performed by the **Aztec Virtual Machine (AVM)**. - -> A public execution request may originate from a public call enqueued by a transaction's private segment ([`enqueuedPublicFunctionCalls`](../calls/enqueued-calls.md)), or from a public [fee preparation](../gas-and-fees#fee-preparation) or [fee distribution](../gas-and-fees#fee-distribution) call. - -In order to execute public contract bytecode, the AVM requires some context. An [**execution context**](#execution-context) contains all information necessary to initiate AVM execution, including the relevant contract's bytecode and all state maintained by the AVM. A **contract call** initializes an execution context and triggers AVM execution within that context. - -Instruction-by-instruction, the AVM [executes](#execution) the bytecode specified in its context. An **instruction** is a bytecode entry that, when executed, modifies the AVM's execution context (in particular its [state](./state)) according to the instruction's definition in the ["AVM Instruction Set"](./instruction-set). Execution within a context ends when the AVM encounters a [**halt**](#halting). - -During execution, additional contract calls may be made. While an [**initial contract call**](#initial-contract-calls) initializes a new execution context directly from a public execution request, a [**nested contract call**](#nested-contract-calls) occurs _during_ AVM execution and is triggered by a **contract call instruction** ([`CALL`](./instruction-set#isa-section-call), [`STATICCALL`](./instruction-set#isa-section-call), or `DELEGATECALL`). It initializes a new execution context (**nested context**) from the current one (**calling context**) and triggers execution within it. When nested call's execution completes, execution proceeds in the calling context. - -A **caller** is a contract call's initiator. The caller of an initial contract call is an Aztec sequencer. The caller of a nested contract call is the AVM itself executing in the calling context. - -## Outline - -- [**Public contract bytecode**](#public-contract-bytecode) (aka AVM bytecode) -- [**Execution context**](#execution-context), outlining the AVM's execution context -- [**Execution**](#execution), outlining control flow, gas tracking, normal halting, and exceptional halting -- [**Initial contract calls**](#initial-contract-calls), outlining the initiation of a contract call from a public execution request -- [**Nested contract calls**](#nested-contract-calls), outlining the initiation of a contract call from an instruction as well as the processing of nested execution results, gas refunds, and state reverts - -> This document is meant to provide a high-level definition of the Aztec Virtual Machine as opposed to a specification of its SNARK implementation. The document therefore mostly omits SNARK or circuit-centric verbiage except when particularly relevant to high-level design decisions. - -This document is supplemented by the following resources: -- **[AVM State](./state.md)** -- **[AVM Instruction Set](./instruction-set)** -- **[AVM Memory Model](./memory-model.md)** -- **[AVM Circuit](./avm-circuit.md)** - -## Public contract bytecode - -A contract's public bytecode is a series of execution instructions for the AVM. Refer to the ["AVM Instruction Set"](./instruction-set) for the details of all supported instructions along with how they modify AVM state. - -The entirety of a contract's public code is represented as a single block of bytecode with a maximum of `MAX_PUBLIC_INSTRUCTIONS_PER_CONTRACT` ($2^{15} = 32768$) instructions. The mechanism used to distinguish between different "functions" in an AVM bytecode program is left as a higher-level abstraction (_e.g._ similar to Solidity's concept of a function selector). - -> See the [Bytecode Validation Circuit](./bytecode-validation-circuit.md) to see how a contract's bytecode can be validated and committed to. - -## Execution Context - -:::note REMINDER -Many terms and definitions here are borrowed from the [Ethereum Yellow Paper](https://ethereum.github.io/yellowpaper/paper.pdf). -::: - -An **execution context** includes the information and state relevant to a contract call's execution. When a contract call is made, an execution context is initialized as specified in the ["Initial contract calls"](#initial-contract-calls) and ["Nested contract calls"](#nested-contract-calls) sections. - -#### _AvmContext_ -| Field | Type | -| --- | --- | -| environment | `ExecutionEnvironment` | -| [machineState](./state#machine-state) | `MachineState` | -| [worldState](./state#avm-world-state) | `AvmWorldState` | -| [worldStateAccessTrace](./state#world-state-access-trace) | `WorldStateAccessTrace` | -| [accruedSubstate](./state#accrued-substate) | `AccruedSubstate` | -| results | `ContractCallResults` | - -### Execution Environment - -A context's **execution environment** remains constant throughout a contract call's execution. When a contract call initializes its execution context, it fully specifies the execution environment. This is expanded on in the ["Initial contract calls"](#initial-contract-calls) and ["Nested contract calls"](#nested-contract-calls) sections. - -#### _ExecutionEnvironment_ -| Field | Type | Description | -| --- | --- | --- | -| origin | `AztecAddress` | | -| feePerL1Gas | `field` | | -| feePerL2Gas | `field` | | -| feePerDaGas | `field` | | -| globals | `PublicGlobalVariables` | | -| address | `AztecAddress` | | -| storageAddress | `AztecAddress` | | -| sender | `AztecAddress` | | -| portal | `AztecAddress` | | -| contractCallDepth | `field` | | -| isStaticCall | `boolean` | | -| isDelegateCall | `boolean` | | -| calldata | `[field; ]` | | -| bytecode | `[field; ]` | | - -### Contract Call Results - -Finally, when a contract call halts, it sets the context's **contract call results** to communicate results to the caller. - -#### _ContractCallResults_ -| Field | Type | Description | -| --- | --- | --- | -| reverted | `boolean` | | -| output | `[field; ]` | | - -## Execution - -Once an execution context has been initialized for a contract call, the [machine state's](./state#machine-state) program counter determines which instruction the AVM executes. For any contract call, the program counter starts at zero, and so instruction execution begins with the very first entry in a contract's bytecode. - -### Program Counter and Control Flow - -The program counter (`machineState.pc`) determines which instruction the AVM executes next (`instr = environment.bytecode[pc]`). Each instruction's execution updates the program counter in some way, which allows the AVM 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`](./instruction-set#isa-section-jump), [`JUMPI`](./instruction-set#isa-section-jumpi), `INTERNALCALL`) modify the program counter based on arguments. - -The `INTERNALCALL` instruction pushes `machineState.pc+1` to `machineState.internalCallStack` and then updates `pc` to the instruction's destination argument (`instr.args.loc`). The `INTERNALRETURN` instruction pops a destination from `machineState.internalCallStack` and assigns the result to `pc`. - -> An instruction will never assign program counter a value from memory (`machineState.memory`). A `JUMP`, `JUMPI`, or `INTERNALCALL` instruction's destination is a constant from the program bytecode. This property allows for easier static program analysis. - -### Gas limits and tracking -> See ["Gas and Fees"](../gas-and-fees) for a deeper dive into Aztec's gas model and for definitions of each type of gas. - -Each instruction has an associated `l1GasCost`, `l2GasCost`, and `daGasCost`. Before an instruction is executed, the VM enforces that there is sufficient gas remaining via the following assertions: -``` -assert machineState.l1GasLeft - instr.l1GasCost > 0 -assert machineState.l2GasLeft - instr.l2GasCost > 0 -assert machineState.daGasLeft - instr.daGasCost > 0 -``` - -> Many instructions (like arithmetic operations) have 0 `l1GasCost` and `daGasCost`. Instructions only incur an L1 or DA cost if they modify the [world state](./state#avm-world-state) or [accrued substate](./state#accrued-substate). - -If these assertions pass, the machine state's gas left is decreased prior to the instruction's core execution: - -``` -machineState.l1GasLeft -= instr.l1GasCost -machineState.l2GasLeft -= instr.l2GasCost -machineState.daGasLeft -= instr.daGasCost -``` - -If either of these assertions _fail_ for an instruction, this triggers an exceptional halt. The gas left is set to 0 and execution reverts. - -``` -machineState.l1GasLeft = 0 -machineState.l2GasLeft = 0 -machineState.daGasLeft = 0 -``` - -> Reverting and exceptional halts are covered in more detail in the ["Halting" section](#halting). - -### Gas cost notes and examples - -An instruction's gas cost is meant to reflect the computational cost of generating a proof of its correct execution. For some instructions, this computational cost changes based on inputs. Here are some examples and important notes: -- [`JUMP`](./instruction-set/#isa-section-jump) is an example of an instruction with constant gas cost. Regardless of its inputs, the instruction always incurs the same `l1GasCost`, `l2GasCost`, and `daGasCost`. -- The [`SET`](./instruction-set/#isa-section-set) instruction operates on a different sized constant (based on its `dstTag`). 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`](./instruction-set/#isa-section-calldatacopy) argument which copies `copySize` words from `environment.calldata` to `machineState.memory`. -- The [`CALL`](./instruction-set/#isa-section-call)/[`STATICCALL`](./instruction-set/#isa-section-call)/`DELEGATECALL` instruction's gas cost is determined by its `*Gas` arguments, but any gas unused by the nested contract call's execution is refunded after its completion ([more on this later](#updating-the-calling-context-after-nested-call-halts)). -- An instruction with "offset" arguments (like [`ADD`](./instruction-set/#isa-section-add) and many others), has increased cost for each offset argument that is flagged as "indirect". - -> 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. - -> An instruction's gas cost takes into account the costs of associated downstream computations. An instruction that triggers accesses to the public data tree (`SLOAD`/`SSTORE`) incurs a cost that accounts for state access validation in later circuits (public kernel or rollup). A contract call instruction (`CALL`/`STATICCALL`/`DELEGATECALL`) incurs a cost accounting for the nested call's complete execution as well as any work required by the public kernel circuit for this additional call. - -### Halting - -A context's execution can end with a **normal halt** or **exceptional halt**. A halt ends execution within the current context and returns control flow to the calling context. - -#### Normal halting - -A normal halt occurs when the VM encounters an explicit halting instruction ([`RETURN`](./instruction-set#isa-section-return) or [`REVERT`](./instruction-set#isa-section-revert)). Such instructions consume gas normally and optionally initialize some output data before finally halting the current context's execution. - -``` -machineState.l1GasLeft -= instr.l1GasCost -machineState.l2GasLeft -= instr.l2GasCost -machineState.daGasLeft -= instr.daGasCost -results.reverted = instr.opcode == REVERT -results.output = machineState.memory[instr.args.retOffset:instr.args.retOffset+instr.args.retSize] -``` - -> Definitions: `retOffset` and `retSize` here are arguments to the [`RETURN`](./instruction-set/#isa-section-return) and [`REVERT`](./instruction-set#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. - -> `results.output` is only relevant when the caller is a contract call itself. In other words, it is only relevant for [nested contract calls](#nested-contract-calls). When an [initial contract call](#initial-contract-calls) (initiated by a public execution request) halts normally, its `results.output` is ignored. - -#### Exceptional halting - -An exceptional halt is not explicitly triggered by an instruction but instead occurs when an exceptional condition is met. - -When an exceptional halt occurs, the context is flagged as consuming all of its allocated gas and is marked as `reverted` with no output data, and then execution within the current context ends. - -``` -machineState.l1GasLeft = 0 -machineState.l2GasLeft = 0 -machineState.daGasLeft = 0 -results.reverted = true -// results.output remains empty -``` - -The AVM's exceptional halting conditions area listed below: - -1. **Insufficient gas** - ``` - assert machineState.l1GasLeft - instr.l1GasCost > 0 - assert machineState.l2GasLeft - instr.l2GasCost > 0 - assert machineState.daGasLeft - instr.l2GasCost > 0 - ``` -1. **Invalid instruction encountered** - ``` - assert environment.bytecode[machineState.pc].opcode <= MAX_AVM_OPCODE - ``` -1. **Jump destination past end of bytecode** - ``` - assert environment.bytecode[machineState.pc].opcode not in {JUMP, JUMPI, INTERNALCALL} - OR instr.args.loc < environment.bytecode.length - ``` -1. **Failed memory tag check** - - Defined per-instruction in the [Instruction Set](./instruction-set) -1. **Maximum memory index ($2^{32}$) exceeded** - ``` - for offset in instr.args.*Offset: - assert offset < 2^32 - ``` -1. **World state modification attempt during a static call** - ``` - assert !environment.isStaticCall - OR environment.bytecode[machineState.pc].opcode not in WS_AS_MODIFYING_OPS - ``` - > Definition: `WS_AS_MODIFYING_OPS` represents the list of all opcodes corresponding to instructions that modify world state or accrued substate. -1. **Maximum contract call depth (1024) exceeded** - ``` - assert environment.contractCallDepth <= 1024 - assert environment.bytecode[machineState.pc].opcode not in {CALL, STATICCALL, DELEGATECALL} - OR environment.contractCallDepth < 1024 - ``` -1. **Maximum contract call calls per execution request (1024) exceeded** - ``` - assert worldStateAccessTrace.contractCalls.length <= 1024 - assert environment.bytecode[machineState.pc].opcode not in {CALL, STATICCALL, DELEGATECALL} - OR worldStateAccessTrace.contractCalls.length < 1024 - ``` -1. **Maximum internal call depth (1024) exceeded** - ``` - assert machineState.internalCallStack.length <= 1024 - assert environment.bytecode[machineState.pc].opcode != INTERNALCALL - OR environment.contractCallDepth < 1024 - ``` -1. **Maximum world state accesses (1024-per-category) exceeded** - ``` - assert worldStateAccessTrace.publicStorageReads.length <= 1024 - AND worldStateAccessTrace.publicStorageWrites.length <= 1024 - AND worldStateAccessTrace.noteHashChecks.length <= 1024 - AND worldStateAccessTrace.newNoteHashes.length <= 1024 - AND worldStateAccessTrace.nullifierChecks.length <= 1024 - AND worldStateAccessTrace.newNullifiers.length <= 1024 - AND worldStateAccessTrace.l1ToL2MessageReads.length <= 1024 - AND worldStateAccessTrace.archiveChecks.length <= 1024 - - // Storage - assert environment.bytecode[machineState.pc].opcode != SLOAD - OR worldStateAccessTrace.publicStorageReads.length < 1024 - assert environment.bytecode[machineState.pc].opcode != SSTORE - OR worldStateAccessTrace.publicStorageWrites.length < 1024 - - // Note hashes - assert environment.bytecode[machineState.pc].opcode != NOTEHASHEXISTS - OR noteHashChecks.length < 1024 - assert environment.bytecode[machineState.pc].opcode != EMITNOTEHASH - OR newNoteHashes.length < 1024 - - // Nullifiers - assert environment.bytecode[machineState.pc].opcode != NULLIFIEREXISTS - OR nullifierChecks.length < 1024 - assert environment.bytecode[machineState.pc].opcode != EMITNULLIFIER - OR newNullifiers.length < 1024 - - // Read L1 to L2 messages - assert environment.bytecode[machineState.pc].opcode != READL1TOL2MSG - OR worldStateAccessTrace.l1ToL2MessagesReads.length < 1024 - - // Archive tree & Headers - assert environment.bytecode[machineState.pc].opcode != HEADERMEMBER - OR archiveChecks.length < 1024 - ``` -1. **Maximum accrued substate entries (per-category) exceeded** - ``` - assert accruedSubstate.unencryptedLogs.length <= MAX_UNENCRYPTED_LOGS - AND accruedSubstate.sentL2ToL1Messages.length <= MAX_SENT_L2_TO_L1_MESSAGES - - // Unencrypted logs - assert environment.bytecode[machineState.pc].opcode != ULOG - OR unencryptedLogs.length < MAX_UNENCRYPTED_LOGS - - // Sent L2 to L1 messages - assert environment.bytecode[machineState.pc].opcode != SENDL2TOL1MSG - OR sentL2ToL1Messages.length < MAX_SENT_L2_TO_L1_MESSAGES - ``` - > Note that ideally the AVM should limit the _total_ accrued substate entries per-category instead of the entries per-call. - -## Initial contract calls - -An **initial contract call** initializes a new execution context from a public execution request. - -### Context initialization for initial contract calls - -An initial contract call initializes its execution context as follows: -``` -context = AvmContext { - environment = INITIAL_EXECUTION_ENVIRONMENT, - machineState = INITIAL_MACHINE_STATE, - worldState = , - worldStateAccessTrace = { [], [], ... [] }, // all trace vectors empty, - accruedSubstate = { [], ... [], }, // all substate vectors empty - results = INITIAL_CONTRACT_CALL_RESULTS, -} -``` - -> Since world state persists between transactions, the latest state is injected into a new AVM context. - -Given a [`PublicCallRequest`](../transactions/tx-object#public-call-request) and its parent [`TxRequest`](../transactions/local-execution#execution-request), these above-listed "`INITIAL_*`" entries are defined as follows: - -``` -INITIAL_EXECUTION_ENVIRONMENT = ExecutionEnvironment { - address = PublicCallRequest.contractAddress, - storageAddress = PublicCallRequest.CallContext.storageContractAddress, - origin = TxRequest.origin, - sender = PublicCallRequest.CallContext.msgSender, - portal = PublicCallRequest.CallContext.portalContractAddress, - feePerL1Gas = TxRequest.feePerL1Gas, - feePerL2Gas = TxRequest.feePerL2Gas, - feePerDaGas = TxRequest.feePerDaGas, - contractCallDepth = 0, - globals = - isStaticCall = PublicCallRequest.CallContext.isStaticCall, - isDelegateCall = PublicCallRequest.CallContext.isDelegateCall, - calldata = PublicCallRequest.args, - bytecode = worldState.contracts[PublicCallRequest.contractAddress], -} - -INITIAL_MACHINE_STATE = MachineState { - l1GasLeft = TxRequest.l1GasLimit, - l2GasLeft = TxRequest.l2GasLimit, - daGasLeft = TxRequest.daGasLimit, - pc = 0, - internalCallStack = [], // initialized as empty - memory = [0, ..., 0], // all 2^32 entries are initialized to zero -} - -INITIAL_CONTRACT_CALL_RESULTS = ContractCallResults { - reverted = false, - output = [], // initialized as empty -} -``` - -## Nested contract calls - -To review, a **nested contract call** occurs _during_ AVM execution and is triggered by a contract call instruction ([`CALL`](./instruction-set/#isa-section-call), [`STATICCALL`](./instruction-set/#isa-section-call), or `DELEGATECALL`). It initializes a new execution context (**nested context**) from the current one (the **calling context**) along with the call instruction's arguments. A nested contract call triggers AVM execution in that new context, and returns execution to the calling context upon completion. - -### Context initialization for nested calls - -A nested contract call initializes its execution context as follows: - -``` -nestedContext = AvmContext { - environment: nestedExecutionEnvironment, // defined below - machineState: nestedMachineState, // defined below - worldState: callingContext.worldState, - worldStateAccessTrace: callingContext.worldStateAccessTrace, - accruedSubstate = { [], ... [], }, // all substate vectors empty - results: INITIAL_CONTRACT_CALL_RESULTS, -} -``` - -While some context members are initialized as empty (as they are for an initial contract call), other entries are derived from the calling context or from the contract call instruction's arguments (`instr.args`). - -The world state is forwarded as-is to the nested context. Any updates made to the world state before this contract call instruction was encountered are carried forward into the nested context. - -The environment and machine state for the new context are initialized as shown below: - -``` -// some assignments reused below -isStaticCall = instr.opcode == STATICCALL_OP -isDelegateCall = instr.opcode == DELEGATECALL_OP -contract = callingContext.worldState.contracts[instr.args.addr] -calldataStart = instr.args.argsOffset -calldataEnd = calldataStart + instr.args.argsSize - -nestedExecutionEnvironment = ExecutionEnvironment { - origin: callingContext.origin, - sender: callingContext.address, - address: instr.args.addr, - storageAddress: isDelegateCall ? callingContext.environment.storageAddress : instr.args.addr, - portal: contract.portal, - feePerL1Gas: callingContext.feePerL1Gas, - feePerL2Gas: callingContext.feePerL2Gas, - feePerDaGas: callingContext.feePerDaGas, - contractCallDepth: callingContext.contractCallDepth + 1, - globals: callingContext.globals, - isStaticCall: isStaticCall, - isDelegateCall: isDelegateCall, - calldata: callingContext.memory[calldataStart:calldataEnd], - bytecode: contract.bytecode, -} - -nestedMachineState = MachineState { - l1GasLeft: callingContext.machineState.memory[instr.args.gasOffset], - l2GasLeft: callingContext.machineState.memory[instr.args.gasOffset+1], - daGasLeft: callingContext.machineState.memory[instr.args.gasOffset+2], - pc = 0, - internalCallStack = [], // initialized as empty - memory = [0, ..., 0], // all 2^32 entries are initialized to zero -} -``` -> The nested context's machine state's `*GasLeft` is initialized based on the call instruction's `gasOffset` argument. The caller allocates some amount of L1, L2, and DA gas to the nested call. It does so using the instruction's `gasOffset` argument. In particular, prior to the contract call instruction, the caller populates `M[gasOffset]` with the nested context's initial `l1GasLeft`. Likewise it populates `M[gasOffset+1]` with `l2GasLeft` and `M[gasOffset+2]` with `daGasLeft`. - -> Recall that initial values named as `INITIAL_*` are the same ones used during [context initialization for an initial contract call](#context-initialization-for-initial-contract-calls). - -> `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 - -A nested context's execution proceeds until it reaches a [halt](#halting). At that point, control returns to the caller, and the calling context is updated based on the nested context and the contract 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] = !nestedContext.results.reverted -``` - -Recall that a contract call is allocated some gas. In particular, the nested call instruction's `gasOffset` input points to an L1, L2, and DA gas allocation for the nested call. As shown in the [section above](#context-initialization-for-nested-calls), a nested call's `machineState.l1GasLeft` is initialized to `callingContext.machineState.memory[instr.args.gasOffset]`. Likewise, `l2GasLeft` is initialized from `gasOffset+1` and `daGasLeft` from `gasOffset+2`. - -As detailed in [the gas section above](#gas-cost-notes-and-examples), every instruction has an associated `instr.l1GasCost`, `instr.l2GasCost`, and `instr.daGasCost`. A nested call instruction's cost is the same as its initial `*GasLeft`. Prior to the nested context's execution, this cost is subtracted from the calling context's remaining gas. - -When a nested context halts, any of its allocated gas that remains unused is refunded to the caller. - -``` -context.l1GasLeft += nestedContext.machineState.l1GasLeft -context.l2GasLeft += nestedContext.machineState.l2GasLeft -context.daGasLeft += nestedContext.machineState.daGasLeft -``` - -If a nested context halts normally with a [`RETURN`](./instruction-set#isa-section-return) or [`REVERT`](./instruction-set#isa-section-revert), it may have some output data (`nestedContext.results.output`). The nested call instruction's `retOffset` and `retSize` arguments specify a region in the calling context's memory to place output data when the nested context halts. - -``` -if instr.args.retSize > 0: - context.memory[instr.args.retOffset:instr.args.retOffset+instr.args.retSize] = nestedContext.results.output -``` - -As long as a nested context has not reverted, its updates to the world state and accrued substate will be absorbed into the calling context. - -``` -if !nestedContext.results.reverted AND instr.opcode != STATICCALL_OP: - context.worldState = nestedContext.worldState - context.accruedSubstate.append(nestedContext.accruedSubstate) -``` - -Regardless of whether a nested context has reverted, its [world state access trace](./state#world-state-access-trace) updates are absorbed into the calling context along with a new `contractCalls` entry. -``` -context.worldStateAccessTrace = nestedContext.worldStateAccessTrace -context.worldStateAccessTrace.contractCalls.append({nestedContext.address, nestedContext.storageAddress, clk}) -``` - -> Reminder: a nested call cannot make updates to the world state or accrued substate if it is a [`STATICCALL`](./instruction-set/#isa-section-staticcall). diff --git a/yellow-paper/docs/public-vm/circuit-index.md b/yellow-paper/docs/public-vm/circuit-index.md new file mode 100644 index 00000000000..568f41f3723 --- /dev/null +++ b/yellow-paper/docs/public-vm/circuit-index.md @@ -0,0 +1,3 @@ +# AVM Circuit + +The AVM circuit's purpose is to prove execution of a sequence of instructions for a public execution request. Regardless of whether execution succeeds or reverts, the circuit always generates a valid proof of execution. diff --git a/yellow-paper/docs/public-vm/context.mdx b/yellow-paper/docs/public-vm/context.mdx new file mode 100644 index 00000000000..5b17027b07f --- /dev/null +++ b/yellow-paper/docs/public-vm/context.mdx @@ -0,0 +1,125 @@ +# Execution Context + +:::note REMINDER +Many terms and definitions here are borrowed from the [Ethereum Yellow Paper](https://ethereum.github.io/yellowpaper/paper.pdf). +::: + +An **execution context** contains the information and state relevant to a contract call's execution. When a contract call is made, an execution context is [initialized](#context-initialization) before the contract code's execution begins. + +#### _AvmContext_ +| Field | Type | +| --- | --- | +| environment | `ExecutionEnvironment` | +| [machineState](./state#machine-state) | `MachineState` | +| [worldState](./state#avm-world-state) | `AvmWorldState` | +| [worldStateAccessTrace](./state#world-state-access-trace) | `WorldStateAccessTrace` | +| [accruedSubstate](./state#accrued-substate) | `AccruedSubstate` | +| results | `ContractCallResults` | + +## Execution Environment + +A context's **execution environment** remains constant throughout a contract call's execution. When a contract call initializes its execution context, it [fully specifies the execution environment](#context-initialization). + +### _ExecutionEnvironment_ +| Field | Type | Description | +| --- | --- | --- | +| address | `AztecAddress` | | +| storageAddress | `AztecAddress` | | +| origin | `AztecAddress` | | +| sender | `AztecAddress` | | +| portal | `EthAddress` | | +| feePerL1Gas | `field` | | +| feePerL2Gas | `field` | | +| feePerDaGas | `field` | | +| contractCallDepth | `field` | Depth of the current call (how many nested calls deep is it). | +| contractCallPointer | `field` | Uniquely identifies each contract call processed by an AVM session. An initial call is assigned pointer value of 1 (expanded on in the AVM circuit section's ["Call Pointer"](./avm-circuit#call-pointer) subsection). | +| globals | `PublicGlobalVariables` | | +| isStaticCall | `boolean` | | +| isDelegateCall | `boolean` | | +| calldata | `[field; ]` | | + +## Contract Call Results + +When a contract call halts, it sets the context's **contract call results** to communicate results to the caller. + +### _ContractCallResults_ +| Field | Type | Description | +| --- | --- | --- | +| reverted | `boolean` | | +| output | `[field; ]` | | + +## Context initialization + +### Initial contract calls + +An **initial contract call** initializes a new execution context from a public execution request. + +``` +context = AvmContext { + environment = INITIAL_EXECUTION_ENVIRONMENT, + machineState = INITIAL_MACHINE_STATE, + worldState = , + worldStateAccessTrace = INITIAL_WORLD_STATE_ACCESS_TRACE, + accruedSubstate = { [], ... [], }, // all substate vectors empty + results = INITIAL_CONTRACT_CALL_RESULTS, +} +``` + +> Since world state persists between transactions, the latest state is injected into a new AVM context. + +Given a [`PublicCallRequest`](../transactions/tx-object#public-call-request) and its parent [`TxRequest`](../transactions/local-execution#execution-request), these above-listed "`INITIAL_*`" entries are defined as follows: + +``` +INITIAL_EXECUTION_ENVIRONMENT = ExecutionEnvironment { + address: PublicCallRequest.contractAddress, + storageAddress: PublicCallRequest.CallContext.storageContractAddress, + origin: TxRequest.origin, + sender: PublicCallRequest.CallContext.msgSender, + portal: PublicCallRequest.CallContext.portalContractAddress, + feePerL1Gas: TxRequest.feePerL1Gas, + feePerL2Gas: TxRequest.feePerL2Gas, + feePerDaGas: TxRequest.feePerDaGas, + contractCallDepth: 0, + contractCallPointer: 1, + globals: + isStaticCall: PublicCallRequest.CallContext.isStaticCall, + isDelegateCall: PublicCallRequest.CallContext.isDelegateCall, + calldata: PublicCallRequest.args, +} + +INITIAL_MACHINE_STATE = MachineState { + l1GasLeft: TxRequest.l1GasLimit, + l2GasLeft: TxRequest.l2GasLimit, + daGasLeft: TxRequest.daGasLimit, + pc: 0, + internalCallStack: [], // initialized as empty + memory: [0, ..., 0], // all 2^32 entries are initialized to zero +} + +INITIAL_WORLD_STATE_ACCESS_TRACE = WorldStateAccessTrace { + accessCounter: 1, + contractCalls: [ // initial contract call is traced + TracedContractCall { + callPointer: nestedContext.environment.callPointer, + address: nestedContext.address, + storageAddress: nestedContext.storageAddress, + counter: 0, + endLifetime: 0, // The call's end-lifetime will be updated later if it or its caller reverts + } + ], + [], ... [], // remaining entries are empty +}, + +INITIAL_CONTRACT_CALL_RESULTS = ContractCallResults { + reverted = false, + output = [], // initialized as empty +} +``` + +### Nested contract calls + +> See the dedicated ["Nested Contract Calls"](./nested-calls) page for a detailed explanation of nested contract calls. + +import NestedContext from "./_nested-context.md"; + + \ No newline at end of file diff --git a/yellow-paper/docs/public-vm/execution.md b/yellow-paper/docs/public-vm/execution.md new file mode 100644 index 00000000000..7173e12daf0 --- /dev/null +++ b/yellow-paper/docs/public-vm/execution.md @@ -0,0 +1,256 @@ +# Execution, Gas, Halting + + +Execution of an AVM program, within a provided [execution context](./context), includes the following steps: + +1. Fetch contract bytecode and decode into a vector of [AVM instructions](./instruction-set) +1. Repeat the next step until a [halt](#halting) is reached +1. Execute the instruction at the index specified by the context's [program counter](#program-counter-and-control-flow) + - Instruction execution will update the program counter + +This routine is represented with the syntax `execute(context)` in ["Nested execution"](./nested-calls#nested-execution) and other sections. + +## Bytecode fetch and decode + +Before execution begins, a contract's bytecode is retrieved. +```jsx +bytecode = context.worldState.contracts[context.environment.address].bytecode +``` + +> As described in ["Contract Deployment"](../contract-deployment), contracts are not stored in a dedicated tree. A [contract class](../contract-deployment/classes) is [represented](../contract-deployment/classes#registration) as an unencrypted log containing the `ContractClass` structure (which contains the bytecode) and a nullifier representing the class identifier. A [contract instance](../contract-deployment/instances) is [represented](../contract-deployment/classes#registration) as an unencrypted log containing the `ContractInstance` structure and a nullifier representing the contract address. + +> Thus, the syntax used above for bytecode retrieval is shorthand for: +>1. Perform a membership check of the contract instance address nullifier +>1. Retrieve the `ContractInstance` from a database that tracks all such unencrypted logs +> ```jsx +> contractInstance = contractInstances[context.environment.address] +> ``` +>1. Perform a membership check of the contract class identifier nullifier +>1. Retrieve the `ContractClass` and its bytecode from a database that tracks all such unencrypted logs +> ```jsx +> contractClass = contractClasses[contractInstance.contract_class_id] +> bytecode = contractClass.packed_public_bytecode +> ``` + +The bytecode is then decoded into a vector of `instructions`. An instruction is referenced throughout this document according to the following interface: + +| Member | Description | +| --- | --- | +| `opcode` | The 8-bit opcode value that identifies the operation an instruction is meant to perform. | +| `indirect` | Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. | +| `inTag` | The [tag/size](./memory-model.md#tags-and-tagged-memory) to check inputs against and/or tag the destination with. | +| `args` | Named arguments as specified for an instruction in the ["Instruction Set"](./instruction-set). As an example, `instr.args.aOffset` refers to an instructions argument named `aOffset`. | +| `execute` | Apply this instruction's transition function to an execution context (_e.g._ `instr.execute(context)`). | + + +## Instruction execution + +Once bytecode has been fetched and decoded into the `instructions` vector, instruction execution begins. + +The AVM executes the instruction at the index specified by the context's program counter. +```jsx +while (!halted) + instr = instructions[machineState.pc] + instr.execute(context) +``` + +An instruction's execution mutates the context's state as specified in the ["Instruction Set"](./instruction-set). + +## Program Counter and Control Flow + +A context is initialized with a program counter of zero, and so instruction execution always begins with a contract's the very first instruction. + +The program counter specifies which instruction the AVM will execute next, and each instruction's execution updates the program counter in some way. This allows the AVM 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`](./instruction-set#isa-section-jump), [`JUMPI`](./instruction-set#isa-section-jumpi), [`INTERNALCALL`](./instruction-set#isa-section-internalcall)) modify the program counter based on arguments. + +The `INTERNALCALL` instruction pushes `machineState.pc+1` to `machineState.internalCallStack` and then updates `pc` to the instruction's destination argument (`instr.args.loc`). The `INTERNALRETURN` instruction pops a destination from `machineState.internalCallStack` and assigns the result to `pc`. + +> An instruction will never assign program counter a value from memory (`machineState.memory`). A `JUMP`, `JUMPI`, or `INTERNALCALL` instruction's destination is a constant from the program bytecode. This property allows for easier static program analysis. + +## Gas checks and tracking +> See ["Gas and Fees"](../gas-and-fees) for a deeper dive into Aztec's gas model and for definitions of each type of gas. + +Each instruction has an associated `l1GasCost`, `l2GasCost`, and `daGasCost`. The AVM uses these values to enforce that sufficient gas is available before executing an instruction, and to deduct the cost from the context's remaining gas. The process of checking and charging gas is referred to in other sections using the following shorthand: + +```jsx +chargeGas(context, l1GasCost, l2GasCost, daGasCost) +``` + +### Checking gas + +Before an instruction is executed, the VM enforces that there is sufficient gas remaining via the following assertions: +``` +assert machineState.l1GasLeft - instr.l1GasCost > 0 +assert machineState.l2GasLeft - instr.l2GasCost > 0 +assert machineState.daGasLeft - instr.daGasCost > 0 +``` + +> Many instructions (like arithmetic operations) have 0 `l1GasCost` and `daGasCost`. Instructions only incur an L1 or DA cost if they modify the [world state](./state#avm-world-state) or [accrued substate](./state#accrued-substate). + +### Charging gas + +If these assertions pass, the machine state's gas left is decreased prior to the instruction's core execution: + +``` +machineState.l1GasLeft -= instr.l1GasCost +machineState.l2GasLeft -= instr.l2GasCost +machineState.daGasLeft -= instr.daGasCost +``` + +If either of these assertions _fail_ for an instruction, this triggers an exceptional halt. The gas left is set to 0 and execution reverts. + +``` +machineState.l1GasLeft = 0 +machineState.l2GasLeft = 0 +machineState.daGasLeft = 0 +``` + +> Reverting and exceptional halts are covered in more detail in the ["Halting" section](#halting). + +### Gas cost notes and examples + +An instruction's gas cost is meant to reflect the computational cost of generating a proof of its correct execution. For some instructions, this computational cost changes based on inputs. Here are some examples and important notes: +- [`JUMP`](./instruction-set/#isa-section-jump) is an example of an instruction with constant gas cost. Regardless of its inputs, the instruction always incurs the same `l1GasCost`, `l2GasCost`, and `daGasCost`. +- The [`SET`](./instruction-set/#isa-section-set) instruction operates on a different sized constant (based on its `dstTag`). 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`](./instruction-set/#isa-section-calldatacopy) argument which copies `copySize` words from `environment.calldata` to `machineState.memory`. +- The [`CALL`](./instruction-set#isa-section-call)/[`STATICCALL`](./instruction-set#isa-section-staticcall)/[`DELEGATECALL`](./instruction-set#isa-section-delegatecall) instruction's gas cost is determined by its `*Gas` arguments, but any gas unused by the nested contract call's execution is refunded after its completion ([more on this later](./nested-calls#updating-the-calling-context-after-nested-call-halts)). +- An instruction with "offset" arguments (like [`ADD`](./instruction-set/#isa-section-add) and many others), has increased cost for each offset argument that is flagged as "indirect". + +> 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. + +> An instruction's gas cost takes into account the costs of associated downstream computations. An instruction that triggers accesses to the public data tree (`SLOAD`/`SSTORE`) incurs a cost that accounts for state access validation in later circuits (public kernel or rollup). A contract call instruction (`CALL`/`STATICCALL`/`DELEGATECALL`) incurs a cost accounting for the nested call's complete execution as well as any work required by the public kernel circuit for this additional call. + +## Halting + +A context's execution can end with a **normal halt** or **exceptional halt**. A halt ends execution within the current context and returns control flow to the calling context. + +### Normal halting + +A normal halt occurs when the VM encounters an explicit halting instruction ([`RETURN`](./instruction-set#isa-section-return) or [`REVERT`](./instruction-set#isa-section-revert)). Such instructions consume gas normally and optionally initialize some output data before finally halting the current context's execution. + +``` +machineState.l1GasLeft -= instr.l1GasCost +machineState.l2GasLeft -= instr.l2GasCost +machineState.daGasLeft -= instr.daGasCost +results.reverted = instr.opcode == REVERT +results.output = machineState.memory[instr.args.retOffset:instr.args.retOffset+instr.args.retSize] +``` + +> Definitions: `retOffset` and `retSize` here are arguments to the [`RETURN`](./instruction-set/#isa-section-return) and [`REVERT`](./instruction-set#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. + +> `results.output` is only relevant when the caller is a contract call itself. In other words, it is only relevant for [nested contract calls](./nested-calls). When an [initial contract call](./context#initial-contract-calls) (initiated by a public execution request) halts normally, its `results.output` is ignored. + +### Exceptional halting + +An exceptional halt is not explicitly triggered by an instruction but instead occurs when an exceptional condition is met. + +When an exceptional halt occurs, the context is flagged as consuming all of its allocated gas and is marked as `reverted` with _no output data_, and then execution within the current context ends. + +``` +machineState.l1GasLeft = 0 +machineState.l2GasLeft = 0 +machineState.daGasLeft = 0 +results.reverted = true +// results.output remains empty +``` + +The AVM's exceptional halting conditions area listed below: + +1. **Insufficient gas** + ``` + assert machineState.l1GasLeft - instr.l1GasCost > 0 + assert machineState.l2GasLeft - instr.l2GasCost > 0 + assert machineState.daGasLeft - instr.l2GasCost > 0 + ``` +1. **Invalid instruction encountered** + ``` + assert instructions[machineState.pc].opcode <= MAX_AVM_OPCODE + ``` +1. **Jump destination past end of program** + ``` + assert instructions[machineState.pc].opcode not in {JUMP, JUMPI, INTERNALCALL} + OR instr.args.loc < instructions.length + ``` +1. **Failed memory tag check** + - Defined per-instruction in the [Instruction Set](./instruction-set) +1. **Out of bounds memory access (max memory offset is $2^{32}-1$)** + ``` + for offset in instr.args.*Offset: + assert offset < 2^32 + ``` +1. **World state modification attempt during a static call** + ``` + assert !environment.isStaticCall + OR instructions[machineState.pc].opcode not in WS_AS_MODIFYING_OPS + ``` + > Definition: `WS_AS_MODIFYING_OPS` represents the list of all opcodes corresponding to instructions that modify world state or accrued substate. +1. **Maximum contract call depth (1024) exceeded** + ``` + assert environment.contractCallDepth <= 1024 + assert instructions[machineState.pc].opcode not in {CALL, STATICCALL, DELEGATECALL} + OR environment.contractCallDepth < 1024 + ``` +1. **Maximum contract call calls per execution request (1024) exceeded** + ``` + assert worldStateAccessTrace.contractCalls.length <= 1024 + assert instructions[machineState.pc].opcode not in {CALL, STATICCALL, DELEGATECALL} + OR worldStateAccessTrace.contractCalls.length < 1024 + ``` +1. **Maximum internal call depth (1024) exceeded** + ``` + assert machineState.internalCallStack.length <= 1024 + assert instructions[machineState.pc].opcode != INTERNALCALL + OR environment.contractCallDepth < 1024 + ``` +1. **Maximum world state accesses (1024-per-category) exceeded** + ``` + assert worldStateAccessTrace.publicStorageReads.length <= 1024 + AND worldStateAccessTrace.publicStorageWrites.length <= 1024 + AND worldStateAccessTrace.noteHashChecks.length <= 1024 + AND worldStateAccessTrace.newNoteHashes.length <= 1024 + AND worldStateAccessTrace.nullifierChecks.length <= 1024 + AND worldStateAccessTrace.newNullifiers.length <= 1024 + AND worldStateAccessTrace.l1ToL2MessageReads.length <= 1024 + AND worldStateAccessTrace.archiveChecks.length <= 1024 + + // Storage + assert instructions[machineState.pc].opcode != SLOAD + OR worldStateAccessTrace.publicStorageReads.length < 1024 + assert instructions[machineState.pc].opcode != SSTORE + OR worldStateAccessTrace.publicStorageWrites.length < 1024 + + // Note hashes + assert instructions[machineState.pc].opcode != NOTEHASHEXISTS + OR noteHashChecks.length < 1024 + assert instructions[machineState.pc].opcode != EMITNOTEHASH + OR newNoteHashes.length < 1024 + + // Nullifiers + assert instructions[machineState.pc].opcode != NULLIFIEREXISTS + OR nullifierChecks.length < 1024 + assert instructions[machineState.pc].opcode != EMITNULLIFIER + OR newNullifiers.length < 1024 + + // Read L1 to L2 messages + assert instructions[machineState.pc].opcode != READL1TOL2MSG + OR worldStateAccessTrace.l1ToL2MessagesReads.length < 1024 + + // Archive tree & Headers + assert instructions[machineState.pc].opcode != HEADERMEMBER + OR archiveChecks.length < 1024 + ``` +1. **Maximum accrued substate entries (per-category) exceeded** + ``` + assert accruedSubstate.unencryptedLogs.length <= MAX_UNENCRYPTED_LOGS + AND accruedSubstate.sentL2ToL1Messages.length <= MAX_SENT_L2_TO_L1_MESSAGES + + // Unencrypted logs + assert instructions[machineState.pc].opcode != ULOG + OR unencryptedLogs.length < MAX_UNENCRYPTED_LOGS + + // Sent L2 to L1 messages + assert instructions[machineState.pc].opcode != SENDL2TOL1MSG + OR sentL2ToL1Messages.length < MAX_SENT_L2_TO_L1_MESSAGES + ``` + > Note that ideally the AVM should limit the _total_ accrued substate entries per-category instead of the entries per-call. diff --git a/yellow-paper/docs/public-vm/gen/_InstructionSet.mdx b/yellow-paper/docs/public-vm/gen/_instruction-set.mdx similarity index 88% rename from yellow-paper/docs/public-vm/gen/_InstructionSet.mdx rename to yellow-paper/docs/public-vm/gen/_instruction-set.mdx index cd0ae5e96b9..ba1c3fe8805 100644 --- a/yellow-paper/docs/public-vm/gen/_InstructionSet.mdx +++ b/yellow-paper/docs/public-vm/gen/_instruction-set.mdx @@ -312,14 +312,14 @@ context.machineState.pc = loc`} 0x2a [`SLOAD`](#isa-section-sload) Load a word from this contract's persistent public storage. Zero is loaded for unwritten slots. -{`M[dstOffset] = context.worldState.publicStorage[context.environment.storageAddress][M[slotOffset]]`} +{`M[dstOffset] = S[M[slotOffset]]`} 0x2b [`SSTORE`](#isa-section-sstore) Write a word to this contract's persistent public storage -{`context.worldState.publicStorage[context.environment.storageAddress][M[slotOffset]] = M[srcOffset]`} +{`S[M[slotOffset]] = M[srcOffset]`} @@ -334,7 +334,18 @@ M[existsOffset] = exists`} - 0x2d [`EMITNOTEHASH`](#isa-section-emitnotehash) + 0x2d [`NOTEHASHEXISTS`](#isa-section-notehashexists) + Check whether a note hash exists in the note hash tree (as of the start of the current block) + +{`exists = context.worldState.noteHashes.has({ + leafIndex: M[leafIndexOffset] + leaf: hash(context.environment.storageAddress, M[leafOffset]), +}) +M[existsOffset] = exists`} + + + + 0x2e [`EMITNOTEHASH`](#isa-section-emitnotehash) Emit a new note hash to be inserted into the note hash tree {`context.worldState.noteHashes.append( @@ -343,7 +354,7 @@ M[existsOffset] = exists`} - 0x2e [`NULLIFIEREXISTS`](#isa-section-nullifierexists) + 0x2f [`NULLIFIEREXISTS`](#isa-section-nullifierexists) Check whether a nullifier exists in the nullifier tree (including nullifiers from earlier in the current transaction or from earlier in the current block) {`exists = context.worldState.nullifiers.has( @@ -353,7 +364,7 @@ M[existsOffset] = exists`} - 0x2f [`EMITNULLIFIER`](#isa-section-emitnullifier) + 0x30 [`EMITNULLIFIER`](#isa-section-emitnullifier) Emit a new nullifier to be inserted into the nullifier tree {`context.worldState.nullifiers.append( @@ -362,8 +373,8 @@ M[existsOffset] = exists`} - 0x30 [`READL1TOL2MSG`](#isa-section-readl1tol2msg) - Check if a message exists in the L1-to-L2 message tree and reads it if so. + 0x31 [`READL1TOL2MSG`](#isa-section-readl1tol2msg) + Check if a message exists in the L1-to-L2 message tree and reads it if so {`exists = context.worldState.l1ToL2Messages.has({ leafIndex: M[msgLeafIndex], leaf: M[msgKeyOffset] @@ -376,14 +387,20 @@ if exists: - 0x31 [`HEADERMEMBER`](#isa-section-headermember) - Retrieve one member from a specified block's header. Revert if header does not yet exist. See ["Archive"](../state/archive) for more. + 0x32 [`HEADERMEMBER`](#isa-section-headermember) + Check if a header exists in the [archive tree](../state/archive) and retrieve the specified member if so -{`M[dstOffset] = context.worldState.headers.get(M[blockIndexOffset])[M[memberIndexOffset]]`} +{`exists = context.worldState.header.has({ + leafIndex: M[blockIndexOffset], leaf: M[msgKeyOffset] +}) +M[existsOffset] = exists +if exists: + header = context.worldState.headers.get(M[blockIndexOffset]) + M[dstOffset] = header[M[memberIndexOffset]] // member`} - 0x32 [`EMITUNENCRYPTEDLOG`](#isa-section-emitunencryptedlog) + 0x33 [`EMITUNENCRYPTEDLOG`](#isa-section-emitunencryptedlog) Emit an unencrypted log {`context.accruedSubstate.unencryptedLogs.append( @@ -395,7 +412,7 @@ if exists: - 0x33 [`SENDL2TOL1MSG`](#isa-section-sendl2tol1msg) + 0x34 [`SENDL2TOL1MSG`](#isa-section-sendl2tol1msg) Send an L2-to-L1 message {`context.accruedSubstate.sentL2ToL1Messages.append( @@ -408,29 +425,52 @@ if exists: - 0x34 [`CALL`](#isa-section-call) + 0x35 [`CALL`](#isa-section-call) Call into another contract -{`M[successOffset] = call( - M[gasOffset], M[gasOffset+1], M[gasOffset+2], - M[addrOffset], - M[argsOffset], M[argsSize], - M[retOffset], M[retSize])`} +{`// instr.args are { gasOffset, addrOffset, argsOffset, retOffset, retSize } +chargeGas(context, + l1GasCost=M[instr.args.gasOffset], + l2GasCost=M[instr.args.gasOffset+1], + daGasCost=M[instr.args.gasOffset+2]) +traceNestedCall(context, instr.args.addrOffset) +nestedContext = deriveContext(context, instr.args, isStaticCall=false, isDelegateCall=false) +execute(nestedContext) +updateContextAfterNestedCall(context, instr.args, nestedContext)`} - 0x35 [`STATICCALL`](#isa-section-staticcall) + 0x36 [`STATICCALL`](#isa-section-staticcall) Call into another contract, disallowing World State and Accrued Substate modifications -{`M[successOffset] = staticcall( - M[gasOffset], M[gasOffset+1], M[gasOffset+2], - M[addrOffset], - M[argsOffset], M[argsSize], - M[retOffset], M[retSize])`} +{`// instr.args are { gasOffset, addrOffset, argsOffset, retOffset, retSize } +chargeGas(context, + l1GasCost=M[instr.args.gasOffset], + l2GasCost=M[instr.args.gasOffset+1], + daGasCost=M[instr.args.gasOffset+2]) +traceNestedCall(context, instr.args.addrOffset) +nestedContext = deriveContext(context, instr.args, isStaticCall=true, isDelegateCall=false) +execute(nestedContext) +updateContextAfterNestedCall(context, instr.args, nestedContext)`} - 0x36 [`RETURN`](#isa-section-return) + 0x37 [`DELEGATECALL`](#isa-section-delegatecall) + Call into another contract, but keep the caller's `sender` and `storageAddress` + +{`// instr.args are { gasOffset, addrOffset, argsOffset, retOffset, retSize } +chargeGas(context, + l1GasCost=M[instr.args.gasOffset], + l2GasCost=M[instr.args.gasOffset+1], + daGasCost=M[instr.args.gasOffset+2]) +traceNestedCall(context, instr.args.addrOffset) +nestedContext = deriveContext(context, instr.args, isStaticCall=false, isDelegateCall=true) +execute(nestedContext) +updateContextAfterNestedCall(context, instr.args, nestedContext)`} + + + + 0x38 [`RETURN`](#isa-section-return) Halt execution within this context (without revert), optionally returning some data {`context.contractCallResults.output = M[retOffset:retOffset+retSize] @@ -438,7 +478,7 @@ halt`} - 0x37 [`REVERT`](#isa-section-revert) + 0x39 [`REVERT`](#isa-section-revert) Halt execution within this context as `reverted`, optionally returning some data {`context.contractCallResults.output = M[retOffset:retOffset+retSize] @@ -1241,11 +1281,11 @@ Load a word from this contract's persistent public storage. Zero is loaded for u - **dstOffset**: memory offset specifying where to store operation's result - **Expression**: -{`M[dstOffset] = context.worldState.publicStorage[context.environment.storageAddress][M[slotOffset]]`} +{`M[dstOffset] = S[M[slotOffset]]`} - **Details**: -{`// Expression is short-hand for +{`// Expression is shorthand for leafIndex = hash(context.environment.storageAddress, M[slotOffset]) exists = context.worldState.publicStorage.has(leafIndex) // exists == previously-written if exists: @@ -1262,7 +1302,7 @@ M[dstOffset] = value`} slot: M[slotOffset], exists: exists, // defined above value: value, // defined above - counter: clk, + counter: ++context.worldStateAccessTrace.accessCounter, } )`} @@ -1286,11 +1326,11 @@ Write a word to this contract's persistent public storage - **slotOffset**: memory offset containing the storage slot to store to - **Expression**: -{`context.worldState.publicStorage[context.environment.storageAddress][M[slotOffset]] = M[srcOffset]`} +{`S[M[slotOffset]] = M[srcOffset]`} - **Details**: -{`// Expression is short-hand for +{`// Expression is shorthand for context.worldState.publicStorage.set({ leafIndex: hash(context.environment.storageAddress, M[slotOffset]), leaf: M[srcOffset], @@ -1303,7 +1343,7 @@ context.worldState.publicStorage.set({ callPointer: context.environment.callPointer, slot: M[slotOffset], value: M[srcOffset], - counter: clk, + counter: ++context.worldStateAccessTrace.accessCounter, } )`} @@ -1341,12 +1381,49 @@ M[existsOffset] = exists`} leafIndex: M[leafIndexOffset] leaf: M[leafOffset], exists: exists, // defined above - counter: clk, + counter: ++context.worldStateAccessTrace.accessCounter, + } +)`} + +- **Triggers downstream circuit operations**: Storage slot siloing (hash with contract address), public data tree update +- **Tag updates**: T[existsOffset] = u8 +- **Bit-size**: 120 + + +### `NOTEHASHEXISTS` +Check whether a note hash exists in the note hash tree (as of the start of the current block) + +[See in table.](#isa-table-notehashexists) + +- **Opcode**: 0x2d +- **Category**: World State - Notes & Nullifiers +- **Flags**: + - **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. +- **Args**: + - **leafOffset**: memory offset of the leaf + - **leafIndexOffset**: memory offset of the leaf index + - **existsOffset**: memory offset specifying where to store operation's result (whether the archive leaf exists) +- **Expression**: + +{`exists = context.worldState.noteHashes.has({ + leafIndex: M[leafIndexOffset] + leaf: hash(context.environment.storageAddress, M[leafOffset]), +}) +M[existsOffset] = exists`} + +- **World State access tracing**: + +{`context.worldStateAccessTrace.noteHashChecks.append( + TracedLeafCheck { + callPointer: context.environment.callPointer, + leafIndex: M[leafIndexOffset] + leaf: M[leafOffset], + exists: exists, // defined above } )`} - **Triggers downstream circuit operations**: Note hash siloing (hash with storage contract address), note hash tree membership check -- **Tag updates**: `T[dstOffset] = u8` +- **Tag updates**: `T[existsOffset] = u8` - **Bit-size**: 120 @@ -1355,7 +1432,7 @@ Emit a new note hash to be inserted into the note hash tree [See in table.](#isa-table-emitnotehash) -- **Opcode**: 0x2d +- **Opcode**: 0x2e - **Category**: World State - Notes & Nullifiers - **Flags**: - **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. @@ -1373,7 +1450,7 @@ Emit a new note hash to be inserted into the note hash tree TracedNoteHash { callPointer: context.environment.callPointer, value: M[noteHashOffset], // unsiloed note hash - counter: clk, + counter: ++context.worldStateAccessTrace.accessCounter, } )`} @@ -1387,7 +1464,7 @@ Check whether a nullifier exists in the nullifier tree (including nullifiers fro [See in table.](#isa-table-nullifierexists) -- **Opcode**: 0x2e +- **Opcode**: 0x2f - **Category**: World State - Notes & Nullifiers - **Flags**: - **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. @@ -1408,12 +1485,12 @@ M[existsOffset] = exists`} callPointer: context.environment.callPointer, leaf: M[nullifierOffset], exists: exists, // defined above - counter: clk, + counter: ++context.worldStateAccessTrace.accessCounter, } )`} - **Triggers downstream circuit operations**: Nullifier siloing (hash with storage contract address), nullifier tree membership check -- **Tag updates**: `T[dstOffset] = u8` +- **Tag updates**: `T[existsOffset] = u8` - **Bit-size**: 88 @@ -1422,7 +1499,7 @@ Emit a new nullifier to be inserted into the nullifier tree [See in table.](#isa-table-emitnullifier) -- **Opcode**: 0x2f +- **Opcode**: 0x30 - **Category**: World State - Notes & Nullifiers - **Flags**: - **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. @@ -1440,7 +1517,7 @@ Emit a new nullifier to be inserted into the nullifier tree TracedNullifier { callPointer: context.environment.callPointer, value: M[nullifierOffset], // unsiloed nullifier - counter: clk, + counter: ++context.worldStateAccessTrace.accessCounter, } )`} @@ -1450,11 +1527,11 @@ Emit a new nullifier to be inserted into the nullifier tree [![](./images/bit-formats/EMITNULLIFIER.png)](./images/bit-formats/EMITNULLIFIER.png) ### `READL1TOL2MSG` -Check if a message exists in the L1-to-L2 message tree and reads it if so. +Check if a message exists in the L1-to-L2 message tree and reads it if so [See in table.](#isa-table-readl1tol2msg) -- **Opcode**: 0x30 +- **Opcode**: 0x31 - **Category**: World State - Messaging - **Flags**: - **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. @@ -1489,41 +1566,56 @@ if exists: - **Additional AVM circuit checks**: `msgKey == sha256_to_field(msg)` - **Triggers downstream circuit operations**: L1-to-L2 message tree membership check -- **Tag updates**: `T[dstOffset:dstOffset+msgSize] = field` +- **Tag updates**: + +{`T[existsOffset] = u8, +T[dstOffset:dstOffset+msgSize] = field`} + - **Bit-size**: 184 [![](./images/bit-formats/READL1TOL2MSG.png)](./images/bit-formats/READL1TOL2MSG.png) ### `HEADERMEMBER` -Retrieve one member from a specified block's header. Revert if header does not yet exist. See ["Archive"](../state/archive) for more. +Check if a header exists in the [archive tree](../state/archive) and retrieve the specified member if so [See in table.](#isa-table-headermember) -- **Opcode**: 0x31 +- **Opcode**: 0x32 - **Category**: World State - Archive Tree & Headers - **Flags**: - **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. - **Args**: - **blockIndexOffset**: memory offset of the block index (same as archive tree leaf index) of the header to access - **memberIndexOffset**: memory offset of the index of the member to retrieve from the header of the specified block + - **existsOffset**: memory offset specifying where to store operation's result (whether the leaf exists in the archive tree) - **dstOffset**: memory offset specifying where to store operation's result (the retrieved header member) - **Expression**: -{`M[dstOffset] = context.worldState.headers.get(M[blockIndexOffset])[M[memberIndexOffset]]`} +{`exists = context.worldState.header.has({ + leafIndex: M[blockIndexOffset], leaf: M[msgKeyOffset] +}) +M[existsOffset] = exists +if exists: + header = context.worldState.headers.get(M[blockIndexOffset]) + M[dstOffset] = header[M[memberIndexOffset]] // member`} - **World State access tracing**: {`context.worldStateAccessTrace.archiveChecks.append( TracedArchiveLeafCheck { leafIndex: M[blockIndexOffset], // leafIndex == blockIndex - leaf: hash(context.worldState.headers.get(M[blockIndexOffset])), + leaf: exists ? hash(header) : 0, // "exists" defined above } )`} - **Additional AVM circuit checks**: Hashes entire header to archive leaf for tracing. Aggregates header accesses and so that a header need only be hashed once. - **Triggers downstream circuit operations**: Archive tree membership check -- **Tag updates**: `T[dstOffset] = field` -- **Bit-size**: 120 +- **Tag updates**: + +{`T[existsOffset] = u8 +T[dstOffset] = field`} + +- **Bit-size**: 152 ### `EMITUNENCRYPTEDLOG` @@ -1531,7 +1623,7 @@ Emit an unencrypted log [See in table.](#isa-table-emitunencryptedlog) -- **Opcode**: 0x32 +- **Opcode**: 0x33 - **Category**: Accrued Substate - Logging - **Flags**: - **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. @@ -1556,7 +1648,7 @@ Send an L2-to-L1 message [See in table.](#isa-table-sendl2tol1msg) -- **Opcode**: 0x33 +- **Opcode**: 0x34 - **Category**: Accrued Substate - Messaging - **Flags**: - **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. @@ -1582,7 +1674,7 @@ Call into another contract [See in table.](#isa-table-call) -- **Opcode**: 0x34 +- **Opcode**: 0x35 - **Category**: Control Flow - Contract Calls - **Flags**: - **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. @@ -1596,15 +1688,25 @@ Call into another contract - **successOffset**: destination memory offset specifying where to store the call's success (0: failure, 1: success) - **Expression**: -{`M[successOffset] = call( - M[gasOffset], M[gasOffset+1], M[gasOffset+2], - M[addrOffset], - M[argsOffset], M[argsSize], - M[retOffset], M[retSize])`} +{`// instr.args are { gasOffset, addrOffset, argsOffset, retOffset, retSize } +chargeGas(context, + l1GasCost=M[instr.args.gasOffset], + l2GasCost=M[instr.args.gasOffset+1], + daGasCost=M[instr.args.gasOffset+2]) +traceNestedCall(context, instr.args.addrOffset) +nestedContext = deriveContext(context, instr.args, isStaticCall=false, isDelegateCall=false) +execute(nestedContext) +updateContextAfterNestedCall(context, instr.args, nestedContext)`} -- **Details**: Creates a new (nested) execution context and triggers execution within it until the nested context halts. - Then resumes execution in the current/calling context. A non-existent contract or one with no code will return success. - See ["Nested contract calls"](./avm#nested-contract-calls) to see how the caller updates its context after the nested call halts. +- **Details**: Creates a new (nested) execution context and triggers execution within that context. + Execution proceeds in the nested context until it reaches a halt at which point + execution resumes in the current/calling context. + A non-existent contract or one with no code will return success. + ["Nested contract calls"](./nested-calls) provides a full explanation of this + instruction along with the shorthand used in the expression above. + The explanation includes details on charging gas for nested calls, + nested context derivation, world state tracing, and updating the parent context + after the nested call halts. - **Tag checks**: `T[gasOffset] == T[gasOffset+1] == T[gasOffset+2] == u32` - **Tag updates**: @@ -1620,7 +1722,7 @@ Call into another contract, disallowing World State and Accrued Substate modific [See in table.](#isa-table-staticcall) -- **Opcode**: 0x35 +- **Opcode**: 0x36 - **Category**: Control Flow - Contract Calls - **Flags**: - **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. @@ -1634,13 +1736,22 @@ Call into another contract, disallowing World State and Accrued Substate modific - **successOffset**: destination memory offset specifying where to store the call's success (0: failure, 1: success) - **Expression**: -{`M[successOffset] = staticcall( - M[gasOffset], M[gasOffset+1], M[gasOffset+2], - M[addrOffset], - M[argsOffset], M[argsSize], - M[retOffset], M[retSize])`} +{`// instr.args are { gasOffset, addrOffset, argsOffset, retOffset, retSize } +chargeGas(context, + l1GasCost=M[instr.args.gasOffset], + l2GasCost=M[instr.args.gasOffset+1], + daGasCost=M[instr.args.gasOffset+2]) +traceNestedCall(context, instr.args.addrOffset) +nestedContext = deriveContext(context, instr.args, isStaticCall=true, isDelegateCall=false) +execute(nestedContext) +updateContextAfterNestedCall(context, instr.args, nestedContext)`} -- **Details**: Same as `CALL`, but disallows World State and Accrued Substate modifications. See ["Nested contract calls"](./avm#nested-contract-calls) to see how the caller updates its context after the nested call halts. +- **Details**: Same as `CALL`, but disallows World State and Accrued Substate modifications. + ["Nested contract calls"](./nested-calls) provides a full explanation of this + instruction along with the shorthand used in the expression above. + The explanation includes details on charging gas for nested calls, + nested context derivation, world state tracing, and updating the parent context + after the nested call halts. - **Tag checks**: `T[gasOffset] == T[gasOffset+1] == T[gasOffset+2] == u32` - **Tag updates**: @@ -1651,12 +1762,57 @@ T[retOffset:retOffset+retSize] = field`} [![](./images/bit-formats/STATICCALL.png)](./images/bit-formats/STATICCALL.png) +### `DELEGATECALL` +Call into another contract, but keep the caller's `sender` and `storageAddress` + +[See in table.](#isa-table-delegatecall) + +- **Opcode**: 0x37 +- **Category**: Control Flow - Contract Calls +- **Flags**: + - **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. +- **Args**: + - **gasOffset**: offset to three words containing `{l1GasLeft, l2GasLeft, daGasLeft}`: amount of gas to provide to the callee + - **addrOffset**: address of the contract to call + - **argsOffset**: memory offset to args (will become the callee's calldata) + - **argsSize**: number of words to pass via callee's calldata + - **retOffset**: destination memory offset specifying where to store the data returned from the callee + - **retSize**: number of words to copy from data returned by callee + - **successOffset**: destination memory offset specifying where to store the call's success (0: failure, 1: success) +- **Expression**: + +{`// instr.args are { gasOffset, addrOffset, argsOffset, retOffset, retSize } +chargeGas(context, + l1GasCost=M[instr.args.gasOffset], + l2GasCost=M[instr.args.gasOffset+1], + daGasCost=M[instr.args.gasOffset+2]) +traceNestedCall(context, instr.args.addrOffset) +nestedContext = deriveContext(context, instr.args, isStaticCall=false, isDelegateCall=true) +execute(nestedContext) +updateContextAfterNestedCall(context, instr.args, nestedContext)`} + +- **Details**: Same as `CALL`, but `sender` and `storageAddress` remains + the same in the nested call as they were in the caller. + ["Nested contract calls"](./nested-calls) provides a full explanation of this + instruction along with the shorthand used in the expression above. + The explanation includes details on charging gas for nested calls, + nested context derivation, world state tracing, and updating the parent context + after the nested call halts. +- **Tag checks**: `T[gasOffset] == T[gasOffset+1] == T[gasOffset+2] == u32` +- **Tag updates**: + +{`T[successOffset] = u8 +T[retOffset:retOffset+retSize] = field`} + +- **Bit-size**: 248 + + ### `RETURN` Halt execution within this context (without revert), optionally returning some data [See in table.](#isa-table-return) -- **Opcode**: 0x36 +- **Opcode**: 0x38 - **Category**: Control Flow - Contract Calls - **Flags**: - **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. @@ -1668,7 +1824,7 @@ Halt execution within this context (without revert), optionally returning some d {`context.contractCallResults.output = M[retOffset:retOffset+retSize] halt`} -- **Details**: Return control flow to the calling context/contract. Caller will accept World State and Accrued Substate modifications. See ["Halting"](./avm#halting) to learn more. See ["Nested contract calls"](./avm#nested-contract-calls) to see how the caller updates its context after the nested call halts. +- **Details**: Return control flow to the calling context/contract. Caller will accept World State and Accrued Substate modifications. See ["Halting"](./execution#halting) to learn more. See ["Nested contract calls"](./nested-calls) to see how the caller updates its context after the nested call halts. - **Bit-size**: 88 [![](./images/bit-formats/RETURN.png)](./images/bit-formats/RETURN.png) @@ -1678,7 +1834,7 @@ Halt execution within this context as `reverted`, optionally returning some data [See in table.](#isa-table-revert) -- **Opcode**: 0x37 +- **Opcode**: 0x39 - **Category**: Control Flow - Contract Calls - **Flags**: - **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`. @@ -1691,7 +1847,7 @@ Halt execution within this context as `reverted`, optionally returning some data context.contractCallResults.reverted = true halt`} -- **Details**: Return control flow to the calling context/contract. Caller will reject World State and Accrued Substate modifications. See ["Halting"](./avm#halting) to learn more. See ["Nested contract calls"](./avm#nested-contract-calls) to see how the caller updates its context after the nested call halts. +- **Details**: Return control flow to the calling context/contract. Caller will reject World State and Accrued Substate modifications. See ["Halting"](./execution#halting) to learn more. See ["Nested contract calls"](./nested-calls) to see how the caller updates its context after the nested call halts. - **Bit-size**: 88 [![](./images/bit-formats/REVERT.png)](./images/bit-formats/REVERT.png) diff --git a/yellow-paper/docs/public-vm/index.md b/yellow-paper/docs/public-vm/index.md new file mode 100644 index 00000000000..98fa1b94eda --- /dev/null +++ b/yellow-paper/docs/public-vm/index.md @@ -0,0 +1,3 @@ +# Aztec (Public) Virtual Machine + +The Aztec Virtual Machine (AVM) executes the public section of a transaction. \ No newline at end of file diff --git a/yellow-paper/docs/public-vm/instruction-set.mdx b/yellow-paper/docs/public-vm/instruction-set.mdx index 3b751dd3984..f02a8693786 100644 --- a/yellow-paper/docs/public-vm/instruction-set.mdx +++ b/yellow-paper/docs/public-vm/instruction-set.mdx @@ -4,12 +4,13 @@ This page lists all of the instructions supported by the Aztec Virtual Machine ( The following notes are relevant to the table and sections below: - `M[offset]` notation is shorthand for `context.machineState.memory[offset]` +- `S[slot]` notation is shorthand for an access to the specified slot in the current contract's public storage (`context.worldState.publicStorage`) after the slot has been siloed by the storage address (`hash(context.environment.storageAddress, slot)`) - Any instruction whose description does not mention a program counter change simply increments it: `context.machineState.pc++` -- All instructions update `context.machineState.*GasLeft` as detailed in ["Gas limits and tracking"](./avm#gas-limits-and-tracking) -- Any instruction can lead to an exceptional halt as specified in ["Exceptional halting"](./avm#exceptional-halting) +- All instructions update `context.machineState.*GasLeft` as detailed in ["Gas limits and tracking"](./execution#gas-checks-and-tracking) +- Any instruction can lead to an exceptional halt as specified in ["Exceptional halting"](./execution#exceptional-halting) - The term `hash` used in expressions below represents a Poseidon hash operation. - Type structures used in world state tracing operations are defined in ["Type Definitions"](./type-structs) -import GeneratedInstructionSet from "./gen/_InstructionSet.mdx"; +import GeneratedInstructionSet from "./gen/_instruction-set.mdx"; diff --git a/yellow-paper/docs/public-vm/intro.md b/yellow-paper/docs/public-vm/intro.md new file mode 100644 index 00000000000..cfb8115e1d3 --- /dev/null +++ b/yellow-paper/docs/public-vm/intro.md @@ -0,0 +1,41 @@ +# Introduction + +:::note reference +Many terms and definitions are borrowed from the [Ethereum Yellow Paper](https://ethereum.github.io/yellowpaper/paper.pdf). +::: + +An Aztec transaction may include one or more **public execution requests**. A public execution request is a request to execute a specified contract's public bytecode given some arguments. Execution of a contract's public bytecode is performed by the **Aztec Virtual Machine (AVM)**. + +> A public execution request may originate from a public call enqueued by a transaction's private segment ([`enqueuedPublicFunctionCalls`](../calls/enqueued-calls.md)), or from a public [fee preparation](../gas-and-fees#fee-preparation) or [fee distribution](../gas-and-fees#fee-distribution) call. + +In order to execute public contract bytecode, the AVM requires some context. An [**execution context**](./context) contains all information necessary to initiate AVM execution, including the relevant contract's bytecode and all state maintained by the AVM. A **contract call** initializes an execution context and triggers AVM execution within that context. + +Instruction-by-instruction, the AVM [executes](./execution) the bytecode specified in its context. An **instruction** is a decoded bytecode entry that, when executed, modifies the AVM's execution context (in particular its [state](./state)) according to the instruction's definition in the ["AVM Instruction Set"](./instruction-set). Execution within a context ends when the AVM encounters a [**halt**](./execution#halting). + +During execution, additional contract calls may be made. While an [**initial contract call**](./context#initial-contract-calls) initializes a new execution context directly from a public execution request, a [**nested contract call**](./nested-calls) occurs _during_ AVM execution and is triggered by a **contract call instruction** ([`CALL`](./instruction-set#isa-section-call), [`STATICCALL`](./instruction-set#isa-section-staticcall), or [`DELEGATECALL`](./instruction-set#isa-section-delegatecall)). It initializes a new execution context (**nested context**) from the current one (**calling context**) and triggers execution within it. When nested call's execution completes, execution proceeds in the calling context. + +A **caller** is a contract call's initiator. The caller of an initial contract call is an Aztec sequencer. The caller of a nested contract call is the AVM itself executing in the calling context. + +## Outline + +- [**State**](./state): the state maintained by the AVM +- [**Memory model**](./memory-model): the AVM's type-tagged memory model +- [**Execution context**](./context): the AVM's execution context and its initialization for initial contract calls +- [**Execution**](#execution): control flow, gas tracking, normal halting, and exceptional halting +- [**Nested contract calls**](./nested-calls): the initiation of a contract call from an instruction as well as the processing of nested execution results, gas refunds, and state reverts +- [**Instruction set**](./instruction-set): the list of all instructions supported by the AVM +- [**AVM Circuit**](./avm-circuit)**: the AVM as a SNARK circuit for proving execution + +> The sections prior to the "AVM Circuit" are meant to provide a high-level definition of the Aztec Virtual Machine as opposed to a specification of its SNARK implementation. They therefore mostly omit SNARK or circuit-centric verbiage except when particularly relevant to the high-level architecture. + +> Refer to the ["AVM Bytecode"](../bytecode#avm-bytecode) section of ["Bytecode"](../bytecode) for an explanation of the AVM's bytecode. + +## Public contract bytecode + + + +A contract's public bytecode is a series of execution instructions for the AVM. Refer to the ["AVM Instruction Set"](./instruction-set) for the details of all supported instructions along with how they modify AVM state. + +The entirety of a contract's public code is represented as a single block of bytecode with a maximum of `MAX_PUBLIC_INSTRUCTIONS_PER_CONTRACT` ($2^{15} = 32768$) instructions. The mechanism used to distinguish between different "functions" in an AVM bytecode program is left as a higher-level abstraction (_e.g._ similar to Solidity's concept of a function selector). + +> See the [Bytecode Validation Circuit](./bytecode-validation-circuit) to see how a contract's bytecode can be validated and committed to. diff --git a/yellow-paper/docs/public-vm/memory-model.md b/yellow-paper/docs/public-vm/memory-model.md index 28f65c03de2..62c6912a13e 100644 --- a/yellow-paper/docs/public-vm/memory-model.md +++ b/yellow-paper/docs/public-vm/memory-model.md @@ -1,4 +1,4 @@ -# Memory State Model +# Memory Model This section describes the AVM memory model, and in particular specifies "internal" VM abstractions that can be mapped to the VM's circuit architecture. @@ -15,7 +15,7 @@ All data regions are linear blocks of memory where each memory cell stores a fin Main memory stores the internal state of the current program being executed. Can be written to as well as read. -The main memory region stores _type tags_ alongside data values. [Type tags are explained further on in this document](#type tags). +The main memory region stores [_type tags_](#types-and-tagged-memory) alongside data values. #### Calldata diff --git a/yellow-paper/docs/public-vm/nested-calls.mdx b/yellow-paper/docs/public-vm/nested-calls.mdx new file mode 100644 index 00000000000..f4937081fd6 --- /dev/null +++ b/yellow-paper/docs/public-vm/nested-calls.mdx @@ -0,0 +1,142 @@ +# Nested Contract Calls + +A **nested contract call** occurs _during_ AVM execution and is triggered by a **contract call instruction**. The AVM [instruction set](./instruction-set) includes three contract call instructions: [`CALL`](./instruction-set#isa-section-call), [`STATICCALL`](./instruction-set#isa-section-staticcall), and [`DELEGATECALL`](./instruction-set#isa-section-delegatecall). + +A nested contract call performs the following operations: +1. [Charge gas](#gas-cost-of-call-instruction) for the nested call +1. [Trace the nested contract call](#tracing-nested-contract-calls) +1. [Derive the **nested context**](#context-initialization-for-nested-calls) from the calling context and the call instruction +1. Initiate [AVM execution](./execution) within the nested context until a halt is reached +1. [Update the **calling context**](#updating-the-calling-context-after-nested-call-halts) after the nested call halts + +Or, in pseudocode: +```jsx +// instr.args are { gasOffset, addrOffset, argsOffset, retOffset, retSize } + +isStaticCall = instr.opcode == STATICCALL +isDelegateCall = instr.opcode == DELEGATECALL + +chargeGas(context, + l1GasCost=M[instr.args.gasOffset], + l2GasCost=M[instr.args.gasOffset+1], + daGasCost=M[instr.args.gasOffset+2]) +traceNestedCall(context, instr.args.addrOffset) +nestedContext = deriveContext(context, instr.args, isStaticCall, isDelegateCall) +execute(nestedContext) +updateContextAfterNestedCall(context, instr.args, nestedContext) +``` + +These call instructions share the same argument definitions: `gasOffset`, `addrOffset`, `argsOffset`, `argsSize`, `retOffset`, `retSize`, and `successOffset` (defined in the [instruction set](./instruction-set)). These arguments will be referred to via those keywords below, and will often be used in conjunction with the `M[offset]` syntax which is shorthand for `context.machineState.memory[offset]`. + +## Tracing nested contract calls + +Before nested execution begins, the contract call is traced. +```jsx +traceNestedCall(context, addrOffset) +// which is shorthand for +context.worldStateAccessTrace.contractCalls.append( + TracedContractCall { + callPointer: context.worldStateAccessTrace.contractCalls.length + 1, + address: M[addrOffset], + storageAddress: M[addrOffset], + counter: ++context.worldStateAccessTrace.accessCounter, + endLifetime: 0, // The call's end-lifetime will be updated later if it or its caller reverts + } +) +``` + +## Context initialization for nested calls + +import NestedContext from "./_nested-context.md"; + + + + +## Gas cost of call instruction + +A call instruction's gas cost is derived from its `gasOffset` argument. In other words, the caller "allocates" gas for a nested call via its `gasOffset` argument. + +As with all instructions, gas is checked and cost is deducted _prior_ to the instruction's execution. +```jsx +chargeGas(context, + l1GasCost=M[gasOffset], + l2GasCost=M[gasOffset+1], + daGasCost=M[gasOffset+2]) +``` + +> The shorthand `chargeGas` is defined in ["Gas checks and tracking"](./execution#gas-checks-and-tracking). + +As with all instructions, gas is checked and cost is deducted _prior_ to the instruction's execution. +```jsx +assert context.machineState.l1GasLeft - l1GasCost > 0 +assert context.machineState.l2GasLeft - l2GasCost > 0 +assert context.machineState.daGasLeft - daGasCost > 0 +context.l1GasLeft -= l1GasCost +context.l2GasLeft -= l2GasCost +context.daGasLeft -= daGasCost +``` + +When the nested call halts, it may not have used up its entire gas allocation. Any unused gas is refunded to the caller as expanded on in ["Updating the calling context after nested call halts"](#updating-the-calling-context-after-nested-call-halts). + +## Nested execution + +Once the nested call's context is initialized, execution within that context begins. +```jsx +execute(nestedContext) +``` + +Execution (and the `execution` shorthand above) is detailed in ["Execution, Gas, Halting"](./execution). Note that execution mutates the nested context. + +## Updating the calling context after nested call halts + +After the nested call halts, the calling context is updated. The call's success is extracted, unused gas is refunded, output data can be copied to the caller's memory, world state and accrued substate are conditionally accepted, and the world state trace is updated. The following shorthand is used to refer to this process in the ["Instruction Set"](./instruction-set): + +```jsx +updateContextAfterNestedCall(context, instr.args, nestedContext) +``` + +The caller checks whether the nested call succeeded, and places the answer in memory. +```jsx +context.machineState.memory[instr.args.successOffset] = !nestedContext.results.reverted +``` + +Any unused gas is refunded to the caller. +```jsx +context.l1GasLeft += nestedContext.machineState.l1GasLeft +context.l2GasLeft += nestedContext.machineState.l2GasLeft +context.daGasLeft += nestedContext.machineState.daGasLeft +``` + +If the call instruction specifies non-zero `retSize`, the caller copies any returned output data to its memory. +```jsx +if retSize > 0: + context.machineState.memory[retOffset:retOffset+retSize] = nestedContext.results.output +``` + +If the nested call succeeded, the caller accepts its world state and accrued substate modifications. +```jsx +if !nestedContext.results.reverted: + context.worldState = nestedContext.worldState + context.accruedSubstate.append(nestedContext.accruedSubstate) +``` + +### Accepting nested call's World State access trace + +If the nested call reverted, the caller initializes the "end-lifetime" of all world state accesses made within the nested call. +```jsx +if nestedContext.results.reverted: + // process all traces (this is shorthand) + for trace in nestedContext.worldStateAccessTrace: + for access in trace: + if access.callPointer >= nestedContext.environment.callPointer: + // don't override end-lifetime already set by a deeper nested call + if access.endLifetime == 0: + access.endLifetime = nestedContext.worldStateAccessTrace.accessCounter +``` + +> A world state access that was made in a deeper nested _reverted_ context will already have its end-lifetime initialized. The caller does _not_ overwrite this access' end-lifetime here as it already has a narrower lifetime. + +Regardless of whether the nested call reverted, the caller accepts its updated world state access trace (with updated lifetimes). +```jsx +context.worldStateAccessTrace = nestedContext.worldStateAccessTrace +``` diff --git a/yellow-paper/docs/public-vm/state.md b/yellow-paper/docs/public-vm/state.md index 78976f63e76..c3dfada2cdd 100644 --- a/yellow-paper/docs/public-vm/state.md +++ b/yellow-paper/docs/public-vm/state.md @@ -15,7 +15,9 @@ This section describes the types of state maintained by the AVM. | `daGasLeft` | `field` | Tracks the amount of DA gas remaining at any point during execution. Initialized from contract call arguments. | | `pc` | `field` | Index into the contract's bytecode indicating which instruction to execute. Initialized to 0 during context initialization. | | `internalCallStack` | `Vector` | A stack of program counters pushed to and popped from by `INTERNALCALL` and `INTERNALRETURN` instructions. Initialized as empty during context initialization. | -| `memory` | `[field; 2^32]` | A $2^{32}$ entry memory space accessible by user code (bytecode instructions). All $2^{32}$ entries are assigned default value 0 during context initialization. See ["Memory Model"](./memory-model) for a complete description of AVM memory. | +| `memory` | `[field; 2^32]` | A $2^{32}$ entry memory space accessible by user code (AVM instructions). All $2^{32}$ entries are assigned default value 0 during context initialization. See ["Memory Model"](./memory-model) for a complete description of AVM memory. | + + ## World State @@ -23,15 +25,16 @@ This section describes the types of state maintained by the AVM. [Aztec's global state](../state) is implemented as a few merkle trees. These trees are exposed to the AVM as follows: -| State | Tree | Merkle Tree Type | AVM Access | -| --- | --- | --- | --- | -| Public Storage | Public Data Tree | Updatable | membership-checks (latest), reads, writes | +| State | Tree | Merkle Tree Type | AVM Access | +| --- | --- | --- | --- | +| Public Storage | Public Data Tree | Updatable | membership-checks (latest), reads, writes | | Note Hashes | Note Hash Tree | Append-only | membership-checks (start-of-block), appends | -| Nullifiers | Nullifier Tree | Indexed | membership-checks (latest), appends | +| Nullifiers | Nullifier Tree | Indexed | membership-checks (latest), appends | | L1-to-L2 Messages | L1-to-L2 Message Tree | Append-only | membership-checks (start-of-block), leaf-preimage-reads | | Headers | Archive Tree | Append-only | membership-checks, leaf-preimage-reads | +| Contracts\* | - | - | - | -> As described in ["Contract Deployment"](../contract-deployment), contracts are not stored in a dedicated tree. A [contract class](../contract-deployment/classes) is [represented](../contract-deployment/classes#registration) as an unencrypted log containing the `ContractClass` structure (which contains the bytecode) and a nullifier representing the class identifier. A [contract instance](../contract-deployment/instances) is [represented](../contract-deployment/classes#registration) as an unencrypted log containing the `ContractInstance` structure and a nullifier representing the contract address. +> \* As described in ["Contract Deployment"](../contract-deployment), contracts are not stored in a dedicated tree. A [contract class](../contract-deployment/classes) is [represented](../contract-deployment/classes#registration) as an unencrypted log containing the `ContractClass` structure (which contains the bytecode) and a nullifier representing the class identifier. A [contract instance](../contract-deployment/instances) is [represented](../contract-deployment/classes#registration) as an unencrypted log containing the `ContractInstance` structure and a nullifier representing the contract address. ### AVM World State @@ -45,12 +48,17 @@ The following table defines an AVM context's world state interface: | Field | AVM Instructions & Access | | --- | --- | +| `contracts` | [`*CALL`](./instruction-set#isa-section-call) (special case, see below\*) | | `publicStorage` | [`SLOAD`](./instruction-set#isa-section-sload) (membership-checks (latest) & reads), [`SSTORE`](./instruction-set#isa-section-sstore) (writes) | | `noteHashes` | [`NOTEHASHEXISTS`](./instruction-set#isa-section-notehashexists) (membership-checks (start-of-block)), [`EMITNOTEHASH`](./instruction-set#isa-section-emitnotehash) (appends) | | `nullifiers` | [`NULLIFIERSEXISTS`](./instruction-set#isa-section-nullifierexists) membership-checks (latest), [`EMITNULLIFIER`](./instruction-set#isa-section-emitnullifier) (appends) | | `l1ToL2Messages` | [`READL1TOL2MSG`](./instruction-set#isa-section-readl1tol2msg) (membership-checks (start-of-block) & leaf-preimage-reads) | | `headers` | [`HEADERMEMBER`](./instruction-set#isa-section-headermember) (membership-checks & leaf-preimage-reads) | +> \* `*CALL` is short for `CALL`/`STATICCALL`/`DELEGATECALL`. + +> \* For the purpose of the AVM, the world state's `contracts` member is readable for [bytecode fetching](./execution#bytecode-fetch-and-decode), and it is effectively updated when a new contract class or instance is created (along with a nullifier for the contract class identifier or contract address). + ### World State Access Trace **The circuit implementation of the AVM does _not_ prove that its world state accesses are valid and properly sequenced**, and does not perform actual tree updates. Thus, _all_ world state accesses, **regardless of whether they are rejected due to a revert**, must be traced and eventually handed off to downstream circuits (public kernel and rollup circuits) for comprehensive validation and tree updates. @@ -63,17 +71,18 @@ This trace of an AVM session's contract calls and world state accesses is named Each entry in the world state access trace is listed below along with its type and the instructions that append to it: -| Trace | Relevant State | Trace Vector Type | Instructions | -| --- | --- | --- | --- | -| `publicStorageReads` | Public Storage | `Vector` | [`SLOAD`](./instruction-set#isa-section-sload) | -| `publicStorageWrites` | Public Storage | `Vector` | [`SSTORE`](./instruction-set#isa-section-sstore) | -| `noteHashChecks` | Note Hashes | `Vector` | [`NOTEHASHEXISTS`](./instruction-set#isa-section-notehashexists) | -| `newNoteHashes` | Note Hashes | `Vector` | [`EMITNOTEHASH`](./instruction-set#isa-section-emitnotehash) | +| Field | Relevant State | Type | Instructions | +| --- | --- | --- | --- | +| `accessCounter` | all state | `field` | incremented by all instructions below | +| `contractCalls` | Contracts | `Vector` | [`*CALL`](./instruction-set#isa-section-call) | +| `publicStorageReads` | Public Storage | `Vector` | [`SLOAD`](./instruction-set#isa-section-sload) | +| `publicStorageWrites` | Public Storage | `Vector` | [`SSTORE`](./instruction-set#isa-section-sstore) | +| `noteHashChecks` | Note Hashes | `Vector` | [`NOTEHASHEXISTS`](./instruction-set#isa-section-notehashexists) | +| `newNoteHashes` | Note Hashes | `Vector` | [`EMITNOTEHASH`](./instruction-set#isa-section-emitnotehash) | | `nullifierChecks` | Nullifiers | `Vector` | [`NULLIFIERSEXISTS`](./instruction-set#isa-section-nullifierexists) | -| `newNullifiers` | Nullifiers | `Vector` | [`EMITNULLIFIER`](./instruction-set#isa-section-emitnullifier) | -| `l1ToL2MessageReads` | L1-To-L2 Messages | `Vector` | [`READL1TOL2MSG`](./instruction-set#isa-section-readl1tol2msg) | -| `archiveChecks` | Headers | `Vector` | [`HEADERMEMBER`](./instruction-set#isa-section-headermember) | -| `contractCalls` | - | `Vector` | [`*CALL`](./instruction-set#isa-section-call) | +| `newNullifiers` | Nullifiers | `Vector` | [`EMITNULLIFIER`](./instruction-set#isa-section-emitnullifier) | +| `l1ToL2MessageReads` | L1-To-L2 Messages | `Vector` | [`READL1TOL2MSG`](./instruction-set#isa-section-readl1tol2msg) | +| `archiveChecks` | Headers | `Vector` | [`HEADERMEMBER`](./instruction-set#isa-section-headermember) | > The types tracked in these trace vectors are defined [here](./type-structs). diff --git a/yellow-paper/docs/public-vm/type-structs.md b/yellow-paper/docs/public-vm/type-structs.md index 122821a487a..2b1793b19cc 100644 --- a/yellow-paper/docs/public-vm/type-structs.md +++ b/yellow-paper/docs/public-vm/type-structs.md @@ -6,9 +6,11 @@ This section lists type definitions relevant to AVM State and Circuit I/O. | Field | Type | Description | | --- | --- | --- | +| `callPointer` | `field` | The call pointer assigned to this call. | | `address` | `field` | The called contract address. | | `storageAddress` | `field` | The storage contract address (different from `address` for delegate calls). | -| `endLifetime` | `field` | End lifetime of a call. Final `clk` for reverted calls, `endLifetime` of parent for successful calls. Successful initial/top-level calls have infinite (max-value) `endLifetime`. | +| `counter` | `field` | When did this occur relative to other world state accesses. | +| `endLifetime` | `field` | End lifetime of a call. Final `accessCounter` for reverted calls, `endLifetime` of parent for successful calls. Successful initial/top-level calls have infinite (max-value) `endLifetime`. | #### _TracedL1ToL2MessageRead_ diff --git a/yellow-paper/sidebars.js b/yellow-paper/sidebars.js index b15773ba563..447505ec054 100644 --- a/yellow-paper/sidebars.js +++ b/yellow-paper/sidebars.js @@ -185,19 +185,29 @@ const sidebars = { ], }, { - label: "Public VM", + label: "Aztec (Public) VM", type: "category", - link: { type: "doc", id: "public-vm/avm" }, + link: { type: "doc", id: "public-vm/index" }, items: [ - "public-vm/avm", + "public-vm/intro", "public-vm/state", - "public-vm/type-structs", "public-vm/memory-model", + "public-vm/context", + "public-vm/execution", + "public-vm/nested-calls", "public-vm/instruction-set", - "public-vm/avm-circuit", - "public-vm/control-flow", - "public-vm/alu", - "public-vm/bytecode-validation-circuit", + { + label: "AVM Circuit", + type: "category", + link: { type: "doc", id: "public-vm/circuit-index" }, + items: [ + "public-vm/avm-circuit", + "public-vm/control-flow", + "public-vm/alu", + "public-vm/bytecode-validation-circuit", + ], + }, + "public-vm/type-structs", ], }, ], diff --git a/yellow-paper/src/preprocess/InstructionSet/InstructionSet.js b/yellow-paper/src/preprocess/InstructionSet/InstructionSet.js index 266ceaaf28d..d86329ca6c0 100644 --- a/yellow-paper/src/preprocess/InstructionSet/InstructionSet.js +++ b/yellow-paper/src/preprocess/InstructionSet/InstructionSet.js @@ -12,6 +12,22 @@ const IN_TAG_DESCRIPTION_NO_FIELD = IN_TAG_DESCRIPTION + " `field` type is NOT s const DST_TAG_DESCRIPTION = "The [tag/size](./memory-model#tags-and-tagged-memory) to tag the destination with but not to check inputs against."; const INDIRECT_FLAG_DESCRIPTION = "Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`."; +const CALL_INSTRUCTION_ARGS = [ + {"name": "gasOffset", "description": "offset to three words containing `{l1GasLeft, l2GasLeft, daGasLeft}`: amount of gas to provide to the callee"}, + {"name": "addrOffset", "description": "address of the contract to call"}, + {"name": "argsOffset", "description": "memory offset to args (will become the callee's calldata)"}, + {"name": "argsSize", "description": "number of words to pass via callee's calldata", "mode": "immediate", "type": "u32"}, + {"name": "retOffset", "description": "destination memory offset specifying where to store the data returned from the callee"}, + {"name": "retSize", "description": "number of words to copy from data returned by callee", "mode": "immediate", "type": "u32"}, + {"name": "successOffset", "description": "destination memory offset specifying where to store the call's success (0: failure, 1: success)", "type": "u8"}, +]; +const CALL_INSTRUCTION_DETAILS = ` + ["Nested contract calls"](./nested-calls) provides a full explanation of this + instruction along with the shorthand used in the expression above. + The explanation includes details on charging gas for nested calls, + nested context derivation, world state tracing, and updating the parent context + after the nested call halts.`; + const INSTRUCTION_SET_RAW = [ { "id": "add", @@ -741,11 +757,11 @@ context.machineState.pc = loc {"name": "dstOffset", "description": "memory offset specifying where to store operation's result"}, ], "Expression": ` -M[dstOffset] = context.worldState.publicStorage[context.environment.storageAddress][M[slotOffset]] +M[dstOffset] = S[M[slotOffset]] `, "Summary": "Load a word from this contract's persistent public storage. Zero is loaded for unwritten slots.", "Details": ` -// Expression is short-hand for +// Expression is shorthand for leafIndex = hash(context.environment.storageAddress, M[slotOffset]) exists = context.worldState.publicStorage.has(leafIndex) // exists == previously-written if exists: @@ -761,7 +777,7 @@ context.worldStateAccessTrace.publicStorageReads.append( slot: M[slotOffset], exists: exists, // defined above value: value, // defined above - counter: clk, + counter: ++context.worldStateAccessTrace.accessCounter, } ) `, @@ -781,11 +797,11 @@ context.worldStateAccessTrace.publicStorageReads.append( {"name": "slotOffset", "description": "memory offset containing the storage slot to store to"}, ], "Expression": ` -context.worldState.publicStorage[context.environment.storageAddress][M[slotOffset]] = M[srcOffset] +S[M[slotOffset]] = M[srcOffset] `, "Summary": "Write a word to this contract's persistent public storage", "Details": ` -// Expression is short-hand for +// Expression is shorthand for context.worldState.publicStorage.set({ leafIndex: hash(context.environment.storageAddress, M[slotOffset]), leaf: M[srcOffset], @@ -797,7 +813,7 @@ context.worldStateAccessTrace.publicStorageWrites.append( callPointer: context.environment.callPointer, slot: M[slotOffset], value: M[srcOffset], - counter: clk, + counter: ++context.worldStateAccessTrace.accessCounter, } ) `, @@ -832,13 +848,47 @@ context.worldStateAccessTrace.noteHashChecks.append( leafIndex: M[leafIndexOffset] leaf: M[leafOffset], exists: exists, // defined above - counter: clk, + counter: ++context.worldStateAccessTrace.accessCounter, + } +) +`, + "Triggers downstream circuit operations": "Storage slot siloing (hash with contract address), public data tree update", + "Tag checks": "", + "Tag updates": "T[existsOffset] = u8", + }, + { + "id": "notehashexists", + "Name": "`NOTEHASHEXISTS`", + "Category": "World State - Notes & Nullifiers", + "Flags": [ + {"name": "indirect", "description": INDIRECT_FLAG_DESCRIPTION}, + ], + "Args": [ + {"name": "leafOffset", "description": "memory offset of the leaf"}, + {"name": "leafIndexOffset", "description": "memory offset of the leaf index"}, + {"name": "existsOffset", "description": "memory offset specifying where to store operation's result (whether the archive leaf exists)"}, + ], + "Expression": ` +exists = context.worldState.noteHashes.has({ + leafIndex: M[leafIndexOffset] + leaf: hash(context.environment.storageAddress, M[leafOffset]), +}) +M[existsOffset] = exists +`, + "Summary": "Check whether a note hash exists in the note hash tree (as of the start of the current block)", + "World State access tracing": ` +context.worldStateAccessTrace.noteHashChecks.append( + TracedLeafCheck { + callPointer: context.environment.callPointer, + leafIndex: M[leafIndexOffset] + leaf: M[leafOffset], + exists: exists, // defined above } ) `, "Triggers downstream circuit operations": "Note hash siloing (hash with storage contract address), note hash tree membership check", "Tag checks": "", - "Tag updates": "`T[dstOffset] = u8`", + "Tag updates": "`T[existsOffset] = u8`", }, { "id": "emitnotehash", @@ -861,7 +911,7 @@ context.worldStateAccessTrace.newNoteHashes.append( TracedNoteHash { callPointer: context.environment.callPointer, value: M[noteHashOffset], // unsiloed note hash - counter: clk, + counter: ++context.worldStateAccessTrace.accessCounter, } ) `, @@ -893,13 +943,13 @@ context.worldStateAccessTrace.nullifierChecks.append( callPointer: context.environment.callPointer, leaf: M[nullifierOffset], exists: exists, // defined above - counter: clk, + counter: ++context.worldStateAccessTrace.accessCounter, } ) `, "Triggers downstream circuit operations": "Nullifier siloing (hash with storage contract address), nullifier tree membership check", "Tag checks": "", - "Tag updates": "`T[dstOffset] = u8`", + "Tag updates": "`T[existsOffset] = u8`", }, { "id": "emitnullifier", @@ -922,7 +972,7 @@ context.worldStateAccessTrace.newNullifiers.append( TracedNullifier { callPointer: context.environment.callPointer, value: M[nullifierOffset], // unsiloed nullifier - counter: clk, + counter: ++context.worldStateAccessTrace.accessCounter, } ) `, @@ -954,7 +1004,7 @@ if exists: leafIndex: M[msgLeafIndex], leaf: M[msgKeyOffset] }) `, - "Summary": "Check if a message exists in the L1-to-L2 message tree and reads it if so.", + "Summary": "Check if a message exists in the L1-to-L2 message tree and reads it if so", "World State access tracing": ` context.worldStateAccessTrace.l1ToL2MessagesReads.append( ReadL1ToL2Message { @@ -969,7 +1019,10 @@ context.worldStateAccessTrace.l1ToL2MessagesReads.append( "Additional AVM circuit checks": "`msgKey == sha256_to_field(msg)`", "Triggers downstream circuit operations": "L1-to-L2 message tree membership check", "Tag checks": "", - "Tag updates": "`T[dstOffset:dstOffset+msgSize] = field`", + "Tag updates": ` +T[existsOffset] = u8, +T[dstOffset:dstOffset+msgSize] = field +`, }, { "id": "headermember", @@ -981,24 +1034,34 @@ context.worldStateAccessTrace.l1ToL2MessagesReads.append( "Args": [ {"name": "blockIndexOffset", "description": "memory offset of the block index (same as archive tree leaf index) of the header to access"}, {"name": "memberIndexOffset", "description": "memory offset of the index of the member to retrieve from the header of the specified block"}, + {"name": "existsOffset", "description": "memory offset specifying where to store operation's result (whether the leaf exists in the archive tree)"}, {"name": "dstOffset", "description": "memory offset specifying where to store operation's result (the retrieved header member)"}, ], "Expression": ` -M[dstOffset] = context.worldState.headers.get(M[blockIndexOffset])[M[memberIndexOffset]] +exists = context.worldState.header.has({ + leafIndex: M[blockIndexOffset], leaf: M[msgKeyOffset] +}) +M[existsOffset] = exists +if exists: + header = context.worldState.headers.get(M[blockIndexOffset]) + M[dstOffset] = header[M[memberIndexOffset]] // member `, - "Summary": "Retrieve one member from a specified block's header. Revert if header does not yet exist. See [\"Archive\"](../state/archive) for more.", + "Summary": "Check if a header exists in the [archive tree](../state/archive) and retrieve the specified member if so", "World State access tracing": ` context.worldStateAccessTrace.archiveChecks.append( TracedArchiveLeafCheck { leafIndex: M[blockIndexOffset], // leafIndex == blockIndex - leaf: hash(context.worldState.headers.get(M[blockIndexOffset])), + leaf: exists ? hash(header) : 0, // "exists" defined above } ) `, "Additional AVM circuit checks": "Hashes entire header to archive leaf for tracing. Aggregates header accesses and so that a header need only be hashed once.", "Triggers downstream circuit operations": "Archive tree membership check", "Tag checks": "", - "Tag updates": "`T[dstOffset] = field`", + "Tag updates": ` +T[existsOffset] = u8 +T[dstOffset] = field +`, }, { "id": "emitunencryptedlog", @@ -1054,26 +1117,24 @@ context.accruedSubstate.sentL2ToL1Messages.append( "Flags": [ {"name": "indirect", "description": INDIRECT_FLAG_DESCRIPTION}, ], - "Args": [ - {"name": "gasOffset", "description": "offset to three words containing `{l1GasLeft, l2GasLeft, daGasLeft}`: amount of gas to provide to the callee"}, - {"name": "addrOffset", "description": "address of the contract to call"}, - {"name": "argsOffset", "description": "memory offset to args (will become the callee's calldata)"}, - {"name": "argsSize", "description": "number of words to pass via callee's calldata", "mode": "immediate", "type": "u32"}, - {"name": "retOffset", "description": "destination memory offset specifying where to store the data returned from the callee"}, - {"name": "retSize", "description": "number of words to copy from data returned by callee", "mode": "immediate", "type": "u32"}, - {"name": "successOffset", "description": "destination memory offset specifying where to store the call's success (0: failure, 1: success)", "type": "u8"}, - ], + "Args": CALL_INSTRUCTION_ARGS, "Expression":` -M[successOffset] = call( - M[gasOffset], M[gasOffset+1], M[gasOffset+2], - M[addrOffset], - M[argsOffset], M[argsSize], - M[retOffset], M[retSize]) +// instr.args are { gasOffset, addrOffset, argsOffset, retOffset, retSize } +chargeGas(context, + l1GasCost=M[instr.args.gasOffset], + l2GasCost=M[instr.args.gasOffset+1], + daGasCost=M[instr.args.gasOffset+2]) +traceNestedCall(context, instr.args.addrOffset) +nestedContext = deriveContext(context, instr.args, isStaticCall=false, isDelegateCall=false) +execute(nestedContext) +updateContextAfterNestedCall(context, instr.args, nestedContext) `, "Summary": "Call into another contract", - "Details": `Creates a new (nested) execution context and triggers execution within it until the nested context halts. - Then resumes execution in the current/calling context. A non-existent contract or one with no code will return success. - See [\"Nested contract calls\"](./avm#nested-contract-calls) to see how the caller updates its context after the nested call halts.`, + "Details": `Creates a new (nested) execution context and triggers execution within that context. + Execution proceeds in the nested context until it reaches a halt at which point + execution resumes in the current/calling context. + A non-existent contract or one with no code will return success. ` + + CALL_INSTRUCTION_DETAILS, "Tag checks": "`T[gasOffset] == T[gasOffset+1] == T[gasOffset+2] == u32`", "Tag updates": ` T[successOffset] = u8 @@ -1087,24 +1148,50 @@ T[retOffset:retOffset+retSize] = field "Flags": [ {"name": "indirect", "description": INDIRECT_FLAG_DESCRIPTION}, ], - "Args": [ - {"name": "gasOffset", "description": "offset to three words containing `{l1GasLeft, l2GasLeft, daGasLeft}`: amount of gas to provide to the callee"}, - {"name": "addrOffset", "description": "address of the contract to call"}, - {"name": "argsOffset", "description": "memory offset to args (will become the callee's calldata)"}, - {"name": "argsSize", "description": "number of words to pass via callee's calldata", "mode": "immediate", "type": "u32"}, - {"name": "retOffset", "description": "destination memory offset specifying where to store the data returned from the callee"}, - {"name": "retSize", "description": "number of words to copy from data returned by callee", "mode": "immediate", "type": "u32"}, - {"name": "successOffset", "description": "destination memory offset specifying where to store the call's success (0: failure, 1: success)", "type": "u8"}, - ], + "Args": CALL_INSTRUCTION_ARGS, "Expression": ` -M[successOffset] = staticcall( - M[gasOffset], M[gasOffset+1], M[gasOffset+2], - M[addrOffset], - M[argsOffset], M[argsSize], - M[retOffset], M[retSize]) +// instr.args are { gasOffset, addrOffset, argsOffset, retOffset, retSize } +chargeGas(context, + l1GasCost=M[instr.args.gasOffset], + l2GasCost=M[instr.args.gasOffset+1], + daGasCost=M[instr.args.gasOffset+2]) +traceNestedCall(context, instr.args.addrOffset) +nestedContext = deriveContext(context, instr.args, isStaticCall=true, isDelegateCall=false) +execute(nestedContext) +updateContextAfterNestedCall(context, instr.args, nestedContext) `, "Summary": "Call into another contract, disallowing World State and Accrued Substate modifications", - "Details": "Same as `CALL`, but disallows World State and Accrued Substate modifications. See [\"Nested contract calls\"](./avm#nested-contract-calls) to see how the caller updates its context after the nested call halts.", + "Details": `Same as \`CALL\`, but disallows World State and Accrued Substate modifications. ` + + CALL_INSTRUCTION_DETAILS, + "Tag checks": "`T[gasOffset] == T[gasOffset+1] == T[gasOffset+2] == u32`", + "Tag updates": ` +T[successOffset] = u8 +T[retOffset:retOffset+retSize] = field +`, + }, + { + "id": "delegatecall", + "Name": "`DELEGATECALL`", + "Category": "Control Flow - Contract Calls", + "Flags": [ + {"name": "indirect", "description": INDIRECT_FLAG_DESCRIPTION}, + ], + "Args": CALL_INSTRUCTION_ARGS, + "Expression": ` +// instr.args are { gasOffset, addrOffset, argsOffset, retOffset, retSize } +chargeGas(context, + l1GasCost=M[instr.args.gasOffset], + l2GasCost=M[instr.args.gasOffset+1], + daGasCost=M[instr.args.gasOffset+2]) +traceNestedCall(context, instr.args.addrOffset) +nestedContext = deriveContext(context, instr.args, isStaticCall=false, isDelegateCall=true) +execute(nestedContext) +updateContextAfterNestedCall(context, instr.args, nestedContext) +`, + "Summary": "Call into another contract, but keep the caller's `sender` and `storageAddress`", + "Details": `Same as \`CALL\`, but \`sender\` and \`storageAddress\` remains + the same in the nested call as they were in the caller. ` + + CALL_INSTRUCTION_DETAILS, "Tag checks": "`T[gasOffset] == T[gasOffset+1] == T[gasOffset+2] == u32`", "Tag updates": ` T[successOffset] = u8 @@ -1127,7 +1214,7 @@ context.contractCallResults.output = M[retOffset:retOffset+retSize] halt `, "Summary": "Halt execution within this context (without revert), optionally returning some data", - "Details": "Return control flow to the calling context/contract. Caller will accept World State and Accrued Substate modifications. See [\"Halting\"](./avm#halting) to learn more. See [\"Nested contract calls\"](./avm#nested-contract-calls) to see how the caller updates its context after the nested call halts.", + "Details": "Return control flow to the calling context/contract. Caller will accept World State and Accrued Substate modifications. See [\"Halting\"](./execution#halting) to learn more. See [\"Nested contract calls\"](./nested-calls) to see how the caller updates its context after the nested call halts.", "Tag checks": "", "Tag updates": "", }, @@ -1148,7 +1235,7 @@ context.contractCallResults.reverted = true halt `, "Summary": "Halt execution within this context as `reverted`, optionally returning some data", - "Details": "Return control flow to the calling context/contract. Caller will reject World State and Accrued Substate modifications. See [\"Halting\"](./avm#halting) to learn more. See [\"Nested contract calls\"](./avm#nested-contract-calls) to see how the caller updates its context after the nested call halts.", + "Details": "Return control flow to the calling context/contract. Caller will reject World State and Accrued Substate modifications. See [\"Halting\"](./execution#halting) to learn more. See [\"Nested contract calls\"](./nested-calls) to see how the caller updates its context after the nested call halts.", "Tag checks": "", "Tag updates": "", }, diff --git a/yellow-paper/src/preprocess/InstructionSet/genMarkdown.js b/yellow-paper/src/preprocess/InstructionSet/genMarkdown.js index fbb9792b111..4070fdc76a5 100644 --- a/yellow-paper/src/preprocess/InstructionSet/genMarkdown.js +++ b/yellow-paper/src/preprocess/InstructionSet/genMarkdown.js @@ -118,7 +118,7 @@ async function generateInstructionSet() { const rootDir = path.join(__dirname, "../../../"); const docsDir = path.join(rootDir, "docs", "docs"); - const relPath = path.relative(docsDir, "docs/public-vm/gen/_InstructionSet.mdx"); + const relPath = path.relative(docsDir, "docs/public-vm/gen/_instruction-set.mdx"); const docsFilePath = path.resolve(docsDir, relPath); const docsDirName = path.dirname(docsFilePath); if (!fs.existsSync(docsDirName)) {