From 4884d833eaf6d8f2e79c87c72ce9a58c99524a69 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Tue, 26 Mar 2024 09:16:53 -0300 Subject: [PATCH] feat(avm): Track gas usage in AVM simulator (#5438) Adds gas tracking for AVM instructions to the simulator. For now, every instruction consumes the same amount of gas, and executions start with an arbitrary amount of gas. If gas is exhausted, it triggers an exceptional halt as defined in the yp. --- .../simulator/src/avm/avm_gas_cost.ts | 94 +++++++++++++++++++ .../simulator/src/avm/avm_machine_state.ts | 35 ++++++- .../simulator/src/avm/avm_simulator.test.ts | 35 +++++-- .../simulator/src/avm/avm_simulator.ts | 3 +- yarn-project/simulator/src/avm/errors.ts | 8 ++ .../simulator/src/avm/fixtures/index.ts | 8 +- .../simulator/src/avm/opcodes/instruction.ts | 47 +++++++++- yarn-project/simulator/src/public/executor.ts | 4 +- 8 files changed, 218 insertions(+), 16 deletions(-) create mode 100644 yarn-project/simulator/src/avm/avm_gas_cost.ts 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); }