When documentation says "your transaction is private," it's easy to treat that as a black box. This post opens the box. Each step below is what actually happens when you click Shield in the Sanect dApp — the real operations, in order, with no hand-waving.
Step 1: Connect and Sign the Derivation Message
You connect MetaMask to the Sanect dApp. When you navigate to the shielded transfer interface, the dApp calls personal_sign with a fixed, domain-separated derivation message specific to Sanect.
This is not a transaction. No gas is spent. MetaMask shows you the plaintext message; you confirm. The resulting signature is 65 bytes of ECDSA output over your private key.
Step 2: Key Derivation — All in Browser
The dApp feeds your MetaMask signature into HKDF-SHA256 to derive two keys:
- Viewing key — lets you see incoming notes, cannot spend
- Spending key — derived from the viewing key, authorizes withdrawals
HKDF (HMAC-based Key Derivation Function) is a standard RFC 5869 construction. The "H" in HKDF stands for HMAC; SHA-256 is the underlying hash. The derivation is deterministic: same MetaMask address, same derivation message, same signature, same keys — every time.
Both keys exist only in browser memory. They are never sent to a server, never written to localStorage, never included in any transaction. When you close the tab, they're gone.
Step 3: Creating a Shielded Note
You specify an amount to shield. The dApp constructs a Note — a data structure that encodes everything needed to later spend the funds:
- asset: the token being shielded (SNCT)
- amount: the value in asnct
- recipient pubkey: the X25519 public key derived from your viewing key
- randomness: 32 bytes of fresh randomness, generated locally, making each note unique
The note is not transmitted as-is. It is committed to a hash and encrypted before anything leaves the browser.
Step 4: Memo Encryption
To allow the recipient to discover their incoming notes by scanning the chain, the note is encrypted and attached to the transaction as a 240-byte memo (144-byte ciphertext envelope + 96-byte FMD-reserved tail).
The encryption process:
- The dApp generates an ephemeral X25519 keypair (fresh for this note)
- ECDH is performed between the ephemeral private key and the recipient's X25519 public key to produce a shared secret
- ChaCha20-Poly1305 encrypts the note using that shared secret
- The ephemeral public key is prepended to the ciphertext — together they form the encrypted memo
Only the holder of the recipient's viewing key can reverse this process. The ephemeral keypair is discarded after use.
Step 5: UltraHonk Proof Generation
Before broadcasting the transaction, the dApp generates an UltraHonk zero-knowledge proof locally using the Noir circuit compiled via Barretenberg. The proof attests to three things without revealing them:
- You control the SNCT you are shielding (you know the private key for the source address)
- The note commitment is computed correctly from the note contents
- There is no double-spend (no nullifier for this note has been published before)
UltraHonk is a PLONK-based proving system from Aztec's Barretenberg library. It uses a universal SRS — no per-circuit trusted setup ceremony. The proof is succinct and the on-chain verifier checks it efficiently. Proof generation runs in WebAssembly in your browser via Noir's WASM prover. It takes approximately 2-3 seconds on modern hardware.
The proof is what makes the privacy guarantee cryptographic rather than just operational. Even if the entire network is watching, they cannot extract the note contents or link it to your address without breaking the underlying elliptic curve assumptions.
Step 6: The On-Chain Transaction
The dApp submits a transaction calling ShieldedPool.deposit(). This transaction includes:
- The SNCT amount being locked (transferred from your address to the ShieldedPool contract)
- The note commitment (a hash, not the note itself)
- The encrypted memo (240 bytes: 144 ciphertext envelope + 96 FMD-reserved)
- The UltraHonk proof
On-chain, the contract verifies the UltraHonk proof, adds the commitment to the append-only Merkle tree, emits a log event, and locks your SNCT. At this point, the deposit is final. Your address's transparent balance decreases; the shielded pool's balance increases.
Step 7: The Recipient Scans
The recipient (which may be you, or someone else you sent the note to) periodically scans the chain for new memos. For each memo, they attempt to decrypt it using their viewing key. If the ChaCha20-Poly1305 authentication tag is valid, decryption succeeded — this note is theirs.
Failed decryption attempts are computationally cheap and leave no trace. Scanning does not reveal which notes you are watching for, only that you are scanning.
What's Private and What's Not
After a deposit, the on-chain record shows that a specific address deposited a specific amount at a specific time. This is public. What is not public: who the note is addressed to, how the note will be spent, or whether the recipient will transfer it onward within the shielded pool before withdrawing.
The sender-receiver link is broken once funds enter the pool. Withdrawal events are also public — they show an amount leaving the pool to an address — but without knowing how long the funds sat in the pool or what path they took, correlating deposits to withdrawals requires guesswork that degrades as pool activity increases.
Amount privacy is not provided in the current version. Deposit and withdrawal amounts are visible to all observers.