Skip to content

Commit

Permalink
feat(avm-simulator): implement NOTEHASHEXISTS
Browse files Browse the repository at this point in the history
  • Loading branch information
fcarreiro committed Mar 1, 2024
1 parent 9633b0f commit c245ca0
Show file tree
Hide file tree
Showing 11 changed files with 360 additions and 53 deletions.
41 changes: 41 additions & 0 deletions avm-transpiler/src/transpile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ fn handle_foreign_call(
inputs: &Vec<ValueOrArray>,
) {
match function.as_str() {
"avmOpcodeNoteHashExists" => handle_note_hash_exists(avm_instrs, destinations, inputs),
"emitNoteHash" | "emitNullifier" => handle_emit_note_hash_or_nullifier(
function.as_str() == "emitNullifier",
avm_instrs,
Expand All @@ -255,6 +256,46 @@ fn handle_foreign_call(
}
}

/// Handle an AVM NOTEHASHEXISTS instruction
/// Adds the new instruction to the avm instructions list.
fn handle_note_hash_exists(
avm_instrs: &mut Vec<AvmInstruction>,
destinations: &Vec<ValueOrArray>,
inputs: &Vec<ValueOrArray>,
) {
let (note_hash_offset_operand, leaf_index_offset_operand) = match &inputs[..] {
[
ValueOrArray::MemoryAddress(nh_offset),
ValueOrArray::MemoryAddress(li_offset)
] => (nh_offset.to_usize() as u32, li_offset.to_usize() as u32),
_ => panic!(
"Transpiler expects ForeignCall::NOTEHASHEXISTS to have 2 inputs of type MemoryAddress, got {:?}", inputs
),
};
let exists_offset_operand = match &destinations[..] {
[ValueOrArray::MemoryAddress(offset)] => offset.to_usize() as u32,
_ => panic!(
"Transpiler expects ForeignCall::NOTEHASHEXISTS to have 1 output of type MemoryAddress, got {:?}", destinations
),
};
avm_instrs.push(AvmInstruction {
opcode: AvmOpcode::NOTEHASHEXISTS,
indirect: Some(ALL_DIRECT),
operands: vec![
AvmOperand::U32 {
value: note_hash_offset_operand,
},
AvmOperand::U32 {
value: leaf_index_offset_operand,
},
AvmOperand::U32 {
value: exists_offset_operand,
},
],
..Default::default()
});
}

/// Handle an AVM EMITNOTEHASH or EMITNULLIFIER instruction
/// (an emitNoteHash or 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 @@ -49,6 +49,9 @@ impl AVMContext {
// #[oracle(contractCallDepth)]
// pub fn contract_call_depth(self) -> Field {}

#[oracle(avmOpcodeNoteHashExists)]
pub fn check_note_hash_exists(self, note_hash: Field, leaf_index: Field) -> u8 {}

#[oracle(emitNoteHash)]
pub fn emit_note_hash(self, note_hash: Field) {}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ contract AvmTest {
// context.contract_call_depth()
// }

#[aztec(public-vm)]
fn check_note_hash_exists(note_hash: Field, leaf_index: Field) -> pub u8 {
context.check_note_hash_exists(note_hash, leaf_index)
}

// Use the standard context interface to emit a new note hash
#[aztec(public-vm)]
fn new_note_hash(note_hash: Field) {
Expand Down
55 changes: 55 additions & 0 deletions yarn-project/simulator/src/avm/avm_simulator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,61 @@ describe('AVM simulator', () => {
});

describe('Test tree access functions from noir contract', () => {
it(`Should execute contract function that checks if a note hash exists (it does not)`, async () => {
const noteHash = new Fr(42);
const leafIndex = new Fr(7);
const calldata = [noteHash, leafIndex];

// Get contract function artifact
const artifact = AvmTestContractArtifact.functions.find(f => f.name === 'avm_check_note_hash_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)]);

// Note hash existence check should be in trace
const trace = context.persistableState.flush();
expect(trace.noteHashChecks).toEqual([expect.objectContaining({ noteHash, leafIndex, exists: false })]);
});
it(`Should execute contract function that checks if a note hash exists (it does)`, async () => {
const noteHash = new Fr(42);
const leafIndex = new Fr(7);
const calldata = [noteHash, leafIndex];

// Get contract function artifact
const artifact = AvmTestContractArtifact.functions.find(f => f.name === 'avm_check_note_hash_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));

// note hash exists!
jest
.spyOn(context.persistableState.hostStorage.commitmentsDb, 'getCommitmentIndex')
.mockReturnValue(Promise.resolve(BigInt(7)));

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)]);

// Note hash existence check should be in trace
const trace = context.persistableState.flush();
expect(trace.noteHashChecks).toEqual([expect.objectContaining({ noteHash, leafIndex, exists: true })]);
});
it(`Should execute contract function to emit note hash (should be traced)`, async () => {
const utxo = new Fr(42);
const calldata = [utxo];
Expand Down
21 changes: 19 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,13 @@ 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';
import { TracedNoteHashCheck, TracedNullifierCheck } from './trace_types.js';

/**
* Data held within the journal
*/
export type JournalData = {
noteHashChecks: TracedNoteHashCheck[];
newNoteHashes: Fr[];
nullifierChecks: TracedNullifierCheck[];
newNullifiers: Fr[];
Expand Down Expand Up @@ -94,11 +95,26 @@ export class AvmPersistableStateManager {
return Promise.resolve(value);
}

/**
* Check if a note hash exists at the given leaf index, trace the check.
*
* @param storageAddress - the address of the contract whose storage is being read from
* @param noteHash - the unsiloed note hash being checked
* @param leafIndex - the leaf index being checked
* @returns true if the note hash exists at the given leaf index, false otherwise
*/
public async checkNoteHashExists(storageAddress: Fr, noteHash: Fr, leafIndex: Fr): Promise<boolean> {
const gotLeafIndex = await this.hostStorage.commitmentsDb.getCommitmentIndex(noteHash);
const exists = gotLeafIndex === leafIndex.toBigInt();
this.trace.traceNoteHashCheck(storageAddress, noteHash, exists, leafIndex);
return Promise.resolve(exists);
}

public writeNoteHash(noteHash: Fr) {
this.trace.traceNewNoteHash(/*storageAddress*/ Fr.ZERO, noteHash);
}

public async checkNullifierExists(storageAddress: Fr, nullifier: Fr) {
public async checkNullifierExists(storageAddress: Fr, nullifier: Fr): Promise<boolean> {
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 @@ -149,6 +165,7 @@ export class AvmPersistableStateManager {
*/
public flush(): JournalData {
return {
noteHashChecks: this.trace.noteHashChecks,
newNoteHashes: this.trace.newNoteHashes,
nullifierChecks: this.trace.nullifierChecks,
newNullifiers: this.trace.newNullifiers,
Expand Down
111 changes: 72 additions & 39 deletions yarn-project/simulator/src/avm/journal/trace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,28 @@ describe('world state access trace', () => {
});

describe('Basic tracing', () => {
it('Should trace commitments', () => {
it('Should trace note hash checks', () => {
const contractAddress = new Fr(1);
const noteHash = new Fr(2);
const exists = true;
const leafIndex = new Fr(42);

trace.traceNoteHashCheck(contractAddress, noteHash, exists, leafIndex);

expect(trace.noteHashChecks).toEqual([
{
callPointer: expect.any(Fr),
storageAddress: contractAddress,
noteHash: noteHash,
exists: exists,
counter: Fr.ZERO, // 0th access
endLifetime: expect.any(Fr),
leafIndex: leafIndex,
},
]);
expect(trace.getAccessCounter()).toBe(1);
});
it('Should trace note hashes', () => {
const contractAddress = new Fr(1);
const utxo = new Fr(2);
trace.traceNewNoteHash(contractAddress, utxo);
Expand Down Expand Up @@ -51,31 +72,36 @@ describe('world state access trace', () => {
const contractAddress = new Fr(1);
const slot = new Fr(2);
const value = new Fr(1);
const nullifier = new Fr(20);
const nullifierExists = false;
const nullifierIsPending = false;
const nullifierLeafIndex = Fr.ZERO;
const commitment = new Fr(10);
const noteHash = new Fr(10);
const noteHashLeafIndex = new Fr(88);
const noteHashExists = false;

let counter = 0;
trace.tracePublicStorageWrite(contractAddress, slot, value);
counter++;
trace.tracePublicStorageRead(contractAddress, slot, value);
counter++;
trace.traceNewNoteHash(contractAddress, commitment);
trace.traceNoteHashCheck(contractAddress, noteHash, noteHashExists, noteHashLeafIndex);
counter++;
trace.traceNewNoteHash(contractAddress, noteHash);
counter++;
trace.traceNullifierCheck(contractAddress, commitment, nullifierExists, nullifierIsPending, nullifierLeafIndex);
trace.traceNullifierCheck(contractAddress, nullifier, nullifierExists, nullifierIsPending, nullifierLeafIndex);
counter++;
trace.traceNewNullifier(contractAddress, commitment);
trace.traceNewNullifier(contractAddress, nullifier);
counter++;
trace.tracePublicStorageWrite(contractAddress, slot, value);
counter++;
trace.tracePublicStorageRead(contractAddress, slot, value);
counter++;
trace.traceNewNoteHash(contractAddress, commitment);
trace.traceNewNoteHash(contractAddress, noteHash);
counter++;
trace.traceNullifierCheck(contractAddress, commitment, nullifierExists, nullifierIsPending, nullifierLeafIndex);
trace.traceNullifierCheck(contractAddress, nullifier, nullifierExists, nullifierIsPending, nullifierLeafIndex);
counter++;
trace.traceNewNullifier(contractAddress, commitment);
trace.traceNewNullifier(contractAddress, nullifier);
counter++;
expect(trace.getAccessCounter()).toEqual(counter);
});
Expand All @@ -85,46 +111,43 @@ describe('world state access trace', () => {
const slot = new Fr(2);
const value = new Fr(1);
const valueT1 = new Fr(2);

const noteHash = new Fr(10);
const noteHashExists = false;
const noteHashLeafIndex = new Fr(88);
const noteHashT1 = new Fr(11);
const noteHashExistsT1 = true;
const noteHashLeafIndexT1 = new Fr(7);

const nullifierExists = false;
const nullifierIsPending = false;
const nullifierLeafIndex = Fr.ZERO;
const commitment = new Fr(10);
const commitmentT1 = new Fr(20);
const nullifier = new Fr(10);
const nullifierT1 = 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);
trace.traceNoteHashCheck(contractAddress, noteHash, noteHashExists, noteHashLeafIndex);
trace.traceNewNoteHash(contractAddress, noteHash);
trace.traceNullifierCheck(contractAddress, nullifier, nullifierExists, nullifierIsPending, nullifierLeafIndex);
trace.traceNewNullifier(contractAddress, nullifier);

const childTrace = new WorldStateAccessTrace(trace);
childTrace.tracePublicStorageWrite(contractAddress, slot, valueT1);
childTrace.tracePublicStorageRead(contractAddress, slot, valueT1);
childTrace.traceNewNoteHash(contractAddress, commitmentT1);
childTrace.traceNoteHashCheck(contractAddress, noteHashT1, noteHashExistsT1, noteHashLeafIndexT1);
childTrace.traceNewNoteHash(contractAddress, nullifierT1);
childTrace.traceNullifierCheck(
contractAddress,
commitmentT1,
nullifierT1,
nullifierExistsT1,
nullifierIsPendingT1,
nullifierLeafIndexT1,
);
childTrace.traceNewNullifier(contractAddress, commitmentT1);
childTrace.traceNewNullifier(contractAddress, nullifierT1);

const childCounterBeforeMerge = childTrace.getAccessCounter();
trace.acceptAndMerge(childTrace);
Expand All @@ -134,15 +157,25 @@ describe('world state access trace', () => {
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]);
expect(trace.newNoteHashes).toEqual([nullifier, nullifierT1]);
expect(trace.newNullifiers).toEqual([nullifier, nullifierT1]);
expect(trace.nullifierChecks).toEqual([
expect.objectContaining({
nullifier: nullifier,
exists: nullifierExists,
isPending: nullifierIsPending,
leafIndex: nullifierLeafIndex,
}),
expect.objectContaining({
nullifier: nullifierT1,
exists: nullifierExistsT1,
isPending: nullifierIsPendingT1,
leafIndex: nullifierLeafIndexT1,
}),
]);
expect(trace.noteHashChecks).toEqual([
expect.objectContaining({ noteHash: noteHash, exists: noteHashExists, leafIndex: noteHashLeafIndex }),
expect.objectContaining({ noteHash: noteHashT1, exists: noteHashExistsT1, leafIndex: noteHashLeafIndexT1 }),
]);
});
});
Loading

0 comments on commit c245ca0

Please sign in to comment.