diff --git a/yarn-project/simulator/src/avm/avm_gas.test.ts b/yarn-project/simulator/src/avm/avm_gas.test.ts index c2c9cd450b7a..0ebb8c78c0ff 100644 --- a/yarn-project/simulator/src/avm/avm_gas.test.ts +++ b/yarn-project/simulator/src/avm/avm_gas.test.ts @@ -6,16 +6,16 @@ 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]], + [new SetInstruction(/*indirect=*/ 0, /*inTag=*/ TypeTag.UINT8, /*value=*/ 1, /*dstOffset=*/ 0), [110, 0, 0]], + [new SetInstruction(/*indirect=*/ 0, /*inTag=*/ TypeTag.UINT32, /*value=*/ 1, /*dstOffset=*/ 0), [110]], + [new CalldataCopy(/*indirect=*/ 0, /*cdOffset=*/ TypeTag.UINT8, /*copySize=*/ 1, /*dstOffset=*/ 0), [110]], + [new CalldataCopy(/*indirect=*/ 0, /*cdOffset=*/ TypeTag.UINT8, /*copySize=*/ 5, /*dstOffset=*/ 0), [550]], + [new Add(/*indirect=*/ 0, /*inTag=*/ TypeTag.UINT8, /*aOffset=*/ 1, /*bOffset=*/ 2, /*dstOffset=*/ 3), [130]], + [new Add(/*indirect=*/ 0, /*inTag=*/ TypeTag.UINT32, /*aOffset=*/ 1, /*bOffset=*/ 2, /*dstOffset=*/ 3), [160]], + [new Add(/*indirect=*/ 3, /*inTag=*/ TypeTag.UINT8, /*aOffset=*/ 1, /*bOffset=*/ 2, /*dstOffset=*/ 3), [150]], + [new Sub(/*indirect=*/ 3, /*inTag=*/ TypeTag.UINT8, /*aOffset=*/ 1, /*bOffset=*/ 2, /*dstOffset=*/ 3), [150]], + [new Mul(/*indirect=*/ 3, /*inTag=*/ TypeTag.UINT8, /*aOffset=*/ 1, /*bOffset=*/ 2, /*dstOffset=*/ 3), [150]], + [new Div(/*indirect=*/ 3, /*inTag=*/ TypeTag.UINT8, /*aOffset=*/ 1, /*bOffset=*/ 2, /*dstOffset=*/ 3), [150]], ] as const)('computes gas cost for %s', async (instruction, [l2GasCost, l1GasCost, daGasCost]) => { const bytecode = encodeToBytecode([instruction]); const context = initContext(); @@ -27,8 +27,8 @@ describe('AVM simulator: dynamic gas costs per instruction', () => { 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); + expect(initialL2GasLeft - context.machineState.l2GasLeft).toEqual(l2GasCost ?? 0); + expect(initialL1GasLeft - context.machineState.l1GasLeft).toEqual(l1GasCost ?? 0); + expect(initialDaGasLeft - context.machineState.daGasLeft).toEqual(daGasCost ?? 0); }); }); diff --git a/yarn-project/simulator/src/avm/avm_gas.ts b/yarn-project/simulator/src/avm/avm_gas.ts index 496453867cbb..6a4cfb3cd7c5 100644 --- a/yarn-project/simulator/src/avm/avm_gas.ts +++ b/yarn-project/simulator/src/avm/avm_gas.ts @@ -21,7 +21,7 @@ export function gasLeftToGas(gasLeft: { l1GasLeft: number; l2GasLeft: number; da } /** Creates a new instance with all values set to zero except the ones set. */ -export function makeGasCost(gasCost: Partial) { +export function makeGas(gasCost: Partial) { return { ...EmptyGas, ...gasCost }; } @@ -53,13 +53,13 @@ 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. */ +/** Base gas costs for each instruction. Additional gas cost may be added on top due to memory or storage accesses, etc. */ export const GasCosts = { [Opcode.ADD]: DynamicGasCost, [Opcode.SUB]: DynamicGasCost, [Opcode.MUL]: DynamicGasCost, [Opcode.DIV]: DynamicGasCost, - [Opcode.FDIV]: TemporaryDefaultGasCost, + [Opcode.FDIV]: DynamicGasCost, [Opcode.EQ]: TemporaryDefaultGasCost, [Opcode.LT]: TemporaryDefaultGasCost, [Opcode.LTE]: TemporaryDefaultGasCost, @@ -88,7 +88,7 @@ export const GasCosts = { [Opcode.BLOCKL1GASLIMIT]: TemporaryDefaultGasCost, [Opcode.BLOCKL2GASLIMIT]: TemporaryDefaultGasCost, [Opcode.BLOCKDAGASLIMIT]: TemporaryDefaultGasCost, - [Opcode.CALLDATACOPY]: DynamicGasCost, + [Opcode.CALLDATACOPY]: TemporaryDefaultGasCost, // Gas [Opcode.L1GASLEFT]: TemporaryDefaultGasCost, [Opcode.L2GASLEFT]: TemporaryDefaultGasCost, @@ -99,7 +99,7 @@ export const GasCosts = { [Opcode.INTERNALCALL]: TemporaryDefaultGasCost, [Opcode.INTERNALRETURN]: TemporaryDefaultGasCost, // Memory - [Opcode.SET]: DynamicGasCost, + [Opcode.SET]: TemporaryDefaultGasCost, [Opcode.MOV]: TemporaryDefaultGasCost, [Opcode.CMOV]: TemporaryDefaultGasCost, // World state @@ -126,8 +126,8 @@ 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 { +/** Returns the fixed base gas cost for a given opcode, or throws if set to dynamic. */ +export function getBaseGasCost(opcode: Opcode): Gas { const cost = GasCosts[opcode]; if (cost === DynamicGasCost) { throw new Error(`Opcode ${Opcode[opcode]} has dynamic gas cost`); @@ -135,24 +135,32 @@ export function getFixedGasCost(opcode: Opcode): Gas { 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 }; +/** Returns the gas cost associated with the memory operations performed. */ +export function getMemoryGasCost(args: { reads?: number; writes?: number; indirectFlags?: number }) { + const { reads, writes, indirectFlags } = args; + const indirectCount = Addressing.fromWire(indirectFlags ?? 0).count(AddressingMode.INDIRECT); + const l2MemoryGasCost = + (reads ?? 0) * GasCostConstants.MEMORY_READ + + (writes ?? 0) * GasCostConstants.MEMORY_WRITE + + indirectCount * GasCostConstants.MEMORY_INDIRECT_READ_PENALTY; + return makeGas({ l2Gas: l2MemoryGasCost }); } /** Constants used in base cost calculations. */ export const GasCostConstants = { - SET_COST_PER_BYTE: 100, - CALLDATACOPY_COST_PER_BYTE: 10, ARITHMETIC_COST_PER_BYTE: 10, - COST_PER_INDIRECT_ACCESS: 5, + MEMORY_READ: 10, + MEMORY_INDIRECT_READ_PENALTY: 10, + MEMORY_WRITE: 100, }; +/** Returns gas cost for an operation on a given type tag based on the base cost per byte. */ +export function getGasCostForTypeTag(tag: TypeTag, baseCost: number) { + return baseCost * getGasCostMultiplierFromTypeTag(tag); +} + /** Returns a multiplier based on the size of the type represented by the tag. Throws on uninitialized or invalid. */ -export function getGasCostMultiplierFromTypeTag(tag: TypeTag) { +function getGasCostMultiplierFromTypeTag(tag: TypeTag) { switch (tag) { case TypeTag.UINT8: return 1; diff --git a/yarn-project/simulator/src/avm/avm_machine_state.ts b/yarn-project/simulator/src/avm/avm_machine_state.ts index c6bf2ee6cc20..2f5412c1f458 100644 --- a/yarn-project/simulator/src/avm/avm_machine_state.ts +++ b/yarn-project/simulator/src/avm/avm_machine_state.ts @@ -1,7 +1,7 @@ import { Fr } from '@aztec/circuits.js'; import { Gas, GasDimensions } from './avm_gas.js'; -import { TaggedMemory } from './avm_memory_types.js'; +import { MeteredTaggedMemory } from './avm_memory_types.js'; import { AvmContractCallResults } from './avm_message_call_result.js'; import { OutOfGasError } from './errors.js'; @@ -32,7 +32,7 @@ export class AvmMachineState { public internalCallStack: number[] = []; /** Memory accessible to user code */ - public readonly memory: TaggedMemory = new TaggedMemory(); + public readonly memory: MeteredTaggedMemory = new MeteredTaggedMemory(); /** * Signals that execution should end. diff --git a/yarn-project/simulator/src/avm/avm_memory_types.test.ts b/yarn-project/simulator/src/avm/avm_memory_types.test.ts index 982f6cea98bf..4f8418de0e51 100644 --- a/yarn-project/simulator/src/avm/avm_memory_types.test.ts +++ b/yarn-project/simulator/src/avm/avm_memory_types.test.ts @@ -1,4 +1,13 @@ -import { Field, TaggedMemory, Uint8, Uint16, Uint32, Uint64, Uint128 } from './avm_memory_types.js'; +import { + Field, + MeteredTaggedMemory, + TaggedMemory, + Uint8, + Uint16, + Uint32, + Uint64, + Uint128, +} from './avm_memory_types.js'; describe('TaggedMemory', () => { it('Elements should be undefined after construction', () => { @@ -37,6 +46,48 @@ describe('TaggedMemory', () => { }); }); +describe('MeteredTaggedMemory', () => { + let mem: MeteredTaggedMemory; + + beforeEach(() => { + mem = new MeteredTaggedMemory(); + }); + + it(`Counts reads`, () => { + mem.get(10); + mem.getAs(20); + expect(mem.getStats()).toEqual({ reads: 2, writes: 0 }); + }); + + it(`Counts reading slices`, () => { + const val = [new Field(5), new Field(6), new Field(7)]; + mem.setSlice(10, val); + mem.clearStats(); + + mem.getSlice(10, 3); + mem.getSliceAs(11, 2); + expect(mem.getStats()).toEqual({ reads: 5, writes: 0 }); + }); + + it(`Counts writes`, () => { + mem.set(10, new Uint8(5)); + expect(mem.getStats()).toEqual({ reads: 0, writes: 1 }); + }); + + it(`Counts writing slices`, () => { + mem.setSlice(10, [new Field(5), new Field(6)]); + expect(mem.getStats()).toEqual({ reads: 0, writes: 2 }); + }); + + it(`Clears stats`, () => { + mem.get(10); + mem.set(20, new Uint8(5)); + expect(mem.getStats()).toEqual({ reads: 1, writes: 1 }); + mem.clearStats(); + expect(mem.getStats()).toEqual({ reads: 0, writes: 0 }); + }); +}); + type IntegralClass = typeof Uint8 | typeof Uint16 | typeof Uint32 | typeof Uint64 | typeof Uint128; describe.each([Uint8, Uint16, Uint32, Uint64, Uint128])('Integral Types', (clsValue: IntegralClass) => { describe(`${clsValue.name}`, () => { diff --git a/yarn-project/simulator/src/avm/avm_memory_types.ts b/yarn-project/simulator/src/avm/avm_memory_types.ts index 22b4c9b50f82..c57e34213dc7 100644 --- a/yarn-project/simulator/src/avm/avm_memory_types.ts +++ b/yarn-project/simulator/src/avm/avm_memory_types.ts @@ -4,7 +4,8 @@ import { DebugLogger, createDebugLogger } from '@aztec/foundation/log'; import { strict as assert } from 'assert'; -import { TagCheckError } from './errors.js'; +import { InstructionExecutionError, TagCheckError } from './errors.js'; +import { Addressing, AddressingMode } from './opcodes/addressing_mode.js'; /** MemoryValue gathers the common operations for all memory types. */ export abstract class MemoryValue { @@ -373,3 +374,65 @@ export class TaggedMemory { } } } + +/** Tagged memory with metering for each memory read and write. */ +export class MeteredTaggedMemory extends TaggedMemory { + private reads: number = 0; + private writes: number = 0; + + public getStats(): MemoryOperations { + return { reads: this.reads, writes: this.writes }; + } + + public clearStats(): MemoryOperations { + const stats = this.getStats(); + this.reads = 0; + this.writes = 0; + return stats; + } + + public assertStats(operations: MemoryOperations & { indirectFlags: number }, type = 'instruction') { + const { reads: expectedReads, writes: expectedWrites, indirectFlags } = operations; + + const totalExpectedReads = expectedReads + Addressing.fromWire(indirectFlags).count(AddressingMode.INDIRECT); + const { reads: actualReads, writes: actualWrites } = this.clearStats(); + if (actualReads !== totalExpectedReads) { + throw new InstructionExecutionError( + `Incorrect number of memory reads for ${type}: expected ${totalExpectedReads} but executed ${actualReads}`, + ); + } + if (actualWrites !== expectedWrites) { + throw new InstructionExecutionError( + `Incorrect number of memory writes for ${type}: expected ${expectedWrites} but executed ${actualWrites}`, + ); + } + } + + public getAs(offset: number): T { + this.reads++; + return super.getAs(offset); + } + + public getSlice(offset: number, size: number): MemoryValue[] { + this.reads += size; + return super.getSlice(offset, size); + } + + public set(offset: number, v: MemoryValue): void { + this.writes++; + super.set(offset, v); + } + + public setSlice(offset: number, vs: MemoryValue[]): void { + this.writes += vs.length; + super.setSlice(offset, vs); + } +} + +/** Tracks number of memory reads and writes. */ +export type MemoryOperations = { + /** How many total reads are performed. Slice reads are count as one per element. */ + reads: number; + /** How many total writes are performed. Slice writes are count as one per element. */ + writes: number; +}; diff --git a/yarn-project/simulator/src/avm/avm_simulator.test.ts b/yarn-project/simulator/src/avm/avm_simulator.test.ts index 287ad4c8acdc..8c1537346b17 100644 --- a/yarn-project/simulator/src/avm/avm_simulator.test.ts +++ b/yarn-project/simulator/src/avm/avm_simulator.test.ts @@ -44,7 +44,7 @@ describe('AVM simulator: injected bytecode', () => { expect(results.reverted).toBe(false); expect(results.output).toEqual([new Fr(3)]); - expect(context.machineState.l2GasLeft).toEqual(initialL2GasLeft - 350); + expect(context.machineState.l2GasLeft).toEqual(initialL2GasLeft - 680); }); it('Should halt if runs out of gas', async () => { diff --git a/yarn-project/simulator/src/avm/opcodes/accrued_substate.ts b/yarn-project/simulator/src/avm/opcodes/accrued_substate.ts index c21d360f896f..49d9cc33d90a 100644 --- a/yarn-project/simulator/src/avm/opcodes/accrued_substate.ts +++ b/yarn-project/simulator/src/avm/opcodes/accrued_substate.ts @@ -42,6 +42,10 @@ export class NoteHashExists extends FixedGasInstruction { context.machineState.incrementPc(); } + + protected memoryOperations() { + return { reads: 2, writes: 1 }; + } } export class EmitNoteHash extends FixedGasInstruction { @@ -64,6 +68,10 @@ export class EmitNoteHash extends FixedGasInstruction { context.machineState.incrementPc(); } + + protected memoryOperations() { + return { reads: 1 }; + } } export class NullifierExists extends FixedGasInstruction { @@ -84,6 +92,10 @@ export class NullifierExists extends FixedGasInstruction { context.machineState.incrementPc(); } + + protected memoryOperations() { + return { reads: 1, writes: 1 }; + } } export class EmitNullifier extends FixedGasInstruction { @@ -117,6 +129,10 @@ export class EmitNullifier extends FixedGasInstruction { context.machineState.incrementPc(); } + + protected memoryOperations() { + return { reads: 1 }; + } } export class L1ToL2MessageExists extends FixedGasInstruction { @@ -148,6 +164,10 @@ export class L1ToL2MessageExists extends FixedGasInstruction { context.machineState.incrementPc(); } + + protected memoryOperations() { + return { reads: 2, writes: 1 }; + } } export class EmitUnencryptedLog extends FixedGasInstruction { @@ -188,6 +208,10 @@ export class EmitUnencryptedLog extends FixedGasInstruction { context.machineState.incrementPc(); } + + protected memoryOperations() { + return { reads: 1 + this.logSize }; + } } export class SendL2ToL1Message extends FixedGasInstruction { @@ -211,4 +235,8 @@ export class SendL2ToL1Message extends FixedGasInstruction { context.machineState.incrementPc(); } + + protected memoryOperations() { + return { reads: 2 }; + } } diff --git a/yarn-project/simulator/src/avm/opcodes/addressing_mode.ts b/yarn-project/simulator/src/avm/opcodes/addressing_mode.ts index 97e19d9f0834..fa579b100af2 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. */ - public readonly modePerOperand: AddressingMode[], + private readonly modePerOperand: AddressingMode[], ) { assert(modePerOperand.length <= 8, 'At most 8 operands are supported'); } @@ -39,6 +39,11 @@ export class Addressing { return wire; } + /** Returns how many operands use the given addressing mode. */ + public count(mode: AddressingMode): number { + return this.modePerOperand.filter(m => m === mode).length; + } + /** * Resolves the offsets using the addressing mode. * @param offsets The offsets to resolve. diff --git a/yarn-project/simulator/src/avm/opcodes/arithmetic.ts b/yarn-project/simulator/src/avm/opcodes/arithmetic.ts index 69381de3343e..9e8ae2a288db 100644 --- a/yarn-project/simulator/src/avm/opcodes/arithmetic.ts +++ b/yarn-project/simulator/src/avm/opcodes/arithmetic.ts @@ -1,11 +1,5 @@ import type { AvmContext } from '../avm_context.js'; -import { - Gas, - GasCostConstants, - getCostFromIndirectAccess, - getGasCostMultiplierFromTypeTag, - sumGas, -} from '../avm_gas.js'; +import { Gas, GasCostConstants, getGasCostForTypeTag, makeGas } from '../avm_gas.js'; import { Field, MemoryValue, TypeTag } from '../avm_memory_types.js'; import { Opcode, OperandType } from '../serialization/instruction_serialization.js'; import { FixedGasInstruction } from './fixed_gas_instruction.js'; @@ -24,12 +18,12 @@ export abstract class ThreeOperandArithmeticInstruction extends ThreeOperandFixe context.machineState.incrementPc(); } - protected gasCost(): Gas { - const arithmeticCost = { - l2Gas: getGasCostMultiplierFromTypeTag(this.inTag) * GasCostConstants.ARITHMETIC_COST_PER_BYTE, - }; - const indirectCost = getCostFromIndirectAccess(this.indirect); - return sumGas(arithmeticCost, indirectCost); + protected baseGasCost(): Gas { + return makeGas({ l2Gas: getGasCostForTypeTag(this.inTag, GasCostConstants.ARITHMETIC_COST_PER_BYTE) }); + } + + protected memoryOperations() { + return { reads: 2, writes: 1 }; } protected abstract compute(a: MemoryValue, b: MemoryValue): MemoryValue; @@ -99,4 +93,12 @@ export class FieldDiv extends FixedGasInstruction { context.machineState.incrementPc(); } + + protected baseGasCost(): Gas { + return makeGas({ l2Gas: getGasCostForTypeTag(TypeTag.FIELD, GasCostConstants.ARITHMETIC_COST_PER_BYTE) }); + } + + protected memoryOperations() { + return { reads: 2, writes: 1 }; + } } diff --git a/yarn-project/simulator/src/avm/opcodes/bitwise.ts b/yarn-project/simulator/src/avm/opcodes/bitwise.ts index 45e758b2f104..225af55af9a5 100644 --- a/yarn-project/simulator/src/avm/opcodes/bitwise.ts +++ b/yarn-project/simulator/src/avm/opcodes/bitwise.ts @@ -3,66 +3,68 @@ import { IntegralValue } from '../avm_memory_types.js'; import { Opcode } from '../serialization/instruction_serialization.js'; import { ThreeOperandFixedGasInstruction, TwoOperandFixedGasInstruction } from './instruction_impl.js'; -export class And extends ThreeOperandFixedGasInstruction { - static readonly type: string = 'AND'; - static readonly opcode = Opcode.AND; - - constructor(indirect: number, inTag: number, aOffset: number, bOffset: number, dstOffset: number) { - super(indirect, inTag, aOffset, bOffset, dstOffset); - } - +abstract class ThreeOperandBitwiseInstruction extends ThreeOperandFixedGasInstruction { protected async internalExecute(context: AvmContext): Promise { context.machineState.memory.checkTags(this.inTag, this.aOffset, this.bOffset); const a = context.machineState.memory.getAs(this.aOffset); const b = context.machineState.memory.getAs(this.bOffset); - const res = a.and(b); + const res = this.compute(a, b); context.machineState.memory.set(this.dstOffset, res); context.machineState.incrementPc(); } -} -export class Or extends ThreeOperandFixedGasInstruction { - static readonly type: string = 'OR'; - static readonly opcode = Opcode.OR; + protected abstract compute(a: IntegralValue, b: IntegralValue): IntegralValue; - constructor(indirect: number, inTag: number, aOffset: number, bOffset: number, dstOffset: number) { - super(indirect, inTag, aOffset, bOffset, dstOffset); + protected memoryOperations() { + return { reads: 2, writes: 1 }; } +} - protected async internalExecute(context: AvmContext): Promise { - context.machineState.memory.checkTags(this.inTag, this.aOffset, this.bOffset); +export class And extends ThreeOperandBitwiseInstruction { + static readonly type: string = 'AND'; + static readonly opcode = Opcode.AND; - const a = context.machineState.memory.getAs(this.aOffset); - const b = context.machineState.memory.getAs(this.bOffset); + protected compute(a: IntegralValue, b: IntegralValue): IntegralValue { + return a.and(b); + } +} - const res = a.or(b); - context.machineState.memory.set(this.dstOffset, res); +export class Or extends ThreeOperandBitwiseInstruction { + static readonly type: string = 'OR'; + static readonly opcode = Opcode.OR; - context.machineState.incrementPc(); + protected compute(a: IntegralValue, b: IntegralValue): IntegralValue { + return a.or(b); } } -export class Xor extends ThreeOperandFixedGasInstruction { +export class Xor extends ThreeOperandBitwiseInstruction { static readonly type: string = 'XOR'; static readonly opcode = Opcode.XOR; - constructor(indirect: number, inTag: number, aOffset: number, bOffset: number, dstOffset: number) { - super(indirect, inTag, aOffset, bOffset, dstOffset); + protected compute(a: IntegralValue, b: IntegralValue): IntegralValue { + return a.xor(b); } +} - protected async internalExecute(context: AvmContext): Promise { - context.machineState.memory.checkTags(this.inTag, this.aOffset, this.bOffset); +export class Shl extends ThreeOperandBitwiseInstruction { + static readonly type: string = 'SHL'; + static readonly opcode = Opcode.SHL; - const a = context.machineState.memory.getAs(this.aOffset); - const b = context.machineState.memory.getAs(this.bOffset); + protected compute(a: IntegralValue, b: IntegralValue): IntegralValue { + return a.shl(b); + } +} - const res = a.xor(b); - context.machineState.memory.set(this.dstOffset, res); +export class Shr extends ThreeOperandBitwiseInstruction { + static readonly type: string = 'SHR'; + static readonly opcode = Opcode.SHR; - context.machineState.incrementPc(); + protected compute(a: IntegralValue, b: IntegralValue): IntegralValue { + return a.shr(b); } } @@ -84,46 +86,8 @@ export class Not extends TwoOperandFixedGasInstruction { context.machineState.incrementPc(); } -} - -export class Shl extends ThreeOperandFixedGasInstruction { - static readonly type: string = 'SHL'; - static readonly opcode = Opcode.SHL; - constructor(indirect: number, inTag: number, aOffset: number, bOffset: number, dstOffset: number) { - super(indirect, inTag, aOffset, bOffset, dstOffset); - } - - protected async internalExecute(context: AvmContext): Promise { - context.machineState.memory.checkTags(this.inTag, this.aOffset, this.bOffset); - - const a = context.machineState.memory.getAs(this.aOffset); - const b = context.machineState.memory.getAs(this.bOffset); - - const res = a.shl(b); - context.machineState.memory.set(this.dstOffset, res); - - context.machineState.incrementPc(); - } -} - -export class Shr extends ThreeOperandFixedGasInstruction { - static readonly type: string = 'SHR'; - static readonly opcode = Opcode.SHR; - - constructor(indirect: number, inTag: number, aOffset: number, bOffset: number, dstOffset: number) { - super(indirect, inTag, aOffset, bOffset, dstOffset); - } - - protected async internalExecute(context: AvmContext): Promise { - context.machineState.memory.checkTags(this.inTag, this.aOffset, this.bOffset); - - const a = context.machineState.memory.getAs(this.aOffset); - const b = context.machineState.memory.getAs(this.bOffset); - - const res = a.shr(b); - context.machineState.memory.set(this.dstOffset, res); - - context.machineState.incrementPc(); + protected memoryOperations() { + return { reads: 1, writes: 1 }; } } diff --git a/yarn-project/simulator/src/avm/opcodes/comparators.test.ts b/yarn-project/simulator/src/avm/opcodes/comparators.test.ts index 4591a05a1954..e35703c97476 100644 --- a/yarn-project/simulator/src/avm/opcodes/comparators.test.ts +++ b/yarn-project/simulator/src/avm/opcodes/comparators.test.ts @@ -36,11 +36,15 @@ describe('Comparators', () => { it('Works on integral types', async () => { context.machineState.memory.setSlice(0, [new Uint32(1), new Uint32(2), new Uint32(3), new Uint32(1)]); - [ + const ops = [ new Eq(/*indirect=*/ 0, TypeTag.UINT32, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 10), new Eq(/*indirect=*/ 0, TypeTag.UINT32, /*aOffset=*/ 0, /*bOffset=*/ 2, /*dstOffset=*/ 11), new Eq(/*indirect=*/ 0, TypeTag.UINT32, /*aOffset=*/ 0, /*bOffset=*/ 3, /*dstOffset=*/ 12), - ].forEach(i => i.execute(context)); + ]; + + for (const op of ops) { + await op.execute(context); + } const actual = context.machineState.memory.getSlice(/*offset=*/ 10, /*size=*/ 3); expect(actual).toEqual([new Uint8(0), new Uint8(0), new Uint8(1)]); @@ -49,11 +53,15 @@ describe('Comparators', () => { it('Works on field elements', async () => { context.machineState.memory.setSlice(0, [new Field(1), new Field(2), new Field(3), new Field(1)]); - [ + const ops = [ new Eq(/*indirect=*/ 0, TypeTag.FIELD, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 10), new Eq(/*indirect=*/ 0, TypeTag.FIELD, /*aOffset=*/ 0, /*bOffset=*/ 2, /*dstOffset=*/ 11), new Eq(/*indirect=*/ 0, TypeTag.FIELD, /*aOffset=*/ 0, /*bOffset=*/ 3, /*dstOffset=*/ 12), - ].forEach(i => i.execute(context)); + ]; + + for (const op of ops) { + await op.execute(context); + } const actual = context.machineState.memory.getSlice(/*offset=*/ 10, /*size=*/ 3); expect(actual).toEqual([new Uint8(0), new Uint8(0), new Uint8(1)]); @@ -70,7 +78,7 @@ describe('Comparators', () => { ]; for (const o of ops) { - await expect(() => o.execute(context)).rejects.toThrow(TagCheckError); + await expect(async () => await o.execute(context)).rejects.toThrow(TagCheckError); } }); }); @@ -100,11 +108,15 @@ describe('Comparators', () => { it('Works on integral types', async () => { context.machineState.memory.setSlice(0, [new Uint32(1), new Uint32(2), new Uint32(0)]); - [ + const ops = [ new Lt(/*indirect=*/ 0, TypeTag.UINT32, /*aOffset=*/ 0, /*bOffset=*/ 0, /*dstOffset=*/ 10), new Lt(/*indirect=*/ 0, TypeTag.UINT32, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 11), new Lt(/*indirect=*/ 0, TypeTag.UINT32, /*aOffset=*/ 0, /*bOffset=*/ 2, /*dstOffset=*/ 12), - ].forEach(i => i.execute(context)); + ]; + + for (const op of ops) { + await op.execute(context); + } const actual = context.machineState.memory.getSlice(/*offset=*/ 10, /*size=*/ 3); expect(actual).toEqual([new Uint8(0), new Uint8(1), new Uint8(0)]); @@ -113,11 +125,15 @@ describe('Comparators', () => { it('Works on field elements', async () => { context.machineState.memory.setSlice(0, [new Field(1), new Field(2), new Field(0)]); - [ + const ops = [ new Lt(/*indirect=*/ 0, TypeTag.FIELD, /*aOffset=*/ 0, /*bOffset=*/ 0, /*dstOffset=*/ 10), new Lt(/*indirect=*/ 0, TypeTag.FIELD, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 11), new Lt(/*indirect=*/ 0, TypeTag.FIELD, /*aOffset=*/ 0, /*bOffset=*/ 2, /*dstOffset=*/ 12), - ].forEach(i => i.execute(context)); + ]; + + for (const op of ops) { + await op.execute(context); + } const actual = context.machineState.memory.getSlice(/*offset=*/ 10, /*size=*/ 3); expect(actual).toEqual([new Uint8(0), new Uint8(1), new Uint8(0)]); @@ -134,7 +150,7 @@ describe('Comparators', () => { ]; for (const o of ops) { - await expect(() => o.execute(context)).rejects.toThrow(TagCheckError); + await expect(async () => await o.execute(context)).rejects.toThrow(TagCheckError); } }); }); @@ -164,11 +180,15 @@ describe('Comparators', () => { it('Works on integral types', async () => { context.machineState.memory.setSlice(0, [new Uint32(1), new Uint32(2), new Uint32(0)]); - [ + const ops = [ new Lte(/*indirect=*/ 0, TypeTag.UINT32, /*aOffset=*/ 0, /*bOffset=*/ 0, /*dstOffset=*/ 10), new Lte(/*indirect=*/ 0, TypeTag.UINT32, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 11), new Lte(/*indirect=*/ 0, TypeTag.UINT32, /*aOffset=*/ 0, /*bOffset=*/ 2, /*dstOffset=*/ 12), - ].forEach(i => i.execute(context)); + ]; + + for (const op of ops) { + await op.execute(context); + } const actual = context.machineState.memory.getSlice(/*offset=*/ 10, /*size=*/ 3); expect(actual).toEqual([new Uint8(1), new Uint8(1), new Uint8(0)]); @@ -177,11 +197,15 @@ describe('Comparators', () => { it('Works on field elements', async () => { context.machineState.memory.setSlice(0, [new Field(1), new Field(2), new Field(0)]); - [ + const ops = [ new Lte(/*indirect=*/ 0, TypeTag.FIELD, /*aOffset=*/ 0, /*bOffset=*/ 0, /*dstOffset=*/ 10), new Lte(/*indirect=*/ 0, TypeTag.FIELD, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 11), new Lte(/*indirect=*/ 0, TypeTag.FIELD, /*aOffset=*/ 0, /*bOffset=*/ 2, /*dstOffset=*/ 12), - ].forEach(i => i.execute(context)); + ]; + + for (const op of ops) { + await op.execute(context); + } const actual = context.machineState.memory.getSlice(/*offset=*/ 10, /*size=*/ 3); expect(actual).toEqual([new Uint8(1), new Uint8(1), new Uint8(0)]); @@ -198,7 +222,7 @@ describe('Comparators', () => { ]; for (const o of ops) { - await expect(() => o.execute(context)).rejects.toThrow(TagCheckError); + await expect(async () => await o.execute(context)).rejects.toThrow(TagCheckError); } }); }); diff --git a/yarn-project/simulator/src/avm/opcodes/comparators.ts b/yarn-project/simulator/src/avm/opcodes/comparators.ts index cef56baf2fee..bb5d3b8ab1fd 100644 --- a/yarn-project/simulator/src/avm/opcodes/comparators.ts +++ b/yarn-project/simulator/src/avm/opcodes/comparators.ts @@ -1,67 +1,51 @@ import type { AvmContext } from '../avm_context.js'; -import { Uint8 } from '../avm_memory_types.js'; +import { MemoryValue, Uint8 } from '../avm_memory_types.js'; import { Opcode } from '../serialization/instruction_serialization.js'; import { ThreeOperandFixedGasInstruction } from './instruction_impl.js'; -export class Eq extends ThreeOperandFixedGasInstruction { - static readonly type: string = 'EQ'; - static readonly opcode = Opcode.EQ; - - constructor(indirect: number, inTag: number, aOffset: number, bOffset: number, dstOffset: number) { - super(indirect, inTag, aOffset, bOffset, dstOffset); - } - +abstract class ComparatorInstruction extends ThreeOperandFixedGasInstruction { protected async internalExecute(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 = new Uint8(a.equals(b) ? 1 : 0); + const dest = new Uint8(this.compare(a, b) ? 1 : 0); context.machineState.memory.set(this.dstOffset, dest); context.machineState.incrementPc(); } -} - -export class Lt extends ThreeOperandFixedGasInstruction { - static readonly type: string = 'LT'; - static readonly opcode = Opcode.LT; - constructor(indirect: number, inTag: number, aOffset: number, bOffset: number, dstOffset: number) { - super(indirect, inTag, aOffset, bOffset, dstOffset); + protected memoryOperations() { + return { reads: 2, writes: 1 }; } - protected async internalExecute(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); + protected abstract compare(a: MemoryValue, b: MemoryValue): boolean; +} - const dest = new Uint8(a.lt(b) ? 1 : 0); - context.machineState.memory.set(this.dstOffset, dest); +export class Eq extends ComparatorInstruction { + static readonly type: string = 'EQ'; + static readonly opcode = Opcode.EQ; - context.machineState.incrementPc(); + protected compare(a: MemoryValue, b: MemoryValue): boolean { + return a.equals(b); } } -export class Lte extends ThreeOperandFixedGasInstruction { - static readonly type: string = 'LTE'; - static readonly opcode = Opcode.LTE; +export class Lt extends ComparatorInstruction { + static readonly type: string = 'LT'; + static readonly opcode = Opcode.LT; - constructor(indirect: number, inTag: number, aOffset: number, bOffset: number, dstOffset: number) { - super(indirect, inTag, aOffset, bOffset, dstOffset); + protected compare(a: MemoryValue, b: MemoryValue): boolean { + return a.lt(b); } +} - protected async internalExecute(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 = new Uint8(a.lt(b) || a.equals(b) ? 1 : 0); - context.machineState.memory.set(this.dstOffset, dest); +export class Lte extends ComparatorInstruction { + static readonly type: string = 'LTE'; + static readonly opcode = Opcode.LTE; - context.machineState.incrementPc(); + protected compare(a: MemoryValue, b: MemoryValue): boolean { + return a.lt(b) || a.equals(b); } } diff --git a/yarn-project/simulator/src/avm/opcodes/control_flow.ts b/yarn-project/simulator/src/avm/opcodes/control_flow.ts index e1b53d5affe9..a51a10420205 100644 --- a/yarn-project/simulator/src/avm/opcodes/control_flow.ts +++ b/yarn-project/simulator/src/avm/opcodes/control_flow.ts @@ -45,6 +45,10 @@ export class JumpI extends FixedGasInstruction { context.machineState.pc = this.loc; } } + + protected memoryOperations() { + return { reads: 1 }; + } } export class InternalCall extends FixedGasInstruction { diff --git a/yarn-project/simulator/src/avm/opcodes/dynamic_gas_instruction.ts b/yarn-project/simulator/src/avm/opcodes/dynamic_gas_instruction.ts new file mode 100644 index 000000000000..2fe658404bcf --- /dev/null +++ b/yarn-project/simulator/src/avm/opcodes/dynamic_gas_instruction.ts @@ -0,0 +1,83 @@ +import type { AvmContext } from '../avm_context.js'; +import { Gas, getBaseGasCost, getMemoryGasCost, sumGas } from '../avm_gas.js'; +import { MemoryOperations } from '../avm_memory_types.js'; +import { Instruction } from './instruction.js'; + +/** + * Base class for AVM instructions with a variable gas cost that depends on data read during execution. + * Orchestrates execution by running the following operations during `execute`: + * - Invokes `loadInputs` to load from memory the inpiuts needed for execution. + * - Requests the expected number of `memoryOperations` to compute the `memoryGasCost`. + * - Computes `gasCost` based on the loaded inputs, using the sum of `baseGasCost` and `memoryGasCost`. + * - Consumes gas based on the computed gas cost, throwing an exceptional halt if not enough. + * - Executes actual logic from `internalExecute`. + * - Asserts the expected memory operations against the actual memory operations. + */ +export abstract class DynamicGasInstruction extends 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 async execute(context: AvmContext): Promise { + context.machineState.memory.clearStats(); + const inputs = this.loadInputs(context); + const gasCost = this.gasCost(inputs); + context.machineState.consumeGas(gasCost); + + await this.internalExecute(context, inputs); + + this.assertMemoryOperations(context, inputs); + } + + /** Loads state for execution and gas metering from memory. */ + protected abstract loadInputs(context: AvmContext): TInputs; + + /** Computes gas cost based on loaded state. */ + protected gasCost(inputs: TInputs): Gas { + return sumGas(this.baseGasCost(inputs), this.memoryGasCost(inputs)); + } + + /** Returns the base gas cost for this operation as read from the GasCosts table. */ + protected baseGasCost(_inputs: TInputs): Gas { + return getBaseGasCost(this.opcode); + } + + /** Returns the memory reads and writes gas cost for this operation as defined by expectedMemoryOperations. */ + protected memoryGasCost(inputs: TInputs): Gas { + return getMemoryGasCost({ indirectFlags: this.getIndirectFlags(), ...this.memoryOperations(inputs) }); + } + + /** + * Returns the expected read and write operations for this instruction. + * Subclasses must override if they access memory. + * Used for computing gas cost and validating correctness against the MeteredTaggedMemory. + */ + protected memoryOperations(_inputs: TInputs): Partial { + return { reads: 0, writes: 0 }; + } + + /** + * 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 inputs - The state loaded from memory and operands. + * @param context - The AvmContext in which the instruction executes. + */ + protected abstract internalExecute(context: AvmContext, inputs: TInputs): Promise; + + /** + * Returns the indirect addressing flags for this instruction if exist, zero otherwise. + * Used for computing memory costs. + */ + private getIndirectFlags() { + return 'indirect' in this ? (this.indirect as number) : 0; + } + + /** Verifies the memory operations executed match the ones expected from `memoryOperations`. */ + protected assertMemoryOperations(context: AvmContext, inputs: TInputs) { + const expected = { reads: 0, writes: 0, indirectFlags: this.getIndirectFlags(), ...this.memoryOperations(inputs) }; + context.machineState.memory.assertStats(expected, this.type); + } +} diff --git a/yarn-project/simulator/src/avm/opcodes/environment_getters.ts b/yarn-project/simulator/src/avm/opcodes/environment_getters.ts index 357d6cd6406d..e7591c32e3d2 100644 --- a/yarn-project/simulator/src/avm/opcodes/environment_getters.ts +++ b/yarn-project/simulator/src/avm/opcodes/environment_getters.ts @@ -20,6 +20,10 @@ abstract class GetterInstruction extends FixedGasInstruction { context.machineState.incrementPc(); } + protected memoryOperations() { + return { writes: 1 }; + } + protected abstract getIt(env: AvmExecutionEnvironment): Fr | number | bigint; } 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 f288769c8ddd..ff0203112a9d 100644 --- a/yarn-project/simulator/src/avm/opcodes/external_calls.test.ts +++ b/yarn-project/simulator/src/avm/opcodes/external_calls.test.ts @@ -71,7 +71,7 @@ describe('External Calls', () => { const retSize = 2; const successOffset = 7; - const otherContextInstructionsL2GasCost = 60; // Includes the cost of the call itself + const otherContextInstructionsL2GasCost = 780; // Includes the cost of the call itself const otherContextInstructionsBytecode = encodeToBytecode([ new CalldataCopy( /*indirect=*/ 0, diff --git a/yarn-project/simulator/src/avm/opcodes/external_calls.ts b/yarn-project/simulator/src/avm/opcodes/external_calls.ts index 3f18ad00f069..7443d9a51e55 100644 --- a/yarn-project/simulator/src/avm/opcodes/external_calls.ts +++ b/yarn-project/simulator/src/avm/opcodes/external_calls.ts @@ -1,15 +1,26 @@ -import { FunctionSelector } from '@aztec/circuits.js'; +import { Fr, FunctionSelector } from '@aztec/circuits.js'; +import { padArrayEnd } from '@aztec/foundation/collection'; import type { AvmContext } from '../avm_context.js'; -import { gasLeftToGas, getCostFromIndirectAccess, getFixedGasCost, sumGas } from '../avm_gas.js'; +import { Gas, gasLeftToGas, 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 { DynamicGasInstruction } from './dynamic_gas_instruction.js'; import { FixedGasInstruction } from './fixed_gas_instruction.js'; -import { Instruction } from './instruction.js'; -abstract class ExternalCall extends Instruction { +type ExternalCallInputs = { + callAddress: Field; + calldata: Fr[]; + l1Gas: number; + l2Gas: number; + daGas: number; + functionSelector: Fr; + retOffset: number; + successOffset: number; +}; +abstract class ExternalCall extends DynamicGasInstruction { // Informs (de)serialization. See Instruction.deserialize. static readonly wireFormat: OperandType[] = [ OperandType.UINT8, @@ -42,7 +53,7 @@ abstract class ExternalCall extends Instruction { super(); } - async execute(context: AvmContext): Promise { + protected loadInputs(context: AvmContext): ExternalCallInputs { const [gasOffset, addrOffset, argsOffset, retOffset, successOffset] = Addressing.fromWire(this.indirect).resolve( [this.gasOffset, this.addrOffset, this.argsOffset, this.retOffset, this.successOffset], context.machineState.memory, @@ -55,12 +66,12 @@ abstract class ExternalCall extends Instruction { 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)); + return { callAddress, calldata, l1Gas, l2Gas, daGas, functionSelector, retOffset, successOffset }; + } + protected async internalExecute(context: AvmContext, inputs: ExternalCallInputs): Promise { + const { callAddress, calldata, l1Gas, l2Gas, daGas, functionSelector, retOffset, successOffset } = inputs; + const allocatedGas = { l1Gas, l2Gas, daGas }; const nestedContext = context.createNestedContractCallContext( callAddress.toFr(), calldata, @@ -72,9 +83,14 @@ abstract class ExternalCall extends Instruction { const nestedCallResults = await new AvmSimulator(nestedContext).execute(); const success = !nestedCallResults.reverted; - // We only take as much data as was specified in the return size -> TODO: should we be reverting here + // We only take as much data as was specified in the return size and pad with zeroes if the return data is smaller + // than the specified size in order to prevent that memory to be left with garbage const returnData = nestedCallResults.output.slice(0, this.retSize); - const convertedReturnData = returnData.map(f => new Field(f)); + const convertedReturnData = padArrayEnd( + returnData.map(f => new Field(f)), + new Field(0), + this.retSize, + ); // Write our return data into memory context.machineState.memory.set(successOffset, new Uint8(success ? 1 : 0)); @@ -93,6 +109,16 @@ abstract class ExternalCall extends Instruction { context.machineState.incrementPc(); } + protected gasCost(inputs: ExternalCallInputs): Gas { + // Add allocated gas to the base gas cost + const { l1Gas, l2Gas, daGas } = inputs; + return sumGas(super.gasCost(inputs), { l1Gas, l2Gas, daGas }); + } + + protected memoryOperations(_inputs: ExternalCallInputs) { + return { reads: this.argsSize + 5, writes: 1 + this.retSize }; + } + public abstract get type(): 'CALL' | 'STATICCALL'; } @@ -136,6 +162,10 @@ export class Return extends FixedGasInstruction { context.machineState.return(output); } + + protected memoryOperations() { + return { reads: this.copySize }; + } } export class Revert extends FixedGasInstruction { @@ -160,4 +190,8 @@ export class Revert extends FixedGasInstruction { context.machineState.revert(output); } + + protected memoryOperations(_inputs: void) { + return { reads: this.retSize }; + } } diff --git a/yarn-project/simulator/src/avm/opcodes/fixed_gas_instruction.ts b/yarn-project/simulator/src/avm/opcodes/fixed_gas_instruction.ts index 34bac461ff6a..5d39a5bf88f3 100644 --- a/yarn-project/simulator/src/avm/opcodes/fixed_gas_instruction.ts +++ b/yarn-project/simulator/src/avm/opcodes/fixed_gas_instruction.ts @@ -1,40 +1,10 @@ import type { AvmContext } from '../avm_context.js'; -import { DynamicGasCost, Gas, GasCosts } from '../avm_gas.js'; -import { Instruction } from './instruction.js'; +import { DynamicGasInstruction } from './dynamic_gas_instruction.js'; /** - * Base class for AVM instructions with a fixed gas cost or computed directly from its operands without requiring memory access. + * Base class for AVM instructions with a fixed gas cost or computed directly from its operands. * Implements execution by consuming gas and calling the abstract internal execute function. */ -export abstract class FixedGasInstruction extends 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 execute(context: AvmContext): Promise { - context.machineState.consumeGas(this.gasCost()); - return this.internalExecute(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. - */ - protected gasCost(): Gas { - const gasCost = GasCosts[this.opcode]; - if (gasCost === DynamicGasCost) { - throw new Error(`Instruction ${this.type} must define its own gas cost`); - } - return gasCost; - } - - /** - * 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. - */ - protected abstract internalExecute(context: AvmContext): Promise; +export abstract class FixedGasInstruction extends DynamicGasInstruction { + protected loadInputs(_context: AvmContext): void {} } diff --git a/yarn-project/simulator/src/avm/opcodes/hashing.ts b/yarn-project/simulator/src/avm/opcodes/hashing.ts index b563a7e84393..647f4805d8c7 100644 --- a/yarn-project/simulator/src/avm/opcodes/hashing.ts +++ b/yarn-project/simulator/src/avm/opcodes/hashing.ts @@ -2,9 +2,10 @@ import { toBigIntBE } from '@aztec/foundation/bigint-buffer'; import { keccak, pedersenHash, poseidonHash, sha256 } from '@aztec/foundation/crypto'; import { AvmContext } from '../avm_context.js'; -import { Field } from '../avm_memory_types.js'; +import { Field, MemoryValue } from '../avm_memory_types.js'; import { Opcode, OperandType } from '../serialization/instruction_serialization.js'; import { Addressing } from './addressing_mode.js'; +import { DynamicGasInstruction } from './dynamic_gas_instruction.js'; import { FixedGasInstruction } from './fixed_gas_instruction.js'; export class Poseidon2 extends FixedGasInstruction { @@ -44,6 +45,10 @@ export class Poseidon2 extends FixedGasInstruction { context.machineState.incrementPc(); } + + protected memoryOperations() { + return { reads: this.messageSize, writes: 1 }; + } } export class Keccak extends FixedGasInstruction { @@ -89,6 +94,10 @@ export class Keccak extends FixedGasInstruction { context.machineState.incrementPc(); } + + protected memoryOperations() { + return { reads: this.messageSize, writes: 2 }; + } } export class Sha256 extends FixedGasInstruction { @@ -134,9 +143,14 @@ export class Sha256 extends FixedGasInstruction { context.machineState.incrementPc(); } + + protected memoryOperations() { + return { reads: this.messageSize, writes: 2 }; + } } -export class Pedersen extends FixedGasInstruction { +type PedersenInputs = { messageSize: number; hashData: MemoryValue[]; dstOffset: number }; +export class Pedersen extends DynamicGasInstruction { static type: string = 'PEDERSEN'; static readonly opcode: Opcode = Opcode.PEDERSEN; @@ -158,7 +172,7 @@ export class Pedersen extends FixedGasInstruction { super(); } - protected async internalExecute(context: AvmContext): Promise { + protected loadInputs(context: AvmContext): PedersenInputs { const [dstOffset, messageOffset, messageSizeOffset] = Addressing.fromWire(this.indirect).resolve( [this.dstOffset, this.messageOffset, this.messageSizeOffset], context.machineState.memory, @@ -168,10 +182,21 @@ export class Pedersen extends FixedGasInstruction { const messageSize = Number(context.machineState.memory.get(messageSizeOffset).toBigInt()); const hashData = context.machineState.memory.getSlice(messageOffset, messageSize); + return { messageSize, hashData, dstOffset }; + } + + protected async internalExecute(context: AvmContext, inputs: PedersenInputs): Promise { + const { hashData, dstOffset } = inputs; + // No domain sep for now const hash = pedersenHash(hashData); context.machineState.memory.set(dstOffset, new Field(hash)); context.machineState.incrementPc(); } + + protected memoryOperations(inputs: PedersenInputs) { + const { messageSize } = inputs; + return { reads: messageSize + 1, writes: 1 }; + } } diff --git a/yarn-project/simulator/src/avm/opcodes/memory.test.ts b/yarn-project/simulator/src/avm/opcodes/memory.test.ts index ecdfdb76bdf6..27280ab85d2b 100644 --- a/yarn-project/simulator/src/avm/opcodes/memory.test.ts +++ b/yarn-project/simulator/src/avm/opcodes/memory.test.ts @@ -161,20 +161,24 @@ describe('Memory instructions', () => { expect(inst.serialize()).toEqual(buf); }); - it('Should upcast between integral types', () => { + it('Should upcast between integral types', async () => { context.machineState.memory.set(0, new Uint8(20n)); context.machineState.memory.set(1, new Uint16(65000n)); context.machineState.memory.set(2, new Uint32(1n << 30n)); context.machineState.memory.set(3, new Uint64(1n << 50n)); context.machineState.memory.set(4, new Uint128(1n << 100n)); - [ + const ops = [ new Cast(/*indirect=*/ 0, /*dstTag=*/ TypeTag.UINT16, /*aOffset=*/ 0, /*dstOffset=*/ 10), new Cast(/*indirect=*/ 0, /*dstTag=*/ TypeTag.UINT32, /*aOffset=*/ 1, /*dstOffset=*/ 11), new Cast(/*indirect=*/ 0, /*dstTag=*/ TypeTag.UINT64, /*aOffset=*/ 2, /*dstOffset=*/ 12), new Cast(/*indirect=*/ 0, /*dstTag=*/ TypeTag.UINT128, /*aOffset=*/ 3, /*dstOffset=*/ 13), new Cast(/*indirect=*/ 0, /*dstTag=*/ TypeTag.UINT128, /*aOffset=*/ 4, /*dstOffset=*/ 14), - ].forEach(i => i.execute(context)); + ]; + + for (const op of ops) { + await op.execute(context); + } const actual = context.machineState.memory.getSlice(/*offset=*/ 10, /*size=*/ 5); expect(actual).toEqual([ @@ -188,20 +192,24 @@ describe('Memory instructions', () => { expect(tags).toEqual([TypeTag.UINT16, TypeTag.UINT32, TypeTag.UINT64, TypeTag.UINT128, TypeTag.UINT128]); }); - it('Should downcast (truncating) between integral types', () => { + it('Should downcast (truncating) between integral types', async () => { context.machineState.memory.set(0, new Uint8(20n)); context.machineState.memory.set(1, new Uint16(65000n)); context.machineState.memory.set(2, new Uint32((1n << 30n) - 1n)); context.machineState.memory.set(3, new Uint64((1n << 50n) - 1n)); context.machineState.memory.set(4, new Uint128((1n << 100n) - 1n)); - [ + const ops = [ new Cast(/*indirect=*/ 0, /*dstTag=*/ TypeTag.UINT8, /*aOffset=*/ 0, /*dstOffset=*/ 10), new Cast(/*indirect=*/ 0, /*dstTag=*/ TypeTag.UINT8, /*aOffset=*/ 1, /*dstOffset=*/ 11), new Cast(/*indirect=*/ 0, /*dstTag=*/ TypeTag.UINT16, /*aOffset=*/ 2, /*dstOffset=*/ 12), new Cast(/*indirect=*/ 0, /*dstTag=*/ TypeTag.UINT32, /*aOffset=*/ 3, /*dstOffset=*/ 13), new Cast(/*indirect=*/ 0, /*dstTag=*/ TypeTag.UINT64, /*aOffset=*/ 4, /*dstOffset=*/ 14), - ].forEach(i => i.execute(context)); + ]; + + for (const op of ops) { + await op.execute(context); + } const actual = context.machineState.memory.getSlice(/*offset=*/ 10, /*size=*/ 5); expect(actual).toEqual([ @@ -215,19 +223,24 @@ describe('Memory instructions', () => { expect(tags).toEqual([TypeTag.UINT8, TypeTag.UINT8, TypeTag.UINT16, TypeTag.UINT32, TypeTag.UINT64]); }); - it('Should upcast from integral types to field', () => { + it('Should upcast from integral types to field', async () => { context.machineState.memory.set(0, new Uint8(20n)); context.machineState.memory.set(1, new Uint16(65000n)); context.machineState.memory.set(2, new Uint32(1n << 30n)); context.machineState.memory.set(3, new Uint64(1n << 50n)); context.machineState.memory.set(4, new Uint128(1n << 100n)); - [ + + const ops = [ new Cast(/*indirect=*/ 0, /*dstTag=*/ TypeTag.FIELD, /*aOffset=*/ 0, /*dstOffset=*/ 10), new Cast(/*indirect=*/ 0, /*dstTag=*/ TypeTag.FIELD, /*aOffset=*/ 1, /*dstOffset=*/ 11), new Cast(/*indirect=*/ 0, /*dstTag=*/ TypeTag.FIELD, /*aOffset=*/ 2, /*dstOffset=*/ 12), new Cast(/*indirect=*/ 0, /*dstTag=*/ TypeTag.FIELD, /*aOffset=*/ 3, /*dstOffset=*/ 13), new Cast(/*indirect=*/ 0, /*dstTag=*/ TypeTag.FIELD, /*aOffset=*/ 4, /*dstOffset=*/ 14), - ].forEach(i => i.execute(context)); + ]; + + for (const op of ops) { + await op.execute(context); + } const actual = context.machineState.memory.getSlice(/*offset=*/ 10, /*size=*/ 5); expect(actual).toEqual([ @@ -241,20 +254,24 @@ describe('Memory instructions', () => { expect(tags).toEqual([TypeTag.FIELD, TypeTag.FIELD, TypeTag.FIELD, TypeTag.FIELD, TypeTag.FIELD]); }); - it('Should downcast (truncating) from field to integral types', () => { + it('Should downcast (truncating) from field to integral types', async () => { context.machineState.memory.set(0, new Field((1n << 200n) - 1n)); context.machineState.memory.set(1, new Field((1n << 200n) - 1n)); context.machineState.memory.set(2, new Field((1n << 200n) - 1n)); context.machineState.memory.set(3, new Field((1n << 200n) - 1n)); context.machineState.memory.set(4, new Field((1n << 200n) - 1n)); - [ + const ops = [ new Cast(/*indirect=*/ 0, /*dstTag=*/ TypeTag.UINT8, /*aOffset=*/ 0, /*dstOffset=*/ 10), new Cast(/*indirect=*/ 0, /*dstTag=*/ TypeTag.UINT16, /*aOffset=*/ 1, /*dstOffset=*/ 11), new Cast(/*indirect=*/ 0, /*dstTag=*/ TypeTag.UINT32, /*aOffset=*/ 2, /*dstOffset=*/ 12), new Cast(/*indirect=*/ 0, /*dstTag=*/ TypeTag.UINT64, /*aOffset=*/ 3, /*dstOffset=*/ 13), new Cast(/*indirect=*/ 0, /*dstTag=*/ TypeTag.UINT128, /*aOffset=*/ 4, /*dstOffset=*/ 14), - ].forEach(i => i.execute(context)); + ]; + + for (const op of ops) { + await op.execute(context); + } const actual = context.machineState.memory.getSlice(/*offset=*/ 10, /*size=*/ 5); expect(actual).toEqual([ diff --git a/yarn-project/simulator/src/avm/opcodes/memory.ts b/yarn-project/simulator/src/avm/opcodes/memory.ts index 0b9e19516197..19565ba7038b 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 { Gas, GasCostConstants, getGasCostMultiplierFromTypeTag, makeGasCost } from '../avm_gas.js'; +import { Gas, getBaseGasCost, makeGas } 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,8 +81,8 @@ export class Set extends FixedGasInstruction { context.machineState.incrementPc(); } - protected gasCost(): Gas { - return makeGasCost({ l2Gas: GasCostConstants.SET_COST_PER_BYTE * getGasCostMultiplierFromTypeTag(this.inTag) }); + protected memoryOperations() { + return { writes: 1 }; } } @@ -119,6 +119,10 @@ export class CMov extends FixedGasInstruction { context.machineState.incrementPc(); } + + protected memoryOperations() { + return { reads: 3, writes: 1 }; + } } export class Cast extends TwoOperandFixedGasInstruction { @@ -140,6 +144,14 @@ export class Cast extends TwoOperandFixedGasInstruction { context.machineState.incrementPc(); } + + protected memoryOperations() { + return { reads: 1, writes: 1 }; + } + + protected assertMemoryOperations() { + // TODO(@spalladino) Fix CAST memory operation check. + } } export class Mov extends FixedGasInstruction { @@ -169,6 +181,10 @@ export class Mov extends FixedGasInstruction { context.machineState.incrementPc(); } + + protected memoryOperations() { + return { reads: 1, writes: 1 }; + } } export class CalldataCopy extends FixedGasInstruction { @@ -199,7 +215,12 @@ export class CalldataCopy extends FixedGasInstruction { context.machineState.incrementPc(); } - protected gasCost(): Gas { - return makeGasCost({ l2Gas: GasCostConstants.CALLDATACOPY_COST_PER_BYTE * this.copySize }); + protected memoryOperations() { + return { writes: this.copySize }; + } + + protected baseGasCost(): Gas { + const base = getBaseGasCost(this.opcode); + return makeGas({ ...base, l2Gas: base.l2Gas * this.copySize }); } } diff --git a/yarn-project/simulator/src/avm/opcodes/storage.ts b/yarn-project/simulator/src/avm/opcodes/storage.ts index 4b33102e1c6b..ab434da9f01c 100644 --- a/yarn-project/simulator/src/avm/opcodes/storage.ts +++ b/yarn-project/simulator/src/avm/opcodes/storage.ts @@ -1,6 +1,7 @@ import { Fr } from '@aztec/foundation/fields'; import type { AvmContext } from '../avm_context.js'; +import { Gas, getBaseGasCost, makeGas } from '../avm_gas.js'; import { Field } from '../avm_memory_types.js'; import { InstructionExecutionError } from '../errors.js'; import { Opcode, OperandType } from '../serialization/instruction_serialization.js'; @@ -55,6 +56,15 @@ export class SStore extends BaseStorageInstruction { context.machineState.incrementPc(); } + + protected baseGasCost(): Gas { + const base = getBaseGasCost(this.opcode); + return makeGas({ ...base, l2Gas: base.l2Gas * this.size }); + } + + protected memoryOperations() { + return { reads: this.size + 1 }; + } } export class SLoad extends BaseStorageInstruction { @@ -85,6 +95,15 @@ export class SLoad extends BaseStorageInstruction { context.machineState.incrementPc(); } + + protected baseGasCost(): Gas { + const base = getBaseGasCost(this.opcode); + return makeGas({ ...base, l2Gas: base.l2Gas * this.size }); + } + + protected memoryOperations() { + return { reads: 1, writes: this.size }; + } } /**