Moved to https://github.com/skaunov/ab2/tree/main/OfflineMembership and hence archived.
Here's Noir circuit to prove and verify ownership of a NFT from a paricular collection (i.e. address) on Aztec.
The circuit takes block number as a point for which assertions are made and bunch of data required for proving. The proof asserts that the address has a note from the contract and it's not nullified.
For proving user needs access to a PXE with their data and the preimage of a note they'd like to use for proving. For verification it's only the exported data of the block is needed, so it could be done completely without access to the chain (or even Internet if user can handover the proof).
Coming soon:
generalization to accept more notes types,- folding proofs at number of blocks,
- ...if you have a demand -- add it as an issue.
<./artifacts> contains compiled items.
The contract dir contains the helper contract needed to get witnesses from PXE while proving.
<./circuit> contains the Noir app.
<./tests> contains script and data useful for development.
There couple of ways to run a circuit. https://noir-lang.org/docs/dev/tutorials/noirjs_app#some-noirjs describes a JS way to use it.
The circuit takes quite a number of arguments, let's see a way to get those. Note that public items are needed for a verifier to process a proof.
public
Notice it contains the block number of interest among the other info.
await pxe.getBlock(blockNumberOfInterest).header.toFields()
const nsk_m = deriveKeys(theWallet.getSecretKey().masterNullifierSecretKey);
[nsk_m.lo, nsk_m.hi]
For proving an user will need to present the full note, which they should have somewhere.
PXE kinda don't store/provide a note content. Aztec examples/tests offer a following way, notice the note should be saved while created.
const theReceipt = await theContract.methods.create_note([...args]).send().wait({
debug: true, waitForNotesSync: true,
});
({ visibleIncomingNotes } = theReceipt.debugInfo!);
visibleIncomingNotes[theNoteDebugIndex].note
public \
visibleIncomingNotes[theNoteDebugIndex].nonce
visibleIncomingNotes[theNoteDebugIndex].storageSlot
Here the helper contract comes into play, since PXE has only on-chain API for getting a value.
import { GetMembershipWitnessContract } from './artifacts/GetMembershipWitness';
const getMembershipWitness = await GetMembershipWitnessContract.at(
await GetMembershipWitnessContract.deploy(wallet)
.send({ contractAddressSalt }).wait()
.contract.address,
theWallet
);
This needs the note hashed for nullifying variant. You can compute that or get/save along with the note itself as in the previous section.
/// see "The note" section
({ noteHashes } = theReceipt.debugInfo!);
const theNoteHashed = noteHashes[theNoteDebugIndex]
it , which you can produce or get/save along with the note itself (as `` - see the previous section).
await getMembershipWitness.methods.get_the(
blockNumberOfInterest,
theNoteHashed
).simulate()
Find nsk_m
and theContract.address
above.
(I didn't found an util computing the nullifier, if you can straighten this - pls, add the correct API.)
await getMembershipWitness.methods.low_nullifier(
blockNumberOfInterest,
poseidon2HashWithSeparator(
[
theNoteHashed,
computeAppNullifierSecretKey(nsk_m, theContract.address)
],
53
)
).simulate()
There's a blocker for making the circuit generic over a note type. NullifiableNote
leaves with only two choice: PrivateContext
or unconstrained
. Current implementation just copy a trait method code in the contrained environment; it seems to me that the root cause is the same reason making the trait to be a boilerplate. (And making up a whole PrivateContext
in this ciruit is just unreasonable.)