Skip to content

About

This is an e2e PoC for using mopro with react native and verifying the proof on solana

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

Mobile → Solana ZK Verification (E2E Spike)

Generate Groth16 proofs on mobile, verify on Solana.

Scope

Goal: Prove the E2E flow works — mobile app generates a ZK proof, Solana program verifies it.

  • Circuit: multiplier2 (Circom/Groth16, BN254)
  • Mobile: React Native + mopro
  • Chain: Solana (local validator → devnet)
  • Verifier: groth16-solana (by Light Protocol)

Project Structure

mobile-solana-e2e/
├── src/                    # mopro Rust lib (proof generation)
├── react-native/           # Mobile app
│   └── src/
│       ├── App.tsx         # Main app with "Verify on Solana" button
│       ├── solana.ts       # Solana client & proof conversion
│       └── polyfills.ts    # Buffer/crypto polyfills for RN
├── test-vectors/           # ZK keys (zkey, wasm)
├── solana-verifier/        # Anchor program (proof verification)
│   ├── programs/
│   │   └── solana-verifier/
│   │       └── src/
│   │           ├── lib.rs           # Main program with verify_proof
│   │           └── verifying_key.rs # Embedded VK (generated)
│   ├── scripts/
│   │   └── parse-vk.js     # VK converter script
│   ├── tests/
│   │   └── solana-verifier.ts  # Test with proof conversion
│   ├── vk.json             # Verification key (from snarkjs)
│   └── Anchor.toml
├── Config.toml             # mopro config
└── Cargo.toml

Prerequisites

  • mopro prerequisites — Rust, cmake, Xcode/Android Studio
  • mopro getting started
  • Solana CLI: sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)"
  • Anchor CLI: cargo install --git https://github.com/coral-xyz/anchor avm --force && avm install latest && avm use latest
  • snarkjs: npm install -g snarkjs

Setup

Mobile App (mopro)

# Build native bindings
mopro build

# Run React Native app
cd react-native
npm install
npm install @react-native-clipboard/clipboard  # For copy functionality
cd ios && pod install && cd ..
npm run ios   # Runs on iPhone 16 Pro simulator

Solana Verifier

cd solana-verifier

# 1. Export verification key from zkey (if not done)
snarkjs zkey export verificationkey ../test-vectors/circom/multiplier2_final.zkey vk.json

# 2. Generate Rust verifying key from vk.json
node scripts/parse-vk.js vk.json > programs/solana-verifier/src/verifying_key.rs

# 3. Start local validator (separate terminal)
solana-test-validator --ledger .ledger

# 4. Create keypair (first time only)
solana-keygen new --outfile ~/.config/solana/id.json

# 5. Build and deploy
anchor build
anchor deploy

# 6. Test
anchor test --skip-local-validator

⚠️ Critical: Proof Format Conversion

The key insight from debugging this spike: groth16-solana requires negating proof.A's y-coordinate.

Why?

The Groth16 verification equation uses -A rather than A. The groth16-solana library expects the proof with this negation already applied.

Mobile App Output (mopro)

{
  "a": {"x": "123...", "y": "456...", "z": "1"},
  "b": {"x": ["...", "..."], "y": ["...", "..."], "z": ["1", "0"]},
  "c": {"x": "789...", "y": "012...", "z": "1"},
  "protocol": "groth16",
  "curve": "bn128"
}

Conversion to Solana Format (TypeScript)

// BN254 field prime
const FIELD_PRIME = BigInt("21888242871839275222246405745257275088696311157297823662689037894645226208583");

// Convert decimal string to 32-byte big-endian array
function toBE32(decimal: string): number[] {
  const hex = BigInt(decimal).toString(16).padStart(64, '0');
  return hex.match(/.{2}/g)!.map(b => parseInt(b, 16));
}

// Negate field element: -y mod p
function negateField(y: string): string {
  return ((FIELD_PRIME - BigInt(y)) % FIELD_PRIME).toString();
}

// Convert G1 point to 64 bytes, with optional negation
function g1ToBytes(point: {x: string, y: string}, negate = false): number[] {
  const y = negate ? negateField(point.y) : point.y;
  return [...toBE32(point.x), ...toBE32(y)];
}

// Convert G2 point to 128 bytes (c1, c0, c1, c0 ordering)
function g2ToBytes(point: {x: string[], y: string[]}): number[] {
  return [
    ...toBE32(point.x[1]), // x.c1
    ...toBE32(point.x[0]), // x.c0
    ...toBE32(point.y[1]), // y.c1
    ...toBE32(point.y[0]), // y.c0
  ];
}

// Usage:
const proofA = g1ToBytes(proof.a, true);   // ⚠️ NEGATE proof_a!
const proofB = g2ToBytes(proof.b);
const proofC = g1ToBytes(proof.c, false);  // Don't negate C
const publicInputs = inputs.map(i => toBE32(i));

Solana Program Input

verify_proof(
    proof_a: [u8; 64],       // G1: x || (-y) (NEGATED!)
    proof_b: [u8; 128],      // G2: x.c1 || x.c0 || y.c1 || y.c0
    proof_c: [u8; 64],       // G1: x || y
    public_inputs: [[u8; 32]; 2]
)

Verification Key Generation

The parse-vk.js script converts snarkjs verification key format to Rust:

node scripts/parse-vk.js vk.json > programs/solana-verifier/src/verifying_key.rs

Notes:

  • G2 point ordering: c1, c0, c1, c0 (not c0, c1)
  • groth16-solana has a typo: field is vk_gamme_g2 (not vk_gamma_g2)

Flow

E2E Flow (Mobile → Solana)

  1. Mobile app generates proof: (a=3, b=4) → proof + public_inputs["12", "3"]
  2. Tap "Verify on Solana" button
  3. App converts proof (negates A, formats bytes) via solana.ts
  4. App sends transaction to Solana validator
  5. Program verifies using groth16-solana and embedded VK
  6. App displays result ✅ or ❌

Manual Flow (for debugging)

  1. Generate proof in app → Copy to clipboard
  2. Paste into solana-verifier/tests/solana-verifier.ts
  3. Run anchor test --skip-local-validator

Public Inputs

For multiplier2 circuit:

  • public_inputs[0] = 12 (output: a × b)
  • public_inputs[1] = 3 (public input: a)
  • b = 4 remains private

Deployed Program

  • Program ID: 9wsdJR6iVCN6gMVEsQbN9hUxp6SG43T86VGgeTm4w6L1
  • Cluster: localhost (solana-test-validator)

E2E Testing

Quick Start (3 terminals)

Terminal 1: Start Solana Validator

cd solana-verifier
solana-test-validator --ledger .ledger

Terminal 2: Deploy Contract (wait for validator to start)

cd solana-verifier
anchor deploy

Terminal 3: Run Mobile App

cd react-native
npm install          # Installs @solana/web3.js and polyfills
cd ios && pod install && cd ..
npm run ios

Test in App

  1. Enter values for a and b (e.g., 3 and 4)
  2. Tap "Generate Circom Proof"
  3. Wait for proof to appear
  4. Tap "🔗 Verify on Solana"
  5. See result: ✅ Verified or ❌ Failed

Expected Output

Success:

✅ Verified on Solana!
Tx: 2Ft4hMJU7MB9RDuX...

Validator not running:

❌ Cannot connect to Solana (is validator running?)

Status

  • Mobile proof generation (mopro/Circom)
  • Solana verifier program scaffolding
  • Verification key export & conversion
  • Build & deploy verifier
  • Manual verification test ✅
  • E2E integration (mobile → Solana directly) ✅

Debugging Notes

This spike involved significant debugging to get the proof format correct.

Using snarkjs to Isolate the Problem

When Solana verification kept failing, we needed to determine if the issue was:

  • The proof itself (invalid), or
  • The byte format conversion (valid proof, wrong encoding)

Solution: Export the proof in snarkjs format and verify it outside Solana:

# The test exports proof to snarkjs format
anchor test --skip-local-validator

# Then verify with snarkjs (same vk.json, same zkey)
snarkjs groth16 verify vk.json test_public.json test_proof.json
# Output: [INFO]  snarkJS: OK!

Why this works:

  • snarkjs uses the same VK and proof format as the mobile app
  • If snarkjs says "OK", the proof is mathematically valid
  • Any Solana failure must be a format/encoding issue, not a proof issue

This narrowed the problem to byte conversion, leading us to discover the proof_a negation requirement.

Root Cause Analysis

  1. Initial failures: Verification kept failing despite snarkjs saying "OK"
  2. Insight: The proof is valid, so the issue is format conversion
  3. Discovery: groth16-solana docs.rs example shows proof_a.neg()
  4. Root cause: groth16-solana requires proof_a to be negated (y-coordinate)
  5. Solution: Negate y-coordinate by computing FIELD_PRIME - y

The G2 ordering (c1, c0 vs c0, c1) was also important to match between VK and proof.

Mobile Implementation Notes

Dependencies Added

"@solana/web3.js": "^1.98.0",
"buffer": "^6.0.3",
"react-native-get-random-values": "^1.11.0"

Key Files

  • src/polyfills.ts — Must be imported first in App.tsx for Buffer/crypto support
  • src/solana.ts — Contains all conversion logic and Solana client code

How it Works

  1. App generates keypair on-the-fly (ephemeral, for testing)
  2. Requests airdrop from local validator (1 SOL for fees)
  3. Builds transaction with proof data using Anchor IDL discriminator
  4. Sends to http://127.0.0.1:8899 (iOS simulator can access localhost)

Production Considerations

  • Replace ephemeral keypair with wallet adapter (Phantom, Solflare)
  • Use devnet/mainnet RPC URL
  • Handle network errors gracefully
  • Consider transaction simulation before sending

References

About

This is an e2e PoC for using mopro with react native and verifying the proof on solana

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages