A privacy protocol for Sui blockchain enabling on-chain transaction obfuscation using ZK.
OCTOPUS: On-Chain Transaction Obfuscation Protocol Underlying Sui
OCTOPUS is a privacy protocol built on Sui, designed to empower users to conduct DeFi activities without revealing transaction amounts, addresses, or on-chain linkage. (The project is currently deployed on both Mainnet and Testnet: sui-octopus.vercel.app) While standard transactions remain transparent, Octopus obscures details by moving tokens into a privacy pool where balances are encrypted and interactions are anonymized. Inspired by Tornado Cash and Zcash, Octopus is specifically optimized for Sui’s unique object model. (System architecture overview: sui-octopus.vercel.app/overview)
Core Mechanism The system adopts a UTXO (Unspent Transaction Output) model, similar to Bitcoin but cryptographically shielded:
Key Management Each user possesses four distinct keys:
Supported Operations The system currently supports Shield, Unshield, and Transfer. Except for Shield, all actions require a ZK Proof (ZKP).
Roadmap
Octopus is built across four interconnected layers — ZK circuits, Move smart contracts, a TypeScript SDK, and a Next.js frontend.
The most time-consuming part is the ZK Circuits (Circom + snarkjs). Three Circom circuits compiled to Groth16 proofs on the BN254 curve power the privacy guarantees:
All three share a lib/merkle_proof.circom that verifies 16-level Poseidon Merkle membership proofs. A selector trick (a + b*(c-a)) picks left/right children without branching. The transfer circuit supports both 1-input and 2-input spends by padding with a dummy note (value=0), using IsZero() to conditionally skip the Merkle proof check for the dummy — avoiding the need for two separate circuits entirely.
Proofs are generated fully client-side in the browser via snarkjs + circuit WASM + ~10 MB proving key served from Next.js public/.
Sui has native first-class sui::groth16 and sui::poseidon_bn254 modules built into the framework — no external verifier library needed on-chain. This was a massive win: proof verification is a single groth16::verify_groth16_proof() call, and the Merkle tree uses poseidon::poseidon_bn254() directly in Move.
The hard part was byte encoding alignment across three independent systems: Circom outputs big-endian BN254 field elements, snarkjs returns uncompressed affine coordinates, and Sui's verifier expects Arkworks little-endian compressed points (G1: 32 bytes with sign bit in MSB; G2: 64 bytes with lexicographic sign comparison over Fq2). We had to implement G1/G2 point compression by hand in TypeScript, including the non-obvious -y comparison over the extension field. Additionally, Circom's public signal output order doesn't match what the Move contract expects, requiring a manual reorder before serialization. A single misplaced bit means the proof rejects on-chain despite being cryptographically valid — debugging that silently-failing boundary took significant effort.
The underlying cryptographic stack also took me lots of time, but it is too hard to explain in plain language. Just check https://sui-octopus.vercel.app/developer

