Skip to main content

nullifier

Deriving a nullifier within an app contract

Let's assume a developer wants a nullifier of a note to be derived as:

nullifier = h(note_hash, nullifier_key);

... where the nullifier_key (Nkapp\Nkapp) belongs to the 'owner' of the note, and where the 'owner' is some address\address.

Here's example for how an app circuit could constrain the nullifier key to be correct:

Diagram

It's easiest to take a look at this first:

Alt text

Within the app circuit

Within the app, we can prove links between:

The link that's missing is to prove that Npkm\Npkm relates to nskapp\nskapp. To compute this missing link requires the nskm\nskm, which MUST NOT be passed into an app circuit, and may only be passed into a kernel circuit. See the next 'Within the kernel circuit' section for details of this logic.

The logic

Nkapp=poseidon2(nskapp)nullifier=poseidon2(note_hash,Nkapp)public_keys_hash=poseidon2(be_string_to_field(az_public_keys_hash"),Npkm,Tpkm,Ivpkm,Ovpkm)address=poseidon2(be_string_to_field(az_contract_address_v1"),public_keys_hash,partial_address)\begin{aligned} \Nkapp &= \text{poseidon2}(\nskapp) \\ \text{nullifier} &= \text{poseidon2}(\text{note\_hash}, \Nkapp) \\ \text{public\_keys\_hash} &= \text{poseidon2}(\text{be\_string\_to\_field}(``\text{az\_public\_keys\_hash}"), \Npkm, \Tpkm, \Ivpkm, \Ovpkm) \\ \address &= \text{poseidon2}(\text{be\_string\_to\_field}(``\text{az\_contract\_address\_v1}"), \text{public\_keys\_hash}, \text{partial\_address}) \end{aligned}

Note: the passing of points directly into the poseidon function is lazy notation: the keys would need to be serialized appropriately as fields into the poseidon function.

Recall an important point: the app circuit MUST NOT be given nskm\nskm. Indeed, nskapp\nskapp is derived (see earlier) as a hardened child of nskm\nskm, to prevent nskm\nskm from being reverse-derived by a malicious circuit. The linking of nskapp\nskapp to nskm\nskm is deferred to the kernel circuit (which can be trusted moreso than an app).

Recall also: Nkapp\Nkapp is used (instead of nskapp\nskapp) solely as a way of giving the user the option of sharing Nkapp\Nkapp with a trusted 3rd party, to give them the ability to view when a note has been nullified (although I'm not sure how useful this is, given that it would require brute-force effort from that party to determine which note hash has been nullified, with very little additional information).

The app circuit exposes, as public inputs, a "nullifier key validation request":

let nullifier_validation_request = KeyValidationRequest {
app_address: app_address,
claimed_hardened_child_sk: nsk_app,
claimed_parent_pk: Npk_m,
}

Within the Kernel Circuit

The kernel circuit can then validate the request (having been given nskm\nskm as a private input to the kernel circuit):

nskapp=derive_hardened_app_siloed_secret_key(“az_nsk_app",app_address,nskm)Npkm=nskmGnskapp==claimed_hardened_child_skNpkm==claimed_parent_pk\begin{aligned} \nskapp &= \text{derive\_hardened\_app\_siloed\_secret\_key}(\text{``az\_nsk\_app"}, \text{app\_address}, \nskm) \\ \Npkm &= \nskm \cdot G \\ \nskapp &== \text{claimed\_hardened\_child\_sk} \\ \Npkm &== \text{claimed\_parent\_pk} \\ \end{aligned}

If the kernel circuit succeeds in these calculations, then the Nkapp\Nkapp has been validated as having a known secret key, and belonging to the address\address.