diff --git a/yarn-project/simulator/src/avm/avm_gas_cost.ts b/yarn-project/simulator/src/avm/avm_gas_cost.ts new file mode 100644 index 00000000000..129f519691c --- /dev/null +++ b/yarn-project/simulator/src/avm/avm_gas_cost.ts @@ -0,0 +1,94 @@ +import { Opcode } from './serialization/instruction_serialization.js'; + +/** Gas cost in L1, L2, and DA for a given instruction. */ +export type GasCost = { + l1Gas: number; + l2Gas: number; + daGas: number; +}; + +/** Gas cost of zero across all gas dimensions. */ +export const EmptyGasCost = { + l1Gas: 0, + l2Gas: 0, + daGas: 0, +}; + +/** Dimensions of gas usage: L1, L2, and DA */ +export const GasDimensions = ['l1Gas', 'l2Gas', 'daGas'] as const; + +/** Temporary default gas cost. We should eventually remove all usage of this variable in favor of actual gas for each opcode. */ +const TemporaryDefaultGasCost = { l1Gas: 0, l2Gas: 10, daGas: 0 }; + +/** Gas costs for each instruction. */ +export const GasCosts: Record = { + [Opcode.ADD]: TemporaryDefaultGasCost, + [Opcode.SUB]: TemporaryDefaultGasCost, + [Opcode.MUL]: TemporaryDefaultGasCost, + [Opcode.DIV]: TemporaryDefaultGasCost, + [Opcode.FDIV]: TemporaryDefaultGasCost, + [Opcode.EQ]: TemporaryDefaultGasCost, + [Opcode.LT]: TemporaryDefaultGasCost, + [Opcode.LTE]: TemporaryDefaultGasCost, + [Opcode.AND]: TemporaryDefaultGasCost, + [Opcode.OR]: TemporaryDefaultGasCost, + [Opcode.XOR]: TemporaryDefaultGasCost, + [Opcode.NOT]: TemporaryDefaultGasCost, + [Opcode.SHL]: TemporaryDefaultGasCost, + [Opcode.SHR]: TemporaryDefaultGasCost, + [Opcode.CAST]: TemporaryDefaultGasCost, + // Execution environment + [Opcode.ADDRESS]: TemporaryDefaultGasCost, + [Opcode.STORAGEADDRESS]: TemporaryDefaultGasCost, + [Opcode.ORIGIN]: TemporaryDefaultGasCost, + [Opcode.SENDER]: TemporaryDefaultGasCost, + [Opcode.PORTAL]: TemporaryDefaultGasCost, + [Opcode.FEEPERL1GAS]: TemporaryDefaultGasCost, + [Opcode.FEEPERL2GAS]: TemporaryDefaultGasCost, + [Opcode.FEEPERDAGAS]: TemporaryDefaultGasCost, + [Opcode.CONTRACTCALLDEPTH]: TemporaryDefaultGasCost, + [Opcode.CHAINID]: TemporaryDefaultGasCost, + [Opcode.VERSION]: TemporaryDefaultGasCost, + [Opcode.BLOCKNUMBER]: TemporaryDefaultGasCost, + [Opcode.TIMESTAMP]: TemporaryDefaultGasCost, + [Opcode.COINBASE]: TemporaryDefaultGasCost, + [Opcode.BLOCKL1GASLIMIT]: TemporaryDefaultGasCost, + [Opcode.BLOCKL2GASLIMIT]: TemporaryDefaultGasCost, + [Opcode.BLOCKDAGASLIMIT]: TemporaryDefaultGasCost, + [Opcode.CALLDATACOPY]: TemporaryDefaultGasCost, + // Gas + [Opcode.L1GASLEFT]: TemporaryDefaultGasCost, + [Opcode.L2GASLEFT]: TemporaryDefaultGasCost, + [Opcode.DAGASLEFT]: TemporaryDefaultGasCost, + // Control flow + [Opcode.JUMP]: TemporaryDefaultGasCost, + [Opcode.JUMPI]: TemporaryDefaultGasCost, + [Opcode.INTERNALCALL]: TemporaryDefaultGasCost, + [Opcode.INTERNALRETURN]: TemporaryDefaultGasCost, + // Memory + [Opcode.SET]: TemporaryDefaultGasCost, + [Opcode.MOV]: TemporaryDefaultGasCost, + [Opcode.CMOV]: TemporaryDefaultGasCost, + // World state + [Opcode.SLOAD]: TemporaryDefaultGasCost, + [Opcode.SSTORE]: TemporaryDefaultGasCost, + [Opcode.NOTEHASHEXISTS]: TemporaryDefaultGasCost, + [Opcode.EMITNOTEHASH]: TemporaryDefaultGasCost, + [Opcode.NULLIFIEREXISTS]: TemporaryDefaultGasCost, + [Opcode.EMITNULLIFIER]: TemporaryDefaultGasCost, + [Opcode.L1TOL2MSGEXISTS]: TemporaryDefaultGasCost, + [Opcode.HEADERMEMBER]: TemporaryDefaultGasCost, + [Opcode.EMITUNENCRYPTEDLOG]: TemporaryDefaultGasCost, + [Opcode.SENDL2TOL1MSG]: TemporaryDefaultGasCost, + // External calls + [Opcode.CALL]: TemporaryDefaultGasCost, + [Opcode.STATICCALL]: TemporaryDefaultGasCost, + [Opcode.DELEGATECALL]: TemporaryDefaultGasCost, + [Opcode.RETURN]: TemporaryDefaultGasCost, + [Opcode.REVERT]: TemporaryDefaultGasCost, + // Gadgets + [Opcode.KECCAK]: TemporaryDefaultGasCost, + [Opcode.POSEIDON]: TemporaryDefaultGasCost, + [Opcode.SHA256]: TemporaryDefaultGasCost, // temp - may be removed, but alot of contracts rely on i: TemporaryDefaultGasCost, + [Opcode.PEDERSEN]: TemporaryDefaultGasCost, // temp - may be removed, but alot of contracts rely on i: TemporaryDefaultGasCost,t +}; diff --git a/yarn-project/simulator/src/avm/avm_machine_state.ts b/yarn-project/simulator/src/avm/avm_machine_state.ts index 6d4dd4e8cb1..5b4888185a6 100644 --- a/yarn-project/simulator/src/avm/avm_machine_state.ts +++ b/yarn-project/simulator/src/avm/avm_machine_state.ts @@ -1,7 +1,9 @@ import { Fr } from '@aztec/circuits.js'; +import { GasCost, GasDimensions } from './avm_gas_cost.js'; import { TaggedMemory } from './avm_memory_types.js'; import { AvmContractCallResults } from './avm_message_call_result.js'; +import { OutOfGasError } from './errors.js'; /** * A few fields of machine state are initialized from AVM session inputs or call instruction arguments @@ -35,7 +37,7 @@ export class AvmMachineState { /** * Signals that execution should end. * AvmContext execution continues executing instructions until the machine state signals "halted" - * */ + */ public halted: boolean = false; /** Signals that execution has reverted normally (this does not cover exceptional halts) */ private reverted: boolean = false; @@ -52,6 +54,28 @@ export class AvmMachineState { return new AvmMachineState(state.l1GasLeft, state.l2GasLeft, state.daGasLeft); } + /** + * Consumes the given gas. + * 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) { + // Assert there is enough gas on every dimension. + const outOfGasDimensions = GasDimensions.filter( + dimension => this[`${dimension}Left`] - (gasCost[dimension] ?? 0) < 0, + ); + // If not, trigger an exceptional halt. + // See https://yp-aztec.netlify.app/docs/public-vm/execution#gas-checks-and-tracking + if (outOfGasDimensions.length > 0) { + this.exceptionalHalt(); + throw new OutOfGasError(outOfGasDimensions); + } + // Otherwise, charge the corresponding gas + for (const dimension of GasDimensions) { + this[`${dimension}Left`] -= gasCost[dimension] ?? 0; + } + } + /** * Most instructions just increment PC before they complete */ @@ -80,6 +104,15 @@ export class AvmMachineState { this.output = output; } + /** + * Flag an exceptional halt. Clears gas left and sets the reverted flag. No output data. + */ + protected exceptionalHalt() { + GasDimensions.forEach(dimension => (this[`${dimension}Left`] = 0)); + this.reverted = true; + this.halted = true; + } + /** * Get a summary of execution results for a halted machine state * @returns summary of execution results diff --git a/yarn-project/simulator/src/avm/avm_simulator.test.ts b/yarn-project/simulator/src/avm/avm_simulator.test.ts index e6d8a083fbe..4f1aa990cd1 100644 --- a/yarn-project/simulator/src/avm/avm_simulator.test.ts +++ b/yarn-project/simulator/src/avm/avm_simulator.test.ts @@ -9,6 +9,7 @@ import { AvmTestContractArtifact } from '@aztec/noir-contracts.js'; import { jest } from '@jest/globals'; import { strict as assert } from 'assert'; +import { AvmMachineState } from './avm_machine_state.js'; import { TypeTag } from './avm_memory_types.js'; import { AvmSimulator } from './avm_simulator.js'; import { @@ -17,26 +18,48 @@ import { initExecutionEnvironment, initGlobalVariables, initL1ToL2MessageOracleInput, + initMachineState, } from './fixtures/index.js'; -import { Add, CalldataCopy, Return } from './opcodes/index.js'; +import { Add, CalldataCopy, Instruction, Return } from './opcodes/index.js'; import { encodeToBytecode } from './serialization/bytecode_serialization.js'; describe('AVM simulator: injected bytecode', () => { - it('Should execute bytecode that performs basic addition', async () => { - const calldata: Fr[] = [new Fr(1), new Fr(2)]; + let calldata: Fr[]; + let ops: Instruction[]; + let bytecode: Buffer; - // Construct bytecode - const bytecode = encodeToBytecode([ + beforeAll(() => { + calldata = [new Fr(1), new Fr(2)]; + ops = [ new CalldataCopy(/*indirect=*/ 0, /*cdOffset=*/ adjustCalldataIndex(0), /*copySize=*/ 2, /*dstOffset=*/ 0), new Add(/*indirect=*/ 0, TypeTag.FIELD, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 2), new Return(/*indirect=*/ 0, /*returnOffset=*/ 2, /*copySize=*/ 1), - ]); + ]; + bytecode = encodeToBytecode(ops); + }); + it('Should execute bytecode that performs basic addition', async () => { const context = initContext({ env: initExecutionEnvironment({ calldata }) }); + const { l2GasLeft: initialL2GasLeft } = AvmMachineState.fromState(context.machineState); const results = await new AvmSimulator(context).executeBytecode(bytecode); + const expectedL2GasUsed = ops.reduce((sum, op) => sum + op.gasCost().l2Gas, 0); expect(results.reverted).toBe(false); expect(results.output).toEqual([new Fr(3)]); + expect(expectedL2GasUsed).toBeGreaterThan(0); + expect(context.machineState.l2GasLeft).toEqual(initialL2GasLeft - expectedL2GasUsed); + }); + + it('Should halt if runs out of gas', async () => { + const context = initContext({ + env: initExecutionEnvironment({ calldata }), + machineState: initMachineState({ l2GasLeft: 5 }), + }); + + const results = await new AvmSimulator(context).executeBytecode(bytecode); + expect(results.reverted).toBe(true); + expect(results.output).toEqual([]); + expect(results.revertReason?.name).toEqual('OutOfGasError'); }); }); diff --git a/yarn-project/simulator/src/avm/avm_simulator.ts b/yarn-project/simulator/src/avm/avm_simulator.ts index 8ef55049780..5404cded450 100644 --- a/yarn-project/simulator/src/avm/avm_simulator.ts +++ b/yarn-project/simulator/src/avm/avm_simulator.ts @@ -50,7 +50,6 @@ export class AvmSimulator { */ public async executeInstructions(instructions: Instruction[]): Promise { assert(instructions.length > 0); - try { // Execute instruction pointed to by the current program counter // continuing until the machine state signifies a halt @@ -65,7 +64,7 @@ export class AvmSimulator { // Execute the instruction. // Normal returns and reverts will return normally here. // "Exceptional halts" will throw. - await instruction.execute(this.context); + await instruction.run(this.context); if (this.context.machineState.pc >= instructions.length) { this.log('Passed end of program!'); diff --git a/yarn-project/simulator/src/avm/errors.ts b/yarn-project/simulator/src/avm/errors.ts index 24d52eea026..cf3b3294227 100644 --- a/yarn-project/simulator/src/avm/errors.ts +++ b/yarn-project/simulator/src/avm/errors.ts @@ -55,3 +55,11 @@ export class TagCheckError extends AvmExecutionError { this.name = 'TagCheckError'; } } + +/** Error thrown when out of gas. */ +export class OutOfGasError extends AvmExecutionError { + constructor(dimensions: string[]) { + super(`Not enough ${dimensions.map(d => d.toUpperCase()).join(', ')} gas left`); + this.name = 'OutOfGasError'; + } +} diff --git a/yarn-project/simulator/src/avm/fixtures/index.ts b/yarn-project/simulator/src/avm/fixtures/index.ts index 2e75d31be4c..ed81c4fde16 100644 --- a/yarn-project/simulator/src/avm/fixtures/index.ts +++ b/yarn-project/simulator/src/avm/fixtures/index.ts @@ -85,13 +85,13 @@ export function initGlobalVariables(overrides?: Partial): Globa } /** - * Create an empty instance of the Machine State where all values are zero, unless overridden in the overrides object + * Create an empty instance of the Machine State where all values are set to a large enough amount, unless overridden in the overrides object */ export function initMachineState(overrides?: Partial): AvmMachineState { return AvmMachineState.fromState({ - l1GasLeft: overrides?.l1GasLeft ?? 0, - l2GasLeft: overrides?.l2GasLeft ?? 0, - daGasLeft: overrides?.daGasLeft ?? 0, + l1GasLeft: overrides?.l1GasLeft ?? 1e6, + l2GasLeft: overrides?.l2GasLeft ?? 1e6, + daGasLeft: overrides?.daGasLeft ?? 1e6, }); } diff --git a/yarn-project/simulator/src/avm/opcodes/instruction.ts b/yarn-project/simulator/src/avm/opcodes/instruction.ts index afe7f6e5209..094e4af20d8 100644 --- a/yarn-project/simulator/src/avm/opcodes/instruction.ts +++ b/yarn-project/simulator/src/avm/opcodes/instruction.ts @@ -1,8 +1,9 @@ import { strict as assert } from 'assert'; import type { AvmContext } from '../avm_context.js'; +import { EmptyGasCost, GasCost, GasCosts } from '../avm_gas_cost.js'; import { BufferCursor } from '../serialization/buffer_cursor.js'; -import { OperandType, deserialize, serialize } from '../serialization/instruction_serialization.js'; +import { Opcode, OperandType, deserialize, serialize } from '../serialization/instruction_serialization.js'; type InstructionConstructor = { new (...args: any[]): Instruction; @@ -14,6 +15,24 @@ type InstructionConstructor = { * It's most important aspects are execute and (de)serialize. */ export abstract class 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 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. + */ + public gasCost(): GasCost { + return GasCosts[this.opcode] ?? EmptyGasCost; + } + /** * Execute the instruction. * Instruction sub-classes must implement this. @@ -21,7 +40,7 @@ export abstract class Instruction { * each instruction until the machine state signals "halted". * @param context - The AvmContext in which the instruction executes. */ - public abstract execute(context: AvmContext): Promise; + protected abstract execute(context: AvmContext): Promise; /** * Generate a string representation of the instruction including @@ -61,4 +80,28 @@ export abstract class Instruction { const args = res.slice(1); // Remove opcode. return new this(...args); } + + /** + * Returns the stringified type of the instruction. + * Instruction sub-classes should have a static `type` property. + */ + public get type(): string { + const type = 'type' in this.constructor && (this.constructor.type as string); + if (!type) { + throw new Error(`Instruction class ${this.constructor.name} does not have a static 'type' property defined.`); + } + return type; + } + + /** + * Returns the opcode of the instruction. + * Instruction sub-classes should have a static `opcode` property. + */ + public get opcode(): Opcode { + const opcode = 'opcode' in this.constructor ? (this.constructor.opcode as Opcode) : undefined; + if (opcode === undefined || Opcode[opcode] === undefined) { + throw new Error(`Instruction class ${this.constructor.name} does not have a static 'opcode' property defined.`); + } + return opcode; + } } diff --git a/yarn-project/simulator/src/public/executor.ts b/yarn-project/simulator/src/public/executor.ts index f78fe90dc1d..c7278091e58 100644 --- a/yarn-project/simulator/src/public/executor.ts +++ b/yarn-project/simulator/src/public/executor.ts @@ -229,13 +229,15 @@ export class PublicExecutor { const hostStorage = new HostStorage(this.stateDb, this.contractsDb, this.commitmentsDb); const worldStateJournal = new AvmPersistableStateManager(hostStorage); const executionEnv = temporaryCreateAvmExecutionEnvironment(execution, globalVariables); - const machineState = new AvmMachineState(0, 0, 0); + // TODO(@spalladino) Load initial gas from the public execution request + const machineState = new AvmMachineState(100_000, 100_000, 100_000); const context = new AvmContext(worldStateJournal, executionEnv, machineState); const simulator = new AvmSimulator(context); const result = await simulator.execute(); const newWorldState = context.persistableState.flush(); + // TODO(@spalladino) Read gas left from machineState and return it return temporaryConvertAvmResults(execution, newWorldState, result); }