Back to Fhevm

RFC001

sdk/js-sdk/notes/rfc/RFC001.md

0.13.0-019.0 KB
Original Source

Status: Implemented Authors: @Aurora Makovac @Alex B. Created: February 17, 2026 Updated: March 30, 2026 — updated to reflect current naming conventions and API surface

Target Repository: zama-ai/fhevm Related Components: Hardhat plugin, Rust SDK


1. Abstract

This RFC describes the architectural restructuring of the relayer-sdk into @fhevm/sdk — a layered, composable, library-agnostic SDK for building on FHEVM chains.

The restructuring has been implemented as a single package (@fhevm/sdk) with multiple entry points, a protocol-agnostic core, and thin adapters for ethers.js v6 and viem. The architecture follows a viem-inspired composable design with symbol-based security, lazy WASM loading, and strict module boundaries.

What was achieved

  • Library-agnostic coresrc/core/ contains all protocol logic with zero ethers/viem dependencies
  • Thin adapterssrc/ethers/ and src/viem/ are ~200 LOC each, bridging native types to core abstractions
  • Modular WASM — Encrypt (TFHE ~5MB) and decrypt (TKMS ~600KB) modules load independently and lazily
  • Tree-shakeable — Using viem never pulls ethers and vice versa; unused modules are not bundled
  • Safe by default — Symbol-keyed access control, opaque TrustedClient sealing, frozen objects, private #fields
  • Single package, multiple entry points — Simpler than the originally proposed multi-package monorepo
  • Action tiers — Five entry points under @fhevm/sdk/actions/* organize standalone functions by capability requirement

2. Design principles (as implemented)

2.1 Library-agnostic core

The core (src/core/):

  • Does NOT import ethers or viem
  • Defines its own abstractions: TrustedClient, EthereumModule, FhevmRuntime
  • Exposes integration points via module factories and adapter tokens
  • Treats client libraries as pluggable layers sealed behind opaque TrustedClient wrappers

Adapters (src/ethers/, src/viem/) are thin wrappers that:

  • Implement EthereumModule using their respective library (ABI encoding, contract reads, signature recovery)
  • Seal/unseal native clients via symbol-keyed TrustedClient
  • Re-export core actions as client methods via decorators

2.2 Viem-inspired composable design

  • Small composable action functions (each file exports: function, Parameters type, ReturnType type)
  • Explicit configuration — no hidden global state beyond the one-time setFhevmRuntimeConfig()
  • Client composition via .extend() decorator chaining
  • Tests colocated with implementation (*.test.ts alongside source)

2.3 Tree-shakeable architecture

  • Using @fhevm/sdk/viem does NOT pull in ethers (and vice versa)
  • WASM modules are separately loadable — encrypt-only clients skip TKMS, decrypt-only skip TFHE
  • All entry points produce ESM with "sideEffects": false
  • Actions are standalone functions that can be imported individually from @fhevm/sdk/actions/*

2.4 Minimal footprint

  • Zero runtime dependencies beyond ethers or viem (peer, not bundled)
  • WASM loaded lazily on first use, not at import time
  • Core package has no dependency on any WASM binary at the type level

2.5 Safe by default

  • TrustedClient uses opaque sealing — core layer never sees native ethers/viem objects
  • Symbol-keyed PRIVATE_TOKEN prevents unauthorized CoreFhevmImpl instantiation
  • E2eTransportKeypair hides TKMS private key behind #privateFields — never exposed to application code
  • ES2015 #field syntax for all internal state
  • Object.freeze() on chain definitions, runtime instances, client internals
  • Error messages never include sensitive data

3. Package structure (implemented)

Rather than the originally proposed multi-package monorepo (fhevm-types, fhevm-core, fhevm-wasm-sdk, etc.), the SDK ships as a single package with multiple entry points. This decision was driven by:

  • Simpler dependency management (no inter-package version coordination)
  • Easier releases (single version, single publish)
  • Tree-shaking achieves the same bundle-size goals as separate packages
  • Reduced operational friction vs. a monorepo of 6+ packages
@fhevm/sdk                       ← package name
├── @fhevm/sdk/ethers            ← Ethers.js v6 adapter
├── @fhevm/sdk/viem              ← Viem adapter
├── @fhevm/sdk/chains            ← Chain definitions (framework-agnostic)
├── @fhevm/sdk/actions/base      ← Base actions (publicDecrypt, ACL checks, input proofs)
├── @fhevm/sdk/actions/encrypt   ← Encrypt actions (encrypt, generateZkProof)
├── @fhevm/sdk/actions/decrypt   ← Decrypt actions (decrypt, generateE2eTransportKeypair)
├── @fhevm/sdk/actions/chain     ← Chain utility actions (signDecryptionPermit, EIP-712, keypair ops)
├── @fhevm/sdk/actions/host      ← Host contract read actions (resolveFhevmConfig, contract data)

Entry points are defined via subdirectory package.json files.


4. Source layout

src/
├── core/                          ← Protocol-agnostic business logic
│   ├── actions/                   ← All action functions (organized by tier)
│   │   ├── base/                  ← publicDecrypt, fetchVerifiedInputProof, ACL checks, signers
│   │   ├── chain/                 ← signDecryptionPermit, createKmsUserDecryptEIP712, keypair ops
│   │   ├── decrypt/               ← decrypt, generateE2eTransportKeypair, decryptKmsSignedcryptedShares
│   │   ├── encrypt/               ← encrypt, generateZkProof
│   │   └── host/                  ← Contract reads (ACL, InputVerifier, KMSVerifier, FhevmExecutor)
│   ├── base/                      ← Primitives (address, bytes, object, errors, trustedValue)
│   ├── chains/
│   │   ├── definitions/           ← mainnet.ts, sepolia.ts
│   │   ├── utils.ts               ← defineFhevmChain()
│   │   └── index.ts               ← @fhevm/sdk/chains entry point
│   ├── clients/
│   │   └── decorators/            ← baseActions, encryptActions, decryptActions
│   ├── modules/
│   │   ├── encrypt/               ← EncryptModule (TFHE WASM bindings)
│   │   ├── decrypt/               ← DecryptModule (TKMS WASM bindings)
│   │   ├── ethereum/              ← EthereumModule interface + TrustedClient
│   │   └── relayer/               ← RelayerModule (HTTP client for relayer API)
│   ├── runtime/                   ← CoreFhevm-p.ts (client), CoreFhevmRuntime-p.ts (runtime)
│   ├── types/                     ← All shared types
│   ├── coprocessor/               ← Coprocessor logic
│   ├── handle/                    ← EncryptedValue / handle parsing logic
│   ├── host-contracts/            ← ABI definitions
│   ├── kms/                       ← E2eTransportKeypair, SignedDecryptionPermit, KMS EIP-712
│   └── errors/                    ← Structured error types
│
├── ethers/                        ← Ethers.js v6 adapter
│   ├── clients/                   ← createFhevmClient, createFhevmEncryptClient, createFhevmDecryptClient, createFhevmBaseClient
│   ├── internal/                  ← ethers-p.ts (runtime config, TrustedClient sealing), ethereum.ts (EthereumModule impl)
│   └── index.ts                   ← @fhevm/sdk/ethers entry point
│
├── viem/                          ← Viem adapter
│   ├── clients/                   ← (same pattern as ethers)
│   ├── internal/
│   └── index.ts                   ← @fhevm/sdk/viem entry point
│
└── wasm/                          ← WASM bindings
    ├── tfhe/                      ← TFHE WASM (~5MB, encryption)
    ├── tkms/                      ← TKMS WASM (~600KB, decryption)
    ├── wasmBaseUrl.ts             ← ESM: import.meta.url
    └── wasmBaseUrl.cts            ← CJS: __filename

How this maps to the originally proposed packages

RFC001 Proposed PackageImplemented As
fhevm-typessrc/core/types/ — types are co-located, not a separate package
fhevm-coresrc/core/ (actions, base, chains, clients, handle, host-contracts, kms)
fhevm-wasm-sdksrc/core/modules/encrypt/ + src/core/modules/decrypt/ + src/wasm/
core-relayer-sdksrc/core/modules/relayer/ + src/core/actions/
fhevm-ethers-wrappersrc/ethers/ (~200 LOC adapter)
fhevm-viem-wrappersrc/viem/ (~200 LOC adapter)

The single-package approach achieves the same separation of concerns through directory boundaries and TypeScript's module system, while avoiding the coordination overhead of 6+ packages.


5. Layered architecture

┌─────────────────────────────────────────────────────┐
│  @fhevm/sdk/ethers          @fhevm/sdk/viem         │  Adapters
│  (setFhevmRuntimeConfig,    (setFhevmRuntimeConfig, │  (thin wrappers)
│   createFhevmClient, ...)    createFhevmClient, ...)│
├─────────────────────────────────────────────────────┤
│  src/core/clients/decorators/                       │  Decorators
│  (baseActions, encryptActions, decryptActions)       │  (curry actions → methods)
├─────────────────────────────────────────────────────┤
│  src/core/actions/                                  │  Actions
│  (encrypt, decrypt, publicDecrypt,                  │  (standalone functions)
│   signDecryptionPermit, fetchFheEncryptionKeyBytes) │
├─────────────────────────────────────────────────────┤
│  src/core/runtime/                                  │  Runtime
│  (CoreFhevm, FhevmRuntime, extend())                │  (client + module composition)
├─────────────────────────────────────────────────────┤
│  src/core/modules/                                  │  Modules
│  ├── encrypt/  (TFHE WASM)                          │  (WASM, HTTP, ABI)
│  ├── decrypt/  (TKMS WASM)                          │
│  ├── relayer/  (HTTP client)                        │
│  └── ethereum/ (TrustedClient, EthereumModule)      │
├─────────────────────────────────────────────────────┤
│  src/core/base/                                     │  Base
│  (address, bytes, trustedValue, errors)             │  (zero-dep primitives)
└─────────────────────────────────────────────────────┘

Dependency direction is strictly top-down. Core never imports from adapters. Actions never import from decorators. Modules never import from actions.


6. Client composition model

Clients are built by composing a base CoreFhevm with decorator actions via .extend():

ts
// Inside createFhevmClient() — both ethers and viem adapters
const c = createCoreFhevm(PRIVATE_TOKEN, {
  chain,
  runtime: getAdapterRuntime(),  // includes EthereumModule + RelayerModule
  client: provider,              // sealed as TrustedClient
});

return c
  .extend(baseActions)           // adds: publicDecrypt, signDecryptionPermit, parseE2eTransportKeypair, ...
  .extend(decryptActions)        // adds: decrypt, generateE2eTransportKeypair, createUserDecryptEIP712, ...
  .extend(encryptActions);       // adds: encrypt

Four pre-composed client variants:

FactoryDecorators AppliedWASM Cost
createFhevmClient()base + decrypt + encrypt~5.6MB + 50MB key
createFhevmEncryptClient()base + encrypt~5MB + 50MB key
createFhevmDecryptClient()base + decrypt~600KB
createFhevmBaseClient()baseNone

The runtime itself is extended with modules:

  • EncryptModule — TFHE WASM operations (lazy-loaded on first encrypt())
  • DecryptModule — TKMS WASM operations (lazy-loaded on first decrypt())
  • RelayerModule — HTTP client for relayer API (always available)
  • EthereumModule — ABI encoding, contract reads, signature recovery (adapter-specific)

7. Integration approach (resolved)

The original RFC presented two options: "Direct Wrapper" vs. "Explicit Layer Injection."

The implemented approach is Direct Wrapper — each adapter provides factory functions that handle all internal wiring:

ts
// With viem
import { setFhevmRuntimeConfig, createFhevmClient } from "@fhevm/sdk/viem";
import { sepolia } from "@fhevm/sdk/chains";

setFhevmRuntimeConfig({});
const provider = createPublicClient({ chain: viemSepolia, transport: http() });
const client = createFhevmClient({ chain: sepolia, provider });

// With ethers
import { setFhevmRuntimeConfig, createFhevmClient } from "@fhevm/sdk/ethers";
import { sepolia } from "@fhevm/sdk/chains";

setFhevmRuntimeConfig({});
const provider = new ethers.BrowserProvider(window.ethereum);
const client = createFhevmClient({ chain: sepolia, provider });

Developers never need to manually inject modules, create runtimes, or compose layers. The factory handles everything. The .extend() mechanism remains available internally and for advanced use cases.


8. Dual CJS/ESM build

The SDK ships both CommonJS and ESM from a single TypeScript source:

src/_esm/   ← ESM output (module: esnext, "sideEffects": false)
src/_cjs/   ← CJS output (module: commonjs)
src/_types/ ← Declaration files (.d.ts + .d.ts.map)

WASM base URL resolution is the only CJS/ESM divergence:

  • ESM (wasmBaseUrl.ts): new URL("./file", import.meta.url)
  • CJS (wasmBaseUrl.cts): require("node:url").pathToFileURL(__filename)
  • Switched via package.json "imports" field: #wasm/baseUrl

9. Security architecture

MechanismPurpose
TrustedClient (opaque sealed value)Prevents native ethers/viem client from leaking into core
PRIVATE_TOKEN (symbol)Only adapter code can create CoreFhevm instances
PRIVATE_VIEM_TOKEN / PRIVATE_ETHERS_TOKENOnly the originating adapter can unseal its TrustedClient
E2eTransportKeypair (#privateFields)TKMS private key never exposed as raw bytes to user code
#privateFieldsES2015 hard-private fields on all internal classes
Object.freeze() + Object.defineProperty(..., { writable: false })Immutable chain definitions, frozen runtime, locked client properties
defineFhevmChain()simpleDeepFreeze()Chain objects are recursively frozen at definition time

10. Breaking changes from @zama-fhe/relayer-sdk

AreaOld SDKNew SDK
InitializationinitSDK() + createInstance(config)setFhevmRuntimeConfig() + createFhevmClient({ chain, provider })
EncryptionBuilder: createEncryptedInput().add32(42).encrypt()Declarative: encrypt({ values: [...], contractAddress, userAddress })
Encrypt result{ handles, inputProof }{ externalEncryptedValues, inputProof }
Key managementgenerateKeypair() → raw { publicKey, privateKey }generateE2eTransportKeypair() → opaque E2eTransportKeypair
Permit + signingcreateEIP712() + wallet sign + manual bundlingsignDecryptionPermit({ signer, e2eTransportKeypair, ... }) — all-in-one
Decrypt9 positional paramsObject: decrypt({ e2eTransportKeypair, encryptedValues, signedPermit })
Decrypt resultRecord<handle, value>ClearValue[] with typed .value, .fheType, .encryptedValue
Public decryptpublicDecrypt(handles)publicDecrypt({ encryptedValues })PublicDecryptionProof
Type namesFhevmHandle, DecryptedFhevmHandleEncryptedValue, ClearValue
Chain configFlat SepoliaConfig objectFhevmChain with nested fhevm.contracts, fhevm.gateway
Entry points@zama-fhe/relayer-sdk/web vs /node@fhevm/sdk/ethers, @fhevm/sdk/viem, @fhevm/sdk/chains, @fhevm/sdk/actions/*

Breaking changes were accepted as necessary for the architectural goals. The new API is object-based throughout (no positional param ambiguity), type-safe (wrong client variant = compile error), and secure by default.


11. Hardhat plugin compatibility

The Hardhat plugin remains a separate package but is designed to consume @fhevm/sdk directly. The SDK's modular architecture supports this:

  • Chain definitions (@fhevm/sdk/chains) can define hardhatLocal for local testing
  • The same createFhevmClient() API works for both production and test environments
  • Mock mode (replacing real WASM with deterministic stubs) is a planned extension point

12. Future: Non-EVM chains

The core is chain-agnostic by design:

  • FhevmChain type is a plain data object (no EVM-specific methods)
  • EthereumModule is an adapter interface — a SolanaModule would implement the same contract-read / signature-recovery interface
  • TrustedClient sealing works with any native client type
  • WASM modules (TFHE, TKMS) are chain-independent — encryption and decryption are purely cryptographic

A Solana adapter would follow the same pattern as src/viem/ or src/ethers/: implement EthereumModule (or a renamed ChainModule) and provide createFhevmClient() that seals the Solana client.


13. Open questions (remaining)

  1. Mock mode — Not yet implemented. Planned as a module replacement: MockEncryptModule and MockDecryptModule that skip WASM entirely for local dev/testing.
  2. Hardhat plugin migration — Plugin needs to be updated to consume @fhevm/sdk instead of @zama-fhe/relayer-sdk.
  3. Monorepo move — The SDK still lives in its own repository. Moving into fhevm monorepo is still planned but not yet executed.
  4. Package naming — Settled on @fhevm/sdk with subpath exports. The originally debated names (relayer-sdk, protocol-sdk, js-sdk) were not used.

14. Conclusion

The restructuring achieved the original goals:

  • Clear separation of concerns — Core / modules / adapters / actions are strictly layered
  • Library-agnostic core — Zero ethers/viem imports in src/core/
  • Tree-shakeable — Viem users never bundle ethers, encrypt-only clients skip TKMS WASM
  • Safe by default — Symbol-based access control, opaque sealing, frozen objects, private fields
  • Composable — Actions as standalone functions, decorator chaining, module extension
  • Single-package simplicity — Multiple entry points, one version, one publish — with the same modularity as 6+ packages

The result is a low-level, composable SDK that higher-level libraries (Hardhat plugin, React hooks, wallet SDKs) can safely build upon.