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:
-
Private Voting Systems
- DAO governance where votes are secret but results are verifiable
- Elections without voter intimidation
- Anonymous polls and surveys
-
Anonymous Credentials
- Age verification without revealing birthdate
- Membership proofs without exposing identity
- Qualification verification with selective disclosure
-
Shielded Transactions
- Transfer value without revealing amounts
- Hide sender and receiver identities
- Cross-chain privacy between Mina and Zcash
-
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 refactoredSimpleMerkleTree.getWitness()
to properly format the witness data before passing it to theMerkleWitness8
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:- Verifies the nullifier has value
0
(unused) at the current root - Updates the value to
1
(used) and computes the new root - 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 aroundStruct
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
Technologies used
