Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Social Cipher] Profile.io proof personal information with NFT (age-verification) #17

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
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
82 changes: 82 additions & 0 deletions profile.io-social-cipher/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Profile.io: privacy preserving identity, nationality and age verification

The Profile.io team are building an open platform and network for individuals and organisations to verify themselves and their online presence, to unlock trusted transactions, including privacy preserving stablecoin payments between users and organisations.

Using a profile.io user's indentity verification KYC results to prove user's adult verification and/or nationality, gender, any other user's personal information without revealing personal data by using ~~Aztec Connect~~ Noir circuit ZKP. Scoping service for third parties to verify such information privately, either through profile.io/verify or their own user flows (eg. exploring potentially using Frames.js).


## Challenge Selection

- [ ] ZKEmail Guardian
- [x] Social Cipher

**Note**: You can change which challenges you've selected during the competition if you'd like. You are not bound by your choices or descriptions entered during the one week check-in.

## Team information

[Profile.io](https://www.profile.io/)

## Technical Approach

We'd like to use Aztec ZKP for proving adult verification (or any other user info) on our app.
~~We have already smart contract that mints NFTs on Polygon mainnet~~

The plan is to use ZKP and Aztec Note when user inputs his/her identity including age then the user can use it when the age verification is required without revealing details.
By default, all user information is private. When a verifier request proof, the user can select data that want to reveal and send. Only the verifier can open and see the requested data.

Here is the user flow:
1. In FE (frontend), a user inputs his birthdate (ex: 20/04/1995)
1. The FE calls Aztec contract to mint a private NFT with the Note which has his birthdate (at this step, PXE creates ZKP when a private function is called on the Aztec contract if I am understood properly)
1. Now the user owns a private NFT that has a note
1. Another user (verifier) wants to verify if the user is an adult or not. So the verifier sends an age-verification-request with his address.
1. The user doesn't want to reveal his exact age but can prove whether he is adult or not by ~~using ZKP (or Note?)~~ sending a note that is encrypted with verifier's address.
1. There is a "request age-verification" button on the UI and when verifier clicks that button -> Calling Aztec contract to request the user's age verification -> If the user accept the request -> FE displays whether the user is an adult or not on the UI.


## Some technical questions
a. At above step 2, the created Note is an actual ZKP? I am confused the relation between Note and ZKP?

b. At above step 6, can Aztec contract retrieve age-NFT data without the NFT owner's permission? Do I need the "Witness"?

c. At above step 6, if Aztec contract can retrieve age-NFT data (birthdate), can the Aztec contract process calculation of the Note data? For example, `isUserAdult(note)`, `isUserOver50(note)`, `isUserOver70(note)`.

## Expected Outcomes
* A user should be able to add personal encrypted data and only the user can see the data by default.

* The user should be able to mint NFTs that contains encrypted personal data

* A verifier can request to personal information of users such as age, nationalities, gender, etc (For the first implementation, whether user is over 18 years or not).

* The user can accept the request and send note that only can be opened by the verifier.

Providing a quick age verification feature.

## Lessons Learned (For Submission)

- What are the most important takeaways from your project?

As a newbie in Aztec world, understanding its concept and implementation were such a challenge.
I'd say understanding how the "privacy" is protected is the key. Many logics are executed in PXE which is in user's device and the proof (ZKP) will be posted
to the on-chain via sequencer.

The note is another important thing I have learnt. I believe it has lots of potentials since it can be open and close (private/public) by owner.

I am still discovering Aztec wallet/account. Not sure how to manage wallet/account is the best way. Hopefully I will figure it out soon.

- Are there any patterns or best practices that you've learned that would be useful for other projects?

It's not any patterns or best practices but the 'aztec-packages' is very useful source to learn about real Aztec implementations.

- Highlight reusable code patterns, key code snippets, and best practices - what are some of the ‘lego bricks’ you’ve built, and how could someone else best use them?

This repo is aming for specific situation. Like minting a NFT with a note that contains user's personal information.
However, this can be useful source for interactions between the frontend and Aztec contract.

## Project Links (For Submission)

* The code is in this repo
* We are looking for an opportunity to integrate Aztec contract with [Profile.io](https://www.profile.io)

## Video Demo (For Submission)

[Demo link](https://www.youtube.com/watch?v=BBuhFJGYk50)
10 changes: 10 additions & 0 deletions profile.io-social-cipher/aztec/Nargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[package]
name = "zkpTestContract"
type = "contract"
authors = [""]
compiler_version = ">=0.33.0"

[dependencies]
aztec = { git="https://github.com/AztecProtocol/aztec-packages/", tag="aztec-packages-v0.57.0", directory="noir-projects/aztec-nr/aztec" }
value_note = { git="https://github.com/AztecProtocol/aztec-packages/", tag="aztec-packages-v0.57.0", directory="noir-projects/aztec-nr/value-note"}
easy_private_state = { git="https://github.com/AztecProtocol/aztec-packages/", tag="aztec-packages-v0.57.0", directory="noir-projects/aztec-nr/easy-private-state"}
160 changes: 160 additions & 0 deletions profile.io-social-cipher/aztec/src/artifacts/Profile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@

/* Autogenerated file, do not edit! */

/* eslint-disable */
import {
type AbiType,
AztecAddress,
type AztecAddressLike,
CompleteAddress,
Contract,
type ContractArtifact,
ContractBase,
ContractFunctionInteraction,
type ContractInstanceWithAddress,
type ContractMethod,
type ContractStorageLayout,
type ContractNotes,
decodeFromAbi,
DeployMethod,
EthAddress,
type EthAddressLike,
EventSelector,
type FieldLike,
Fr,
type FunctionSelectorLike,
L1EventPayload,
loadContractArtifact,
type NoirCompiledContract,
NoteSelector,
Point,
type PublicKey,
type UnencryptedL2Log,
type Wallet,
type WrappedFieldLike,
} from '@aztec/aztec.js';
import ProfileContractArtifactJson from '../../target/zkpTestContract-Profile.json' assert { type: 'json' };
export const ProfileContractArtifact = loadContractArtifact(ProfileContractArtifactJson as NoirCompiledContract);



/**
* Type-safe interface for contract Profile;
*/
export class ProfileContract extends ContractBase {

private constructor(
instance: ContractInstanceWithAddress,
wallet: Wallet,
) {
super(instance, ProfileContractArtifact, wallet);
}



/**
* Creates a contract instance.
* @param address - The deployed contract's address.
* @param wallet - The wallet to use when interacting with the contract.
* @returns A promise that resolves to a new Contract instance.
*/
public static async at(
address: AztecAddress,
wallet: Wallet,
) {
return Contract.at(address, ProfileContract.artifact, wallet) as Promise<ProfileContract>;
}


/**
* Creates a tx to deploy a new instance of this contract.
*/
public static deploy(wallet: Wallet, initial_supply: (bigint | number), owner: AztecAddressLike, outgoing_viewer: AztecAddressLike) {
return new DeployMethod<ProfileContract>(Fr.ZERO, wallet, ProfileContractArtifact, ProfileContract.at, Array.from(arguments).slice(1));
}

/**
* Creates a tx to deploy a new instance of this contract using the specified public keys hash to derive the address.
*/
public static deployWithPublicKeysHash(publicKeysHash: Fr, wallet: Wallet, initial_supply: (bigint | number), owner: AztecAddressLike, outgoing_viewer: AztecAddressLike) {
return new DeployMethod<ProfileContract>(publicKeysHash, wallet, ProfileContractArtifact, ProfileContract.at, Array.from(arguments).slice(2));
}

/**
* Creates a tx to deploy a new instance of this contract using the specified constructor method.
*/
public static deployWithOpts<M extends keyof ProfileContract['methods']>(
opts: { publicKeysHash?: Fr; method?: M; wallet: Wallet },
...args: Parameters<ProfileContract['methods'][M]>
) {
return new DeployMethod<ProfileContract>(
opts.publicKeysHash ?? Fr.ZERO,
opts.wallet,
ProfileContractArtifact,
ProfileContract.at,
Array.from(arguments).slice(1),
opts.method ?? 'constructor',
);
}



/**
* Returns this contract's artifact.
*/
public static get artifact(): ContractArtifact {
return ProfileContractArtifact;
}


public static get storage(): ContractStorageLayout<'balances' | 'is_adult' | 'profile_nfts'> {
return {
balances: {
slot: new Fr(1n),
},
is_adult: {
slot: new Fr(2n),
},
profile_nfts: {
slot: new Fr(3n),
}
} as ContractStorageLayout<'balances' | 'is_adult' | 'profile_nfts'>;
}


public static get notes(): ContractNotes<'ProfileNFT' | 'ValueNote'> {
return {
ProfileNFT: {
id: new NoteSelector(4239572523),
},
ValueNote: {
id: new NoteSelector(1038582377),
}
} as ContractNotes<'ProfileNFT' | 'ValueNote'>;
}


/** Type-safe wrappers for the public methods exposed by the contract. */
public declare methods: {

/** compute_note_hash_and_optionally_a_nullifier(contract_address: struct, nonce: field, storage_slot: field, note_type_id: field, compute_nullifier: boolean, serialized_note: array) */
compute_note_hash_and_optionally_a_nullifier: ((contract_address: AztecAddressLike, nonce: FieldLike, storage_slot: FieldLike, note_type_id: FieldLike, compute_nullifier: boolean, serialized_note: FieldLike[]) => ContractFunctionInteraction) & Pick<ContractMethod, 'selector'>;

/** constructor(initial_supply: integer, owner: struct, outgoing_viewer: struct) */
constructor: ((initial_supply: (bigint | number), owner: AztecAddressLike, outgoing_viewer: AztecAddressLike) => ContractFunctionInteraction) & Pick<ContractMethod, 'selector'>;

/** get_profile_nfts(owner: struct, page_index: integer) */
get_profile_nfts: ((owner: AztecAddressLike, page_index: (bigint | number)) => ContractFunctionInteraction) & Pick<ContractMethod, 'selector'>;

/** mint(amount: integer, owner: struct, outgoing_viewer: struct) */
mint: ((amount: (bigint | number), owner: AztecAddressLike, outgoing_viewer: AztecAddressLike) => ContractFunctionInteraction) & Pick<ContractMethod, 'selector'>;

/** mintNFT(to: struct, token_id: field, is_adult: boolean) */
mintNFT: ((to: AztecAddressLike, token_id: FieldLike, is_adult: boolean) => ContractFunctionInteraction) & Pick<ContractMethod, 'selector'>;

/** update_is_adult(amount: integer, owner: struct, recipient: struct) */
update_is_adult: ((amount: (bigint | number), owner: AztecAddressLike, recipient: AztecAddressLike) => ContractFunctionInteraction) & Pick<ContractMethod, 'selector'>;
};


}
120 changes: 120 additions & 0 deletions profile.io-social-cipher/aztec/src/main.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
use dep::aztec::macros::aztec;

#[aztec]
contract Profile {
use dep::aztec::{
note::utils::compute_note_hash_for_nullify, keys::getters::get_nsk_app, oracle::random::random,
encrypted_logs::encrypted_note_emission::encode_and_encrypt_note,
note::constants::MAX_NOTES_PER_PAGE,
keys::getters::get_public_keys, prelude::{AztecAddress, PrivateSet, SharedImmutable, Map, NullifiableNote, NoteHeader, PrivateContext, NoteViewerOptions},
protocol_types::{constants::GENERATOR_INDEX__NOTE_NULLIFIER, hash::poseidon2_hash_with_separator, traits::{Empty, Eq}},
macros::notes::partial_note
};
use dep::value_note::balance_utils;
use dep::easy_private_state::EasyPrivateUint;
use dep::aztec::macros::{storage::storage, functions::{initializer, private}};
use dep::value_note::value_note::ValueNote;

// TODO: remove from this file and import from outside
#[partial_note(quote { token_id})]
struct ProfileNFT {
token_id: Field,
is_adult: bool,
npk_m_hash: Field,
}

// TODO: remove from this file and import from outside
impl ProfileNFT {
pub fn new(token_id: Field, is_adult: bool, npk_m_hash: Field) -> Self {
ProfileNFT { token_id, is_adult, npk_m_hash, header: NoteHeader::empty() }
}
}

// TODO: remove from this file and import from outside
impl Eq for ProfileNFT {
fn eq(self, other: Self) -> bool {
(self.token_id == other.token_id)
& (self.npk_m_hash == other.npk_m_hash)
}
}

// TODO: remove from this file and import from outside
impl NullifiableNote for ProfileNFT {
fn compute_nullifier(self, context: &mut PrivateContext, note_hash_for_nullify: Field) -> Field {
let secret = context.request_nsk_app(self.npk_m_hash);
poseidon2_hash_with_separator(
[
note_hash_for_nullify,
secret
],
GENERATOR_INDEX__NOTE_NULLIFIER as Field
)
}

// TODO: remove from this file and import from outside
unconstrained fn compute_nullifier_without_context(self) -> Field {
let note_hash_for_nullify = compute_note_hash_for_nullify(self);
let secret = get_nsk_app(self.npk_m_hash);
poseidon2_hash_with_separator(
[
note_hash_for_nullify,
secret
],
GENERATOR_INDEX__NOTE_NULLIFIER as Field
)
}
}

#[storage]
struct Storage<Context> {
profile_nfts: Map<AztecAddress, PrivateSet<ProfileNFT, Context>, Context>,
}

/**
* initialize the contract's initial state variables.
*/
#[private]
#[initializer]
fn constructor(initial_supply: u64, owner: AztecAddress, outgoing_viewer: AztecAddress) {
// leave it in case
}

// TODO: requester might not need since there is msg.sender
#[public]
fn request_age_verification(target_user: AztecAddress, requester: AztecAddress) {
// WIP
}

#[private]
fn mintNFT(to: AztecAddress, token_id: Field, is_adult: bool) {
let profile_nfts = storage.profile_nfts;
let to_keys = get_public_keys(to);
let mut nft_note = ProfileNFT::new(token_id, is_adult, to_keys.npk_m.hash());

// TODO: sending an event to the NFT owner after update storage. Is this meaningful? Can it be a proof?
// TODO: emitting event is a way to send message to someone?
profile_nfts.at(to).insert(&mut nft_note).emit(encode_and_encrypt_note(
&mut context,
to_keys.ovpk_m,
to_keys.ivpk_m,
to
));
}

// get nft note info such as token_id, is_adult
unconstrained fn get_profile_nfts(owner: AztecAddress, page_index: u32) -> [(Field, bool); 10] {
let offset = page_index * MAX_NOTES_PER_PAGE;
let mut options = NoteViewerOptions::new();
let profile_nfts = storage.profile_nfts;
let notes = profile_nfts.at(owner).view_notes(options.set_offset(offset));
let mut owned_nft_ids = [(0, false); MAX_NOTES_PER_PAGE];

for i in 0..options.limit {
if i < notes.len() {
owned_nft_ids[i] = (notes.get_unchecked(i).token_id, notes.get_unchecked(i).is_adult); // returns as [token_id, is_adult]
}
}

(owned_nft_ids)
}
}

Large diffs are not rendered by default.

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions profile.io-social-cipher/frontend/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}
Loading