diff --git a/yarn-project/simulator/src/avm/avm_gas_cost.test.ts b/yarn-project/simulator/src/avm/avm_gas_cost.test.ts new file mode 100644 index 000000000000..c2c9cd450b7a --- /dev/null +++ b/yarn-project/simulator/src/avm/avm_gas_cost.test.ts @@ -0,0 +1,34 @@ +import { TypeTag } from './avm_memory_types.js'; +import { AvmSimulator } from './avm_simulator.js'; +import { initContext } from './fixtures/index.js'; +import { Add, CalldataCopy, Div, Mul, Set as SetInstruction, Sub } from './opcodes/index.js'; +import { encodeToBytecode } from './serialization/bytecode_serialization.js'; + +describe('AVM simulator: dynamic gas costs per instruction', () => { + it.each([ + [new SetInstruction(/*indirect=*/ 0, /*inTag=*/ TypeTag.UINT8, /*value=*/ 1, /*dstOffset=*/ 0), [100, 0, 0]], + [new SetInstruction(/*indirect=*/ 0, /*inTag=*/ TypeTag.UINT32, /*value=*/ 1, /*dstOffset=*/ 0), [400, 0, 0]], + [new CalldataCopy(/*indirect=*/ 0, /*cdOffset=*/ TypeTag.UINT8, /*copySize=*/ 1, /*dstOffset=*/ 0), [10, 0, 0]], + [new CalldataCopy(/*indirect=*/ 0, /*cdOffset=*/ TypeTag.UINT8, /*copySize=*/ 5, /*dstOffset=*/ 0), [50, 0, 0]], + [new Add(/*indirect=*/ 0, /*inTag=*/ TypeTag.UINT8, /*aOffset=*/ 1, /*bOffset=*/ 2, /*dstOffset=*/ 3), [10, 0, 0]], + [new Add(/*indirect=*/ 0, /*inTag=*/ TypeTag.UINT32, /*aOffset=*/ 1, /*bOffset=*/ 2, /*dstOffset=*/ 3), [40, 0, 0]], + [new Add(/*indirect=*/ 3, /*inTag=*/ TypeTag.UINT8, /*aOffset=*/ 1, /*bOffset=*/ 2, /*dstOffset=*/ 3), [20, 0, 0]], + [new Sub(/*indirect=*/ 3, /*inTag=*/ TypeTag.UINT8, /*aOffset=*/ 1, /*bOffset=*/ 2, /*dstOffset=*/ 3), [20, 0, 0]], + [new Mul(/*indirect=*/ 3, /*inTag=*/ TypeTag.UINT8, /*aOffset=*/ 1, /*bOffset=*/ 2, /*dstOffset=*/ 3), [20, 0, 0]], + [new Div(/*indirect=*/ 3, /*inTag=*/ TypeTag.UINT8, /*aOffset=*/ 1, /*bOffset=*/ 2, /*dstOffset=*/ 3), [20, 0, 0]], + ] as const)('computes gas cost for %s', async (instruction, [l2GasCost, l1GasCost, daGasCost]) => { + const bytecode = encodeToBytecode([instruction]); + const context = initContext(); + const { + l2GasLeft: initialL2GasLeft, + daGasLeft: initialDaGasLeft, + l1GasLeft: initialL1GasLeft, + } = context.machineState; + + await new AvmSimulator(context).executeBytecode(bytecode); + + expect(initialL2GasLeft - context.machineState.l2GasLeft).toEqual(l2GasCost); + expect(initialL1GasLeft - context.machineState.l1GasLeft).toEqual(l1GasCost); + expect(initialDaGasLeft - context.machineState.daGasLeft).toEqual(daGasCost); + }); +}); diff --git a/yarn-project/simulator/src/avm/avm_gas_cost.ts b/yarn-project/simulator/src/avm/avm_gas_cost.ts index 129f519691c2..69f80e2dfe7b 100644 --- a/yarn-project/simulator/src/avm/avm_gas_cost.ts +++ b/yarn-project/simulator/src/avm/avm_gas_cost.ts @@ -1,3 +1,4 @@ +import { TypeTag } from './avm_memory_types.js'; import { Opcode } from './serialization/instruction_serialization.js'; /** Gas cost in L1, L2, and DA for a given instruction. */ @@ -7,6 +8,11 @@ export type GasCost = { daGas: number; }; +/** Creates a new instance with all values set to zero except the ones set. */ +export function makeGasCost(gasCost: Partial) { + return { ...EmptyGasCost, ...gasCost }; +} + /** Gas cost of zero across all gas dimensions. */ export const EmptyGasCost = { l1Gas: 0, @@ -14,18 +20,21 @@ export const EmptyGasCost = { daGas: 0, }; -/** Dimensions of gas usage: L1, L2, and DA */ +/** Dimensions of gas usage: L1, L2, and DA. */ export const GasDimensions = ['l1Gas', 'l2Gas', 'daGas'] as const; +/** Null object to represent a gas cost that's dynamic instead of fixed for a given instruction. */ +export const DynamicGasCost = Symbol('DynamicGasCost'); + /** 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, +export const GasCosts = { + [Opcode.ADD]: DynamicGasCost, + [Opcode.SUB]: DynamicGasCost, + [Opcode.MUL]: DynamicGasCost, + [Opcode.DIV]: DynamicGasCost, [Opcode.FDIV]: TemporaryDefaultGasCost, [Opcode.EQ]: TemporaryDefaultGasCost, [Opcode.LT]: TemporaryDefaultGasCost, @@ -55,7 +64,7 @@ export const GasCosts: Record = { [Opcode.BLOCKL1GASLIMIT]: TemporaryDefaultGasCost, [Opcode.BLOCKL2GASLIMIT]: TemporaryDefaultGasCost, [Opcode.BLOCKDAGASLIMIT]: TemporaryDefaultGasCost, - [Opcode.CALLDATACOPY]: TemporaryDefaultGasCost, + [Opcode.CALLDATACOPY]: DynamicGasCost, // Gas [Opcode.L1GASLEFT]: TemporaryDefaultGasCost, [Opcode.L2GASLEFT]: TemporaryDefaultGasCost, @@ -66,7 +75,7 @@ export const GasCosts: Record = { [Opcode.INTERNALCALL]: TemporaryDefaultGasCost, [Opcode.INTERNALRETURN]: TemporaryDefaultGasCost, // Memory - [Opcode.SET]: TemporaryDefaultGasCost, + [Opcode.SET]: DynamicGasCost, [Opcode.MOV]: TemporaryDefaultGasCost, [Opcode.CMOV]: TemporaryDefaultGasCost, // World state @@ -91,4 +100,33 @@ export const GasCosts: Record = { [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 +} as const; + +/** 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, }; + +/** Returns a multiplier based on the size of the type represented by the tag. Throws on uninitialized or invalid. */ +export function getGasCostMultiplierFromTypeTag(tag: TypeTag) { + switch (tag) { + case TypeTag.UINT8: + return 1; + case TypeTag.UINT16: + return 2; + case TypeTag.UINT32: + return 4; + case TypeTag.UINT64: + return 8; + case TypeTag.UINT128: + return 16; + case TypeTag.FIELD: + return 32; + case TypeTag.INVALID: + case TypeTag.UNINITIALIZED: + throw new Error(`Invalid tag type for gas cost multiplier: ${TypeTag[tag]}`); + } +} diff --git a/yarn-project/simulator/src/avm/avm_simulator.test.ts b/yarn-project/simulator/src/avm/avm_simulator.test.ts index 77294b8f27a4..f1932f03be94 100644 --- a/yarn-project/simulator/src/avm/avm_simulator.test.ts +++ b/yarn-project/simulator/src/avm/avm_simulator.test.ts @@ -45,7 +45,7 @@ describe('AVM simulator: injected bytecode', () => { expect(results.reverted).toBe(false); expect(results.output).toEqual([new Fr(3)]); - expect(context.machineState.l2GasLeft).toEqual(initialL2GasLeft - 30); + expect(context.machineState.l2GasLeft).toEqual(initialL2GasLeft - 350); }); it('Should halt if runs out of gas', async () => { diff --git a/yarn-project/simulator/src/avm/fixtures/index.ts b/yarn-project/simulator/src/avm/fixtures/index.ts index ed81c4fde166..88fc65a2de2e 100644 --- a/yarn-project/simulator/src/avm/fixtures/index.ts +++ b/yarn-project/simulator/src/avm/fixtures/index.ts @@ -89,9 +89,9 @@ export function initGlobalVariables(overrides?: Partial): Globa */ export function initMachineState(overrides?: Partial): AvmMachineState { return AvmMachineState.fromState({ - l1GasLeft: overrides?.l1GasLeft ?? 1e6, - l2GasLeft: overrides?.l2GasLeft ?? 1e6, - daGasLeft: overrides?.daGasLeft ?? 1e6, + l1GasLeft: overrides?.l1GasLeft ?? 100e6, + l2GasLeft: overrides?.l2GasLeft ?? 100e6, + daGasLeft: overrides?.daGasLeft ?? 100e6, }); } diff --git a/yarn-project/simulator/src/avm/opcodes/addressing_mode.ts b/yarn-project/simulator/src/avm/opcodes/addressing_mode.ts index 280d15e1adee..97e19d9f0834 100644 --- a/yarn-project/simulator/src/avm/opcodes/addressing_mode.ts +++ b/yarn-project/simulator/src/avm/opcodes/addressing_mode.ts @@ -12,7 +12,7 @@ export enum AddressingMode { export class Addressing { public constructor( /** The addressing mode for each operand. The length of this array is the number of operands of the instruction. */ - private readonly modePerOperand: AddressingMode[], + public readonly modePerOperand: AddressingMode[], ) { assert(modePerOperand.length <= 8, 'At most 8 operands are supported'); } diff --git a/yarn-project/simulator/src/avm/opcodes/arithmetic.ts b/yarn-project/simulator/src/avm/opcodes/arithmetic.ts index 87ef9dee15f7..44790a692d87 100644 --- a/yarn-project/simulator/src/avm/opcodes/arithmetic.ts +++ b/yarn-project/simulator/src/avm/opcodes/arithmetic.ts @@ -1,84 +1,71 @@ import type { AvmContext } from '../avm_context.js'; -import { Field, TypeTag } from '../avm_memory_types.js'; +import { GasCost, GasCostConstants, getGasCostMultiplierFromTypeTag, makeGasCost } from '../avm_gas_cost.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'; -export class Add extends ThreeOperandInstruction { - static readonly type: string = 'ADD'; - static readonly opcode = Opcode.ADD; - - constructor(indirect: number, inTag: number, aOffset: number, bOffset: number, dstOffset: number) { - super(indirect, inTag, aOffset, bOffset, dstOffset); - } - +export abstract class ThreeOperandArithmeticInstruction extends ThreeOperandInstruction { async execute(context: AvmContext): Promise { context.machineState.memory.checkTags(this.inTag, this.aOffset, this.bOffset); const a = context.machineState.memory.get(this.aOffset); const b = context.machineState.memory.get(this.bOffset); - const dest = a.add(b); + const dest = this.compute(a, b); context.machineState.memory.set(this.dstOffset, dest); context.machineState.incrementPc(); } -} -export class Sub extends ThreeOperandInstruction { - static readonly type: string = 'SUB'; - static readonly opcode = Opcode.SUB; + protected gasCost(): GasCost { + const indirectCount = Addressing.fromWire(this.indirect).modePerOperand.filter( + mode => mode === AddressingMode.INDIRECT, + ).length; - constructor(indirect: number, inTag: number, aOffset: number, bOffset: number, dstOffset: number) { - super(indirect, inTag, aOffset, bOffset, dstOffset); + const l2Gas = + indirectCount * GasCostConstants.ARITHMETIC_COST_PER_INDIRECT_ACCESS + + getGasCostMultiplierFromTypeTag(this.inTag) * GasCostConstants.ARITHMETIC_COST_PER_BYTE; + return makeGasCost({ l2Gas }); } - async execute(context: AvmContext): Promise { - const a = context.machineState.memory.get(this.aOffset); - const b = context.machineState.memory.get(this.bOffset); + protected abstract compute(a: MemoryValue, b: MemoryValue): MemoryValue; +} - const dest = a.sub(b); - context.machineState.memory.set(this.dstOffset, dest); +export class Add extends ThreeOperandArithmeticInstruction { + static readonly type: string = 'ADD'; + static readonly opcode = Opcode.ADD; - context.machineState.incrementPc(); + protected compute(a: MemoryValue, b: MemoryValue): MemoryValue { + return a.add(b); } } -export class Mul extends ThreeOperandInstruction { - static type: string = 'MUL'; - static readonly opcode = Opcode.MUL; +export class Sub extends ThreeOperandArithmeticInstruction { + static readonly type: string = 'SUB'; + static readonly opcode = Opcode.SUB; - constructor(indirect: number, inTag: number, aOffset: number, bOffset: number, dstOffset: number) { - super(indirect, inTag, aOffset, bOffset, dstOffset); + protected compute(a: MemoryValue, b: MemoryValue): MemoryValue { + return a.sub(b); } +} - async execute(context: AvmContext): Promise { - const a = context.machineState.memory.get(this.aOffset); - const b = context.machineState.memory.get(this.bOffset); - - const dest = a.mul(b); - context.machineState.memory.set(this.dstOffset, dest); +export class Mul extends ThreeOperandArithmeticInstruction { + static type: string = 'MUL'; + static readonly opcode = Opcode.MUL; - context.machineState.incrementPc(); + protected compute(a: MemoryValue, b: MemoryValue): MemoryValue { + return a.mul(b); } } -export class Div extends ThreeOperandInstruction { +export class Div extends ThreeOperandArithmeticInstruction { static type: string = 'DIV'; static readonly opcode = Opcode.DIV; - constructor(indirect: number, inTag: number, aOffset: number, bOffset: number, dstOffset: number) { - super(indirect, inTag, aOffset, bOffset, dstOffset); - } - - async execute(context: AvmContext): Promise { - const a = context.machineState.memory.get(this.aOffset); - const b = context.machineState.memory.get(this.bOffset); - - const dest = a.div(b); - context.machineState.memory.set(this.dstOffset, dest); - - context.machineState.incrementPc(); + protected compute(a: MemoryValue, b: MemoryValue): MemoryValue { + return a.div(b); } } diff --git a/yarn-project/simulator/src/avm/opcodes/instruction.ts b/yarn-project/simulator/src/avm/opcodes/instruction.ts index 97e9f4a961be..2660052c2650 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 { EmptyGasCost, GasCost, GasCosts } from '../avm_gas_cost.js'; +import { DynamicGasCost, GasCost, GasCosts } from '../avm_gas_cost.js'; import { BufferCursor } from '../serialization/buffer_cursor.js'; import { Opcode, OperandType, deserialize, serialize } from '../serialization/instruction_serialization.js'; @@ -30,7 +30,11 @@ export abstract class Instruction { * Instruction sub-classes can override this if their gas cost is not fixed. */ protected gasCost(): GasCost { - return GasCosts[this.opcode] ?? EmptyGasCost; + const gasCost = GasCosts[this.opcode]; + if (gasCost === DynamicGasCost) { + throw new Error(`Instruction ${this.type} must define its own gas cost`); + } + return gasCost; } /** diff --git a/yarn-project/simulator/src/avm/opcodes/memory.ts b/yarn-project/simulator/src/avm/opcodes/memory.ts index 36bd9a189959..87a5ccdd4655 100644 --- a/yarn-project/simulator/src/avm/opcodes/memory.ts +++ b/yarn-project/simulator/src/avm/opcodes/memory.ts @@ -1,4 +1,5 @@ import type { AvmContext } from '../avm_context.js'; +import { GasCost, GasCostConstants, getGasCostMultiplierFromTypeTag, makeGasCost } from '../avm_gas_cost.js'; import { Field, TaggedMemory, TypeTag } from '../avm_memory_types.js'; import { InstructionExecutionError } from '../errors.js'; import { BufferCursor } from '../serialization/buffer_cursor.js'; @@ -79,6 +80,10 @@ export class Set extends Instruction { context.machineState.incrementPc(); } + + protected gasCost(): GasCost { + return makeGasCost({ l2Gas: GasCostConstants.SET_COST_PER_BYTE * getGasCostMultiplierFromTypeTag(this.inTag) }); + } } export class CMov extends Instruction { @@ -193,4 +198,8 @@ export class CalldataCopy extends Instruction { context.machineState.incrementPc(); } + + protected gasCost(): GasCost { + return makeGasCost({ l2Gas: GasCostConstants.CALLDATACOPY_COST_PER_BYTE * this.copySize }); + } }