From 0b3c85e13a565e0f7f67afa4b29979f91815ae7f Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Wed, 27 Mar 2024 19:28:29 -0300 Subject: [PATCH 1/3] feat(avm): Gas usage for nested calls --- .../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} | 51 ++++++-- .../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 | 112 +++++++----------- .../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, 206 insertions(+), 148 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 eff362ec1d6..2a89343e990 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 @@ -315,7 +315,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]); @@ -348,7 +348,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]); @@ -359,7 +359,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..01b09d53840 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 }; + 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 3ae5e6dfe3c..df9652fee69 100644 --- a/yarn-project/simulator/src/avm/avm_context.ts +++ b/yarn-project/simulator/src/avm/avm_context.ts @@ -2,6 +2,7 @@ import { AztecAddress, FunctionSelector } from '@aztec/circuits.js'; import { Fr } from '@aztec/foundation/fields'; import { AvmExecutionEnvironment } from './avm_execution_environment.js'; +import { Gas, gasToGasLeft } from './avm_gas.js'; import { AvmMachineState } from './avm_machine_state.js'; import { 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 69f80e2dfe7..80f30eb0590 100644 --- a/yarn-project/simulator/src/avm/avm_gas_cost.ts +++ b/yarn-project/simulator/src/avm/avm_gas.ts @@ -1,20 +1,40 @@ 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 }; +} + +/** Adds multiple instances of Gas. */ +export function addGas(...gases: Partial[]) { + return { + l1Gas: gases.reduce((acc, gas) => acc + (gas.l1Gas ?? 0), 0), + l2Gas: gases.reduce((acc, gas) => acc + (gas.l2Gas ?? 0), 0), + daGas: gases.reduce((acc, gas) => acc + (gas.daGas ?? 0), 0), + }; } -/** Gas cost of zero across all gas dimensions. */ -export const EmptyGasCost = { +/** Zero gas across all gas dimensions. */ +export const EmptyGas = { l1Gas: 0, l2Gas: 0, daGas: 0, @@ -102,12 +122,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 5b4888185a6..c6bf2ee6cc2 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 { Fr } from '@aztec/circuits.js'; -import { GasCost, GasDimensions } from './avm_gas_cost.js'; +import { 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 7f4b07462a0..22b4c9b50f8 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 44790a692d8..61252d43450 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 { GasCost, GasCostConstants, getGasCostMultiplierFromTypeTag, makeGasCost } from '../avm_gas_cost.js'; +import { + Gas, + GasCostConstants, + addGas, + getCostFromIndirectAccess, + getGasCostMultiplierFromTypeTag, +} from '../avm_gas.js'; import { Field, 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 addGas(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 d6112104d45..89fec377cd3 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..25d210425cd 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 { Gas, addGas, gasLeftToGas, getCostFromIndirectAccess, getFixedGasCost } 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.getAs(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(addGas(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,75 +91,32 @@ 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(); + public get type(): 'CALL' | 'STATICCALL' { + const type = super.type; + if (type !== 'CALL' && type !== 'STATICCALL') { + throw new Error(`Invalid type for ExternalCall instruction: ${type}`); + } + return type; } - 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(); - - const nestedContext = context.createNestedContractStaticCallContext( - callAddress.toFr(), - calldata, - FunctionSelector.fromField(functionSelector), - ); - - const nestedCallResults = await new AvmSimulator(nestedContext).execute(); - const success = !nestedCallResults.reverted; - - // 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)); + protected execute(_context: AvmContext): Promise { + throw new Error(`Unimplemented`); + } - // Write our return data into memory - context.machineState.memory.set(successOffset, new Uint8(success ? 1 : 0)); - context.machineState.memory.setSlice(retOffset, convertedReturnData); + protected gasCost(): Gas { + throw new Error(`Unimplemented`); + } +} - if (success) { - context.persistableState.acceptNestedCallState(nestedContext.persistableState); - } else { - context.persistableState.rejectNestedCallState(nestedContext.persistableState); - } +export class Call extends ExternalCall { + static type: string = 'CALL'; + static readonly opcode: Opcode = Opcode.CALL; +} - context.machineState.incrementPc(); - } +export class StaticCall extends ExternalCall { + static type: string = 'STATICCALL'; + static readonly opcode: Opcode = Opcode.STATICCALL; } export class Return extends Instruction { diff --git a/yarn-project/simulator/src/avm/opcodes/instruction.ts b/yarn-project/simulator/src/avm/opcodes/instruction.ts index 2660052c265..9f785abeb28 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, GasCost, GasCosts } from '../avm_gas_cost.js'; +import { DynamicGasCost, Gas, GasCosts } from '../avm_gas.js'; import { BufferCursor } from '../serialization/buffer_cursor.js'; import { Opcode, 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 87a5ccdd465..48994210d59 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 { GasCost, GasCostConstants, getGasCostMultiplierFromTypeTag, makeGasCost } from '../avm_gas_cost.js'; +import { 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 From 9f4063b546793cef507ca7aeb34141d5b06b0193 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Fri, 29 Mar 2024 10:29:23 -0300 Subject: [PATCH 2/3] Address comments from review --- .../simulator/src/avm/avm_context.test.ts | 2 +- yarn-project/simulator/src/avm/avm_gas.ts | 19 ++++++----- .../simulator/src/avm/opcodes/arithmetic.ts | 4 +-- .../src/avm/opcodes/external_calls.ts | 32 +++++++++++-------- 4 files changed, 32 insertions(+), 25 deletions(-) diff --git a/yarn-project/simulator/src/avm/avm_context.test.ts b/yarn-project/simulator/src/avm/avm_context.test.ts index 01b09d53840..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,7 @@ describe('Avm Context', () => { const newAddress = AztecAddress.random(); const newCalldata = [new Fr(1), new Fr(2)]; - const allocatedGas = { l1Gas: 1, l2Gas: 2, daGas: 3 }; + 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( diff --git a/yarn-project/simulator/src/avm/avm_gas.ts b/yarn-project/simulator/src/avm/avm_gas.ts index 80f30eb0590..c02998f4779 100644 --- a/yarn-project/simulator/src/avm/avm_gas.ts +++ b/yarn-project/simulator/src/avm/avm_gas.ts @@ -24,17 +24,20 @@ export function makeGasCost(gasCost: Partial) { return { ...EmptyGas, ...gasCost }; } -/** Adds multiple instances of Gas. */ -export function addGas(...gases: Partial[]) { - return { - l1Gas: gases.reduce((acc, gas) => acc + (gas.l1Gas ?? 0), 0), - l2Gas: gases.reduce((acc, gas) => acc + (gas.l2Gas ?? 0), 0), - daGas: gases.reduce((acc, gas) => acc + (gas.daGas ?? 0), 0), - }; +/** 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, + ); } /** Zero gas across all gas dimensions. */ -export const EmptyGas = { +export const EmptyGas: Gas = { l1Gas: 0, l2Gas: 0, daGas: 0, diff --git a/yarn-project/simulator/src/avm/opcodes/arithmetic.ts b/yarn-project/simulator/src/avm/opcodes/arithmetic.ts index 61252d43450..69ecbe41cd9 100644 --- a/yarn-project/simulator/src/avm/opcodes/arithmetic.ts +++ b/yarn-project/simulator/src/avm/opcodes/arithmetic.ts @@ -2,9 +2,9 @@ import type { AvmContext } from '../avm_context.js'; import { Gas, GasCostConstants, - addGas, getCostFromIndirectAccess, getGasCostMultiplierFromTypeTag, + sumGas, } from '../avm_gas.js'; import { Field, MemoryValue, TypeTag } from '../avm_memory_types.js'; import { Opcode, OperandType } from '../serialization/instruction_serialization.js'; @@ -29,7 +29,7 @@ export abstract class ThreeOperandArithmeticInstruction extends ThreeOperandInst l2Gas: getGasCostMultiplierFromTypeTag(this.inTag) * GasCostConstants.ARITHMETIC_COST_PER_BYTE, }; const indirectCost = getCostFromIndirectAccess(this.indirect); - return addGas(arithmeticCost, indirectCost); + return sumGas(arithmeticCost, indirectCost); } protected abstract compute(a: MemoryValue, b: MemoryValue): MemoryValue; diff --git a/yarn-project/simulator/src/avm/opcodes/external_calls.ts b/yarn-project/simulator/src/avm/opcodes/external_calls.ts index 25d210425cd..da090f7d179 100644 --- a/yarn-project/simulator/src/avm/opcodes/external_calls.ts +++ b/yarn-project/simulator/src/avm/opcodes/external_calls.ts @@ -1,7 +1,7 @@ import { FunctionSelector } from '@aztec/circuits.js'; import type { AvmContext } from '../avm_context.js'; -import { Gas, addGas, gasLeftToGas, getCostFromIndirectAccess, getFixedGasCost } from '../avm_gas.js'; +import { 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'; @@ -49,7 +49,7 @@ abstract class ExternalCall extends Instruction { 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.getAs(gasOffset).toNumber(); + 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(); @@ -58,7 +58,7 @@ abstract class ExternalCall extends Instruction { const baseGas = getFixedGasCost(this.opcode); const addressingGasCost = getCostFromIndirectAccess(this.indirect); const allocatedGas = { l1Gas, l2Gas, daGas }; - context.machineState.consumeGas(addGas(baseGas, addressingGasCost, allocatedGas)); + context.machineState.consumeGas(sumGas(baseGas, addressingGasCost, allocatedGas)); const nestedContext = context.createNestedContractCallContext( callAddress.toFr(), @@ -92,31 +92,35 @@ abstract class ExternalCall extends Instruction { context.machineState.incrementPc(); } - public get type(): 'CALL' | 'STATICCALL' { - const type = super.type; - if (type !== 'CALL' && type !== 'STATICCALL') { - throw new Error(`Invalid type for ExternalCall instruction: ${type}`); - } - return type; - } + public abstract get type(): 'CALL' | 'STATICCALL'; protected execute(_context: AvmContext): Promise { - throw new Error(`Unimplemented`); + throw new Error( + `Instructions with dynamic gas calculation run all logic on the main execute function and do not override the internal execute.`, + ); } protected gasCost(): Gas { - throw new Error(`Unimplemented`); + throw new Error(`Instructions with dynamic gas calculation compute gas as part of the main execute function.`); } } export class Call extends ExternalCall { - static type: string = 'CALL'; + static type = 'CALL' as const; static readonly opcode: Opcode = Opcode.CALL; + + public get type() { + return Call.type; + } } export class StaticCall extends ExternalCall { - static type: string = 'STATICCALL'; + static type = 'STATICCALL' as const; static readonly opcode: Opcode = Opcode.STATICCALL; + + public get type() { + return StaticCall.type; + } } export class Return extends Instruction { From edb982ec8e27d3ac1b8335f622ee0f6793fa5593 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Fri, 29 Mar 2024 11:02:02 -0300 Subject: [PATCH 3/3] refactor(avm): Introduce fixed gas cost instruction abstraction --- yarn-project/simulator/src/avm/avm_gas.ts | 3 +- .../simulator/src/avm/avm_simulator.ts | 2 +- .../src/avm/opcodes/accrued_substate.ts | 30 ++++++------- .../simulator/src/avm/opcodes/arithmetic.ts | 12 +++--- .../simulator/src/avm/opcodes/bitwise.ts | 26 ++++++------ .../simulator/src/avm/opcodes/comparators.ts | 14 +++---- .../simulator/src/avm/opcodes/control_flow.ts | 18 ++++---- .../src/avm/opcodes/environment_getters.ts | 6 +-- .../src/avm/opcodes/external_calls.test.ts | 6 +-- .../src/avm/opcodes/external_calls.ts | 23 ++++------ .../src/avm/opcodes/fixed_gas_instruction.ts | 40 ++++++++++++++++++ .../simulator/src/avm/opcodes/hashing.ts | 18 ++++---- .../simulator/src/avm/opcodes/instruction.ts | 27 +----------- .../src/avm/opcodes/instruction_impl.ts | 42 +++++++++++-------- .../simulator/src/avm/opcodes/memory.test.ts | 2 +- .../simulator/src/avm/opcodes/memory.ts | 24 +++++------ .../simulator/src/avm/opcodes/storage.ts | 8 ++-- 17 files changed, 157 insertions(+), 144 deletions(-) create mode 100644 yarn-project/simulator/src/avm/opcodes/fixed_gas_instruction.ts diff --git a/yarn-project/simulator/src/avm/avm_gas.ts b/yarn-project/simulator/src/avm/avm_gas.ts index c02998f4779..496453867cb 100644 --- a/yarn-project/simulator/src/avm/avm_gas.ts +++ b/yarn-project/simulator/src/avm/avm_gas.ts @@ -1,4 +1,5 @@ import { TypeTag } from './avm_memory_types.js'; +import { InstructionExecutionError } from './errors.js'; import { Addressing, AddressingMode } from './opcodes/addressing_mode.js'; import { Opcode } from './serialization/instruction_serialization.js'; @@ -167,6 +168,6 @@ export function getGasCostMultiplierFromTypeTag(tag: TypeTag) { return 32; case TypeTag.INVALID: case TypeTag.UNINITIALIZED: - throw new Error(`Invalid tag type for gas cost multiplier: ${TypeTag[tag]}`); + throw new InstructionExecutionError(`Invalid tag type for gas cost multiplier: ${TypeTag[tag]}`); } } diff --git a/yarn-project/simulator/src/avm/avm_simulator.ts b/yarn-project/simulator/src/avm/avm_simulator.ts index 5404cded450..2118af4a9d0 100644 --- a/yarn-project/simulator/src/avm/avm_simulator.ts +++ b/yarn-project/simulator/src/avm/avm_simulator.ts @@ -64,7 +64,7 @@ export class AvmSimulator { // Execute the instruction. // Normal returns and reverts will return normally here. // "Exceptional halts" will throw. - await instruction.run(this.context); + await instruction.execute(this.context); if (this.context.machineState.pc >= instructions.length) { this.log('Passed end of program!'); diff --git a/yarn-project/simulator/src/avm/opcodes/accrued_substate.ts b/yarn-project/simulator/src/avm/opcodes/accrued_substate.ts index 15535182288..c21d360f896 100644 --- a/yarn-project/simulator/src/avm/opcodes/accrued_substate.ts +++ b/yarn-project/simulator/src/avm/opcodes/accrued_substate.ts @@ -4,10 +4,10 @@ import { InstructionExecutionError } from '../errors.js'; import { NullifierCollisionError } from '../journal/nullifiers.js'; import { Opcode, OperandType } from '../serialization/instruction_serialization.js'; import { Addressing } from './addressing_mode.js'; -import { Instruction } from './instruction.js'; +import { FixedGasInstruction } from './fixed_gas_instruction.js'; import { StaticCallStorageAlterError } from './storage.js'; -export class NoteHashExists extends Instruction { +export class NoteHashExists extends FixedGasInstruction { static type: string = 'NOTEHASHEXISTS'; static readonly opcode: Opcode = Opcode.NOTEHASHEXISTS; // Informs (de)serialization. See Instruction.deserialize. @@ -28,7 +28,7 @@ export class NoteHashExists extends Instruction { super(); } - async execute(context: AvmContext): Promise { + protected async internalExecute(context: AvmContext): Promise { // Note that this instruction accepts any type in memory, and converts to Field. const noteHash = context.machineState.memory.get(this.noteHashOffset).toFr(); const leafIndex = context.machineState.memory.get(this.leafIndexOffset).toFr(); @@ -44,7 +44,7 @@ export class NoteHashExists extends Instruction { } } -export class EmitNoteHash extends Instruction { +export class EmitNoteHash extends FixedGasInstruction { static type: string = 'EMITNOTEHASH'; static readonly opcode: Opcode = Opcode.EMITNOTEHASH; // Informs (de)serialization. See Instruction.deserialize. @@ -54,7 +54,7 @@ export class EmitNoteHash extends Instruction { super(); } - async execute(context: AvmContext): Promise { + protected async internalExecute(context: AvmContext): Promise { if (context.environment.isStaticCall) { throw new StaticCallStorageAlterError(); } @@ -66,7 +66,7 @@ export class EmitNoteHash extends Instruction { } } -export class NullifierExists extends Instruction { +export class NullifierExists extends FixedGasInstruction { static type: string = 'NULLIFIEREXISTS'; static readonly opcode: Opcode = Opcode.NULLIFIEREXISTS; // Informs (de)serialization. See Instruction.deserialize. @@ -76,7 +76,7 @@ export class NullifierExists extends Instruction { super(); } - async execute(context: AvmContext): Promise { + protected async internalExecute(context: AvmContext): Promise { const nullifier = context.machineState.memory.get(this.nullifierOffset).toFr(); const exists = await context.persistableState.checkNullifierExists(context.environment.storageAddress, nullifier); @@ -86,7 +86,7 @@ export class NullifierExists extends Instruction { } } -export class EmitNullifier extends Instruction { +export class EmitNullifier extends FixedGasInstruction { static type: string = 'EMITNULLIFIER'; static readonly opcode: Opcode = Opcode.EMITNULLIFIER; // Informs (de)serialization. See Instruction.deserialize. @@ -96,7 +96,7 @@ export class EmitNullifier extends Instruction { super(); } - async execute(context: AvmContext): Promise { + protected async internalExecute(context: AvmContext): Promise { if (context.environment.isStaticCall) { throw new StaticCallStorageAlterError(); } @@ -119,7 +119,7 @@ export class EmitNullifier extends Instruction { } } -export class L1ToL2MessageExists extends Instruction { +export class L1ToL2MessageExists extends FixedGasInstruction { static type: string = 'L1TOL2MSGEXISTS'; static readonly opcode: Opcode = Opcode.L1TOL2MSGEXISTS; // Informs (de)serialization. See Instruction.deserialize. @@ -140,7 +140,7 @@ export class L1ToL2MessageExists extends Instruction { super(); } - async execute(context: AvmContext): Promise { + protected async internalExecute(context: AvmContext): Promise { const msgHash = context.machineState.memory.get(this.msgHashOffset).toFr(); const msgLeafIndex = context.machineState.memory.get(this.msgLeafIndexOffset).toFr(); const exists = await context.persistableState.checkL1ToL2MessageExists(msgHash, msgLeafIndex); @@ -150,7 +150,7 @@ export class L1ToL2MessageExists extends Instruction { } } -export class EmitUnencryptedLog extends Instruction { +export class EmitUnencryptedLog extends FixedGasInstruction { static type: string = 'EMITUNENCRYPTEDLOG'; static readonly opcode: Opcode = Opcode.EMITUNENCRYPTEDLOG; // Informs (de)serialization. See Instruction.deserialize. @@ -171,7 +171,7 @@ export class EmitUnencryptedLog extends Instruction { super(); } - async execute(context: AvmContext): Promise { + protected async internalExecute(context: AvmContext): Promise { if (context.environment.isStaticCall) { throw new StaticCallStorageAlterError(); } @@ -190,7 +190,7 @@ export class EmitUnencryptedLog extends Instruction { } } -export class SendL2ToL1Message extends Instruction { +export class SendL2ToL1Message extends FixedGasInstruction { static type: string = 'SENDL2TOL1MSG'; static readonly opcode: Opcode = Opcode.SENDL2TOL1MSG; // Informs (de)serialization. See Instruction.deserialize. @@ -200,7 +200,7 @@ export class SendL2ToL1Message extends Instruction { super(); } - async execute(context: AvmContext): Promise { + protected async internalExecute(context: AvmContext): Promise { if (context.environment.isStaticCall) { throw new StaticCallStorageAlterError(); } diff --git a/yarn-project/simulator/src/avm/opcodes/arithmetic.ts b/yarn-project/simulator/src/avm/opcodes/arithmetic.ts index 69ecbe41cd9..69381de3343 100644 --- a/yarn-project/simulator/src/avm/opcodes/arithmetic.ts +++ b/yarn-project/simulator/src/avm/opcodes/arithmetic.ts @@ -8,11 +8,11 @@ import { } from '../avm_gas.js'; import { Field, MemoryValue, TypeTag } from '../avm_memory_types.js'; import { Opcode, OperandType } from '../serialization/instruction_serialization.js'; -import { Instruction } from './instruction.js'; -import { ThreeOperandInstruction } from './instruction_impl.js'; +import { FixedGasInstruction } from './fixed_gas_instruction.js'; +import { ThreeOperandFixedGasInstruction } from './instruction_impl.js'; -export abstract class ThreeOperandArithmeticInstruction extends ThreeOperandInstruction { - async execute(context: AvmContext): Promise { +export abstract class ThreeOperandArithmeticInstruction extends ThreeOperandFixedGasInstruction { + protected async internalExecute(context: AvmContext): Promise { context.machineState.memory.checkTags(this.inTag, this.aOffset, this.bOffset); const a = context.machineState.memory.get(this.aOffset); @@ -71,7 +71,7 @@ export class Div extends ThreeOperandArithmeticInstruction { } } -export class FieldDiv extends Instruction { +export class FieldDiv extends FixedGasInstruction { static type: string = 'FDIV'; static readonly opcode = Opcode.FDIV; @@ -88,7 +88,7 @@ export class FieldDiv extends Instruction { super(); } - async execute(context: AvmContext): Promise { + protected async internalExecute(context: AvmContext): Promise { context.machineState.memory.checkTags(TypeTag.FIELD, this.aOffset, this.bOffset); const a = context.machineState.memory.getAs(this.aOffset); diff --git a/yarn-project/simulator/src/avm/opcodes/bitwise.ts b/yarn-project/simulator/src/avm/opcodes/bitwise.ts index 9161c8bb22c..45e758b2f10 100644 --- a/yarn-project/simulator/src/avm/opcodes/bitwise.ts +++ b/yarn-project/simulator/src/avm/opcodes/bitwise.ts @@ -1,9 +1,9 @@ import type { AvmContext } from '../avm_context.js'; import { IntegralValue } from '../avm_memory_types.js'; import { Opcode } from '../serialization/instruction_serialization.js'; -import { ThreeOperandInstruction, TwoOperandInstruction } from './instruction_impl.js'; +import { ThreeOperandFixedGasInstruction, TwoOperandFixedGasInstruction } from './instruction_impl.js'; -export class And extends ThreeOperandInstruction { +export class And extends ThreeOperandFixedGasInstruction { static readonly type: string = 'AND'; static readonly opcode = Opcode.AND; @@ -11,7 +11,7 @@ export class And extends ThreeOperandInstruction { super(indirect, inTag, aOffset, bOffset, dstOffset); } - async execute(context: AvmContext): Promise { + protected async internalExecute(context: AvmContext): Promise { context.machineState.memory.checkTags(this.inTag, this.aOffset, this.bOffset); const a = context.machineState.memory.getAs(this.aOffset); @@ -24,7 +24,7 @@ export class And extends ThreeOperandInstruction { } } -export class Or extends ThreeOperandInstruction { +export class Or extends ThreeOperandFixedGasInstruction { static readonly type: string = 'OR'; static readonly opcode = Opcode.OR; @@ -32,7 +32,7 @@ export class Or extends ThreeOperandInstruction { super(indirect, inTag, aOffset, bOffset, dstOffset); } - async execute(context: AvmContext): Promise { + protected async internalExecute(context: AvmContext): Promise { context.machineState.memory.checkTags(this.inTag, this.aOffset, this.bOffset); const a = context.machineState.memory.getAs(this.aOffset); @@ -45,7 +45,7 @@ export class Or extends ThreeOperandInstruction { } } -export class Xor extends ThreeOperandInstruction { +export class Xor extends ThreeOperandFixedGasInstruction { static readonly type: string = 'XOR'; static readonly opcode = Opcode.XOR; @@ -53,7 +53,7 @@ export class Xor extends ThreeOperandInstruction { super(indirect, inTag, aOffset, bOffset, dstOffset); } - async execute(context: AvmContext): Promise { + protected async internalExecute(context: AvmContext): Promise { context.machineState.memory.checkTags(this.inTag, this.aOffset, this.bOffset); const a = context.machineState.memory.getAs(this.aOffset); @@ -66,7 +66,7 @@ export class Xor extends ThreeOperandInstruction { } } -export class Not extends TwoOperandInstruction { +export class Not extends TwoOperandFixedGasInstruction { static readonly type: string = 'NOT'; static readonly opcode = Opcode.NOT; @@ -74,7 +74,7 @@ export class Not extends TwoOperandInstruction { super(indirect, inTag, aOffset, dstOffset); } - async execute(context: AvmContext): Promise { + protected async internalExecute(context: AvmContext): Promise { context.machineState.memory.checkTags(this.inTag, this.aOffset); const a = context.machineState.memory.getAs(this.aOffset); @@ -86,7 +86,7 @@ export class Not extends TwoOperandInstruction { } } -export class Shl extends ThreeOperandInstruction { +export class Shl extends ThreeOperandFixedGasInstruction { static readonly type: string = 'SHL'; static readonly opcode = Opcode.SHL; @@ -94,7 +94,7 @@ export class Shl extends ThreeOperandInstruction { super(indirect, inTag, aOffset, bOffset, dstOffset); } - async execute(context: AvmContext): Promise { + protected async internalExecute(context: AvmContext): Promise { context.machineState.memory.checkTags(this.inTag, this.aOffset, this.bOffset); const a = context.machineState.memory.getAs(this.aOffset); @@ -107,7 +107,7 @@ export class Shl extends ThreeOperandInstruction { } } -export class Shr extends ThreeOperandInstruction { +export class Shr extends ThreeOperandFixedGasInstruction { static readonly type: string = 'SHR'; static readonly opcode = Opcode.SHR; @@ -115,7 +115,7 @@ export class Shr extends ThreeOperandInstruction { super(indirect, inTag, aOffset, bOffset, dstOffset); } - async execute(context: AvmContext): Promise { + protected async internalExecute(context: AvmContext): Promise { context.machineState.memory.checkTags(this.inTag, this.aOffset, this.bOffset); const a = context.machineState.memory.getAs(this.aOffset); diff --git a/yarn-project/simulator/src/avm/opcodes/comparators.ts b/yarn-project/simulator/src/avm/opcodes/comparators.ts index 62145da0b84..cef56baf2fe 100644 --- a/yarn-project/simulator/src/avm/opcodes/comparators.ts +++ b/yarn-project/simulator/src/avm/opcodes/comparators.ts @@ -1,9 +1,9 @@ import type { AvmContext } from '../avm_context.js'; import { Uint8 } from '../avm_memory_types.js'; import { Opcode } from '../serialization/instruction_serialization.js'; -import { ThreeOperandInstruction } from './instruction_impl.js'; +import { ThreeOperandFixedGasInstruction } from './instruction_impl.js'; -export class Eq extends ThreeOperandInstruction { +export class Eq extends ThreeOperandFixedGasInstruction { static readonly type: string = 'EQ'; static readonly opcode = Opcode.EQ; @@ -11,7 +11,7 @@ export class Eq extends ThreeOperandInstruction { super(indirect, inTag, aOffset, bOffset, dstOffset); } - async execute(context: AvmContext): Promise { + protected async internalExecute(context: AvmContext): Promise { context.machineState.memory.checkTags(this.inTag, this.aOffset, this.bOffset); const a = context.machineState.memory.get(this.aOffset); @@ -24,7 +24,7 @@ export class Eq extends ThreeOperandInstruction { } } -export class Lt extends ThreeOperandInstruction { +export class Lt extends ThreeOperandFixedGasInstruction { static readonly type: string = 'LT'; static readonly opcode = Opcode.LT; @@ -32,7 +32,7 @@ export class Lt extends ThreeOperandInstruction { super(indirect, inTag, aOffset, bOffset, dstOffset); } - async execute(context: AvmContext): Promise { + protected async internalExecute(context: AvmContext): Promise { context.machineState.memory.checkTags(this.inTag, this.aOffset, this.bOffset); const a = context.machineState.memory.get(this.aOffset); @@ -45,7 +45,7 @@ export class Lt extends ThreeOperandInstruction { } } -export class Lte extends ThreeOperandInstruction { +export class Lte extends ThreeOperandFixedGasInstruction { static readonly type: string = 'LTE'; static readonly opcode = Opcode.LTE; @@ -53,7 +53,7 @@ export class Lte extends ThreeOperandInstruction { super(indirect, inTag, aOffset, bOffset, dstOffset); } - async execute(context: AvmContext): Promise { + protected async internalExecute(context: AvmContext): Promise { context.machineState.memory.checkTags(this.inTag, this.aOffset, this.bOffset); const a = context.machineState.memory.get(this.aOffset); diff --git a/yarn-project/simulator/src/avm/opcodes/control_flow.ts b/yarn-project/simulator/src/avm/opcodes/control_flow.ts index 0ebc96b6ccb..e1b53d5affe 100644 --- a/yarn-project/simulator/src/avm/opcodes/control_flow.ts +++ b/yarn-project/simulator/src/avm/opcodes/control_flow.ts @@ -2,9 +2,9 @@ import type { AvmContext } from '../avm_context.js'; import { IntegralValue } from '../avm_memory_types.js'; import { InstructionExecutionError } from '../errors.js'; import { Opcode, OperandType } from '../serialization/instruction_serialization.js'; -import { Instruction } from './instruction.js'; +import { FixedGasInstruction } from './fixed_gas_instruction.js'; -export class Jump extends Instruction { +export class Jump extends FixedGasInstruction { static type: string = 'JUMP'; static readonly opcode: Opcode = Opcode.JUMP; // Informs (de)serialization. See Instruction.deserialize. @@ -14,12 +14,12 @@ export class Jump extends Instruction { super(); } - async execute(context: AvmContext): Promise { + protected async internalExecute(context: AvmContext): Promise { context.machineState.pc = this.jumpOffset; } } -export class JumpI extends Instruction { +export class JumpI extends FixedGasInstruction { static type: string = 'JUMPI'; static readonly opcode: Opcode = Opcode.JUMPI; @@ -35,7 +35,7 @@ export class JumpI extends Instruction { super(); } - async execute(context: AvmContext): Promise { + protected async internalExecute(context: AvmContext): Promise { const condition = context.machineState.memory.getAs(this.condOffset); // TODO: reconsider this casting @@ -47,7 +47,7 @@ export class JumpI extends Instruction { } } -export class InternalCall extends Instruction { +export class InternalCall extends FixedGasInstruction { static readonly type: string = 'INTERNALCALL'; static readonly opcode: Opcode = Opcode.INTERNALCALL; // Informs (de)serialization. See Instruction.deserialize. @@ -57,13 +57,13 @@ export class InternalCall extends Instruction { super(); } - async execute(context: AvmContext): Promise { + protected async internalExecute(context: AvmContext): Promise { context.machineState.internalCallStack.push(context.machineState.pc + 1); context.machineState.pc = this.loc; } } -export class InternalReturn extends Instruction { +export class InternalReturn extends FixedGasInstruction { static readonly type: string = 'INTERNALRETURN'; static readonly opcode: Opcode = Opcode.INTERNALRETURN; // Informs (de)serialization. See Instruction.deserialize. @@ -73,7 +73,7 @@ export class InternalReturn extends Instruction { super(); } - async execute(context: AvmContext): Promise { + protected async internalExecute(context: AvmContext): Promise { const jumpOffset = context.machineState.internalCallStack.pop(); if (jumpOffset === undefined) { throw new InstructionExecutionError('Internal call stack empty!'); diff --git a/yarn-project/simulator/src/avm/opcodes/environment_getters.ts b/yarn-project/simulator/src/avm/opcodes/environment_getters.ts index f2ddd5d67f6..357d6cd6406 100644 --- a/yarn-project/simulator/src/avm/opcodes/environment_getters.ts +++ b/yarn-project/simulator/src/avm/opcodes/environment_getters.ts @@ -4,9 +4,9 @@ import type { AvmContext } from '../avm_context.js'; import type { AvmExecutionEnvironment } from '../avm_execution_environment.js'; import { Field } from '../avm_memory_types.js'; import { Opcode, OperandType } from '../serialization/instruction_serialization.js'; -import { Instruction } from './instruction.js'; +import { FixedGasInstruction } from './fixed_gas_instruction.js'; -abstract class GetterInstruction extends Instruction { +abstract class GetterInstruction extends FixedGasInstruction { // Informs (de)serialization. See Instruction.deserialize. static readonly wireFormat: OperandType[] = [OperandType.UINT8, OperandType.UINT8, OperandType.UINT32]; @@ -14,7 +14,7 @@ abstract class GetterInstruction extends Instruction { super(); } - async execute(context: AvmContext): Promise { + protected async internalExecute(context: AvmContext): Promise { const res = new Field(this.getIt(context.environment)); context.machineState.memory.set(this.dstOffset, res); context.machineState.incrementPc(); 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 89fec377cd3..f288769c8dd 100644 --- a/yarn-project/simulator/src/avm/opcodes/external_calls.test.ts +++ b/yarn-project/simulator/src/avm/opcodes/external_calls.test.ts @@ -105,7 +105,7 @@ describe('External Calls', () => { successOffset, /*temporaryFunctionSelectorOffset=*/ 0, ); - await instruction.run(context); + await instruction.execute(context); const successValue = context.machineState.memory.get(successOffset); expect(successValue).toEqual(new Uint8(1n)); @@ -166,7 +166,7 @@ describe('External Calls', () => { /*temporaryFunctionSelectorOffset=*/ 0, ); - await expect(() => instruction.run(context)).rejects.toThrow(/Not enough.*gas left/i); + await expect(() => instruction.execute(context)).rejects.toThrow(/Not enough.*gas left/i); }); }); @@ -244,7 +244,7 @@ describe('External Calls', () => { successOffset, /*temporaryFunctionSelectorOffset=*/ 0, ); - await instruction.run(context); + await instruction.execute(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 da090f7d179..3f18ad00f06 100644 --- a/yarn-project/simulator/src/avm/opcodes/external_calls.ts +++ b/yarn-project/simulator/src/avm/opcodes/external_calls.ts @@ -1,11 +1,12 @@ import { FunctionSelector } from '@aztec/circuits.js'; import type { AvmContext } from '../avm_context.js'; -import { Gas, gasLeftToGas, getCostFromIndirectAccess, getFixedGasCost, sumGas } from '../avm_gas.js'; +import { 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 { FixedGasInstruction } from './fixed_gas_instruction.js'; import { Instruction } from './instruction.js'; abstract class ExternalCall extends Instruction { @@ -41,7 +42,7 @@ abstract class ExternalCall extends Instruction { super(); } - async run(context: AvmContext): Promise { + 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, @@ -93,16 +94,6 @@ abstract class ExternalCall extends Instruction { } public abstract get type(): 'CALL' | 'STATICCALL'; - - 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.`, - ); - } - - protected gasCost(): Gas { - throw new Error(`Instructions with dynamic gas calculation compute gas as part of the main execute function.`); - } } export class Call extends ExternalCall { @@ -123,7 +114,7 @@ export class StaticCall extends ExternalCall { } } -export class Return extends Instruction { +export class Return extends FixedGasInstruction { static type: string = 'RETURN'; static readonly opcode: Opcode = Opcode.RETURN; // Informs (de)serialization. See Instruction.deserialize. @@ -138,7 +129,7 @@ export class Return extends Instruction { super(); } - async execute(context: AvmContext): Promise { + protected async internalExecute(context: AvmContext): Promise { const [returnOffset] = Addressing.fromWire(this.indirect).resolve([this.returnOffset], context.machineState.memory); const output = context.machineState.memory.getSlice(returnOffset, this.copySize).map(word => word.toFr()); @@ -147,7 +138,7 @@ export class Return extends Instruction { } } -export class Revert extends Instruction { +export class Revert extends FixedGasInstruction { static type: string = 'REVERT'; static readonly opcode: Opcode = Opcode.REVERT; // Informs (de)serialization. See Instruction.deserialize. @@ -162,7 +153,7 @@ export class Revert extends Instruction { super(); } - async execute(context: AvmContext): Promise { + protected async internalExecute(context: AvmContext): Promise { const [returnOffset] = Addressing.fromWire(this.indirect).resolve([this.returnOffset], context.machineState.memory); const output = context.machineState.memory.getSlice(returnOffset, this.retSize).map(word => word.toFr()); diff --git a/yarn-project/simulator/src/avm/opcodes/fixed_gas_instruction.ts b/yarn-project/simulator/src/avm/opcodes/fixed_gas_instruction.ts new file mode 100644 index 00000000000..34bac461ff6 --- /dev/null +++ b/yarn-project/simulator/src/avm/opcodes/fixed_gas_instruction.ts @@ -0,0 +1,40 @@ +import type { AvmContext } from '../avm_context.js'; +import { DynamicGasCost, Gas, GasCosts } from '../avm_gas.js'; +import { Instruction } from './instruction.js'; + +/** + * Base class for AVM instructions with a fixed gas cost or computed directly from its operands without requiring memory access. + * Implements execution by consuming gas and calling the abstract internal execute function. + */ +export abstract class FixedGasInstruction extends Instruction { + /** + * Consumes gas and executes the instruction. + * This is the main entry point for the instruction. + * @param context - The AvmContext in which the instruction executes. + */ + public execute(context: AvmContext): Promise { + context.machineState.consumeGas(this.gasCost()); + return this.internalExecute(context); + } + + /** + * 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(): Gas { + const gasCost = GasCosts[this.opcode]; + if (gasCost === DynamicGasCost) { + throw new Error(`Instruction ${this.type} must define its own gas cost`); + } + return gasCost; + } + + /** + * Execute the instruction. + * Instruction sub-classes must implement this. + * As an AvmContext executes its contract code, it calls this function for + * each instruction until the machine state signals "halted". + * @param context - The AvmContext in which the instruction executes. + */ + protected abstract internalExecute(context: AvmContext): Promise; +} diff --git a/yarn-project/simulator/src/avm/opcodes/hashing.ts b/yarn-project/simulator/src/avm/opcodes/hashing.ts index a7e54e539e6..b563a7e8439 100644 --- a/yarn-project/simulator/src/avm/opcodes/hashing.ts +++ b/yarn-project/simulator/src/avm/opcodes/hashing.ts @@ -5,9 +5,9 @@ import { AvmContext } from '../avm_context.js'; import { Field } from '../avm_memory_types.js'; import { Opcode, OperandType } from '../serialization/instruction_serialization.js'; import { Addressing } from './addressing_mode.js'; -import { Instruction } from './instruction.js'; +import { FixedGasInstruction } from './fixed_gas_instruction.js'; -export class Poseidon2 extends Instruction { +export class Poseidon2 extends FixedGasInstruction { static type: string = 'POSEIDON2'; static readonly opcode: Opcode = Opcode.POSEIDON; @@ -29,7 +29,7 @@ export class Poseidon2 extends Instruction { super(); } - async execute(context: AvmContext): Promise { + protected async internalExecute(context: AvmContext): Promise { // We hash a set of field elements const [dstOffset, messageOffset] = Addressing.fromWire(this.indirect).resolve( [this.dstOffset, this.messageOffset], @@ -46,7 +46,7 @@ export class Poseidon2 extends Instruction { } } -export class Keccak extends Instruction { +export class Keccak extends FixedGasInstruction { static type: string = 'KECCAK'; static readonly opcode: Opcode = Opcode.KECCAK; @@ -69,7 +69,7 @@ export class Keccak extends Instruction { } // Note hash output is 32 bytes, so takes up two fields - async execute(context: AvmContext): Promise { + protected async internalExecute(context: AvmContext): Promise { // We hash a set of field elements const [dstOffset, messageOffset] = Addressing.fromWire(this.indirect).resolve( [this.dstOffset, this.messageOffset], @@ -91,7 +91,7 @@ export class Keccak extends Instruction { } } -export class Sha256 extends Instruction { +export class Sha256 extends FixedGasInstruction { static type: string = 'SHA256'; static readonly opcode: Opcode = Opcode.SHA256; @@ -114,7 +114,7 @@ export class Sha256 extends Instruction { } // Note hash output is 32 bytes, so takes up two fields - async execute(context: AvmContext): Promise { + protected async internalExecute(context: AvmContext): Promise { const [dstOffset, messageOffset] = Addressing.fromWire(this.indirect).resolve( [this.dstOffset, this.messageOffset], context.machineState.memory, @@ -136,7 +136,7 @@ export class Sha256 extends Instruction { } } -export class Pedersen extends Instruction { +export class Pedersen extends FixedGasInstruction { static type: string = 'PEDERSEN'; static readonly opcode: Opcode = Opcode.PEDERSEN; @@ -158,7 +158,7 @@ export class Pedersen extends Instruction { super(); } - async execute(context: AvmContext): Promise { + protected async internalExecute(context: AvmContext): Promise { const [dstOffset, messageOffset, messageSizeOffset] = Addressing.fromWire(this.indirect).resolve( [this.dstOffset, this.messageOffset, this.messageSizeOffset], context.machineState.memory, diff --git a/yarn-project/simulator/src/avm/opcodes/instruction.ts b/yarn-project/simulator/src/avm/opcodes/instruction.ts index 9f785abeb28..3aa62898efd 100644 --- a/yarn-project/simulator/src/avm/opcodes/instruction.ts +++ b/yarn-project/simulator/src/avm/opcodes/instruction.ts @@ -1,7 +1,6 @@ import { strict as assert } from 'assert'; import type { AvmContext } from '../avm_context.js'; -import { DynamicGasCost, Gas, GasCosts } from '../avm_gas.js'; import { BufferCursor } from '../serialization/buffer_cursor.js'; import { Opcode, OperandType, deserialize, serialize } from '../serialization/instruction_serialization.js'; @@ -20,31 +19,7 @@ export abstract class Instruction { * This is the main entry point for the instruction. * @param context - The AvmContext in which the instruction executes. */ - public run(context: AvmContext): Promise { - context.machineState.consumeGas(this.gasCost()); - return this.execute(context); - } - - /** - * 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(): Gas { - const gasCost = GasCosts[this.opcode]; - if (gasCost === DynamicGasCost) { - throw new Error(`Instruction ${this.type} must define its own gas cost`); - } - return gasCost; - } - - /** - * Execute the instruction. - * Instruction sub-classes must implement this. - * As an AvmContext executes its contract code, it calls this function for - * each instruction until the machine state signals "halted". - * @param context - The AvmContext in which the instruction executes. - */ - protected abstract execute(context: AvmContext): Promise; + public abstract execute(context: AvmContext): Promise; /** * Generate a string representation of the instruction including diff --git a/yarn-project/simulator/src/avm/opcodes/instruction_impl.ts b/yarn-project/simulator/src/avm/opcodes/instruction_impl.ts index fb8b8621a71..e4670851f97 100644 --- a/yarn-project/simulator/src/avm/opcodes/instruction_impl.ts +++ b/yarn-project/simulator/src/avm/opcodes/instruction_impl.ts @@ -1,19 +1,32 @@ import { OperandType } from '../serialization/instruction_serialization.js'; -import { Instruction } from './instruction.js'; +import { FixedGasInstruction } from './fixed_gas_instruction.js'; + +/** Wire format that informs deserialization for instructions with two operands. */ +export const TwoOperandWireFormat = [ + OperandType.UINT8, + OperandType.UINT8, + OperandType.UINT8, + OperandType.UINT32, + OperandType.UINT32, +]; + +/** Wire format that informs deserialization for instructions with three operands. */ +export const ThreeOperandWireFormat = [ + OperandType.UINT8, + OperandType.UINT8, + OperandType.UINT8, + OperandType.UINT32, + OperandType.UINT32, + OperandType.UINT32, +]; /** * Covers (de)serialization for an instruction with: * indirect, inTag, and two UINT32s. */ -export abstract class TwoOperandInstruction extends Instruction { +export abstract class TwoOperandFixedGasInstruction extends FixedGasInstruction { // Informs (de)serialization. See Instruction.deserialize. - static readonly wireFormat: OperandType[] = [ - OperandType.UINT8, - OperandType.UINT8, - OperandType.UINT8, - OperandType.UINT32, - OperandType.UINT32, - ]; + static readonly wireFormat: OperandType[] = TwoOperandWireFormat; constructor( protected indirect: number, @@ -29,16 +42,9 @@ export abstract class TwoOperandInstruction extends Instruction { * Covers (de)serialization for an instruction with: * indirect, inTag, and three UINT32s. */ -export abstract class ThreeOperandInstruction extends Instruction { +export abstract class ThreeOperandFixedGasInstruction extends FixedGasInstruction { // Informs (de)serialization. See Instruction.deserialize. - static readonly wireFormat: OperandType[] = [ - OperandType.UINT8, - OperandType.UINT8, - OperandType.UINT8, - OperandType.UINT32, - OperandType.UINT32, - OperandType.UINT32, - ]; + static readonly wireFormat: OperandType[] = ThreeOperandWireFormat; constructor( protected indirect: number, diff --git a/yarn-project/simulator/src/avm/opcodes/memory.test.ts b/yarn-project/simulator/src/avm/opcodes/memory.test.ts index 0b8102f98c7..ecdfdb76bdf 100644 --- a/yarn-project/simulator/src/avm/opcodes/memory.test.ts +++ b/yarn-project/simulator/src/avm/opcodes/memory.test.ts @@ -135,7 +135,7 @@ describe('Memory instructions', () => { it('should throw if tag is FIELD, UNINITIALIZED, INVALID', async () => { for (const tag of [TypeTag.FIELD, TypeTag.UNINITIALIZED, TypeTag.INVALID]) { await expect( - new Set(/*indirect=*/ 0, /*inTag=*/ tag, /*value=*/ 1234n, /*offset=*/ 1).execute(context), + async () => await new Set(/*indirect=*/ 0, /*inTag=*/ tag, /*value=*/ 1234n, /*offset=*/ 1).execute(context), ).rejects.toThrow(InstructionExecutionError); } }); diff --git a/yarn-project/simulator/src/avm/opcodes/memory.ts b/yarn-project/simulator/src/avm/opcodes/memory.ts index 48994210d59..0b9e1951619 100644 --- a/yarn-project/simulator/src/avm/opcodes/memory.ts +++ b/yarn-project/simulator/src/avm/opcodes/memory.ts @@ -5,8 +5,8 @@ import { InstructionExecutionError } from '../errors.js'; import { BufferCursor } from '../serialization/buffer_cursor.js'; import { Opcode, OperandType, deserialize, serialize } from '../serialization/instruction_serialization.js'; import { Addressing } from './addressing_mode.js'; -import { Instruction } from './instruction.js'; -import { TwoOperandInstruction } from './instruction_impl.js'; +import { FixedGasInstruction } from './fixed_gas_instruction.js'; +import { TwoOperandFixedGasInstruction } from './instruction_impl.js'; const TAG_TO_OPERAND_TYPE = new Map([ [TypeTag.UINT8, OperandType.UINT8], @@ -25,7 +25,7 @@ function getOperandTypeFromInTag(inTag: number | bigint): OperandType { return tagOperandType; } -export class Set extends Instruction { +export class Set extends FixedGasInstruction { static readonly type: string = 'SET'; static readonly opcode: Opcode = Opcode.SET; @@ -69,7 +69,7 @@ export class Set extends Instruction { return new this(...args); } - async execute(context: AvmContext): Promise { + protected async internalExecute(context: AvmContext): Promise { // Per the YP, the tag cannot be a field. if ([TypeTag.FIELD, TypeTag.UNINITIALIZED, TypeTag.INVALID].includes(this.inTag)) { throw new InstructionExecutionError(`Invalid tag ${TypeTag[this.inTag]} for SET.`); @@ -86,7 +86,7 @@ export class Set extends Instruction { } } -export class CMov extends Instruction { +export class CMov extends FixedGasInstruction { static readonly type: string = 'CMOV'; static readonly opcode: Opcode = Opcode.CMOV; // Informs (de)serialization. See Instruction.deserialize. @@ -109,7 +109,7 @@ export class CMov extends Instruction { super(); } - async execute(context: AvmContext): Promise { + protected async internalExecute(context: AvmContext): Promise { const a = context.machineState.memory.get(this.aOffset); const b = context.machineState.memory.get(this.bOffset); const cond = context.machineState.memory.get(this.condOffset); @@ -121,7 +121,7 @@ export class CMov extends Instruction { } } -export class Cast extends TwoOperandInstruction { +export class Cast extends TwoOperandFixedGasInstruction { static readonly type: string = 'CAST'; static readonly opcode = Opcode.CAST; @@ -129,7 +129,7 @@ export class Cast extends TwoOperandInstruction { super(indirect, dstTag, aOffset, dstOffset); } - async execute(context: AvmContext): Promise { + protected async internalExecute(context: AvmContext): Promise { const a = context.machineState.memory.get(this.aOffset); // TODO: consider not using toBigInt() @@ -142,7 +142,7 @@ export class Cast extends TwoOperandInstruction { } } -export class Mov extends Instruction { +export class Mov extends FixedGasInstruction { static readonly type: string = 'MOV'; static readonly opcode: Opcode = Opcode.MOV; // Informs (de)serialization. See Instruction.deserialize. @@ -157,7 +157,7 @@ export class Mov extends Instruction { super(); } - async execute(context: AvmContext): Promise { + protected async internalExecute(context: AvmContext): Promise { const [srcOffset, dstOffset] = Addressing.fromWire(this.indirect).resolve( [this.srcOffset, this.dstOffset], context.machineState.memory, @@ -171,7 +171,7 @@ export class Mov extends Instruction { } } -export class CalldataCopy extends Instruction { +export class CalldataCopy extends FixedGasInstruction { static readonly type: string = 'CALLDATACOPY'; static readonly opcode: Opcode = Opcode.CALLDATACOPY; // Informs (de)serialization. See Instruction.deserialize. @@ -187,7 +187,7 @@ export class CalldataCopy extends Instruction { super(); } - async execute(context: AvmContext): Promise { + protected async internalExecute(context: AvmContext): Promise { const [dstOffset] = Addressing.fromWire(this.indirect).resolve([this.dstOffset], context.machineState.memory); const transformedData = context.environment.calldata diff --git a/yarn-project/simulator/src/avm/opcodes/storage.ts b/yarn-project/simulator/src/avm/opcodes/storage.ts index 3ca3bfa19aa..4b33102e1c6 100644 --- a/yarn-project/simulator/src/avm/opcodes/storage.ts +++ b/yarn-project/simulator/src/avm/opcodes/storage.ts @@ -5,9 +5,9 @@ import { Field } from '../avm_memory_types.js'; import { InstructionExecutionError } from '../errors.js'; import { Opcode, OperandType } from '../serialization/instruction_serialization.js'; import { Addressing } from './addressing_mode.js'; -import { Instruction } from './instruction.js'; +import { FixedGasInstruction } from './fixed_gas_instruction.js'; -abstract class BaseStorageInstruction extends Instruction { +abstract class BaseStorageInstruction extends FixedGasInstruction { // Informs (de)serialization. See Instruction.deserialize. public static readonly wireFormat: OperandType[] = [ OperandType.UINT8, @@ -35,7 +35,7 @@ export class SStore extends BaseStorageInstruction { super(indirect, srcOffset, srcSize, slotOffset); } - async execute(context: AvmContext): Promise { + protected async internalExecute(context: AvmContext): Promise { if (context.environment.isStaticCall) { throw new StaticCallStorageAlterError(); } @@ -65,7 +65,7 @@ export class SLoad extends BaseStorageInstruction { super(indirect, slotOffset, size, dstOffset); } - async execute(context: AvmContext): Promise { + protected async internalExecute(context: AvmContext): Promise { const [aOffset, size, bOffset] = Addressing.fromWire(this.indirect).resolve( [this.aOffset, this.size, this.bOffset], context.machineState.memory,