diff --git a/yarn-project/foundation/src/types/index.ts b/yarn-project/foundation/src/types/index.ts index ff34c2156117..4da880fb1e42 100644 --- a/yarn-project/foundation/src/types/index.ts +++ b/yarn-project/foundation/src/types/index.ts @@ -4,5 +4,11 @@ export type FieldsOf = { [P in keyof T as T[P] extends Function ? never : P]: T[P]; }; +/** Extracts methods of a type. */ +export type FunctionsOf = { + // eslint-disable-next-line @typescript-eslint/ban-types + [P in keyof T as T[P] extends Function ? P : never]: T[P]; +}; + /** Marks a set of properties of a type as optional. */ export type PartialBy = Omit & Partial>; diff --git a/yarn-project/simulator/src/avm/avm_gas.test.ts b/yarn-project/simulator/src/avm/avm_gas.test.ts index c2c9cd450b7a..c37edbab6ef4 100644 --- a/yarn-project/simulator/src/avm/avm_gas.test.ts +++ b/yarn-project/simulator/src/avm/avm_gas.test.ts @@ -6,16 +6,23 @@ 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]], + // BASE_GAS(10) * 1 + MEMORY_WRITE(100) = 110 + [new SetInstruction(/*indirect=*/ 0, /*inTag=*/ TypeTag.UINT8, /*value=*/ 1, /*dstOffset=*/ 0), [110, 0, 0]], + // BASE_GAS(10) * 1 + MEMORY_WRITE(100) = 110 + [new SetInstruction(/*indirect=*/ 0, /*inTag=*/ TypeTag.UINT32, /*value=*/ 1, /*dstOffset=*/ 0), [110]], + // BASE_GAS(10) * 1 + MEMORY_WRITE(100) = 110 + [new CalldataCopy(/*indirect=*/ 0, /*cdOffset=*/ TypeTag.UINT8, /*copySize=*/ 1, /*dstOffset=*/ 0), [110]], + // BASE_GAS(10) * 5 + MEMORY_WRITE(100) * 5 = 550 + [new CalldataCopy(/*indirect=*/ 0, /*cdOffset=*/ TypeTag.UINT8, /*copySize=*/ 5, /*dstOffset=*/ 0), [550]], + // BASE_GAS(10) * 1 + MEMORY_READ(10) * 2 + MEMORY_WRITE(100) = 130 + [new Add(/*indirect=*/ 0, /*inTag=*/ TypeTag.UINT8, /*aOffset=*/ 1, /*bOffset=*/ 2, /*dstOffset=*/ 3), [130]], + // BASE_GAS(10) * 4 + MEMORY_READ(10) * 2 + MEMORY_WRITE(100) = 160 + [new Add(/*indirect=*/ 0, /*inTag=*/ TypeTag.UINT32, /*aOffset=*/ 1, /*bOffset=*/ 2, /*dstOffset=*/ 3), [160]], + // BASE_GAS(10) * 1 + MEMORY_READ(10) * 2 + MEMORY_INDIRECT_READ_PENALTY(10) * 2 + MEMORY_WRITE(100) = 150 + [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 +34,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 aa070045981f..c6923d35484b 100644 --- a/yarn-project/simulator/src/avm/avm_gas.ts +++ b/yarn-project/simulator/src/avm/avm_gas.ts @@ -1,4 +1,5 @@ import { TypeTag } from './avm_memory_types.js'; +import { InstructionExecutionError } from './errors.js'; import { Addressing, AddressingMode } from './opcodes/addressing_mode.js'; import { Opcode } from './serialization/instruction_serialization.js'; @@ -20,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 }; } @@ -36,6 +37,11 @@ export function sumGas(...gases: Partial[]) { ); } +/** Multiplies a gas instance by a scalar. */ +export function mulGas(gas: Partial, scalar: number) { + return { l1Gas: (gas.l1Gas ?? 0) * scalar, l2Gas: (gas.l2Gas ?? 0) * scalar, daGas: (gas.daGas ?? 0) * scalar }; +} + /** Zero gas across all gas dimensions. */ export const EmptyGas: Gas = { l1Gas: 0, @@ -52,12 +58,12 @@ 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 = { - [Opcode.ADD]: DynamicGasCost, - [Opcode.SUB]: DynamicGasCost, - [Opcode.MUL]: DynamicGasCost, - [Opcode.DIV]: DynamicGasCost, +/** Base gas costs for each instruction. Additional gas cost may be added on top due to memory or storage accesses, etc. */ +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, @@ -87,7 +93,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, @@ -98,7 +104,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 @@ -124,10 +130,10 @@ export const GasCosts = { [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; +}; -/** 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 +141,31 @@ 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; indirect?: number }) { + const { reads, writes, indirect } = args; + const indirectCount = Addressing.fromWire(indirect ?? 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: Gas) { + return mulGas(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; @@ -168,6 +181,6 @@ export function getGasCostMultiplierFromTypeTag(tag: TypeTag) { return 32; case TypeTag.INVALID: case TypeTag.UNINITIALIZED: - throw new Error(`Invalid tag type for gas cost multiplier: ${TypeTag[tag]}`); + throw new InstructionExecutionError(`Invalid tag type for gas cost multiplier: ${TypeTag[tag]}`); } } 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..f316b2f42a38 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,58 @@ describe('TaggedMemory', () => { }); }); +describe('MeteredTaggedMemory', () => { + let mem: MeteredTaggedMemory; + + beforeEach(() => { + mem = new MeteredTaggedMemory(new TaggedMemory()); + }); + + it(`Counts reads`, () => { + mem.get(10); + mem.getAs(20); + expect(mem.reset()).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.reset(); + + mem.getSlice(10, 3); + mem.getSliceAs(11, 2); + expect(mem.reset()).toEqual({ reads: 5, writes: 0 }); + }); + + it(`Counts writes`, () => { + mem.set(10, new Uint8(5)); + expect(mem.reset()).toEqual({ reads: 0, writes: 1 }); + }); + + it(`Counts writing slices`, () => { + mem.setSlice(10, [new Field(5), new Field(6)]); + expect(mem.reset()).toEqual({ reads: 0, writes: 2 }); + }); + + it(`Clears stats`, () => { + mem.get(10); + mem.set(20, new Uint8(5)); + expect(mem.reset()).toEqual({ reads: 1, writes: 1 }); + expect(mem.reset()).toEqual({ reads: 0, writes: 0 }); + }); + + it(`Asserts stats`, () => { + mem.get(10); + mem.set(20, new Uint8(5)); + expect(() => mem.assert({ reads: 1, writes: 1 })).not.toThrow(); + }); + + it(`Throws on failed stat assertion`, () => { + mem.get(10); + expect(() => mem.assert({ reads: 1, writes: 1 })).toThrow(); + }); +}); + 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 a1b0be40c3f4..becb59270385 100644 --- a/yarn-project/simulator/src/avm/avm_memory_types.ts +++ b/yarn-project/simulator/src/avm/avm_memory_types.ts @@ -1,10 +1,12 @@ import { toBufferBE } from '@aztec/foundation/bigint-buffer'; import { Fr } from '@aztec/foundation/fields'; import { type DebugLogger, createDebugLogger } from '@aztec/foundation/log'; +import { type FunctionsOf } from '@aztec/foundation/types'; 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 { @@ -203,10 +205,16 @@ export enum TypeTag { INVALID, } +// Lazy interface definition for tagged memory +export type TaggedMemoryInterface = FunctionsOf; + // TODO: Consider automatic conversion when getting undefined values. -export class TaggedMemory { +export class TaggedMemory implements TaggedMemoryInterface { static readonly log: DebugLogger = createDebugLogger('aztec:avm_simulator:memory'); + // Whether to track and validate memory accesses for each instruction. + static readonly TRACK_MEMORY_ACCESSES = process.env.NODE_ENV === 'test'; + // FIXME: memory should be 2^32, but TS doesn't allow for arrays that big. static readonly MAX_MEMORY_SIZE = Number((1n << 32n) - 2n); private _mem: MemoryValue[]; @@ -216,6 +224,11 @@ export class TaggedMemory { this._mem = []; } + /** Returns a MeteredTaggedMemory instance to track the number of reads and writes if TRACK_MEMORY_ACCESSES is set. */ + public track(type: string = 'instruction') { + return TaggedMemory.TRACK_MEMORY_ACCESSES ? new MeteredTaggedMemory(this, type) : this; + } + public get(offset: number): MemoryValue { assert(offset < TaggedMemory.MAX_MEMORY_SIZE); const value = this.getAs(offset); @@ -376,4 +389,110 @@ export class TaggedMemory { throw new Error(`${TypeTag[tag]} is not a valid integral type.`); } } + + /** No-op. Implemented here for compatibility with the MeteredTaggedMemory. */ + public assert(_operations: Partial) {} } + +/** Tagged memory wrapper with metering for each memory read and write operation. */ +export class MeteredTaggedMemory implements TaggedMemoryInterface { + private reads: number = 0; + private writes: number = 0; + + constructor(private wrapped: TaggedMemory, private type: string = 'instruction') {} + + /** Returns the number of reads and writes tracked so far and resets them to zero. */ + public reset(): MemoryOperations { + const stats = { reads: this.reads, writes: this.writes }; + this.reads = 0; + this.writes = 0; + return stats; + } + + /** + * Asserts that the exact number of memory operations have been performed. + * Indirect represents the flags for indirect accesses: each bit set to one counts as an extra read. + */ + public assert(operations: Partial) { + const { reads: expectedReads, writes: expectedWrites, indirect } = { reads: 0, writes: 0, ...operations }; + + const totalExpectedReads = expectedReads + Addressing.fromWire(indirect ?? 0).count(AddressingMode.INDIRECT); + const { reads: actualReads, writes: actualWrites } = this.reset(); + if (actualReads !== totalExpectedReads) { + throw new InstructionExecutionError( + `Incorrect number of memory reads for ${this.type}: expected ${totalExpectedReads} but executed ${actualReads}`, + ); + } + if (actualWrites !== expectedWrites) { + throw new InstructionExecutionError( + `Incorrect number of memory writes for ${this.type}: expected ${expectedWrites} but executed ${actualWrites}`, + ); + } + } + + public track(type: string = 'instruction'): MeteredTaggedMemory { + return new MeteredTaggedMemory(this.wrapped, type); + } + + public get(offset: number): MemoryValue { + this.reads++; + return this.wrapped.get(offset); + } + + public getSliceAs(offset: number, size: number): T[] { + this.reads += size; + return this.wrapped.getSliceAs(offset, size); + } + + public getAs(offset: number): T { + this.reads++; + return this.wrapped.getAs(offset); + } + + public getSlice(offset: number, size: number): MemoryValue[] { + this.reads += size; + return this.wrapped.getSlice(offset, size); + } + + public set(offset: number, v: MemoryValue): void { + this.writes++; + this.wrapped.set(offset, v); + } + + public setSlice(offset: number, vs: MemoryValue[]): void { + this.writes += vs.length; + this.wrapped.setSlice(offset, vs); + } + + public getSliceTags(offset: number, size: number): TypeTag[] { + return this.wrapped.getSliceTags(offset, size); + } + + public getTag(offset: number): TypeTag { + return this.wrapped.getTag(offset); + } + + public checkTag(tag: TypeTag, offset: number): void { + this.wrapped.checkTag(tag, offset); + } + + public checkIsValidMemoryOffsetTag(offset: number): void { + this.wrapped.checkIsValidMemoryOffsetTag(offset); + } + + public checkTags(tag: TypeTag, ...offsets: number[]): void { + this.wrapped.checkTags(tag, ...offsets); + } + + public checkTagsRange(tag: TypeTag, startOffset: number, size: number): void { + this.wrapped.checkTagsRange(tag, startOffset, size); + } +} + +/** 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 b11decf28326..b5247881e7e8 100644 --- a/yarn-project/simulator/src/avm/avm_simulator.test.ts +++ b/yarn-project/simulator/src/avm/avm_simulator.test.ts @@ -49,7 +49,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/avm_simulator.ts b/yarn-project/simulator/src/avm/avm_simulator.ts index 2448b5e1bafe..b2221f51b056 100644 --- a/yarn-project/simulator/src/avm/avm_simulator.ts +++ b/yarn-project/simulator/src/avm/avm_simulator.ts @@ -64,7 +64,7 @@ export class AvmSimulator { // Execute the instruction. // Normal returns and reverts will return normally here. // "Exceptional halts" will throw. - await instruction.run(this.context); + await instruction.execute(this.context); if (this.context.machineState.pc >= instructions.length) { this.log('Passed end of program!'); diff --git a/yarn-project/simulator/src/avm/opcodes/accrued_substate.ts b/yarn-project/simulator/src/avm/opcodes/accrued_substate.ts index 15535182288e..b633dd5f55f1 100644 --- a/yarn-project/simulator/src/avm/opcodes/accrued_substate.ts +++ b/yarn-project/simulator/src/avm/opcodes/accrued_substate.ts @@ -28,18 +28,23 @@ export class NoteHashExists extends Instruction { super(); } - async execute(context: AvmContext): Promise { + public async execute(context: AvmContext): Promise { + const memoryOperations = { reads: 2, writes: 1, indirect: this.indirect }; + const memory = context.machineState.memory.track(this.type); + context.machineState.consumeGas(this.gasCost(memoryOperations)); + // Note that this instruction accepts any type in memory, and converts to Field. - const noteHash = context.machineState.memory.get(this.noteHashOffset).toFr(); - const leafIndex = context.machineState.memory.get(this.leafIndexOffset).toFr(); + const noteHash = memory.get(this.noteHashOffset).toFr(); + const leafIndex = memory.get(this.leafIndexOffset).toFr(); const exists = await context.persistableState.checkNoteHashExists( context.environment.storageAddress, noteHash, leafIndex, ); - context.machineState.memory.set(this.existsOffset, exists ? new Uint8(1) : new Uint8(0)); + memory.set(this.existsOffset, exists ? new Uint8(1) : new Uint8(0)); + memory.assert(memoryOperations); context.machineState.incrementPc(); } } @@ -54,14 +59,19 @@ export class EmitNoteHash extends Instruction { super(); } - async execute(context: AvmContext): Promise { + public async execute(context: AvmContext): Promise { + const memoryOperations = { reads: 1, indirect: this.indirect }; + const memory = context.machineState.memory.track(this.type); + context.machineState.consumeGas(this.gasCost(memoryOperations)); + if (context.environment.isStaticCall) { throw new StaticCallStorageAlterError(); } - const noteHash = context.machineState.memory.get(this.noteHashOffset).toFr(); + const noteHash = memory.get(this.noteHashOffset).toFr(); context.persistableState.writeNoteHash(noteHash); + memory.assert(memoryOperations); context.machineState.incrementPc(); } } @@ -76,12 +86,17 @@ export class NullifierExists extends Instruction { super(); } - async execute(context: AvmContext): Promise { - const nullifier = context.machineState.memory.get(this.nullifierOffset).toFr(); + public async execute(context: AvmContext): Promise { + const memoryOperations = { reads: 1, writes: 1, indirect: this.indirect }; + const memory = context.machineState.memory.track(this.type); + context.machineState.consumeGas(this.gasCost(memoryOperations)); + + const nullifier = memory.get(this.nullifierOffset).toFr(); const exists = await context.persistableState.checkNullifierExists(context.environment.storageAddress, nullifier); - context.machineState.memory.set(this.existsOffset, exists ? new Uint8(1) : new Uint8(0)); + memory.set(this.existsOffset, exists ? new Uint8(1) : new Uint8(0)); + memory.assert(memoryOperations); context.machineState.incrementPc(); } } @@ -96,12 +111,16 @@ export class EmitNullifier extends Instruction { super(); } - async execute(context: AvmContext): Promise { + public async execute(context: AvmContext): Promise { if (context.environment.isStaticCall) { throw new StaticCallStorageAlterError(); } - const nullifier = context.machineState.memory.get(this.nullifierOffset).toFr(); + const memoryOperations = { reads: 1, indirect: this.indirect }; + const memory = context.machineState.memory.track(this.type); + context.machineState.consumeGas(this.gasCost(memoryOperations)); + + const nullifier = memory.get(this.nullifierOffset).toFr(); try { await context.persistableState.writeNullifier(context.environment.storageAddress, nullifier); } catch (e) { @@ -115,6 +134,7 @@ export class EmitNullifier extends Instruction { } } + memory.assert(memoryOperations); context.machineState.incrementPc(); } } @@ -140,12 +160,17 @@ export class L1ToL2MessageExists extends Instruction { super(); } - async execute(context: AvmContext): Promise { - const msgHash = context.machineState.memory.get(this.msgHashOffset).toFr(); - const msgLeafIndex = context.machineState.memory.get(this.msgLeafIndexOffset).toFr(); + public async execute(context: AvmContext): Promise { + const memoryOperations = { reads: 2, writes: 1, indirect: this.indirect }; + const memory = context.machineState.memory.track(this.type); + context.machineState.consumeGas(this.gasCost(memoryOperations)); + + const msgHash = memory.get(this.msgHashOffset).toFr(); + const msgLeafIndex = memory.get(this.msgLeafIndexOffset).toFr(); const exists = await context.persistableState.checkL1ToL2MessageExists(msgHash, msgLeafIndex); - context.machineState.memory.set(this.existsOffset, exists ? new Uint8(1) : new Uint8(0)); + memory.set(this.existsOffset, exists ? new Uint8(1) : new Uint8(0)); + memory.assert(memoryOperations); context.machineState.incrementPc(); } } @@ -171,21 +196,26 @@ export class EmitUnencryptedLog extends Instruction { super(); } - async execute(context: AvmContext): Promise { + public async execute(context: AvmContext): Promise { if (context.environment.isStaticCall) { throw new StaticCallStorageAlterError(); } + const memoryOperations = { reads: 1 + this.logSize, indirect: this.indirect }; + const memory = context.machineState.memory.track(this.type); + context.machineState.consumeGas(this.gasCost(memoryOperations)); + const [eventSelectorOffset, logOffset] = Addressing.fromWire(this.indirect).resolve( [this.eventSelectorOffset, this.logOffset], - context.machineState.memory, + memory, ); const contractAddress = context.environment.address; - const event = context.machineState.memory.get(eventSelectorOffset).toFr(); - const log = context.machineState.memory.getSlice(logOffset, this.logSize).map(f => f.toFr()); + const event = memory.get(eventSelectorOffset).toFr(); + const log = memory.getSlice(logOffset, this.logSize).map(f => f.toFr()); context.persistableState.writeLog(contractAddress, event, log); + memory.assert(memoryOperations); context.machineState.incrementPc(); } } @@ -200,15 +230,20 @@ export class SendL2ToL1Message extends Instruction { super(); } - async execute(context: AvmContext): Promise { + public async execute(context: AvmContext): Promise { if (context.environment.isStaticCall) { throw new StaticCallStorageAlterError(); } - const recipient = context.machineState.memory.get(this.recipientOffset).toFr(); - const content = context.machineState.memory.get(this.contentOffset).toFr(); + const memoryOperations = { reads: 2, indirect: this.indirect }; + const memory = context.machineState.memory.track(this.type); + context.machineState.consumeGas(this.gasCost(memoryOperations)); + + const recipient = memory.get(this.recipientOffset).toFr(); + const content = memory.get(this.contentOffset).toFr(); context.persistableState.writeL1Message(recipient, content); + memory.assert(memoryOperations); context.machineState.incrementPc(); } } diff --git a/yarn-project/simulator/src/avm/opcodes/addressing_mode.ts b/yarn-project/simulator/src/avm/opcodes/addressing_mode.ts index c3d0308c1c87..911f297f7843 100644 --- a/yarn-project/simulator/src/avm/opcodes/addressing_mode.ts +++ b/yarn-project/simulator/src/avm/opcodes/addressing_mode.ts @@ -1,6 +1,6 @@ import { strict as assert } from 'assert'; -import { type TaggedMemory } from '../avm_memory_types.js'; +import { type TaggedMemoryInterface } from '../avm_memory_types.js'; export enum AddressingMode { DIRECT, @@ -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,13 +39,18 @@ 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. * @param mem The memory to use for resolution. * @returns The resolved offsets. The length of the returned array is the same as the length of the input array. */ - public resolve(offsets: number[], mem: TaggedMemory): number[] { + public resolve(offsets: number[], mem: TaggedMemoryInterface): number[] { assert(offsets.length <= this.modePerOperand.length); const resolved = new Array(offsets.length); for (const [i, offset] of offsets.entries()) { diff --git a/yarn-project/simulator/src/avm/opcodes/arithmetic.ts b/yarn-project/simulator/src/avm/opcodes/arithmetic.ts index a3186d427cc9..137b92ab878a 100644 --- a/yarn-project/simulator/src/avm/opcodes/arithmetic.ts +++ b/yarn-project/simulator/src/avm/opcodes/arithmetic.ts @@ -1,35 +1,32 @@ import type { AvmContext } from '../avm_context.js'; -import { - type Gas, - GasCostConstants, - getCostFromIndirectAccess, - getGasCostMultiplierFromTypeTag, - sumGas, -} from '../avm_gas.js'; -import { type Field, type MemoryValue, TypeTag } from '../avm_memory_types.js'; +import { getBaseGasCost, getGasCostForTypeTag, getMemoryGasCost, sumGas } from '../avm_gas.js'; +import { type Field, type MemoryOperations, type MemoryValue, TypeTag } from '../avm_memory_types.js'; import { Opcode, OperandType } from '../serialization/instruction_serialization.js'; import { Instruction } from './instruction.js'; import { ThreeOperandInstruction } from './instruction_impl.js'; export abstract class ThreeOperandArithmeticInstruction extends ThreeOperandInstruction { - async execute(context: AvmContext): Promise { - context.machineState.memory.checkTags(this.inTag, this.aOffset, this.bOffset); + public async execute(context: AvmContext): Promise { + const memoryOperations = { reads: 2, writes: 1, indirect: this.indirect }; + const memory = context.machineState.memory.track(this.type); + context.machineState.consumeGas(this.gasCost(memoryOperations)); - const a = context.machineState.memory.get(this.aOffset); - const b = context.machineState.memory.get(this.bOffset); + memory.checkTags(this.inTag, this.aOffset, this.bOffset); + + const a = memory.get(this.aOffset); + const b = memory.get(this.bOffset); const dest = this.compute(a, b); - context.machineState.memory.set(this.dstOffset, dest); + memory.set(this.dstOffset, dest); + memory.assert(memoryOperations); 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 gasCost(memoryOps: Partial) { + const baseGasCost = getGasCostForTypeTag(this.inTag, getBaseGasCost(this.opcode)); + const memoryGasCost = getMemoryGasCost(memoryOps); + return sumGas(baseGasCost, memoryGasCost); } protected abstract compute(a: MemoryValue, b: MemoryValue): MemoryValue; @@ -88,15 +85,26 @@ export class FieldDiv extends Instruction { super(); } - async execute(context: AvmContext): Promise { - context.machineState.memory.checkTags(TypeTag.FIELD, this.aOffset, this.bOffset); + public async execute(context: AvmContext): Promise { + const memoryOperations = { reads: 2, writes: 1, indirect: this.indirect }; + const memory = context.machineState.memory.track(this.type); + context.machineState.consumeGas(this.gasCost(memoryOperations)); + + memory.checkTags(TypeTag.FIELD, this.aOffset, this.bOffset); - const a = context.machineState.memory.getAs(this.aOffset); - const b = context.machineState.memory.getAs(this.bOffset); + const a = memory.getAs(this.aOffset); + const b = memory.getAs(this.bOffset); const dest = a.fdiv(b); - context.machineState.memory.set(this.dstOffset, dest); + memory.set(this.dstOffset, dest); + memory.assert(memoryOperations); context.machineState.incrementPc(); } + + protected gasCost(memoryOps: Partial) { + const baseGasCost = getGasCostForTypeTag(TypeTag.FIELD, getBaseGasCost(this.opcode)); + const memoryGasCost = getMemoryGasCost(memoryOps); + return sumGas(baseGasCost, memoryGasCost); + } } diff --git a/yarn-project/simulator/src/avm/opcodes/bitwise.ts b/yarn-project/simulator/src/avm/opcodes/bitwise.ts index a7aeb0cfe0bc..5d7e3cf8d67d 100644 --- a/yarn-project/simulator/src/avm/opcodes/bitwise.ts +++ b/yarn-project/simulator/src/avm/opcodes/bitwise.ts @@ -3,66 +3,69 @@ import { type IntegralValue } from '../avm_memory_types.js'; import { Opcode } from '../serialization/instruction_serialization.js'; import { ThreeOperandInstruction, TwoOperandInstruction } from './instruction_impl.js'; -export class And extends ThreeOperandInstruction { - 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 ThreeOperandInstruction { + public async execute(context: AvmContext): Promise { + const memoryOperations = { reads: 2, writes: 1, indirect: this.indirect }; + const memory = context.machineState.memory.track(this.type); + context.machineState.consumeGas(this.gasCost(memoryOperations)); - async execute(context: AvmContext): Promise { - context.machineState.memory.checkTags(this.inTag, this.aOffset, this.bOffset); + 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 a = memory.getAs(this.aOffset); + const b = memory.getAs(this.bOffset); - const res = a.and(b); - context.machineState.memory.set(this.dstOffset, res); + const res = this.compute(a, b); + memory.set(this.dstOffset, res); + memory.assert(memoryOperations); context.machineState.incrementPc(); } + + protected abstract compute(a: IntegralValue, b: IntegralValue): IntegralValue; } -export class Or extends ThreeOperandInstruction { - static readonly type: string = 'OR'; - static readonly opcode = Opcode.OR; +export class And extends ThreeOperandBitwiseInstruction { + 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); + protected compute(a: IntegralValue, b: IntegralValue): IntegralValue { + return a.and(b); } +} - async execute(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.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 ThreeOperandInstruction { +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); } +} - async execute(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); } } @@ -74,56 +77,19 @@ export class Not extends TwoOperandInstruction { super(indirect, inTag, aOffset, dstOffset); } - async execute(context: AvmContext): Promise { - context.machineState.memory.checkTags(this.inTag, this.aOffset); - - const a = context.machineState.memory.getAs(this.aOffset); - - const res = a.not(); - context.machineState.memory.set(this.dstOffset, res); - - context.machineState.incrementPc(); - } -} - -export class Shl extends ThreeOperandInstruction { - 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); - } - - async execute(context: AvmContext): Promise { - context.machineState.memory.checkTags(this.inTag, this.aOffset, this.bOffset); + public async execute(context: AvmContext): Promise { + const memoryOperations = { reads: 1, writes: 1, indirect: this.indirect }; + const memory = context.machineState.memory.track(this.type); + context.machineState.consumeGas(this.gasCost(memoryOperations)); - const a = context.machineState.memory.getAs(this.aOffset); - const b = context.machineState.memory.getAs(this.bOffset); + memory.checkTags(this.inTag, this.aOffset); - const res = a.shl(b); - context.machineState.memory.set(this.dstOffset, res); + const a = memory.getAs(this.aOffset); - context.machineState.incrementPc(); - } -} - -export class Shr extends ThreeOperandInstruction { - 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); - } - - async execute(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); + const res = a.not(); + memory.set(this.dstOffset, res); + memory.assert(memoryOperations); context.machineState.incrementPc(); } } diff --git a/yarn-project/simulator/src/avm/opcodes/comparators.test.ts b/yarn-project/simulator/src/avm/opcodes/comparators.test.ts index 34ee18be64f7..c9ae2d31fa53 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 62145da0b84b..919b7cf34318 100644 --- a/yarn-project/simulator/src/avm/opcodes/comparators.ts +++ b/yarn-project/simulator/src/avm/opcodes/comparators.ts @@ -1,67 +1,52 @@ import type { AvmContext } from '../avm_context.js'; -import { Uint8 } from '../avm_memory_types.js'; +import { type MemoryValue, Uint8 } from '../avm_memory_types.js'; import { Opcode } from '../serialization/instruction_serialization.js'; import { ThreeOperandInstruction } from './instruction_impl.js'; -export class Eq extends ThreeOperandInstruction { - 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 ThreeOperandInstruction { + public async execute(context: AvmContext): Promise { + const memoryOperations = { reads: 2, writes: 1, indirect: this.indirect }; + const memory = context.machineState.memory.track(this.type); + context.machineState.consumeGas(this.gasCost(memoryOperations)); - async execute(context: AvmContext): Promise { - context.machineState.memory.checkTags(this.inTag, this.aOffset, this.bOffset); + 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 a = memory.get(this.aOffset); + const b = memory.get(this.bOffset); - const dest = new Uint8(a.equals(b) ? 1 : 0); - context.machineState.memory.set(this.dstOffset, dest); + const dest = new Uint8(this.compare(a, b) ? 1 : 0); + memory.set(this.dstOffset, dest); + memory.assert(memoryOperations); context.machineState.incrementPc(); } + + protected abstract compare(a: MemoryValue, b: MemoryValue): boolean; } -export class Lt extends ThreeOperandInstruction { - static readonly type: string = 'LT'; - static readonly opcode = Opcode.LT; +export class Eq extends ComparatorInstruction { + 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); + protected compare(a: MemoryValue, b: MemoryValue): boolean { + return a.equals(b); } +} - 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 = new Uint8(a.lt(b) ? 1 : 0); - context.machineState.memory.set(this.dstOffset, dest); +export class Lt extends ComparatorInstruction { + static readonly type: string = 'LT'; + static readonly opcode = Opcode.LT; - context.machineState.incrementPc(); + protected compare(a: MemoryValue, b: MemoryValue): boolean { + return a.lt(b); } } -export class Lte extends ThreeOperandInstruction { +export class Lte extends ComparatorInstruction { static readonly type: string = 'LTE'; static readonly opcode = Opcode.LTE; - constructor(indirect: number, inTag: number, aOffset: number, bOffset: number, dstOffset: number) { - super(indirect, inTag, aOffset, bOffset, dstOffset); - } - - 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 = new Uint8(a.lt(b) || a.equals(b) ? 1 : 0); - context.machineState.memory.set(this.dstOffset, dest); - - 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 4f88e6139f18..7c44322fa733 100644 --- a/yarn-project/simulator/src/avm/opcodes/control_flow.ts +++ b/yarn-project/simulator/src/avm/opcodes/control_flow.ts @@ -14,8 +14,12 @@ export class Jump extends Instruction { super(); } - async execute(context: AvmContext): Promise { + public async execute(context: AvmContext): Promise { + context.machineState.consumeGas(this.gasCost()); + context.machineState.pc = this.jumpOffset; + + context.machineState.memory.assert({}); } } @@ -35,8 +39,12 @@ export class JumpI extends Instruction { super(); } - async execute(context: AvmContext): Promise { - const condition = context.machineState.memory.getAs(this.condOffset); + public async execute(context: AvmContext): Promise { + const memoryOperations = { reads: 1, indirect: this.indirect }; + const memory = context.machineState.memory.track(this.type); + context.machineState.consumeGas(this.gasCost(memoryOperations)); + + const condition = memory.getAs(this.condOffset); // TODO: reconsider this casting if (condition.toBigInt() == 0n) { @@ -44,6 +52,8 @@ export class JumpI extends Instruction { } else { context.machineState.pc = this.loc; } + + memory.assert(memoryOperations); } } @@ -57,9 +67,13 @@ export class InternalCall extends Instruction { super(); } - async execute(context: AvmContext): Promise { + public async execute(context: AvmContext): Promise { + context.machineState.consumeGas(this.gasCost()); + context.machineState.internalCallStack.push(context.machineState.pc + 1); context.machineState.pc = this.loc; + + context.machineState.memory.assert({}); } } @@ -73,11 +87,15 @@ export class InternalReturn extends Instruction { super(); } - async execute(context: AvmContext): Promise { + public async execute(context: AvmContext): Promise { + context.machineState.consumeGas(this.gasCost()); + const jumpOffset = context.machineState.internalCallStack.pop(); if (jumpOffset === undefined) { throw new InstructionExecutionError('Internal call stack empty!'); } context.machineState.pc = jumpOffset; + + context.machineState.memory.assert({}); } } diff --git a/yarn-project/simulator/src/avm/opcodes/environment_getters.ts b/yarn-project/simulator/src/avm/opcodes/environment_getters.ts index b82ed7508448..aa9b848a239c 100644 --- a/yarn-project/simulator/src/avm/opcodes/environment_getters.ts +++ b/yarn-project/simulator/src/avm/opcodes/environment_getters.ts @@ -14,9 +14,15 @@ abstract class GetterInstruction extends Instruction { super(); } - async execute(context: AvmContext): Promise { + public async execute(context: AvmContext): Promise { + const memoryOperations = { writes: 1, indirect: this.indirect }; + const memory = context.machineState.memory.track(this.type); + context.machineState.consumeGas(this.gasCost(memoryOperations)); + const res = new Field(this.getIt(context.environment)); - context.machineState.memory.set(this.dstOffset, res); + memory.set(this.dstOffset, res); + + memory.assert(memoryOperations); context.machineState.incrementPc(); } 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 90db43f19894..4ba9e6b1fecb 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, @@ -105,7 +105,7 @@ describe('External Calls', () => { successOffset, /*temporaryFunctionSelectorOffset=*/ 0, ); - await instruction.run(context); + await instruction.execute(context); const successValue = context.machineState.memory.get(successOffset); expect(successValue).toEqual(new Uint8(1n)); @@ -166,7 +166,7 @@ describe('External Calls', () => { /*temporaryFunctionSelectorOffset=*/ 0, ); - await expect(() => instruction.run(context)).rejects.toThrow(/Not enough.*gas left/i); + await expect(() => instruction.execute(context)).rejects.toThrow(/Not enough.*gas left/i); }); }); @@ -244,7 +244,7 @@ describe('External Calls', () => { successOffset, /*temporaryFunctionSelectorOffset=*/ 0, ); - await instruction.run(context); + await instruction.execute(context); // No revert has occurred, but the nested execution has failed const successValue = context.machineState.memory.get(successOffset); diff --git a/yarn-project/simulator/src/avm/opcodes/external_calls.ts b/yarn-project/simulator/src/avm/opcodes/external_calls.ts index c02c1ad42db3..3037225f545e 100644 --- a/yarn-project/simulator/src/avm/opcodes/external_calls.ts +++ b/yarn-project/simulator/src/avm/opcodes/external_calls.ts @@ -1,7 +1,8 @@ import { FunctionSelector } from '@aztec/circuits.js'; +import { padArrayEnd } from '@aztec/foundation/collection'; import type { AvmContext } from '../avm_context.js'; -import { type Gas, gasLeftToGas, getCostFromIndirectAccess, getFixedGasCost, sumGas } from '../avm_gas.js'; +import { 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'; @@ -41,24 +42,24 @@ abstract class ExternalCall extends Instruction { super(); } - async run(context: AvmContext): Promise { + public async execute(context: AvmContext) { + const memory = context.machineState.memory.track(this.type); const [gasOffset, addrOffset, argsOffset, retOffset, successOffset] = Addressing.fromWire(this.indirect).resolve( [this.gasOffset, this.addrOffset, this.argsOffset, this.retOffset, this.successOffset], - context.machineState.memory, + memory, ); - const callAddress = context.machineState.memory.getAs(addrOffset); - const calldata = context.machineState.memory.getSlice(argsOffset, this.argsSize).map(f => f.toFr()); - const l1Gas = context.machineState.memory.get(gasOffset).toNumber(); - const l2Gas = context.machineState.memory.getAs(gasOffset + 1).toNumber(); - const daGas = context.machineState.memory.getAs(gasOffset + 2).toNumber(); - const functionSelector = context.machineState.memory.getAs(this.temporaryFunctionSelectorOffset).toFr(); + const callAddress = memory.getAs(addrOffset); + const calldata = memory.getSlice(argsOffset, this.argsSize).map(f => f.toFr()); + const l1Gas = memory.get(gasOffset).toNumber(); + const l2Gas = memory.getAs(gasOffset + 1).toNumber(); + const daGas = memory.getAs(gasOffset + 2).toNumber(); + const functionSelector = 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)); + const memoryOperations = { reads: this.argsSize + 5, writes: 1 + this.retSize, indirect: this.indirect }; + const totalGas = sumGas(this.gasCost(memoryOperations), allocatedGas); + context.machineState.consumeGas(totalGas); const nestedContext = context.createNestedContractCallContext( callAddress.toFr(), @@ -71,13 +72,18 @@ 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)); - context.machineState.memory.setSlice(retOffset, convertedReturnData); + memory.set(successOffset, new Uint8(success ? 1 : 0)); + memory.setSlice(retOffset, convertedReturnData); // Refund unused gas context.machineState.refundGas(gasLeftToGas(nestedContext.machineState)); @@ -89,20 +95,11 @@ abstract class ExternalCall extends Instruction { context.persistableState.rejectNestedCallState(nestedContext.persistableState); } + memory.assert(memoryOperations); context.machineState.incrementPc(); } public abstract get type(): 'CALL' | 'STATICCALL'; - - protected execute(_context: AvmContext): Promise { - throw new Error( - `Instructions with dynamic gas calculation run all logic on the main execute function and do not override the internal execute.`, - ); - } - - protected gasCost(): Gas { - throw new Error(`Instructions with dynamic gas calculation compute gas as part of the main execute function.`); - } } export class Call extends ExternalCall { @@ -138,12 +135,17 @@ export class Return extends Instruction { super(); } - async execute(context: AvmContext): Promise { - const [returnOffset] = Addressing.fromWire(this.indirect).resolve([this.returnOffset], context.machineState.memory); + public async execute(context: AvmContext): Promise { + const memoryOperations = { reads: this.copySize, indirect: this.indirect }; + const memory = context.machineState.memory.track(this.type); + context.machineState.consumeGas(this.gasCost(memoryOperations)); - const output = context.machineState.memory.getSlice(returnOffset, this.copySize).map(word => word.toFr()); + const [returnOffset] = Addressing.fromWire(this.indirect).resolve([this.returnOffset], memory); + + const output = memory.getSlice(returnOffset, this.copySize).map(word => word.toFr()); context.machineState.return(output); + memory.assert(memoryOperations); } } @@ -162,11 +164,16 @@ export class Revert extends Instruction { super(); } - async execute(context: AvmContext): Promise { - const [returnOffset] = Addressing.fromWire(this.indirect).resolve([this.returnOffset], context.machineState.memory); + public async execute(context: AvmContext): Promise { + const memoryOperations = { reads: this.retSize, indirect: this.indirect }; + const memory = context.machineState.memory.track(this.type); + context.machineState.consumeGas(this.gasCost(memoryOperations)); + + const [returnOffset] = Addressing.fromWire(this.indirect).resolve([this.returnOffset], memory); - const output = context.machineState.memory.getSlice(returnOffset, this.retSize).map(word => word.toFr()); + const output = memory.getSlice(returnOffset, this.retSize).map(word => word.toFr()); context.machineState.revert(output); + memory.assert(memoryOperations); } } diff --git a/yarn-project/simulator/src/avm/opcodes/hashing.ts b/yarn-project/simulator/src/avm/opcodes/hashing.ts index aecbd4a71a1d..31c5f7ac54cb 100644 --- a/yarn-project/simulator/src/avm/opcodes/hashing.ts +++ b/yarn-project/simulator/src/avm/opcodes/hashing.ts @@ -29,19 +29,24 @@ export class Poseidon2 extends Instruction { super(); } - async execute(context: AvmContext): Promise { + public async execute(context: AvmContext): Promise { + const memoryOperations = { reads: this.messageSize, writes: 1, indirect: this.indirect }; + const memory = context.machineState.memory.track(this.type); + context.machineState.consumeGas(this.gasCost(memoryOperations)); + // We hash a set of field elements const [dstOffset, messageOffset] = Addressing.fromWire(this.indirect).resolve( [this.dstOffset, this.messageOffset], - context.machineState.memory, + memory, ); // Memory pointer will be indirect - const hashData = context.machineState.memory.getSlice(messageOffset, this.messageSize).map(word => word.toBuffer()); + const hashData = memory.getSlice(messageOffset, this.messageSize).map(word => word.toBuffer()); const hash = poseidonHash(hashData); - context.machineState.memory.set(dstOffset, new Field(hash)); + memory.set(dstOffset, new Field(hash)); + memory.assert(memoryOperations); context.machineState.incrementPc(); } } @@ -69,14 +74,18 @@ export class Keccak extends Instruction { } // Note hash output is 32 bytes, so takes up two fields - async execute(context: AvmContext): Promise { + public async execute(context: AvmContext): Promise { + const memoryOperations = { reads: this.messageSize, writes: 2, indirect: this.indirect }; + const memory = context.machineState.memory.track(this.type); + context.machineState.consumeGas(this.gasCost(memoryOperations)); + // We hash a set of field elements const [dstOffset, messageOffset] = Addressing.fromWire(this.indirect).resolve( [this.dstOffset, this.messageOffset], - context.machineState.memory, + memory, ); - const hashData = context.machineState.memory.getSlice(messageOffset, this.messageSize).map(word => word.toBuffer()); + const hashData = memory.getSlice(messageOffset, this.messageSize).map(word => word.toBuffer()); const hash = keccak(Buffer.concat(hashData)); @@ -84,9 +93,10 @@ export class Keccak extends Instruction { const high = new Field(toBigIntBE(hash.subarray(0, 16))); const low = new Field(toBigIntBE(hash.subarray(16, 32))); - context.machineState.memory.set(dstOffset, high); - context.machineState.memory.set(dstOffset + 1, low); + memory.set(dstOffset, high); + memory.set(dstOffset + 1, low); + memory.assert(memoryOperations); context.machineState.incrementPc(); } } @@ -114,14 +124,18 @@ export class Sha256 extends Instruction { } // Note hash output is 32 bytes, so takes up two fields - async execute(context: AvmContext): Promise { + public async execute(context: AvmContext): Promise { + const memoryOperations = { reads: this.messageSize, writes: 2, indirect: this.indirect }; + const memory = context.machineState.memory.track(this.type); + context.machineState.consumeGas(this.gasCost(memoryOperations)); + const [dstOffset, messageOffset] = Addressing.fromWire(this.indirect).resolve( [this.dstOffset, this.messageOffset], - context.machineState.memory, + memory, ); // We hash a set of field elements - const hashData = context.machineState.memory.getSlice(messageOffset, this.messageSize).map(word => word.toBuffer()); + const hashData = memory.getSlice(messageOffset, this.messageSize).map(word => word.toBuffer()); const hash = sha256(Buffer.concat(hashData)); @@ -129,9 +143,10 @@ export class Sha256 extends Instruction { const high = new Field(toBigIntBE(hash.subarray(0, 16))); const low = new Field(toBigIntBE(hash.subarray(16, 32))); - context.machineState.memory.set(dstOffset, high); - context.machineState.memory.set(dstOffset + 1, low); + memory.set(dstOffset, high); + memory.set(dstOffset + 1, low); + memory.assert(memoryOperations); context.machineState.incrementPc(); } } @@ -160,20 +175,26 @@ export class Pedersen extends Instruction { super(); } - async execute(context: AvmContext): Promise { + public async execute(context: AvmContext): Promise { + const memory = context.machineState.memory.track(this.type); const [genIndexOffset, dstOffset, messageOffset, messageSizeOffset] = Addressing.fromWire(this.indirect).resolve( [this.genIndexOffset, this.dstOffset, this.messageOffset, this.messageSizeOffset], - context.machineState.memory, + memory, ); // We hash a set of field elements - const genIndex = Number(context.machineState.memory.get(genIndexOffset).toBigInt()); - const messageSize = Number(context.machineState.memory.get(messageSizeOffset).toBigInt()); - const hashData = context.machineState.memory.getSlice(messageOffset, messageSize); + const genIndex = Number(memory.get(genIndexOffset).toBigInt()); + const messageSize = Number(memory.get(messageSizeOffset).toBigInt()); + const hashData = memory.getSlice(messageOffset, messageSize); + + const memoryOperations = { reads: messageSize + 2, writes: 1, indirect: this.indirect }; + context.machineState.consumeGas(this.gasCost(memoryOperations)); + // No domain sep for now const hash = pedersenHash(hashData, genIndex); - context.machineState.memory.set(dstOffset, new Field(hash)); + memory.set(dstOffset, new Field(hash)); + memory.assert(memoryOperations); context.machineState.incrementPc(); } } diff --git a/yarn-project/simulator/src/avm/opcodes/instruction.ts b/yarn-project/simulator/src/avm/opcodes/instruction.ts index 5fd01bb9bf8d..0d5eed0485a5 100644 --- a/yarn-project/simulator/src/avm/opcodes/instruction.ts +++ b/yarn-project/simulator/src/avm/opcodes/instruction.ts @@ -1,7 +1,8 @@ import { strict as assert } from 'assert'; import type { AvmContext } from '../avm_context.js'; -import { DynamicGasCost, type Gas, GasCosts } from '../avm_gas.js'; +import { getBaseGasCost, getMemoryGasCost, sumGas } from '../avm_gas.js'; +import { type MemoryOperations } from '../avm_memory_types.js'; import { type BufferCursor } from '../serialization/buffer_cursor.js'; import { Opcode, type OperandType, deserialize, serialize } from '../serialization/instruction_serialization.js'; @@ -20,31 +21,7 @@ export abstract class 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. - */ - 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 execute(context: AvmContext): Promise; + public abstract execute(context: AvmContext): Promise; /** * Generate a string representation of the instruction including @@ -85,6 +62,17 @@ export abstract class Instruction { return new this(...args); } + /** + * Computes gas cost for the instruction based on its base cost and memory operations. + * @param memoryOps Memory operations performed by the instruction. + * @returns Gas cost. + */ + protected gasCost(memoryOps: Partial = {}) { + const baseGasCost = getBaseGasCost(this.opcode); + const memoryGasCost = getMemoryGasCost(memoryOps); + return sumGas(baseGasCost, memoryGasCost); + } + /** * Returns the stringified type of the instruction. * Instruction sub-classes should have a static `type` property. diff --git a/yarn-project/simulator/src/avm/opcodes/instruction_impl.ts b/yarn-project/simulator/src/avm/opcodes/instruction_impl.ts index fb8b8621a711..c31eec89e4b0 100644 --- a/yarn-project/simulator/src/avm/opcodes/instruction_impl.ts +++ b/yarn-project/simulator/src/avm/opcodes/instruction_impl.ts @@ -1,19 +1,32 @@ import { OperandType } from '../serialization/instruction_serialization.js'; import { Instruction } from './instruction.js'; +/** Wire format that informs deserialization for instructions with two operands. */ +export const TwoOperandWireFormat = [ + OperandType.UINT8, + OperandType.UINT8, + OperandType.UINT8, + OperandType.UINT32, + OperandType.UINT32, +]; + +/** Wire format that informs deserialization for instructions with three operands. */ +export const ThreeOperandWireFormat = [ + OperandType.UINT8, + OperandType.UINT8, + OperandType.UINT8, + OperandType.UINT32, + OperandType.UINT32, + OperandType.UINT32, +]; + /** * Covers (de)serialization for an instruction with: * indirect, inTag, and two UINT32s. */ export abstract class TwoOperandInstruction extends Instruction { // Informs (de)serialization. See Instruction.deserialize. - static readonly wireFormat: OperandType[] = [ - OperandType.UINT8, - OperandType.UINT8, - OperandType.UINT8, - OperandType.UINT32, - OperandType.UINT32, - ]; + static readonly wireFormat: OperandType[] = TwoOperandWireFormat; constructor( protected indirect: number, @@ -31,14 +44,7 @@ export abstract class TwoOperandInstruction extends Instruction { */ export abstract class ThreeOperandInstruction extends Instruction { // Informs (de)serialization. See Instruction.deserialize. - static readonly wireFormat: OperandType[] = [ - OperandType.UINT8, - OperandType.UINT8, - OperandType.UINT8, - OperandType.UINT32, - OperandType.UINT32, - OperandType.UINT32, - ]; + static readonly wireFormat: OperandType[] = ThreeOperandWireFormat; constructor( protected indirect: number, diff --git a/yarn-project/simulator/src/avm/opcodes/memory.test.ts b/yarn-project/simulator/src/avm/opcodes/memory.test.ts index c133f4a4c04a..d2b4eab29c2d 100644 --- a/yarn-project/simulator/src/avm/opcodes/memory.test.ts +++ b/yarn-project/simulator/src/avm/opcodes/memory.test.ts @@ -135,7 +135,7 @@ describe('Memory instructions', () => { it('should throw if tag is FIELD, UNINITIALIZED, INVALID', async () => { for (const tag of [TypeTag.FIELD, TypeTag.UNINITIALIZED, TypeTag.INVALID]) { await expect( - new Set(/*indirect=*/ 0, /*inTag=*/ tag, /*value=*/ 1234n, /*offset=*/ 1).execute(context), + async () => await new Set(/*indirect=*/ 0, /*inTag=*/ tag, /*value=*/ 1234n, /*offset=*/ 1).execute(context), ).rejects.toThrow(InstructionExecutionError); } }); @@ -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 441a1129e5fe..81eb5fd8ea7e 100644 --- a/yarn-project/simulator/src/avm/opcodes/memory.ts +++ b/yarn-project/simulator/src/avm/opcodes/memory.ts @@ -1,6 +1,6 @@ import type { AvmContext } from '../avm_context.js'; -import { type Gas, GasCostConstants, getGasCostMultiplierFromTypeTag, makeGasCost } from '../avm_gas.js'; -import { Field, TaggedMemory, TypeTag } from '../avm_memory_types.js'; +import { getBaseGasCost, getMemoryGasCost, mulGas, sumGas } from '../avm_gas.js'; +import { Field, type MemoryOperations, TaggedMemory, TypeTag } from '../avm_memory_types.js'; import { InstructionExecutionError } from '../errors.js'; import { BufferCursor } from '../serialization/buffer_cursor.js'; import { Opcode, OperandType, deserialize, serialize } from '../serialization/instruction_serialization.js'; @@ -69,21 +69,22 @@ export class Set extends Instruction { return new this(...args); } - async execute(context: AvmContext): Promise { + public async execute(context: AvmContext): Promise { + const memoryOperations = { writes: 1, indirect: this.indirect }; + const memory = context.machineState.memory.track(this.type); + context.machineState.consumeGas(this.gasCost(memoryOperations)); + // Per the YP, the tag cannot be a field. if ([TypeTag.FIELD, TypeTag.UNINITIALIZED, TypeTag.INVALID].includes(this.inTag)) { throw new InstructionExecutionError(`Invalid tag ${TypeTag[this.inTag]} for SET.`); } const res = TaggedMemory.integralFromTag(this.value, this.inTag); - context.machineState.memory.set(this.dstOffset, res); + memory.set(this.dstOffset, res); + memory.assert(memoryOperations); context.machineState.incrementPc(); } - - protected gasCost(): Gas { - return makeGasCost({ l2Gas: GasCostConstants.SET_COST_PER_BYTE * getGasCostMultiplierFromTypeTag(this.inTag) }); - } } export class CMov extends Instruction { @@ -109,14 +110,19 @@ export class CMov extends Instruction { super(); } - async execute(context: AvmContext): Promise { - const a = context.machineState.memory.get(this.aOffset); - const b = context.machineState.memory.get(this.bOffset); - const cond = context.machineState.memory.get(this.condOffset); + public async execute(context: AvmContext): Promise { + const memoryOperations = { reads: 3, writes: 1, indirect: this.indirect }; + const memory = context.machineState.memory.track(this.type); + context.machineState.consumeGas(this.gasCost(memoryOperations)); + + const a = memory.get(this.aOffset); + const b = memory.get(this.bOffset); + const cond = memory.get(this.condOffset); // TODO: reconsider toBigInt() here - context.machineState.memory.set(this.dstOffset, cond.toBigInt() > 0 ? a : b); + memory.set(this.dstOffset, cond.toBigInt() > 0 ? a : b); + memory.assert(memoryOperations); context.machineState.incrementPc(); } } @@ -129,15 +135,20 @@ export class Cast extends TwoOperandInstruction { super(indirect, dstTag, aOffset, dstOffset); } - async execute(context: AvmContext): Promise { - const a = context.machineState.memory.get(this.aOffset); + public async execute(context: AvmContext): Promise { + const memoryOperations = { reads: 1, writes: 1, indirect: this.indirect }; + const memory = context.machineState.memory.track(this.type); + context.machineState.consumeGas(this.gasCost(memoryOperations)); + + const a = memory.get(this.aOffset); // TODO: consider not using toBigInt() const casted = this.inTag == TypeTag.FIELD ? new Field(a.toBigInt()) : TaggedMemory.integralFromTag(a.toBigInt(), this.inTag); - context.machineState.memory.set(this.dstOffset, casted); + memory.set(this.dstOffset, casted); + memory.assert(memoryOperations); context.machineState.incrementPc(); } } @@ -157,16 +168,18 @@ export class Mov extends Instruction { super(); } - async execute(context: AvmContext): Promise { - const [srcOffset, dstOffset] = Addressing.fromWire(this.indirect).resolve( - [this.srcOffset, this.dstOffset], - context.machineState.memory, - ); + public async execute(context: AvmContext): Promise { + const memoryOperations = { reads: 1, writes: 1, indirect: this.indirect }; + const memory = context.machineState.memory.track(this.type); + context.machineState.consumeGas(this.gasCost(memoryOperations)); + + const [srcOffset, dstOffset] = Addressing.fromWire(this.indirect).resolve([this.srcOffset, this.dstOffset], memory); - const a = context.machineState.memory.get(srcOffset); + const a = memory.get(srcOffset); - context.machineState.memory.set(dstOffset, a); + memory.set(dstOffset, a); + memory.assert(memoryOperations); context.machineState.incrementPc(); } } @@ -187,19 +200,26 @@ export class CalldataCopy extends Instruction { super(); } - async execute(context: AvmContext): Promise { - const [dstOffset] = Addressing.fromWire(this.indirect).resolve([this.dstOffset], context.machineState.memory); + public async execute(context: AvmContext): Promise { + const memoryOperations = { writes: this.copySize, indirect: this.indirect }; + const memory = context.machineState.memory.track(this.type); + context.machineState.consumeGas(this.gasCost(memoryOperations)); + + const [dstOffset] = Addressing.fromWire(this.indirect).resolve([this.dstOffset], memory); const transformedData = context.environment.calldata .slice(this.cdOffset, this.cdOffset + this.copySize) .map(f => new Field(f)); - context.machineState.memory.setSlice(dstOffset, transformedData); + memory.setSlice(dstOffset, transformedData); + memory.assert(memoryOperations); context.machineState.incrementPc(); } - protected gasCost(): Gas { - return makeGasCost({ l2Gas: GasCostConstants.CALLDATACOPY_COST_PER_BYTE * this.copySize }); + protected gasCost(memoryOps: Partial = {}) { + const baseGasCost = mulGas(getBaseGasCost(this.opcode), this.copySize); + const memoryGasCost = getMemoryGasCost(memoryOps); + return sumGas(baseGasCost, memoryGasCost); } } diff --git a/yarn-project/simulator/src/avm/opcodes/storage.ts b/yarn-project/simulator/src/avm/opcodes/storage.ts index 3ca3bfa19aaf..c72541c0572e 100644 --- a/yarn-project/simulator/src/avm/opcodes/storage.ts +++ b/yarn-project/simulator/src/avm/opcodes/storage.ts @@ -1,7 +1,8 @@ import { Fr } from '@aztec/foundation/fields'; import type { AvmContext } from '../avm_context.js'; -import { Field } from '../avm_memory_types.js'; +import { type Gas, getBaseGasCost, getMemoryGasCost, mulGas, sumGas } from '../avm_gas.js'; +import { Field, type MemoryOperations } from '../avm_memory_types.js'; import { InstructionExecutionError } from '../errors.js'; import { Opcode, OperandType } from '../serialization/instruction_serialization.js'; import { Addressing } from './addressing_mode.js'; @@ -25,6 +26,12 @@ abstract class BaseStorageInstruction extends Instruction { ) { super(); } + + protected gasCost(memoryOps: Partial): Gas { + const baseGasCost = mulGas(getBaseGasCost(this.opcode), this.size); + const memoryGasCost = getMemoryGasCost(memoryOps); + return sumGas(baseGasCost, memoryGasCost); + } } export class SStore extends BaseStorageInstruction { @@ -35,24 +42,26 @@ export class SStore extends BaseStorageInstruction { super(indirect, srcOffset, srcSize, slotOffset); } - async execute(context: AvmContext): Promise { + public async execute(context: AvmContext): Promise { if (context.environment.isStaticCall) { throw new StaticCallStorageAlterError(); } - const [srcOffset, slotOffset] = Addressing.fromWire(this.indirect).resolve( - [this.aOffset, this.bOffset], - context.machineState.memory, - ); + const memoryOperations = { reads: this.size + 1, indirect: this.indirect }; + const memory = context.machineState.memory.track(this.type); + context.machineState.consumeGas(this.gasCost(memoryOperations)); - const slot = context.machineState.memory.get(slotOffset).toFr(); - const data = context.machineState.memory.getSlice(srcOffset, this.size).map(field => field.toFr()); + const [srcOffset, slotOffset] = Addressing.fromWire(this.indirect).resolve([this.aOffset, this.bOffset], memory); + + const slot = memory.get(slotOffset).toFr(); + const data = memory.getSlice(srcOffset, this.size).map(field => field.toFr()); for (const [index, value] of Object.entries(data)) { const adjustedSlot = slot.add(new Fr(BigInt(index))); context.persistableState.writeStorage(context.environment.storageAddress, adjustedSlot, value); } + memory.assert(memoryOperations); context.machineState.incrementPc(); } } @@ -65,13 +74,17 @@ export class SLoad extends BaseStorageInstruction { super(indirect, slotOffset, size, dstOffset); } - async execute(context: AvmContext): Promise { + public async execute(context: AvmContext): Promise { + const memoryOperations = { writes: this.size, reads: 1, indirect: this.indirect }; + const memory = context.machineState.memory.track(this.type); + context.machineState.consumeGas(this.gasCost(memoryOperations)); + const [aOffset, size, bOffset] = Addressing.fromWire(this.indirect).resolve( [this.aOffset, this.size, this.bOffset], - context.machineState.memory, + memory, ); - const slot = context.machineState.memory.get(aOffset); + const slot = memory.get(aOffset); // Write each read value from storage into memory for (let i = 0; i < size; i++) { @@ -80,10 +93,11 @@ export class SLoad extends BaseStorageInstruction { new Fr(slot.toBigInt() + BigInt(i)), ); - context.machineState.memory.set(bOffset + i, new Field(data)); + memory.set(bOffset + i, new Field(data)); } context.machineState.incrementPc(); + memory.assert(memoryOperations); } }