From 056ad5500424605774308508089688397067a33b Mon Sep 17 00:00:00 2001 From: fcarreiro Date: Thu, 16 May 2024 18:44:55 +0000 Subject: [PATCH] feat(avm-simulator): cap gas for external calls --- .../vm/avm_trace/aztec_constants.hpp | 1 - .../protocol-specs/public-vm/nested-calls.mdx | 55 ++++++++++++------- .../src/core/libraries/ConstantsGen.sol | 1 - .../aztec-nr/aztec/src/context/avm_context.nr | 12 ++-- .../crates/types/src/constants.nr | 2 - yarn-project/circuits.js/src/constants.gen.ts | 1 - .../simulator/src/avm/fixtures/index.ts | 4 +- .../src/avm/opcodes/external_calls.ts | 13 +++-- 8 files changed, 51 insertions(+), 38 deletions(-) diff --git a/barretenberg/cpp/src/barretenberg/vm/avm_trace/aztec_constants.hpp b/barretenberg/cpp/src/barretenberg/vm/avm_trace/aztec_constants.hpp index 0a1ad1a01c2..b7228aa873b 100644 --- a/barretenberg/cpp/src/barretenberg/vm/avm_trace/aztec_constants.hpp +++ b/barretenberg/cpp/src/barretenberg/vm/avm_trace/aztec_constants.hpp @@ -63,7 +63,6 @@ const size_t ARGS_HASH_CHUNK_COUNT = 64; const size_t MAX_ARGS_LENGTH = ARGS_HASH_CHUNK_COUNT * ARGS_HASH_CHUNK_LENGTH; const size_t INITIAL_L2_BLOCK_NUM = 1; const size_t BLOB_SIZE_IN_BYTES = 31 * 4096; -const size_t NESTED_CALL_L2_GAS_BUFFER = 20000; const size_t MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS = 20000; const size_t MAX_PACKED_BYTECODE_SIZE_PER_PRIVATE_FUNCTION_IN_FIELDS = 3000; const size_t MAX_PACKED_BYTECODE_SIZE_PER_UNCONSTRAINED_FUNCTION_IN_FIELDS = 3000; diff --git a/docs/docs/protocol-specs/public-vm/nested-calls.mdx b/docs/docs/protocol-specs/public-vm/nested-calls.mdx index 5da498f755e..182a248fbed 100644 --- a/docs/docs/protocol-specs/public-vm/nested-calls.mdx +++ b/docs/docs/protocol-specs/public-vm/nested-calls.mdx @@ -3,6 +3,7 @@ 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 @@ -10,19 +11,25 @@ A nested contract call performs the following operations: 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, - l2GasCost=M[instr.args.gasOffset], - daGasCost=M[instr.args.gasOffset+1]) -traceNestedCall(context, instr.args.addrOffset) -nestedContext = deriveContext(context, instr.args, isStaticCall, isDelegateCall) -execute(nestedContext) -updateContextAfterNestedCall(context, instr.args, nestedContext) +isStaticCall = instr.opcode == STATICCALL; +isDelegateCall = instr.opcode == DELEGATECALL; +l2GasCost = min(M[instr.args.gasOffset], context.machineState.l2GasLeft); +daGasCost = min(M[instr.args.gasOffset + 1], context.machineState.daGasLeft); + +chargeGas(context, l2GasCost, daGasCost); +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]`. @@ -30,6 +37,7 @@ These call instructions share the same argument definitions: `gasOffset`, `addrO ## Tracing nested contract calls Before nested execution begins, the contract call is traced. + ```jsx traceNestedCall(context, addrOffset) // which is shorthand for @@ -50,21 +58,20 @@ 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, - l2GasCost=M[gasOffset], - daGasCost=M[gasOffset+1]) +chargeGas(context, l2GasCost, daGasCost); ``` > 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.l2GasLeft - l2GasCost >= 0 assert context.machineState.daGasLeft - daGasCost >= 0 @@ -77,8 +84,9 @@ When the nested call halts, it may not have used up its entire gas allocation. A ## Nested execution Once the nested call's context is initialized, execution within that context begins. + ```jsx -execute(nestedContext) +execute(nestedContext); ``` Execution (and the `execution` shorthand above) is detailed in ["Execution, Gas, Halting"](./execution). Note that execution mutates the nested context. @@ -88,27 +96,32 @@ Execution (and the `execution` shorthand above) is detailed in ["Execution, Gas, 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) +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 +context.machineState.memory[instr.args.successOffset] = + !nestedContext.results.reverted; ``` Any unused gas is refunded to the caller. + ```jsx -context.l2GasLeft += nestedContext.machineState.l2GasLeft -context.daGasLeft += nestedContext.machineState.daGasLeft +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 @@ -118,6 +131,7 @@ if !nestedContext.results.reverted: ### 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) @@ -132,6 +146,7 @@ if nestedContext.results.reverted: > 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 end-lifetimes). + ```jsx -context.worldStateAccessTrace = nestedContext.worldStateAccessTrace +context.worldStateAccessTrace = nestedContext.worldStateAccessTrace; ``` diff --git a/l1-contracts/src/core/libraries/ConstantsGen.sol b/l1-contracts/src/core/libraries/ConstantsGen.sol index 1ae61e2cbec..070b69694c1 100644 --- a/l1-contracts/src/core/libraries/ConstantsGen.sol +++ b/l1-contracts/src/core/libraries/ConstantsGen.sol @@ -76,7 +76,6 @@ library Constants { uint256 internal constant INITIALIZATION_SLOT_SEPARATOR = 1000_000_000; uint256 internal constant INITIAL_L2_BLOCK_NUM = 1; uint256 internal constant BLOB_SIZE_IN_BYTES = 31 * 4096; - uint256 internal constant NESTED_CALL_L2_GAS_BUFFER = 20000; uint256 internal constant MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS = 20000; uint256 internal constant MAX_PACKED_BYTECODE_SIZE_PER_PRIVATE_FUNCTION_IN_FIELDS = 3000; uint256 internal constant MAX_PACKED_BYTECODE_SIZE_PER_UNCONSTRAINED_FUNCTION_IN_FIELDS = 3000; diff --git a/noir-projects/aztec-nr/aztec/src/context/avm_context.nr b/noir-projects/aztec-nr/aztec/src/context/avm_context.nr index 5c1b776796a..8e6b6355921 100644 --- a/noir-projects/aztec-nr/aztec/src/context/avm_context.nr +++ b/noir-projects/aztec-nr/aztec/src/context/avm_context.nr @@ -1,8 +1,5 @@ use crate::hash::{compute_secret_hash, compute_message_hash, compute_message_nullifier}; -use dep::protocol_types::{ - address::{AztecAddress, EthAddress}, - constants::{L1_TO_L2_MESSAGE_LENGTH, NESTED_CALL_L2_GAS_BUFFER}, header::Header -}; +use dep::protocol_types::{address::{AztecAddress, EthAddress}, constants::L1_TO_L2_MESSAGE_LENGTH, header::Header}; use dep::protocol_types::traits::{Deserialize, Serialize, Empty}; use dep::protocol_types::abis::function_selector::FunctionSelector; use dep::protocol_types::abis::public_circuit_public_inputs::PublicCircuitPublicInputs; @@ -200,9 +197,12 @@ impl Empty for AvmContext { // Helper functions fn gas_for_call(user_gas: GasOpts) -> [Field; 2] { + // This will get capped by the actual gas left. + // TODO: replace with max possible field. + let MAX_POSSIBLE_GAS: Field = 1_000_000_000_000_000_000_000; [ - user_gas.l2_gas.unwrap_or_else(|| l2_gas_left() - NESTED_CALL_L2_GAS_BUFFER), - user_gas.da_gas.unwrap_or_else(|| da_gas_left()) + user_gas.l2_gas.unwrap_or_else(|| MAX_POSSIBLE_GAS), + user_gas.da_gas.unwrap_or_else(|| MAX_POSSIBLE_GAS) ] } diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr b/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr index bd8748c2b4e..a8a1d67fd7d 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr @@ -102,8 +102,6 @@ global MAX_ARGS_LENGTH: u64 = ARGS_HASH_CHUNK_COUNT * ARGS_HASH_CHUNK_LENGTH; global INITIALIZATION_SLOT_SEPARATOR: Field = 1000_000_000; global INITIAL_L2_BLOCK_NUM: Field = 1; global BLOB_SIZE_IN_BYTES: Field = 31 * 4096; -// How much gas is subtracted from L2GASLEFT when making a nested public call by default in the AVM -global NESTED_CALL_L2_GAS_BUFFER = 20000; // CONTRACT CLASS CONSTANTS global MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS: u64 = 20000; diff --git a/yarn-project/circuits.js/src/constants.gen.ts b/yarn-project/circuits.js/src/constants.gen.ts index f21806ca868..686661fb012 100644 --- a/yarn-project/circuits.js/src/constants.gen.ts +++ b/yarn-project/circuits.js/src/constants.gen.ts @@ -62,7 +62,6 @@ export const MAX_ARGS_LENGTH = ARGS_HASH_CHUNK_COUNT * ARGS_HASH_CHUNK_LENGTH; export const INITIALIZATION_SLOT_SEPARATOR = 1000_000_000; export const INITIAL_L2_BLOCK_NUM = 1; export const BLOB_SIZE_IN_BYTES = 31 * 4096; -export const NESTED_CALL_L2_GAS_BUFFER = 20000; export const MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS = 20000; export const MAX_PACKED_BYTECODE_SIZE_PER_PRIVATE_FUNCTION_IN_FIELDS = 3000; export const MAX_PACKED_BYTECODE_SIZE_PER_UNCONSTRAINED_FUNCTION_IN_FIELDS = 3000; diff --git a/yarn-project/simulator/src/avm/fixtures/index.ts b/yarn-project/simulator/src/avm/fixtures/index.ts index 1ef3dadcc39..a867136dc47 100644 --- a/yarn-project/simulator/src/avm/fixtures/index.ts +++ b/yarn-project/simulator/src/avm/fixtures/index.ts @@ -90,8 +90,8 @@ export function initGlobalVariables(overrides?: Partial): Globa */ export function initMachineState(overrides?: Partial): AvmMachineState { return AvmMachineState.fromState({ - l2GasLeft: overrides?.l2GasLeft ?? 100e6, - daGasLeft: overrides?.daGasLeft ?? 100e6, + l2GasLeft: overrides?.l2GasLeft ?? 1e8, + daGasLeft: overrides?.daGasLeft ?? 1e8, }); } diff --git a/yarn-project/simulator/src/avm/opcodes/external_calls.ts b/yarn-project/simulator/src/avm/opcodes/external_calls.ts index 21d96882ed9..d21818ea17e 100644 --- a/yarn-project/simulator/src/avm/opcodes/external_calls.ts +++ b/yarn-project/simulator/src/avm/opcodes/external_calls.ts @@ -1,6 +1,8 @@ import { FunctionSelector, Gas } from '@aztec/circuits.js'; import { padArrayEnd } from '@aztec/foundation/collection'; +import { strict as assert } from 'assert'; + import { convertAvmResultsToPxResult, createPublicExecution } from '../../public/transitional_adaptors.js'; import type { AvmContext } from '../avm_context.js'; import { gasLeftToGas, sumGas } from '../avm_gas.js'; @@ -57,16 +59,17 @@ abstract class ExternalCall extends Instruction { const callAddress = memory.getAs(addrOffset); const calldataSize = memory.get(argsSizeOffset).toNumber(); const calldata = memory.getSlice(argsOffset, calldataSize).map(f => f.toFr()); - const l2Gas = memory.get(gasOffset).toNumber(); - const daGas = memory.getAs(gasOffset + 1).toNumber(); const functionSelector = memory.getAs(this.functionSelectorOffset).toFr(); + // Gas allocation is capped by the amount of gas left in the current context. + const allocatedL2Gas = Math.min(memory.get(gasOffset).toNumber(), context.machineState.l2GasLeft); + const allocatedDaGas = Math.min(memory.get(gasOffset + 1).toNumber(), context.machineState.daGasLeft); // If we are already in a static call, we propagate the environment. const callType = context.environment.isStaticCall ? 'STATICCALL' : this.type; - const allocatedGas = { l2Gas, daGas }; const memoryOperations = { reads: calldataSize + 5, writes: 1 + this.retSize, indirect: this.indirect }; - const totalGas = sumGas(this.gasCost(memoryOperations), allocatedGas); - context.machineState.consumeGas(totalGas); + const allocatedGas = { l2Gas: allocatedL2Gas, daGas: allocatedDaGas }; + const totalGasToConsume = sumGas(this.gasCost(memoryOperations), allocatedGas); + context.machineState.consumeGas(totalGasToConsume); // TRANSITIONAL: This should be removed once the kernel handles and entire enqueued call per circuit const nestedContext = context.createNestedContractCallContext(