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