Skip to content

Commit

Permalink
feat: change public nullifiers api (#5660)
Browse files Browse the repository at this point in the history
PublicContext.nullifier_exists now takes an `unsiloed_nullifier` and an `address`.
  • Loading branch information
fcarreiro authored Apr 10, 2024
1 parent c4e41a9 commit 986e7f9
Show file tree
Hide file tree
Showing 12 changed files with 76 additions and 43 deletions.
11 changes: 9 additions & 2 deletions avm-transpiler/src/transpile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -523,13 +523,17 @@ fn handle_nullifier_exists(
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());
if destinations.len() != 1 || inputs.len() != 2 {
panic!("Transpiler expects ForeignCall::CHECKNULLIFIEREXISTS to have 1 destinations and 2 inputs, 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 address_offset_operand = match &inputs[1] {
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"),
Expand All @@ -541,6 +545,9 @@ fn handle_nullifier_exists(
AvmOperand::U32 {
value: nullifier_offset_operand,
},
AvmOperand::U32 {
value: address_offset_operand,
},
AvmOperand::U32 {
value: exists_offset_operand,
},
Expand Down
6 changes: 3 additions & 3 deletions noir-projects/aztec-nr/aztec/src/context/avm_context.nr
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,8 @@ impl PublicContextInterface for AvmContext {
AztecAddress::zero()
}

fn nullifier_exists(self, nullifier: Field) -> bool {
nullifier_exists(nullifier) == 1
fn nullifier_exists(self, unsiloed_nullifier: Field, address: AztecAddress) -> bool {
nullifier_exists(unsiloed_nullifier, address.to_field()) == 1
}

fn push_nullifier_read_request(&mut self, nullifier: Field) {
Expand Down Expand Up @@ -278,7 +278,7 @@ fn note_hash_exists(note_hash: Field, leaf_index: Field) -> u8 {}
fn emit_note_hash(note_hash: Field) {}

#[oracle(avmOpcodeNullifierExists)]
fn nullifier_exists(nullifier: Field) -> u8 {}
fn nullifier_exists(nullifier: Field, address: Field) -> u8 {}

#[oracle(avmOpcodeEmitNullifier)]
fn emit_nullifier(nullifier: Field) {}
Expand Down
2 changes: 1 addition & 1 deletion noir-projects/aztec-nr/aztec/src/context/interface.nr
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,5 @@ trait PublicContextInterface {
function_selector: FunctionSelector,
args: [Field; ARGS_COUNT]
) -> [Field; RETURN_VALUES_LENGTH];
fn nullifier_exists(self, nullifier: Field) -> bool;
fn nullifier_exists(self, unsiloed_nullifier: Field, address: AztecAddress) -> bool;
}
8 changes: 5 additions & 3 deletions noir-projects/aztec-nr/aztec/src/context/public_context.nr
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use dep::protocol_types::{
public_circuit_public_inputs::PublicCircuitPublicInputs, read_request::ReadRequest,
side_effect::{SideEffect, SideEffectLinkedToNoteHash}
},
address::{AztecAddress, EthAddress},
hash::silo_nullifier, address::{AztecAddress, EthAddress},
constants::{
MAX_NEW_NOTE_HASHES_PER_CALL, MAX_NEW_L2_TO_L1_MSGS_PER_CALL, MAX_NEW_NULLIFIERS_PER_CALL,
MAX_PUBLIC_CALL_STACK_LENGTH_PER_CALL, MAX_PUBLIC_DATA_READS_PER_CALL,
Expand Down Expand Up @@ -220,8 +220,10 @@ impl PublicContextInterface for PublicContext {
self.inputs.public_global_variables.fee_recipient
}

fn nullifier_exists(self, nullifier: Field) -> bool {
nullifier_exists_oracle(nullifier) == 1
fn nullifier_exists(self, unsiloed_nullifier: Field, address: AztecAddress) -> bool {
// Current public can only check for settled nullifiers, so we always silo.
let siloed_nullifier = silo_nullifier(address, unsiloed_nullifier);
nullifier_exists_oracle(siloed_nullifier) == 1
}

fn push_nullifier_read_request(&mut self, nullifier: Field) {
Expand Down
8 changes: 3 additions & 5 deletions noir-projects/aztec-nr/aztec/src/initializer.nr
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,13 @@ fn mark_as_initialized<TContext>(context: &mut TContext) where TContext: Context
}

pub fn assert_is_initialized_public(context: &mut PublicContext) {
let init_nullifier = compute_contract_initialization_nullifier(context.this_address());
assert(context.nullifier_exists(init_nullifier), "Not initialized");
let init_nullifier = compute_unsiloed_contract_initialization_nullifier(context.this_address());
assert(context.nullifier_exists(init_nullifier, context.this_address()), "Not initialized");
}

pub fn assert_is_initialized_avm(context: &mut AvmContext) {
// WARNING: the AVM always expects UNSILOED nullifiers!
// TODO(fcarreiro@): Change current private/public to take unsiloed nullifiers and an address.
let init_nullifier = compute_unsiloed_contract_initialization_nullifier(context.this_address());
assert(context.nullifier_exists(init_nullifier), "Not initialized");
assert(context.nullifier_exists(init_nullifier, context.this_address()), "Not initialized");
}

pub fn assert_is_initialized_private(context: &mut PrivateContext) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,10 +196,8 @@ contract AvmTest {
}

#[aztec(public)]
fn assert_unsiloed_nullifier_acvm(nullifier: Field) {
// ACVM requires siloed nullifier.
let siloed_nullifier = silo_nullifier(context.this_address(), nullifier);
assert(context.nullifier_exists(siloed_nullifier));
fn assert_unsiloed_nullifier_acvm(unsiloed_nullifier: Field) {
assert(context.nullifier_exists(unsiloed_nullifier, context.this_address()));
}

#[aztec(public-vm)]
Expand Down Expand Up @@ -365,19 +363,19 @@ contract AvmTest {
// Use the standard context interface to check for a nullifier
#[aztec(public-vm)]
fn nullifier_exists(nullifier: Field) -> pub bool {
context.nullifier_exists(nullifier)
context.nullifier_exists(nullifier, context.this_address())
}

#[aztec(public-vm)]
fn assert_nullifier_exists(nullifier: Field) {
assert(context.nullifier_exists(nullifier));
assert(context.nullifier_exists(nullifier, context.this_address()));
}

// Use the standard context interface to emit a new nullifier
#[aztec(public-vm)]
fn emit_nullifier_and_check(nullifier: Field) {
context.push_new_nullifier(nullifier, 0);
let exists = context.nullifier_exists(nullifier);
let exists = context.nullifier_exists(nullifier, context.this_address());
assert(exists, "Nullifier was just created, but its existence wasn't detected!");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,10 +198,15 @@ contract InclusionProofs {
}
}

#[aztec(public)]
fn push_nullifier_public(nullifier: Field) {
context.push_new_nullifier(nullifier, 0);
}

// Proves nullifier existed at latest block
#[aztec(public)]
fn test_nullifier_inclusion_from_public(nullifier: Field) {
assert(context.nullifier_exists(nullifier));
assert(context.nullifier_exists(nullifier, context.this_address()));
}

#[aztec(private)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,10 +222,9 @@ describe('e2e_inclusion_proofs_contract', () => {
});

it('proves existence of a nullifier in public context', async () => {
const block = await pxe.getBlock(deploymentBlockNumber);
const nullifier = block?.body.txEffects[0].nullifiers[0];

await contract.methods.test_nullifier_inclusion_from_public(nullifier!).send().wait();
const unsiloedNullifier = new Fr(123456789n);
await contract.methods.push_nullifier_public(unsiloedNullifier).send().wait();
await contract.methods.test_nullifier_inclusion_from_public(unsiloedNullifier).send().wait();
});

it('nullifier existence failure case', async () => {
Expand Down
20 changes: 14 additions & 6 deletions yarn-project/simulator/src/avm/opcodes/accrued_substate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,11 +167,13 @@ describe('Accrued Substate', () => {
NullifierExists.opcode, // opcode
0x01, // indirect
...Buffer.from('12345678', 'hex'), // nullifierOffset
...Buffer.from('02345678', 'hex'), // addressOffset
...Buffer.from('456789AB', 'hex'), // existsOffset
]);
const inst = new NullifierExists(
/*indirect=*/ 0x01,
/*nullifierOffset=*/ 0x12345678,
/*addressOffset=*/ 0x02345678,
/*existsOffset=*/ 0x456789ab,
);

Expand All @@ -182,47 +184,53 @@ describe('Accrued Substate', () => {
it('Should correctly show false when nullifier does not exist', async () => {
const value = new Field(69n);
const nullifierOffset = 0;
const existsOffset = 1;
const addressOffset = 1;
const existsOffset = 2;

// mock host storage this so that persistable state's checkNullifierExists returns UNDEFINED
const commitmentsDb = mock<CommitmentsDB>();
commitmentsDb.getNullifierIndex.mockResolvedValue(Promise.resolve(undefined));
const hostStorage = initHostStorage({ commitmentsDb });
context = initContext({ persistableState: new AvmPersistableStateManager(hostStorage) });
const address = new Field(context.environment.storageAddress.toField());

context.machineState.memory.set(nullifierOffset, value);
await new NullifierExists(/*indirect=*/ 0, nullifierOffset, existsOffset).execute(context);
context.machineState.memory.set(addressOffset, address);
await new NullifierExists(/*indirect=*/ 0, nullifierOffset, addressOffset, existsOffset).execute(context);

const exists = context.machineState.memory.getAs<Uint8>(existsOffset);
expect(exists).toEqual(new Uint8(0));

const journalState = context.persistableState.flush();
expect(journalState.nullifierChecks).toEqual([
expect.objectContaining({ nullifier: value.toFr(), exists: false }),
expect.objectContaining({ nullifier: value.toFr(), storageAddress: address.toFr(), exists: false }),
]);
});

it('Should correctly show true when nullifier exists', async () => {
const value = new Field(69n);
const nullifierOffset = 0;
const existsOffset = 1;
const addressOffset = 1;
const existsOffset = 2;
const storedLeafIndex = BigInt(42);

// mock host storage this so that persistable state's checkNullifierExists returns true
const commitmentsDb = mock<CommitmentsDB>();
commitmentsDb.getNullifierIndex.mockResolvedValue(Promise.resolve(storedLeafIndex));
const hostStorage = initHostStorage({ commitmentsDb });
context = initContext({ persistableState: new AvmPersistableStateManager(hostStorage) });
const address = new Field(context.environment.storageAddress.toField());

context.machineState.memory.set(nullifierOffset, value);
await new NullifierExists(/*indirect=*/ 0, nullifierOffset, existsOffset).execute(context);
context.machineState.memory.set(addressOffset, address);
await new NullifierExists(/*indirect=*/ 0, nullifierOffset, addressOffset, existsOffset).execute(context);

const exists = context.machineState.memory.getAs<Uint8>(existsOffset);
expect(exists).toEqual(new Uint8(1));

const journalState = context.persistableState.flush();
expect(journalState.nullifierChecks).toEqual([
expect.objectContaining({ nullifier: value.toFr(), exists: true }),
expect.objectContaining({ nullifier: value.toFr(), storageAddress: address.toFr(), exists: true }),
]);
});
});
Expand Down
20 changes: 16 additions & 4 deletions yarn-project/simulator/src/avm/opcodes/accrued_substate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,19 +80,31 @@ 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];
static readonly wireFormat = [
OperandType.UINT8,
OperandType.UINT8,
OperandType.UINT32,
OperandType.UINT32,
OperandType.UINT32,
];

constructor(private indirect: number, private nullifierOffset: number, private existsOffset: number) {
constructor(
private indirect: number,
private nullifierOffset: number,
private addressOffset: number,
private existsOffset: number,
) {
super();
}

public async execute(context: AvmContext): Promise<void> {
const memoryOperations = { reads: 1, writes: 1, indirect: this.indirect };
const memoryOperations = { reads: 2, writes: 1, indirect: this.indirect };
const memory = context.machineState.memory.track(this.type);
context.machineState.consumeGas(this.gasCost(memoryOperations));

const nullifier = memory.get(this.nullifierOffset).toFr();
const exists = await context.persistableState.checkNullifierExists(context.environment.storageAddress, nullifier);
const address = memory.get(this.addressOffset).toFr();
const exists = await context.persistableState.checkNullifierExists(address, nullifier);

memory.set(this.existsOffset, exists ? new Uint8(1) : new Uint8(0));

Expand Down
12 changes: 7 additions & 5 deletions yellow-paper/docs/public-vm/gen/_instruction-set.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -353,8 +353,8 @@ M[existsOffset] = exists`}
<td style={{'text-align': 'center'}}>0x2f</td> <td style={{'text-align': 'center'}}><a id='isa-table-nullifierexists'/><Markdown>[`NULLIFIEREXISTS`](#isa-section-nullifierexists)</Markdown></td>
<td><Markdown>Check whether a nullifier exists in the nullifier tree (including nullifiers from earlier in the current transaction or from earlier in the current block)</Markdown></td>
<td><CodeBlock language="jsx">
{`exists = context.worldState.nullifiers.has(
hash(context.environment.storageAddress, M[nullifierOffset])
{`exists = pendingNullifiers.has(M[addressOffset], M[nullifierOffset]) || context.worldState.nullifiers.has(
hash(M[addressOffset], M[nullifierOffset])
)
M[existsOffset] = exists`}
</CodeBlock></td>
Expand Down Expand Up @@ -1464,11 +1464,12 @@ Check whether a nullifier exists in the nullifier tree (including nullifiers fro
- **indirect**: Toggles whether each memory-offset argument is an indirect offset. Rightmost bit corresponds to 0th offset arg, etc. Indirect offsets result in memory accesses like `M[M[offset]]` instead of the more standard `M[offset]`.
- **Args**:
- **nullifierOffset**: memory offset of the unsiloed nullifier
- **addressOffset**: memory offset of the storage address
- **existsOffset**: memory offset specifying where to store operation's result (whether the nullifier exists)
- **Expression**:
<CodeBlock language="jsx">
{`exists = context.worldState.nullifiers.has(
hash(context.environment.storageAddress, M[nullifierOffset])
{`exists = pendingNullifiers.has(M[addressOffset], M[nullifierOffset]) || context.worldState.nullifiers.has(
hash(M[addressOffset], M[nullifierOffset])
)
M[existsOffset] = exists`}
</CodeBlock>
Expand All @@ -1478,14 +1479,15 @@ M[existsOffset] = exists`}
TracedNullifierCheck {
callPointer: context.environment.callPointer,
nullifier: M[nullifierOffset],
storageAddress: M[addressOffset],
exists: exists, // defined above
counter: ++context.worldStateAccessTrace.accessCounter,
}
)`}
</CodeBlock>
- **Triggers downstream circuit operations**: Nullifier siloing (hash with storage contract address), nullifier tree membership check
- **Tag updates**: `T[existsOffset] = u8`
- **Bit-size**: 88
- **Bit-size**: 120


### <a id='isa-section-emitnullifier'/>`EMITNULLIFIER`
Expand Down
6 changes: 4 additions & 2 deletions yellow-paper/src/preprocess/InstructionSet/InstructionSet.js
Original file line number Diff line number Diff line change
Expand Up @@ -912,11 +912,12 @@ context.worldStateAccessTrace.newNoteHashes.append(
],
"Args": [
{"name": "nullifierOffset", "description": "memory offset of the unsiloed nullifier"},
{"name": "addressOffset", "description": "memory offset of the storage address"},
{"name": "existsOffset", "description": "memory offset specifying where to store operation's result (whether the nullifier exists)"},
],
"Expression": `
exists = context.worldState.nullifiers.has(
hash(context.environment.storageAddress, M[nullifierOffset])
exists = pendingNullifiers.has(M[addressOffset], M[nullifierOffset]) || context.worldState.nullifiers.has(
hash(M[addressOffset], M[nullifierOffset])
)
M[existsOffset] = exists
`,
Expand All @@ -926,6 +927,7 @@ context.worldStateAccessTrace.nullifierChecks.append(
TracedNullifierCheck {
callPointer: context.environment.callPointer,
nullifier: M[nullifierOffset],
storageAddress: M[addressOffset],
exists: exists, // defined above
counter: ++context.worldStateAccessTrace.accessCounter,
}
Expand Down

0 comments on commit 986e7f9

Please sign in to comment.