Core Concepts

The Anchor Program

The Althea program lives on Solana, written with the Anchor framework. It exposes one instruction (submit_proof) and manages two PDAs (NullifierRecord and AttestationRecord). It enforces everything the off-chain pipeline can't.

submit_proof instruction

programs/africazk/src/lib.rs
use anchor_lang::prelude::*;

declare_id!("AfricaZK1111111111111111111111111111111111");

#[program]
pub mod africazk {
    use super::*;

    pub fn submit_proof(
        ctx: Context<SubmitProof>,
        proof_bytes: Vec<u8>,
        public_signals: [u8; 64], // valid (32) + nullifier (32)
        ax: [u8; 32],
        ay: [u8; 32],
    ) -> Result<()> {
        require!(public_signals[..32] == ONE_LE, ErrorCode::InvalidProof);
        require!(ax == AFRICAZK_AX, ErrorCode::WrongIssuer);
        require!(ay == AFRICAZK_AY, ErrorCode::WrongIssuer);

        // light-protocol verifier hook lives here in a future release
        verify_groth16(&proof_bytes, &public_signals, &ax, &ay)?;

        let attestation = &mut ctx.accounts.attestation;
        attestation.verified = true;
        attestation.timestamp = Clock::get()?.unix_timestamp;
        attestation.protocol = *b"Althea-v1\0\0\0\0\0";
        attestation.revoked = false;

        Ok(())
    }
}

Accounts

programs/africazk/src/accounts.rs
#[derive(Accounts)]
#[instruction(proof_bytes: Vec<u8>, public_signals: [u8; 64])]
pub struct SubmitProof<'info> {
    #[account(mut)]
    pub user: Signer<'info>,

    #[account(
        init,
        payer = user,
        space = 8 + NullifierRecord::SIZE,
        seeds = [b"africazk-nullifier", &public_signals[32..]],
        bump
    )]
    pub nullifier_record: Account<'info, NullifierRecord>,

    #[account(
        init,
        payer = user,
        space = 8 + AttestationRecord::SIZE,
        seeds = [b"africazk-attestation", user.key().as_ref()],
        bump
    )]
    pub attestation: Account<'info, AttestationRecord>,

    pub system_program: Program<'info, System>,
}

Both PDAs are init accounts — they fail if they already exist. That fail-on-init is the entire duplicate-prevention story for nullifiers, and the attestation-uniqueness story per wallet.

Error codes

CodeCause
InvalidProofvalid signal is not 1, or Groth16 verification fails
WrongIssuerAx/Ay do not match the program-baked Althea public key
DuplicateNullifierNullifierRecord PDA already exists — this identity has been used
DuplicateAttestationAttestationRecord PDA already exists — this wallet already verified

Upgrade path

Current builds inline a stub Groth16 verifier. The architecture is designed to slot in Light Protocol's on-chain Groth16 verifier as a drop-in replacement of verify_groth16 — the instruction signature and PDA layout stay identical, so existing dApps and SDK callers don't need to change.

See also