Files
fhe/sdk/relayer/docs/user-decryption.md
T
2026-01-25 13:58:30 -08:00

4.6 KiB
Raw Blame History

User decryption

This document explains how to perform user decryption. User decryption is required when you want a user to access their private data without it being exposed to the blockchain.

User decryption in FHEVM enables the secure sharing or reuse of encrypted data under a new public key without exposing the plaintext.

This feature is essential for scenarios where encrypted data must be transferred between contracts, dApps, or users while maintaining its confidentiality.

When to use user decryption

User decryption is particularly useful for allowing individual users to securely access and decrypt their private data, such as balances or counters, while maintaining data confidentiality.

Overview

The user decryption process involves retrieving ciphertext from the blockchain and performing user-decryption on the client-side. In other words we take the data that has been encrypted by the KMS, decrypt it and encrypt it with the user's private key, so only he can access the information.

This ensures that the data remains encrypted under the blockchains FHE key but can be securely shared with a user by re-encrypting it under the users NaCl public key.

User decryption is facilitated by the Relayer and the Key Management System (KMS). The workflow consists of the following:

  1. Retrieving the ciphertext from the blockchain using a contracts view function.
  2. Re-encrypting the ciphertext client-side with the users public key, ensuring only the user can decrypt it.

Step 1: retrieve the ciphertext

To retrieve the ciphertext that needs to be decrypted, you can implement a view function in your smart contract. Below is an example implementation:

import "@fhevm/solidity/lib/FHE.sol";

contract ConfidentialERC20 {
  ...
  function balanceOf(account address) public view returns (euint64) {
    return balances[msg.sender];
  }
  ...
}

Here, balanceOf allows retrieval of the users encrypted balance handle stored on the blockchain. Doing this will return the ciphertext handle, an identifier for the underlying ciphertext.

{% hint style="warning" %} For the user to be able to user decrypt (also called re-encrypt) the ciphertext value the access control (ACL) needs to be set properly using the FHE.allow(ciphertext, address) function in the solidity contract holding the ciphertext. For more details on the topic please refer to the ACL documentation. {% endhint %}

Step 2: decrypt the ciphertext

Using those ciphertext handles, user decryption is performed client-side using the @luxfhe/relayer-sdk library. The userDecrypt function takes a list of handles, allowing you to decrypt multiple ciphertexts in a single request. In this example, provide just one handle. The user needs to have created an instance object prior to that (for more context see the relayer-sdk setup page).

{% hint style="info" %} The total bit length of all ciphertexts being decrypted in a single request must not exceed 2048 bits. Each encrypted type has a specific bit length, for instance euint8 uses 8 bits and euint16 uses 16 bits. For the full list of encrypted types and their corresponding bit lengths, refer to the encrypted types documentation. {% endhint %}

// instance: [`FhevmInstance`] from `luxfhe/relayer-sdk`
// signer: [`Signer`] from ethers (could a [`Wallet`])
// ciphertextHandle: [`string`]
// contractAddress: [`string`]

const keypair = instance.generateKeypair();
// userDecrypt can take a batch of handles (with their corresponding contract addresses).
// In this example we only pass one handle.
const handleContractPairs = [
  {
    handle: ciphertextHandle,
    contractAddress: contractAddress,
  },
];
const startTimeStamp = Math.floor(Date.now() / 1000).toString();
const durationDays = '10'; // String for consistency
const contractAddresses = [contractAddress];

const eip712 = instance.createEIP712(
  keypair.publicKey,
  contractAddresses,
  startTimeStamp,
  durationDays,
);

const signature = await signer.signTypedData(
  eip712.domain,
  {
    UserDecryptRequestVerification: eip712.types.UserDecryptRequestVerification,
  },
  eip712.message,
);

const result = await instance.userDecrypt(
  handleContractPairs,
  keypair.privateKey,
  keypair.publicKey,
  signature.replace('0x', ''),
  contractAddresses,
  signer.address,
  startTimeStamp,
  durationDays,
);

// result maps each handle to its decrypted value
const decryptedValue = result[ciphertextHandle];