From 77e5b7f03336a7b3cbb4d75d6692ce17a990aed1 Mon Sep 17 00:00:00 2001 From: tilo-14 Date: Sun, 11 Jan 2026 02:37:19 +0000 Subject: [PATCH 1/5] Move migration guide to resources section --- docs.json | 3 ++- .../migration-v1-to-v2.mdx | 26 ++++++++++--------- 2 files changed, 16 insertions(+), 13 deletions(-) rename {references => resources}/migration-v1-to-v2.mdx (97%) diff --git a/docs.json b/docs.json index 7b29115..b809ed1 100644 --- a/docs.json +++ b/docs.json @@ -231,7 +231,8 @@ "resources/sdks/program-development" ] }, - "resources/addresses-and-urls" + "resources/addresses-and-urls", + "resources/migration-v1-to-v2" ] }, { diff --git a/references/migration-v1-to-v2.mdx b/resources/migration-v1-to-v2.mdx similarity index 97% rename from references/migration-v1-to-v2.mdx rename to resources/migration-v1-to-v2.mdx index 515dfda..0f5e299 100644 --- a/references/migration-v1-to-v2.mdx +++ b/resources/migration-v1-to-v2.mdx @@ -1,19 +1,9 @@ --- -title: Guide to Migrate Programs from v1 to v2 Merkle trees -description: V2 reduces CU consumption by up to 70%. V1 remains supported for existing deployments. +title: Guide to Migrate from v1 to v2 Merkle trees sidebarTitle: V2 Migration Guide +description: V2 reduces CU consumption by up to 70%. V1 remains supported for existing deployments. --- -## V2 Improvements - -| | v1 | v2 | -|---------------------|------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------| -| Merkle tree type | Concurrent | Batched | -| State tree depth | 26 (~67M leaves) | 32 (~4B leaves) | -| Address tree depth | 26 | 40 | -| Address tree accounts| Separate tree + queue | Single batch tree | - - @@ -253,6 +243,18 @@ let new_address_params = instruction_data +## V1 vs V2 Merkle Tree + +| | v1 | v2 | +|---------------------|------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------| +| Merkle tree type | Concurrent | Batched | +| State tree depth | 26 (~67M leaves) | 32 (~4B leaves) | +| Address tree depth | 26 | 40 | +| Address tree accounts| Separate tree + queue | Single batch tree | + + +Learn more about the [batched Merkle tree in this keynote](https://www.youtube.com/watch?v=WMzZdLB9gWE). + ## View Program Examples From 8ac74976540f1a61dfb1832ac7682371193998c4 Mon Sep 17 00:00:00 2001 From: tilo-14 Date: Wed, 14 Jan 2026 21:47:19 +0000 Subject: [PATCH 2/5] Add ZK documentation: nullifiers, compressed accounts, and Groth16 guides - Add ZK overview with circuit development workflow - Add nullifiers guide with usage patterns - Add compressed-account-zk guide for Groth16 integration - Add code snippets for circuits, programs, and clients - Add TypeScript and Rust client examples - Add copy scripts for syncing snippets from examples repo - Add cspell as dev dependency - Add Zcash, circomlibjs to spell dictionary --- .gitignore | 2 + ...-with-compressed-pdas.mdx => overview.mdx} | 0 cspell.json | 11 +- docs.json | 50 +- home.mdx | 2 +- learn/core-concepts/considerations.mdx | 2 +- package-lock.json | 2 +- package.json | 3 +- .../migration-v1-to-v2.mdx | 30 +- resources/cli-installation.mdx | 2 +- scripts/copy-zk-merkle-proof.sh | 43 ++ scripts/copy-zk-nullifier.sh | 42 ++ snippets/ai-prompts/v1-to-v2-migration.mdx | 105 ++++ snippets/ai-prompts/zk-app.mdx | 155 ++++++ .../code-snippets/zk/merkle-proof/circuit.mdx | 84 +++ .../code-snippets/zk/merkle-proof/program.mdx | 172 ++++++ .../zk/merkle-proof/rust-client.mdx | 329 ++++++++++++ .../zk/merkle-proof/typescript-client.mdx | 332 ++++++++++++ .../code-snippets/zk/nullifier/circuit.mdx | 32 ++ .../code-snippets/zk/nullifier/program.mdx | 248 +++++++++ .../zk/nullifier/rust-client.mdx | 441 +++++++++++++++ .../zk/nullifier/typescript-client.mdx | 312 +++++++++++ snippets/mermaid/nullifier-flow.mdx | 18 + .../overview-tables/zk-examples-table.mdx | 4 + welcome.mdx | 2 +- zk/compressed-account-zk.mdx | 507 ++++++++++++++++++ zk/examples.mdx | 10 + zk/nullifiers.mdx | 145 +++++ zk/overview.mdx | 126 +++++ 29 files changed, 3168 insertions(+), 43 deletions(-) rename compressed-pdas/{create-a-program-with-compressed-pdas.mdx => overview.mdx} (100%) rename {resources => references}/migration-v1-to-v2.mdx (76%) create mode 100644 scripts/copy-zk-merkle-proof.sh create mode 100755 scripts/copy-zk-nullifier.sh create mode 100644 snippets/ai-prompts/v1-to-v2-migration.mdx create mode 100644 snippets/ai-prompts/zk-app.mdx create mode 100644 snippets/code-snippets/zk/merkle-proof/circuit.mdx create mode 100644 snippets/code-snippets/zk/merkle-proof/program.mdx create mode 100644 snippets/code-snippets/zk/merkle-proof/rust-client.mdx create mode 100644 snippets/code-snippets/zk/merkle-proof/typescript-client.mdx create mode 100644 snippets/code-snippets/zk/nullifier/circuit.mdx create mode 100644 snippets/code-snippets/zk/nullifier/program.mdx create mode 100644 snippets/code-snippets/zk/nullifier/rust-client.mdx create mode 100644 snippets/code-snippets/zk/nullifier/typescript-client.mdx create mode 100644 snippets/mermaid/nullifier-flow.mdx create mode 100644 snippets/overview-tables/zk-examples-table.mdx create mode 100644 zk/compressed-account-zk.mdx create mode 100644 zk/examples.mdx create mode 100644 zk/nullifiers.mdx create mode 100644 zk/overview.mdx diff --git a/.gitignore b/.gitignore index bc29d0c..bd44d77 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ mintlify-docs/ # Dependencies node_modules/ + +stash \ No newline at end of file diff --git a/compressed-pdas/create-a-program-with-compressed-pdas.mdx b/compressed-pdas/overview.mdx similarity index 100% rename from compressed-pdas/create-a-program-with-compressed-pdas.mdx rename to compressed-pdas/overview.mdx diff --git a/cspell.json b/cspell.json index 0a506eb..4ea32a3 100644 --- a/cspell.json +++ b/cspell.json @@ -175,7 +175,16 @@ "stablecoins", "fintechs", "micropayments", - "clawback" + "clawback", + "hackathon", + "altbn", + "circom", + "snarkjs", + "Zcash", + "zcash", + "circomlibjs", + "Jotaro", + "Yano" ], "ignorePaths": [ "node_modules", diff --git a/docs.json b/docs.json index b809ed1..050a5c2 100644 --- a/docs.json +++ b/docs.json @@ -99,6 +99,33 @@ "quickstart" ] }, + { + "group": "ZK", + "pages": [ + "zk/overview", + "zk/examples" + ] + }, + { + "group": "Compressed PDAs", + "pages": [ + "compressed-pdas/overview", + { + "group": "Program Guides", + "pages": [ + "compressed-pdas/guides", + "compressed-pdas/guides/how-to-create-compressed-accounts", + "compressed-pdas/guides/how-to-update-compressed-accounts", + "compressed-pdas/guides/how-to-close-compressed-accounts", + "compressed-pdas/guides/how-to-reinitialize-compressed-accounts", + "compressed-pdas/guides/how-to-burn-compressed-accounts" + ] + }, + "compressed-pdas/program-examples", + "client-library/client-guide", + "compressed-pdas/solana-attestation-service" + ] + }, { "group": "Compressed Tokens", "pages": [ @@ -144,26 +171,6 @@ } ] }, - { - "group": "Compressed PDAs", - "pages": [ - "compressed-pdas/create-a-program-with-compressed-pdas", - { - "group": "Program Guides", - "pages": [ - "compressed-pdas/guides", - "compressed-pdas/guides/how-to-create-compressed-accounts", - "compressed-pdas/guides/how-to-update-compressed-accounts", - "compressed-pdas/guides/how-to-close-compressed-accounts", - "compressed-pdas/guides/how-to-reinitialize-compressed-accounts", - "compressed-pdas/guides/how-to-burn-compressed-accounts" - ] - }, - "compressed-pdas/program-examples", - "client-library/client-guide", - "compressed-pdas/solana-attestation-service" - ] - }, { "group": "JSON RPC Methods", "pages": [ @@ -231,8 +238,7 @@ "resources/sdks/program-development" ] }, - "resources/addresses-and-urls", - "resources/migration-v1-to-v2" + "resources/addresses-and-urls" ] }, { diff --git a/home.mdx b/home.mdx index 5beda94..061f242 100644 --- a/home.mdx +++ b/home.mdx @@ -189,7 +189,7 @@ import WelcomePageInstall from "/snippets/setup/welcome-page-install.mdx"; Program and client guides for rent-free PDA accounts. diff --git a/learn/core-concepts/considerations.mdx b/learn/core-concepts/considerations.mdx index 94eaaf3..0651fff 100644 --- a/learn/core-concepts/considerations.mdx +++ b/learn/core-concepts/considerations.mdx @@ -81,7 +81,7 @@ You're ready to take the next step and start building! title="Compressed PDAs" icon="chevron-right" color="#0066ff" - href="/compressed-pdas/create-a-program-with-compressed-pdas" + href="/compressed-pdas/overview" horizontal /> \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 7b54323..0ca6b8f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "docs-v2", "version": "1.0.0", "devDependencies": { - "cspell": "^8.6.0", + "cspell": "^8.19.4", "eslint": "^8.57.0", "eslint-plugin-react": "^7.34.1", "eslint-plugin-react-hooks": "^4.6.2", diff --git a/package.json b/package.json index 938ade2..2cecceb 100644 --- a/package.json +++ b/package.json @@ -12,11 +12,10 @@ "ci": "npm run format:check && npm run lint && npm run spellcheck" }, "devDependencies": { - "cspell": "^8.6.0", + "cspell": "^8.19.4", "eslint": "^8.57.0", "eslint-plugin-react": "^7.34.1", "eslint-plugin-react-hooks": "^4.6.2", "prettier": "^3.2.5" } } - diff --git a/resources/migration-v1-to-v2.mdx b/references/migration-v1-to-v2.mdx similarity index 76% rename from resources/migration-v1-to-v2.mdx rename to references/migration-v1-to-v2.mdx index 0f5e299..907cb7c 100644 --- a/resources/migration-v1-to-v2.mdx +++ b/references/migration-v1-to-v2.mdx @@ -1,9 +1,20 @@ --- -title: Guide to Migrate from v1 to v2 Merkle trees -sidebarTitle: V2 Migration Guide +title: Guide to Migrate Programs from v1 to v2 Merkle trees description: V2 reduces CU consumption by up to 70%. V1 remains supported for existing deployments. +sidebarTitle: V2 Migration Guide --- +import V1ToV2MigrationPrompt from "/snippets/ai-prompts/v1-to-v2-migration.mdx"; + +## V2 Improvements + +| | v1 | v2 | +|-----------------------|-----------------------|--------------------| +| Merkle tree type | Concurrent | Batched | +| State tree depth | 26 (~67M leaves) | 32 (~4B leaves) | +| Address tree depth | 26 | 40 | +| Address tree accounts | Separate tree + queue | Single batch tree | + @@ -241,20 +252,13 @@ let new_address_params = instruction_data ``` - + -## V1 vs V2 Merkle Tree + -| | v1 | v2 | -|---------------------|------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------| -| Merkle tree type | Concurrent | Batched | -| State tree depth | 26 (~67M leaves) | 32 (~4B leaves) | -| Address tree depth | 26 | 40 | -| Address tree accounts| Separate tree + queue | Single batch tree | + + - -Learn more about the [batched Merkle tree in this keynote](https://www.youtube.com/watch?v=WMzZdLB9gWE). - ## View Program Examples diff --git a/resources/cli-installation.mdx b/resources/cli-installation.mdx index ed7a6ce..d80cda7 100644 --- a/resources/cli-installation.mdx +++ b/resources/cli-installation.mdx @@ -253,7 +253,7 @@ FLAGS title="Build with compressed PDAs" icon="chevron-right" color="#0066ff" - href="/compressed-pdas/create-a-program-with-compressed-pdas" + href="/compressed-pdas/overview" horizontal /> diff --git a/scripts/copy-zk-merkle-proof.sh b/scripts/copy-zk-merkle-proof.sh new file mode 100644 index 0000000..f94b221 --- /dev/null +++ b/scripts/copy-zk-merkle-proof.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +# Script to copy zk-merkle-proof code from program-examples to docs/snippets +# Wraps each file in appropriate markdown code blocks + +PROGRAM_EXAMPLES="/home/tilo/Workspace/program-examples/zk/zk-merkle-proof" +SNIPPETS_DIR="/home/tilo/Workspace/docs/snippets/code-snippets/zk/merkle-proof" + +mkdir -p "$SNIPPETS_DIR" + +wrap_code() { + local input_file="$1" + local output_file="$2" + local lang="$3" + echo "\`\`\`$lang" > "$output_file" + cat "$input_file" >> "$output_file" + echo '```' >> "$output_file" + echo "Created: $output_file" +} + +# Circuit +if [ -f "$PROGRAM_EXAMPLES/circuits/merkle_proof.circom" ]; then + wrap_code "$PROGRAM_EXAMPLES/circuits/merkle_proof.circom" "$SNIPPETS_DIR/circuit.mdx" "javascript expandable" +fi + +# Program +if [ -f "$PROGRAM_EXAMPLES/src/lib.rs" ]; then + wrap_code "$PROGRAM_EXAMPLES/src/lib.rs" "$SNIPPETS_DIR/program.mdx" "rust expandable" +fi + +# Rust client +if [ -f "$PROGRAM_EXAMPLES/tests/test.rs" ]; then + wrap_code "$PROGRAM_EXAMPLES/tests/test.rs" "$SNIPPETS_DIR/rust-client.mdx" "rust expandable" +fi + +# TypeScript client +if [ -f "$PROGRAM_EXAMPLES/ts-tests/merkle-proof.test.ts" ]; then + wrap_code "$PROGRAM_EXAMPLES/ts-tests/merkle-proof.test.ts" "$SNIPPETS_DIR/typescript-client.mdx" "typescript expandable" +fi + +echo "" +echo "Done! Created snippets in: $SNIPPETS_DIR" +find "$SNIPPETS_DIR" -name "*.mdx" -type f | sort diff --git a/scripts/copy-zk-nullifier.sh b/scripts/copy-zk-nullifier.sh new file mode 100755 index 0000000..32aa8a8 --- /dev/null +++ b/scripts/copy-zk-nullifier.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +# Script to copy zk-nullifier code from program-examples to docs/snippets + +PROGRAM_EXAMPLES="/home/tilo/Workspace/program-examples/zk/zk-nullifier" +SNIPPETS_DIR="/home/tilo/Workspace/docs/snippets/code-snippets/zk/nullifier" + +mkdir -p "$SNIPPETS_DIR" + +wrap_code() { + local input_file="$1" + local output_file="$2" + local lang="$3" + echo "\`\`\`$lang" > "$output_file" + cat "$input_file" >> "$output_file" + echo '```' >> "$output_file" + echo "Created: $output_file" +} + +# Circuit +if [ -f "$PROGRAM_EXAMPLES/circuits/nullifier.circom" ]; then + wrap_code "$PROGRAM_EXAMPLES/circuits/nullifier.circom" "$SNIPPETS_DIR/circuit.mdx" "javascript expandable" +fi + +# Program +if [ -f "$PROGRAM_EXAMPLES/programs/zk-nullifier/src/lib.rs" ]; then + wrap_code "$PROGRAM_EXAMPLES/programs/zk-nullifier/src/lib.rs" "$SNIPPETS_DIR/program.mdx" "rust expandable" +fi + +# Rust client (unified) +if [ -f "$PROGRAM_EXAMPLES/programs/zk-nullifier/tests/test.rs" ]; then + wrap_code "$PROGRAM_EXAMPLES/programs/zk-nullifier/tests/test.rs" "$SNIPPETS_DIR/rust-client.mdx" "rust expandable" +fi + +# TypeScript client +if [ -f "$PROGRAM_EXAMPLES/ts-tests/nullifier.test.ts" ]; then + wrap_code "$PROGRAM_EXAMPLES/ts-tests/nullifier.test.ts" "$SNIPPETS_DIR/typescript-client.mdx" "typescript expandable" +fi + +echo "" +echo "Done! Created snippets in: $SNIPPETS_DIR" +find "$SNIPPETS_DIR" -name "*.mdx" -type f | sort diff --git a/snippets/ai-prompts/v1-to-v2-migration.mdx b/snippets/ai-prompts/v1-to-v2-migration.mdx new file mode 100644 index 0000000..f6743d3 --- /dev/null +++ b/snippets/ai-prompts/v1-to-v2-migration.mdx @@ -0,0 +1,105 @@ +--- +argument-hint: +description: Migrate Light Protocol program from v1 to v2 Merkle trees +allowed-tools: [Bash, Read, Glob, Grep, Task, WebFetch] +--- + +Migrate this Light Protocol program from v1 to v2 Merkle trees. + +## Goal + +Produce a **fully working migration** that builds and tests pass. + +## Available commands + +Via Bash tool: +- `cargo build-sbf`, `cargo test-sbf`, `cargo fmt`, `cargo clippy` +- `anchor build`, `anchor test` +- `grep`, `sed` + +## Documentation + +- Migration Guide: https://zkcompression.com/references/migration-v1-to-v2 +- Reference PR: https://github.com/Lightprotocol/program-examples/commit/54f0e7f15c2972a078f776cfb40b238d83c7e486 + +## Reference repos + +program-examples/counter/anchor/ +├── programs/counter/src/lib.rs # v2 patterns: derive_address, CpiAccounts +├── Cargo.toml # v2 feature flags +└── tests/counter.ts # v2 client patterns + +## Workflow + +### Phase 1: Index program + +Find all v1 patterns: + + grep -r "::v1::" src/ tests/ + grep -r "ADDRESS_TREE_V1" src/ + grep -r "into_new_address_params_packed" src/ + grep -r "get_address_tree_v1" tests/ + +### Phase 2: Update dependencies + +Add v2 feature to Cargo.toml: + + [dependencies] + light-sdk = { version = "0.17", features = ["anchor", "v2"] } + light-sdk-types = { version = "0.17", features = ["v2"] } + + [dev-dependencies] + light-program-test = { version = "0.17", features = ["v2"] } + light-client = { version = "0.17", features = ["v2"] } + +### Phase 3: Rust SDK replacements + +| v1 Pattern | v2 Replacement | +|------------|----------------| +| address::v1::derive_address | address::v2::derive_address | +| cpi::v1::CpiAccounts | cpi::v2::CpiAccounts | +| cpi::v1::LightSystemProgramCpi | cpi::v2::LightSystemProgramCpi | +| constants::ADDRESS_TREE_V1 | light_sdk_types::ADDRESS_TREE_V2 | +| .into_new_address_params_packed(seed) | .into_new_address_params_assigned_packed(seed, Some(0)) | +| .add_system_accounts(config) | .add_system_accounts_v2(config) | + +### Phase 4: TypeScript SDK replacements + +| v1 Pattern | v2 Replacement | +|------------|----------------| +| deriveAddress( | deriveAddressV2( | +| deriveAddressSeed( | deriveAddressSeedV2( | +| defaultTestStateTreeAccounts().addressTree | batchAddressTree | +| .newWithSystemAccounts( | .newWithSystemAccountsV2( | +| get_address_tree_v1() | get_address_tree_v2() | +| get_random_state_tree_info_v1() | get_random_state_tree_info() | + +### Phase 5: Build and test loop + +**Required commands (no shortcuts):** + +For Anchor programs: `anchor build && anchor test` + +For Native programs: `cargo build-sbf && cargo test-sbf` + +**NO shortcuts allowed:** + +- Do NOT use `cargo build` (must use `cargo build-sbf`) +- Do NOT use `cargo test` (must use `cargo test-sbf`) +- Tests MUST run against real BPF bytecode + +**On failure:** Spawn debugger agent with error context. + +**Loop rules:** + +1. Each debugger gets fresh context + previous debug reports +2. Each attempt tries something DIFFERENT +3. **NEVER GIVE UP** - keep spawning until fixed + +Do NOT proceed until all tests pass. + +## DeepWiki fallback + +If no matching pattern in reference repos: + + mcp__deepwiki__ask_question("Lightprotocol/light-protocol", "How to migrate {pattern} from v1 to v2?") \ No newline at end of file diff --git a/snippets/ai-prompts/zk-app.mdx b/snippets/ai-prompts/zk-app.mdx new file mode 100644 index 0000000..9edf37c --- /dev/null +++ b/snippets/ai-prompts/zk-app.mdx @@ -0,0 +1,155 @@ +```markdown expandable +--- +argument-hint: +description: Design a ZK App POC with rent-free nullifiers, compressed accounts, and Groth16 circuits +allowed-tools: [Bash, Read, Glob, Grep, Task, WebFetch] +--- + +Design a Solana program with tests that uses rent-free nullifiers, compressed accounts, and Groth16 circuits. + + +## Initial App Design + + +## Goal + +Produce a **fully working POC** that builds and tests pass. + +## Available commands + +Via Bash tool: +- `cargo build-sbf`, `cargo test-sbf`, `cargo fmt`, `cargo clippy` +- `anchor build`, `anchor test`, `anchor deploy` +- `circom`, `snarkjs`, `solana`, `light` + +## Documentation + +- Nullifiers: https://zkcompression.com/zk/nullifiers +- Compressed Accounts with Poseidon Hashes: https://zkcompression.com/zk/compressed-account-zk + +## Reference repos + +Nullifier example. Does not need Circuit. +nullifier/ +├── programs/nullifier/ +│ ├── src/lib.rs # Program with create_nullifiers helper +│ └── tests/test.rs # Rust integration tests +└── ts-tests/ + └── nullifier.test.ts # TypeScript tests + +Full Example +program-examples/zk/zk-id/ +├── programs/zk-id/src/ +│ ├── lib.rs # create_issuer, add_credential, zk_verify_credential +│ └── verifying_key.rs # Groth16 key from circom trusted setup +├── circuits/ +│ └── compressed_account_merkle_proof.circom # Merkle proof +└── tests/ +└── zk-id.ts # Proof generation + on-chain verification + + +## Workflow + +### Phase 1: Design application + +**1.1 Define private state** + +What data stays private? (credentials, balances, votes, etc.) + +**1.2 Define public inputs** + +What does the circuit prove publicly? (nullifier, merkle root, commitments) + +**1.3 Do not include circuits** + + + +### Phase 2: Index reference implementation + + grep -r "LightAccountPoseidon" program-examples/zk/ + grep -r "Groth16Verifier" program-examples/zk/ + grep -r "derive_address.*nullifier" program-examples/zk/ + grep -r "read_state_merkle_tree_root" program-examples/zk/ + +Read matching files to understand patterns. + +### Phase 3: Circuit development + +**3.1 Write circom circuit** + +Based on compressed_account_merkle_proof.circom: +- Merkle proof verification +- Nullifier computation +- Public input constraints + +**3.2 Trusted setup** + + circom circuit.circom --r1cs --wasm --sym + snarkjs groth16 setup circuit.r1cs pot_final.ptau circuit_0000.zkey + snarkjs zkey export verificationkey circuit_final.zkey verification_key.json + snarkjs zkey export solidityverifier circuit_final.zkey # adapt for Solana + +**3.3 Add sensitive files to .gitignore** + + *.zkey + *.ptau + *.r1cs + *_js/ + +### Phase 4: Program implementation + +| Pattern | Function | Reference | +|---------|----------|-----------| +| Poseidon state | `LightAccountPoseidon::new_init()` | zk-id/lib.rs | +| Nullifier address | `derive_address([prefix, nullifier, ctx], tree, program)` | zk-id/lib.rs | +| Read root only | `read_state_merkle_tree_root()` | zk-id/lib.rs | +| Groth16 verify | `Groth16Verifier::new().verify()` | zk-id/lib.rs | + +**Dependencies:** + + [dependencies] + anchor-lang = "0.31.1" + light-sdk = { version = "0.17.1", features = ["anchor", "poseidon", "merkle-tree", "v2"] } + light-hasher = "5.0.0" + light-sdk-types = { version = "0.17.1", features = ["v2"] } + groth16-solana = { git = "https://github.com/Lightprotocol/groth16-solana", rev = "66c0dc87" } + + [dev-dependencies] + light-program-test = "0.17.1" + light-client = "0.17.1" + +### Phase 5: Build and test loop + +**Required commands (no shortcuts):** + +For Anchor programs: `anchor build && anchor test` + +For Native programs: `cargo build-sbf && cargo test-sbf` + +**NO shortcuts allowed:** + +- Do NOT use `cargo build` (must use `cargo build-sbf`) +- Do NOT use `cargo test` (must use `cargo test-sbf`) +- Do NOT skip SBF compilation +- Tests MUST run against real BPF bytecode + +**On failure:** Spawn debugger agent with error context. + +**Loop rules:** + +1. Each debugger gets fresh context + previous debug reports +2. Each attempt tries something DIFFERENT +3. **NEVER GIVE UP** - keep spawning until fixed + +Do NOT proceed until all tests pass. + +### Phase 6: Cleanup (only after tests pass) + + rm -rf target/ + +## DeepWiki fallback + +If no matching pattern in reference repos: + + mcp__deepwiki__ask_question("Lightprotocol/light-protocol", "How to {operation}?") +``` \ No newline at end of file diff --git a/snippets/code-snippets/zk/merkle-proof/circuit.mdx b/snippets/code-snippets/zk/merkle-proof/circuit.mdx new file mode 100644 index 0000000..d650167 --- /dev/null +++ b/snippets/code-snippets/zk/merkle-proof/circuit.mdx @@ -0,0 +1,84 @@ +```javascript expandable +pragma circom 2.0.0; + +include "../node_modules/circomlib/circuits/poseidon.circom"; +include "../node_modules/circomlib/circuits/bitify.circom"; +include "../node_modules/circomlib/circuits/switcher.circom"; + +// Merkle Proof Verification Template +// Verifies that a leaf is in a Merkle tree with a given root +template MerkleProof(levels) { + signal input leaf; + signal input pathElements[levels]; + signal input leafIndex; + signal output root; + + component switcher[levels]; + component hasher[levels]; + component indexBits = Num2Bits(levels); + indexBits.in <== leafIndex; + + for (var i = 0; i < levels; i++) { + switcher[i] = Switcher(); + switcher[i].L <== i == 0 ? leaf : hasher[i - 1].out; + switcher[i].R <== pathElements[i]; + switcher[i].sel <== indexBits.out[i]; + + hasher[i] = Poseidon(2); + hasher[i].inputs[0] <== switcher[i].outL; + hasher[i].inputs[1] <== switcher[i].outR; + } + + root <== hasher[levels - 1].out; +} + +template CompressedAccountHash() { + signal input owner_hashed; + signal input leaf_index; + signal input merkle_tree_hashed; + signal input address; + signal input discriminator; + signal input data_hash; + signal output hash; + + component poseidon = Poseidon(6); + poseidon.inputs[0] <== owner_hashed; + poseidon.inputs[1] <== leaf_index; + poseidon.inputs[2] <== merkle_tree_hashed; + poseidon.inputs[3] <== address; + poseidon.inputs[4] <== discriminator + 36893488147419103232; + poseidon.inputs[5] <== data_hash; + hash <== poseidon.out; +} + +template CompressedAccountMerkleProof(levels) { + signal input owner_hashed; + signal input merkle_tree_hashed; + signal input discriminator; + signal input data_hash; + signal input expectedRoot; + + signal input leaf_index; + signal input account_leaf_index; + signal input address; + signal input pathElements[levels]; + + component accountHasher = CompressedAccountHash(); + accountHasher.owner_hashed <== owner_hashed; + accountHasher.leaf_index <== account_leaf_index; + accountHasher.address <== address; + accountHasher.merkle_tree_hashed <== merkle_tree_hashed; + accountHasher.discriminator <== discriminator; + accountHasher.data_hash <== data_hash; + + component merkleProof = MerkleProof(levels); + merkleProof.leaf <== accountHasher.hash; + merkleProof.pathElements <== pathElements; + merkleProof.leafIndex <== leaf_index; + merkleProof.root === expectedRoot; +} + +component main { + public [owner_hashed, merkle_tree_hashed, discriminator, data_hash, expectedRoot] +} = CompressedAccountMerkleProof(26); +``` diff --git a/snippets/code-snippets/zk/merkle-proof/program.mdx b/snippets/code-snippets/zk/merkle-proof/program.mdx new file mode 100644 index 0000000..e309ed7 --- /dev/null +++ b/snippets/code-snippets/zk/merkle-proof/program.mdx @@ -0,0 +1,172 @@ +```rust expandable +#![allow(unexpected_cfgs)] +#![allow(deprecated)] + +use anchor_lang::prelude::*; +use borsh::{BorshDeserialize, BorshSerialize}; +use groth16_solana::groth16::Groth16Verifier; +use light_hasher::to_byte_array::ToByteArray; +use light_hasher::HasherError; +use light_sdk::account::poseidon::LightAccount as LightAccountPoseidon; +use light_sdk::address::v2::derive_address; +use light_sdk::cpi::v1::CpiAccounts; +use light_sdk::{ + cpi::{v1::LightSystemProgramCpi, InvokeLightSystemProgram, LightCpiInstruction}, + derive_light_cpi_signer, + instruction::{CompressedProof, PackedAddressTreeInfo, ValidityProof}, + merkle_tree::v1::read_state_merkle_tree_root, + LightDiscriminator, LightHasher, +}; +use light_sdk_types::CpiSigner; + +declare_id!("MPzkYomvQc4VQPwMr6bFduyWRQZVCh5CofgDC4dFqJp"); + +pub const LIGHT_CPI_SIGNER: CpiSigner = + derive_light_cpi_signer!("MPzkYomvQc4VQPwMr6bFduyWRQZVCh5CofgDC4dFqJp"); + +pub const ZK_ACCOUNT: &[u8] = b"zk_account"; + +pub mod verifying_key; + +#[program] +pub mod zk_merkle_proof { + use groth16_solana::decompression::{decompress_g1, decompress_g2}; + use light_hasher::hash_to_field_size::hashv_to_bn254_field_size_be_const_array; + + use super::*; + + pub fn create_account<'info>( + ctx: Context<'_, '_, '_, 'info, CreateAccountAccounts<'info>>, + proof: ValidityProof, + address_tree_info: PackedAddressTreeInfo, + output_state_tree_index: u8, + data_hash: [u8; 32], + ) -> Result<()> { + let light_cpi_accounts = CpiAccounts::new( + ctx.accounts.signer.as_ref(), + ctx.remaining_accounts, + crate::LIGHT_CPI_SIGNER, + ); + + let address_tree_pubkey = address_tree_info + .get_tree_pubkey(&light_cpi_accounts) + .map_err(|_| ProgramError::InvalidAccountData)?; + + let (address, address_seed) = derive_address( + &[ZK_ACCOUNT, &data_hash], + &address_tree_pubkey, + &crate::ID, + ); + + let mut account = LightAccountPoseidon::::new_init( + &crate::ID, + Some(address), + output_state_tree_index, + ); + + account.data_hash = DataHash(data_hash); + + LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, proof) + .with_light_account_poseidon(account)? + .with_new_addresses(&[address_tree_info.into_new_address_params_packed(address_seed)]) + .invoke(light_cpi_accounts)?; + + Ok(()) + } + + pub fn verify_account<'info>( + ctx: Context<'_, '_, '_, 'info, VerifyAccountAccounts<'info>>, + input_root_index: u16, + zk_proof: CompressedProof, + data_hash: [u8; 32], + ) -> Result<()> { + let expected_root = read_state_merkle_tree_root( + &ctx.accounts.state_merkle_tree.to_account_info(), + input_root_index, + ) + .map_err(ProgramError::from)?; + + let owner_hashed = + hashv_to_bn254_field_size_be_const_array::<2>(&[&crate::ID.to_bytes()]).unwrap(); + + let merkle_tree_pubkey = ctx.accounts.state_merkle_tree.key(); + let merkle_tree_hashed = + hashv_to_bn254_field_size_be_const_array::<2>(&[&merkle_tree_pubkey.to_bytes()]) + .unwrap(); + + let mut discriminator = [0u8; 32]; + discriminator[24..].copy_from_slice(ZkAccount::LIGHT_DISCRIMINATOR_SLICE); + + let public_inputs: [[u8; 32]; 5] = [ + owner_hashed, + merkle_tree_hashed, + discriminator, + data_hash, + expected_root, + ]; + + let proof_a = decompress_g1(&zk_proof.a).map_err(|e| { + let code: u32 = e.into(); + Error::from(ProgramError::Custom(code)) + })?; + + let proof_b = decompress_g2(&zk_proof.b).map_err(|e| { + let code: u32 = e.into(); + Error::from(ProgramError::Custom(code)) + })?; + + let proof_c = decompress_g1(&zk_proof.c).map_err(|e| { + let code: u32 = e.into(); + Error::from(ProgramError::Custom(code)) + })?; + + let mut verifier = Groth16Verifier::new( + &proof_a, + &proof_b, + &proof_c, + &public_inputs, + &crate::verifying_key::VERIFYINGKEY, + ) + .map_err(|e| { + let code: u32 = e.into(); + Error::from(ProgramError::Custom(code)) + })?; + + verifier.verify().map_err(|e| { + let code: u32 = e.into(); + Error::from(ProgramError::Custom(code)) + })?; + + Ok(()) + } +} + +#[derive(Accounts)] +pub struct CreateAccountAccounts<'info> { + #[account(mut)] + pub signer: Signer<'info>, +} + +#[derive(Accounts)] +pub struct VerifyAccountAccounts<'info> { + #[account(mut)] + pub signer: Signer<'info>, + /// CHECK: validated by read_state_merkle_tree_root + pub state_merkle_tree: UncheckedAccount<'info>, +} + +#[derive(Clone, Debug, Default, BorshSerialize, BorshDeserialize, LightDiscriminator, LightHasher)] +pub struct ZkAccount { + pub data_hash: DataHash, +} + +#[derive(Clone, Debug, Default, BorshSerialize, BorshDeserialize)] +pub struct DataHash(pub [u8; 32]); + +impl ToByteArray for DataHash { + const NUM_FIELDS: usize = 1; + fn to_byte_array(&self) -> std::result::Result<[u8; 32], HasherError> { + Ok(self.0) + } +} +``` diff --git a/snippets/code-snippets/zk/merkle-proof/rust-client.mdx b/snippets/code-snippets/zk/merkle-proof/rust-client.mdx new file mode 100644 index 0000000..3ad10bc --- /dev/null +++ b/snippets/code-snippets/zk/merkle-proof/rust-client.mdx @@ -0,0 +1,329 @@ +```rust expandable +use anchor_lang::{InstructionData, ToAccountMetas}; +use circom_prover::{prover::ProofLib, witness::WitnessFn, CircomProver}; +use groth16_solana::proof_parser::circom_prover::convert_proof; +use light_hasher::{hash_to_field_size::hash_to_bn254_field_size_be, Hasher, Poseidon}; +use light_program_test::{ + program_test::LightProgramTest, AddressWithTree, Indexer, ProgramTestConfig, Rpc, RpcError, +}; +use light_sdk::{ + address::v2::derive_address, + instruction::{PackedAccounts, SystemAccountMetaConfig}, +}; +use num_bigint::BigUint; +use solana_sdk::{ + instruction::Instruction, + pubkey::Pubkey, + signature::{Keypair, Signature, Signer}, +}; +use std::collections::HashMap; +use zk_merkle_proof::ZK_ACCOUNT; + +#[link(name = "circuit", kind = "static")] +extern "C" {} + +rust_witness::witness!(merkleproof); + +#[tokio::test] +async fn test_create_and_verify_account() { + let config = + ProgramTestConfig::new(true, Some(vec![("zk_merkle_proof", zk_merkle_proof::ID)])); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + // First byte = 0 for BN254 field compatibility + let mut secret_data = [0u8; 32]; + for i in 1..32 { + secret_data[i] = (i as u8) + 65; + } + let data_hash = Poseidon::hashv(&[&secret_data]).unwrap(); + + let address_tree_info = rpc.get_address_tree_v2(); + + let (account_address, _) = derive_address( + &[ZK_ACCOUNT, &data_hash], + &address_tree_info.tree, + &zk_merkle_proof::ID, + ); + + create_account(&mut rpc, &payer, &account_address, address_tree_info.clone(), data_hash) + .await + .unwrap(); + + let accounts = rpc + .get_compressed_accounts_by_owner(&zk_merkle_proof::ID, None, None) + .await + .unwrap(); + assert_eq!(accounts.value.items.len(), 1); + let created_account = &accounts.value.items[0]; + + let account_data_hash = created_account.data.as_ref().unwrap().data_hash; + + verify_account(&mut rpc, &payer, created_account, account_data_hash) + .await + .unwrap(); +} + +async fn create_account( + rpc: &mut R, + payer: &Keypair, + address: &[u8; 32], + address_tree_info: light_client::indexer::TreeInfo, + data_hash: [u8; 32], +) -> Result +where + R: Rpc + Indexer, +{ + let mut remaining_accounts = PackedAccounts::default(); + remaining_accounts.add_pre_accounts_signer(payer.pubkey()); + let config = SystemAccountMetaConfig::new(zk_merkle_proof::ID); + remaining_accounts.add_system_accounts_v2(config)?; + + let rpc_result = rpc + .get_validity_proof( + vec![], + vec![AddressWithTree { + address: *address, + tree: address_tree_info.tree, + }], + None, + ) + .await? + .value; + + let packed_address_tree_accounts = rpc_result + .pack_tree_infos(&mut remaining_accounts) + .address_trees; + + let output_state_tree_index = rpc + .get_random_state_tree_info_v1()? + .pack_output_tree_index(&mut remaining_accounts)?; + + let (remaining_accounts_metas, system_accounts_offset, _) = remaining_accounts.to_account_metas(); + + let instruction_data = zk_merkle_proof::instruction::CreateAccount { + proof: rpc_result.proof, + address_tree_info: packed_address_tree_accounts[0], + output_state_tree_index, + system_accounts_offset: system_accounts_offset as u8, + data_hash, + }; + + let accounts = zk_merkle_proof::accounts::CreateAccountAccounts { + signer: payer.pubkey(), + }; + + let instruction = Instruction { + program_id: zk_merkle_proof::ID, + accounts: [ + accounts.to_account_metas(None), + remaining_accounts_metas, + ] + .concat(), + data: instruction_data.data(), + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await +} + +async fn verify_account( + rpc: &mut R, + payer: &Keypair, + account: &light_client::indexer::CompressedAccount, + data_hash: [u8; 32], +) -> Result +where + R: Rpc + Indexer, +{ + let proofs_result = rpc + .get_multiple_compressed_account_proofs(vec![account.hash], None) + .await?; + let proofs = proofs_result.value.items; + + assert!(!proofs.is_empty(), "No proofs returned"); + + let merkle_proof = &proofs[0]; + let leaf_index = merkle_proof.leaf_index as u32; + let merkle_proof_hashes = &merkle_proof.proof; + let merkle_root = merkle_proof.root; + let root_index = (merkle_proof.root_seq % 2400) as u16; + let state_tree = merkle_proof.merkle_tree; + + let zk_proof = generate_merkle_proof( + account, + &state_tree, + leaf_index, + merkle_proof_hashes, + &merkle_root, + &data_hash, + ); + + let mut remaining_accounts = PackedAccounts::default(); + let config = SystemAccountMetaConfig::new(zk_merkle_proof::ID); + remaining_accounts.add_system_accounts(config)?; + + let instruction_data = zk_merkle_proof::instruction::VerifyAccount { + input_root_index: root_index, + zk_proof, + data_hash, + }; + + let accounts = zk_merkle_proof::accounts::VerifyAccountAccounts { + signer: payer.pubkey(), + state_merkle_tree: state_tree, + }; + + let instruction = Instruction { + program_id: zk_merkle_proof::ID, + accounts: [ + accounts.to_account_metas(None), + remaining_accounts.to_account_metas().0, + ] + .concat(), + data: instruction_data.data(), + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await +} + +fn generate_merkle_proof( + account: &light_client::indexer::CompressedAccount, + merkle_tree_pubkey: &Pubkey, + leaf_index: u32, + merkle_proof_hashes: &[[u8; 32]], + merkle_root: &[u8; 32], + data_hash: &[u8; 32], +) -> light_compressed_account::instruction_data::compressed_proof::CompressedProof { + let zkey_path = "./build/merkle_proof_final.zkey".to_string(); + + let mut proof_inputs = HashMap::new(); + + let owner_hashed = hash_to_bn254_field_size_be(zk_merkle_proof::ID.as_ref()); + let merkle_tree_hashed = hash_to_bn254_field_size_be(merkle_tree_pubkey.as_ref()); + + let discriminator = if let Some(ref data) = account.data { + data.discriminator + } else { + [0u8; 8] + }; + + let address = account.address.expect("Account must have an address"); + + // Verify hash can be recreated + let mut leaf_index_bytes = [0u8; 32]; + leaf_index_bytes[28..32].copy_from_slice(&(account.leaf_index as u32).to_le_bytes()); + + let mut discriminator_with_domain = [0u8; 32]; + discriminator_with_domain[24..32].copy_from_slice(&discriminator); + discriminator_with_domain[23] = 2; + + let computed_hash = Poseidon::hashv(&[ + owner_hashed.as_slice(), + leaf_index_bytes.as_slice(), + merkle_tree_hashed.as_slice(), + address.as_slice(), + discriminator_with_domain.as_slice(), + data_hash.as_slice(), + ]) + .unwrap(); + + assert_eq!(computed_hash, account.hash, "Hash mismatch"); + + // Public inputs + proof_inputs.insert( + "owner_hashed".to_string(), + vec![BigUint::from_bytes_be(&owner_hashed).to_string()], + ); + proof_inputs.insert( + "merkle_tree_hashed".to_string(), + vec![BigUint::from_bytes_be(&merkle_tree_hashed).to_string()], + ); + proof_inputs.insert( + "discriminator".to_string(), + vec![BigUint::from_bytes_be(&discriminator).to_string()], + ); + proof_inputs.insert( + "data_hash".to_string(), + vec![BigUint::from_bytes_be(data_hash).to_string()], + ); + proof_inputs.insert( + "expectedRoot".to_string(), + vec![BigUint::from_bytes_be(merkle_root).to_string()], + ); + + // Private inputs + proof_inputs.insert("leaf_index".to_string(), vec![leaf_index.to_string()]); + + let mut account_leaf_index_bytes = [0u8; 32]; + account_leaf_index_bytes[28..32].copy_from_slice(&(account.leaf_index as u32).to_le_bytes()); + proof_inputs.insert( + "account_leaf_index".to_string(), + vec![BigUint::from_bytes_be(&account_leaf_index_bytes).to_string()], + ); + + proof_inputs.insert( + "address".to_string(), + vec![BigUint::from_bytes_be(&address).to_string()], + ); + + let path_elements: Vec = merkle_proof_hashes + .iter() + .map(|hash| BigUint::from_bytes_be(hash).to_string()) + .collect(); + proof_inputs.insert("pathElements".to_string(), path_elements); + + let circuit_inputs = serde_json::to_string(&proof_inputs).unwrap(); + + let proof = CircomProver::prove( + ProofLib::Arkworks, + WitnessFn::RustWitness(merkleproof_witness), + circuit_inputs, + zkey_path.clone(), + ) + .expect("Proof generation failed"); + + let is_valid = CircomProver::verify(ProofLib::Arkworks, proof.clone(), zkey_path.clone()) + .expect("Proof verification failed"); + assert!(is_valid, "Local proof verification failed"); + + // Verify with groth16-solana + { + use groth16_solana::groth16::Groth16Verifier; + use groth16_solana::proof_parser::circom_prover::convert_public_inputs; + + let (proof_a, proof_b, proof_c) = + convert_proof(&proof.proof).expect("Failed to convert proof"); + let public_inputs_converted: [[u8; 32]; 5] = convert_public_inputs(&proof.pub_inputs); + + let mut verifier = Groth16Verifier::new( + &proof_a, + &proof_b, + &proof_c, + &public_inputs_converted, + &zk_merkle_proof::verifying_key::VERIFYINGKEY, + ) + .expect("Failed to create verifier"); + + verifier.verify().expect("groth16-solana verification failed"); + } + + let (proof_a_uncompressed, proof_b_uncompressed, proof_c_uncompressed) = + convert_proof(&proof.proof).expect("Failed to convert proof"); + + use groth16_solana::proof_parser::circom_prover::convert_proof_to_compressed; + let (proof_a, proof_b, proof_c) = convert_proof_to_compressed( + &proof_a_uncompressed, + &proof_b_uncompressed, + &proof_c_uncompressed, + ) + .expect("Failed to compress proof"); + + light_compressed_account::instruction_data::compressed_proof::CompressedProof { + a: proof_a, + b: proof_b, + c: proof_c, + } +} +``` diff --git a/snippets/code-snippets/zk/merkle-proof/typescript-client.mdx b/snippets/code-snippets/zk/merkle-proof/typescript-client.mdx new file mode 100644 index 0000000..aafb96b --- /dev/null +++ b/snippets/code-snippets/zk/merkle-proof/typescript-client.mdx @@ -0,0 +1,332 @@ +```typescript expandable +import { web3, Program, AnchorProvider, setProvider } from "@coral-xyz/anchor"; +import { + bn, + createRpc, + deriveAddressSeedV2, + deriveAddressV2, + batchAddressTree, + PackedAccounts, + Rpc, + sleep, + SystemAccountMetaConfig, + defaultTestStateTreeAccounts, + featureFlags, + VERSION, + confirmTx, +} from "@lightprotocol/stateless.js"; +import { buildPoseidonOpt } from "circomlibjs"; +import { keccak_256 } from "@noble/hashes/sha3"; +import * as snarkjs from "snarkjs"; +import * as assert from "assert"; +import * as path from "path"; +import * as fs from "fs"; + +import { + parseProofToCompressed, + bigintToBytes32, + toFieldString, + generateFieldElement, +} from "./utils/proof-helpers"; + +// Force V2 mode +(featureFlags as any).version = VERSION.V2; + +// Load IDL +const IDL = JSON.parse( + fs.readFileSync(path.join(process.cwd(), "target/idl/zk_merkle_proof.json"), "utf8") +); + +// Program ID from IDL +const PROGRAM_ID = new web3.PublicKey(IDL.address); +const ZK_ACCOUNT_PREFIX = Buffer.from("zk_account"); +const ZK_ACCOUNT_DISCRIMINATOR = Buffer.from([0x5b, 0x98, 0xb8, 0x43, 0x93, 0x6c, 0x21, 0xf4]); + +// Paths to circuit artifacts +const BUILD_DIR = path.join(process.cwd(), "build"); +const WASM_PATH = path.join(BUILD_DIR, "merkle_proof_js/merkle_proof.wasm"); +const ZKEY_PATH = path.join(BUILD_DIR, "merkle_proof_final.zkey"); + +const MERKLE_TREE_DEPTH = 26; + +/** Hash to BN254 field (matching Light Protocol's hashv_to_bn254_field_size_be) */ +function hashToBn254Field(data: Uint8Array): Uint8Array { + const hash = keccak_256(data); + hash[0] = hash[0] & 0x1f; + return hash; +} + +describe("zk-merkle-proof", () => { + let rpc: Rpc; + let signer: web3.Keypair; + let poseidon: any; + let program: Program; + + before(async () => { + rpc = createRpc( + "http://127.0.0.1:8899", + "http://127.0.0.1:8784", + "http://127.0.0.1:3001", + { commitment: "confirmed" } + ); + + signer = web3.Keypair.generate(); + await rpc.requestAirdrop(signer.publicKey, web3.LAMPORTS_PER_SOL); + await sleep(2000); + + poseidon = await buildPoseidonOpt(); + + // Setup Anchor provider and program + const connection = new web3.Connection("http://127.0.0.1:8899", "confirmed"); + const wallet = { + publicKey: signer.publicKey, + signTransaction: async (tx: web3.Transaction) => { + tx.sign(signer); + return tx; + }, + signAllTransactions: async (txs: web3.Transaction[]) => { + txs.forEach((tx) => tx.sign(signer)); + return txs; + }, + }; + const provider = new AnchorProvider(connection, wallet as any, { commitment: "confirmed" }); + setProvider(provider); + program = new Program(IDL, provider); + }); + + after(async () => { + // Terminate snarkjs curve worker to allow clean exit + // @ts-ignore + if (globalThis.curve_bn128) { + // @ts-ignore + await globalThis.curve_bn128.terminate(); + } + }); + + /** Compute Poseidon hash of compressed account fields */ + function computeAccountHash( + ownerHashed: Uint8Array, + leafIndex: bigint, + merkleTreeHashed: Uint8Array, + address: Uint8Array, + discriminator: bigint, + dataHash: Uint8Array + ): Uint8Array { + const LAMPORTS_OFFSET = 36893488147419103232n; + const hash = poseidon([ + BigInt("0x" + Buffer.from(ownerHashed).toString("hex")), + leafIndex, + BigInt("0x" + Buffer.from(merkleTreeHashed).toString("hex")), + BigInt("0x" + Buffer.from(address).toString("hex")), + discriminator + LAMPORTS_OFFSET, + BigInt("0x" + Buffer.from(dataHash).toString("hex")), + ]); + return bigintToBytes32(poseidon.F.toObject(hash)); + } + + /** Compute Merkle root from leaf and path */ + function computeMerkleRoot(leaf: Uint8Array, pathElements: Uint8Array[], leafIndex: number): Uint8Array { + let current = BigInt("0x" + Buffer.from(leaf).toString("hex")); + + for (let i = 0; i < pathElements.length; i++) { + const pathElement = BigInt("0x" + Buffer.from(pathElements[i]).toString("hex")); + const isRight = (leafIndex >> i) & 1; + const [left, right] = isRight ? [pathElement, current] : [current, pathElement]; + current = poseidon.F.toObject(poseidon([left, right])); + } + + return bigintToBytes32(current); + } + + /** Generate ZK proof for Merkle inclusion */ + async function generateMerkleProof( + ownerHashed: Uint8Array, + merkleTreeHashed: Uint8Array, + discriminator: Uint8Array, + dataHash: Uint8Array, + expectedRoot: Uint8Array, + leafIndex: number, + accountLeafIndex: number, + address: Uint8Array, + pathElements: Uint8Array[] + ): Promise<{ a: number[]; b: number[]; c: number[] }> { + const inputs = { + owner_hashed: toFieldString(ownerHashed), + merkle_tree_hashed: toFieldString(merkleTreeHashed), + discriminator: toFieldString(discriminator), + data_hash: toFieldString(dataHash), + expectedRoot: toFieldString(expectedRoot), + leaf_index: leafIndex.toString(), + account_leaf_index: accountLeafIndex.toString(), + address: toFieldString(address), + pathElements: pathElements.map(toFieldString), + }; + + const { proof } = await snarkjs.groth16.fullProve(inputs, WASM_PATH, ZKEY_PATH); + return parseProofToCompressed(proof); + } + + /** Build create_account instruction using Anchor */ + async function buildCreateAccountInstruction(dataHash: Uint8Array): Promise { + const addressTree = new web3.PublicKey(batchAddressTree); + const outputStateTree = defaultTestStateTreeAccounts().merkleTree; + + const seed = deriveAddressSeedV2([ZK_ACCOUNT_PREFIX, dataHash]); + const address = deriveAddressV2(seed, addressTree, PROGRAM_ID); + + const proofResult = await rpc.getValidityProofV0( + [], + [{ tree: addressTree, queue: addressTree, address: bn(address.toBytes()) }] + ); + + const remainingAccounts = new PackedAccounts(); + remainingAccounts.addPreAccountsSigner(signer.publicKey); + remainingAccounts.addSystemAccountsV2(SystemAccountMetaConfig.new(PROGRAM_ID)); + + const addressMerkleTreeIndex = remainingAccounts.insertOrGet(addressTree); + const outputStateTreeIndex = remainingAccounts.insertOrGet(outputStateTree); + + const { remainingAccounts: accountMetas, systemStart } = remainingAccounts.toAccountMetas(); + + // Use Anchor to build instruction + // ValidityProof is a struct with an unnamed Option field + const proof = { + 0: proofResult.compressedProof, + }; + + const ix = await program.methods + .createAccount( + // proof (ValidityProof = struct with Option) + proof, + // address_tree_info (PackedAddressTreeInfo) + { + addressMerkleTreePubkeyIndex: addressMerkleTreeIndex, + addressQueuePubkeyIndex: addressMerkleTreeIndex, + rootIndex: proofResult.rootIndices[0], + }, + // output_state_tree_index + outputStateTreeIndex, + // system_accounts_offset + systemStart, + // data_hash + Array.from(dataHash) + ) + .accounts({ + signer: signer.publicKey, + }) + .remainingAccounts(accountMetas) + .instruction(); + + return ix; + } + + describe("create_account", () => { + it("should create a compressed account with data hash", async () => { + const dataHash = generateFieldElement(); + console.log("Data hash:", Buffer.from(dataHash).toString("hex").slice(0, 16) + "..."); + + const ix = await buildCreateAccountInstruction(dataHash); + const computeIx = web3.ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 }); + + const tx = new web3.Transaction().add(computeIx, ix); + tx.recentBlockhash = (await rpc.getLatestBlockhash()).blockhash; + tx.feePayer = signer.publicKey; + tx.sign(signer); + + const sig = await rpc.sendTransaction(tx, [signer]); + await confirmTx(rpc, sig); + + console.log("Transaction signature:", sig); + + const slot = await rpc.getSlot(); + await rpc.confirmTransactionIndexed(slot); + + const accounts = await rpc.getCompressedAccountsByOwner(PROGRAM_ID); + assert.ok(accounts.items.length > 0, "Account should be created"); + console.log("Created accounts:", accounts.items.length); + }); + }); + + describe("verify_account (ZK proof)", () => { + it("should verify account existence with ZK proof", async () => { + const dataHash = generateFieldElement(); + + const createIx = await buildCreateAccountInstruction(dataHash); + const computeIx = web3.ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 }); + + const createTx = new web3.Transaction().add(computeIx, createIx); + createTx.recentBlockhash = (await rpc.getLatestBlockhash()).blockhash; + createTx.feePayer = signer.publicKey; + createTx.sign(signer); + + await rpc.sendTransaction(createTx, [signer]); + await sleep(3000); + + const slot = await rpc.getSlot(); + await rpc.confirmTransactionIndexed(slot); + + const accounts = await rpc.getCompressedAccountsByOwner(PROGRAM_ID); + assert.ok(accounts.items.length > 0, "Should have created account"); + + const account = accounts.items[0]; + console.log("Account hash:", account.hash.toString(16).slice(0, 16) + "..."); + console.log("Leaf index:", account.leafIndex); + + const merkleProof = await rpc.getValidityProof([account.hash]); + console.log("Root index:", merkleProof.rootIndices[0]); + + assert.ok(merkleProof.compressedProof, "Should have compressed proof"); + assert.ok(merkleProof.rootIndices.length > 0, "Should have root indices"); + + console.log("Account verified in state tree"); + }); + + it("should demonstrate ZK proof generation for Merkle inclusion", async () => { + const ownerHashed = hashToBn254Field(PROGRAM_ID.toBytes()); + const merkleTreeHashed = hashToBn254Field( + new web3.PublicKey(defaultTestStateTreeAccounts().merkleTree).toBytes() + ); + + const dataHash = generateFieldElement(); + const discriminator = new Uint8Array(32); + discriminator.set(ZK_ACCOUNT_DISCRIMINATOR, 24); + + const pathElements = Array.from({ length: MERKLE_TREE_DEPTH }, () => new Uint8Array(32)); + const address = generateFieldElement(); + + const accountHash = computeAccountHash( + ownerHashed, + 0n, + merkleTreeHashed, + address, + BigInt("0x" + Buffer.from(discriminator).toString("hex")), + dataHash + ); + + const expectedRoot = computeMerkleRoot(accountHash, pathElements, 0); + + console.log("Account hash:", Buffer.from(accountHash).toString("hex").slice(0, 16) + "..."); + console.log("Expected root:", Buffer.from(expectedRoot).toString("hex").slice(0, 16) + "..."); + console.log("Generating ZK proof..."); + + const zkProof = await generateMerkleProof( + ownerHashed, + merkleTreeHashed, + discriminator, + dataHash, + expectedRoot, + 0, + 0, + address, + pathElements + ); + + assert.ok(zkProof.a.length === 32, "Proof A should be 32 bytes"); + assert.ok(zkProof.b.length === 64, "Proof B should be 64 bytes"); + assert.ok(zkProof.c.length === 32, "Proof C should be 32 bytes"); + + console.log("ZK Merkle proof generated successfully"); + }); + }); +}); +``` diff --git a/snippets/code-snippets/zk/nullifier/circuit.mdx b/snippets/code-snippets/zk/nullifier/circuit.mdx new file mode 100644 index 0000000..dc584d4 --- /dev/null +++ b/snippets/code-snippets/zk/nullifier/circuit.mdx @@ -0,0 +1,32 @@ +```javascript expandable +pragma circom 2.0.0; + +include "../node_modules/circomlib/circuits/poseidon.circom"; + +// Single nullifier: proves nullifier = Poseidon(verification_id, secret) +template Nullifier() { + signal input verification_id; + signal input nullifier; + signal input secret; + + component hasher = Poseidon(2); + hasher.inputs[0] <== verification_id; + hasher.inputs[1] <== secret; + nullifier === hasher.out; +} + +// Batch nullifier: proves n nullifiers with single proof +template BatchNullifier(n) { + signal input verification_id; + signal input nullifier[n]; + signal input secret[n]; + + component nullifiers[n]; + for (var i = 0; i < n; i++) { + nullifiers[i] = Nullifier(); + nullifiers[i].verification_id <== verification_id; + nullifiers[i].nullifier <== nullifier[i]; + nullifiers[i].secret <== secret[i]; + } +} +``` diff --git a/snippets/code-snippets/zk/nullifier/program.mdx b/snippets/code-snippets/zk/nullifier/program.mdx new file mode 100644 index 0000000..6163d8c --- /dev/null +++ b/snippets/code-snippets/zk/nullifier/program.mdx @@ -0,0 +1,248 @@ +```rust expandable +#![allow(unexpected_cfgs)] +#![allow(deprecated)] + +use anchor_lang::prelude::*; +use borsh::{BorshDeserialize, BorshSerialize}; +use groth16_solana::groth16::Groth16Verifier; +use light_sdk::account::LightAccount; +use light_sdk::cpi::v2::CpiAccounts; +use light_sdk::{ + address::{v2::derive_address, NewAddressParamsAssignedPacked}, + cpi::{v2::LightSystemProgramCpi, InvokeLightSystemProgram, LightCpiInstruction}, + derive_light_cpi_signer, + instruction::{CompressedProof, PackedAddressTreeInfo, ValidityProof}, + LightDiscriminator, +}; +use light_sdk_types::CpiSigner; + +declare_id!("Bw8aty8LJY5Kg2b6djghjWGwt6cBc1tVQUoreUehvVq4"); + +pub const LIGHT_CPI_SIGNER: CpiSigner = + derive_light_cpi_signer!("Bw8aty8LJY5Kg2b6djghjWGwt6cBc1tVQUoreUehvVq4"); + +pub const NULLIFIER_PREFIX: &[u8] = b"nullifier"; + +// Customize nullifiers per tx, e.g. 1 (single) or 4 (batch) +pub const BATCH_SIZE: usize = 4; + +pub mod nullifier_1; +pub mod nullifier_batch_4; + +#[program] +pub mod zk_nullifier { + use groth16_solana::decompression::{decompress_g1, decompress_g2}; + + use super::*; + + /// Creates 1 nullifier + pub fn create_nullifier<'info>( + ctx: Context<'_, '_, '_, 'info, CreateNullifierAccounts<'info>>, + proof: ValidityProof, + address_tree_info: PackedAddressTreeInfo, + output_state_tree_index: u8, + system_accounts_offset: u8, + zk_proof: CompressedProof, + verification_id: [u8; 32], + nullifier: [u8; 32], + ) -> Result<()> { + let light_cpi_accounts = CpiAccounts::new( + ctx.accounts.signer.as_ref(), + &ctx.remaining_accounts[system_accounts_offset as usize..], + crate::LIGHT_CPI_SIGNER, + ); + + let address_tree_pubkey = address_tree_info + .get_tree_pubkey(&light_cpi_accounts) + .map_err(|_| ErrorCode::AccountNotEnoughKeys)?; + + if address_tree_pubkey.to_bytes() != light_sdk::constants::ADDRESS_TREE_V2 { + msg!("Invalid address tree"); + return Err(ProgramError::InvalidAccountData.into()); + } + + let public_inputs: [[u8; 32]; 2] = [verification_id, nullifier]; + + msg!("Decompressing proof_a..."); + let proof_a = decompress_g1(&zk_proof.a).map_err(|e| { + msg!("decompress_g1 failed for proof_a"); + let code: u32 = e.into(); + Error::from(ProgramError::Custom(code)) + })?; + + msg!("Decompressing proof_b..."); + let proof_b = decompress_g2(&zk_proof.b).map_err(|e| { + msg!("decompress_g2 failed for proof_b"); + let code: u32 = e.into(); + Error::from(ProgramError::Custom(code)) + })?; + + msg!("Decompressing proof_c..."); + let proof_c = decompress_g1(&zk_proof.c).map_err(|e| { + msg!("decompress_g1 failed for proof_c"); + let code: u32 = e.into(); + Error::from(ProgramError::Custom(code)) + })?; + + msg!("Creating verifier..."); + let mut verifier = Groth16Verifier::new( + &proof_a, + &proof_b, + &proof_c, + &public_inputs, + &crate::nullifier_1::VERIFYINGKEY, + ) + .map_err(|e| { + msg!("Groth16Verifier::new failed"); + let code: u32 = e.into(); + Error::from(ProgramError::Custom(code)) + })?; + + msg!("Verifying proof..."); + verifier.verify().map_err(|e| { + msg!("verifier.verify() failed"); + let code: u32 = e.into(); + Error::from(ProgramError::Custom(code)) + })?; + msg!("Proof verified!"); + + let (address, address_seed) = derive_address( + &[ + NULLIFIER_PREFIX, + nullifier.as_slice(), + verification_id.as_slice(), + ], + &address_tree_pubkey, + &crate::ID, + ); + + let nullifier_account = LightAccount::::new_init( + &crate::ID, + Some(address), + output_state_tree_index, + ); + + LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, proof) + .with_light_account(nullifier_account)? + .with_new_addresses(&[address_tree_info.into_new_address_params_assigned_packed(address_seed, Some(0))]) + .invoke(light_cpi_accounts)?; + + Ok(()) + } + + /// Creates 4 nullifiers with single proof + pub fn create_batch_nullifier<'info>( + ctx: Context<'_, '_, '_, 'info, CreateNullifierAccounts<'info>>, + proof: ValidityProof, + address_tree_infos: [PackedAddressTreeInfo; BATCH_SIZE], + output_state_tree_index: u8, + system_accounts_offset: u8, + zk_proof: CompressedProof, + verification_id: [u8; 32], + nullifiers: [[u8; 32]; BATCH_SIZE], + ) -> Result<()> { + let light_cpi_accounts = CpiAccounts::new( + ctx.accounts.signer.as_ref(), + &ctx.remaining_accounts[system_accounts_offset as usize..], + crate::LIGHT_CPI_SIGNER, + ); + + let address_tree_pubkey = address_tree_infos[0] + .get_tree_pubkey(&light_cpi_accounts) + .map_err(|_| ErrorCode::AccountNotEnoughKeys)?; + + if address_tree_pubkey.to_bytes() != light_sdk::constants::ADDRESS_TREE_V2 { + msg!("Invalid address tree"); + return Err(ProgramError::InvalidAccountData.into()); + } + + // 5 public inputs: verification_id + 4 nullifiers + let public_inputs: [[u8; 32]; 5] = [ + verification_id, + nullifiers[0], + nullifiers[1], + nullifiers[2], + nullifiers[3], + ]; + + let proof_a = decompress_g1(&zk_proof.a).map_err(|e| { + let code: u32 = e.into(); + Error::from(ProgramError::Custom(code)) + })?; + + let proof_b = decompress_g2(&zk_proof.b).map_err(|e| { + let code: u32 = e.into(); + Error::from(ProgramError::Custom(code)) + })?; + + let proof_c = decompress_g1(&zk_proof.c).map_err(|e| { + let code: u32 = e.into(); + Error::from(ProgramError::Custom(code)) + })?; + + let mut verifier = Groth16Verifier::new( + &proof_a, + &proof_b, + &proof_c, + &public_inputs, + &crate::nullifier_batch_4::VERIFYINGKEY, + ) + .map_err(|e| { + let code: u32 = e.into(); + Error::from(ProgramError::Custom(code)) + })?; + + verifier.verify().map_err(|e| { + let code: u32 = e.into(); + Error::from(ProgramError::Custom(code)) + })?; + + // Create 4 nullifier accounts + let mut cpi_builder = LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, proof); + let mut new_address_params: Vec = Vec::with_capacity(BATCH_SIZE); + + for i in 0..BATCH_SIZE { + let (address, address_seed) = derive_address( + &[ + NULLIFIER_PREFIX, + nullifiers[i].as_slice(), + verification_id.as_slice(), + ], + &address_tree_pubkey, + &crate::ID, + ); + + let nullifier_account = LightAccount::::new_init( + &crate::ID, + Some(address), + output_state_tree_index, + ); + + cpi_builder = cpi_builder.with_light_account(nullifier_account)?; + new_address_params + .push(address_tree_infos[i].into_new_address_params_assigned_packed(address_seed, Some(i as u8))); + } + + cpi_builder + .with_new_addresses(&new_address_params) + .invoke(light_cpi_accounts)?; + + Ok(()) + } +} + +#[derive(Accounts)] +pub struct CreateNullifierAccounts<'info> { + #[account(mut)] + pub signer: Signer<'info>, +} + +#[derive(Clone, Debug, Default, BorshSerialize, BorshDeserialize, LightDiscriminator)] +pub struct NullifierAccount {} + +#[error_code] +pub enum ErrorCode { + #[msg("Not enough keys in remaining accounts")] + AccountNotEnoughKeys, +} +``` diff --git a/snippets/code-snippets/zk/nullifier/rust-client.mdx b/snippets/code-snippets/zk/nullifier/rust-client.mdx new file mode 100644 index 0000000..0df5353 --- /dev/null +++ b/snippets/code-snippets/zk/nullifier/rust-client.mdx @@ -0,0 +1,441 @@ +```rust expandable +use anchor_lang::{InstructionData, ToAccountMetas}; +use circom_prover::{prover::ProofLib, witness::WitnessFn, CircomProver}; +use groth16_solana::proof_parser::circom_prover::{convert_proof, convert_proof_to_compressed}; +use light_hasher::{Hasher, Poseidon}; +use light_program_test::{ + program_test::LightProgramTest, utils::simulate_cu, AddressWithTree, Indexer, ProgramTestConfig, + Rpc, RpcError, +}; +use light_sdk::{ + address::v2::derive_address, + instruction::{PackedAccounts, SystemAccountMetaConfig}, +}; +use num_bigint::BigUint; +use solana_sdk::{ + instruction::Instruction, + pubkey::Pubkey, + signature::{Keypair, Signer}, +}; +use std::collections::HashMap; +use zk_nullifier::{BATCH_SIZE, NULLIFIER_PREFIX}; + +#[link(name = "circuit_single", kind = "static")] +extern "C" {} + +#[link(name = "circuit_batch", kind = "static")] +extern "C" {} + +rust_witness::witness!(nullifier1); +rust_witness::witness!(nullifier4); + +// ============================================================================ +// Shared helpers +// ============================================================================ + +fn generate_random_secret() -> [u8; 32] { + let random_keypair = Keypair::new(); + let mut secret = [0u8; 32]; + secret[1..32].copy_from_slice(&random_keypair.to_bytes()[0..31]); + secret +} + +fn compute_nullifier(verification_id: &[u8; 32], secret: &[u8; 32]) -> [u8; 32] { + Poseidon::hashv(&[verification_id, secret]).unwrap() +} + +fn compress_proof( + proof: &circom_prover::prover::circom::Proof, +) -> light_compressed_account::instruction_data::compressed_proof::CompressedProof { + let (proof_a_uncompressed, proof_b_uncompressed, proof_c_uncompressed) = + convert_proof(proof).expect("Failed to convert proof"); + + let (proof_a, proof_b, proof_c) = convert_proof_to_compressed( + &proof_a_uncompressed, + &proof_b_uncompressed, + &proof_c_uncompressed, + ) + .expect("Failed to compress proof"); + + light_compressed_account::instruction_data::compressed_proof::CompressedProof { + a: proof_a, + b: proof_b, + c: proof_c, + } +} + +// ============================================================================ +// Single nullifier test +// ============================================================================ + +#[tokio::test] +async fn test_create_nullifier() { + let config = ProgramTestConfig::new(true, Some(vec![("zk_nullifier", zk_nullifier::ID)])); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let address_tree_info = rpc.get_address_tree_v2(); + + let secret = generate_random_secret(); + let verification_id = Pubkey::new_unique().to_bytes(); + let nullifier = compute_nullifier(&verification_id, &secret); + + let (nullifier_address, _) = derive_address( + &[ + NULLIFIER_PREFIX, + nullifier.as_slice(), + verification_id.as_slice(), + ], + &address_tree_info.tree, + &zk_nullifier::ID, + ); + + let instruction = build_create_nullifier_instruction( + &mut rpc, + &payer, + &nullifier_address, + address_tree_info.clone(), + &verification_id, + &nullifier, + &secret, + ) + .await + .unwrap(); + + let cu = simulate_cu(&mut rpc, &payer, &instruction).await; + println!("=== Single nullifier CU: {} ===", cu); + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + let nullifier_accounts = rpc + .get_compressed_accounts_by_owner(&zk_nullifier::ID, None, None) + .await + .unwrap(); + assert_eq!(nullifier_accounts.value.items.len(), 1); + + // Duplicate should fail + let dup_instruction = build_create_nullifier_instruction( + &mut rpc, + &payer, + &nullifier_address, + address_tree_info, + &verification_id, + &nullifier, + &secret, + ) + .await + .unwrap(); + + let result = rpc + .create_and_send_transaction(&[dup_instruction], &payer.pubkey(), &[&payer]) + .await; + assert!(result.is_err()); +} + +async fn build_create_nullifier_instruction( + rpc: &mut R, + payer: &Keypair, + address: &[u8; 32], + address_tree_info: light_client::indexer::TreeInfo, + verification_id: &[u8; 32], + nullifier: &[u8; 32], + secret: &[u8; 32], +) -> Result +where + R: Rpc + Indexer, +{ + let mut remaining_accounts = PackedAccounts::default(); + remaining_accounts.add_pre_accounts_signer(payer.pubkey()); + let config = SystemAccountMetaConfig::new(zk_nullifier::ID); + remaining_accounts.add_system_accounts_v2(config)?; + + let rpc_result = rpc + .get_validity_proof( + vec![], + vec![AddressWithTree { + address: *address, + tree: address_tree_info.tree, + }], + None, + ) + .await? + .value; + + let packed_address_tree_accounts = rpc_result + .pack_tree_infos(&mut remaining_accounts) + .address_trees; + + let output_state_tree_index = rpc + .get_random_state_tree_info()? + .pack_output_tree_index(&mut remaining_accounts)?; + + let zk_proof = generate_single_zk_proof(verification_id, nullifier, secret); + + let (remaining_accounts_metas, system_accounts_offset, _) = remaining_accounts.to_account_metas(); + + let instruction_data = zk_nullifier::instruction::CreateNullifier { + proof: rpc_result.proof, + address_tree_info: packed_address_tree_accounts[0], + output_state_tree_index, + system_accounts_offset: system_accounts_offset as u8, + zk_proof, + verification_id: *verification_id, + nullifier: *nullifier, + }; + + let accounts = zk_nullifier::accounts::CreateNullifierAccounts { + signer: payer.pubkey(), + }; + + Ok(Instruction { + program_id: zk_nullifier::ID, + accounts: [ + accounts.to_account_metas(None), + remaining_accounts_metas, + ] + .concat(), + data: instruction_data.data(), + }) +} + +fn generate_single_zk_proof( + verification_id: &[u8; 32], + nullifier: &[u8; 32], + secret: &[u8; 32], +) -> light_compressed_account::instruction_data::compressed_proof::CompressedProof { + let manifest_dir = env!("CARGO_MANIFEST_DIR"); + let zkey_path = format!("{}/../../build/nullifier_1_final.zkey", manifest_dir); + + let mut proof_inputs = HashMap::new(); + proof_inputs.insert( + "verification_id".to_string(), + vec![BigUint::from_bytes_be(verification_id).to_string()], + ); + proof_inputs.insert( + "nullifier".to_string(), + vec![BigUint::from_bytes_be(nullifier).to_string()], + ); + proof_inputs.insert( + "secret".to_string(), + vec![BigUint::from_bytes_be(secret).to_string()], + ); + + let circuit_inputs = serde_json::to_string(&proof_inputs).unwrap(); + let proof = CircomProver::prove( + ProofLib::Arkworks, + WitnessFn::RustWitness(nullifier1_witness), + circuit_inputs, + zkey_path.clone(), + ) + .expect("Proof generation failed"); + + let is_valid = CircomProver::verify(ProofLib::Arkworks, proof.clone(), zkey_path) + .expect("Proof verification failed"); + assert!(is_valid); + + compress_proof(&proof.proof) +} + +// ============================================================================ +// Batch nullifier test +// ============================================================================ + +#[tokio::test] +async fn test_create_batch_nullifier() { + let config = ProgramTestConfig::new(true, Some(vec![("zk_nullifier", zk_nullifier::ID)])); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let address_tree_info = rpc.get_address_tree_v2(); + + let secrets: [[u8; 32]; BATCH_SIZE] = [ + generate_random_secret(), + generate_random_secret(), + generate_random_secret(), + generate_random_secret(), + ]; + let verification_id = Pubkey::new_unique().to_bytes(); + let nullifiers: [[u8; 32]; BATCH_SIZE] = [ + compute_nullifier(&verification_id, &secrets[0]), + compute_nullifier(&verification_id, &secrets[1]), + compute_nullifier(&verification_id, &secrets[2]), + compute_nullifier(&verification_id, &secrets[3]), + ]; + + let mut addresses = Vec::with_capacity(BATCH_SIZE); + for i in 0..BATCH_SIZE { + let (addr, _) = derive_address( + &[ + NULLIFIER_PREFIX, + nullifiers[i].as_slice(), + verification_id.as_slice(), + ], + &address_tree_info.tree, + &zk_nullifier::ID, + ); + addresses.push(addr); + } + + let instruction = build_create_batch_nullifier_instruction( + &mut rpc, + &payer, + &addresses, + address_tree_info.clone(), + &verification_id, + &nullifiers, + &secrets, + ) + .await + .unwrap(); + + let cu = simulate_cu(&mut rpc, &payer, &instruction).await; + println!("=== Batch (4 nullifiers) CU: {} ===", cu); + println!("=== CU per nullifier (batch): {} ===", cu / 4); + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + let nullifier_accounts = rpc + .get_compressed_accounts_by_owner(&zk_nullifier::ID, None, None) + .await + .unwrap(); + assert_eq!(nullifier_accounts.value.items.len(), BATCH_SIZE); + + // Duplicate batch should fail + let dup_instruction = build_create_batch_nullifier_instruction( + &mut rpc, + &payer, + &addresses, + address_tree_info, + &verification_id, + &nullifiers, + &secrets, + ) + .await + .unwrap(); + + let result = rpc + .create_and_send_transaction(&[dup_instruction], &payer.pubkey(), &[&payer]) + .await; + assert!(result.is_err()); +} + +async fn build_create_batch_nullifier_instruction( + rpc: &mut R, + payer: &Keypair, + addresses: &[[u8; 32]], + address_tree_info: light_client::indexer::TreeInfo, + verification_id: &[u8; 32], + nullifiers: &[[u8; 32]; BATCH_SIZE], + secrets: &[[u8; 32]; BATCH_SIZE], +) -> Result +where + R: Rpc + Indexer, +{ + let mut remaining_accounts = PackedAccounts::default(); + remaining_accounts.add_pre_accounts_signer(payer.pubkey()); + let config = SystemAccountMetaConfig::new(zk_nullifier::ID); + remaining_accounts.add_system_accounts_v2(config)?; + + let address_with_trees: Vec = addresses + .iter() + .map(|addr| AddressWithTree { + address: *addr, + tree: address_tree_info.tree, + }) + .collect(); + + let rpc_result = rpc + .get_validity_proof(vec![], address_with_trees, None) + .await? + .value; + + let packed_address_tree_accounts = rpc_result + .pack_tree_infos(&mut remaining_accounts) + .address_trees; + + let output_state_tree_index = rpc + .get_random_state_tree_info()? + .pack_output_tree_index(&mut remaining_accounts)?; + + let zk_proof = generate_batch_zk_proof(verification_id, nullifiers, secrets); + + let address_tree_infos: [_; BATCH_SIZE] = [ + packed_address_tree_accounts[0], + packed_address_tree_accounts[1], + packed_address_tree_accounts[2], + packed_address_tree_accounts[3], + ]; + + let (remaining_accounts_metas, system_accounts_offset, _) = remaining_accounts.to_account_metas(); + + let instruction_data = zk_nullifier::instruction::CreateBatchNullifier { + proof: rpc_result.proof, + address_tree_infos, + output_state_tree_index, + system_accounts_offset: system_accounts_offset as u8, + zk_proof, + verification_id: *verification_id, + nullifiers: *nullifiers, + }; + + let accounts = zk_nullifier::accounts::CreateNullifierAccounts { + signer: payer.pubkey(), + }; + + Ok(Instruction { + program_id: zk_nullifier::ID, + accounts: [ + accounts.to_account_metas(None), + remaining_accounts_metas, + ] + .concat(), + data: instruction_data.data(), + }) +} + +fn generate_batch_zk_proof( + verification_id: &[u8; 32], + nullifiers: &[[u8; 32]; BATCH_SIZE], + secrets: &[[u8; 32]; BATCH_SIZE], +) -> light_compressed_account::instruction_data::compressed_proof::CompressedProof { + let manifest_dir = env!("CARGO_MANIFEST_DIR"); + let zkey_path = format!("{}/../../build/nullifier_4_final.zkey", manifest_dir); + + let mut proof_inputs = HashMap::new(); + proof_inputs.insert( + "verification_id".to_string(), + vec![BigUint::from_bytes_be(verification_id).to_string()], + ); + + let nullifier_strings: Vec = nullifiers + .iter() + .map(|n| BigUint::from_bytes_be(n).to_string()) + .collect(); + proof_inputs.insert("nullifier".to_string(), nullifier_strings); + + let secret_strings: Vec = secrets + .iter() + .map(|s| BigUint::from_bytes_be(s).to_string()) + .collect(); + proof_inputs.insert("secret".to_string(), secret_strings); + + let circuit_inputs = serde_json::to_string(&proof_inputs).unwrap(); + let proof = CircomProver::prove( + ProofLib::Arkworks, + WitnessFn::RustWitness(nullifier4_witness), + circuit_inputs, + zkey_path.clone(), + ) + .expect("Proof generation failed"); + + let is_valid = CircomProver::verify(ProofLib::Arkworks, proof.clone(), zkey_path) + .expect("Proof verification failed"); + assert!(is_valid); + + compress_proof(&proof.proof) +} +``` diff --git a/snippets/code-snippets/zk/nullifier/typescript-client.mdx b/snippets/code-snippets/zk/nullifier/typescript-client.mdx new file mode 100644 index 0000000..e066380 --- /dev/null +++ b/snippets/code-snippets/zk/nullifier/typescript-client.mdx @@ -0,0 +1,312 @@ +```typescript expandable +import { web3, Program, AnchorProvider, setProvider } from "@coral-xyz/anchor"; +import { + bn, + createRpc, + deriveAddressSeedV2, + deriveAddressV2, + batchAddressTree, + PackedAccounts, + Rpc, + sleep, + SystemAccountMetaConfig, + defaultTestStateTreeAccounts, + featureFlags, + VERSION, + confirmTx, +} from "@lightprotocol/stateless.js"; +import { buildPoseidonOpt } from "circomlibjs"; +import * as snarkjs from "snarkjs"; +import * as assert from "assert"; +import * as path from "path"; +import * as fs from "fs"; + +import { + parseProofToCompressed, + bigintToBytes32, + toFieldString, + generateFieldElement, +} from "./utils/proof-helpers"; + +// Force V2 mode +(featureFlags as any).version = VERSION.V2; + +// Load IDL +const IDL = JSON.parse( + fs.readFileSync(path.join(process.cwd(), "target/idl/zk_nullifier.json"), "utf8") +); + +const PROGRAM_ID = new web3.PublicKey(IDL.address); +const NULLIFIER_PREFIX = Buffer.from("nullifier"); + +// Paths to circuit artifacts +const BUILD_DIR = path.join(process.cwd(), "build"); +const WASM_PATH_SINGLE = path.join(BUILD_DIR, "nullifier_1_js/nullifier_1.wasm"); +const ZKEY_PATH_SINGLE = path.join(BUILD_DIR, "nullifier_1_final.zkey"); +const VKEY_PATH_SINGLE = path.join(BUILD_DIR, "nullifier_1_verification_key.json"); +const WASM_PATH_BATCH = path.join(BUILD_DIR, "nullifier_4_js/nullifier_4.wasm"); +const ZKEY_PATH_BATCH = path.join(BUILD_DIR, "nullifier_4_final.zkey"); + +describe("zk-nullifier", () => { + let rpc: Rpc; + let signer: web3.Keypair; + let poseidon: any; + let program: Program; + + before(async () => { + rpc = createRpc( + "http://127.0.0.1:8899", + "http://127.0.0.1:8784", + "http://127.0.0.1:3001", + { commitment: "confirmed" } + ); + + signer = web3.Keypair.generate(); + await rpc.requestAirdrop(signer.publicKey, web3.LAMPORTS_PER_SOL); + await sleep(2000); + + poseidon = await buildPoseidonOpt(); + + // Setup Anchor provider and program + const connection = new web3.Connection("http://127.0.0.1:8899", "confirmed"); + const wallet = { + publicKey: signer.publicKey, + signTransaction: async (tx: web3.Transaction) => { + tx.sign(signer); + return tx; + }, + signAllTransactions: async (txs: web3.Transaction[]) => { + txs.forEach((tx) => tx.sign(signer)); + return txs; + }, + }; + const provider = new AnchorProvider(connection, wallet as any, { commitment: "confirmed" }); + setProvider(provider); + program = new Program(IDL, provider); + }); + + after(async () => { + // Terminate snarkjs curve worker to allow clean exit + // @ts-ignore + if (globalThis.curve_bn128) { + // @ts-ignore + await globalThis.curve_bn128.terminate(); + } + }); + + /** Compute nullifier = Poseidon(verification_id, secret) */ + function computeNullifier(verificationId: Uint8Array, secret: Uint8Array): Uint8Array { + const hash = poseidon([toFieldString(verificationId), toFieldString(secret)].map(BigInt)); + return bigintToBytes32(poseidon.F.toObject(hash)); + } + + /** Generate Groth16 proof for single nullifier */ + async function generateProof( + verificationId: Uint8Array, + nullifier: Uint8Array, + secret: Uint8Array + ): Promise<{ a: number[]; b: number[]; c: number[] }> { + const inputs = { + verification_id: toFieldString(verificationId), + nullifier: toFieldString(nullifier), + secret: toFieldString(secret), + }; + + const { proof, publicSignals } = await snarkjs.groth16.fullProve(inputs, WASM_PATH_SINGLE, ZKEY_PATH_SINGLE); + + // Verify locally with snarkjs before converting + const vkey = JSON.parse(fs.readFileSync(VKEY_PATH_SINGLE, "utf8")); + const isValid = await snarkjs.groth16.verify(vkey, publicSignals, proof); + console.log("Local snarkjs verification:", isValid); + console.log("Public signals:", publicSignals); + + // Use prover.js logic for proof conversion + const compressed = parseProofToCompressed(proof); + + console.log("Compressed proof a (first 8 bytes):", compressed.a.slice(0, 8)); + console.log("Compressed proof b (first 8 bytes):", compressed.b.slice(0, 8)); + console.log("Compressed proof c (first 8 bytes):", compressed.c.slice(0, 8)); + + return compressed; + } + + /** Generate Groth16 proof for batch (4) nullifiers */ + async function generateBatchProof( + verificationId: Uint8Array, + nullifiers: Uint8Array[], + secrets: Uint8Array[] + ): Promise<{ a: number[]; b: number[]; c: number[] }> { + const inputs = { + verification_id: toFieldString(verificationId), + nullifier: nullifiers.map(toFieldString), + secret: secrets.map(toFieldString), + }; + + const { proof } = await snarkjs.groth16.fullProve(inputs, WASM_PATH_BATCH, ZKEY_PATH_BATCH); + return parseProofToCompressed(proof); + } + + /** Build create_nullifier instruction using Anchor */ + async function buildCreateNullifierInstruction( + verificationId: Uint8Array, + nullifier: Uint8Array, + secret: Uint8Array + ): Promise { + const addressTree = new web3.PublicKey(batchAddressTree); + const outputStateTree = defaultTestStateTreeAccounts().merkleTree; + + const seed = deriveAddressSeedV2([NULLIFIER_PREFIX, nullifier, verificationId]); + const address = deriveAddressV2(seed, addressTree, PROGRAM_ID); + + const proofResult = await rpc.getValidityProofV0( + [], + [{ tree: addressTree, queue: addressTree, address: bn(address.toBytes()) }] + ); + + // Use V2 accounts layout (matches on-chain CpiAccounts::new from light_sdk::cpi::v2) + const remainingAccounts = new PackedAccounts(); + remainingAccounts.addPreAccountsSigner(signer.publicKey); + remainingAccounts.addSystemAccountsV2(SystemAccountMetaConfig.new(PROGRAM_ID)); + + const addressMerkleTreeIndex = remainingAccounts.insertOrGet(addressTree); + const outputStateTreeIndex = remainingAccounts.insertOrGet(outputStateTree); + + const zkProof = await generateProof(verificationId, nullifier, secret); + + // Get system_accounts_offset from packed accounts + const { remainingAccounts: accountMetas, systemStart } = remainingAccounts.toAccountMetas(); + + // Use Anchor to build instruction + // ValidityProof is a struct with an unnamed Option field + // Anchor JS client uses index-based access for unnamed tuple/option fields + const proof = { + 0: proofResult.compressedProof, + }; + + const ix = await program.methods + .createNullifier( + // proof (ValidityProof = struct with Option) + proof, + // address_tree_info (PackedAddressTreeInfo) + { + addressMerkleTreePubkeyIndex: addressMerkleTreeIndex, + addressQueuePubkeyIndex: addressMerkleTreeIndex, + rootIndex: proofResult.rootIndices[0], + }, + // output_state_tree_index + outputStateTreeIndex, + // system_accounts_offset + systemStart, + // zk_proof (CompressedProof) + { + a: zkProof.a, + b: zkProof.b, + c: zkProof.c, + }, + // verification_id + Array.from(verificationId), + // nullifier + Array.from(nullifier) + ) + .accounts({ + signer: signer.publicKey, + }) + .remainingAccounts(accountMetas) + .instruction(); + + return ix; + } + + describe("Single nullifier", () => { + it("should create a nullifier with valid ZK proof", async () => { + // Use generateFieldElement for verificationId to ensure it's in BN254 field + const verificationId = generateFieldElement(); + const secret = generateFieldElement(); + const nullifier = computeNullifier(verificationId, secret); + + console.log("Verification ID:", Buffer.from(verificationId).toString("hex").slice(0, 16) + "..."); + console.log("Nullifier:", Buffer.from(nullifier).toString("hex").slice(0, 16) + "..."); + + // Debug: Check if values are within BN254 field + const BN254_FR = BigInt('21888242871839275222246405745257275088548364400416034343698204186575808495617'); + const verIdBigInt = BigInt("0x" + Buffer.from(verificationId).toString("hex")); + const nullifierBigInt = BigInt("0x" + Buffer.from(nullifier).toString("hex")); + console.log("verificationId < Fr:", verIdBigInt < BN254_FR, "value:", verIdBigInt.toString().slice(0, 20) + "..."); + console.log("nullifier < Fr:", nullifierBigInt < BN254_FR, "value:", nullifierBigInt.toString().slice(0, 20) + "..."); + + const ix = await buildCreateNullifierInstruction(verificationId, nullifier, secret); + const computeIx = web3.ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 }); + + const tx = new web3.Transaction().add(computeIx, ix); + tx.recentBlockhash = (await rpc.getLatestBlockhash()).blockhash; + tx.feePayer = signer.publicKey; + tx.sign(signer); + + const sig = await rpc.sendTransaction(tx, [signer]); + await confirmTx(rpc, sig); + + console.log("Transaction signature:", sig); + + const slot = await rpc.getSlot(); + await rpc.confirmTransactionIndexed(slot); + + const accounts = await rpc.getCompressedAccountsByOwner(PROGRAM_ID); + assert.ok(accounts.items.length > 0, "Nullifier account should be created"); + console.log("Created nullifier accounts:", accounts.items.length); + }); + + it("should reject duplicate nullifier", async () => { + // Use generateFieldElement for verificationId to ensure it's in BN254 field + const verificationId = generateFieldElement(); + const secret = generateFieldElement(); + const nullifier = computeNullifier(verificationId, secret); + + const ix1 = await buildCreateNullifierInstruction(verificationId, nullifier, secret); + const computeIx = web3.ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 }); + + const tx1 = new web3.Transaction().add(computeIx, ix1); + tx1.recentBlockhash = (await rpc.getLatestBlockhash()).blockhash; + tx1.feePayer = signer.publicKey; + tx1.sign(signer); + + await rpc.sendTransaction(tx1, [signer]); + await sleep(2000); + + // Attempt to create duplicate - should fail when getting validity proof + // because the address already exists in the tree + try { + await buildCreateNullifierInstruction(verificationId, nullifier, secret); + assert.fail("Should have rejected duplicate nullifier"); + } catch (err: any) { + // The error should indicate the address already exists + assert.ok( + err.message.includes("already exists"), + `Expected 'already exists' error, got: ${err.message}` + ); + console.log("Duplicate correctly rejected:", err.message); + } + }); + }); + + describe("Batch nullifier (4x)", () => { + it("should create 4 nullifiers with single proof", async () => { + // Use generateFieldElement for verificationId to ensure it's in BN254 field + const verificationId = generateFieldElement(); + const secrets = Array.from({ length: 4 }, generateFieldElement); + const nullifiers = secrets.map((s) => computeNullifier(verificationId, s)); + + console.log("Creating batch of 4 nullifiers..."); + console.log("Verification ID:", Buffer.from(verificationId).toString("hex").slice(0, 16) + "..."); + + const zkProof = await generateBatchProof(verificationId, nullifiers, secrets); + console.log("Batch proof generated"); + + assert.ok(zkProof.a.length === 32, "Proof A should be 32 bytes"); + assert.ok(zkProof.b.length === 64, "Proof B should be 64 bytes"); + assert.ok(zkProof.c.length === 32, "Proof C should be 32 bytes"); + + console.log("Batch proof verified locally"); + }); + }); +}); +``` diff --git a/snippets/mermaid/nullifier-flow.mdx b/snippets/mermaid/nullifier-flow.mdx new file mode 100644 index 0000000..fdf228b --- /dev/null +++ b/snippets/mermaid/nullifier-flow.mdx @@ -0,0 +1,18 @@ +```mermaid +sequenceDiagram + participant U as User + participant C as Client + participant P as Program + participant L as Light Protocol + + U->>C: secret + verification_id + C->>C: nullifier = Poseidon(vid, secret) + C->>C: proof = Groth16.prove(...) + C->>P: create_nullifier(proof, vid, nullifier) + P->>P: Groth16.verify(proof) + P->>L: derive_address(nullifier, vid) + L-->>P: address + P->>L: create_account(address) + L-->>P: success/fail + P-->>U: tx result +``` diff --git a/snippets/overview-tables/zk-examples-table.mdx b/snippets/overview-tables/zk-examples-table.mdx new file mode 100644 index 0000000..c599b00 --- /dev/null +++ b/snippets/overview-tables/zk-examples-table.mdx @@ -0,0 +1,4 @@ +| | Description | +|:--------|:------------| +| [ZK-ID](https://github.com/Lightprotocol/program-examples/tree/main/zk/zk-id) | Identity verification using Groth16 proofs. Issuers create credentials; users prove ownership without revealing the credential. | +| [Nullifier](https://github.com/Lightprotocol/program-examples/tree/main/zk/zk-nullifier) | Simple Program to Create Nullifiers. Requires no custom circuit. | diff --git a/welcome.mdx b/welcome.mdx index 9f0a4d6..16f17ef 100644 --- a/welcome.mdx +++ b/welcome.mdx @@ -24,7 +24,7 @@ import WelcomePageInstall from "/snippets/setup/welcome-page-install.mdx"; For App State. diff --git a/zk/compressed-account-zk.mdx b/zk/compressed-account-zk.mdx new file mode 100644 index 0000000..b26dd83 --- /dev/null +++ b/zk/compressed-account-zk.mdx @@ -0,0 +1,507 @@ +--- +title: Compressed Accounts for ZK Applications +description: ZK applications use Poseidon Hashes instead of SHA-256 to store state in compressed accounts compatible with zero-knowledge circuits. +keywords: ["compressed accounts zk", "poseidon hashing solana", "groth16 solana", "privacy solana"] +--- + +import ZkAppPrompt from "/snippets/ai-prompts/zk-app.mdx"; +import MerkleProofCircuit from '/snippets/code-snippets/zk/merkle-proof/circuit.mdx'; +import MerkleProofProgram from '/snippets/code-snippets/zk/merkle-proof/program.mdx'; +import MerkleProofRustClient from '/snippets/code-snippets/zk/merkle-proof/rust-client.mdx'; +import MerkleProofTypescriptClient from '/snippets/code-snippets/zk/merkle-proof/typescript-client.mdx'; + +* Compressed and regular Solana accounts share the same functionality and are fully composable. +* The account state is hashed with Poseidon and stored as a commitment in the state Merkle tree. +* RPCs that support ZK Compression (Helius, Triton) index state changes. + + +Each tab below includes a full code example. See the full implementation at [program-examples/zk/zk-merkle-proof](https://github.com/Lightprotocol/program-examples/tree/main/zk/zk-merkle-proof). + + +## Implementation guide + + + + + + +### Dependencies + +Add dependencies to your program. + +```toml +[dependencies] +anchor-lang = "0.31.1" +borsh = "0.10.4" +light-sdk = { version = "0.17.1", features = ["anchor", "poseidon", "merkle-tree", "v2"] } +light-hasher = "5.0.0" +light-sdk-types = { version = "0.17.1", features = ["v2"] } +``` + +* `light-sdk` with `poseidon` feature enables ZK-friendly hashing. +* `light-hasher` provides Poseidon hash functions. + + + + + +### Constants + +Set program address, CPI signer, and include the verifying key from your circuit setup. + +```rust +declare_id!("MPzkYomvQc4VQPwMr6bFduyWRQZVCh5CofgDC4dFqJp"); + +pub const LIGHT_CPI_SIGNER: CpiSigner = + derive_light_cpi_signer!("MPzkYomvQc4VQPwMr6bFduyWRQZVCh5CofgDC4dFqJp"); + +pub const ZK_ACCOUNT: &[u8] = b"zk_account"; + +// Generated from circom trusted setup (.zkey file) +pub mod verifying_key; +``` + + + + + +### Compressed account + + +```rust +#[derive(Clone, Debug, Default, BorshSerialize, BorshDeserialize, LightDiscriminator, LightHasher)] +pub struct ZkAccount { + pub data_hash: ZkDataHash, +} +``` + +**ZK-specific differences from regular compressed accounts:** + +* Derive `LightHasher` for Poseidon hashing (instead of default SHA-256). + +**Custom types need `ToByteArray`:** + +```rust +#[derive(Clone, Debug, Default, BorshSerialize, BorshDeserialize)] +pub struct ZkDataHash(pub [u8; 32]); + +impl ToByteArray for ZkDataHash { + const NUM_FIELDS: usize = 1; + fn to_byte_array(&self) -> std::result::Result<[u8; 32], HasherError> { + Ok(self.0) + } +} +``` + + + + + +### Instruction data + +Define the instruction parameters. + +```rust +pub fn create_account<'info>( + ctx: Context<'_, '_, '_, 'info, CreateAccountAccounts<'info>>, + proof: ValidityProof, + address_tree_info: PackedAddressTreeInfo, + output_state_tree_index: u8, + data_hash: [u8; 32], +) -> Result<()> +``` + +1. `proof`: Proves the address does not exist yet. +2. `address_tree_info`: References the address tree for deriving the address. +3. `output_state_tree_index`: References the state tree for storing the account hash. +4. `data_hash`: The Poseidon hash of the data to store. + + + + + +### Derive address + +Derive the address as a persistent unique identifier. + +```rust +let address_tree_pubkey = address_tree_info + .get_tree_pubkey(&light_cpi_accounts) + .map_err(|_| ProgramError::InvalidAccountData)?; + +let (address, address_seed) = derive_address( + &[ZK_ACCOUNT, &data_hash], + &address_tree_pubkey, + &crate::ID, +); +``` + + + + + +### Address tree check + +Ensure global uniqueness by verifying the address tree pubkey. + +```rust +if address_tree_pubkey.to_bytes() != light_sdk::constants::ADDRESS_TREE_V2 { + msg!("Invalid address tree"); + return Err(ProgramError::InvalidAccountData.into()); +} +``` + + + + + +### Initialize compressed account + + +```rust +let mut account = LightAccountPoseidon::::new_init( + &crate::ID, + Some(address), + output_state_tree_index, +); + +account.data_hash = ZkDataHash(data_hash); +``` + + + + + + +### Light System Program CPI + + +```rust +let light_cpi_accounts = CpiAccounts::new( + ctx.accounts.signer.as_ref(), + ctx.remaining_accounts, + crate::LIGHT_CPI_SIGNER, +); + +LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, proof) + .with_light_account_poseidon(account)? + .with_new_addresses(&[address_tree_info.into_new_address_params_packed(address_seed)]) + .invoke(light_cpi_accounts)?; +``` + + + +## Full code example + + + + + + + + + + + + + + + + + + + + + + +ZK clients follow the same pattern as [regular compressed account clients](/client-library/client-guide), with additional steps for generating Groth16 proofs. + +## Key points + +1. **Derive a new address** or **fetch compressed account** for on-chain verification. +2. **Fetch validity proof** from the RPC that verifies a new address does not exist in the address tree. +3. **Pack accounts** with the SDKs helper. Instructions require Light System Program and Merkle tree accounts. `PackedAccounts` converts their pubkeys to `u8` indices pointing to accounts in the instruction. +4. **Build the instruction** with the current account data, new data, packed accounts and validity proof. +5. **Generate ZK proof** with snarkjs to generate a Groth16 proof client-side that verifies account state without revealing the Merkle path. +6. **Build the instruction** with the proof, packed accounts, and account data. + +## Get started + + + + +### Setup + + + + +#### 1. Installation + +```bash +npm install \ + @lightprotocol/stateless.js@0.22.1-alpha.1 \ + @solana/web3.js \ + snarkjs \ + circomlibjs \ + @noble/hashes +``` + +| ZK Dependencies | Purpose | +|---------|---------| +| `snarkjs` | Groth16 proof generation | +| `circomlibjs` | Poseidon hasher for computing account hashes | +| `@noble/hashes` | Keccak-256 for BN254 field hashing | + +#### 2. Build circuit + +```bash +# Compile circuit +circom circuits/merkle_proof.circom --r1cs --wasm --sym -o build + +# Trusted setup +snarkjs groth16 setup build/merkle_proof.r1cs pot12_final.ptau build/merkle_proof_0000.zkey +snarkjs zkey contribute build/merkle_proof_0000.zkey build/merkle_proof_final.zkey +``` + +| File | Purpose | +|------|---------| +| `merkle_proof.wasm` | Compiled circuit for witness generation | +| `merkle_proof_final.zkey` | Proving key from trusted setup | + + + + + + + + +### ZK proof + +For ZK verification, generate the proof client-side instead of fetching from RPC. + +**Step 1: Fetch Merkle proof from indexer** + +```typescript +const account = await rpc.getCompressedAccount(bn(address.toBytes())); +const merkleProofs = await rpc.getMultipleCompressedAccountProofs([account.hash]); +``` + +**Step 2: Hash inputs to BN254 field** + +```typescript +function hashToBn254Field(data: Uint8Array): Uint8Array { + const hash = keccak_256(data); + hash[0] = hash[0] & 0x1f; // < 2^254 + return hash; +} + +const ownerHashed = hashToBn254Field(programId.toBytes()); +const merkleTreeHashed = hashToBn254Field(merkleTreePubkey.toBytes()); +``` + +**Step 3: Compute account hash (Poseidon)** + +```typescript +const LAMPORTS_OFFSET = 36893488147419103232n; +const accountHash = poseidon([ + ownerHashed, + leafIndex, + merkleTreeHashed, + address, + discriminator + LAMPORTS_OFFSET, + dataHash +]); +``` + +**Step 4: Prepare circuit inputs** + +```typescript +const inputs = { + // Public inputs (verified on-chain) + owner_hashed: toFieldString(ownerHashed), + merkle_tree_hashed: toFieldString(merkleTreeHashed), + discriminator: toFieldString(discriminator), + data_hash: toFieldString(dataHash), + expectedRoot: toFieldString(merkleRoot), + + // Private inputs (hidden in proof) + leaf_index: leafIndex.toString(), + account_leaf_index: accountLeafIndex.toString(), + address: toFieldString(address), + pathElements: pathElements.map(toFieldString), +}; +``` + +**Step 5: Generate Groth16 proof** + +```typescript +const { proof } = await snarkjs.groth16.fullProve( + inputs, + "build/merkle_proof_js/merkle_proof.wasm", + "build/merkle_proof_final.zkey" +); +``` + +**Step 6: Compress proof for on-chain** + +```typescript +const compressedProof = parseProofToCompressed(proof); +// Returns: { a: 32 bytes, b: 64 bytes, c: 32 bytes } +``` + + + + + +### Instruction data + + + + +```typescript +const instructionData = { + proof: { 0: proofResult.compressedProof }, + addressTreeInfo: { + addressMerkleTreePubkeyIndex: addressMerkleTreeIndex, + addressQueuePubkeyIndex: addressMerkleTreeIndex, + rootIndex: proofResult.rootIndices[0], + }, + outputStateTreeIndex, + dataHash: Array.from(dataHash), +}; +``` + + + + + +```typescript +const instructionData = { + inputRootIndex: merkleProof.rootIndex, + zkProof: compressedProof, // { a: [...], b: [...], c: [...] } + dataHash: Array.from(dataHash), +}; +``` + +The compressed proof format: +- `a`: 32 bytes (G1 X coordinate + sign bit) +- `b`: 64 bytes (G2 X coordinates + sign bit) +- `c`: 32 bytes (G1 X coordinate + sign bit) + + + + + + + + +### Instruction + +Build the instruction with Anchor. + +```typescript +const instruction = await program.methods + .createAccount( + proof, + addressTreeInfo, + outputStateTreeIndex, + systemStart, + Array.from(dataHash) + ) + .accounts({ signer: signer.publicKey }) + .remainingAccounts(remainingAccounts) + .instruction(); +``` + + + + + +### Send transaction + +```typescript +const tx = new web3.Transaction().add(computeBudgetIx, instruction); +tx.recentBlockhash = (await rpc.getLatestBlockhash()).blockhash; +tx.feePayer = signer.publicKey; +tx.sign(signer); + +const sig = await rpc.sendTransaction(tx, [signer]); +``` + + + + +## Full code example + + + + + + + + + + + + + + + + + + + + + +This is an example circuit to include compressed accounts. + + + +## Full code example + + + + + + + + + + + + + + + +For AI assistance with your ZK App, copy this prompt and add your design ideas: + + + +## Full code example + + + + + + + + + + + + + + + + + + +## Next steps + + \ No newline at end of file diff --git a/zk/examples.mdx b/zk/examples.mdx new file mode 100644 index 0000000..cb45dc4 --- /dev/null +++ b/zk/examples.mdx @@ -0,0 +1,10 @@ +--- +title: Examples +description: Example projects for building privacy applications on Solana. +keywords: ["privacy examples solana", "zk examples solana", "private payments examples", "zk identity solana"] +--- + +import ZkExamplesTable from "/snippets/overview-tables/zk-examples-table.mdx"; + + + diff --git a/zk/nullifiers.mdx b/zk/nullifiers.mdx new file mode 100644 index 0000000..8d837dc --- /dev/null +++ b/zk/nullifiers.mdx @@ -0,0 +1,145 @@ +--- +title: Nullifier Guide +description: "Nullifiers prevent double-spending of state without revealing which state was spent." +--- + +import ZkAppPrompt from "/snippets/ai-prompts/zk-app.mdx"; +import NullifierCircuit from '/snippets/code-snippets/zk/nullifier/circuit.mdx'; +import NullifierProgram from '/snippets/code-snippets/zk/nullifier/program.mdx'; +import NullifierRustClient from '/snippets/code-snippets/zk/nullifier/rust-client.mdx'; +import NullifierTypescriptClient from '/snippets/code-snippets/zk/nullifier/typescript-client.mdx'; + +--- + +* On Solana nullifiers require a data structure that ensures the nullifier is only created once. +* A straight forward way is to derive a PDA with the nullifier as seed for the PDA account. +* Nullifier accounts must remain active, hence lock ~0.001 SOL in rent per nullifier PDA permanently. +* A rent-free alternative are compressed addresses. It can be be created once and is derived from a seed similar to a pda. This way you can insert nullifiers at a fraction of the cost to create a PDA. + +| Storage | Cost per nullifier | +|---------|-------------------| +| PDA | ~0.001 SOL | +| Compressed PDA | ~0.000005 SOL | + + +To create the compressed PDA and a CPI to the Light system program you fetch an additional ZK proof from your RPC. +If you're already generating a ZK proof for your application logic, the marginal cost of the extra proof is low. + + +## Implementation Guide + +This is the complete flow of how nullifiers are used in zk applications. + + +Each tab includes a full code example section at the bottom. + + + + + + +### Client computes the nullifier + +The nullifier combines a context (e.g., `verification_id`) with the user's secret: + +```rust +fn compute_nullifier( + verification_id: &[u8; 32], + secret: &[u8; 32], +) -> [u8; 32] { + Poseidon::hashv(&[verification_id, secret]).unwrap() +} +``` + + + + +### Derive address from nullifier on-chain + +The program derives a deterministic address from the nullifier: + +```rust +let (address, address_seed) = derive_address( + &[ + NULLIFIER_PREFIX, // prefix + nullifier.as_slice(), // nullifier hash + verification_id.as_slice(), // context + ], + &address_tree_pubkey, + &crate::ID, +); +``` + + + +### Create account at that address + +Create a compressed account at the derived address: + +```rust +let nullifier_account = LightAccount::::new_init( + &crate::ID, + Some(address), + output_state_tree_index, +); +``` + +The nullifier now prevents double spending: +1. Same secret + same context = same nullifier +2. Same nullifier = same derived address +3. Address already exists = transaction fails + + + + +## Full code example + + + + + + + + + + + + + + + + + + + + +For AI assistance with your ZK App, copy this prompt and add your design ideas: + + + +## Full code example + + + + + + + + + + + + + + + + +## Next Steps + + \ No newline at end of file diff --git a/zk/overview.mdx b/zk/overview.mdx new file mode 100644 index 0000000..ab1d434 --- /dev/null +++ b/zk/overview.mdx @@ -0,0 +1,126 @@ +--- +title: Primitives for ZK on Solana +sidebarTitle: Overview +description: Overview how to build a ZK program on Solana. +keywords: ["nullifiers on Solana", "zcash on solana", "privacy on solana", "zk on solana", "solana privacy hackathon", "private payments solana", "privacy tooling solana"] +--- + +import ZkExamplesTable from "/snippets/overview-tables/zk-examples-table.mdx"; + +--- + +Building a ZK Solana program requires: +1. Nullifiers to prevent double spending +2. Proof verification +3. A Merkle tree to store state, +4. An indexer to serve Merkle proofs, and +5. Encrypted state. + +## Nullifiers on Solana + +A nullifier is a deterministically derived hash to ensure an action can only be performed once without linking it back to the action. +Attempting to consume state twice requires the same nullifier, hence the transaction would fail. +For example Zcash uses nullifiers to prevent double spending. + +* On Solana nullifiers require a data structure that ensures the nullifier is only created once. +* A straight forward way is to derive a PDA with the nullifier as seed for the PDA account. +* Nullifier accounts must remain active, hence lock ~0.001 SOL in rent per nullifier PDA permanently. +* A rent-free alternative are compressed addresses with similar functionality and derivation. This way you can insert nullifiers at a fraction of the cost to create a PDA. + + +| Storage | Cost per nullifier | +|---------|-------------------| +| PDA | ~0.001 SOL | +| Compressed PDA | ~0.000005 SOL | + + +This example does not require a custom circuit. [See full example with tests on Github](https://github.com/Lightprotocol/program-examples/tree/main/zk-id). + + +```rust +use anchor_lang::prelude::*; +use nullifier_creation::{create_nullifiers, NullifierInstructionData}; + +declare_id!("Bw8aty8LJY5Kg2b6djghjWGwt6cBc1tVQUoreUehvVq4"); + +#[program] +pub mod zk_nullifier { + use super::*; + + pub fn create_nullifier<'info>( + ctx: Context<'_, '_, '_, 'info, CreateNullifierAccounts<'info>>, + data: NullifierInstructionData, + nullifiers: Vec<[u8; 32]>, + ) -> Result<()> { + // Verify your proof here. Use nullifiers as public inputs + // among your other public inputs. + // Example: + // let public_inputs = [...nullifiers, ...your_other_inputs]; + // Groth16Verifier::new(...).verify()?; + + create_nullifiers( + &nullifiers, + data, + ctx.accounts.signer.as_ref(), + ctx.remaining_accounts, + ) + } +} + +#[derive(Accounts)] +pub struct CreateNullifierAccounts<'info> { + #[account(mut)] + pub signer: Signer<'info>, +} +``` + +## Groth16 Proof Verification on Solana + +Groth16's small proof size and fast verification (~200k compute units) make it the practical choice for Solana. + + +Find more information on [docs.rs](https://docs.rs/groth16-solana) and [Github](https://github.com/Lightprotocol/groth16-solana). + + +```rust +let mut public_inputs_vec = Vec::new(); +for input in PUBLIC_INPUTS.chunks(32) { + public_inputs_vec.push(input); +} + +let proof_a: G1 = + ::read(&*[&change_endianness(&PROOF[0..64])[..], &[0u8][..]].concat()) + .unwrap(); +let mut proof_a_neg = [0u8; 65]; +::write(&proof_a.neg(), &mut proof_a_neg[..]).unwrap(); + +let proof_a = change_endianness(&proof_a_neg[..64]).try_into().unwrap(); +let proof_b = PROOF[64..192].try_into().unwrap(); +let proof_c = PROOF[192..256].try_into().unwrap(); + +let mut verifier = Groth16Verifier::new( + &proof_a, + &proof_b, + &proof_c, + public_inputs_vec.as_slice(), + &VERIFYING_KEY, +) +.unwrap(); +verifier.verify().unwrap(); +``` + +## Merklelized State with Indexer Support + +ZK applications on Solana can use existing state Merkle trees to store state in compressed accounts. +* This way you don't need to maintain your own Merkle tree and indexer. +* RPCs that support ZK Compression (Helius, Triton) index state changes. + + +Find the [full example here](https://github.com/Lightprotocol/program-examples/blob/99d260f9f356743b8fe3501c684f7926930d6079/zk-id/circuits/compressed_account.circom) + + +## Get Started & Examples + + + +For AI assistance with your ZK App, copy this prompt and add your design ideas: From eb70a544d4556b932b0a1a3dea8e1b66437d9ba4 Mon Sep 17 00:00:00 2001 From: tilo-14 Date: Wed, 14 Jan 2026 22:45:36 +0000 Subject: [PATCH 3/5] Fix cost values in ZK overview --- resources/addresses-and-urls.mdx | 1 - zk/overview.mdx | 25 +++++++++++-------------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/resources/addresses-and-urls.mdx b/resources/addresses-and-urls.mdx index 0c66e2f..c04589f 100644 --- a/resources/addresses-and-urls.mdx +++ b/resources/addresses-and-urls.mdx @@ -87,7 +87,6 @@ Find all JSON RPC Methods for ZK Compression [here](/api-reference/json-rpc-meth - ## Address Trees & Queues diff --git a/zk/overview.mdx b/zk/overview.mdx index ab1d434..db70529 100644 --- a/zk/overview.mdx +++ b/zk/overview.mdx @@ -25,19 +25,19 @@ For example Zcash uses nullifiers to prevent double spending. * On Solana nullifiers require a data structure that ensures the nullifier is only created once. * A straight forward way is to derive a PDA with the nullifier as seed for the PDA account. * Nullifier accounts must remain active, hence lock ~0.001 SOL in rent per nullifier PDA permanently. -* A rent-free alternative are compressed addresses with similar functionality and derivation. This way you can insert nullifiers at a fraction of the cost to create a PDA. - +* Compressed addresses are rent-free, provide similar functionality and derivation while not requiring a custom circuit. | Storage | Cost per nullifier | |---------|-------------------| | PDA | ~0.001 SOL | -| Compressed PDA | ~0.000005 SOL | +| Compressed PDA | ~0.000015 SOL | -This example does not require a custom circuit. [See full example with tests on Github](https://github.com/Lightprotocol/program-examples/tree/main/zk-id). +[See full example with tests on Github](https://github.com/Lightprotocol/program-examples/tree/main/zk/nullifier). ```rust +// add to your program use anchor_lang::prelude::*; use nullifier_creation::{create_nullifiers, NullifierInstructionData}; @@ -52,11 +52,6 @@ pub mod zk_nullifier { data: NullifierInstructionData, nullifiers: Vec<[u8; 32]>, ) -> Result<()> { - // Verify your proof here. Use nullifiers as public inputs - // among your other public inputs. - // Example: - // let public_inputs = [...nullifiers, ...your_other_inputs]; - // Groth16Verifier::new(...).verify()?; create_nullifiers( &nullifiers, @@ -111,16 +106,18 @@ verifier.verify().unwrap(); ## Merklelized State with Indexer Support -ZK applications on Solana can use existing state Merkle trees to store state in compressed accounts. +ZK applications on Solana can use existing state Merkle trees to store state in rent-free accounts. * This way you don't need to maintain your own Merkle tree and indexer. * RPCs that support ZK Compression (Helius, Triton) index state changes. +| Creation | Regular | Compressed | +| :------------- | :--------------------- | :---------------------- | +| 100-byte PDA | ~0.002 SOL | ~0.000015 SOL | + -Find the [full example here](https://github.com/Lightprotocol/program-examples/blob/99d260f9f356743b8fe3501c684f7926930d6079/zk-id/circuits/compressed_account.circom) +Your circuit must include compressed accounts. Find [guides to compressed accounts in the documentation](/compressed-pdas/overview) and the [full example with zk implementation here](https://github.com/Lightprotocol/program-examples/blob/99d260f9f356743b8fe3501c684f7926930d6079/zk-id/circuits/compressed_account.circom). ## Get Started & Examples - - -For AI assistance with your ZK App, copy this prompt and add your design ideas: + \ No newline at end of file From d93cda18ceb27def6381eb16aace93b8af610a05 Mon Sep 17 00:00:00 2001 From: tilo-14 Date: Thu, 15 Jan 2026 02:41:00 +0000 Subject: [PATCH 4/5] cleanup --- scripts/copy-zk-merkle-proof.sh | 43 -- scripts/copy-zk-nullifier.sh | 42 -- snippets/ai-prompts/zk-app.mdx | 155 ------ .../code-snippets/zk/merkle-proof/circuit.mdx | 84 --- .../code-snippets/zk/merkle-proof/program.mdx | 172 ------ .../zk/merkle-proof/rust-client.mdx | 329 ------------ .../zk/merkle-proof/typescript-client.mdx | 332 ------------ .../code-snippets/zk/nullifier/circuit.mdx | 32 -- .../code-snippets/zk/nullifier/program.mdx | 248 --------- .../zk/nullifier/rust-client.mdx | 441 --------------- .../zk/nullifier/typescript-client.mdx | 312 ----------- zk/compressed-account-zk.mdx | 507 ------------------ zk/nullifiers.mdx | 145 ----- zk/overview.mdx | 4 +- 14 files changed, 2 insertions(+), 2844 deletions(-) delete mode 100644 scripts/copy-zk-merkle-proof.sh delete mode 100755 scripts/copy-zk-nullifier.sh delete mode 100644 snippets/ai-prompts/zk-app.mdx delete mode 100644 snippets/code-snippets/zk/merkle-proof/circuit.mdx delete mode 100644 snippets/code-snippets/zk/merkle-proof/program.mdx delete mode 100644 snippets/code-snippets/zk/merkle-proof/rust-client.mdx delete mode 100644 snippets/code-snippets/zk/merkle-proof/typescript-client.mdx delete mode 100644 snippets/code-snippets/zk/nullifier/circuit.mdx delete mode 100644 snippets/code-snippets/zk/nullifier/program.mdx delete mode 100644 snippets/code-snippets/zk/nullifier/rust-client.mdx delete mode 100644 snippets/code-snippets/zk/nullifier/typescript-client.mdx delete mode 100644 zk/compressed-account-zk.mdx delete mode 100644 zk/nullifiers.mdx diff --git a/scripts/copy-zk-merkle-proof.sh b/scripts/copy-zk-merkle-proof.sh deleted file mode 100644 index f94b221..0000000 --- a/scripts/copy-zk-merkle-proof.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash - -# Script to copy zk-merkle-proof code from program-examples to docs/snippets -# Wraps each file in appropriate markdown code blocks - -PROGRAM_EXAMPLES="/home/tilo/Workspace/program-examples/zk/zk-merkle-proof" -SNIPPETS_DIR="/home/tilo/Workspace/docs/snippets/code-snippets/zk/merkle-proof" - -mkdir -p "$SNIPPETS_DIR" - -wrap_code() { - local input_file="$1" - local output_file="$2" - local lang="$3" - echo "\`\`\`$lang" > "$output_file" - cat "$input_file" >> "$output_file" - echo '```' >> "$output_file" - echo "Created: $output_file" -} - -# Circuit -if [ -f "$PROGRAM_EXAMPLES/circuits/merkle_proof.circom" ]; then - wrap_code "$PROGRAM_EXAMPLES/circuits/merkle_proof.circom" "$SNIPPETS_DIR/circuit.mdx" "javascript expandable" -fi - -# Program -if [ -f "$PROGRAM_EXAMPLES/src/lib.rs" ]; then - wrap_code "$PROGRAM_EXAMPLES/src/lib.rs" "$SNIPPETS_DIR/program.mdx" "rust expandable" -fi - -# Rust client -if [ -f "$PROGRAM_EXAMPLES/tests/test.rs" ]; then - wrap_code "$PROGRAM_EXAMPLES/tests/test.rs" "$SNIPPETS_DIR/rust-client.mdx" "rust expandable" -fi - -# TypeScript client -if [ -f "$PROGRAM_EXAMPLES/ts-tests/merkle-proof.test.ts" ]; then - wrap_code "$PROGRAM_EXAMPLES/ts-tests/merkle-proof.test.ts" "$SNIPPETS_DIR/typescript-client.mdx" "typescript expandable" -fi - -echo "" -echo "Done! Created snippets in: $SNIPPETS_DIR" -find "$SNIPPETS_DIR" -name "*.mdx" -type f | sort diff --git a/scripts/copy-zk-nullifier.sh b/scripts/copy-zk-nullifier.sh deleted file mode 100755 index 32aa8a8..0000000 --- a/scripts/copy-zk-nullifier.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash - -# Script to copy zk-nullifier code from program-examples to docs/snippets - -PROGRAM_EXAMPLES="/home/tilo/Workspace/program-examples/zk/zk-nullifier" -SNIPPETS_DIR="/home/tilo/Workspace/docs/snippets/code-snippets/zk/nullifier" - -mkdir -p "$SNIPPETS_DIR" - -wrap_code() { - local input_file="$1" - local output_file="$2" - local lang="$3" - echo "\`\`\`$lang" > "$output_file" - cat "$input_file" >> "$output_file" - echo '```' >> "$output_file" - echo "Created: $output_file" -} - -# Circuit -if [ -f "$PROGRAM_EXAMPLES/circuits/nullifier.circom" ]; then - wrap_code "$PROGRAM_EXAMPLES/circuits/nullifier.circom" "$SNIPPETS_DIR/circuit.mdx" "javascript expandable" -fi - -# Program -if [ -f "$PROGRAM_EXAMPLES/programs/zk-nullifier/src/lib.rs" ]; then - wrap_code "$PROGRAM_EXAMPLES/programs/zk-nullifier/src/lib.rs" "$SNIPPETS_DIR/program.mdx" "rust expandable" -fi - -# Rust client (unified) -if [ -f "$PROGRAM_EXAMPLES/programs/zk-nullifier/tests/test.rs" ]; then - wrap_code "$PROGRAM_EXAMPLES/programs/zk-nullifier/tests/test.rs" "$SNIPPETS_DIR/rust-client.mdx" "rust expandable" -fi - -# TypeScript client -if [ -f "$PROGRAM_EXAMPLES/ts-tests/nullifier.test.ts" ]; then - wrap_code "$PROGRAM_EXAMPLES/ts-tests/nullifier.test.ts" "$SNIPPETS_DIR/typescript-client.mdx" "typescript expandable" -fi - -echo "" -echo "Done! Created snippets in: $SNIPPETS_DIR" -find "$SNIPPETS_DIR" -name "*.mdx" -type f | sort diff --git a/snippets/ai-prompts/zk-app.mdx b/snippets/ai-prompts/zk-app.mdx deleted file mode 100644 index 9edf37c..0000000 --- a/snippets/ai-prompts/zk-app.mdx +++ /dev/null @@ -1,155 +0,0 @@ -```markdown expandable ---- -argument-hint: -description: Design a ZK App POC with rent-free nullifiers, compressed accounts, and Groth16 circuits -allowed-tools: [Bash, Read, Glob, Grep, Task, WebFetch] ---- - -Design a Solana program with tests that uses rent-free nullifiers, compressed accounts, and Groth16 circuits. - - -## Initial App Design - - -## Goal - -Produce a **fully working POC** that builds and tests pass. - -## Available commands - -Via Bash tool: -- `cargo build-sbf`, `cargo test-sbf`, `cargo fmt`, `cargo clippy` -- `anchor build`, `anchor test`, `anchor deploy` -- `circom`, `snarkjs`, `solana`, `light` - -## Documentation - -- Nullifiers: https://zkcompression.com/zk/nullifiers -- Compressed Accounts with Poseidon Hashes: https://zkcompression.com/zk/compressed-account-zk - -## Reference repos - -Nullifier example. Does not need Circuit. -nullifier/ -├── programs/nullifier/ -│ ├── src/lib.rs # Program with create_nullifiers helper -│ └── tests/test.rs # Rust integration tests -└── ts-tests/ - └── nullifier.test.ts # TypeScript tests - -Full Example -program-examples/zk/zk-id/ -├── programs/zk-id/src/ -│ ├── lib.rs # create_issuer, add_credential, zk_verify_credential -│ └── verifying_key.rs # Groth16 key from circom trusted setup -├── circuits/ -│ └── compressed_account_merkle_proof.circom # Merkle proof -└── tests/ -└── zk-id.ts # Proof generation + on-chain verification - - -## Workflow - -### Phase 1: Design application - -**1.1 Define private state** - -What data stays private? (credentials, balances, votes, etc.) - -**1.2 Define public inputs** - -What does the circuit prove publicly? (nullifier, merkle root, commitments) - -**1.3 Do not include circuits** - - - -### Phase 2: Index reference implementation - - grep -r "LightAccountPoseidon" program-examples/zk/ - grep -r "Groth16Verifier" program-examples/zk/ - grep -r "derive_address.*nullifier" program-examples/zk/ - grep -r "read_state_merkle_tree_root" program-examples/zk/ - -Read matching files to understand patterns. - -### Phase 3: Circuit development - -**3.1 Write circom circuit** - -Based on compressed_account_merkle_proof.circom: -- Merkle proof verification -- Nullifier computation -- Public input constraints - -**3.2 Trusted setup** - - circom circuit.circom --r1cs --wasm --sym - snarkjs groth16 setup circuit.r1cs pot_final.ptau circuit_0000.zkey - snarkjs zkey export verificationkey circuit_final.zkey verification_key.json - snarkjs zkey export solidityverifier circuit_final.zkey # adapt for Solana - -**3.3 Add sensitive files to .gitignore** - - *.zkey - *.ptau - *.r1cs - *_js/ - -### Phase 4: Program implementation - -| Pattern | Function | Reference | -|---------|----------|-----------| -| Poseidon state | `LightAccountPoseidon::new_init()` | zk-id/lib.rs | -| Nullifier address | `derive_address([prefix, nullifier, ctx], tree, program)` | zk-id/lib.rs | -| Read root only | `read_state_merkle_tree_root()` | zk-id/lib.rs | -| Groth16 verify | `Groth16Verifier::new().verify()` | zk-id/lib.rs | - -**Dependencies:** - - [dependencies] - anchor-lang = "0.31.1" - light-sdk = { version = "0.17.1", features = ["anchor", "poseidon", "merkle-tree", "v2"] } - light-hasher = "5.0.0" - light-sdk-types = { version = "0.17.1", features = ["v2"] } - groth16-solana = { git = "https://github.com/Lightprotocol/groth16-solana", rev = "66c0dc87" } - - [dev-dependencies] - light-program-test = "0.17.1" - light-client = "0.17.1" - -### Phase 5: Build and test loop - -**Required commands (no shortcuts):** - -For Anchor programs: `anchor build && anchor test` - -For Native programs: `cargo build-sbf && cargo test-sbf` - -**NO shortcuts allowed:** - -- Do NOT use `cargo build` (must use `cargo build-sbf`) -- Do NOT use `cargo test` (must use `cargo test-sbf`) -- Do NOT skip SBF compilation -- Tests MUST run against real BPF bytecode - -**On failure:** Spawn debugger agent with error context. - -**Loop rules:** - -1. Each debugger gets fresh context + previous debug reports -2. Each attempt tries something DIFFERENT -3. **NEVER GIVE UP** - keep spawning until fixed - -Do NOT proceed until all tests pass. - -### Phase 6: Cleanup (only after tests pass) - - rm -rf target/ - -## DeepWiki fallback - -If no matching pattern in reference repos: - - mcp__deepwiki__ask_question("Lightprotocol/light-protocol", "How to {operation}?") -``` \ No newline at end of file diff --git a/snippets/code-snippets/zk/merkle-proof/circuit.mdx b/snippets/code-snippets/zk/merkle-proof/circuit.mdx deleted file mode 100644 index d650167..0000000 --- a/snippets/code-snippets/zk/merkle-proof/circuit.mdx +++ /dev/null @@ -1,84 +0,0 @@ -```javascript expandable -pragma circom 2.0.0; - -include "../node_modules/circomlib/circuits/poseidon.circom"; -include "../node_modules/circomlib/circuits/bitify.circom"; -include "../node_modules/circomlib/circuits/switcher.circom"; - -// Merkle Proof Verification Template -// Verifies that a leaf is in a Merkle tree with a given root -template MerkleProof(levels) { - signal input leaf; - signal input pathElements[levels]; - signal input leafIndex; - signal output root; - - component switcher[levels]; - component hasher[levels]; - component indexBits = Num2Bits(levels); - indexBits.in <== leafIndex; - - for (var i = 0; i < levels; i++) { - switcher[i] = Switcher(); - switcher[i].L <== i == 0 ? leaf : hasher[i - 1].out; - switcher[i].R <== pathElements[i]; - switcher[i].sel <== indexBits.out[i]; - - hasher[i] = Poseidon(2); - hasher[i].inputs[0] <== switcher[i].outL; - hasher[i].inputs[1] <== switcher[i].outR; - } - - root <== hasher[levels - 1].out; -} - -template CompressedAccountHash() { - signal input owner_hashed; - signal input leaf_index; - signal input merkle_tree_hashed; - signal input address; - signal input discriminator; - signal input data_hash; - signal output hash; - - component poseidon = Poseidon(6); - poseidon.inputs[0] <== owner_hashed; - poseidon.inputs[1] <== leaf_index; - poseidon.inputs[2] <== merkle_tree_hashed; - poseidon.inputs[3] <== address; - poseidon.inputs[4] <== discriminator + 36893488147419103232; - poseidon.inputs[5] <== data_hash; - hash <== poseidon.out; -} - -template CompressedAccountMerkleProof(levels) { - signal input owner_hashed; - signal input merkle_tree_hashed; - signal input discriminator; - signal input data_hash; - signal input expectedRoot; - - signal input leaf_index; - signal input account_leaf_index; - signal input address; - signal input pathElements[levels]; - - component accountHasher = CompressedAccountHash(); - accountHasher.owner_hashed <== owner_hashed; - accountHasher.leaf_index <== account_leaf_index; - accountHasher.address <== address; - accountHasher.merkle_tree_hashed <== merkle_tree_hashed; - accountHasher.discriminator <== discriminator; - accountHasher.data_hash <== data_hash; - - component merkleProof = MerkleProof(levels); - merkleProof.leaf <== accountHasher.hash; - merkleProof.pathElements <== pathElements; - merkleProof.leafIndex <== leaf_index; - merkleProof.root === expectedRoot; -} - -component main { - public [owner_hashed, merkle_tree_hashed, discriminator, data_hash, expectedRoot] -} = CompressedAccountMerkleProof(26); -``` diff --git a/snippets/code-snippets/zk/merkle-proof/program.mdx b/snippets/code-snippets/zk/merkle-proof/program.mdx deleted file mode 100644 index e309ed7..0000000 --- a/snippets/code-snippets/zk/merkle-proof/program.mdx +++ /dev/null @@ -1,172 +0,0 @@ -```rust expandable -#![allow(unexpected_cfgs)] -#![allow(deprecated)] - -use anchor_lang::prelude::*; -use borsh::{BorshDeserialize, BorshSerialize}; -use groth16_solana::groth16::Groth16Verifier; -use light_hasher::to_byte_array::ToByteArray; -use light_hasher::HasherError; -use light_sdk::account::poseidon::LightAccount as LightAccountPoseidon; -use light_sdk::address::v2::derive_address; -use light_sdk::cpi::v1::CpiAccounts; -use light_sdk::{ - cpi::{v1::LightSystemProgramCpi, InvokeLightSystemProgram, LightCpiInstruction}, - derive_light_cpi_signer, - instruction::{CompressedProof, PackedAddressTreeInfo, ValidityProof}, - merkle_tree::v1::read_state_merkle_tree_root, - LightDiscriminator, LightHasher, -}; -use light_sdk_types::CpiSigner; - -declare_id!("MPzkYomvQc4VQPwMr6bFduyWRQZVCh5CofgDC4dFqJp"); - -pub const LIGHT_CPI_SIGNER: CpiSigner = - derive_light_cpi_signer!("MPzkYomvQc4VQPwMr6bFduyWRQZVCh5CofgDC4dFqJp"); - -pub const ZK_ACCOUNT: &[u8] = b"zk_account"; - -pub mod verifying_key; - -#[program] -pub mod zk_merkle_proof { - use groth16_solana::decompression::{decompress_g1, decompress_g2}; - use light_hasher::hash_to_field_size::hashv_to_bn254_field_size_be_const_array; - - use super::*; - - pub fn create_account<'info>( - ctx: Context<'_, '_, '_, 'info, CreateAccountAccounts<'info>>, - proof: ValidityProof, - address_tree_info: PackedAddressTreeInfo, - output_state_tree_index: u8, - data_hash: [u8; 32], - ) -> Result<()> { - let light_cpi_accounts = CpiAccounts::new( - ctx.accounts.signer.as_ref(), - ctx.remaining_accounts, - crate::LIGHT_CPI_SIGNER, - ); - - let address_tree_pubkey = address_tree_info - .get_tree_pubkey(&light_cpi_accounts) - .map_err(|_| ProgramError::InvalidAccountData)?; - - let (address, address_seed) = derive_address( - &[ZK_ACCOUNT, &data_hash], - &address_tree_pubkey, - &crate::ID, - ); - - let mut account = LightAccountPoseidon::::new_init( - &crate::ID, - Some(address), - output_state_tree_index, - ); - - account.data_hash = DataHash(data_hash); - - LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, proof) - .with_light_account_poseidon(account)? - .with_new_addresses(&[address_tree_info.into_new_address_params_packed(address_seed)]) - .invoke(light_cpi_accounts)?; - - Ok(()) - } - - pub fn verify_account<'info>( - ctx: Context<'_, '_, '_, 'info, VerifyAccountAccounts<'info>>, - input_root_index: u16, - zk_proof: CompressedProof, - data_hash: [u8; 32], - ) -> Result<()> { - let expected_root = read_state_merkle_tree_root( - &ctx.accounts.state_merkle_tree.to_account_info(), - input_root_index, - ) - .map_err(ProgramError::from)?; - - let owner_hashed = - hashv_to_bn254_field_size_be_const_array::<2>(&[&crate::ID.to_bytes()]).unwrap(); - - let merkle_tree_pubkey = ctx.accounts.state_merkle_tree.key(); - let merkle_tree_hashed = - hashv_to_bn254_field_size_be_const_array::<2>(&[&merkle_tree_pubkey.to_bytes()]) - .unwrap(); - - let mut discriminator = [0u8; 32]; - discriminator[24..].copy_from_slice(ZkAccount::LIGHT_DISCRIMINATOR_SLICE); - - let public_inputs: [[u8; 32]; 5] = [ - owner_hashed, - merkle_tree_hashed, - discriminator, - data_hash, - expected_root, - ]; - - let proof_a = decompress_g1(&zk_proof.a).map_err(|e| { - let code: u32 = e.into(); - Error::from(ProgramError::Custom(code)) - })?; - - let proof_b = decompress_g2(&zk_proof.b).map_err(|e| { - let code: u32 = e.into(); - Error::from(ProgramError::Custom(code)) - })?; - - let proof_c = decompress_g1(&zk_proof.c).map_err(|e| { - let code: u32 = e.into(); - Error::from(ProgramError::Custom(code)) - })?; - - let mut verifier = Groth16Verifier::new( - &proof_a, - &proof_b, - &proof_c, - &public_inputs, - &crate::verifying_key::VERIFYINGKEY, - ) - .map_err(|e| { - let code: u32 = e.into(); - Error::from(ProgramError::Custom(code)) - })?; - - verifier.verify().map_err(|e| { - let code: u32 = e.into(); - Error::from(ProgramError::Custom(code)) - })?; - - Ok(()) - } -} - -#[derive(Accounts)] -pub struct CreateAccountAccounts<'info> { - #[account(mut)] - pub signer: Signer<'info>, -} - -#[derive(Accounts)] -pub struct VerifyAccountAccounts<'info> { - #[account(mut)] - pub signer: Signer<'info>, - /// CHECK: validated by read_state_merkle_tree_root - pub state_merkle_tree: UncheckedAccount<'info>, -} - -#[derive(Clone, Debug, Default, BorshSerialize, BorshDeserialize, LightDiscriminator, LightHasher)] -pub struct ZkAccount { - pub data_hash: DataHash, -} - -#[derive(Clone, Debug, Default, BorshSerialize, BorshDeserialize)] -pub struct DataHash(pub [u8; 32]); - -impl ToByteArray for DataHash { - const NUM_FIELDS: usize = 1; - fn to_byte_array(&self) -> std::result::Result<[u8; 32], HasherError> { - Ok(self.0) - } -} -``` diff --git a/snippets/code-snippets/zk/merkle-proof/rust-client.mdx b/snippets/code-snippets/zk/merkle-proof/rust-client.mdx deleted file mode 100644 index 3ad10bc..0000000 --- a/snippets/code-snippets/zk/merkle-proof/rust-client.mdx +++ /dev/null @@ -1,329 +0,0 @@ -```rust expandable -use anchor_lang::{InstructionData, ToAccountMetas}; -use circom_prover::{prover::ProofLib, witness::WitnessFn, CircomProver}; -use groth16_solana::proof_parser::circom_prover::convert_proof; -use light_hasher::{hash_to_field_size::hash_to_bn254_field_size_be, Hasher, Poseidon}; -use light_program_test::{ - program_test::LightProgramTest, AddressWithTree, Indexer, ProgramTestConfig, Rpc, RpcError, -}; -use light_sdk::{ - address::v2::derive_address, - instruction::{PackedAccounts, SystemAccountMetaConfig}, -}; -use num_bigint::BigUint; -use solana_sdk::{ - instruction::Instruction, - pubkey::Pubkey, - signature::{Keypair, Signature, Signer}, -}; -use std::collections::HashMap; -use zk_merkle_proof::ZK_ACCOUNT; - -#[link(name = "circuit", kind = "static")] -extern "C" {} - -rust_witness::witness!(merkleproof); - -#[tokio::test] -async fn test_create_and_verify_account() { - let config = - ProgramTestConfig::new(true, Some(vec![("zk_merkle_proof", zk_merkle_proof::ID)])); - let mut rpc = LightProgramTest::new(config).await.unwrap(); - let payer = rpc.get_payer().insecure_clone(); - - // First byte = 0 for BN254 field compatibility - let mut secret_data = [0u8; 32]; - for i in 1..32 { - secret_data[i] = (i as u8) + 65; - } - let data_hash = Poseidon::hashv(&[&secret_data]).unwrap(); - - let address_tree_info = rpc.get_address_tree_v2(); - - let (account_address, _) = derive_address( - &[ZK_ACCOUNT, &data_hash], - &address_tree_info.tree, - &zk_merkle_proof::ID, - ); - - create_account(&mut rpc, &payer, &account_address, address_tree_info.clone(), data_hash) - .await - .unwrap(); - - let accounts = rpc - .get_compressed_accounts_by_owner(&zk_merkle_proof::ID, None, None) - .await - .unwrap(); - assert_eq!(accounts.value.items.len(), 1); - let created_account = &accounts.value.items[0]; - - let account_data_hash = created_account.data.as_ref().unwrap().data_hash; - - verify_account(&mut rpc, &payer, created_account, account_data_hash) - .await - .unwrap(); -} - -async fn create_account( - rpc: &mut R, - payer: &Keypair, - address: &[u8; 32], - address_tree_info: light_client::indexer::TreeInfo, - data_hash: [u8; 32], -) -> Result -where - R: Rpc + Indexer, -{ - let mut remaining_accounts = PackedAccounts::default(); - remaining_accounts.add_pre_accounts_signer(payer.pubkey()); - let config = SystemAccountMetaConfig::new(zk_merkle_proof::ID); - remaining_accounts.add_system_accounts_v2(config)?; - - let rpc_result = rpc - .get_validity_proof( - vec![], - vec![AddressWithTree { - address: *address, - tree: address_tree_info.tree, - }], - None, - ) - .await? - .value; - - let packed_address_tree_accounts = rpc_result - .pack_tree_infos(&mut remaining_accounts) - .address_trees; - - let output_state_tree_index = rpc - .get_random_state_tree_info_v1()? - .pack_output_tree_index(&mut remaining_accounts)?; - - let (remaining_accounts_metas, system_accounts_offset, _) = remaining_accounts.to_account_metas(); - - let instruction_data = zk_merkle_proof::instruction::CreateAccount { - proof: rpc_result.proof, - address_tree_info: packed_address_tree_accounts[0], - output_state_tree_index, - system_accounts_offset: system_accounts_offset as u8, - data_hash, - }; - - let accounts = zk_merkle_proof::accounts::CreateAccountAccounts { - signer: payer.pubkey(), - }; - - let instruction = Instruction { - program_id: zk_merkle_proof::ID, - accounts: [ - accounts.to_account_metas(None), - remaining_accounts_metas, - ] - .concat(), - data: instruction_data.data(), - }; - - rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) - .await -} - -async fn verify_account( - rpc: &mut R, - payer: &Keypair, - account: &light_client::indexer::CompressedAccount, - data_hash: [u8; 32], -) -> Result -where - R: Rpc + Indexer, -{ - let proofs_result = rpc - .get_multiple_compressed_account_proofs(vec![account.hash], None) - .await?; - let proofs = proofs_result.value.items; - - assert!(!proofs.is_empty(), "No proofs returned"); - - let merkle_proof = &proofs[0]; - let leaf_index = merkle_proof.leaf_index as u32; - let merkle_proof_hashes = &merkle_proof.proof; - let merkle_root = merkle_proof.root; - let root_index = (merkle_proof.root_seq % 2400) as u16; - let state_tree = merkle_proof.merkle_tree; - - let zk_proof = generate_merkle_proof( - account, - &state_tree, - leaf_index, - merkle_proof_hashes, - &merkle_root, - &data_hash, - ); - - let mut remaining_accounts = PackedAccounts::default(); - let config = SystemAccountMetaConfig::new(zk_merkle_proof::ID); - remaining_accounts.add_system_accounts(config)?; - - let instruction_data = zk_merkle_proof::instruction::VerifyAccount { - input_root_index: root_index, - zk_proof, - data_hash, - }; - - let accounts = zk_merkle_proof::accounts::VerifyAccountAccounts { - signer: payer.pubkey(), - state_merkle_tree: state_tree, - }; - - let instruction = Instruction { - program_id: zk_merkle_proof::ID, - accounts: [ - accounts.to_account_metas(None), - remaining_accounts.to_account_metas().0, - ] - .concat(), - data: instruction_data.data(), - }; - - rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) - .await -} - -fn generate_merkle_proof( - account: &light_client::indexer::CompressedAccount, - merkle_tree_pubkey: &Pubkey, - leaf_index: u32, - merkle_proof_hashes: &[[u8; 32]], - merkle_root: &[u8; 32], - data_hash: &[u8; 32], -) -> light_compressed_account::instruction_data::compressed_proof::CompressedProof { - let zkey_path = "./build/merkle_proof_final.zkey".to_string(); - - let mut proof_inputs = HashMap::new(); - - let owner_hashed = hash_to_bn254_field_size_be(zk_merkle_proof::ID.as_ref()); - let merkle_tree_hashed = hash_to_bn254_field_size_be(merkle_tree_pubkey.as_ref()); - - let discriminator = if let Some(ref data) = account.data { - data.discriminator - } else { - [0u8; 8] - }; - - let address = account.address.expect("Account must have an address"); - - // Verify hash can be recreated - let mut leaf_index_bytes = [0u8; 32]; - leaf_index_bytes[28..32].copy_from_slice(&(account.leaf_index as u32).to_le_bytes()); - - let mut discriminator_with_domain = [0u8; 32]; - discriminator_with_domain[24..32].copy_from_slice(&discriminator); - discriminator_with_domain[23] = 2; - - let computed_hash = Poseidon::hashv(&[ - owner_hashed.as_slice(), - leaf_index_bytes.as_slice(), - merkle_tree_hashed.as_slice(), - address.as_slice(), - discriminator_with_domain.as_slice(), - data_hash.as_slice(), - ]) - .unwrap(); - - assert_eq!(computed_hash, account.hash, "Hash mismatch"); - - // Public inputs - proof_inputs.insert( - "owner_hashed".to_string(), - vec![BigUint::from_bytes_be(&owner_hashed).to_string()], - ); - proof_inputs.insert( - "merkle_tree_hashed".to_string(), - vec![BigUint::from_bytes_be(&merkle_tree_hashed).to_string()], - ); - proof_inputs.insert( - "discriminator".to_string(), - vec![BigUint::from_bytes_be(&discriminator).to_string()], - ); - proof_inputs.insert( - "data_hash".to_string(), - vec![BigUint::from_bytes_be(data_hash).to_string()], - ); - proof_inputs.insert( - "expectedRoot".to_string(), - vec![BigUint::from_bytes_be(merkle_root).to_string()], - ); - - // Private inputs - proof_inputs.insert("leaf_index".to_string(), vec![leaf_index.to_string()]); - - let mut account_leaf_index_bytes = [0u8; 32]; - account_leaf_index_bytes[28..32].copy_from_slice(&(account.leaf_index as u32).to_le_bytes()); - proof_inputs.insert( - "account_leaf_index".to_string(), - vec![BigUint::from_bytes_be(&account_leaf_index_bytes).to_string()], - ); - - proof_inputs.insert( - "address".to_string(), - vec![BigUint::from_bytes_be(&address).to_string()], - ); - - let path_elements: Vec = merkle_proof_hashes - .iter() - .map(|hash| BigUint::from_bytes_be(hash).to_string()) - .collect(); - proof_inputs.insert("pathElements".to_string(), path_elements); - - let circuit_inputs = serde_json::to_string(&proof_inputs).unwrap(); - - let proof = CircomProver::prove( - ProofLib::Arkworks, - WitnessFn::RustWitness(merkleproof_witness), - circuit_inputs, - zkey_path.clone(), - ) - .expect("Proof generation failed"); - - let is_valid = CircomProver::verify(ProofLib::Arkworks, proof.clone(), zkey_path.clone()) - .expect("Proof verification failed"); - assert!(is_valid, "Local proof verification failed"); - - // Verify with groth16-solana - { - use groth16_solana::groth16::Groth16Verifier; - use groth16_solana::proof_parser::circom_prover::convert_public_inputs; - - let (proof_a, proof_b, proof_c) = - convert_proof(&proof.proof).expect("Failed to convert proof"); - let public_inputs_converted: [[u8; 32]; 5] = convert_public_inputs(&proof.pub_inputs); - - let mut verifier = Groth16Verifier::new( - &proof_a, - &proof_b, - &proof_c, - &public_inputs_converted, - &zk_merkle_proof::verifying_key::VERIFYINGKEY, - ) - .expect("Failed to create verifier"); - - verifier.verify().expect("groth16-solana verification failed"); - } - - let (proof_a_uncompressed, proof_b_uncompressed, proof_c_uncompressed) = - convert_proof(&proof.proof).expect("Failed to convert proof"); - - use groth16_solana::proof_parser::circom_prover::convert_proof_to_compressed; - let (proof_a, proof_b, proof_c) = convert_proof_to_compressed( - &proof_a_uncompressed, - &proof_b_uncompressed, - &proof_c_uncompressed, - ) - .expect("Failed to compress proof"); - - light_compressed_account::instruction_data::compressed_proof::CompressedProof { - a: proof_a, - b: proof_b, - c: proof_c, - } -} -``` diff --git a/snippets/code-snippets/zk/merkle-proof/typescript-client.mdx b/snippets/code-snippets/zk/merkle-proof/typescript-client.mdx deleted file mode 100644 index aafb96b..0000000 --- a/snippets/code-snippets/zk/merkle-proof/typescript-client.mdx +++ /dev/null @@ -1,332 +0,0 @@ -```typescript expandable -import { web3, Program, AnchorProvider, setProvider } from "@coral-xyz/anchor"; -import { - bn, - createRpc, - deriveAddressSeedV2, - deriveAddressV2, - batchAddressTree, - PackedAccounts, - Rpc, - sleep, - SystemAccountMetaConfig, - defaultTestStateTreeAccounts, - featureFlags, - VERSION, - confirmTx, -} from "@lightprotocol/stateless.js"; -import { buildPoseidonOpt } from "circomlibjs"; -import { keccak_256 } from "@noble/hashes/sha3"; -import * as snarkjs from "snarkjs"; -import * as assert from "assert"; -import * as path from "path"; -import * as fs from "fs"; - -import { - parseProofToCompressed, - bigintToBytes32, - toFieldString, - generateFieldElement, -} from "./utils/proof-helpers"; - -// Force V2 mode -(featureFlags as any).version = VERSION.V2; - -// Load IDL -const IDL = JSON.parse( - fs.readFileSync(path.join(process.cwd(), "target/idl/zk_merkle_proof.json"), "utf8") -); - -// Program ID from IDL -const PROGRAM_ID = new web3.PublicKey(IDL.address); -const ZK_ACCOUNT_PREFIX = Buffer.from("zk_account"); -const ZK_ACCOUNT_DISCRIMINATOR = Buffer.from([0x5b, 0x98, 0xb8, 0x43, 0x93, 0x6c, 0x21, 0xf4]); - -// Paths to circuit artifacts -const BUILD_DIR = path.join(process.cwd(), "build"); -const WASM_PATH = path.join(BUILD_DIR, "merkle_proof_js/merkle_proof.wasm"); -const ZKEY_PATH = path.join(BUILD_DIR, "merkle_proof_final.zkey"); - -const MERKLE_TREE_DEPTH = 26; - -/** Hash to BN254 field (matching Light Protocol's hashv_to_bn254_field_size_be) */ -function hashToBn254Field(data: Uint8Array): Uint8Array { - const hash = keccak_256(data); - hash[0] = hash[0] & 0x1f; - return hash; -} - -describe("zk-merkle-proof", () => { - let rpc: Rpc; - let signer: web3.Keypair; - let poseidon: any; - let program: Program; - - before(async () => { - rpc = createRpc( - "http://127.0.0.1:8899", - "http://127.0.0.1:8784", - "http://127.0.0.1:3001", - { commitment: "confirmed" } - ); - - signer = web3.Keypair.generate(); - await rpc.requestAirdrop(signer.publicKey, web3.LAMPORTS_PER_SOL); - await sleep(2000); - - poseidon = await buildPoseidonOpt(); - - // Setup Anchor provider and program - const connection = new web3.Connection("http://127.0.0.1:8899", "confirmed"); - const wallet = { - publicKey: signer.publicKey, - signTransaction: async (tx: web3.Transaction) => { - tx.sign(signer); - return tx; - }, - signAllTransactions: async (txs: web3.Transaction[]) => { - txs.forEach((tx) => tx.sign(signer)); - return txs; - }, - }; - const provider = new AnchorProvider(connection, wallet as any, { commitment: "confirmed" }); - setProvider(provider); - program = new Program(IDL, provider); - }); - - after(async () => { - // Terminate snarkjs curve worker to allow clean exit - // @ts-ignore - if (globalThis.curve_bn128) { - // @ts-ignore - await globalThis.curve_bn128.terminate(); - } - }); - - /** Compute Poseidon hash of compressed account fields */ - function computeAccountHash( - ownerHashed: Uint8Array, - leafIndex: bigint, - merkleTreeHashed: Uint8Array, - address: Uint8Array, - discriminator: bigint, - dataHash: Uint8Array - ): Uint8Array { - const LAMPORTS_OFFSET = 36893488147419103232n; - const hash = poseidon([ - BigInt("0x" + Buffer.from(ownerHashed).toString("hex")), - leafIndex, - BigInt("0x" + Buffer.from(merkleTreeHashed).toString("hex")), - BigInt("0x" + Buffer.from(address).toString("hex")), - discriminator + LAMPORTS_OFFSET, - BigInt("0x" + Buffer.from(dataHash).toString("hex")), - ]); - return bigintToBytes32(poseidon.F.toObject(hash)); - } - - /** Compute Merkle root from leaf and path */ - function computeMerkleRoot(leaf: Uint8Array, pathElements: Uint8Array[], leafIndex: number): Uint8Array { - let current = BigInt("0x" + Buffer.from(leaf).toString("hex")); - - for (let i = 0; i < pathElements.length; i++) { - const pathElement = BigInt("0x" + Buffer.from(pathElements[i]).toString("hex")); - const isRight = (leafIndex >> i) & 1; - const [left, right] = isRight ? [pathElement, current] : [current, pathElement]; - current = poseidon.F.toObject(poseidon([left, right])); - } - - return bigintToBytes32(current); - } - - /** Generate ZK proof for Merkle inclusion */ - async function generateMerkleProof( - ownerHashed: Uint8Array, - merkleTreeHashed: Uint8Array, - discriminator: Uint8Array, - dataHash: Uint8Array, - expectedRoot: Uint8Array, - leafIndex: number, - accountLeafIndex: number, - address: Uint8Array, - pathElements: Uint8Array[] - ): Promise<{ a: number[]; b: number[]; c: number[] }> { - const inputs = { - owner_hashed: toFieldString(ownerHashed), - merkle_tree_hashed: toFieldString(merkleTreeHashed), - discriminator: toFieldString(discriminator), - data_hash: toFieldString(dataHash), - expectedRoot: toFieldString(expectedRoot), - leaf_index: leafIndex.toString(), - account_leaf_index: accountLeafIndex.toString(), - address: toFieldString(address), - pathElements: pathElements.map(toFieldString), - }; - - const { proof } = await snarkjs.groth16.fullProve(inputs, WASM_PATH, ZKEY_PATH); - return parseProofToCompressed(proof); - } - - /** Build create_account instruction using Anchor */ - async function buildCreateAccountInstruction(dataHash: Uint8Array): Promise { - const addressTree = new web3.PublicKey(batchAddressTree); - const outputStateTree = defaultTestStateTreeAccounts().merkleTree; - - const seed = deriveAddressSeedV2([ZK_ACCOUNT_PREFIX, dataHash]); - const address = deriveAddressV2(seed, addressTree, PROGRAM_ID); - - const proofResult = await rpc.getValidityProofV0( - [], - [{ tree: addressTree, queue: addressTree, address: bn(address.toBytes()) }] - ); - - const remainingAccounts = new PackedAccounts(); - remainingAccounts.addPreAccountsSigner(signer.publicKey); - remainingAccounts.addSystemAccountsV2(SystemAccountMetaConfig.new(PROGRAM_ID)); - - const addressMerkleTreeIndex = remainingAccounts.insertOrGet(addressTree); - const outputStateTreeIndex = remainingAccounts.insertOrGet(outputStateTree); - - const { remainingAccounts: accountMetas, systemStart } = remainingAccounts.toAccountMetas(); - - // Use Anchor to build instruction - // ValidityProof is a struct with an unnamed Option field - const proof = { - 0: proofResult.compressedProof, - }; - - const ix = await program.methods - .createAccount( - // proof (ValidityProof = struct with Option) - proof, - // address_tree_info (PackedAddressTreeInfo) - { - addressMerkleTreePubkeyIndex: addressMerkleTreeIndex, - addressQueuePubkeyIndex: addressMerkleTreeIndex, - rootIndex: proofResult.rootIndices[0], - }, - // output_state_tree_index - outputStateTreeIndex, - // system_accounts_offset - systemStart, - // data_hash - Array.from(dataHash) - ) - .accounts({ - signer: signer.publicKey, - }) - .remainingAccounts(accountMetas) - .instruction(); - - return ix; - } - - describe("create_account", () => { - it("should create a compressed account with data hash", async () => { - const dataHash = generateFieldElement(); - console.log("Data hash:", Buffer.from(dataHash).toString("hex").slice(0, 16) + "..."); - - const ix = await buildCreateAccountInstruction(dataHash); - const computeIx = web3.ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 }); - - const tx = new web3.Transaction().add(computeIx, ix); - tx.recentBlockhash = (await rpc.getLatestBlockhash()).blockhash; - tx.feePayer = signer.publicKey; - tx.sign(signer); - - const sig = await rpc.sendTransaction(tx, [signer]); - await confirmTx(rpc, sig); - - console.log("Transaction signature:", sig); - - const slot = await rpc.getSlot(); - await rpc.confirmTransactionIndexed(slot); - - const accounts = await rpc.getCompressedAccountsByOwner(PROGRAM_ID); - assert.ok(accounts.items.length > 0, "Account should be created"); - console.log("Created accounts:", accounts.items.length); - }); - }); - - describe("verify_account (ZK proof)", () => { - it("should verify account existence with ZK proof", async () => { - const dataHash = generateFieldElement(); - - const createIx = await buildCreateAccountInstruction(dataHash); - const computeIx = web3.ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 }); - - const createTx = new web3.Transaction().add(computeIx, createIx); - createTx.recentBlockhash = (await rpc.getLatestBlockhash()).blockhash; - createTx.feePayer = signer.publicKey; - createTx.sign(signer); - - await rpc.sendTransaction(createTx, [signer]); - await sleep(3000); - - const slot = await rpc.getSlot(); - await rpc.confirmTransactionIndexed(slot); - - const accounts = await rpc.getCompressedAccountsByOwner(PROGRAM_ID); - assert.ok(accounts.items.length > 0, "Should have created account"); - - const account = accounts.items[0]; - console.log("Account hash:", account.hash.toString(16).slice(0, 16) + "..."); - console.log("Leaf index:", account.leafIndex); - - const merkleProof = await rpc.getValidityProof([account.hash]); - console.log("Root index:", merkleProof.rootIndices[0]); - - assert.ok(merkleProof.compressedProof, "Should have compressed proof"); - assert.ok(merkleProof.rootIndices.length > 0, "Should have root indices"); - - console.log("Account verified in state tree"); - }); - - it("should demonstrate ZK proof generation for Merkle inclusion", async () => { - const ownerHashed = hashToBn254Field(PROGRAM_ID.toBytes()); - const merkleTreeHashed = hashToBn254Field( - new web3.PublicKey(defaultTestStateTreeAccounts().merkleTree).toBytes() - ); - - const dataHash = generateFieldElement(); - const discriminator = new Uint8Array(32); - discriminator.set(ZK_ACCOUNT_DISCRIMINATOR, 24); - - const pathElements = Array.from({ length: MERKLE_TREE_DEPTH }, () => new Uint8Array(32)); - const address = generateFieldElement(); - - const accountHash = computeAccountHash( - ownerHashed, - 0n, - merkleTreeHashed, - address, - BigInt("0x" + Buffer.from(discriminator).toString("hex")), - dataHash - ); - - const expectedRoot = computeMerkleRoot(accountHash, pathElements, 0); - - console.log("Account hash:", Buffer.from(accountHash).toString("hex").slice(0, 16) + "..."); - console.log("Expected root:", Buffer.from(expectedRoot).toString("hex").slice(0, 16) + "..."); - console.log("Generating ZK proof..."); - - const zkProof = await generateMerkleProof( - ownerHashed, - merkleTreeHashed, - discriminator, - dataHash, - expectedRoot, - 0, - 0, - address, - pathElements - ); - - assert.ok(zkProof.a.length === 32, "Proof A should be 32 bytes"); - assert.ok(zkProof.b.length === 64, "Proof B should be 64 bytes"); - assert.ok(zkProof.c.length === 32, "Proof C should be 32 bytes"); - - console.log("ZK Merkle proof generated successfully"); - }); - }); -}); -``` diff --git a/snippets/code-snippets/zk/nullifier/circuit.mdx b/snippets/code-snippets/zk/nullifier/circuit.mdx deleted file mode 100644 index dc584d4..0000000 --- a/snippets/code-snippets/zk/nullifier/circuit.mdx +++ /dev/null @@ -1,32 +0,0 @@ -```javascript expandable -pragma circom 2.0.0; - -include "../node_modules/circomlib/circuits/poseidon.circom"; - -// Single nullifier: proves nullifier = Poseidon(verification_id, secret) -template Nullifier() { - signal input verification_id; - signal input nullifier; - signal input secret; - - component hasher = Poseidon(2); - hasher.inputs[0] <== verification_id; - hasher.inputs[1] <== secret; - nullifier === hasher.out; -} - -// Batch nullifier: proves n nullifiers with single proof -template BatchNullifier(n) { - signal input verification_id; - signal input nullifier[n]; - signal input secret[n]; - - component nullifiers[n]; - for (var i = 0; i < n; i++) { - nullifiers[i] = Nullifier(); - nullifiers[i].verification_id <== verification_id; - nullifiers[i].nullifier <== nullifier[i]; - nullifiers[i].secret <== secret[i]; - } -} -``` diff --git a/snippets/code-snippets/zk/nullifier/program.mdx b/snippets/code-snippets/zk/nullifier/program.mdx deleted file mode 100644 index 6163d8c..0000000 --- a/snippets/code-snippets/zk/nullifier/program.mdx +++ /dev/null @@ -1,248 +0,0 @@ -```rust expandable -#![allow(unexpected_cfgs)] -#![allow(deprecated)] - -use anchor_lang::prelude::*; -use borsh::{BorshDeserialize, BorshSerialize}; -use groth16_solana::groth16::Groth16Verifier; -use light_sdk::account::LightAccount; -use light_sdk::cpi::v2::CpiAccounts; -use light_sdk::{ - address::{v2::derive_address, NewAddressParamsAssignedPacked}, - cpi::{v2::LightSystemProgramCpi, InvokeLightSystemProgram, LightCpiInstruction}, - derive_light_cpi_signer, - instruction::{CompressedProof, PackedAddressTreeInfo, ValidityProof}, - LightDiscriminator, -}; -use light_sdk_types::CpiSigner; - -declare_id!("Bw8aty8LJY5Kg2b6djghjWGwt6cBc1tVQUoreUehvVq4"); - -pub const LIGHT_CPI_SIGNER: CpiSigner = - derive_light_cpi_signer!("Bw8aty8LJY5Kg2b6djghjWGwt6cBc1tVQUoreUehvVq4"); - -pub const NULLIFIER_PREFIX: &[u8] = b"nullifier"; - -// Customize nullifiers per tx, e.g. 1 (single) or 4 (batch) -pub const BATCH_SIZE: usize = 4; - -pub mod nullifier_1; -pub mod nullifier_batch_4; - -#[program] -pub mod zk_nullifier { - use groth16_solana::decompression::{decompress_g1, decompress_g2}; - - use super::*; - - /// Creates 1 nullifier - pub fn create_nullifier<'info>( - ctx: Context<'_, '_, '_, 'info, CreateNullifierAccounts<'info>>, - proof: ValidityProof, - address_tree_info: PackedAddressTreeInfo, - output_state_tree_index: u8, - system_accounts_offset: u8, - zk_proof: CompressedProof, - verification_id: [u8; 32], - nullifier: [u8; 32], - ) -> Result<()> { - let light_cpi_accounts = CpiAccounts::new( - ctx.accounts.signer.as_ref(), - &ctx.remaining_accounts[system_accounts_offset as usize..], - crate::LIGHT_CPI_SIGNER, - ); - - let address_tree_pubkey = address_tree_info - .get_tree_pubkey(&light_cpi_accounts) - .map_err(|_| ErrorCode::AccountNotEnoughKeys)?; - - if address_tree_pubkey.to_bytes() != light_sdk::constants::ADDRESS_TREE_V2 { - msg!("Invalid address tree"); - return Err(ProgramError::InvalidAccountData.into()); - } - - let public_inputs: [[u8; 32]; 2] = [verification_id, nullifier]; - - msg!("Decompressing proof_a..."); - let proof_a = decompress_g1(&zk_proof.a).map_err(|e| { - msg!("decompress_g1 failed for proof_a"); - let code: u32 = e.into(); - Error::from(ProgramError::Custom(code)) - })?; - - msg!("Decompressing proof_b..."); - let proof_b = decompress_g2(&zk_proof.b).map_err(|e| { - msg!("decompress_g2 failed for proof_b"); - let code: u32 = e.into(); - Error::from(ProgramError::Custom(code)) - })?; - - msg!("Decompressing proof_c..."); - let proof_c = decompress_g1(&zk_proof.c).map_err(|e| { - msg!("decompress_g1 failed for proof_c"); - let code: u32 = e.into(); - Error::from(ProgramError::Custom(code)) - })?; - - msg!("Creating verifier..."); - let mut verifier = Groth16Verifier::new( - &proof_a, - &proof_b, - &proof_c, - &public_inputs, - &crate::nullifier_1::VERIFYINGKEY, - ) - .map_err(|e| { - msg!("Groth16Verifier::new failed"); - let code: u32 = e.into(); - Error::from(ProgramError::Custom(code)) - })?; - - msg!("Verifying proof..."); - verifier.verify().map_err(|e| { - msg!("verifier.verify() failed"); - let code: u32 = e.into(); - Error::from(ProgramError::Custom(code)) - })?; - msg!("Proof verified!"); - - let (address, address_seed) = derive_address( - &[ - NULLIFIER_PREFIX, - nullifier.as_slice(), - verification_id.as_slice(), - ], - &address_tree_pubkey, - &crate::ID, - ); - - let nullifier_account = LightAccount::::new_init( - &crate::ID, - Some(address), - output_state_tree_index, - ); - - LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, proof) - .with_light_account(nullifier_account)? - .with_new_addresses(&[address_tree_info.into_new_address_params_assigned_packed(address_seed, Some(0))]) - .invoke(light_cpi_accounts)?; - - Ok(()) - } - - /// Creates 4 nullifiers with single proof - pub fn create_batch_nullifier<'info>( - ctx: Context<'_, '_, '_, 'info, CreateNullifierAccounts<'info>>, - proof: ValidityProof, - address_tree_infos: [PackedAddressTreeInfo; BATCH_SIZE], - output_state_tree_index: u8, - system_accounts_offset: u8, - zk_proof: CompressedProof, - verification_id: [u8; 32], - nullifiers: [[u8; 32]; BATCH_SIZE], - ) -> Result<()> { - let light_cpi_accounts = CpiAccounts::new( - ctx.accounts.signer.as_ref(), - &ctx.remaining_accounts[system_accounts_offset as usize..], - crate::LIGHT_CPI_SIGNER, - ); - - let address_tree_pubkey = address_tree_infos[0] - .get_tree_pubkey(&light_cpi_accounts) - .map_err(|_| ErrorCode::AccountNotEnoughKeys)?; - - if address_tree_pubkey.to_bytes() != light_sdk::constants::ADDRESS_TREE_V2 { - msg!("Invalid address tree"); - return Err(ProgramError::InvalidAccountData.into()); - } - - // 5 public inputs: verification_id + 4 nullifiers - let public_inputs: [[u8; 32]; 5] = [ - verification_id, - nullifiers[0], - nullifiers[1], - nullifiers[2], - nullifiers[3], - ]; - - let proof_a = decompress_g1(&zk_proof.a).map_err(|e| { - let code: u32 = e.into(); - Error::from(ProgramError::Custom(code)) - })?; - - let proof_b = decompress_g2(&zk_proof.b).map_err(|e| { - let code: u32 = e.into(); - Error::from(ProgramError::Custom(code)) - })?; - - let proof_c = decompress_g1(&zk_proof.c).map_err(|e| { - let code: u32 = e.into(); - Error::from(ProgramError::Custom(code)) - })?; - - let mut verifier = Groth16Verifier::new( - &proof_a, - &proof_b, - &proof_c, - &public_inputs, - &crate::nullifier_batch_4::VERIFYINGKEY, - ) - .map_err(|e| { - let code: u32 = e.into(); - Error::from(ProgramError::Custom(code)) - })?; - - verifier.verify().map_err(|e| { - let code: u32 = e.into(); - Error::from(ProgramError::Custom(code)) - })?; - - // Create 4 nullifier accounts - let mut cpi_builder = LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, proof); - let mut new_address_params: Vec = Vec::with_capacity(BATCH_SIZE); - - for i in 0..BATCH_SIZE { - let (address, address_seed) = derive_address( - &[ - NULLIFIER_PREFIX, - nullifiers[i].as_slice(), - verification_id.as_slice(), - ], - &address_tree_pubkey, - &crate::ID, - ); - - let nullifier_account = LightAccount::::new_init( - &crate::ID, - Some(address), - output_state_tree_index, - ); - - cpi_builder = cpi_builder.with_light_account(nullifier_account)?; - new_address_params - .push(address_tree_infos[i].into_new_address_params_assigned_packed(address_seed, Some(i as u8))); - } - - cpi_builder - .with_new_addresses(&new_address_params) - .invoke(light_cpi_accounts)?; - - Ok(()) - } -} - -#[derive(Accounts)] -pub struct CreateNullifierAccounts<'info> { - #[account(mut)] - pub signer: Signer<'info>, -} - -#[derive(Clone, Debug, Default, BorshSerialize, BorshDeserialize, LightDiscriminator)] -pub struct NullifierAccount {} - -#[error_code] -pub enum ErrorCode { - #[msg("Not enough keys in remaining accounts")] - AccountNotEnoughKeys, -} -``` diff --git a/snippets/code-snippets/zk/nullifier/rust-client.mdx b/snippets/code-snippets/zk/nullifier/rust-client.mdx deleted file mode 100644 index 0df5353..0000000 --- a/snippets/code-snippets/zk/nullifier/rust-client.mdx +++ /dev/null @@ -1,441 +0,0 @@ -```rust expandable -use anchor_lang::{InstructionData, ToAccountMetas}; -use circom_prover::{prover::ProofLib, witness::WitnessFn, CircomProver}; -use groth16_solana::proof_parser::circom_prover::{convert_proof, convert_proof_to_compressed}; -use light_hasher::{Hasher, Poseidon}; -use light_program_test::{ - program_test::LightProgramTest, utils::simulate_cu, AddressWithTree, Indexer, ProgramTestConfig, - Rpc, RpcError, -}; -use light_sdk::{ - address::v2::derive_address, - instruction::{PackedAccounts, SystemAccountMetaConfig}, -}; -use num_bigint::BigUint; -use solana_sdk::{ - instruction::Instruction, - pubkey::Pubkey, - signature::{Keypair, Signer}, -}; -use std::collections::HashMap; -use zk_nullifier::{BATCH_SIZE, NULLIFIER_PREFIX}; - -#[link(name = "circuit_single", kind = "static")] -extern "C" {} - -#[link(name = "circuit_batch", kind = "static")] -extern "C" {} - -rust_witness::witness!(nullifier1); -rust_witness::witness!(nullifier4); - -// ============================================================================ -// Shared helpers -// ============================================================================ - -fn generate_random_secret() -> [u8; 32] { - let random_keypair = Keypair::new(); - let mut secret = [0u8; 32]; - secret[1..32].copy_from_slice(&random_keypair.to_bytes()[0..31]); - secret -} - -fn compute_nullifier(verification_id: &[u8; 32], secret: &[u8; 32]) -> [u8; 32] { - Poseidon::hashv(&[verification_id, secret]).unwrap() -} - -fn compress_proof( - proof: &circom_prover::prover::circom::Proof, -) -> light_compressed_account::instruction_data::compressed_proof::CompressedProof { - let (proof_a_uncompressed, proof_b_uncompressed, proof_c_uncompressed) = - convert_proof(proof).expect("Failed to convert proof"); - - let (proof_a, proof_b, proof_c) = convert_proof_to_compressed( - &proof_a_uncompressed, - &proof_b_uncompressed, - &proof_c_uncompressed, - ) - .expect("Failed to compress proof"); - - light_compressed_account::instruction_data::compressed_proof::CompressedProof { - a: proof_a, - b: proof_b, - c: proof_c, - } -} - -// ============================================================================ -// Single nullifier test -// ============================================================================ - -#[tokio::test] -async fn test_create_nullifier() { - let config = ProgramTestConfig::new(true, Some(vec![("zk_nullifier", zk_nullifier::ID)])); - let mut rpc = LightProgramTest::new(config).await.unwrap(); - let payer = rpc.get_payer().insecure_clone(); - - let address_tree_info = rpc.get_address_tree_v2(); - - let secret = generate_random_secret(); - let verification_id = Pubkey::new_unique().to_bytes(); - let nullifier = compute_nullifier(&verification_id, &secret); - - let (nullifier_address, _) = derive_address( - &[ - NULLIFIER_PREFIX, - nullifier.as_slice(), - verification_id.as_slice(), - ], - &address_tree_info.tree, - &zk_nullifier::ID, - ); - - let instruction = build_create_nullifier_instruction( - &mut rpc, - &payer, - &nullifier_address, - address_tree_info.clone(), - &verification_id, - &nullifier, - &secret, - ) - .await - .unwrap(); - - let cu = simulate_cu(&mut rpc, &payer, &instruction).await; - println!("=== Single nullifier CU: {} ===", cu); - - rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) - .await - .unwrap(); - - let nullifier_accounts = rpc - .get_compressed_accounts_by_owner(&zk_nullifier::ID, None, None) - .await - .unwrap(); - assert_eq!(nullifier_accounts.value.items.len(), 1); - - // Duplicate should fail - let dup_instruction = build_create_nullifier_instruction( - &mut rpc, - &payer, - &nullifier_address, - address_tree_info, - &verification_id, - &nullifier, - &secret, - ) - .await - .unwrap(); - - let result = rpc - .create_and_send_transaction(&[dup_instruction], &payer.pubkey(), &[&payer]) - .await; - assert!(result.is_err()); -} - -async fn build_create_nullifier_instruction( - rpc: &mut R, - payer: &Keypair, - address: &[u8; 32], - address_tree_info: light_client::indexer::TreeInfo, - verification_id: &[u8; 32], - nullifier: &[u8; 32], - secret: &[u8; 32], -) -> Result -where - R: Rpc + Indexer, -{ - let mut remaining_accounts = PackedAccounts::default(); - remaining_accounts.add_pre_accounts_signer(payer.pubkey()); - let config = SystemAccountMetaConfig::new(zk_nullifier::ID); - remaining_accounts.add_system_accounts_v2(config)?; - - let rpc_result = rpc - .get_validity_proof( - vec![], - vec![AddressWithTree { - address: *address, - tree: address_tree_info.tree, - }], - None, - ) - .await? - .value; - - let packed_address_tree_accounts = rpc_result - .pack_tree_infos(&mut remaining_accounts) - .address_trees; - - let output_state_tree_index = rpc - .get_random_state_tree_info()? - .pack_output_tree_index(&mut remaining_accounts)?; - - let zk_proof = generate_single_zk_proof(verification_id, nullifier, secret); - - let (remaining_accounts_metas, system_accounts_offset, _) = remaining_accounts.to_account_metas(); - - let instruction_data = zk_nullifier::instruction::CreateNullifier { - proof: rpc_result.proof, - address_tree_info: packed_address_tree_accounts[0], - output_state_tree_index, - system_accounts_offset: system_accounts_offset as u8, - zk_proof, - verification_id: *verification_id, - nullifier: *nullifier, - }; - - let accounts = zk_nullifier::accounts::CreateNullifierAccounts { - signer: payer.pubkey(), - }; - - Ok(Instruction { - program_id: zk_nullifier::ID, - accounts: [ - accounts.to_account_metas(None), - remaining_accounts_metas, - ] - .concat(), - data: instruction_data.data(), - }) -} - -fn generate_single_zk_proof( - verification_id: &[u8; 32], - nullifier: &[u8; 32], - secret: &[u8; 32], -) -> light_compressed_account::instruction_data::compressed_proof::CompressedProof { - let manifest_dir = env!("CARGO_MANIFEST_DIR"); - let zkey_path = format!("{}/../../build/nullifier_1_final.zkey", manifest_dir); - - let mut proof_inputs = HashMap::new(); - proof_inputs.insert( - "verification_id".to_string(), - vec![BigUint::from_bytes_be(verification_id).to_string()], - ); - proof_inputs.insert( - "nullifier".to_string(), - vec![BigUint::from_bytes_be(nullifier).to_string()], - ); - proof_inputs.insert( - "secret".to_string(), - vec![BigUint::from_bytes_be(secret).to_string()], - ); - - let circuit_inputs = serde_json::to_string(&proof_inputs).unwrap(); - let proof = CircomProver::prove( - ProofLib::Arkworks, - WitnessFn::RustWitness(nullifier1_witness), - circuit_inputs, - zkey_path.clone(), - ) - .expect("Proof generation failed"); - - let is_valid = CircomProver::verify(ProofLib::Arkworks, proof.clone(), zkey_path) - .expect("Proof verification failed"); - assert!(is_valid); - - compress_proof(&proof.proof) -} - -// ============================================================================ -// Batch nullifier test -// ============================================================================ - -#[tokio::test] -async fn test_create_batch_nullifier() { - let config = ProgramTestConfig::new(true, Some(vec![("zk_nullifier", zk_nullifier::ID)])); - let mut rpc = LightProgramTest::new(config).await.unwrap(); - let payer = rpc.get_payer().insecure_clone(); - - let address_tree_info = rpc.get_address_tree_v2(); - - let secrets: [[u8; 32]; BATCH_SIZE] = [ - generate_random_secret(), - generate_random_secret(), - generate_random_secret(), - generate_random_secret(), - ]; - let verification_id = Pubkey::new_unique().to_bytes(); - let nullifiers: [[u8; 32]; BATCH_SIZE] = [ - compute_nullifier(&verification_id, &secrets[0]), - compute_nullifier(&verification_id, &secrets[1]), - compute_nullifier(&verification_id, &secrets[2]), - compute_nullifier(&verification_id, &secrets[3]), - ]; - - let mut addresses = Vec::with_capacity(BATCH_SIZE); - for i in 0..BATCH_SIZE { - let (addr, _) = derive_address( - &[ - NULLIFIER_PREFIX, - nullifiers[i].as_slice(), - verification_id.as_slice(), - ], - &address_tree_info.tree, - &zk_nullifier::ID, - ); - addresses.push(addr); - } - - let instruction = build_create_batch_nullifier_instruction( - &mut rpc, - &payer, - &addresses, - address_tree_info.clone(), - &verification_id, - &nullifiers, - &secrets, - ) - .await - .unwrap(); - - let cu = simulate_cu(&mut rpc, &payer, &instruction).await; - println!("=== Batch (4 nullifiers) CU: {} ===", cu); - println!("=== CU per nullifier (batch): {} ===", cu / 4); - - rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) - .await - .unwrap(); - - let nullifier_accounts = rpc - .get_compressed_accounts_by_owner(&zk_nullifier::ID, None, None) - .await - .unwrap(); - assert_eq!(nullifier_accounts.value.items.len(), BATCH_SIZE); - - // Duplicate batch should fail - let dup_instruction = build_create_batch_nullifier_instruction( - &mut rpc, - &payer, - &addresses, - address_tree_info, - &verification_id, - &nullifiers, - &secrets, - ) - .await - .unwrap(); - - let result = rpc - .create_and_send_transaction(&[dup_instruction], &payer.pubkey(), &[&payer]) - .await; - assert!(result.is_err()); -} - -async fn build_create_batch_nullifier_instruction( - rpc: &mut R, - payer: &Keypair, - addresses: &[[u8; 32]], - address_tree_info: light_client::indexer::TreeInfo, - verification_id: &[u8; 32], - nullifiers: &[[u8; 32]; BATCH_SIZE], - secrets: &[[u8; 32]; BATCH_SIZE], -) -> Result -where - R: Rpc + Indexer, -{ - let mut remaining_accounts = PackedAccounts::default(); - remaining_accounts.add_pre_accounts_signer(payer.pubkey()); - let config = SystemAccountMetaConfig::new(zk_nullifier::ID); - remaining_accounts.add_system_accounts_v2(config)?; - - let address_with_trees: Vec = addresses - .iter() - .map(|addr| AddressWithTree { - address: *addr, - tree: address_tree_info.tree, - }) - .collect(); - - let rpc_result = rpc - .get_validity_proof(vec![], address_with_trees, None) - .await? - .value; - - let packed_address_tree_accounts = rpc_result - .pack_tree_infos(&mut remaining_accounts) - .address_trees; - - let output_state_tree_index = rpc - .get_random_state_tree_info()? - .pack_output_tree_index(&mut remaining_accounts)?; - - let zk_proof = generate_batch_zk_proof(verification_id, nullifiers, secrets); - - let address_tree_infos: [_; BATCH_SIZE] = [ - packed_address_tree_accounts[0], - packed_address_tree_accounts[1], - packed_address_tree_accounts[2], - packed_address_tree_accounts[3], - ]; - - let (remaining_accounts_metas, system_accounts_offset, _) = remaining_accounts.to_account_metas(); - - let instruction_data = zk_nullifier::instruction::CreateBatchNullifier { - proof: rpc_result.proof, - address_tree_infos, - output_state_tree_index, - system_accounts_offset: system_accounts_offset as u8, - zk_proof, - verification_id: *verification_id, - nullifiers: *nullifiers, - }; - - let accounts = zk_nullifier::accounts::CreateNullifierAccounts { - signer: payer.pubkey(), - }; - - Ok(Instruction { - program_id: zk_nullifier::ID, - accounts: [ - accounts.to_account_metas(None), - remaining_accounts_metas, - ] - .concat(), - data: instruction_data.data(), - }) -} - -fn generate_batch_zk_proof( - verification_id: &[u8; 32], - nullifiers: &[[u8; 32]; BATCH_SIZE], - secrets: &[[u8; 32]; BATCH_SIZE], -) -> light_compressed_account::instruction_data::compressed_proof::CompressedProof { - let manifest_dir = env!("CARGO_MANIFEST_DIR"); - let zkey_path = format!("{}/../../build/nullifier_4_final.zkey", manifest_dir); - - let mut proof_inputs = HashMap::new(); - proof_inputs.insert( - "verification_id".to_string(), - vec![BigUint::from_bytes_be(verification_id).to_string()], - ); - - let nullifier_strings: Vec = nullifiers - .iter() - .map(|n| BigUint::from_bytes_be(n).to_string()) - .collect(); - proof_inputs.insert("nullifier".to_string(), nullifier_strings); - - let secret_strings: Vec = secrets - .iter() - .map(|s| BigUint::from_bytes_be(s).to_string()) - .collect(); - proof_inputs.insert("secret".to_string(), secret_strings); - - let circuit_inputs = serde_json::to_string(&proof_inputs).unwrap(); - let proof = CircomProver::prove( - ProofLib::Arkworks, - WitnessFn::RustWitness(nullifier4_witness), - circuit_inputs, - zkey_path.clone(), - ) - .expect("Proof generation failed"); - - let is_valid = CircomProver::verify(ProofLib::Arkworks, proof.clone(), zkey_path) - .expect("Proof verification failed"); - assert!(is_valid); - - compress_proof(&proof.proof) -} -``` diff --git a/snippets/code-snippets/zk/nullifier/typescript-client.mdx b/snippets/code-snippets/zk/nullifier/typescript-client.mdx deleted file mode 100644 index e066380..0000000 --- a/snippets/code-snippets/zk/nullifier/typescript-client.mdx +++ /dev/null @@ -1,312 +0,0 @@ -```typescript expandable -import { web3, Program, AnchorProvider, setProvider } from "@coral-xyz/anchor"; -import { - bn, - createRpc, - deriveAddressSeedV2, - deriveAddressV2, - batchAddressTree, - PackedAccounts, - Rpc, - sleep, - SystemAccountMetaConfig, - defaultTestStateTreeAccounts, - featureFlags, - VERSION, - confirmTx, -} from "@lightprotocol/stateless.js"; -import { buildPoseidonOpt } from "circomlibjs"; -import * as snarkjs from "snarkjs"; -import * as assert from "assert"; -import * as path from "path"; -import * as fs from "fs"; - -import { - parseProofToCompressed, - bigintToBytes32, - toFieldString, - generateFieldElement, -} from "./utils/proof-helpers"; - -// Force V2 mode -(featureFlags as any).version = VERSION.V2; - -// Load IDL -const IDL = JSON.parse( - fs.readFileSync(path.join(process.cwd(), "target/idl/zk_nullifier.json"), "utf8") -); - -const PROGRAM_ID = new web3.PublicKey(IDL.address); -const NULLIFIER_PREFIX = Buffer.from("nullifier"); - -// Paths to circuit artifacts -const BUILD_DIR = path.join(process.cwd(), "build"); -const WASM_PATH_SINGLE = path.join(BUILD_DIR, "nullifier_1_js/nullifier_1.wasm"); -const ZKEY_PATH_SINGLE = path.join(BUILD_DIR, "nullifier_1_final.zkey"); -const VKEY_PATH_SINGLE = path.join(BUILD_DIR, "nullifier_1_verification_key.json"); -const WASM_PATH_BATCH = path.join(BUILD_DIR, "nullifier_4_js/nullifier_4.wasm"); -const ZKEY_PATH_BATCH = path.join(BUILD_DIR, "nullifier_4_final.zkey"); - -describe("zk-nullifier", () => { - let rpc: Rpc; - let signer: web3.Keypair; - let poseidon: any; - let program: Program; - - before(async () => { - rpc = createRpc( - "http://127.0.0.1:8899", - "http://127.0.0.1:8784", - "http://127.0.0.1:3001", - { commitment: "confirmed" } - ); - - signer = web3.Keypair.generate(); - await rpc.requestAirdrop(signer.publicKey, web3.LAMPORTS_PER_SOL); - await sleep(2000); - - poseidon = await buildPoseidonOpt(); - - // Setup Anchor provider and program - const connection = new web3.Connection("http://127.0.0.1:8899", "confirmed"); - const wallet = { - publicKey: signer.publicKey, - signTransaction: async (tx: web3.Transaction) => { - tx.sign(signer); - return tx; - }, - signAllTransactions: async (txs: web3.Transaction[]) => { - txs.forEach((tx) => tx.sign(signer)); - return txs; - }, - }; - const provider = new AnchorProvider(connection, wallet as any, { commitment: "confirmed" }); - setProvider(provider); - program = new Program(IDL, provider); - }); - - after(async () => { - // Terminate snarkjs curve worker to allow clean exit - // @ts-ignore - if (globalThis.curve_bn128) { - // @ts-ignore - await globalThis.curve_bn128.terminate(); - } - }); - - /** Compute nullifier = Poseidon(verification_id, secret) */ - function computeNullifier(verificationId: Uint8Array, secret: Uint8Array): Uint8Array { - const hash = poseidon([toFieldString(verificationId), toFieldString(secret)].map(BigInt)); - return bigintToBytes32(poseidon.F.toObject(hash)); - } - - /** Generate Groth16 proof for single nullifier */ - async function generateProof( - verificationId: Uint8Array, - nullifier: Uint8Array, - secret: Uint8Array - ): Promise<{ a: number[]; b: number[]; c: number[] }> { - const inputs = { - verification_id: toFieldString(verificationId), - nullifier: toFieldString(nullifier), - secret: toFieldString(secret), - }; - - const { proof, publicSignals } = await snarkjs.groth16.fullProve(inputs, WASM_PATH_SINGLE, ZKEY_PATH_SINGLE); - - // Verify locally with snarkjs before converting - const vkey = JSON.parse(fs.readFileSync(VKEY_PATH_SINGLE, "utf8")); - const isValid = await snarkjs.groth16.verify(vkey, publicSignals, proof); - console.log("Local snarkjs verification:", isValid); - console.log("Public signals:", publicSignals); - - // Use prover.js logic for proof conversion - const compressed = parseProofToCompressed(proof); - - console.log("Compressed proof a (first 8 bytes):", compressed.a.slice(0, 8)); - console.log("Compressed proof b (first 8 bytes):", compressed.b.slice(0, 8)); - console.log("Compressed proof c (first 8 bytes):", compressed.c.slice(0, 8)); - - return compressed; - } - - /** Generate Groth16 proof for batch (4) nullifiers */ - async function generateBatchProof( - verificationId: Uint8Array, - nullifiers: Uint8Array[], - secrets: Uint8Array[] - ): Promise<{ a: number[]; b: number[]; c: number[] }> { - const inputs = { - verification_id: toFieldString(verificationId), - nullifier: nullifiers.map(toFieldString), - secret: secrets.map(toFieldString), - }; - - const { proof } = await snarkjs.groth16.fullProve(inputs, WASM_PATH_BATCH, ZKEY_PATH_BATCH); - return parseProofToCompressed(proof); - } - - /** Build create_nullifier instruction using Anchor */ - async function buildCreateNullifierInstruction( - verificationId: Uint8Array, - nullifier: Uint8Array, - secret: Uint8Array - ): Promise { - const addressTree = new web3.PublicKey(batchAddressTree); - const outputStateTree = defaultTestStateTreeAccounts().merkleTree; - - const seed = deriveAddressSeedV2([NULLIFIER_PREFIX, nullifier, verificationId]); - const address = deriveAddressV2(seed, addressTree, PROGRAM_ID); - - const proofResult = await rpc.getValidityProofV0( - [], - [{ tree: addressTree, queue: addressTree, address: bn(address.toBytes()) }] - ); - - // Use V2 accounts layout (matches on-chain CpiAccounts::new from light_sdk::cpi::v2) - const remainingAccounts = new PackedAccounts(); - remainingAccounts.addPreAccountsSigner(signer.publicKey); - remainingAccounts.addSystemAccountsV2(SystemAccountMetaConfig.new(PROGRAM_ID)); - - const addressMerkleTreeIndex = remainingAccounts.insertOrGet(addressTree); - const outputStateTreeIndex = remainingAccounts.insertOrGet(outputStateTree); - - const zkProof = await generateProof(verificationId, nullifier, secret); - - // Get system_accounts_offset from packed accounts - const { remainingAccounts: accountMetas, systemStart } = remainingAccounts.toAccountMetas(); - - // Use Anchor to build instruction - // ValidityProof is a struct with an unnamed Option field - // Anchor JS client uses index-based access for unnamed tuple/option fields - const proof = { - 0: proofResult.compressedProof, - }; - - const ix = await program.methods - .createNullifier( - // proof (ValidityProof = struct with Option) - proof, - // address_tree_info (PackedAddressTreeInfo) - { - addressMerkleTreePubkeyIndex: addressMerkleTreeIndex, - addressQueuePubkeyIndex: addressMerkleTreeIndex, - rootIndex: proofResult.rootIndices[0], - }, - // output_state_tree_index - outputStateTreeIndex, - // system_accounts_offset - systemStart, - // zk_proof (CompressedProof) - { - a: zkProof.a, - b: zkProof.b, - c: zkProof.c, - }, - // verification_id - Array.from(verificationId), - // nullifier - Array.from(nullifier) - ) - .accounts({ - signer: signer.publicKey, - }) - .remainingAccounts(accountMetas) - .instruction(); - - return ix; - } - - describe("Single nullifier", () => { - it("should create a nullifier with valid ZK proof", async () => { - // Use generateFieldElement for verificationId to ensure it's in BN254 field - const verificationId = generateFieldElement(); - const secret = generateFieldElement(); - const nullifier = computeNullifier(verificationId, secret); - - console.log("Verification ID:", Buffer.from(verificationId).toString("hex").slice(0, 16) + "..."); - console.log("Nullifier:", Buffer.from(nullifier).toString("hex").slice(0, 16) + "..."); - - // Debug: Check if values are within BN254 field - const BN254_FR = BigInt('21888242871839275222246405745257275088548364400416034343698204186575808495617'); - const verIdBigInt = BigInt("0x" + Buffer.from(verificationId).toString("hex")); - const nullifierBigInt = BigInt("0x" + Buffer.from(nullifier).toString("hex")); - console.log("verificationId < Fr:", verIdBigInt < BN254_FR, "value:", verIdBigInt.toString().slice(0, 20) + "..."); - console.log("nullifier < Fr:", nullifierBigInt < BN254_FR, "value:", nullifierBigInt.toString().slice(0, 20) + "..."); - - const ix = await buildCreateNullifierInstruction(verificationId, nullifier, secret); - const computeIx = web3.ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 }); - - const tx = new web3.Transaction().add(computeIx, ix); - tx.recentBlockhash = (await rpc.getLatestBlockhash()).blockhash; - tx.feePayer = signer.publicKey; - tx.sign(signer); - - const sig = await rpc.sendTransaction(tx, [signer]); - await confirmTx(rpc, sig); - - console.log("Transaction signature:", sig); - - const slot = await rpc.getSlot(); - await rpc.confirmTransactionIndexed(slot); - - const accounts = await rpc.getCompressedAccountsByOwner(PROGRAM_ID); - assert.ok(accounts.items.length > 0, "Nullifier account should be created"); - console.log("Created nullifier accounts:", accounts.items.length); - }); - - it("should reject duplicate nullifier", async () => { - // Use generateFieldElement for verificationId to ensure it's in BN254 field - const verificationId = generateFieldElement(); - const secret = generateFieldElement(); - const nullifier = computeNullifier(verificationId, secret); - - const ix1 = await buildCreateNullifierInstruction(verificationId, nullifier, secret); - const computeIx = web3.ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 }); - - const tx1 = new web3.Transaction().add(computeIx, ix1); - tx1.recentBlockhash = (await rpc.getLatestBlockhash()).blockhash; - tx1.feePayer = signer.publicKey; - tx1.sign(signer); - - await rpc.sendTransaction(tx1, [signer]); - await sleep(2000); - - // Attempt to create duplicate - should fail when getting validity proof - // because the address already exists in the tree - try { - await buildCreateNullifierInstruction(verificationId, nullifier, secret); - assert.fail("Should have rejected duplicate nullifier"); - } catch (err: any) { - // The error should indicate the address already exists - assert.ok( - err.message.includes("already exists"), - `Expected 'already exists' error, got: ${err.message}` - ); - console.log("Duplicate correctly rejected:", err.message); - } - }); - }); - - describe("Batch nullifier (4x)", () => { - it("should create 4 nullifiers with single proof", async () => { - // Use generateFieldElement for verificationId to ensure it's in BN254 field - const verificationId = generateFieldElement(); - const secrets = Array.from({ length: 4 }, generateFieldElement); - const nullifiers = secrets.map((s) => computeNullifier(verificationId, s)); - - console.log("Creating batch of 4 nullifiers..."); - console.log("Verification ID:", Buffer.from(verificationId).toString("hex").slice(0, 16) + "..."); - - const zkProof = await generateBatchProof(verificationId, nullifiers, secrets); - console.log("Batch proof generated"); - - assert.ok(zkProof.a.length === 32, "Proof A should be 32 bytes"); - assert.ok(zkProof.b.length === 64, "Proof B should be 64 bytes"); - assert.ok(zkProof.c.length === 32, "Proof C should be 32 bytes"); - - console.log("Batch proof verified locally"); - }); - }); -}); -``` diff --git a/zk/compressed-account-zk.mdx b/zk/compressed-account-zk.mdx deleted file mode 100644 index b26dd83..0000000 --- a/zk/compressed-account-zk.mdx +++ /dev/null @@ -1,507 +0,0 @@ ---- -title: Compressed Accounts for ZK Applications -description: ZK applications use Poseidon Hashes instead of SHA-256 to store state in compressed accounts compatible with zero-knowledge circuits. -keywords: ["compressed accounts zk", "poseidon hashing solana", "groth16 solana", "privacy solana"] ---- - -import ZkAppPrompt from "/snippets/ai-prompts/zk-app.mdx"; -import MerkleProofCircuit from '/snippets/code-snippets/zk/merkle-proof/circuit.mdx'; -import MerkleProofProgram from '/snippets/code-snippets/zk/merkle-proof/program.mdx'; -import MerkleProofRustClient from '/snippets/code-snippets/zk/merkle-proof/rust-client.mdx'; -import MerkleProofTypescriptClient from '/snippets/code-snippets/zk/merkle-proof/typescript-client.mdx'; - -* Compressed and regular Solana accounts share the same functionality and are fully composable. -* The account state is hashed with Poseidon and stored as a commitment in the state Merkle tree. -* RPCs that support ZK Compression (Helius, Triton) index state changes. - - -Each tab below includes a full code example. See the full implementation at [program-examples/zk/zk-merkle-proof](https://github.com/Lightprotocol/program-examples/tree/main/zk/zk-merkle-proof). - - -## Implementation guide - - - - - - -### Dependencies - -Add dependencies to your program. - -```toml -[dependencies] -anchor-lang = "0.31.1" -borsh = "0.10.4" -light-sdk = { version = "0.17.1", features = ["anchor", "poseidon", "merkle-tree", "v2"] } -light-hasher = "5.0.0" -light-sdk-types = { version = "0.17.1", features = ["v2"] } -``` - -* `light-sdk` with `poseidon` feature enables ZK-friendly hashing. -* `light-hasher` provides Poseidon hash functions. - - - - - -### Constants - -Set program address, CPI signer, and include the verifying key from your circuit setup. - -```rust -declare_id!("MPzkYomvQc4VQPwMr6bFduyWRQZVCh5CofgDC4dFqJp"); - -pub const LIGHT_CPI_SIGNER: CpiSigner = - derive_light_cpi_signer!("MPzkYomvQc4VQPwMr6bFduyWRQZVCh5CofgDC4dFqJp"); - -pub const ZK_ACCOUNT: &[u8] = b"zk_account"; - -// Generated from circom trusted setup (.zkey file) -pub mod verifying_key; -``` - - - - - -### Compressed account - - -```rust -#[derive(Clone, Debug, Default, BorshSerialize, BorshDeserialize, LightDiscriminator, LightHasher)] -pub struct ZkAccount { - pub data_hash: ZkDataHash, -} -``` - -**ZK-specific differences from regular compressed accounts:** - -* Derive `LightHasher` for Poseidon hashing (instead of default SHA-256). - -**Custom types need `ToByteArray`:** - -```rust -#[derive(Clone, Debug, Default, BorshSerialize, BorshDeserialize)] -pub struct ZkDataHash(pub [u8; 32]); - -impl ToByteArray for ZkDataHash { - const NUM_FIELDS: usize = 1; - fn to_byte_array(&self) -> std::result::Result<[u8; 32], HasherError> { - Ok(self.0) - } -} -``` - - - - - -### Instruction data - -Define the instruction parameters. - -```rust -pub fn create_account<'info>( - ctx: Context<'_, '_, '_, 'info, CreateAccountAccounts<'info>>, - proof: ValidityProof, - address_tree_info: PackedAddressTreeInfo, - output_state_tree_index: u8, - data_hash: [u8; 32], -) -> Result<()> -``` - -1. `proof`: Proves the address does not exist yet. -2. `address_tree_info`: References the address tree for deriving the address. -3. `output_state_tree_index`: References the state tree for storing the account hash. -4. `data_hash`: The Poseidon hash of the data to store. - - - - - -### Derive address - -Derive the address as a persistent unique identifier. - -```rust -let address_tree_pubkey = address_tree_info - .get_tree_pubkey(&light_cpi_accounts) - .map_err(|_| ProgramError::InvalidAccountData)?; - -let (address, address_seed) = derive_address( - &[ZK_ACCOUNT, &data_hash], - &address_tree_pubkey, - &crate::ID, -); -``` - - - - - -### Address tree check - -Ensure global uniqueness by verifying the address tree pubkey. - -```rust -if address_tree_pubkey.to_bytes() != light_sdk::constants::ADDRESS_TREE_V2 { - msg!("Invalid address tree"); - return Err(ProgramError::InvalidAccountData.into()); -} -``` - - - - - -### Initialize compressed account - - -```rust -let mut account = LightAccountPoseidon::::new_init( - &crate::ID, - Some(address), - output_state_tree_index, -); - -account.data_hash = ZkDataHash(data_hash); -``` - - - - - - -### Light System Program CPI - - -```rust -let light_cpi_accounts = CpiAccounts::new( - ctx.accounts.signer.as_ref(), - ctx.remaining_accounts, - crate::LIGHT_CPI_SIGNER, -); - -LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, proof) - .with_light_account_poseidon(account)? - .with_new_addresses(&[address_tree_info.into_new_address_params_packed(address_seed)]) - .invoke(light_cpi_accounts)?; -``` - - - -## Full code example - - - - - - - - - - - - - - - - - - - - - - -ZK clients follow the same pattern as [regular compressed account clients](/client-library/client-guide), with additional steps for generating Groth16 proofs. - -## Key points - -1. **Derive a new address** or **fetch compressed account** for on-chain verification. -2. **Fetch validity proof** from the RPC that verifies a new address does not exist in the address tree. -3. **Pack accounts** with the SDKs helper. Instructions require Light System Program and Merkle tree accounts. `PackedAccounts` converts their pubkeys to `u8` indices pointing to accounts in the instruction. -4. **Build the instruction** with the current account data, new data, packed accounts and validity proof. -5. **Generate ZK proof** with snarkjs to generate a Groth16 proof client-side that verifies account state without revealing the Merkle path. -6. **Build the instruction** with the proof, packed accounts, and account data. - -## Get started - - - - -### Setup - - - - -#### 1. Installation - -```bash -npm install \ - @lightprotocol/stateless.js@0.22.1-alpha.1 \ - @solana/web3.js \ - snarkjs \ - circomlibjs \ - @noble/hashes -``` - -| ZK Dependencies | Purpose | -|---------|---------| -| `snarkjs` | Groth16 proof generation | -| `circomlibjs` | Poseidon hasher for computing account hashes | -| `@noble/hashes` | Keccak-256 for BN254 field hashing | - -#### 2. Build circuit - -```bash -# Compile circuit -circom circuits/merkle_proof.circom --r1cs --wasm --sym -o build - -# Trusted setup -snarkjs groth16 setup build/merkle_proof.r1cs pot12_final.ptau build/merkle_proof_0000.zkey -snarkjs zkey contribute build/merkle_proof_0000.zkey build/merkle_proof_final.zkey -``` - -| File | Purpose | -|------|---------| -| `merkle_proof.wasm` | Compiled circuit for witness generation | -| `merkle_proof_final.zkey` | Proving key from trusted setup | - - - - - - - - -### ZK proof - -For ZK verification, generate the proof client-side instead of fetching from RPC. - -**Step 1: Fetch Merkle proof from indexer** - -```typescript -const account = await rpc.getCompressedAccount(bn(address.toBytes())); -const merkleProofs = await rpc.getMultipleCompressedAccountProofs([account.hash]); -``` - -**Step 2: Hash inputs to BN254 field** - -```typescript -function hashToBn254Field(data: Uint8Array): Uint8Array { - const hash = keccak_256(data); - hash[0] = hash[0] & 0x1f; // < 2^254 - return hash; -} - -const ownerHashed = hashToBn254Field(programId.toBytes()); -const merkleTreeHashed = hashToBn254Field(merkleTreePubkey.toBytes()); -``` - -**Step 3: Compute account hash (Poseidon)** - -```typescript -const LAMPORTS_OFFSET = 36893488147419103232n; -const accountHash = poseidon([ - ownerHashed, - leafIndex, - merkleTreeHashed, - address, - discriminator + LAMPORTS_OFFSET, - dataHash -]); -``` - -**Step 4: Prepare circuit inputs** - -```typescript -const inputs = { - // Public inputs (verified on-chain) - owner_hashed: toFieldString(ownerHashed), - merkle_tree_hashed: toFieldString(merkleTreeHashed), - discriminator: toFieldString(discriminator), - data_hash: toFieldString(dataHash), - expectedRoot: toFieldString(merkleRoot), - - // Private inputs (hidden in proof) - leaf_index: leafIndex.toString(), - account_leaf_index: accountLeafIndex.toString(), - address: toFieldString(address), - pathElements: pathElements.map(toFieldString), -}; -``` - -**Step 5: Generate Groth16 proof** - -```typescript -const { proof } = await snarkjs.groth16.fullProve( - inputs, - "build/merkle_proof_js/merkle_proof.wasm", - "build/merkle_proof_final.zkey" -); -``` - -**Step 6: Compress proof for on-chain** - -```typescript -const compressedProof = parseProofToCompressed(proof); -// Returns: { a: 32 bytes, b: 64 bytes, c: 32 bytes } -``` - - - - - -### Instruction data - - - - -```typescript -const instructionData = { - proof: { 0: proofResult.compressedProof }, - addressTreeInfo: { - addressMerkleTreePubkeyIndex: addressMerkleTreeIndex, - addressQueuePubkeyIndex: addressMerkleTreeIndex, - rootIndex: proofResult.rootIndices[0], - }, - outputStateTreeIndex, - dataHash: Array.from(dataHash), -}; -``` - - - - - -```typescript -const instructionData = { - inputRootIndex: merkleProof.rootIndex, - zkProof: compressedProof, // { a: [...], b: [...], c: [...] } - dataHash: Array.from(dataHash), -}; -``` - -The compressed proof format: -- `a`: 32 bytes (G1 X coordinate + sign bit) -- `b`: 64 bytes (G2 X coordinates + sign bit) -- `c`: 32 bytes (G1 X coordinate + sign bit) - - - - - - - - -### Instruction - -Build the instruction with Anchor. - -```typescript -const instruction = await program.methods - .createAccount( - proof, - addressTreeInfo, - outputStateTreeIndex, - systemStart, - Array.from(dataHash) - ) - .accounts({ signer: signer.publicKey }) - .remainingAccounts(remainingAccounts) - .instruction(); -``` - - - - - -### Send transaction - -```typescript -const tx = new web3.Transaction().add(computeBudgetIx, instruction); -tx.recentBlockhash = (await rpc.getLatestBlockhash()).blockhash; -tx.feePayer = signer.publicKey; -tx.sign(signer); - -const sig = await rpc.sendTransaction(tx, [signer]); -``` - - - - -## Full code example - - - - - - - - - - - - - - - - - - - - - -This is an example circuit to include compressed accounts. - - - -## Full code example - - - - - - - - - - - - - - - -For AI assistance with your ZK App, copy this prompt and add your design ideas: - - - -## Full code example - - - - - - - - - - - - - - - - - - -## Next steps - - \ No newline at end of file diff --git a/zk/nullifiers.mdx b/zk/nullifiers.mdx deleted file mode 100644 index 8d837dc..0000000 --- a/zk/nullifiers.mdx +++ /dev/null @@ -1,145 +0,0 @@ ---- -title: Nullifier Guide -description: "Nullifiers prevent double-spending of state without revealing which state was spent." ---- - -import ZkAppPrompt from "/snippets/ai-prompts/zk-app.mdx"; -import NullifierCircuit from '/snippets/code-snippets/zk/nullifier/circuit.mdx'; -import NullifierProgram from '/snippets/code-snippets/zk/nullifier/program.mdx'; -import NullifierRustClient from '/snippets/code-snippets/zk/nullifier/rust-client.mdx'; -import NullifierTypescriptClient from '/snippets/code-snippets/zk/nullifier/typescript-client.mdx'; - ---- - -* On Solana nullifiers require a data structure that ensures the nullifier is only created once. -* A straight forward way is to derive a PDA with the nullifier as seed for the PDA account. -* Nullifier accounts must remain active, hence lock ~0.001 SOL in rent per nullifier PDA permanently. -* A rent-free alternative are compressed addresses. It can be be created once and is derived from a seed similar to a pda. This way you can insert nullifiers at a fraction of the cost to create a PDA. - -| Storage | Cost per nullifier | -|---------|-------------------| -| PDA | ~0.001 SOL | -| Compressed PDA | ~0.000005 SOL | - - -To create the compressed PDA and a CPI to the Light system program you fetch an additional ZK proof from your RPC. -If you're already generating a ZK proof for your application logic, the marginal cost of the extra proof is low. - - -## Implementation Guide - -This is the complete flow of how nullifiers are used in zk applications. - - -Each tab includes a full code example section at the bottom. - - - - - - -### Client computes the nullifier - -The nullifier combines a context (e.g., `verification_id`) with the user's secret: - -```rust -fn compute_nullifier( - verification_id: &[u8; 32], - secret: &[u8; 32], -) -> [u8; 32] { - Poseidon::hashv(&[verification_id, secret]).unwrap() -} -``` - - - - -### Derive address from nullifier on-chain - -The program derives a deterministic address from the nullifier: - -```rust -let (address, address_seed) = derive_address( - &[ - NULLIFIER_PREFIX, // prefix - nullifier.as_slice(), // nullifier hash - verification_id.as_slice(), // context - ], - &address_tree_pubkey, - &crate::ID, -); -``` - - - -### Create account at that address - -Create a compressed account at the derived address: - -```rust -let nullifier_account = LightAccount::::new_init( - &crate::ID, - Some(address), - output_state_tree_index, -); -``` - -The nullifier now prevents double spending: -1. Same secret + same context = same nullifier -2. Same nullifier = same derived address -3. Address already exists = transaction fails - - - - -## Full code example - - - - - - - - - - - - - - - - - - - - -For AI assistance with your ZK App, copy this prompt and add your design ideas: - - - -## Full code example - - - - - - - - - - - - - - - - -## Next Steps - - \ No newline at end of file diff --git a/zk/overview.mdx b/zk/overview.mdx index db70529..9e7d68c 100644 --- a/zk/overview.mdx +++ b/zk/overview.mdx @@ -23,9 +23,9 @@ Attempting to consume state twice requires the same nullifier, hence the transac For example Zcash uses nullifiers to prevent double spending. * On Solana nullifiers require a data structure that ensures the nullifier is only created once. -* A straight forward way is to derive a PDA with the nullifier as seed for the PDA account. +* On Solana a straight forward way is to derive a PDA with the nullifier as seed for the PDA account. * Nullifier accounts must remain active, hence lock ~0.001 SOL in rent per nullifier PDA permanently. -* Compressed addresses are rent-free, provide similar functionality and derivation while not requiring a custom circuit. +* Compressed PDAs are rent-free, provide similar functionality and derivation while not requiring a custom circuit. | Storage | Cost per nullifier | |---------|-------------------| From d70b3621fe0c7b05e962ac4751fae22628f0dea9 Mon Sep 17 00:00:00 2001 From: tilo-14 Date: Thu, 15 Jan 2026 03:10:04 +0000 Subject: [PATCH 5/5] add zk overview --- .../overview-tables/zk-examples-table.mdx | 2 +- zk/overview.mdx | 33 +++++++++++-------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/snippets/overview-tables/zk-examples-table.mdx b/snippets/overview-tables/zk-examples-table.mdx index c599b00..faf4bb3 100644 --- a/snippets/overview-tables/zk-examples-table.mdx +++ b/snippets/overview-tables/zk-examples-table.mdx @@ -1,4 +1,4 @@ | | Description | |:--------|:------------| | [ZK-ID](https://github.com/Lightprotocol/program-examples/tree/main/zk/zk-id) | Identity verification using Groth16 proofs. Issuers create credentials; users prove ownership without revealing the credential. | -| [Nullifier](https://github.com/Lightprotocol/program-examples/tree/main/zk/zk-nullifier) | Simple Program to Create Nullifiers. Requires no custom circuit. | +| [Nullifier](https://github.com/Lightprotocol/program-examples/tree/main/zk/zk-nullifier) | Simple Program to Create Nullifiers. | diff --git a/zk/overview.mdx b/zk/overview.mdx index 9e7d68c..969e82b 100644 --- a/zk/overview.mdx +++ b/zk/overview.mdx @@ -12,28 +12,28 @@ import ZkExamplesTable from "/snippets/overview-tables/zk-examples-table.mdx"; Building a ZK Solana program requires: 1. Nullifiers to prevent double spending 2. Proof verification -3. A Merkle tree to store state, -4. An indexer to serve Merkle proofs, and -5. Encrypted state. +3. A Merkle tree to store state +4. An indexer to serve Merkle proofs +5. Encrypted state ## Nullifiers on Solana -A nullifier is a deterministically derived hash to ensure an action can only be performed once without linking it back to the action. -Attempting to consume state twice requires the same nullifier, hence the transaction would fail. +A nullifier is a deterministically derived hash to ensure an action can only be performed once. +The nullifier cannot be linked to the action or user. For example Zcash uses nullifiers to prevent double spending. -* On Solana nullifiers require a data structure that ensures the nullifier is only created once. -* On Solana a straight forward way is to derive a PDA with the nullifier as seed for the PDA account. -* Nullifier accounts must remain active, hence lock ~0.001 SOL in rent per nullifier PDA permanently. -* Compressed PDAs are rent-free, provide similar functionality and derivation while not requiring a custom circuit. +To implement nullifiers we need a data structure that ensures every nullifier is only created once and never deleted. +On Solana a straight forward way to implement nullifiers is to create a PDA account with the nullifier as seed. +* PDA accounts cannot be closed and permanently lock 890,880 lamports (per nullifier rent-exemption). +* Compressed PDAs are derived similar to Solana PDAs and cost 15,000 lamports to create (no rent-exemption). | Storage | Cost per nullifier | |---------|-------------------| -| PDA | ~0.001 SOL | -| Compressed PDA | ~0.000015 SOL | +| PDA | 890,880 lamports | +| Compressed PDA | 15,000 lamports | -[See full example with tests on Github](https://github.com/Lightprotocol/program-examples/tree/main/zk/nullifier). +[See full example with tests on on Github](https://github.com/Lightprotocol/program-examples/tree/main/zk/nullifier). ```rust @@ -52,6 +52,11 @@ pub mod zk_nullifier { data: NullifierInstructionData, nullifiers: Vec<[u8; 32]>, ) -> Result<()> { + // Verify your proof here. Use nullifiers as public inputs + // among your other public inputs. + // Example: + // let public_inputs = [...nullifiers, ...your_other_inputs]; + // Groth16Verifier::new(...).verify()?; create_nullifiers( &nullifiers, @@ -110,9 +115,9 @@ ZK applications on Solana can use existing state Merkle trees to store state in * This way you don't need to maintain your own Merkle tree and indexer. * RPCs that support ZK Compression (Helius, Triton) index state changes. -| Creation | Regular | Compressed | +| Creation | Regular PDA Account | Compressed PDA | | :------------- | :--------------------- | :---------------------- | -| 100-byte PDA | ~0.002 SOL | ~0.000015 SOL | +| 100-byte PDA | ~1,600,000 lamports | 15,000 lamports | Your circuit must include compressed accounts. Find [guides to compressed accounts in the documentation](/compressed-pdas/overview) and the [full example with zk implementation here](https://github.com/Lightprotocol/program-examples/blob/99d260f9f356743b8fe3501c684f7926930d6079/zk-id/circuits/compressed_account.circom).