diff --git a/account-comparison/CLAUDE.md b/account-comparison/CLAUDE.md new file mode 100644 index 0000000..f2d3cbe --- /dev/null +++ b/account-comparison/CLAUDE.md @@ -0,0 +1,81 @@ +# Account Comparison + +Side-by-side implementation of Solana accounts vs compressed accounts. Shows equivalent create and update operations for both account types using identical data structures. + +## Summary + +- Demonstrates equivalent PDA and compressed account patterns with identical seeds `["account", user.key()]` +- Compressed accounts use `LightAccount::new_init` for creation and `LightAccount::new_mut` for updates +- Updates require client to pass existing state because on-chain storage is a Poseidon hash +- All fields marked `#[hash]` are included in Poseidon hash computation + +## Source Structure + +``` +programs/account-comparison/src/ + lib.rs # Program entrypoint, instructions, account structs +programs/account-comparison/tests/ + test_solana_account.rs # LiteSVM tests for standard accounts + test_compressed_account.rs # Light Protocol tests for compressed accounts +``` + +## Accounts + +### AccountData (Solana PDA) + +| Field | Type | Size | +|-------|------|------| +| discriminator | `[u8; 8]` | 8 bytes | +| user | `Pubkey` | 32 bytes | +| name | `String` | 4 + name_len (max 60 chars) | +| data | `[u8; 128]` | 128 bytes | + +- **Seeds**: `["account", user.key()]` +- **Discriminator**: 8 bytes, SHA256("account:AccountData")[0..8] +- **Space**: 232 bytes. String uses Borsh serialization (4-byte length prefix + variable content). + +### CompressedAccountData (LightAccount) + +```rust +#[derive(LightDiscriminator, LightHasher)] +pub struct CompressedAccountData { + #[hash] pub user: Pubkey, + #[hash] pub name: String, + #[hash] pub data: [u8; 128], +} +``` + +- **Address seeds**: `["account", user.key()]` +- **Discriminator**: `LightDiscriminator` derive generates from struct name +- **Hashing**: Poseidon hash of all `#[hash]` fields (user, name, data) + +### CPI Signer + +```rust +const CPI_SIGNER: CpiSigner = derive_light_cpi_signer!("FYX4GmKJYzSiycc7XZKf12NGXNE9siSx1cJubYJniHcv"); +``` + +## Instructions + +| # | Instruction | Accounts | Parameters | Logic | +|---|-------------|----------|------------|-------| +| 0 | `create_account` | `user` (signer, mut), `account` (init PDA), `system_program` | `name: String` | Initializes PDA with seeds `["account", user]`, sets `data = [1u8; 128]` | +| 1 | `update_data` | `user` (signer, mut), `account` (mut, has_one = user) | `data: [u8; 128]` | Overwrites account.data | +| 2 | `create_compressed_account` | `user` (signer, mut) + remaining_accounts | `name`, `proof`, `address_tree_info`, `output_tree_index` | Validates ADDRESS_TREE_V2, derives address, calls `LightAccount::new_init`, invokes light-system-program | +| 3 | `update_compressed_account` | `user` (signer, mut) + remaining_accounts | `new_data`, `existing_data`, `name`, `proof`, `account_meta` | Reconstructs state via `LightAccount::new_mut`, verifies user ownership, invokes light-system-program | + +## Security + +| Check | Location | Description | +|-------|----------|-------------| +| Address tree validation | `create_compressed_account` | Verifies `address_tree_pubkey.to_bytes() == ADDRESS_TREE_V2` | +| Owner verification | `update_compressed_account` | Asserts `compressed_account.user == ctx.accounts.user.key()` | +| PDA constraint | `update_data` | Anchor `has_one = user` constraint | +| Signer requirement | All instructions | User must sign transaction | + +## Errors + +| Error | Message | +|-------|---------| +| `CustomError::Unauthorized` | "No authority to perform this action" | +| `ProgramError::InvalidAccountData` | Returned when address tree validation fails | diff --git a/airdrop-implementations/README.md b/airdrop-implementations/README.md new file mode 100644 index 0000000..b157aaa --- /dev/null +++ b/airdrop-implementations/README.md @@ -0,0 +1,28 @@ +# Airdrop Implementations + +Simple Implementation: simple-claim - Distributes compressed tokens that get decompressed on claim + +Advanced Implementation: distributor - Distributes SPL tokens, uses compressed PDAs to track claims + + +## Quick comparison + +| | simple-claim | distributor | +|--|------------------|-------------| +| Time-lock | Slot-based (all-or-nothing) | Timestamp-based (linear vesting) | +| Partial claims | No | Yes | +| Clawback | No | Yes | +| Admin controls | No | Yes | + +## Cost (10,000 recipients) + +| | simple-claim | distributor | Regular | +|--|----------------:|------------:|----------------:| +| Setup | ~0.03 SOL | ~0.002 SOL | ~0.002 SOL | +| Claim tracking | 0 | ~0.03 SOL | ~6 SOL | +| **Total** | **~0.03 SOL** | **~0.03 SOL** | **~6 SOL** | + +## Getting started + +- [simple-claim README](./simple-claim/README.md) +- [distributor README](./distributor/README.md) \ No newline at end of file diff --git a/basic-operations/CLAUDE.md b/basic-operations/CLAUDE.md new file mode 100644 index 0000000..1da7f5d --- /dev/null +++ b/basic-operations/CLAUDE.md @@ -0,0 +1,117 @@ +# Basic Operations Examples + +Example programs showing all basic operations for compressed accounts. + +## Summary + +- Demonstrates create, update, close, reinit, and burn operations +- Anchor uses Poseidon hashing (`light_sdk::account::LightAccount`); Native uses SHA-256 (`light_sdk::account::sha::LightAccount`) +- All programs derive addresses from seeds `[b"message", signer.key.as_ref()]` + `ADDRESS_TREE_V2` + program ID +- Close preserves address for reinitialization; burn permanently deletes address + +## [Anchor README](anchor/README.md) | [Native README](native/README.md) + +## Source structure + +``` +basic-operations/ +├── anchor/ # Anchor framework (Poseidon hashing) +│ ├── create/programs/create/src/lib.rs +│ ├── update/programs/update/src/lib.rs +│ ├── close/programs/close/src/lib.rs +│ ├── reinit/programs/reinit/src/lib.rs +│ └── burn/programs/burn/src/lib.rs +└── native/ # Native Solana (SHA-256 hashing) + └── programs/ + ├── create/src/lib.rs + ├── update/src/lib.rs + ├── close/src/lib.rs + ├── reinit/src/lib.rs + └── burn/src/lib.rs +``` + +## Accounts + +### MyCompressedAccount + +Shared account structure across all examples. Derives `LightDiscriminator` for compressed account type identification. + +```rust +#[derive(LightDiscriminator)] +pub struct MyCompressedAccount { + pub owner: Pubkey, // Account owner + pub message: String, // User-defined message +} +``` + +## Instructions + +Native programs use `InstructionType` enum discriminators (first byte of instruction data). + +### create_account (discriminator: 0) + +| Parameter | Type | Description | +|-----------|------|-------------| +| `proof` | `ValidityProof` | ZK proof for address non-existence | +| `address_tree_info` | `PackedAddressTreeInfo` | Address tree metadata | +| `output_state_tree_index` | `u8` | Target state tree index | +| `message` | `String` | Initial message content | + +Calls `LightAccount::new_init()` with derived address, invokes `LightSystemProgramCpi` with `with_new_addresses()`. + +### update_account (discriminator: 1) + +| Parameter | Type | Description | +|-----------|------|-------------| +| `proof` | `ValidityProof` | ZK proof for account existence | +| `account_meta` | `CompressedAccountMeta` | Current account metadata | +| `current_account` / `current_message` | varies | Current account state for verification | +| `new_message` | `String` | Updated message content | + +Calls `LightAccount::new_mut()` with current state, modifies message, invokes CPI. + +### close_account (discriminator: 1) + +| Parameter | Type | Description | +|-----------|------|-------------| +| `proof` | `ValidityProof` | ZK proof for account existence | +| `account_meta` | `CompressedAccountMeta` | Current account metadata | +| `current_message` | `String` | Current message for verification | + +Calls `LightAccount::new_close()` - clears data but preserves address for reinitialization. + +### reinit_account (discriminator: 2) + +| Parameter | Type | Description | +|-----------|------|-------------| +| `proof` | `ValidityProof` | ZK proof for empty account at address | +| `account_meta` | `CompressedAccountMeta` | Account metadata | + +Calls `LightAccount::new_empty()` to reinitialize previously closed account. + +### burn_account (discriminator: 1) + +| Parameter | Type | Description | +|-----------|------|-------------| +| `proof` | `ValidityProof` | ZK proof for account existence | +| `account_meta` | `CompressedAccountMetaBurn` | Account metadata (burn-specific) | +| `current_message` / `current_account` | varies | Current state for verification | + +Calls `LightAccount::new_burn()` - permanently deletes account. Address cannot be reused. + +## Security + +- Address tree validation: Checks `address_tree_pubkey.to_bytes() == ADDRESS_TREE_V2` +- Program ID verification (native only): `program_id != &ID` returns `IncorrectProgramId` +- Signer required: First account must be mutable signer +- State verification: Close/update/burn require current state to match on-chain data + +## Errors + +| Error | Source | Condition | +|-------|--------|-----------| +| `ProgramError::IncorrectProgramId` | Native entrypoint | Program ID mismatch | +| `ProgramError::InvalidInstructionData` | Entrypoint | Empty or malformed instruction data | +| `ProgramError::NotEnoughAccountKeys` | All | Missing required accounts | +| `ProgramError::InvalidAccountData` | All | Invalid address tree | +| `LightSdkError::Borsh` | Native | Instruction data deserialization failure | diff --git a/counter/CLAUDE.md b/counter/CLAUDE.md new file mode 100644 index 0000000..db7178a --- /dev/null +++ b/counter/CLAUDE.md @@ -0,0 +1,77 @@ +# Counter Program + +A counter program that stores state in compressed accounts. Three implementations: Anchor, Native (solana_program), and Pinocchio. + +## Summary + +- Compressed PDA with address derived from `["counter", signer]` seeds +- `LightAccount` lifecycle: `new_init()` for create, `new_mut()` for update, `new_close()` for delete +- Owner field hashed with Poseidon via `#[hash]` attribute for account hash verification +- Address tree validation enforces `ADDRESS_TREE_V2` +- Closed addresses cannot be reused + +## [anchor/README.md](anchor/README.md) + +## Source structure + +``` +counter/ +├── anchor/programs/counter/src/lib.rs # Anchor implementation +├── native/src/lib.rs # Native solana_program implementation +└── pinocchio/src/lib.rs # Pinocchio implementation +``` + +## Accounts + +### CounterAccount (compressed PDA) + +Discriminator: `LightDiscriminator` derive macro generates 8-byte discriminator from struct name hash. + +| Field | Type | Hashing | Description | +|-------|------|---------|-------------| +| `owner` | `Pubkey` | Poseidon (`#[hash]`) | Counter owner, included in account hash. | +| `value` | `u64` | None | Counter value, Borsh-serialized only. | + +**Address derivation:** + +```rust +derive_address(&[b"counter", signer.key().as_ref()], &address_tree_pubkey, &program_id) +``` + +## Instructions + +| Discriminator | Enum Variant | Accounts | Logic | +|---------------|--------------|----------|-------| +| 0 | `CreateCounter` | signer (mut), remaining_accounts | Validates `address_tree_pubkey == ADDRESS_TREE_V2`. Derives address from seeds. Calls `LightAccount::new_init()`, sets owner to signer, value to 0. | +| 1 | `IncrementCounter` | signer (mut), remaining_accounts | Calls `LightAccount::new_mut()` with current state. Executes `checked_add(1)`. Invokes Light System Program. | +| 2 | `DecrementCounter` | signer (mut), remaining_accounts | Calls `LightAccount::new_mut()` with current state. Executes `checked_sub(1)`. Invokes Light System Program. | +| 3 | `ResetCounter` | signer (mut), remaining_accounts | Calls `LightAccount::new_mut()` with current state. Sets value to 0. Invokes Light System Program. | +| 4 | `CloseCounter` | signer (mut), remaining_accounts | Calls `LightAccount::new_close()` (input state only, no output). Address cannot be reused. | + +### Instruction data structs + +| Struct | Fields | +|--------|--------| +| `CreateCounterInstructionData` | `proof`, `address_tree_info`, `output_state_tree_index` | +| `IncrementCounterInstructionData` | `proof`, `counter_value`, `account_meta` | +| `DecrementCounterInstructionData` | `proof`, `counter_value`, `account_meta` | +| `ResetCounterInstructionData` | `proof`, `counter_value`, `account_meta` | +| `CloseCounterInstructionData` | `proof`, `counter_value`, `account_meta` | + +## Security + +| Check | Location | Description | +|-------|----------|-------------| +| Address tree validation | `create_counter` | Rejects if `address_tree_pubkey != ADDRESS_TREE_V2`. | +| Overflow protection | `increment_counter` | Uses `checked_add(1)`. | +| Underflow protection | `decrement_counter` | Uses `checked_sub(1)`. | +| Owner binding | All mutations | Owner reconstructed from signer, included in account hash verification. | +| Program ID check | Native/Pinocchio | Validates `program_id == ID` at entry. | + +## Errors + +| Code | Name | Description | +|------|------|-------------| +| 1 | `Unauthorized` | No authority to perform action. | +| 2 | `Overflow` | Counter increment would overflow u64. | +| 3 | `Underflow` | Counter decrement would underflow below 0. | diff --git a/create-and-update/CLAUDE.md b/create-and-update/CLAUDE.md new file mode 100644 index 0000000..c5700fa --- /dev/null +++ b/create-and-update/CLAUDE.md @@ -0,0 +1,77 @@ +# Create and Update + +Demonstrates compressed account lifecycle operations: creation, updates, and atomic multi-account operations in single instructions. + +## Summary + +- Create compressed accounts with derived addresses using `LightAccount::new_init()` +- Update existing accounts via `LightAccount::new_mut()` which validates input state hash +- Execute atomic multi-account operations in single CPI calls (create+update, update+update, create+create) +- Addresses derived from `[seed, signer.key()]` with program ID and address tree + +## [README](README.md) + +## Source structure + +``` +programs/create-and-update/src/ +└── lib.rs # Program entry, instructions, accounts, state structs +``` + +## Accounts + +### Anchor accounts + +| Account | Type | Description | +|---------|------|-------------| +| `signer` | `Signer` | Transaction fee payer and account owner. Marked `mut`. | + +### Compressed account state + +| Struct | Fields | Discriminator | +|--------|--------|---------------| +| `DataAccount` | `owner: Pubkey`, `message: String` | 8-byte hash of "DataAccount" via `LightDiscriminator` | +| `ByteDataAccount` | `owner: Pubkey`, `data: [u8; 31]` | 8-byte hash of "ByteDataAccount" via `LightDiscriminator` | + +### PDAs and address derivation + +| Address | Seeds | Description | +|---------|-------|-------------| +| First address | `[FIRST_SEED, signer.key()]` | Derived via `derive_address` with program ID and address tree. | +| Second address | `[SECOND_SEED, signer.key()]` | Derived via `derive_address` with program ID and address tree. | + +Constants: +- `FIRST_SEED`: `b"first"` +- `SECOND_SEED`: `b"second"` +- `LIGHT_CPI_SIGNER`: Derived via `derive_light_cpi_signer!` macro from program ID + +### Instruction data structs + +| Struct | Fields | Used by | +|--------|--------|---------| +| `ExistingCompressedAccountIxData` | `account_meta: CompressedAccountMeta`, `message: String`, `update_message: String` | `create_and_update`, `update_two_accounts` | +| `NewCompressedAccountIxData` | `address_tree_info: PackedAddressTreeInfo`, `message: String` | `create_and_update` | + +## Instructions + +| Discriminator | Instruction | Accounts | Parameters | Logic | +|---------------|-------------|----------|------------|-------| +| sighash("create_compressed_account") | `create_compressed_account` | `GenericAnchorAccounts` + remaining accounts | `proof`, `address_tree_info`, `output_state_tree_index`, `message` | Validates address tree is ADDRESS_TREE_V2. Derives address from FIRST_SEED + signer. Creates `DataAccount` via `LightAccount::new_init()`. Invokes Light System Program CPI. | +| sighash("create_and_update") | `create_and_update` | `GenericAnchorAccounts` + remaining accounts | `proof`, `existing_account`, `new_account` | Creates new `DataAccount` at SECOND_SEED. Updates existing account via `LightAccount::new_mut()` (validates current state hash). Single CPI with both operations. | +| sighash("update_two_accounts") | `update_two_accounts` | `GenericAnchorAccounts` + remaining accounts | `proof`, `first_account`, `second_account` | Updates two existing `DataAccount` messages atomically via `LightAccount::new_mut()`. Single CPI call. | +| sighash("create_two_accounts") | `create_two_accounts` | `GenericAnchorAccounts` + remaining accounts | `proof`, `address_tree_info`, `output_state_tree_index`, `byte_data`, `message` | Creates `ByteDataAccount` at FIRST_SEED and `DataAccount` at SECOND_SEED in single CPI. | + +## Security + +| Check | Location | Description | +|-------|----------|-------------| +| Address tree validation | `lib.rs:49-52`, `lib.rs:97-100`, `lib.rs:218-221` | Verifies `address_tree_pubkey` matches `ADDRESS_TREE_V2`. | +| Signer authorization | Anchor `#[account(mut)]` | Signer must sign transaction and pay fees. | +| CPI signer derivation | `lib.rs:17-18` | `LIGHT_CPI_SIGNER` derived from program ID via `derive_light_cpi_signer!` macro. | + +## Errors + +| Error | Source | Cause | +|-------|--------|-------| +| `AccountNotEnoughKeys` | `ErrorCode::AccountNotEnoughKeys` | Address tree pubkey cannot be retrieved from remaining accounts. | +| `InvalidAccountData` | `ProgramError::InvalidAccountData` | Address tree pubkey does not match `ADDRESS_TREE_V2`. | diff --git a/read-only/CLAUDE.md b/read-only/CLAUDE.md new file mode 100644 index 0000000..9971ad1 --- /dev/null +++ b/read-only/CLAUDE.md @@ -0,0 +1,69 @@ +# Read-Only Example Program + +Creates and reads compressed accounts. Verifies on-chain state without modification. + +## Summary + +- Creates compressed accounts with derived addresses and validates them read-only via Light System Program CPI +- Read-only validation reconstructs account data client-side, program verifies data hash matches proven state +- Requires v2 address trees (`ADDRESS_TREE_V2`) +- Uses `LightAccount::new_read_only()` for non-mutating access + +## [README](README.md) + +## Source structure + +``` +src/ +└── lib.rs # Program entry, instructions, account definitions +tests/ +└── test.rs # Integration tests for create and read operations +``` + +## Accounts + +### Anchor accounts + +| Account | Type | Description | +|---------|------|-------------| +| `signer` | `Signer` | Transaction fee payer and account owner. Mutable. | + +### DataAccount (compressed) + +8-byte discriminator derived via `LightDiscriminator`. Stored in separate discriminator field, not in data bytes. + +```rust +pub struct DataAccount { + pub owner: Pubkey, // Account owner's public key + pub message: String, // User-defined message +} +``` + +**Address derivation:** `["first", signer.key()]` via `derive_address()` with address tree and program ID. + +### ExistingCompressedAccountIxData + +Instruction data struct for `read`. Contains `CompressedAccountMetaReadOnly` (tree indices, leaf info) and current `message` for hash verification. + +## Instructions + +| Instruction | Accounts | Parameters | Logic | +|-------------|----------|------------|-------| +| `create_compressed_account` | `signer` (mut) + remaining accounts | `proof: ValidityProof`, `address_tree_info: PackedAddressTreeInfo`, `output_state_tree_index: u8`, `message: String` | Validates address tree is v2. Derives address from seeds. Initializes `LightAccount` with owner and message. Invokes Light System Program CPI with `with_new_addresses()`. | +| `read` | `signer` (mut) + remaining accounts | `proof: ValidityProof`, `existing_account: ExistingCompressedAccountIxData` | Reconstructs `DataAccount` from instruction data. Creates `LightAccount::new_read_only()` which computes data hash from provided fields. Light System Program CPI verifies hash matches proven Merkle leaf. | + +## Security + +| Check | Location | Description | +|-------|----------|-------------| +| Address tree validation | `create_compressed_account` | Verifies `address_tree_pubkey` matches `ADDRESS_TREE_V2` constant | +| Signer verification | Both instructions | `signer` account is `Signer` type, Anchor validates signature | +| Owner assignment | `create_compressed_account` | Sets `data_account.owner = signer.key()` | +| Read-only data hash verification | `read` | Light System Program verifies reconstructed data hashes match proven state | + +## Errors + +| Error | Source | Cause | +|-------|--------|-------| +| `AccountNotEnoughKeys` | `create_compressed_account` | Address tree pubkey lookup failed from `CpiAccounts` | +| `InvalidAccountData` | `create_compressed_account` | Address tree pubkey does not match `ADDRESS_TREE_V2` | diff --git a/zk-id/CLAUDE.md b/zk-id/CLAUDE.md new file mode 100644 index 0000000..98a1586 --- /dev/null +++ b/zk-id/CLAUDE.md @@ -0,0 +1,122 @@ +# ZK-ID Program + +Zero-knowledge identity verification using Groth16 proofs with compressed accounts. + +## Summary + +- Issuers create credentials for users; users prove credential ownership without revealing the credential +- Credential keypair: private key = `Sha256(sign("CREDENTIAL"))` truncated to 248 bits; public key = `Poseidon(private_key)` +- Nullifier = `Poseidon(verification_id, credential_private_key)` - prevents double-use per verification context +- ZK circuit verifies 26-level Merkle proof of credential account inclusion + +## [README](README.md) + +## Source Structure + +``` +src/ +├── lib.rs # Program entry, instructions, account structs, error codes +└── verifying_key.rs # Groth16 verifying key constants (8 public inputs) + +circuits/ +├── compressed_account_merkle_proof.circom # Main circuit (26-level Merkle proof) +├── compressed_account.circom # CompressedAccountHash template +├── credential.circom # Keypair and CredentialOwnership templates +└── merkle_proof.circom # MerkleProof template +``` + +## Accounts + +### Compressed Accounts (Light Protocol) + +| Account | Seeds | Fields | Hashing | +|---------|-------|--------|---------| +| `IssuerAccount` | `[b"issuer", signer_pubkey]` | `issuer_pubkey: Pubkey`, `num_credentials_issued: u64` | SHA256 | +| `CredentialAccount` | `[b"credential", credential_pubkey]` | `issuer: Pubkey` (#[hash]), `credential_pubkey: CredentialPubkey` | Poseidon | +| `EncryptedEventAccount` | `[b"ZK_ID_CHECK", nullifier, verification_id]` | `data: Vec` | SHA256 | + +### Anchor Accounts + +| Struct | Fields | +|--------|--------| +| `GenericAnchorAccounts` | `signer: Signer` (mut) | +| `VerifyAccounts` | `signer: Signer` (mut), `input_merkle_tree: UncheckedAccount` | + +### Address Derivation + +All addresses derive using `derive_address()` with `ADDRESS_TREE_V2`: + +```rust +derive_address(&[seed_prefix, identifier], &address_tree_pubkey, &program_id) +``` + +## Instructions + +| # | Instruction | Accounts | Parameters | Logic | +|---|-------------|----------|------------|-------| +| 0 | `create_issuer` | `GenericAnchorAccounts` + CPI accounts | `proof`, `address_tree_info`, `output_state_tree_index` | Derives address from `[ISSUER, signer]`, creates `IssuerAccount` with `num_credentials_issued = 0` | +| 1 | `add_credential` | `GenericAnchorAccounts` + CPI accounts | `proof`, `address_tree_info`, `output_state_tree_index`, `issuer_account_meta`, `credential_pubkey`, `num_credentials_issued` | Mutates issuer (increments counter), derives address from `[CREDENTIAL, credential_pubkey]`, creates `CredentialAccount` | +| 2 | `zk_verify_credential` | `VerifyAccounts` + CPI accounts | `proof`, `address_tree_info`, `output_state_tree_index`, `input_root_index`, `public_data`, `credential_proof`, `issuer`, `nullifier`, `verification_id` | Reads Merkle root, constructs 8 public inputs, decompresses G1/G2 points, verifies Groth16 proof, creates `EncryptedEventAccount` | + +## ZK Circuit (CompressedAccountMerkleProof) + +**Public inputs** (8 signals): +1. `owner_hashed` - Program ID hashed to BN254 field +2. `merkle_tree_hashed` - State tree pubkey hashed to BN254 field +3. `discriminator` - 8-byte account discriminator +4. `issuer_hashed` - Issuer pubkey hashed to BN254 field +5. `expectedRoot` - Merkle tree root +6. `verification_id` - 31-byte external context +7. `public_encrypted_data_hash` - SHA256 of encrypted data (first byte zeroed) +8. `nullifier` - Prevents double-spending + +**Private inputs**: +- `credentialPrivateKey` - User's credential secret +- `leaf_index`, `account_leaf_index`, `address` - Account position +- `pathElements[26]` - Merkle proof +- `encrypted_data_hash` - Must match public input + +**Circuit flow**: +1. Derive `credential_pubkey = Poseidon(privateKey)` via `Keypair` template +2. Verify `nullifier = Poseidon(verification_id, privateKey)` +3. Compute `data_hash = Poseidon(issuer_hashed, credential_pubkey)` +4. Compute account hash via `CompressedAccountHash` (adds discriminator domain `+36893488147419103232`) +5. Verify 26-level Merkle proof against `expectedRoot` +6. Verify `public_encrypted_data_hash === encrypted_data_hash` + +### Compressed Account Hash + +The circuit computes: +``` +Poseidon(owner_hashed, leaf_index, merkle_tree_hashed, address, discriminator + 0x2000000000000000, data_hash) +``` + +The `+0x2000000000000000` (36893488147419103232) sets byte 23 to `0x02` for domain separation. + +## Security + +| Check | Location | Description | +|-------|----------|-------------| +| Address tree validation | `create_issuer:60-63`, `add_credential:130-133`, `zk_verify_credential:187-190` | Rejects if `address_tree_pubkey != ADDRESS_TREE_V2` | +| Issuer authorization | `add_credential:111-118` | Reconstructs `IssuerAccount` with signer as `issuer_pubkey`; CPI fails if hash mismatch | +| Counter overflow | `add_credential:121-124` | Uses `checked_add()` for `num_credentials_issued` | +| Groth16 verification | `zk_verify_credential:269-284` | Decompresses G1/G2 points, creates `Groth16Verifier`, calls `verify()` | +| Merkle tree owner/discriminator | `zk_verify_credential:203-207` | Reads root via `read_state_merkle_tree_root()` which validates account owner and discriminator | + +### Privacy Properties + +- Credential verification is private (credential not exposed during proof verification) +- Transaction payer is visible; use a relayer or fresh keypair for full privacy +- Each credential can only be used once per `verification_id` (event account address acts as nullifier) +- Only credential owner can produce a valid proof (requires `credentialPrivateKey`) + +## Errors + +| Code | Name | Message | +|------|------|---------| +| `InvalidIssuer` | 6000 | Invalid issuer: signer is not the issuer of this account | +| `AccountNotEnoughKeys` | 6001 | Not enough keys in remaining accounts | + +Additional errors from `groth16-solana` (returned as `ProgramError::Custom(code)`): +- G1/G2 decompression failures +- Proof verification failures