noir by example

zk age verification og source

a simplified system where alice can prove she was born before a certain date without revealing her actual birthdate – or phrased differently:

issuing a claim

some signing authority (the hospital, a government, etc) will need to sign a hashed representation of alice’s identity (subject), attesting to her birthdate – so we’ll construct a standardized payload

// Constructs a payload for the issuer to sign, claiming that
// the subject was born on the given date.
fn construct_claim_payload(
    subject: Field,
    birthyear: Field,
    birthmonth : Field,
    birthday : Field,
) -> [u8; 32] {
    let h1 = std::hash::pedersen([subject, birthyear, birthmonth, birthday])[0];
    let h2 = h1.to_be_bytes(32);
    let mut result = [0; 32];
    for i in 0..32 {
        result[i] = h2[i];
    }
    result
}

we use std::hash::pedersen to convert alice’s data into a unique 32-byte hash, which is signed by the trusted third party, and stored off chain however alice chooses – this hash will be used as a hidden input in proofs that alice generates later

the primary advantage of pedersen is its simplicity and efficiency, making it an attractive option in circuits

a small test can make it easy to construct & output claim payloads (essentially what the signer would call in prod)

#[test]
fn test_construct_claim_payload() {
    let want = [0x0b, 0xb7, 0xda, 0x0f, 0x91, 0x33, 0x82, 0x0c, 0xee, 0x8a, 0x9c, 0xe7, 0xda, 0x04, 0x1e, 0x22, 0x6a, 0x19, 0x12, 0xbe, 0xb5, 0x2b, 0x96, 0xa1, 0x41, 0x3e, 0xb7, 0x41, 0x15, 0xeb, 0x7a, 0xd7];
    let got = construct_claim_payload(1234567890, 2003, 1, 2);
    assert(want == got);

    std::println(got);
}

checking a claim & proving things about it

alice wants to generate a proof that her birthdate is before some date

that relationship can be defined through a set of assertions

// Applies basic constraints such that the subject's birthdate is
// never after the required birthdate.
fn check_claim(
    required_birthyear: Field,
    required_birthmonth: Field,
    required_birthday: Field,
    subject_birthyear: Field,
    subject_birthmonth: Field,
    subject_birthday: Field,
) {
    assert(required_birthyear as u16 >= subject_birthyear as u16);
    if (required_birthyear == subject_birthyear) {
        assert(required_birthmonth as u8 >= subject_birthmonth as u8);
        if (required_birthmonth == subject_birthmonth) {
            assert(required_birthday as u8 >= subject_birthday as u8);
        }
    }
}

which is then the first constraint enforced in the circuit’s entrypoint

fn main(
    //
    // public inputs:
    // the things that verifiers will provide as a challenge to alice
    //
    required_birthyear: pub Field,
    required_birthmonth: pub Field,
    required_birthday: pub Field,
    issuer_public_key_x: pub [u8; 32],
    issuer_public_key_y: pub [u8; 32],
    subject: pub Field,
    //
    // private inputs:
    // the knowledge that alice wants to keep secret, but still
    // prove computation over (i.e. the challenge)
    //
    issuer_signature: [u8; 64],
    subject_birthyear: Field,
    subject_birthmonth: Field,
    subject_birthday: Field,
) -> pub bool {

    check_claim(
        required_birthyear,
        required_birthmonth,
        required_birthday,
        subject_birthyear,
        subject_birthmonth,
        subject_birthday,
    );

    let claim_payload = construct_claim_payload(
        subject,
        subject_birthyear,
        subject_birthmonth,
        subject_birthday,
    );

    let valid = std::ecdsa_secp256k1::verify_signature(
        issuer_public_key_x,
        issuer_public_key_y,
        issuer_signature,
        claim_payload,
    );
    assert(valid == true);
    valid
}

with this, alice can prove she knows some signature, that covers some payload that has certain attributes

so its not zero knowledge, but it’s also not a ton knowledge

NOTE: at the time of writing, signing within noir tests isn’t well supported, so i just computed in go separately

#[test]
fn test_valid_signature() {
    // Claim
    let subject = 1234567890;
    let subject_birthyear = 2003; 
    let subject_birthmonth = 1;
    let subject_birthday = 2;

    // Signature covering claim
    // This public key corresponds to private key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 (1st one generated by anvil)
    let issuer_public_key_x: [u8; 32] = [0x83,0x18,0x53,0x5B,0x54,0x10,0x5D,0x4A,0x7A,0xAE,0x60,0xC0,0x8F,0xC4,0x5F,0x96,0x87,0x18,0x1B,0x4F,0xDF,0xC6,0x25,0xBD,0x1A,0x75,0x3F,0xA7,0x39,0x7F,0xED,0x75];
    let issuer_public_key_y: [u8; 32] = [0x35,0x47,0xF1,0x1C,0xA8,0x69,0x66,0x46,0xF2,0xF3,0xAC,0xB0,0x8E,0x31,0x01,0x6A,0xFA,0xC2,0x3E,0x63,0x0C,0x5D,0x11,0xF5,0x9F,0x61,0xFE,0xF5,0x7B,0x0D,0x2A,0xA5];
    let issuer_signature: [u8; 64] = [0x88, 0xbb, 0x8d, 0x8e, 0x4d, 0x5f, 0x7e, 0x0a, 0x85, 0x3b, 0x5e, 0x4c, 0xda, 0xf3, 0x92, 0x24, 0x4d, 0x46, 0xf2, 0x2a, 0xdc, 0x0f, 0x4c, 0x28, 0x52, 0x7a, 0x28, 0xac, 0xf0, 0xa6, 0x2f, 0x3b, 0x1e, 0xf9, 0xfe, 0xbd, 0x3a, 0xde, 0xea, 0xed, 0x27, 0x6a, 0x32, 0x87, 0xe5, 0xdb, 0xf4, 0x32, 0x7a, 0x9c, 0x20, 0xce, 0xed, 0x40, 0x3e, 0xdb, 0xa5, 0x8d, 0xbd, 0xef, 0x01, 0xe6, 0x9b, 0xc6];

    let is_valid = main(
        2003,                   // required_birthyear
        1,                      // required_birthmonth
        3,                      // required_birthday
        issuer_public_key_x,
        issuer_public_key_y,
        subject,
        issuer_signature,
        subject_birthyear,
        subject_birthmonth,
        subject_birthday,
    );
    assert(is_valid == true);
}

try it out install the noir toolchain if you haven't yet:
        curl -L https://noirup.org/install | bash
        noirup --nightly
        
then checkout and run the example:
        git clone https://github.com/sambarnes/noir-by-example.git
        cd noir-by-example/circuits/gadgets/zk-age-verification
        nargo test --show-output
        

prev (zk voting)

...