CoFHE makes encrypted computation practical for on-chain applications — but the sharp edge, especially for new builders, has always been decryption: when you can decrypt, who can decrypt, and what it means to “reveal” a value in a public system.
With the new @cofhe/sdk, we’re shipping a clearer, safer decryption model that matches how real applications behave:
- sometimes you only need plaintext locally (to render a UI)
- sometimes you need plaintext on-chain (to drive protocol logic), and you need a verifiable proof for that reveal
This post explains what changed, why we changed it, and how to integrate it cleanly.
The problem: “decrypt” isn’t one thing
On public chains, decryption is never just “get me the number.” It’s a security boundary:
- If a value is decrypted locally, it can remain confidential on-chain.
- If a value is decrypted for a transaction, it becomes part of public state (or at least public calldata/events), and other contracts/users can now reason about it.
Historically, it’s easy for developers to conflate these intents — and that leads to footguns:
- accidentally revealing values when you only needed them for display
- wiring in heavy on-chain flows for simple UI reads
- unclear “who is allowed to decrypt?” behavior
What we shipped: two explicit decryption flows
1) Decrypt to View (UI-only)
Use this when you want to show plaintext in your app while keeping the value confidential on-chain.
- API: client.decryptForView(ctHash, utype).execute()
- Returns: a typed plaintext value (e.g. bigint, boolean, address string)
- Permit: always required
On-chain, these encrypted handles are stored as bytes32. In your client code you’ll usually treat ctHash as a 0x... string or a bigint — both forms are accepted by the SDK.
Conceptually, the dapp is saying:
“I’m authorized to see this, but I’m not publishing it.”
You’ll usually pair this with contract reads that return encrypted handles (e.g. euint64), transforming them to { ctHash, utype } before decrypting.
2) Decrypt to Transact (on-chain publish)
Use this when the plaintext must be revealed verifiably on-chain (examples: unshielding, auction reveals, game outcomes, any “reveal step”).
- API: client.decryptForTx(ctHash).withPermit().execute() or .withoutPermit().execute()
- Returns: { ctHash, decryptedValue, signature }
- Permit: depends on contract ACL
- use .withoutPermit() only if the contract made the handle publicly decryptable (e.g. FHE.allowPublic / FHE.allowGlobal)
- otherwise use .withPermit(...) so the Threshold Network can enforce per-address ACL
That output is designed to plug directly into contract-side verification/publishing patterns:
- FHE.publishDecryptResult(...)
- FHE.verifyDecryptResult(...)
Conceptually, the dapp is saying:
“I’m revealing this publicly, and here is a proof this plaintext matches the ciphertext.”
Why this is better (the receipts)
Clear security boundary (less accidental leakage)
By separating view vs publish intent at the API level, the SDK forces a conscious choice.
If you need a UI value → stay in the view flow If you need a protocol reveal → opt into on-chain + signature verification
Better developer ergonomics (fewer ambiguous states)
Permits are explicit and first-class:
- you create or import them
- you select them
- decrypt operations reflect whether a permit is required
This makes wallet prompts and auth flows easier to reason about.
More composable protocol design
decryptForTx returns a payload that directly matches on-chain verification flows, enabling reusable reveal patterns across protocols.
Integration walkthrough: choosing the right flow
Decision guide
- Plaintext only for UI? → decryptForView
- Plaintext must be public on-chain? → decryptForTx + publish/verify
Common patterns
Pattern A: Private balances / per-user views
- Read encrypted handle from contract
- Ensure user has permit
- Use decryptForView
Pattern B: Reveal step (unshield, auction, claim)
- Ensure contract ACL is correct (FHE.allow, FHE.allowSender, FHE.allowPublic)
- Use decryptForTx (with or without permit depending on ACL)
- Pass { ctHash, decryptedValue, signature } into contract
Failure modes and how to debug
Decrypt-for-view fails
Usually:
- missing/incorrect permit (chainId + account scoped)
- wrong utype
- ACL doesn’t grant access
Decrypt-for-tx fails
Usually:
- .withoutPermit() but no public decrypt ACL
- .withPermit() but permit doesn’t match handle or chain/account
- contract-side verification is wrong (missing verifyDecryptResult)
Closing: shipping the safer default
The theme of this release is explicitness:
- explicit intent (view vs publish)
- explicit authorization (permits)
- explicit verification (signatures)
If you’re migrating, audit every decrypt call:
→ label it UI-only or protocol reveal
Quick migration guide: legacy FHE.decrypt → new flow
1) Replace decrypt trigger with explicit permission
Old:
FHE.decrypt(encryptedValue)
New:
- FHE.allowPublic(encryptedValue)
- or FHE.allow(encryptedValue, address)
2) Move decryption to the client
- Public:
client.decryptForTx(ctHash).withoutPermit().execute() - Restricted:
client.decryptForTx(ctHash).withPermit(...).execute()
Returns:
- decryptedValue
- signature
- ctHash
3) Update contract finalize logic
- Use FHE.publishDecryptResult(...) for stored results
- Use FHE.verifyDecryptResult(...) for one-time validation
4) Keep getDecryptResultSafe only if needed
- Still valid for reading published results
- Not needed if using verifyDecryptResult
Docs + migration links
- Migration tutorial: https://cofhe-docs.fhenix.zone/tutorials/migrating-from-fhe-decrypt
- Decryption operations: https://cofhe-docs.fhenix.zone/fhe-library/core-concepts/decryption-operations
- Client SDK decrypt-to-tx guide: https://cofhe-docs.fhenix.zone/client-sdk/guides/decrypt-to-tx

.png)