diff --git a/avm-transpiler/src/transpile.rs b/avm-transpiler/src/transpile.rs index c53b24b6883..b3417d18ad2 100644 --- a/avm-transpiler/src/transpile.rs +++ b/avm-transpiler/src/transpile.rs @@ -244,6 +244,7 @@ fn handle_foreign_call( destinations, inputs, ), + "nullifierExists" => handle_nullifier_exists(avm_instrs, destinations, inputs), "keccak256" | "sha256" => { handle_2_field_hash_instruction(avm_instrs, function, destinations, inputs) } @@ -298,6 +299,40 @@ fn handle_emit_note_hash_or_nullifier( }); } +/// Handle an AVM NULLIFIEREXISTS instruction +/// (a nullifierExists brillig foreign call was encountered) +/// Adds the new instruction to the avm instructions list. +fn handle_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::NULLIFIEREXISTS, + indirect: Some(ALL_DIRECT), + operands: vec![ + AvmOperand::U32 { + value: nullifier_offset_operand, + }, + AvmOperand::U32 { + value: exists_offset_operand, + }, + ], + ..Default::default() + }); +} + /// Two field hash instructions represent instruction's that's outputs are larger than a field element /// /// This includes: diff --git a/noir-projects/aztec-nr/aztec/src/context/avm.nr b/noir-projects/aztec-nr/aztec/src/context/avm.nr index a300862dd25..11853e67b04 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 3e466334e0c..8050704f67f 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/avm_simulator.test.ts b/yarn-project/simulator/src/avm/avm_simulator.test.ts index ee8278963ef..977ece373b9 100644 --- a/yarn-project/simulator/src/avm/avm_simulator.test.ts +++ b/yarn-project/simulator/src/avm/avm_simulator.test.ts @@ -280,6 +280,86 @@ describe('AVM simulator', () => { expect(context.persistableState.flush().newNullifiers).toEqual([utxo]); }); + it(`Should execute contract function that checks if a nullifier existence (it does not)`, async () => { + const utxo = new Fr(42); + const calldata = [utxo]; + + // Get contract function artifact + const artifact = AvmTestContractArtifact.functions.find(f => f.name === 'avm_check_nullifier_exists')!; + + // Decode bytecode into instructions + const bytecode = Buffer.from(artifact.bytecode, 'base64'); + + const context = initContext({ env: initExecutionEnvironment({ calldata }) }); + jest + .spyOn(context.persistableState.hostStorage.contractsDb, 'getBytecode') + .mockReturnValue(Promise.resolve(bytecode)); + + await new AvmSimulator(context).execute(); + const results = await new AvmSimulator(context).execute(); + expect(results.reverted).toBe(false); + expect(results.output).toEqual([/*exists=false*/ new Fr(0)]); + + // Nullifier existence check should be in trace + const sideEffects = context.persistableState.flush(); + expect(sideEffects.nullifierChecks.length).toEqual(1); + expect(sideEffects.nullifierChecks[0].exists).toEqual(false); + }); + it(`Should execute contract function that checks if a nullifier existence (it does)`, async () => { + const utxo = new Fr(42); + const calldata = [utxo]; + + // Get contract function artifact + const artifact = AvmTestContractArtifact.functions.find(f => f.name === 'avm_check_nullifier_exists')!; + + // Decode bytecode into instructions + const bytecode = Buffer.from(artifact.bytecode, 'base64'); + + const context = initContext({ env: initExecutionEnvironment({ calldata }) }); + jest + .spyOn(context.persistableState.hostStorage.contractsDb, 'getBytecode') + .mockReturnValue(Promise.resolve(bytecode)); + + // nullifier exists! + jest + .spyOn(context.persistableState.hostStorage.commitmentsDb, 'getNullifierIndex') + .mockReturnValue(Promise.resolve(BigInt(42))); + + await new AvmSimulator(context).execute(); + const results = await new AvmSimulator(context).execute(); + expect(results.reverted).toBe(false); + expect(results.output).toEqual([/*exists=true*/ new Fr(1)]); + + // Nullifier existence check should be in trace + const sideEffects = context.persistableState.flush(); + expect(sideEffects.nullifierChecks.length).toEqual(1); + expect(sideEffects.nullifierChecks[0].exists).toEqual(true); + }); + it(`Should execute contract function that checks emits a nullifier and checks its existence`, async () => { + const utxo = new Fr(42); + const calldata = [utxo]; + + // Get contract function artifact + const artifact = AvmTestContractArtifact.functions.find(f => f.name === 'avm_emit_nullifier_and_check')!; + + // Decode bytecode into instructions + const bytecode = Buffer.from(artifact.bytecode, 'base64'); + + const context = initContext({ env: initExecutionEnvironment({ calldata }) }); + jest + .spyOn(context.persistableState.hostStorage.contractsDb, 'getBytecode') + .mockReturnValue(Promise.resolve(bytecode)); + + await new AvmSimulator(context).execute(); + const results = await new AvmSimulator(context).execute(); + expect(results.reverted).toBe(false); + + // Nullifier existence check should be in trace + const sideEffects = context.persistableState.flush(); + expect(sideEffects.newNullifiers).toEqual([utxo]); + expect(sideEffects.nullifierChecks.length).toEqual(1); + expect(sideEffects.nullifierChecks[0].exists).toEqual(true); + }); it(`Should execute contract function that emits same nullifier twice (should fail)`, async () => { const utxo = new Fr(42); const calldata = [utxo]; diff --git a/yarn-project/simulator/src/avm/journal/journal.test.ts b/yarn-project/simulator/src/avm/journal/journal.test.ts index a4af031ebbb..dabeee98751 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,10 @@ 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 +186,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 +195,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 +219,12 @@ 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 083e7881923..b55b8d8f338 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 6c974bfcc61..e26ecb185e4 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; @@ -9,7 +10,7 @@ describe('world state access trace', () => { trace = new WorldStateAccessTrace(); }); - describe('UTXOs', () => { + describe('Basic tracing', () => { it('Should trace commitments', () => { const contractAddress = new Fr(1); const utxo = new Fr(2); @@ -17,7 +18,26 @@ 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]); + expect(trace.getAccessCounter()).toEqual(1); + }); it('Should trace nullifiers', () => { const contractAddress = new Fr(1); const utxo = new Fr(2); @@ -27,35 +47,102 @@ describe('world state access trace', () => { }); }); + it('Access counter should properly count accesses', () => { + const contractAddress = new Fr(1); + const slot = new Fr(2); + const value = new Fr(1); + const nullifierExists = false; + const nullifierIsPending = false; + const nullifierLeafIndex = Fr.ZERO; + const commitment = new Fr(10); + + let counter = 0; + trace.tracePublicStorageWrite(contractAddress, slot, value); + counter++; + trace.tracePublicStorageRead(contractAddress, slot, value); + counter++; + trace.traceNewNoteHash(contractAddress, commitment); + counter++; + trace.traceNullifierCheck(contractAddress, commitment, nullifierExists, nullifierIsPending, nullifierLeafIndex); + counter++; + trace.traceNewNullifier(contractAddress, commitment); + counter++; + trace.tracePublicStorageWrite(contractAddress, slot, value); + counter++; + trace.tracePublicStorageRead(contractAddress, slot, value); + counter++; + trace.traceNewNoteHash(contractAddress, commitment); + counter++; + trace.traceNullifierCheck(contractAddress, commitment, nullifierExists, nullifierIsPending, nullifierLeafIndex); + counter++; + trace.traceNewNullifier(contractAddress, commitment); + counter++; + expect(trace.getAccessCounter()).toEqual(counter); + }); + it('Should merge two traces together', () => { const contractAddress = new Fr(1); 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 = { + nullifier: commitment, + exists: nullifierExists, + isPending: nullifierIsPending, + leafIndex: nullifierLeafIndex, + }; + const expectedNullifierCheckT1 = { + nullifier: commitmentT1, + exists: nullifierExistsT1, + 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); 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); + const childCounterBeforeMerge = childTrace.getAccessCounter(); trace.acceptAndMerge(childTrace); - expect(trace.getAccessCounter()).toEqual(8); + expect(trace.getAccessCounter()).toEqual(childCounterBeforeMerge); 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.map(c => ({ + nullifier: c.nullifier, + exists: c.exists, + isPending: c.isPending, + leafIndex: c.leafIndex, + })), + ).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 bb84e466f52..dcd266a4313 100644 --- a/yarn-project/simulator/src/avm/journal/trace.ts +++ b/yarn-project/simulator/src/avm/journal/trace.ts @@ -1,5 +1,7 @@ import { Fr } from '@aztec/foundation/fields'; +import { TracedNullifierCheck } from './trace_types.js'; + export class WorldStateAccessTrace { public accessCounter: number; //public contractCalls: Array = []; @@ -12,7 +14,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[] = []; @@ -27,7 +29,7 @@ export class WorldStateAccessTrace { } public tracePublicStorageRead(storageAddress: Fr, slot: Fr, value: Fr /*, _exists: boolean*/) { - // TODO: check if some threshold is reached for max storage reads + // TODO(4805): check if some threshold is reached for max storage reads // (need access to parent length, or trace needs to be initialized with parent's contents) //const traced: TracedPublicStorageRead = { // callPointer: Fr.ZERO, @@ -44,7 +46,7 @@ export class WorldStateAccessTrace { } public tracePublicStorageWrite(storageAddress: Fr, slot: Fr, value: Fr) { - // TODO: check if some threshold is reached for max storage writes + // TODO(4805): check if some threshold is reached for max storage writes // (need access to parent length, or trace needs to be initialized with parent's contents) //const traced: TracedPublicStorageWrite = { // callPointer: Fr.ZERO, @@ -60,7 +62,7 @@ export class WorldStateAccessTrace { } public traceNewNoteHash(_storageAddress: Fr, noteHash: Fr) { - // TODO: check if some threshold is reached for max new note hash + // TODO(4805): check if some threshold is reached for max new note hash //const traced: TracedNoteHash = { // callPointer: Fr.ZERO, // storageAddress, @@ -73,8 +75,24 @@ export class WorldStateAccessTrace { this.incrementAccessCounter(); } + public traceNullifierCheck(storageAddress: Fr, nullifier: Fr, exists: boolean, isPending: boolean, leafIndex: Fr) { + // TODO(4805): 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 + // TODO(4805): check if some threshold is reached for max new nullifier //const traced: TracedNullifier = { // callPointer: Fr.ZERO, // storageAddress, @@ -105,6 +123,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 00000000000..9fa6f646f33 --- /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 4072c61bd27..b18fc8d7d61 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,17 @@ 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 +27,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 +47,78 @@ 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 +178,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 +208,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 +230,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 04d16b5c264..dbee66f7e01 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; diff --git a/yarn-project/simulator/src/avm/serialization/bytecode_serialization.ts b/yarn-project/simulator/src/avm/serialization/bytecode_serialization.ts index 472de699af5..eb976211034 100644 --- a/yarn-project/simulator/src/avm/serialization/bytecode_serialization.ts +++ b/yarn-project/simulator/src/avm/serialization/bytecode_serialization.ts @@ -26,6 +26,7 @@ import { Mov, Mul, Not, + NullifierExists, Or, Origin, Portal, @@ -113,7 +114,7 @@ const INSTRUCTION_SET = () => [SStore.opcode, SStore], // Public Storage //[NoteHashExists.opcode, NoteHashExists], // Notes & Nullifiers [EmitNoteHash.opcode, EmitNoteHash], // Notes & Nullifiers - //[NullifierExists.opcode, NullifierExists], // Notes & Nullifiers + [NullifierExists.opcode, NullifierExists], // Notes & Nullifiers [EmitNullifier.opcode, EmitNullifier], // Notes & Nullifiers //[Readl1tol2msg.opcode, Readl1tol2msg], // Messages //[HeaderMember.opcode, HeaderMember], // Header