diff --git a/yarn-project/acir-simulator/src/avm/avm_memory_types.ts b/yarn-project/acir-simulator/src/avm/avm_memory_types.ts index 6967a58fd8f..8963276813b 100644 --- a/yarn-project/acir-simulator/src/avm/avm_memory_types.ts +++ b/yarn-project/acir-simulator/src/avm/avm_memory_types.ts @@ -16,6 +16,11 @@ export abstract class MemoryValue { // Use sparingly. public abstract toBigInt(): bigint; + + // To field + public toFr(): Fr { + return new Fr(this.toBigInt()); + } } export abstract class IntegralValue extends MemoryValue { @@ -237,6 +242,11 @@ export class TaggedMemory { return this._mem.slice(offset, offset + size); } + public getSliceAs(offset: number, size: number): T[] { + assert(offset < TaggedMemory.MAX_MEMORY_SIZE); + return this._mem.slice(offset, offset + size) as T[]; + } + public getSliceTags(offset: number, size: number): TypeTag[] { assert(offset < TaggedMemory.MAX_MEMORY_SIZE); return this._mem.slice(offset, offset + size).map(TaggedMemory.getTag); diff --git a/yarn-project/acir-simulator/src/avm/journal/journal.test.ts b/yarn-project/acir-simulator/src/avm/journal/journal.test.ts index 1d5d0d08671..58b0b3345b9 100644 --- a/yarn-project/acir-simulator/src/avm/journal/journal.test.ts +++ b/yarn-project/acir-simulator/src/avm/journal/journal.test.ts @@ -86,14 +86,14 @@ describe('journal', () => { describe('UTXOs', () => { it('Should maintain commitments', () => { const utxo = new Fr(1); - journal.writeCommitment(utxo); + journal.writeNoteHash(utxo); const journalUpdates = journal.flush(); - expect(journalUpdates.newCommitments).toEqual([utxo]); + expect(journalUpdates.newNoteHashes).toEqual([utxo]); }); it('Should maintain l1 messages', () => { - const utxo = new Fr(1); + const utxo = [new Fr(1)]; journal.writeL1Message(utxo); const journalUpdates = journal.flush(); @@ -123,16 +123,20 @@ describe('journal', () => { const valueT1 = new Fr(2); const commitment = new Fr(10); const commitmentT1 = new Fr(20); + const logs = [new Fr(1), new Fr(2)]; + const logsT1 = [new Fr(3), new Fr(4)]; journal.writeStorage(contractAddress, key, value); - journal.writeCommitment(commitment); - journal.writeL1Message(commitment); + journal.writeNoteHash(commitment); + journal.writeLog(logs); + journal.writeL1Message(logs); journal.writeNullifier(commitment); const journal1 = new AvmJournal(journal.hostStorage, journal); journal.writeStorage(contractAddress, key, valueT1); - journal.writeCommitment(commitmentT1); - journal.writeL1Message(commitmentT1); + journal.writeNoteHash(commitmentT1); + journal.writeLog(logsT1); + journal.writeL1Message(logsT1); journal.writeNullifier(commitmentT1); journal1.mergeWithParent(); @@ -143,8 +147,9 @@ describe('journal', () => { // Check that the UTXOs are merged const journalUpdates: JournalData = journal.flush(); - expect(journalUpdates.newCommitments).toEqual([commitment, commitmentT1]); - expect(journalUpdates.newL1Messages).toEqual([commitment, commitmentT1]); + expect(journalUpdates.newNoteHashes).toEqual([commitment, commitmentT1]); + expect(journalUpdates.newLogs).toEqual([logs, logsT1]); + expect(journalUpdates.newL1Messages).toEqual([logs, logsT1]); expect(journalUpdates.newNullifiers).toEqual([commitment, commitmentT1]); }); diff --git a/yarn-project/acir-simulator/src/avm/journal/journal.ts b/yarn-project/acir-simulator/src/avm/journal/journal.ts index 4d5b92fec6b..6168ce2cdf2 100644 --- a/yarn-project/acir-simulator/src/avm/journal/journal.ts +++ b/yarn-project/acir-simulator/src/avm/journal/journal.ts @@ -7,11 +7,10 @@ import { HostStorage } from './host_storage.js'; * Data held within the journal */ export type JournalData = { - newCommitments: Fr[]; - - newL1Messages: Fr[]; - + newNoteHashes: Fr[]; newNullifiers: Fr[]; + newL1Messages: Fr[][]; + newLogs: Fr[][]; /** contract address -\> key -\> value */ storageWrites: Map>; }; @@ -34,11 +33,11 @@ export class AvmJournal { private storageReads: Map> = new Map(); // New written state - private newCommitments: Fr[] = []; + private newNoteHashes: Fr[] = []; private newNullifiers: Fr[] = []; - private newL1Message: Fr[] = []; - // New Substrate + // New Substate + private newL1Messages: Fr[][] = []; private newLogs: Fr[][] = []; // contract address -> key -> value @@ -102,27 +101,22 @@ export class AvmJournal { return this.hostStorage.publicStateDb.storageRead(contractAddress, key); } - /** - - * @param commitment - - */ - public writeCommitment(commitment: Fr) { - this.newCommitments.push(commitment); + public writeNoteHash(noteHash: Fr) { + this.newNoteHashes.push(noteHash); } - /** - - * @param message - - */ - public writeL1Message(message: Fr) { - this.newL1Message.push(message); + public writeL1Message(message: Fr[]) { + this.newL1Messages.push(message); } - /** - - * @param nullifier - - */ public writeNullifier(nullifier: Fr) { this.newNullifiers.push(nullifier); } + public writeLog(log: Fr[]) { + this.newLogs.push(log); + } + /** * Merge Journal into parent * - Utxo objects are concatenated @@ -133,26 +127,26 @@ export class AvmJournal { throw new RootJournalCannotBeMerged(); } - const incomingFlush = this.flush(); - // Merge UTXOs - this.parentJournal.newCommitments = this.parentJournal.newCommitments.concat(incomingFlush.newCommitments); - this.parentJournal.newL1Message = this.parentJournal.newL1Message.concat(incomingFlush.newL1Messages); - this.parentJournal.newNullifiers = this.parentJournal.newNullifiers.concat(incomingFlush.newNullifiers); + this.parentJournal.newNoteHashes = this.parentJournal.newNoteHashes.concat(this.newNoteHashes); + this.parentJournal.newL1Messages = this.parentJournal.newL1Messages.concat(this.newL1Messages); + this.parentJournal.newNullifiers = this.parentJournal.newNullifiers.concat(this.newNullifiers); // Merge Public State - mergeContractMaps(this.parentJournal.storageWrites, incomingFlush.storageWrites); + mergeContractMaps(this.parentJournal.storageWrites, this.storageWrites); } - /** Access the current state of the journal + /** + * Access the current state of the journal * - * @returns a JournalData object that can be used to write to the storage + * @returns a JournalData object */ public flush(): JournalData { return { - newCommitments: this.newCommitments, - newL1Messages: this.newL1Message, + newNoteHashes: this.newNoteHashes, newNullifiers: this.newNullifiers, + newL1Messages: this.newL1Messages, + newLogs: this.newLogs, storageWrites: this.storageWrites, }; } diff --git a/yarn-project/acir-simulator/src/avm/opcodes/accrued_substate.test.ts b/yarn-project/acir-simulator/src/avm/opcodes/accrued_substate.test.ts new file mode 100644 index 00000000000..fd24b23eda4 --- /dev/null +++ b/yarn-project/acir-simulator/src/avm/opcodes/accrued_substate.test.ts @@ -0,0 +1,89 @@ +import { mock } from 'jest-mock-extended'; + +import { AvmMachineState } from '../avm_machine_state.js'; +import { Field } from '../avm_memory_types.js'; +import { initExecutionEnvironment } from '../fixtures/index.js'; +import { HostStorage } from '../journal/host_storage.js'; +import { AvmJournal } from '../journal/journal.js'; +import { EmitNoteHash, EmitNullifier, EmitUnencryptedLog, SendL2ToL1Message } from './accrued_substate.js'; +import { StaticCallStorageAlterError } from './storage.js'; + +describe('Accrued Substate', () => { + let journal: AvmJournal; + let machineState: AvmMachineState; + + beforeEach(() => { + const hostStorage = mock(); + journal = new AvmJournal(hostStorage); + machineState = new AvmMachineState(initExecutionEnvironment()); + }); + + it('Should append a new note hash correctly', async () => { + const value = new Field(69n); + machineState.memory.set(0, value); + + await new EmitNoteHash(0).execute(machineState, journal); + + const journalState = journal.flush(); + const expected = [value.toFr()]; + expect(journalState.newNoteHashes).toEqual(expected); + }); + + it('Should append a new nullifier correctly', async () => { + const value = new Field(69n); + machineState.memory.set(0, value); + + await new EmitNullifier(0).execute(machineState, journal); + + const journalState = journal.flush(); + const expected = [value.toFr()]; + expect(journalState.newNullifiers).toEqual(expected); + }); + + it('Should append unencrypted logs correctly', async () => { + const startOffset = 0; + + const values = [new Field(69n), new Field(420n), new Field(Field.MODULUS - 1n)]; + machineState.memory.setSlice(0, values); + + const length = values.length; + + await new EmitUnencryptedLog(startOffset, length).execute(machineState, journal); + + const journalState = journal.flush(); + const expected = values.map(v => v.toFr()); + expect(journalState.newLogs).toEqual([expected]); + }); + + it('Should append l1 to l2 messages correctly', async () => { + const startOffset = 0; + + const values = [new Field(69n), new Field(420n), new Field(Field.MODULUS - 1n)]; + machineState.memory.setSlice(0, values); + + const length = values.length; + + await new SendL2ToL1Message(startOffset, length).execute(machineState, journal); + + const journalState = journal.flush(); + const expected = values.map(v => v.toFr()); + expect(journalState.newLogs).toEqual([expected]); + }); + + it('All substate instructions should fail within a static call', async () => { + const executionEnvironment = initExecutionEnvironment({ isStaticCall: true }); + machineState = new AvmMachineState(executionEnvironment); + + const instructions = [ + new EmitNoteHash(0), + new EmitNullifier(0), + new EmitUnencryptedLog(0, 1), + new SendL2ToL1Message(0, 1), + ]; + + for (const instruction of instructions) { + const inst = () => instruction.execute(machineState, journal); + await expect(inst()).rejects.toThrowError(StaticCallStorageAlterError); + } + }); +}); diff --git a/yarn-project/acir-simulator/src/avm/opcodes/accrued_substate.ts b/yarn-project/acir-simulator/src/avm/opcodes/accrued_substate.ts index e69de29bb2d..de54edaa0c7 100644 --- a/yarn-project/acir-simulator/src/avm/opcodes/accrued_substate.ts +++ b/yarn-project/acir-simulator/src/avm/opcodes/accrued_substate.ts @@ -0,0 +1,84 @@ +import { AvmMachineState } from '../avm_machine_state.js'; +import { AvmJournal } from '../journal/journal.js'; +import { Instruction } from './instruction.js'; +import { StaticCallStorageAlterError } from './storage.js'; + +export class EmitNoteHash extends Instruction { + static type: string = 'EMITNOTEHASH'; + static numberOfOperands = 1; + + constructor(private noteHashOffset: number) { + super(); + } + + async execute(machineState: AvmMachineState, journal: AvmJournal): Promise { + if (machineState.executionEnvironment.isStaticCall) { + throw new StaticCallStorageAlterError(); + } + + const noteHash = machineState.memory.get(this.noteHashOffset).toFr(); + journal.writeNoteHash(noteHash); + + this.incrementPc(machineState); + } +} + +export class EmitNullifier extends Instruction { + static type: string = 'EMITNULLIFIER'; + static numberOfOperands = 1; + + constructor(private nullifierOffset: number) { + super(); + } + + async execute(machineState: AvmMachineState, journal: AvmJournal): Promise { + if (machineState.executionEnvironment.isStaticCall) { + throw new StaticCallStorageAlterError(); + } + + const nullifier = machineState.memory.get(this.nullifierOffset).toFr(); + journal.writeNullifier(nullifier); + + this.incrementPc(machineState); + } +} + +export class EmitUnencryptedLog extends Instruction { + static type: string = 'EMITUNENCRYPTEDLOG'; + static numberOfOperands = 2; + + constructor(private logOffset: number, private logSize: number) { + super(); + } + + async execute(machineState: AvmMachineState, journal: AvmJournal): Promise { + if (machineState.executionEnvironment.isStaticCall) { + throw new StaticCallStorageAlterError(); + } + + const log = machineState.memory.getSlice(this.logOffset, this.logSize).map(f => f.toFr()); + journal.writeLog(log); + + this.incrementPc(machineState); + } +} + +export class SendL2ToL1Message extends Instruction { + static type: string = 'EMITUNENCRYPTEDLOG'; + static numberOfOperands = 2; + + constructor(private msgOffset: number, private msgSize: number) { + super(); + } + + async execute(machineState: AvmMachineState, journal: AvmJournal): Promise { + if (machineState.executionEnvironment.isStaticCall) { + throw new StaticCallStorageAlterError(); + } + + const msg = machineState.memory.getSlice(this.msgOffset, this.msgSize).map(f => f.toFr()); + journal.writeLog(msg); + + this.incrementPc(machineState); + } +} diff --git a/yarn-project/acir-simulator/src/avm/opcodes/instruction.ts b/yarn-project/acir-simulator/src/avm/opcodes/instruction.ts index cccef0861fe..434bfc257db 100644 --- a/yarn-project/acir-simulator/src/avm/opcodes/instruction.ts +++ b/yarn-project/acir-simulator/src/avm/opcodes/instruction.ts @@ -20,13 +20,26 @@ export abstract class Instruction { } static checkTags(machineState: AvmMachineState, tag: TypeTag, ...offsets: number[]) { - for (const off of offsets) { - if (machineState.memory.getTag(off) !== tag) { - const error = `Offset ${off} has tag ${TypeTag[machineState.memory.getTag(off)]}, expected ${TypeTag[tag]}`; - throw new InstructionExecutionError(error); - } + for (const offset of offsets) { + checkTag(machineState, tag, offset); } } + + static checkTagsRange(machineState: AvmMachineState, tag: TypeTag, startOffset: number, size: number) { + for (let offset = startOffset; offset < startOffset + size; offset++) { + checkTag(machineState, tag, offset); + } + } +} + +/** + * Checks that the memory at the given offset has the given tag. + */ +function checkTag(machineState: AvmMachineState, tag: TypeTag, offset: number) { + if (machineState.memory.getTag(offset) !== tag) { + const error = `Offset ${offset} has tag ${TypeTag[machineState.memory.getTag(offset)]}, expected ${TypeTag[tag]}`; + throw new InstructionExecutionError(error); + } } export class InstructionExecutionError extends Error {