Skip to content

Commit

Permalink
feat(avm): Track gas usage in AVM simulator (#5438)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
spalladino authored Mar 26, 2024
1 parent f024751 commit 4884d83
Show file tree
Hide file tree
Showing 8 changed files with 218 additions and 16 deletions.
94 changes: 94 additions & 0 deletions yarn-project/simulator/src/avm/avm_gas_cost.ts
Original file line number Diff line number Diff line change
@@ -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, GasCost> = {
[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
};
35 changes: 34 additions & 1 deletion yarn-project/simulator/src/avm/avm_machine_state.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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;
Expand All @@ -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<GasCost>) {
// 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
*/
Expand Down Expand Up @@ -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
Expand Down
35 changes: 29 additions & 6 deletions yarn-project/simulator/src/avm/avm_simulator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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');
});
});

Expand Down
3 changes: 1 addition & 2 deletions yarn-project/simulator/src/avm/avm_simulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ export class AvmSimulator {
*/
public async executeInstructions(instructions: Instruction[]): Promise<AvmContractCallResults> {
assert(instructions.length > 0);

try {
// Execute instruction pointed to by the current program counter
// continuing until the machine state signifies a halt
Expand All @@ -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!');
Expand Down
8 changes: 8 additions & 0 deletions yarn-project/simulator/src/avm/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}
}
8 changes: 4 additions & 4 deletions yarn-project/simulator/src/avm/fixtures/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,13 @@ export function initGlobalVariables(overrides?: Partial<GlobalVariables>): 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>): 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,
});
}

Expand Down
47 changes: 45 additions & 2 deletions yarn-project/simulator/src/avm/opcodes/instruction.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -14,14 +15,32 @@ 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<void> {
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.
* 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.
*/
public abstract execute(context: AvmContext): Promise<void>;
protected abstract execute(context: AvmContext): Promise<void>;

/**
* Generate a string representation of the instruction including
Expand Down Expand Up @@ -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;
}
}
4 changes: 3 additions & 1 deletion yarn-project/simulator/src/public/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down

0 comments on commit 4884d83

Please sign in to comment.