From 611a4158b3d9a75349f982edddaee2b14ddea826 Mon Sep 17 00:00:00 2001 From: dbanks12 Date: Sat, 24 Feb 2024 00:35:46 +0000 Subject: [PATCH] feat(avm-simulator): add NULLIFIEREXISTS opcode to avm simulator, transpiler, noir test, TS tests --- avm-transpiler/src/transpile.rs | 35 ++++++++ .../aztec-nr/aztec/src/context/avm.nr | 3 + .../contracts/avm_test_contract/src/main.nr | 14 ++++ .../simulator/src/avm/journal/journal.test.ts | 32 +++++++- .../simulator/src/avm/journal/journal.ts | 7 +- .../simulator/src/avm/journal/trace.test.ts | 57 ++++++++++++- .../simulator/src/avm/journal/trace.ts | 20 ++++- .../simulator/src/avm/journal/trace_types.ts | 80 +++++++++++++++++++ .../src/avm/opcodes/accrued_substate.test.ts | 78 +++++++++++++++--- .../src/avm/opcodes/accrued_substate.ts | 21 +++++ 10 files changed, 329 insertions(+), 18 deletions(-) create mode 100644 yarn-project/simulator/src/avm/journal/trace_types.ts diff --git a/avm-transpiler/src/transpile.rs b/avm-transpiler/src/transpile.rs index 82d93e473064..b37927ce3d43 100644 --- a/avm-transpiler/src/transpile.rs +++ b/avm-transpiler/src/transpile.rs @@ -239,6 +239,7 @@ fn handle_foreign_call( ) { match function.as_str() { "emitNoteHash" => emit_emit_note_hash(avm_instrs, destinations, inputs), + "nullifierExists" => emit_nullifier_exists(avm_instrs, destinations, inputs), "emitNullifier" => emit_emit_nullifier(avm_instrs, destinations, inputs), "keccak256" | "sha256" => { emit_2_field_hash_instruction(avm_instrs, function, destinations, inputs) @@ -275,6 +276,40 @@ fn emit_emit_note_hash( }); } +/// Emit an AVM NULLIFIEREXISTS instruction +/// (a nullifierExists brillig foreign call was encountered) +/// Adds the new instruction to the avm instructions list. +fn emit_nullifier_exists( + avm_instrs: &mut Vec, + destinations: &Vec, + inputs: &Vec, +) { + if destinations.len() != 1 || inputs.len() != 1 { + panic!("Transpiler expects ForeignCall::CHECKNULLIFIEREXISTS to have 1 destinations and 1 input, got {} and {}", destinations.len(), inputs.len()); + } + let nullifier_offset_operand = match &inputs[0] { + ValueOrArray::MemoryAddress(offset) => offset.to_usize() as u32, + _ => panic!("Transpiler does not know how to handle ForeignCall::EMITNOTEHASH with HeapArray/Vector inputs"), + }; + let exists_offset_operand = match &destinations[0] { + ValueOrArray::MemoryAddress(offset) => offset.to_usize() as u32, + _ => panic!("Transpiler does not know how to handle ForeignCall::EMITNOTEHASH with HeapArray/Vector inputs"), + }; + avm_instrs.push(AvmInstruction { + opcode: AvmOpcode::EMITNULLIFIER, + indirect: Some(ALL_DIRECT), + operands: vec![ + AvmOperand::U32 { + value: nullifier_offset_operand, + }, + AvmOperand::U32 { + value: exists_offset_operand, + }, + ], + ..Default::default() + }); +} + /// Emit an AVM EMITNULLIFIER instruction /// (an emitNullifier brillig foreign call was encountered) /// Adds the new instruction to the avm instructions list. diff --git a/noir-projects/aztec-nr/aztec/src/context/avm.nr b/noir-projects/aztec-nr/aztec/src/context/avm.nr index a300862dd259..11853e67b046 100644 --- a/noir-projects/aztec-nr/aztec/src/context/avm.nr +++ b/noir-projects/aztec-nr/aztec/src/context/avm.nr @@ -52,6 +52,9 @@ impl AVMContext { #[oracle(emitNoteHash)] pub fn emit_note_hash(self, note_hash: Field) {} + #[oracle(nullifierExists)] + pub fn check_nullifier_exists(self, nullifier: Field) -> u8 {} + #[oracle(emitNullifier)] pub fn emit_nullifier(self, nullifier: Field) {} diff --git a/noir-projects/noir-contracts/contracts/avm_test_contract/src/main.nr b/noir-projects/noir-contracts/contracts/avm_test_contract/src/main.nr index 3e466334e0cf..8050704f67f1 100644 --- a/noir-projects/noir-contracts/contracts/avm_test_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/avm_test_contract/src/main.nr @@ -152,6 +152,20 @@ contract AvmTest { context.push_new_nullifier(nullifier, 0); } + // Use the standard context interface to emit a new nullifier + #[aztec(public-vm)] + fn check_nullifier_exists(nullifier: Field) -> pub u8 { + context.check_nullifier_exists(nullifier) + } + + // Use the standard context interface to emit a new nullifier + #[aztec(public-vm)] + fn emit_nullifier_and_check(nullifier: Field) { + context.emit_nullifier(nullifier); + let exists = context.check_nullifier_exists(nullifier); + assert(exists == 1, "Nullifier was just created, but its existence wasn't detected!"); + } + // Create the same nullifier twice (shouldn't work!) #[aztec(public-vm)] fn nullifier_collision(nullifier: Field) { diff --git a/yarn-project/simulator/src/avm/journal/journal.test.ts b/yarn-project/simulator/src/avm/journal/journal.test.ts index a4af031ebbbf..e40eeb1b8953 100644 --- a/yarn-project/simulator/src/avm/journal/journal.test.ts +++ b/yarn-project/simulator/src/avm/journal/journal.test.ts @@ -8,12 +8,13 @@ import { AvmPersistableStateManager, JournalData } from './journal.js'; describe('journal', () => { let publicDb: MockProxy; + let commitmentsDb: MockProxy; let journal: AvmPersistableStateManager; beforeEach(() => { publicDb = mock(); + commitmentsDb = mock(); const contractsDb = mock(); - const commitmentsDb = mock(); const hostStorage = new HostStorage(publicDb, contractsDb, commitmentsDb); journal = new AvmPersistableStateManager(hostStorage); @@ -60,6 +61,27 @@ describe('journal', () => { const journalUpdates = journal.flush(); expect(journalUpdates.newNoteHashes).toEqual([utxo]); }); + it('checkNullifierExists works for missing nullifiers', async () => { + const contractAddress = new Fr(1); + const utxo = new Fr(2); + const exists = await journal.checkNullifierExists(contractAddress, utxo); + expect(exists).toEqual(false); + + const journalUpdates = journal.flush(); + expect(journalUpdates.nullifierChecks.map(c => [c.nullifier, c.exists])).toEqual([[utxo, false]]); + }); + it('checkNullifierExists works for existing nullifiers', async () => { + const contractAddress = new Fr(1); + const utxo = new Fr(2); + const storedLeafIndex = BigInt(42); + + commitmentsDb.getNullifierIndex.mockResolvedValue(Promise.resolve(storedLeafIndex)); + const exists = await journal.checkNullifierExists(contractAddress, utxo); + expect(exists).toEqual(true); + + const journalUpdates = journal.flush(); + expect(journalUpdates.nullifierChecks.map(c => [c.nullifier, c.exists])).toEqual([[utxo, true]]); + }); it('Should maintain nullifiers', async () => { const contractAddress = new Fr(1); const utxo = new Fr(2); @@ -100,6 +122,7 @@ describe('journal', () => { journal.writeLog(logs); journal.writeL1Message(logs); await journal.writeNullifier(contractAddress, commitment); + await journal.checkNullifierExists(contractAddress, commitment); const childJournal = new AvmPersistableStateManager(journal.hostStorage, journal); childJournal.writeStorage(contractAddress, key, valueT1); @@ -108,6 +131,7 @@ describe('journal', () => { childJournal.writeLog(logsT1); childJournal.writeL1Message(logsT1); await childJournal.writeNullifier(contractAddress, commitmentT1); + await childJournal.checkNullifierExists(contractAddress, commitmentT1); journal.acceptNestedCallState(childJournal); @@ -132,6 +156,7 @@ describe('journal', () => { expect(journalUpdates.newNoteHashes).toEqual([commitment, commitmentT1]); expect(journalUpdates.newLogs).toEqual([logs, logsT1]); expect(journalUpdates.newL1Messages).toEqual([logs, logsT1]); + expect(journalUpdates.nullifierChecks.map(c => [c.nullifier, c.exists])).toEqual([[commitment, true], [commitmentT1, true]]); expect(journalUpdates.newNullifiers).toEqual([commitment, commitmentT1]); }); @@ -158,6 +183,7 @@ describe('journal', () => { await journal.readStorage(contractAddress, key); journal.writeNoteHash(commitment); await journal.writeNullifier(contractAddress, commitment); + await journal.checkNullifierExists(contractAddress, commitment); journal.writeLog(logs); journal.writeL1Message(logs); @@ -166,6 +192,7 @@ describe('journal', () => { await childJournal.readStorage(contractAddress, key); childJournal.writeNoteHash(commitmentT1); await childJournal.writeNullifier(contractAddress, commitmentT1); + await childJournal.checkNullifierExists(contractAddress, commitmentT1); childJournal.writeLog(logsT1); childJournal.writeL1Message(logsT1); @@ -189,8 +216,9 @@ describe('journal', () => { const slotWrites = contractWrites?.get(key.toBigInt()); expect(slotWrites).toEqual([value, valueT1]); - // Check that the UTXOs _traces_ are merged even on rejection + // Check that the world state _traces_ are merged even on rejection expect(journalUpdates.newNoteHashes).toEqual([commitment, commitmentT1]); + expect(journalUpdates.nullifierChecks.map(c => [c.nullifier, c.exists])).toEqual([[commitment, true], [commitmentT1, true]]); expect(journalUpdates.newNullifiers).toEqual([commitment, commitmentT1]); // Check that rejected Accrued Substate is absent diff --git a/yarn-project/simulator/src/avm/journal/journal.ts b/yarn-project/simulator/src/avm/journal/journal.ts index 083e78819230..b55b8d8f3386 100644 --- a/yarn-project/simulator/src/avm/journal/journal.ts +++ b/yarn-project/simulator/src/avm/journal/journal.ts @@ -4,12 +4,14 @@ import { HostStorage } from './host_storage.js'; import { Nullifiers } from './nullifiers.js'; import { PublicStorage } from './public_storage.js'; import { WorldStateAccessTrace } from './trace.js'; +import { TracedNullifierCheck } from './trace_types.js'; /** * Data held within the journal */ export type JournalData = { newNoteHashes: Fr[]; + nullifierChecks: TracedNullifierCheck[]; newNullifiers: Fr[]; newL1Messages: Fr[][]; @@ -97,8 +99,8 @@ export class AvmPersistableStateManager { } public async checkNullifierExists(storageAddress: Fr, nullifier: Fr) { - const [exists, _isPending, _leafIndex] = await this.nullifiers.checkExists(storageAddress, nullifier); - //this.trace.traceNullifierCheck(storageAddress, nullifier, exists, isPending, leafIndex); + const [exists, isPending, leafIndex] = await this.nullifiers.checkExists(storageAddress, nullifier); + this.trace.traceNullifierCheck(storageAddress, nullifier, exists, isPending, leafIndex); return Promise.resolve(exists); } @@ -148,6 +150,7 @@ export class AvmPersistableStateManager { public flush(): JournalData { return { newNoteHashes: this.trace.newNoteHashes, + nullifierChecks: this.trace.nullifierChecks, newNullifiers: this.trace.newNullifiers, newL1Messages: this.newL1Messages, newLogs: this.newLogs, diff --git a/yarn-project/simulator/src/avm/journal/trace.test.ts b/yarn-project/simulator/src/avm/journal/trace.test.ts index 6c974bfcc61f..9f89de19a62e 100644 --- a/yarn-project/simulator/src/avm/journal/trace.test.ts +++ b/yarn-project/simulator/src/avm/journal/trace.test.ts @@ -1,6 +1,7 @@ import { Fr } from '@aztec/foundation/fields'; import { WorldStateAccessTrace } from './trace.js'; +import { TracedNullifierCheck } from './trace_types.js'; describe('world state access trace', () => { let trace: WorldStateAccessTrace; @@ -17,7 +18,25 @@ describe('world state access trace', () => { expect(trace.newNoteHashes).toEqual([utxo]); expect(trace.getAccessCounter()).toEqual(1); }); - + it('Should trace nullifier checks', () => { + const contractAddress = new Fr(1); + const utxo = new Fr(2); + const exists = true; + const isPending = false; + const leafIndex = new Fr(42); + trace.traceNullifierCheck(contractAddress, utxo, exists, isPending, leafIndex); + const expectedCheck: TracedNullifierCheck = { + callPointer: Fr.ZERO, + storageAddress: contractAddress, + nullifier: utxo, + exists: exists, + counter: Fr.ZERO, // 0th access + endLifetime: Fr.ZERO, + isPending: isPending, + leafIndex: leafIndex + }; + expect(trace.nullifierChecks).toEqual([expectedCheck]); + }); it('Should trace nullifiers', () => { const contractAddress = new Fr(1); const utxo = new Fr(2); @@ -32,30 +51,60 @@ describe('world state access trace', () => { const slot = new Fr(2); const value = new Fr(1); const valueT1 = new Fr(2); + const nullifierExists = false; + const nullifierIsPending = false; + const nullifierLeafIndex = Fr.ZERO; const commitment = new Fr(10); const commitmentT1 = new Fr(20); + const nullifierExistsT1 = true; + const nullifierIsPendingT1 = false; + const nullifierLeafIndexT1 = new Fr(42); + + const expectedNullifierCheck: TracedNullifierCheck = { + callPointer: Fr.ZERO, + storageAddress: contractAddress, + nullifier: commitment, + exists: nullifierExists, + counter: new Fr(3), // 3rd access + endLifetime: Fr.ZERO, + isPending: nullifierIsPending, + leafIndex: nullifierLeafIndex, + }; + const expectedNullifierCheckT1: TracedNullifierCheck = { + callPointer: Fr.ZERO, + storageAddress: contractAddress, + nullifier: commitmentT1, + exists: nullifierExistsT1, + counter: new Fr(8), // 8th access + endLifetime: Fr.ZERO, + isPending: nullifierIsPendingT1, + leafIndex: nullifierLeafIndexT1, + }; trace.tracePublicStorageWrite(contractAddress, slot, value); trace.tracePublicStorageRead(contractAddress, slot, value); trace.traceNewNoteHash(contractAddress, commitment); + trace.traceNullifierCheck(contractAddress, commitment, nullifierExists, nullifierIsPending, nullifierLeafIndex); trace.traceNewNullifier(contractAddress, commitment); - expect(trace.getAccessCounter()).toEqual(4); + expect(trace.getAccessCounter()).toEqual(5); // 5 accesses above const childTrace = new WorldStateAccessTrace(trace); childTrace.tracePublicStorageWrite(contractAddress, slot, valueT1); childTrace.tracePublicStorageRead(contractAddress, slot, valueT1); childTrace.traceNewNoteHash(contractAddress, commitmentT1); + childTrace.traceNullifierCheck(contractAddress, commitmentT1, nullifierExistsT1, nullifierIsPendingT1, nullifierLeafIndexT1); childTrace.traceNewNullifier(contractAddress, commitmentT1); - expect(childTrace.getAccessCounter()).toEqual(8); + expect(childTrace.getAccessCounter()).toEqual(10); trace.acceptAndMerge(childTrace); - expect(trace.getAccessCounter()).toEqual(8); + expect(trace.getAccessCounter()).toEqual(10); const slotReads = trace.publicStorageReads?.get(contractAddress.toBigInt())?.get(slot.toBigInt()); const slotWrites = trace.publicStorageWrites?.get(contractAddress.toBigInt())?.get(slot.toBigInt()); expect(slotReads).toEqual([value, valueT1]); expect(slotWrites).toEqual([value, valueT1]); expect(trace.newNoteHashes).toEqual([commitment, commitmentT1]); + expect(trace.nullifierChecks).toEqual([expectedNullifierCheck, expectedNullifierCheckT1]); expect(trace.newNullifiers).toEqual([commitment, commitmentT1]); }); }); diff --git a/yarn-project/simulator/src/avm/journal/trace.ts b/yarn-project/simulator/src/avm/journal/trace.ts index bb84e466f525..ae1e09aece55 100644 --- a/yarn-project/simulator/src/avm/journal/trace.ts +++ b/yarn-project/simulator/src/avm/journal/trace.ts @@ -1,4 +1,5 @@ import { Fr } from '@aztec/foundation/fields'; +import { TracedNullifierCheck } from './trace_types.js'; export class WorldStateAccessTrace { public accessCounter: number; @@ -12,7 +13,7 @@ export class WorldStateAccessTrace { //public noteHashChecks: TracedNoteHashCheck[] = []; //public newNoteHashes: TracedNoteHash[] = []; public newNoteHashes: Fr[] = []; - //public nullifierChecks: TracedNullifierCheck[] = []; + public nullifierChecks: TracedNullifierCheck[] = []; //public newNullifiers: TracedNullifier[] = []; public newNullifiers: Fr[] = []; //public l1toL2MessageReads: TracedL1toL2MessageRead[] = []; @@ -73,6 +74,22 @@ export class WorldStateAccessTrace { this.incrementAccessCounter(); } + public traceNullifierCheck(storageAddress: Fr, nullifier: Fr, exists: boolean, isPending: boolean, leafIndex: Fr) { + // TODO: check if some threshold is reached for max new nullifier + const traced: TracedNullifierCheck = { + callPointer: Fr.ZERO, // FIXME + storageAddress, + nullifier, + exists, + counter: new Fr(this.accessCounter), + endLifetime: Fr.ZERO, + isPending, + leafIndex, + }; + this.nullifierChecks.push(traced); + this.incrementAccessCounter(); + } + public traceNewNullifier(_storageAddress: Fr, nullifier: Fr) { // TODO: check if some threshold is reached for max new nullifier //const traced: TracedNullifier = { @@ -105,6 +122,7 @@ export class WorldStateAccessTrace { mergeContractJournalMaps(this.publicStorageWrites, incomingTrace.publicStorageWrites); // Merge new note hashes and nullifiers this.newNoteHashes = this.newNoteHashes.concat(incomingTrace.newNoteHashes); + this.nullifierChecks = this.nullifierChecks.concat(incomingTrace.nullifierChecks); this.newNullifiers = this.newNullifiers.concat(incomingTrace.newNullifiers); // it is assumed that the incoming trace was initialized with this as parent, so accept counter this.accessCounter = incomingTrace.accessCounter; diff --git a/yarn-project/simulator/src/avm/journal/trace_types.ts b/yarn-project/simulator/src/avm/journal/trace_types.ts new file mode 100644 index 000000000000..9fa6f646f330 --- /dev/null +++ b/yarn-project/simulator/src/avm/journal/trace_types.ts @@ -0,0 +1,80 @@ +import { Fr } from '@aztec/foundation/fields'; + +//export type TracedContractCall = { +// callPointer: Fr; +// address: Fr; +// storageAddress: Fr; +// endLifetime: Fr; +//}; +// +//export type TracedPublicStorageRead = { +// callPointer: Fr; +// storageAddress: Fr; +// exists: boolean; +// slot: Fr; +// value: Fr; +// counter: Fr; +// endLifetime: Fr; +//}; +// +//export type TracedPublicStorageWrite = { +// callPointer: Fr; +// storageAddress: Fr; +// slot: Fr; +// value: Fr; +// counter: Fr; +// endLifetime: Fr; +//}; +// +//export type TracedNoteHashCheck = { +// callPointer: Fr; +// storageAddress: Fr; +// leafIndex: Fr; +// noteHash: Fr; +// exists: boolean; +// counter: Fr; +// endLifetime: Fr; +//}; +// +//export type TracedNoteHash = { +// callPointer: Fr; +// storageAddress: Fr; +// noteHash: Fr; +// counter: Fr; +// endLifetime: Fr; +//}; + +export type TracedNullifierCheck = { + callPointer: Fr; + storageAddress: Fr; + nullifier: Fr; + exists: boolean; + counter: Fr; + endLifetime: Fr; + // the fields below are relevant only to the public kernel + // and are therefore omitted from VM inputs + isPending: boolean; + leafIndex: Fr; +}; + +//export type TracedNullifier = { +// callPointer: Fr; +// storageAddress: Fr; +// nullifier: Fr; +// counter: Fr; +// endLifetime: Fr; +//}; +// +//export type TracedL1toL2MessageRead = { +// callPointer: Fr; +// portal: Fr; // EthAddress +// leafIndex: Fr; +// msgKey: Fr; +// exists: Fr; +// message: []; // omitted from VM public inputs +//}; +// +//export type TracedArchiveLeafCheck = { +// leafIndex: Fr; +// leaf: Fr; +//}; diff --git a/yarn-project/simulator/src/avm/opcodes/accrued_substate.test.ts b/yarn-project/simulator/src/avm/opcodes/accrued_substate.test.ts index 4072c61bd273..2a795e7352a7 100644 --- a/yarn-project/simulator/src/avm/opcodes/accrued_substate.test.ts +++ b/yarn-project/simulator/src/avm/opcodes/accrued_substate.test.ts @@ -2,11 +2,11 @@ import { mock } from 'jest-mock-extended'; import { CommitmentsDB } from '../../index.js'; import { AvmContext } from '../avm_context.js'; -import { Field } from '../avm_memory_types.js'; +import { Field, Uint8 } from '../avm_memory_types.js'; import { InstructionExecutionError } from '../errors.js'; import { initContext, initExecutionEnvironment, initHostStorage } from '../fixtures/index.js'; import { AvmPersistableStateManager } from '../journal/journal.js'; -import { EmitNoteHash, EmitNullifier, EmitUnencryptedLog, SendL2ToL1Message } from './accrued_substate.js'; +import { EmitNoteHash, EmitNullifier, EmitUnencryptedLog, NullifierExists, SendL2ToL1Message } from './accrued_substate.js'; import { StaticCallStorageAlterError } from './storage.js'; describe('Accrued Substate', () => { @@ -21,9 +21,9 @@ describe('Accrued Substate', () => { const buf = Buffer.from([ EmitNoteHash.opcode, // opcode 0x01, // indirect - ...Buffer.from('12345678', 'hex'), // dstOffset + ...Buffer.from('12345678', 'hex'), // offset ]); - const inst = new EmitNoteHash(/*indirect=*/ 0x01, /*dstOffset=*/ 0x12345678); + const inst = new EmitNoteHash(/*indirect=*/ 0x01, /*offset=*/ 0x12345678); expect(EmitNoteHash.deserialize(buf)).toEqual(inst); expect(inst.serialize()).toEqual(buf); @@ -41,14 +41,74 @@ describe('Accrued Substate', () => { }); }); + describe('NullifierExists', () => { + it('Should (de)serialize correctly', () => { + const buf = Buffer.from([ + NullifierExists.opcode, // opcode + 0x01, // indirect + ...Buffer.from('12345678', 'hex'), // nullifierOffset + ...Buffer.from('456789AB', 'hex'), // existsOffset + ]); + const inst = new NullifierExists(/*indirect=*/ 0x01, /*nullifierOffset=*/ 0x12345678, /*existsOffset=*/ 0x456789AB); + + expect(NullifierExists.deserialize(buf)).toEqual(inst); + expect(inst.serialize()).toEqual(buf); + }); + + it('Should correctly show false when nullifier does not exist', async () => { + const value = new Field(69n); + const nullifierOffset = 0; + const existsOffset = 1; + + // mock host storage this so that persistable state's checkNullifierExists returns UNDEFINED + const commitmentsDb = mock(); + commitmentsDb.getNullifierIndex.mockResolvedValue(Promise.resolve(undefined)); + const hostStorage = initHostStorage({ commitmentsDb }); + context = initContext({ persistableState: new AvmPersistableStateManager(hostStorage) }); + + context.machineState.memory.set(nullifierOffset, value); + await new NullifierExists(/*indirect=*/ 0, nullifierOffset, existsOffset).execute(context); + + const exists = context.machineState.memory.getAs(existsOffset); + expect(exists).toEqual(new Uint8(0)); + + const journalState = context.persistableState.flush(); + expect(journalState.nullifierChecks.length).toEqual(1); + expect(journalState.nullifierChecks[0].exists).toEqual(false); + }); + + it('Should correctly show true when nullifier exists', async () => { + const value = new Field(69n); + const nullifierOffset = 0; + const existsOffset = 1; + const storedLeafIndex = BigInt(42); + + // mock host storage this so that persistable state's checkNullifierExists returns true + const commitmentsDb = mock(); + commitmentsDb.getNullifierIndex.mockResolvedValue(Promise.resolve(storedLeafIndex)); + const hostStorage = initHostStorage({ commitmentsDb }); + context = initContext({ persistableState: new AvmPersistableStateManager(hostStorage) }); + + context.machineState.memory.set(nullifierOffset, value); + await new NullifierExists(/*indirect=*/ 0, nullifierOffset, existsOffset).execute(context); + + const exists = context.machineState.memory.getAs(existsOffset); + expect(exists).toEqual(new Uint8(1)); + + const journalState = context.persistableState.flush(); + expect(journalState.nullifierChecks.length).toEqual(1); + expect(journalState.nullifierChecks[0].exists).toEqual(true); + }); + }); + describe('EmitNullifier', () => { it('Should (de)serialize correctly', () => { const buf = Buffer.from([ EmitNullifier.opcode, // opcode 0x01, // indirect - ...Buffer.from('12345678', 'hex'), // dstOffset + ...Buffer.from('12345678', 'hex'), // offset ]); - const inst = new EmitNullifier(/*indirect=*/ 0x01, /*dstOffset=*/ 0x12345678); + const inst = new EmitNullifier(/*indirect=*/ 0x01, /*offset=*/ 0x12345678); expect(EmitNullifier.deserialize(buf)).toEqual(inst); expect(inst.serialize()).toEqual(buf); @@ -108,7 +168,7 @@ describe('Accrued Substate', () => { ...Buffer.from('12345678', 'hex'), // offset ...Buffer.from('a2345678', 'hex'), // length ]); - const inst = new EmitUnencryptedLog(/*indirect=*/ 0x01, /*dstOffset=*/ 0x12345678, /*length=*/ 0xa2345678); + const inst = new EmitUnencryptedLog(/*indirect=*/ 0x01, /*offset=*/ 0x12345678, /*length=*/ 0xa2345678); expect(EmitUnencryptedLog.deserialize(buf)).toEqual(inst); expect(inst.serialize()).toEqual(buf); @@ -138,7 +198,7 @@ describe('Accrued Substate', () => { ...Buffer.from('12345678', 'hex'), // offset ...Buffer.from('a2345678', 'hex'), // length ]); - const inst = new SendL2ToL1Message(/*indirect=*/ 0x01, /*dstOffset=*/ 0x12345678, /*length=*/ 0xa2345678); + const inst = new SendL2ToL1Message(/*indirect=*/ 0x01, /*offset=*/ 0x12345678, /*length=*/ 0xa2345678); expect(SendL2ToL1Message.deserialize(buf)).toEqual(inst); expect(inst.serialize()).toEqual(buf); @@ -160,7 +220,7 @@ describe('Accrued Substate', () => { }); }); - it('All substate instructions should fail within a static call', async () => { + it('All substate emission instructions should fail within a static call', async () => { context = initContext({ env: initExecutionEnvironment({ isStaticCall: true }) }); const instructions = [ diff --git a/yarn-project/simulator/src/avm/opcodes/accrued_substate.ts b/yarn-project/simulator/src/avm/opcodes/accrued_substate.ts index 503c4313a519..084f8b4ebc95 100644 --- a/yarn-project/simulator/src/avm/opcodes/accrued_substate.ts +++ b/yarn-project/simulator/src/avm/opcodes/accrued_substate.ts @@ -1,4 +1,5 @@ import type { AvmContext } from '../avm_context.js'; +import { Uint8 } from '../avm_memory_types.js'; import { InstructionExecutionError } from '../errors.js'; import { NullifierCollisionError } from '../journal/nullifiers.js'; import { Opcode, OperandType } from '../serialization/instruction_serialization.js'; @@ -27,6 +28,26 @@ export class EmitNoteHash extends Instruction { } } +export class NullifierExists extends Instruction { + static type: string = 'NULLIFIEREXISTS'; + static readonly opcode: Opcode = Opcode.NULLIFIEREXISTS; + // Informs (de)serialization. See Instruction.deserialize. + static readonly wireFormat = [OperandType.UINT8, OperandType.UINT8, OperandType.UINT32, OperandType.UINT32]; + + constructor(private indirect: number, private nullifierOffset: number, private existsOffset: number) { + super(); + } + + async execute(context: AvmContext): Promise { + const nullifier = context.machineState.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)); + + context.machineState.incrementPc(); + } +} + export class EmitNullifier extends Instruction { static type: string = 'EMITNULLIFIER'; static readonly opcode: Opcode = Opcode.EMITNULLIFIER;