Skip to content
Joi

Joi

Privacy-First Application Framework

Created on 4th December 2025

Joi

Joi

Privacy-First Application Framework

The problem Joi solves

Privacy is broken in most blockchain applications. Current dApps expose user identities, transaction amounts, and voting choices on public ledgers. This creates:

  • Voter coercion risk - Public votes enable intimidation
  • Financial surveillance - Transaction amounts are visible to everyone
  • Identity linkage - Actions can be traced back to users
  • Lack of selective disclosure - Users can't prove attributes (e.g., "age ≥ 18") without revealing exact values

What Joi Enables

Joi is a production-ready framework that makes privacy-first applications easy to build:

  1. Private Voting Systems

    • DAO governance where votes are secret but results are verifiable
    • Elections without voter intimidation
    • Anonymous polls and surveys
  2. Anonymous Credentials

    • Age verification without revealing birthdate
    • Membership proofs without exposing identity
    • Qualification verification with selective disclosure
  3. Shielded Transactions

    • Transfer value without revealing amounts
    • Hide sender and receiver identities
    • Cross-chain privacy between Mina and Zcash
  4. Privacy-Preserving Data Marketplaces

    • Buy/sell data without revealing parties
    • Prove data quality without exposing data
    • Anonymous payments

Challenges I ran into

Challenges I Ran Into

1. Merkle Witness Constructor Mismatch

Problem: The o1js

MerkleWitness

class expects witness data in a specific format, but I was passing an array of objects

{ isLeft: boolean, sibling: Field }[]

. This caused runtime errors during proof generation.

Solution: I studied the o1js source code and discovered that

MerkleWitness

expects the witness path to be constructed differently. I refactored

SimpleMerkleTree.getWitness()

to properly format the witness data before passing it to the

MerkleWitness8

constructor.

2. Zcash RPC Transaction Fees

Problem: When testing shielded transactions on zcashd regtest, I encountered the error:

"tx unpaid action limit exceeded"

. This was because Zcash NU5 upgrade requires explicit transaction fees, but the default fee was 0.

Solution: I added a

settxfee

RPC call to set a minimum fee of 0.001 ZEC before any transactions. I also updated the integration tests to automatically configure this during setup, and documented it clearly in the README for judges.

3. ZkProgram Compilation Time

Problem: Integration tests with real ZK proofs (

proofsEnabled: true

) took 3-5 minutes to compile the circuits. This made development iteration slow and testing frustrating.

Solution:

  • Created separate test suites: fast unit tests (no proofs) for development, and integration tests (real proofs) for verification
  • Added npm scripts to run specific test suites:

    npm test

    (fast),

    npm run test:integration:voting

    (slow but real)
  • Implemented caching for compiled circuits to avoid recompilation
  • Documented expected compilation times so judges know what to expect

4. Nullifier Double-Spend Prevention

Problem: My initial implementation used a simple hash chain for nullifiers (

newRoot = H(oldRoot, nullifier)

), but this didn't actually check if a nullifier was already used - it just added it to the chain. An attacker could reuse nullifiers.

Solution: I switched to using

MerkleMapWitness

for nullifier tracking. Now the contract:

  1. Verifies the nullifier has value

    0

    (unused) at the current root
  2. Updates the value to

    1

    (used) and computes the new root
  3. This cryptographically prevents double-voting while maintaining privacy

5. Cross-Chain Proof Verification

Problem: Verifying Zcash shielded transaction proofs on Mina is complex because Zcash uses different cryptographic primitives (Groth16 over BLS12-381) than Mina (Kimchi over Pasta curves).

Solution: I implemented a commitment-based bridge pattern instead of full proof verification:

  • Zcash side: Create a commitment to the transaction hash + recipient + amount
  • Mina side: Verify the commitment matches and check Merkle inclusion proof
  • This enables cross-chain privacy without requiring full proof verification (which would be prohibitively expensive)

6. TypeScript Type Safety with o1js

Problem: o1js uses runtime type checking and provable types (

Field

,

Bool

, etc.) which don't always play nicely with TypeScript's static type system. I had many type errors around

Struct

definitions and method signatures.

Solution:

  • Used explicit type annotations everywhere
  • Created helper types for common patterns (e.g.,

    VotePublicInput extends Struct

    )
  • Leveraged

    Provable.if

    for conditional logic instead of JavaScript ternaries
  • Added comprehensive JSDoc comments to clarify expected types

Discussion

Builders also viewed

See more projects on Devfolio