Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

time to support bools?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤷‍♂️ it's on the wishlist


#[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