Skip to content

Commit

Permalink
feat(avm-simulator): add NULLIFIEREXISTS opcode to avm simulator, tra…
Browse files Browse the repository at this point in the history
…nspiler, noir test, TS tests
  • Loading branch information
dbanks12 committed Feb 24, 2024
1 parent f92c3b7 commit 611a415
Show file tree
Hide file tree
Showing 10 changed files with 329 additions and 18 deletions.
35 changes: 35 additions & 0 deletions avm-transpiler/src/transpile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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<AvmInstruction>,
destinations: &Vec<ValueOrArray>,
inputs: &Vec<ValueOrArray>,
) {
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.
Expand Down
3 changes: 3 additions & 0 deletions noir-projects/aztec-nr/aztec/src/context/avm.nr
Original file line number Diff line number Diff line change
Expand Up @@ -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) {}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
32 changes: 30 additions & 2 deletions yarn-project/simulator/src/avm/journal/journal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ import { AvmPersistableStateManager, JournalData } from './journal.js';

describe('journal', () => {
let publicDb: MockProxy<PublicStateDB>;
let commitmentsDb: MockProxy<CommitmentsDB>;
let journal: AvmPersistableStateManager;

beforeEach(() => {
publicDb = mock<PublicStateDB>();
commitmentsDb = mock<CommitmentsDB>();
const contractsDb = mock<PublicContractsDB>();
const commitmentsDb = mock<CommitmentsDB>();

const hostStorage = new HostStorage(publicDb, contractsDb, commitmentsDb);
journal = new AvmPersistableStateManager(hostStorage);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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);

Expand All @@ -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]);
});

Expand All @@ -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);

Expand All @@ -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);

Expand All @@ -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
Expand Down
7 changes: 5 additions & 2 deletions yarn-project/simulator/src/avm/journal/journal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[][];
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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,
Expand Down
57 changes: 53 additions & 4 deletions yarn-project/simulator/src/avm/journal/trace.test.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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);
Expand All @@ -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]);
});
});
20 changes: 19 additions & 1 deletion yarn-project/simulator/src/avm/journal/trace.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Fr } from '@aztec/foundation/fields';
import { TracedNullifierCheck } from './trace_types.js';

export class WorldStateAccessTrace {
public accessCounter: number;
Expand All @@ -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[] = [];
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 611a415

Please sign in to comment.