From 11699c82eed49a464a7f766111beb1b4d4edfcd6 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Thu, 4 Apr 2024 11:51:27 -0300 Subject: [PATCH] feat(avm): Gas usage for nested calls (#5495) Adds gas metering for CALLs and STATICCALLs in the AVM. Both opcodes have a fixed gas cost, an extra cost for indirect accesses to memory, and consume whatever gas they pass onto the nested call. The unused gas from the nested call gets refunded when the call returns, as specified on the yellow paper. --- .../aztec-nr/aztec/src/context/avm_context.nr | 4 +- .../contracts/avm_test_contract/src/main.nr | 6 +- .../simulator/src/avm/avm_context.test.ts | 12 +- yarn-project/simulator/src/avm/avm_context.ts | 44 ++----- .../{avm_gas_cost.test.ts => avm_gas.test.ts} | 0 .../src/avm/{avm_gas_cost.ts => avm_gas.ts} | 54 +++++++-- .../simulator/src/avm/avm_machine_state.ts | 11 +- .../simulator/src/avm/avm_memory_types.ts | 5 + .../simulator/src/avm/opcodes/arithmetic.ts | 24 ++-- .../src/avm/opcodes/external_calls.test.ts | 69 +++++++++-- .../src/avm/opcodes/external_calls.ts | 110 +++++++----------- .../simulator/src/avm/opcodes/instruction.ts | 4 +- .../simulator/src/avm/opcodes/memory.ts | 6 +- yellow-paper/docs/public-vm/nested-calls.mdx | 6 +- 14 files changed, 210 insertions(+), 145 deletions(-) rename yarn-project/simulator/src/avm/{avm_gas_cost.test.ts => avm_gas.test.ts} (100%) rename yarn-project/simulator/src/avm/{avm_gas_cost.ts => avm_gas.ts} (73%) 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 69d34d8d622..1be10fa0f40 100644 --- a/noir-projects/aztec-nr/aztec/src/context/avm_context.nr +++ b/noir-projects/aztec-nr/aztec/src/context/avm_context.nr @@ -123,7 +123,7 @@ impl PublicContextInterface for AvmContext { temporary_function_selector: FunctionSelector, args: [Field; ARGS_COUNT] ) -> [Field; RETURN_VALUES_LENGTH] { - let gas = [/*l1_gas*/42, /*l2_gas*/24, /*da_gas*/420]; + let gas = [/*l1_gas*/10000, /*l2_gas*/10000, /*da_gas*/10000]; let results = call( gas, @@ -144,7 +144,7 @@ impl PublicContextInterface for AvmContext { temporary_function_selector: FunctionSelector, args: [Field; ARGS_COUNT] ) -> [Field; RETURN_VALUES_LENGTH] { - let gas = [/*l1_gas*/42, /*l2_gas*/24, /*da_gas*/420]; + let gas = [/*l1_gas*/10000, /*l2_gas*/10000, /*da_gas*/10000]; let (data_to_return, success): ([Field; RETURN_VALUES_LENGTH], u8) = call_static( gas, diff --git a/noir-projects/noir-contracts/contracts/avm_test_contract/src/main.nr b/noir-projects/noir-contracts/contracts/avm_test_contract/src/main.nr index 190dc2cb007..a9c4e54152d 100644 --- a/noir-projects/noir-contracts/contracts/avm_test_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/avm_test_contract/src/main.nr @@ -339,7 +339,7 @@ contract AvmTest { #[aztec(public-vm)] fn raw_nested_call_to_add(arg_a: Field, arg_b: Field) -> pub Field { let selector = FunctionSelector::from_signature("add_args_return(Field,Field)").to_field(); - let gas = [/*l1_gas*/42, /*l2_gas*/24, /*da_gas*/420]; + let gas = [/*l1_gas*/10000, /*l2_gas*/10000, /*da_gas*/10000]; // Nested call let results = context.call_public_function_raw(gas, context.this_address(), selector, [arg_a, arg_b]); @@ -372,7 +372,7 @@ contract AvmTest { #[aztec(public-vm)] fn raw_nested_static_call_to_add(arg_a: Field, arg_b: Field) -> pub (Field, u8) { let selector = FunctionSelector::from_signature("add_args_return(Field,Field)").to_field(); - let gas = [/*l1_gas*/42, /*l2_gas*/24, /*da_gas*/420]; + let gas = [/*l1_gas*/10000, /*l2_gas*/10000, /*da_gas*/10000]; let (result_data, success): ([Field; 1], u8) = context.static_call_public_function_raw(gas, context.this_address(), selector, [arg_a, arg_b]); @@ -383,7 +383,7 @@ contract AvmTest { #[aztec(public-vm)] fn raw_nested_static_call_to_set_storage() -> pub u8 { let selector = FunctionSelector::from_signature("set_storage_single(Field)").to_field(); - let gas = [/*l1_gas*/42, /*l2_gas*/24, /*da_gas*/420]; + let gas = [/*l1_gas*/10000, /*l2_gas*/10000, /*da_gas*/10000]; let calldata: [Field; 1] = [20]; let (_data_to_return, success): ([Field; 0], u8) = context.static_call_public_function_raw(gas, context.this_address(), selector, calldata); diff --git a/yarn-project/simulator/src/avm/avm_context.test.ts b/yarn-project/simulator/src/avm/avm_context.test.ts index 089e1d3f4c4..c281f3cf928 100644 --- a/yarn-project/simulator/src/avm/avm_context.test.ts +++ b/yarn-project/simulator/src/avm/avm_context.test.ts @@ -9,7 +9,8 @@ describe('Avm Context', () => { const newAddress = AztecAddress.random(); const newCalldata = [new Fr(1), new Fr(2)]; - const newContext = context.createNestedContractCallContext(newAddress, newCalldata); + const allocatedGas = { l1Gas: 1, l2Gas: 2, daGas: 3 }; // How much of the current call gas we pass to the nested call + const newContext = context.createNestedContractCallContext(newAddress, newCalldata, allocatedGas, 'CALL'); expect(newContext.environment).toEqual( allSameExcept(context.environment, { @@ -23,6 +24,9 @@ describe('Avm Context', () => { expect(newContext.machineState).toEqual( allSameExcept(context.machineState, { pc: 0, + l1GasLeft: 1, + l2GasLeft: 2, + daGasLeft: 3, }), ); @@ -36,7 +40,8 @@ describe('Avm Context', () => { const newAddress = AztecAddress.random(); const newCalldata = [new Fr(1), new Fr(2)]; - const newContext = context.createNestedContractStaticCallContext(newAddress, newCalldata); + const allocatedGas = { l1Gas: 1, l2Gas: 2, daGas: 3 }; + const newContext = context.createNestedContractCallContext(newAddress, newCalldata, allocatedGas, 'STATICCALL'); expect(newContext.environment).toEqual( allSameExcept(context.environment, { @@ -50,6 +55,9 @@ describe('Avm Context', () => { expect(newContext.machineState).toEqual( allSameExcept(context.machineState, { pc: 0, + l1GasLeft: 1, + l2GasLeft: 2, + daGasLeft: 3, }), ); diff --git a/yarn-project/simulator/src/avm/avm_context.ts b/yarn-project/simulator/src/avm/avm_context.ts index 269908b3ae1..c9c5e13ef76 100644 --- a/yarn-project/simulator/src/avm/avm_context.ts +++ b/yarn-project/simulator/src/avm/avm_context.ts @@ -2,6 +2,7 @@ import { type AztecAddress, FunctionSelector } from '@aztec/circuits.js'; import { type Fr } from '@aztec/foundation/fields'; import { type AvmExecutionEnvironment } from './avm_execution_environment.js'; +import { type Gas, gasToGasLeft } from './avm_gas.js'; import { AvmMachineState } from './avm_machine_state.js'; import { type AvmPersistableStateManager } from './journal/journal.js'; @@ -33,47 +34,24 @@ export class AvmContext { * * @param address - The contract instance to initialize a context for * @param calldata - Data/arguments for nested call + * @param allocatedGas - Gas allocated for the nested call + * @param callType - Type of call (CALL or STATICCALL) * @returns new AvmContext instance */ public createNestedContractCallContext( address: AztecAddress, calldata: Fr[], + allocatedGas: Gas, + callType: 'CALL' | 'STATICCALL', temporaryFunctionSelector: FunctionSelector = FunctionSelector.empty(), ): AvmContext { - const newExecutionEnvironment = this.environment.deriveEnvironmentForNestedCall( - address, - calldata, - temporaryFunctionSelector, - ); + const deriveFn = + callType === 'CALL' + ? this.environment.deriveEnvironmentForNestedCall + : this.environment.deriveEnvironmentForNestedStaticCall; + const newExecutionEnvironment = deriveFn.call(this.environment, address, calldata, temporaryFunctionSelector); const forkedWorldState = this.persistableState.fork(); - const machineState = AvmMachineState.fromState(this.machineState); - return new AvmContext(forkedWorldState, newExecutionEnvironment, machineState); - } - - /** - * Prepare a new AVM context that will be ready for an external/nested static call - * - Fork the world state journal - * - Derive a machine state from the current state - * - E.g., gas metering is preserved but pc is reset - * - Derive an execution environment from the caller/parent - * - Alter both address and storageAddress - * - * @param address - The contract instance to initialize a context for - * @param calldata - Data/arguments for nested call - * @returns new AvmContext instance - */ - public createNestedContractStaticCallContext( - address: AztecAddress, - calldata: Fr[], - temporaryFunctionSelector: FunctionSelector = FunctionSelector.empty(), - ): AvmContext { - const newExecutionEnvironment = this.environment.deriveEnvironmentForNestedStaticCall( - address, - calldata, - temporaryFunctionSelector, - ); - const forkedWorldState = this.persistableState.fork(); - const machineState = AvmMachineState.fromState(this.machineState); + const machineState = AvmMachineState.fromState(gasToGasLeft(allocatedGas)); return new AvmContext(forkedWorldState, newExecutionEnvironment, machineState); } } diff --git a/yarn-project/simulator/src/avm/avm_gas_cost.test.ts b/yarn-project/simulator/src/avm/avm_gas.test.ts similarity index 100% rename from yarn-project/simulator/src/avm/avm_gas_cost.test.ts rename to yarn-project/simulator/src/avm/avm_gas.test.ts diff --git a/yarn-project/simulator/src/avm/avm_gas_cost.ts b/yarn-project/simulator/src/avm/avm_gas.ts similarity index 73% rename from yarn-project/simulator/src/avm/avm_gas_cost.ts rename to yarn-project/simulator/src/avm/avm_gas.ts index 1ba321a1c8f..aa070045981 100644 --- a/yarn-project/simulator/src/avm/avm_gas_cost.ts +++ b/yarn-project/simulator/src/avm/avm_gas.ts @@ -1,20 +1,43 @@ import { TypeTag } from './avm_memory_types.js'; +import { Addressing, AddressingMode } from './opcodes/addressing_mode.js'; import { Opcode } from './serialization/instruction_serialization.js'; -/** Gas cost in L1, L2, and DA for a given instruction. */ -export type GasCost = { +/** Gas counters in L1, L2, and DA. */ +export type Gas = { l1Gas: number; l2Gas: number; daGas: number; }; +/** Maps a Gas struct to gasLeft properties. */ +export function gasToGasLeft(gas: Gas) { + return { l1GasLeft: gas.l1Gas, l2GasLeft: gas.l2Gas, daGasLeft: gas.daGas }; +} + +/** Maps gasLeft properties to a gas struct. */ +export function gasLeftToGas(gasLeft: { l1GasLeft: number; l2GasLeft: number; daGasLeft: number }) { + return { l1Gas: gasLeft.l1GasLeft, l2Gas: gasLeft.l2GasLeft, daGas: gasLeft.daGasLeft }; +} + /** Creates a new instance with all values set to zero except the ones set. */ -export function makeGasCost(gasCost: Partial) { - return { ...EmptyGasCost, ...gasCost }; +export function makeGasCost(gasCost: Partial) { + return { ...EmptyGas, ...gasCost }; +} + +/** Sums together multiple instances of Gas. */ +export function sumGas(...gases: Partial[]) { + return gases.reduce( + (acc: Gas, gas) => ({ + l1Gas: acc.l1Gas + (gas.l1Gas ?? 0), + l2Gas: acc.l2Gas + (gas.l2Gas ?? 0), + daGas: acc.daGas + (gas.daGas ?? 0), + }), + EmptyGas, + ); } -/** Gas cost of zero across all gas dimensions. */ -export const EmptyGasCost = { +/** Zero gas across all gas dimensions. */ +export const EmptyGas: Gas = { l1Gas: 0, l2Gas: 0, daGas: 0, @@ -103,12 +126,29 @@ export const GasCosts = { [Opcode.PEDERSEN]: TemporaryDefaultGasCost, // temp - may be removed, but alot of contracts rely on i: TemporaryDefaultGasCost,t } as const; +/** Returns the fixed gas cost for a given opcode, or throws if set to dynamic. */ +export function getFixedGasCost(opcode: Opcode): Gas { + const cost = GasCosts[opcode]; + if (cost === DynamicGasCost) { + throw new Error(`Opcode ${Opcode[opcode]} has dynamic gas cost`); + } + return cost; +} + +/** Returns the additional cost from indirect accesses to memory. */ +export function getCostFromIndirectAccess(indirect: number): Partial { + const indirectCount = Addressing.fromWire(indirect).modePerOperand.filter( + mode => mode === AddressingMode.INDIRECT, + ).length; + return { l2Gas: indirectCount * GasCostConstants.COST_PER_INDIRECT_ACCESS }; +} + /** Constants used in base cost calculations. */ export const GasCostConstants = { SET_COST_PER_BYTE: 100, CALLDATACOPY_COST_PER_BYTE: 10, ARITHMETIC_COST_PER_BYTE: 10, - ARITHMETIC_COST_PER_INDIRECT_ACCESS: 5, + COST_PER_INDIRECT_ACCESS: 5, }; /** Returns a multiplier based on the size of the type represented by the tag. Throws on uninitialized or invalid. */ diff --git a/yarn-project/simulator/src/avm/avm_machine_state.ts b/yarn-project/simulator/src/avm/avm_machine_state.ts index a7b6f04334f..178ca1adcf2 100644 --- a/yarn-project/simulator/src/avm/avm_machine_state.ts +++ b/yarn-project/simulator/src/avm/avm_machine_state.ts @@ -1,6 +1,6 @@ import { type Fr } from '@aztec/circuits.js'; -import { type GasCost, GasDimensions } from './avm_gas_cost.js'; +import { type Gas, GasDimensions } from './avm_gas.js'; import { TaggedMemory } from './avm_memory_types.js'; import { AvmContractCallResults } from './avm_message_call_result.js'; import { OutOfGasError } from './errors.js'; @@ -59,7 +59,7 @@ export class AvmMachineState { * Should any of the gas dimensions get depleted, it sets all gas left to zero and triggers * an exceptional halt by throwing an OutOfGasError. */ - public consumeGas(gasCost: Partial) { + public consumeGas(gasCost: Partial) { // Assert there is enough gas on every dimension. const outOfGasDimensions = GasDimensions.filter( dimension => this[`${dimension}Left`] - (gasCost[dimension] ?? 0) < 0, @@ -76,6 +76,13 @@ export class AvmMachineState { } } + /** Increases the gas left by the amounts specified. */ + public refundGas(gasRefund: Partial) { + for (const dimension of GasDimensions) { + this[`${dimension}Left`] += gasRefund[dimension] ?? 0; + } + } + /** * Most instructions just increment PC before they complete */ diff --git a/yarn-project/simulator/src/avm/avm_memory_types.ts b/yarn-project/simulator/src/avm/avm_memory_types.ts index fdaf9c63968..a1b0be40c3f 100644 --- a/yarn-project/simulator/src/avm/avm_memory_types.ts +++ b/yarn-project/simulator/src/avm/avm_memory_types.ts @@ -30,6 +30,11 @@ export abstract class MemoryValue { return new Fr(this.toBigInt()); } + // To number. Throws if exceeds max safe int. + public toNumber(): number { + return this.toFr().toNumber(); + } + public toString(): string { return `${this.constructor.name}(0x${this.toBigInt().toString(16)})`; } diff --git a/yarn-project/simulator/src/avm/opcodes/arithmetic.ts b/yarn-project/simulator/src/avm/opcodes/arithmetic.ts index 597ead05cc7..a3186d427cc 100644 --- a/yarn-project/simulator/src/avm/opcodes/arithmetic.ts +++ b/yarn-project/simulator/src/avm/opcodes/arithmetic.ts @@ -1,8 +1,13 @@ import type { AvmContext } from '../avm_context.js'; -import { type GasCost, GasCostConstants, getGasCostMultiplierFromTypeTag, makeGasCost } from '../avm_gas_cost.js'; +import { + type Gas, + GasCostConstants, + getCostFromIndirectAccess, + getGasCostMultiplierFromTypeTag, + sumGas, +} from '../avm_gas.js'; import { type Field, type MemoryValue, TypeTag } from '../avm_memory_types.js'; import { Opcode, OperandType } from '../serialization/instruction_serialization.js'; -import { Addressing, AddressingMode } from './addressing_mode.js'; import { Instruction } from './instruction.js'; import { ThreeOperandInstruction } from './instruction_impl.js'; @@ -19,15 +24,12 @@ export abstract class ThreeOperandArithmeticInstruction extends ThreeOperandInst context.machineState.incrementPc(); } - protected gasCost(): GasCost { - const indirectCount = Addressing.fromWire(this.indirect).modePerOperand.filter( - mode => mode === AddressingMode.INDIRECT, - ).length; - - const l2Gas = - indirectCount * GasCostConstants.ARITHMETIC_COST_PER_INDIRECT_ACCESS + - getGasCostMultiplierFromTypeTag(this.inTag) * GasCostConstants.ARITHMETIC_COST_PER_BYTE; - return makeGasCost({ l2Gas }); + protected gasCost(): Gas { + const arithmeticCost = { + l2Gas: getGasCostMultiplierFromTypeTag(this.inTag) * GasCostConstants.ARITHMETIC_COST_PER_BYTE, + }; + const indirectCost = getCostFromIndirectAccess(this.indirect); + return sumGas(arithmeticCost, indirectCost); } protected abstract compute(a: MemoryValue, b: MemoryValue): MemoryValue; diff --git a/yarn-project/simulator/src/avm/opcodes/external_calls.test.ts b/yarn-project/simulator/src/avm/opcodes/external_calls.test.ts index 9647aaeea3d..90db43f1989 100644 --- a/yarn-project/simulator/src/avm/opcodes/external_calls.test.ts +++ b/yarn-project/simulator/src/avm/opcodes/external_calls.test.ts @@ -57,18 +57,21 @@ describe('External Calls', () => { expect(inst.serialize()).toEqual(buf); }); - // TODO(https://github.com/AztecProtocol/aztec-packages/issues/3992): gas not implemented it('Should execute a call correctly', async () => { const gasOffset = 0; - const gas = Fr.zero(); - const addrOffset = 1; + const l1Gas = 1e6; + const l2Gas = 2e6; + const daGas = 3e6; + const addrOffset = 3; const addr = new Fr(123456n); - const argsOffset = 2; + const argsOffset = 4; const args = [new Field(1n), new Field(2n), new Field(3n)]; const argsSize = args.length; const retOffset = 8; const retSize = 2; const successOffset = 7; + + const otherContextInstructionsL2GasCost = 60; // Includes the cost of the call itself const otherContextInstructionsBytecode = encodeToBytecode([ new CalldataCopy( /*indirect=*/ 0, @@ -80,9 +83,13 @@ describe('External Calls', () => { new Return(/*indirect=*/ 0, /*retOffset=*/ 0, /*size=*/ 2), ]); - context.machineState.memory.set(0, new Field(gas)); - context.machineState.memory.set(1, new Field(addr)); - context.machineState.memory.setSlice(2, args); + const { l1GasLeft: initialL1Gas, l2GasLeft: initialL2Gas, daGasLeft: initialDaGas } = context.machineState; + + context.machineState.memory.set(0, new Field(l1Gas)); + context.machineState.memory.set(1, new Field(l2Gas)); + context.machineState.memory.set(2, new Field(daGas)); + context.machineState.memory.set(3, new Field(addr)); + context.machineState.memory.setSlice(4, args); jest .spyOn(context.persistableState.hostStorage.contractsDb, 'getBytecode') .mockReturnValue(Promise.resolve(otherContextInstructionsBytecode)); @@ -98,7 +105,7 @@ describe('External Calls', () => { successOffset, /*temporaryFunctionSelectorOffset=*/ 0, ); - await instruction.execute(context); + await instruction.run(context); const successValue = context.machineState.memory.get(successOffset); expect(successValue).toEqual(new Uint8(1n)); @@ -116,6 +123,50 @@ describe('External Calls', () => { const slotNumber = 1n; const expectedStoredValue = new Fr(1n); expect(nestedContractWrites!.get(slotNumber)).toEqual(expectedStoredValue); + + // Check that the nested gas call was used and refunded + expect(context.machineState.l1GasLeft).toEqual(initialL1Gas); + expect(context.machineState.l2GasLeft).toEqual(initialL2Gas - otherContextInstructionsL2GasCost); + expect(context.machineState.daGasLeft).toEqual(initialDaGas); + }); + + it('Should refuse to execute a call if not enough gas', async () => { + const gasOffset = 0; + const l1Gas = 1e12; // We request more gas than what we have + const l2Gas = 2e6; + const daGas = 3e6; + const addrOffset = 3; + const addr = new Fr(123456n); + const argsOffset = 4; + const args = [new Field(1n), new Field(2n), new Field(3n)]; + const argsSize = args.length; + const retOffset = 8; + const retSize = 2; + const successOffset = 7; + + context.machineState.memory.set(0, new Field(l1Gas)); + context.machineState.memory.set(1, new Field(l2Gas)); + context.machineState.memory.set(2, new Field(daGas)); + context.machineState.memory.set(3, new Field(addr)); + context.machineState.memory.setSlice(4, args); + + jest + .spyOn(context.persistableState.hostStorage.contractsDb, 'getBytecode') + .mockRejectedValue(new Error('No bytecode expected to be requested since not enough gas')); + + const instruction = new Call( + /*indirect=*/ 0, + gasOffset, + addrOffset, + argsOffset, + argsSize, + retOffset, + retSize, + successOffset, + /*temporaryFunctionSelectorOffset=*/ 0, + ); + + await expect(() => instruction.run(context)).rejects.toThrow(/Not enough.*gas left/i); }); }); @@ -193,7 +244,7 @@ describe('External Calls', () => { successOffset, /*temporaryFunctionSelectorOffset=*/ 0, ); - await instruction.execute(context); + await instruction.run(context); // No revert has occurred, but the nested execution has failed const successValue = context.machineState.memory.get(successOffset); diff --git a/yarn-project/simulator/src/avm/opcodes/external_calls.ts b/yarn-project/simulator/src/avm/opcodes/external_calls.ts index 9a8e5b6d69c..c02c1ad42db 100644 --- a/yarn-project/simulator/src/avm/opcodes/external_calls.ts +++ b/yarn-project/simulator/src/avm/opcodes/external_calls.ts @@ -1,15 +1,14 @@ import { FunctionSelector } from '@aztec/circuits.js'; import type { AvmContext } from '../avm_context.js'; +import { type Gas, gasLeftToGas, getCostFromIndirectAccess, getFixedGasCost, sumGas } from '../avm_gas.js'; import { Field, Uint8 } from '../avm_memory_types.js'; import { AvmSimulator } from '../avm_simulator.js'; import { Opcode, OperandType } from '../serialization/instruction_serialization.js'; import { Addressing } from './addressing_mode.js'; import { Instruction } from './instruction.js'; -export class Call extends Instruction { - static type: string = 'CALL'; - static readonly opcode: Opcode = Opcode.CALL; +abstract class ExternalCall extends Instruction { // Informs (de)serialization. See Instruction.deserialize. static readonly wireFormat: OperandType[] = [ OperandType.UINT8, @@ -27,7 +26,7 @@ export class Call extends Instruction { constructor( private indirect: number, - private _gasOffset: number /* Unused due to no formal gas implementation at this moment */, + private gasOffset: number /* Unused due to no formal gas implementation at this moment */, private addrOffset: number, private argsOffset: number, private argsSize: number, @@ -42,20 +41,30 @@ export class Call extends Instruction { super(); } - // TODO(https://github.com/AztecProtocol/aztec-packages/issues/3992): there is no concept of remaining / available gas at this moment - async execute(context: AvmContext): Promise { - const [_gasOffset, addrOffset, argsOffset, retOffset, successOffset] = Addressing.fromWire(this.indirect).resolve( - [this._gasOffset, this.addrOffset, this.argsOffset, this.retOffset, this.successOffset], + async run(context: AvmContext): Promise { + const [gasOffset, addrOffset, argsOffset, retOffset, successOffset] = Addressing.fromWire(this.indirect).resolve( + [this.gasOffset, this.addrOffset, this.argsOffset, this.retOffset, this.successOffset], context.machineState.memory, ); const callAddress = context.machineState.memory.getAs(addrOffset); const calldata = context.machineState.memory.getSlice(argsOffset, this.argsSize).map(f => f.toFr()); + const l1Gas = context.machineState.memory.get(gasOffset).toNumber(); + const l2Gas = context.machineState.memory.getAs(gasOffset + 1).toNumber(); + const daGas = context.machineState.memory.getAs(gasOffset + 2).toNumber(); const functionSelector = context.machineState.memory.getAs(this.temporaryFunctionSelectorOffset).toFr(); + // Consume a base fixed gas cost for the call opcode, plus whatever is allocated for the nested call + const baseGas = getFixedGasCost(this.opcode); + const addressingGasCost = getCostFromIndirectAccess(this.indirect); + const allocatedGas = { l1Gas, l2Gas, daGas }; + context.machineState.consumeGas(sumGas(baseGas, addressingGasCost, allocatedGas)); + const nestedContext = context.createNestedContractCallContext( callAddress.toFr(), calldata, + allocatedGas, + this.type, FunctionSelector.fromField(functionSelector), ); @@ -70,6 +79,10 @@ export class Call extends Instruction { context.machineState.memory.set(successOffset, new Uint8(success ? 1 : 0)); context.machineState.memory.setSlice(retOffset, convertedReturnData); + // Refund unused gas + context.machineState.refundGas(gasLeftToGas(nestedContext.machineState)); + + // TODO: Should we merge the changes from a nested call in the case of a STATIC call? if (success) { context.persistableState.acceptNestedCallState(nestedContext.persistableState); } else { @@ -78,74 +91,35 @@ export class Call extends Instruction { context.machineState.incrementPc(); } -} - -export class StaticCall extends Instruction { - static type: string = 'STATICCALL'; - static readonly opcode: Opcode = Opcode.STATICCALL; - // Informs (de)serialization. See Instruction.deserialize. - static readonly wireFormat: OperandType[] = [ - OperandType.UINT8, - OperandType.UINT8, - OperandType.UINT32, - OperandType.UINT32, - OperandType.UINT32, - OperandType.UINT32, - OperandType.UINT32, - OperandType.UINT32, - OperandType.UINT32, - /* temporary function selector */ - OperandType.UINT32, - ]; - - constructor( - private indirect: number, - private _gasOffset: number /* Unused due to no formal gas implementation at this moment */, - private addrOffset: number, - private argsOffset: number, - private argsSize: number, - private retOffset: number, - private retSize: number, - private successOffset: number, - private temporaryFunctionSelectorOffset: number, - ) { - super(); - } - - async execute(context: AvmContext): Promise { - const [_gasOffset, addrOffset, argsOffset, retOffset, successOffset] = Addressing.fromWire(this.indirect).resolve( - [this._gasOffset, this.addrOffset, this.argsOffset, this.retOffset, this.successOffset], - context.machineState.memory, - ); - const callAddress = context.machineState.memory.get(addrOffset); - const calldata = context.machineState.memory.getSlice(argsOffset, this.argsSize).map(f => f.toFr()); - const functionSelector = context.machineState.memory.getAs(this.temporaryFunctionSelectorOffset).toFr(); + public abstract get type(): 'CALL' | 'STATICCALL'; - const nestedContext = context.createNestedContractStaticCallContext( - callAddress.toFr(), - calldata, - FunctionSelector.fromField(functionSelector), + protected execute(_context: AvmContext): Promise { + throw new Error( + `Instructions with dynamic gas calculation run all logic on the main execute function and do not override the internal execute.`, ); + } - const nestedCallResults = await new AvmSimulator(nestedContext).execute(); - const success = !nestedCallResults.reverted; + protected gasCost(): Gas { + throw new Error(`Instructions with dynamic gas calculation compute gas as part of the main execute function.`); + } +} - // We only take as much data as was specified in the return size -> TODO: should we be reverting here - const returnData = nestedCallResults.output.slice(0, this.retSize); - const convertedReturnData = returnData.map(f => new Field(f)); +export class Call extends ExternalCall { + static type = 'CALL' as const; + static readonly opcode: Opcode = Opcode.CALL; - // Write our return data into memory - context.machineState.memory.set(successOffset, new Uint8(success ? 1 : 0)); - context.machineState.memory.setSlice(retOffset, convertedReturnData); + public get type() { + return Call.type; + } +} - if (success) { - context.persistableState.acceptNestedCallState(nestedContext.persistableState); - } else { - context.persistableState.rejectNestedCallState(nestedContext.persistableState); - } +export class StaticCall extends ExternalCall { + static type = 'STATICCALL' as const; + static readonly opcode: Opcode = Opcode.STATICCALL; - context.machineState.incrementPc(); + public get type() { + return StaticCall.type; } } diff --git a/yarn-project/simulator/src/avm/opcodes/instruction.ts b/yarn-project/simulator/src/avm/opcodes/instruction.ts index 93bf6c307dc..5fd01bb9bf8 100644 --- a/yarn-project/simulator/src/avm/opcodes/instruction.ts +++ b/yarn-project/simulator/src/avm/opcodes/instruction.ts @@ -1,7 +1,7 @@ import { strict as assert } from 'assert'; import type { AvmContext } from '../avm_context.js'; -import { DynamicGasCost, type GasCost, GasCosts } from '../avm_gas_cost.js'; +import { DynamicGasCost, type Gas, GasCosts } from '../avm_gas.js'; import { type BufferCursor } from '../serialization/buffer_cursor.js'; import { Opcode, type OperandType, deserialize, serialize } from '../serialization/instruction_serialization.js'; @@ -29,7 +29,7 @@ export abstract class Instruction { * Loads default gas cost for the instruction from the GasCosts table. * Instruction sub-classes can override this if their gas cost is not fixed. */ - protected gasCost(): GasCost { + protected gasCost(): Gas { const gasCost = GasCosts[this.opcode]; if (gasCost === DynamicGasCost) { throw new Error(`Instruction ${this.type} must define its own gas cost`); diff --git a/yarn-project/simulator/src/avm/opcodes/memory.ts b/yarn-project/simulator/src/avm/opcodes/memory.ts index 74137183d8b..441a1129e5f 100644 --- a/yarn-project/simulator/src/avm/opcodes/memory.ts +++ b/yarn-project/simulator/src/avm/opcodes/memory.ts @@ -1,5 +1,5 @@ import type { AvmContext } from '../avm_context.js'; -import { type GasCost, GasCostConstants, getGasCostMultiplierFromTypeTag, makeGasCost } from '../avm_gas_cost.js'; +import { type Gas, GasCostConstants, getGasCostMultiplierFromTypeTag, makeGasCost } from '../avm_gas.js'; import { Field, TaggedMemory, TypeTag } from '../avm_memory_types.js'; import { InstructionExecutionError } from '../errors.js'; import { BufferCursor } from '../serialization/buffer_cursor.js'; @@ -81,7 +81,7 @@ export class Set extends Instruction { context.machineState.incrementPc(); } - protected gasCost(): GasCost { + protected gasCost(): Gas { return makeGasCost({ l2Gas: GasCostConstants.SET_COST_PER_BYTE * getGasCostMultiplierFromTypeTag(this.inTag) }); } } @@ -199,7 +199,7 @@ export class CalldataCopy extends Instruction { context.machineState.incrementPc(); } - protected gasCost(): GasCost { + protected gasCost(): Gas { return makeGasCost({ l2Gas: GasCostConstants.CALLDATACOPY_COST_PER_BYTE * this.copySize }); } } diff --git a/yellow-paper/docs/public-vm/nested-calls.mdx b/yellow-paper/docs/public-vm/nested-calls.mdx index 97ac4c40b53..167f08fc2af 100644 --- a/yellow-paper/docs/public-vm/nested-calls.mdx +++ b/yellow-paper/docs/public-vm/nested-calls.mdx @@ -68,9 +68,9 @@ chargeGas(context, 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 +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