Skip to main content

Overview

Decryption is the process of converting encrypted data back into its original form. In the context of Fully Homomorphic Encryption (FHE), decryption allows for the retrieval of results after performing computations on encrypted data. Decryption in CoFHE is a multi-step process that involves both off-chain and on-chain components:
  1. A client requests decryption off-chain and receives the plaintext along with a Threshold Network signature.
  2. The plaintext and signature are submitted on-chain, where the contract publishes or verifies the result.
Learn more about our unique MPC decryption threshold network in the Threshold Network guide.

Decryption Methods: Transaction vs View

CoFHE provides two primary ways to perform decryption, each suited for different use cases:

1. Decrypt for Transaction (decryptForTx)

The client calls decryptForTx(ctHash) off-chain to obtain the plaintext and a Threshold Network signature. These are then submitted on-chain via FHE.publishDecryptResult or FHE.verifyDecryptResult, making the result verifiable by the contract. Common examples:
  • Unshield a confidential token: reveal the encrypted amount so the contract can finalize the public transfer.
  • Finalize a private auction / game move: bids or moves are submitted encrypted, and the winner is revealed later in a verifiable way.

2. Decrypt for View (decryptForView)

The client calls decryptForView off-chain to obtain the plaintext for display in a UI. No on-chain transaction or signature is needed. Common examples:
  • Displaying a user’s confidential balance in a wallet UI.
  • Showing the current state of an encrypted value without revealing it on-chain.
Use decryptForTx when you need to act on the decrypted value in a smart contract. Use decryptForView when you only need to display the value in a UI.

Comparison Table

MethodVisibilityGas CostSmart Contract UsableBest For
decryptForTxPublic (once published on-chain)Gas for the publish/verify txYesPublic results, contract logic
decryptForViewPrivate (off-chain only)NoneNoUI display, confidential data

The Decryption Flow

Step 1: Grant decryption permissions (on-chain)

Before anyone can request decryption, the ciphertext handle must have the appropriate ACL permissions. Use one of the following in your contract:
  • FHE.allowPublic(ctHash) — anyone can request decryption (common for unshield flows)
  • FHE.allow(ctHash, address) — only a specific address can request decryption
  • FHE.allowSender(ctHash) — only msg.sender can request decryption
// Example: allow anyone to decrypt when the auction closes
function closeBidding() external onlyAuctioneer {
    FHE.allowPublic(highestBid);
    auctionClosed = true;
}
See Access Control for the full list of permission methods, including FHE.allowPublic().

Step 2: Request decryption off-chain (client-side)

The client calls decryptForTx(ctHash) to obtain the plaintext and a Threshold Network signature. Choose the permit mode that matches the contract’s ACL policy:
const decryptResult = await client
   .decryptForTx(ctHash)
   .withoutPermit()
   .execute();

// decryptResult.ctHash        — the ciphertext handle
// decryptResult.decryptedValue — the plaintext (bigint)
// decryptResult.signature      — the Threshold Network signature
decryptForTx always returns the plaintext as a bigint. Your contract determines whether that value is interpreted as uint32, uint64, etc.

Step 3: Publish or verify on-chain

Submit the plaintext and signature to your contract. You have two options:

Option A: FHE.publishDecryptResult

Publishes the decrypted value on-chain, making it available for any contract to read.
import "@fhenixprotocol/cofhe-contracts/FHE.sol";

function revealWinner(euint64 ctHash, uint64 plaintext, bytes calldata signature) external onlyAuctioneer {
    FHE.publishDecryptResult(ctHash, plaintext, signature);
    winningBid = plaintext;
    emit RevealedWinningBid(highestBidder, plaintext);
}

Option B: FHE.verifyDecryptResult

Verifies the signature without publishing the result globally. Use this when your contract only needs to confirm the plaintext is authentic.
import "@fhenixprotocol/cofhe-contracts/FHE.sol";

function unshield(bytes32 ctHash, uint32 plaintext, bytes calldata signature) external {
    require(FHE.verifyDecryptResult(ctHash, plaintext, signature), "Invalid decrypt signature");
    // ...continue with protocol logic...
}

Full Example Contract

Here’s a complete example showing the new decryption flow in an auction contract:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "@fhenixprotocol/cofhe-contracts/FHE.sol";

contract EncryptedAuction {
    euint64 public highestBid;
    address public highestBidder;
    address public auctioneer;
    bool public auctionClosed;
    uint64 public winningBid;

    event BidPlaced(address indexed bidder);
    event AuctionClosed();
    event RevealedWinningBid(address indexed winner, uint64 amount);

    modifier onlyAuctioneer() {
        require(msg.sender == auctioneer, "Only auctioneer can call this");
        _;
    }

    constructor() {
        auctioneer = msg.sender;
    }

    // Place an encrypted bid
    function placeBid(InEuint64 memory encryptedBid) external {
        require(!auctionClosed, "Auction is closed");

        euint64 bid = FHE.asEuint64(encryptedBid);
        ebool isHigher = bid.gt(highestBid);

        // Update highest bid if this bid is higher
        euint64 newHighestBid = FHE.select(isHigher, bid, highestBid);
        FHE.allowThis(newHighestBid);

        highestBid = newHighestBid;
        highestBidder = msg.sender;

        emit BidPlaced(msg.sender);
    }

    // Close the auction and allow public decryption of the winning bid
    function closeBidding() external onlyAuctioneer {
        require(!auctionClosed, "Auction already closed");

        FHE.allowPublic(highestBid);
        auctionClosed = true;

        emit AuctionClosed();
    }

    // Reveal the winner by publishing the decrypted result with proof
    function revealWinner(euint64 ctHash, uint64 plaintext, bytes calldata signature) external {
        require(auctionClosed, "Auction must be closed first");

        FHE.publishDecryptResult(ctHash, plaintext, signature);

        winningBid = plaintext;
        emit RevealedWinningBid(highestBidder, plaintext);
    }
}
The client-side flow to reveal the winner:
// 1. Read the encrypted highest bid from the contract
const ctHash = await auctionContract.highestBid();

// 2. Request decryption off-chain (no permit needed since allowPublic was used)
const decryptResult = await client
  .decryptForTx(ctHash)
  .withoutPermit()
  .execute();

// 3. Submit the result on-chain
const tx = await auctionContract.revealWinner(
  decryptResult.ctHash,
  decryptResult.decryptedValue,
  decryptResult.signature
);
await tx.wait();

Best Practices

Use allowPublic for values meant to be revealed

When a value is intended to become public (e.g. unshielding, auction reveals), use FHE.allowPublic() so anyone can trigger the decryption without needing a permit.

Check Access Control Before Decrypting

Ensure only authorized parties can request decryption. Use FHE.allow() or FHE.allowSender() for restricted access. See Access Control.

Choose the right on-chain method

Use FHE.publishDecryptResult when you want the result stored publicly on-chain. Use FHE.verifyDecryptResult when you only need to confirm the plaintext is authentic without publishing it.

Use decryptForView for UI-only reads

If you only need to display a value in your UI and don’t need an on-chain-verifiable signature, use decryptForView instead of decryptForTx to avoid unnecessary on-chain transactions.

Common Pitfalls

  • Missing ACL permissions: If no allow* was called for the ciphertext handle, decryption requests will be denied. Make sure to grant permissions before the client requests decryption.
  • Permit mode must be selected: When using decryptForTx, you must call exactly one of .withPermit(...) or .withoutPermit() before .execute().
  • Wrong chain/account: Permits are scoped to chainId + account. If you get an ACL/permit error, double-check you’re connected to the expected chain and account.
  • Type mismatch: decryptedValue is always a bigint. If your Solidity function expects a smaller integer type (e.g. uint32), make sure the value is within range.