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 (#4747)
  • Loading branch information
dbanks12 authored Feb 27, 2024
1 parent 20dc67b commit 707f572
Show file tree
Hide file tree
Showing 12 changed files with 471 additions and 24 deletions.
35 changes: 35 additions & 0 deletions avm-transpiler/src/transpile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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<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::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:
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
80 changes: 80 additions & 0 deletions yarn-project/simulator/src/avm/avm_simulator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
38 changes: 36 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,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]);
});

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

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

Expand All @@ -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
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
Loading

0 comments on commit 707f572

Please sign in to comment.