Using fhevmjs for Encrypted Smart Contract Development

The transfer function for encrypted data operates securely by:

  1. Verifying Authorization: Ensures the sender has access to the encrypted amount using TFHE access control.

  2. Homomorphic Balance Updates: Balances are updated securely using FHE computations.

  3. Granting Allowance: Updated balances are granted permissions for both the contract and the respective users.

Getting Started with fhevmjs

1. Installing fhevmjs

Install fhevmjs using any of the following package managers:

npm install fhevmjs
yarn add fhevmjs
pnpm install fhevmjs

2. Configuring Your Project

fhevmjs uses the ECMAScript Module (ESM) format. To ensure compatibility, configure the type field in your package.json as follows:

{
  "name": "your-project",
  "version": "1.0.0",
  "type": "module",
  "dependencies": {
    "fhevmjs": "^1.0.0"
  }
}

For projects using "type": "commonjs", force the web version of fhevmjs by importing as follows:

import { createInstance } from 'fhevmjs/web';

3. Initializing fhevmjs

Before using fhevmjs, load the WebAssembly (WASM) modules required by TFHE:

import { initFhevm } from 'fhevmjs';

const init = async () => {
  await initFhevm(); // Load the necessary WASM modules
};

init().then(() => {
  console.log('TFHE WASM modules loaded');
});

4. Creating an Instance

Create an fhevmjs instance to interact with encrypted functionalities:

import { initFhevm, createInstance } from 'fhevmjs';

const init = async () => {
  await initFhevm(); // Load TFHE WASM modules
  const instance = await createInstance({
    network: window.ethereum,                 // Ethereum provider (e.g., MetaMask)
    gatewayUrl: 'https://gateway.cypherscan.ai', // Gateway URL
    // Other parameters as needed
  });
  return instance;
};

init().then((instance) => {
  console.log('fhevmjs instance created:', instance);
});

With the fhevmjs instance created, you can now utilize its methods to:

  • Encrypt Parameters: Prepare encrypted inputs for your smart contracts.

  • Perform Reencryption: Reencrypt ciphertexts for use on the client side.

  • Interact with Encrypted Smart Contracts: Execute functions that operate on encrypted data.

5. Asynchronous Decryption Operation

Decryption operations in the fhEVM framework are asynchronous. To use these capabilities, your smart contract must extend the GatewayCaller contract, which automatically imports the Gateway Solidity library. The following example demonstrates how to implement asynchronous decryption in your contract.

Example: Asynchronous Decryption in a Smart Contract

pragma solidity ^0.8.24;

import "fhevm/lib/TFHE.sol";
import "fhevm/gateway/GatewayCaller.sol";

contract TestAsyncDecrypt is GatewayCaller {
    ebool xBool; // Encrypted boolean value
    bool public yBool; // Decrypted boolean value

    constructor() {
        // Initialize an encrypted boolean and allow the contract to manipulate it
        xBool = TFHE.asEbool(true);
        TFHE.allow(xBool, address(this));
    }

    function requestBool() public {
        // Prepare the ciphertext for decryption
        uint256[] memory cts = new uint256[](1);
        cts[0] = Gateway.toUint256(xBool);

        // Request decryption, providing the callback selector
        Gateway.requestDecryption(cts, this.myCustomCallback.selector, 0, block.timestamp + 100, false);
    }

    function myCustomCallback(uint256 /*requestID*/, bool decryptedInput) public onlyGateway returns (bool) {
        // Process the decrypted boolean input
        yBool = decryptedInput;
        return yBool;
    }
}

Note that a GatewayContract contract is already predeployed on the fhEVM testnet, and a default relayer account is added through the specification of the environment variable PRIVATE_KEY_GATEWAY_RELAYER in the .env file. Relayers are the only accounts authorized to fulfill the decryption requests. However, GatewayContract will still check the KMS signature during fulfillment, so we trust the relayer only to forward the request on time—a rogue relayer could not cheat by sending fake decryption results.

The Gateway.requestDecryption function is used to initiate an asynchronous decryption request. Its interface is as follows:

function requestDecryption(
    uint256[] memory ct,
    bytes4 callbackSelector,
    uint256 msgValue,
    uint256 maxTimestamp,
    bool passSignaturesToCaller
) returns(uint256 requestID)

Parameters:

ct: An array of ciphertext handles (e.g., uint256 values). These could be derived from types like ebool, euintX, or eaddress. Example:

uint256[] memory ct = Gateway.toUint256(xBool);

callbackSelector:

Function selector for the callback function to be invoked once the decryption is fulfilled. Example callback structure if passSignaturesToCaller is false:

function callback(uint256 requestID, XXX x_0, XXX x_1, ..., XXX x_N-1) external onlyGateway;

If passSignaturesToCaller is true, include a bytes[] memory signatures parameter:

function callback(uint256 requestID, XXX x_0, ..., bytes[] memory signatures) external onlyGateway;
  • msgValue: This is the value in native tokens to be sent to the calling contract during fulfillment, i.e., when the callback is called with the results of decryption.

  • maxTimestamp: The maximum timestamp after which the callback will not be able to receive the decryption results (i.e., the fulfillment transaction will fail if this timestamp is exceeded). This is useful for time-sensitive applications where it’s preferable to reject outdated decryption results.

  • passSignaturesToCaller: This determines whether the callback should transmit KMS signatures. Useful if the dApp developer wants to remove trust from the Gateway service and prefers to verify KMS signatures directly within their dApp smart contract. A concrete example of verifying KMS signatures inside a dApp is available in the requestBoolTrustless function.

6. Utility Functions for Additional Parameters

If you need to pass extra arguments to be used inside the callback, you can use any of the following utility functions during the request. These functions store additional values in the storage of your smart contract:

Add Parameter Functions:

function addParamsEBool(uint256 requestID, ebool _ebool) internal;
function addParamsEUintX(uint256 requestID, euintX _euintX) internal;
function addParamsEAddress(uint256 requestID, eaddress _eaddress) internal;
function addParamsUint256(uint256 requestID, uint256 _uint) internal;

Get Parameter Functions:

function getParamsEBool(uint256 requestID) internal;
function getParamsEUintX(uint256 requestID) internal;
function getParamsEAddress(uint256 requestID) internal;
function getParamsUint256(uint256 requestID) internal;

7. Example: Adding Parameters to a Request

This example demonstrates attaching uint256 parameters to a decryption request and retrieving them during callback execution.

pragma solidity ^0.8.24;

import "../lib/TFHE.sol";
import "../gateway/GatewayCaller.sol";

contract TestAsyncDecrypt is GatewayCaller {
    euint32 xUint32;
    uint32 public yUint32;

    constructor() {
        xUint32 = TFHE.asEuint32(32);
        TFHE.allow(xUint32, address(this));
    }

    function requestUint32(uint32 input1, uint32 input2) public {
        uint256[] memory cts = Gateway.toUint256(xUint32);
        uint256 requestID = Gateway.requestDecryption(cts, this.callbackUint32.selector, 0, block.timestamp + 100, false);
        addParamsUint256(requestID, input1);
        addParamsUint256(requestID, input2);
    }

    function callbackUint32(uint256 requestID, uint32 decryptedInput) public onlyGateway returns (uint32) {
        uint256[] memory params = getParamsUint256(requestID);
        unchecked {
            uint32 result = uint32(params[0]) + uint32(params[1]) + decryptedInput;
            yUint32 = result;
            return result;
        }
    }
}

8. Hardhat Testing for Asynchronous Decryption

When the decryption request is fulfilled by the relayer, the GatewayContract contract, when calling the callback function, will also emit the following event:

event ResultCallback(uint256 indexed requestID, bool success, bytes result);

  • The first argument is the requestID of the corresponding decryption request.

  • success is a boolean assessing if the call to the callback succeeded.

  • result is the bytes array corresponding to the return data from the callback.

To test asynchronous decryption, use the helper functions asyncDecrypt and awaitAllDecryptionResults in your Hardhat tests.

Test Example:

import { asyncDecrypt, awaitAllDecryptionResults } from "../asyncDecrypt";
import { expect } from "chai";
import { ethers } from "hardhat";

describe("TestAsyncDecrypt", function () {
  before(async function () {
    await asyncDecrypt();
    this.signers = await ethers.getSigners();
  });

  beforeEach(async function () {
    const contractFactory = await ethers.getContractFactory("TestAsyncDecrypt");
    this.contract = await contractFactory.deploy();
  });

  it("test async decrypt uint32", async function () {
    const tx = await this.contract.requestUint32(5, 15, { gasLimit: 500_000 });
    await tx.wait();
    await awaitAllDecryptionResults();

    const y = await this.contract.yUint32();
    expect(y).to.equal(52); // 5 + 15 + 32
  });
});

You should set up the gateway handler by calling asyncDecrypt at the top of the before block. Notice that when testing on the fhEVM, a decryption is fulfilled usually 2 blocks after the request, while in mocked mode, the fulfillment will always happen as soon as you call the awaitAllDecryptionResults helper function. A good way to standardize Hardhat tests is hence to always call awaitAllDecryptionResults, which will ensure that all pending decryptions are fulfilled in both modes.

9. Re-encryption

Re-encryption allows ciphertexts encrypted with the blockchain key to be re-encrypted using a NaCl public key for secure client-side use.

Step 1: Smart Contract Implementation

Add a view function to return encrypted data:

pragma solidity ^0.8.24;

import "fhevm/lib/TFHE.sol";

contract EncryptedERC20 {
    mapping(address => euint64) private balances;

    function balanceOf(address account) public view returns (euint64) {
        return balances[account];
    }
}

Step 2: Client-Side Implementation

import { createInstance } from "fhevmjs";
import { ethers } from "ethers";
import abi from "./abi.json";

const CONTRACT_ADDRESS = "YOUR_CONTRACT_ADDRESS";
const provider = new ethers.BrowserProvider(window.ethereum);
await provider.send("eth_requestAccounts", []);
const signer = await provider.getSigner();
const userAddress = await signer.getAddress();

const instance = await createInstance({
    networkUrl: "https://testnet-rpc.cypher.z1labs.ai",
    gatewayUrl: "https://gateway.cypherscan.ai",
});

// Generate keypair for re-encryption
const { publicKey, privateKey } = instance.generateKeypair();

// Sign public key with EIP-712
const eip712 = instance.createEIP712(publicKey, CONTRACT_ADDRESS);
const signature = await signer._signTypedData(eip712.domain, eip712.types, eip712.message);

// Retrieve encrypted balance
const contract = new ethers.Contract(CONTRACT_ADDRESS, abi, signer);
const encryptedBalance = await contract.balanceOf(userAddress);

// Perform re-encryption
const decryptedBalance = await instance.reencrypt(
    encryptedBalance,
    privateKey,
    publicKey,
    signature,
    CONTRACT_ADDRESS,
    userAddress
);

console.log("Decrypted balance:", decryptedBalance);

Last updated