Skip to content

Commit 9bdd1fe

Browse files
authored
Merge pull request #149 from OpenZeppelin/zama-integration-improvements
chore: Improve Zama Relayer integration docs
2 parents 046eaed + 1f4df86 commit 9bdd1fe

4 files changed

Lines changed: 196 additions & 154 deletions

File tree

content/relayer/1.4.x/guides/zama-fhevm-counter-guide.mdx

Lines changed: 57 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,29 @@ title: Zama FHEVM Counter Guide
44

55
## Overview
66

7-
This guide walks through an end-to-end integration between OpenZeppelin Relayer and a Zama FHEVM contract, using the counter example shipped with the [OpenZeppelin Relayer SDK](https://github.com/OpenZeppelin/openzeppelin-relayer-sdk). The counter contract is deployed from [Zama's fhevm-hardhat-template](https://github.com/zama-ai/fhevm-hardhat-template) and demonstrates the two relayer responsibilities in an FHEVM flow:
7+
This guide walks through an end-to-end integration between the OpenZeppelin Relayer and a Zama FHEVM contract, using the counter example shipped with the [OpenZeppelin Relayer SDK](https://github.com/OpenZeppelin/openzeppelin-relayer-sdk). The counter contract is deployed from [Zama's fhevm-hardhat-template](https://github.com/zama-ai/fhevm-hardhat-template) and demonstrates the two OpenZeppelin Relayer responsibilities in an FHEVM flow:
88

99
- **Transaction submission**: sending an encrypted `increment()` call on-chain.
1010
- **EIP-712 signing**: signing the typed-data payload that authorizes user decryption of the counter's encrypted state.
1111

12+
<Callout type="info">
13+
14+
**Terminology.** In this guide:
15+
16+
- **OpenZeppelin Relayer** (or "OZ Relayer") — this service. It holds an EVM signer, submits on-chain transactions, and signs EIP-712 payloads.
17+
- **Zama Relayer** — the Zama-operated service that the [`@zama-fhe/relayer-sdk`](https://www.npmjs.com/package/@zama-fhe/relayer-sdk) talks to under the hood. It serves FHE public keys and routes decryption requests to the Zama KMS / coprocessor. Applications interact with it only indirectly through the Zama Relayer SDK. It holds no OpenZeppelin key material and does not sign EIP-712.
18+
19+
</Callout>
20+
1221
By the end of this guide you will have:
1322

1423
- Configured a Zama FHE instance in your application.
1524
- Read and decrypted an encrypted counter value from the contract.
16-
- Submitted an encrypted increment through the relayer and waited for confirmation.
25+
- Submitted an encrypted increment through the OpenZeppelin Relayer and waited for confirmation.
1726
- Re-read and decrypted the updated value.
1827

1928
<Callout>
20-
The FHE encryption/decryption primitives run in your application using the Zama SDK. The relayer never sees cleartext values or your decryption keypair — it only sends transactions and signs EIP-712 payloads.
29+
The FHE encryption/decryption primitives run in your application using the Zama Relayer SDK. The OpenZeppelin Relayer never sees cleartext values or your decryption keypair — it only sends transactions and signs EIP-712 payloads.
2130
</Callout>
2231

2332
## Prerequisites
@@ -42,29 +51,32 @@ The rest of this guide explains the moving parts, references the example files,
4251

4352
Four components participate in the flow:
4453

45-
- **Your script**: orchestrates the flow and prints progress.
46-
- **OpenZeppelin Relayer**: signs typed data and submits transactions.
47-
- **Zama Relayer SDK instance**: encrypts inputs and manages decryption flows.
48-
- **FHEVM contract**: stores encrypted state on-chain.
54+
- **Your script** orchestrates the flow and prints progress.
55+
- **OpenZeppelin Relayer**signs typed data and submits transactions. The only component holding an EVM signer.
56+
- **Zama Relayer SDK (client)** + **Zama Relayer (service)** — the SDK encrypts inputs, generates keypairs for user decryption, and builds EIP-712 payloads; the Zama Relayer service serves FHE public keys and routes decryption requests to the Zama KMS / coprocessor. The application only interacts with it indirectly through the SDK.
57+
- **FHEVM contract** stores encrypted state on-chain.
4958

50-
The key boundary is that the relayer does not do any encryption. The Zama SDK handles encryption and decryption primitives; the relayer provides transaction execution and signature authorization.
59+
The key boundary is that the OpenZeppelin Relayer does not do any encryption or decryption. The Zama Relayer SDK (and, behind it, the Zama Relayer service) handles all FHE primitives; the OpenZeppelin Relayer only provides EVM transaction execution and EIP-712 signing.
5160

5261
```
53-
┌─────────────┐ ┌───────────────────────┐ ┌──────────────┐
54-
│ Your app │──────▶│ OpenZeppelin Relayer │──────▶│ FHEVM network│
55-
│ (Zama SDK) │ │ sendTransaction + │ │ Sepolia / │
56-
│ │◀──────│ signTypedData │ │ Mainnet │
57-
└─────────────┘ └───────────────────────┘ └──────────────┘
62+
┌─────────────┐ ┌─────────────────────────┐ ┌──────────────┐
63+
│ Your app │──────▶│ OpenZeppelin Relayer │──────▶│ FHEVM network│
64+
│ (Zama SDK) │ │ sendTransaction + │ │ Sepolia / │
65+
│ │◀──────│ signTypedData │ │ Mainnet │
66+
└─────────────┘ └─────────────────────────┘ └──────────────┘
5867
5968
60-
Zama Gateway
61-
(public decrypt /
62-
user decrypt)
69+
┌─────────────────────────────┐
70+
│ Zama Relayer │
71+
│ FHE public keys + │
72+
│ decryption request routing │
73+
│ (accessed via Zama SDK) │
74+
└─────────────────────────────┘
6375
```
6476

65-
## Relayer Configuration
77+
## OpenZeppelin Relayer Configuration
6678

67-
Zama FHEVM contracts live on standard EVM networks, so the relayer is configured as a regular `evm` relayer.
79+
Zama FHEVM contracts live on standard EVM networks, so the OpenZeppelin Relayer is configured as a regular `evm` relayer.
6880

6981
```json
7082
{
@@ -96,9 +108,9 @@ Zama FHEVM contracts live on standard EVM networks, so the relayer is configured
96108

97109
**Important notes:**
98110

99-
- The relayer signer is used both for submitting the encrypted transaction and for signing the EIP-712 payload consumed by the Zama Gateway during user decryption. The same key backs both operations.
111+
- The OpenZeppelin Relayer's signer is used both for submitting the encrypted transaction and for signing the EIP-712 payload that the Zama Relayer SDK requires for user decryption. The same key backs both operations.
100112
- For production, prefer a hosted signer (AWS KMS, Google Cloud KMS, Turnkey, CDP) over `local`.
101-
- The relayer must be funded on the target network so it can pay gas for FHEVM contract calls.
113+
- The OpenZeppelin Relayer must be funded on the target network so it can pay gas for FHEVM contract calls.
102114

103115
## Installation
104116

@@ -130,7 +142,7 @@ RELAYER_BASE_PATH=http://localhost:8080
130142
# ZAMA_PRIVATE_KEY=
131143
```
132144

133-
- `RELAYER_BASE_PATH` defaults to `http://localhost:8080` if not set.
145+
- `RELAYER_BASE_PATH` defaults to `http://localhost:8080` if not set. This points at your OpenZeppelin Relayer.
134146
- `RPC_URL` defaults to the public Sepolia RPC if not set.
135147
- If `ZAMA_PUBLIC_KEY` and `ZAMA_PRIVATE_KEY` are not set, the script generates a fresh decryption keypair on each run. Reusing the same keypair is useful for consistent user-decryption behavior across runs.
136148

@@ -145,8 +157,8 @@ npx ts-node examples/relayers/zama/counter.ts
145157
The script should:
146158

147159
1. Read the encrypted counter handle from the contract.
148-
2. Attempt decryption (public first, then user decryption with a relayer-signed EIP-712 payload).
149-
3. Submit an encrypted `increment()` through the relayer and poll until the transaction is mined or confirmed.
160+
2. Attempt decryption (public first, then user decryption with an OpenZeppelin Relayer signed EIP-712 payload).
161+
3. Submit an encrypted `increment()` through the OpenZeppelin Relayer and poll until the transaction is mined or confirmed.
150162
4. Re-read and decrypt the updated counter value.
151163

152164
## Generating a Reusable Decryption Keypair
@@ -160,14 +172,14 @@ npx ts-node examples/relayers/zama/generate-keypair.ts
160172
Copy the printed `ZAMA_PUBLIC_KEY` and `ZAMA_PRIVATE_KEY` values into your `.env` file.
161173

162174
<Callout>
163-
The decryption keypair is an application-side secret. Do not pass the private key to the relayer; it is only used by the Zama SDK to decrypt results returned by the Gateway.
175+
The decryption keypair is an application side secret. Do not pass the private key to the OpenZeppelin Relayer; it is only used by the Zama Relayer SDK to decrypt results returned by the Zama Relayer.
164176
</Callout>
165177

166178
## Walkthrough
167179

168-
The following snippets show the relayer-specific integration points from `counter.ts`. Full code is in the [SDK repository](https://github.com/OpenZeppelin/openzeppelin-relayer-sdk/tree/main/examples/relayers/zama).
180+
The following snippets show the OpenZeppelin Relayer's specific integration points from `counter.ts`. Full code is in the [SDK repository](https://github.com/OpenZeppelin/openzeppelin-relayer-sdk/tree/main/examples/relayers/zama).
169181

170-
### 1. Initialize the relayer client and Zama SDK
182+
### 1. Initialize the OpenZeppelin Relayer client and Zama SDK
171183

172184
```typescript
173185
import { Configuration, RelayersApi } from '@openzeppelin/relayer-sdk';
@@ -190,9 +202,9 @@ const provider = new JsonRpcProvider(process.env.RPC_URL!);
190202
const instance = await createInstance(zamaConfig);
191203
```
192204

193-
### 2. Fetch the relayer's on-chain address
205+
### 2. Fetch the OpenZeppelin Relayer's on-chain address
194206

195-
The relayer's EVM address is needed both when building encrypted inputs and when authorizing user decryption.
207+
The OpenZeppelin Relayer's EVM address is needed both when building encrypted inputs and when authorizing user decryption.
196208

197209
```typescript
198210
const relayerInfo = await relayersApi.getRelayer(process.env.RELAYER_ID!);
@@ -201,7 +213,7 @@ const relayerAddress = getAddress(relayerInfo.data.data!.address!);
201213

202214
### 3. Read and decrypt the encrypted counter
203215

204-
The contract exposes a `getCount()` view that returns the encrypted handle. Decoding the handle is a plain EVM call — no relayer involvement.
216+
The contract exposes a `getCount()` view that returns the encrypted handle. Decoding the handle is a plain EVM call — no OpenZeppelin Relayer involvement.
205217

206218
```typescript
207219
import { Interface } from 'ethers';
@@ -218,9 +230,9 @@ async function getCount(contractAddress: string): Promise<string> {
218230

219231
The script first attempts public decryption. If the handle is not publicly decryptable, it falls back to user decryption (covered in [Decryption Model](#decryption-model) below).
220232

221-
### 4. Submit an encrypted `increment()` via the relayer
233+
### 4. Submit an encrypted `increment()` via the OpenZeppelin Relayer
222234

223-
Encryption happens locally with the Zama SDK. The encrypted handle plus input proof are encoded into a normal EVM transaction and handed to `sendTransaction`.
235+
Encryption happens locally with the Zama Relayer SDK. The encrypted handle plus input proof are encoded into a normal EVM transaction and handed to the OpenZeppelin Relayer's `sendTransaction`.
224236

225237
```typescript
226238
import { Speed } from '@openzeppelin/relayer-sdk';
@@ -244,7 +256,7 @@ const transactionId = txResponse.data.data!.id!;
244256

245257
### 5. Poll for confirmation
246258

247-
Poll `getTransactionById` until the transaction reaches `mined` or `confirmed`:
259+
Poll `getTransactionById` on the OpenZeppelin Relayer until the transaction reaches `mined` or `confirmed`:
248260

249261
```typescript
250262
import type { EvmTransactionResponse } from '@openzeppelin/relayer-sdk';
@@ -256,7 +268,7 @@ async function waitForConfirmation(transactionId: string): Promise<string | unde
256268
const status = await relayersApi.getTransactionById(process.env.RELAYER_ID!, transactionId);
257269
const tx = status.data.data as EvmTransactionResponse | undefined;
258270

259-
if (!tx) throw new Error(`Transaction ${transactionId} not returned by relayer`);
271+
if (!tx) throw new Error(`Transaction ${transactionId} not returned by the OpenZeppelin Relayer`);
260272
if (tx.status === 'mined' || tx.status === 'confirmed') return tx.hash;
261273
if (tx.status === 'failed' || tx.status === 'canceled' || tx.status === 'expired') {
262274
throw new Error(`Transaction ${tx.status}: ${tx.status_reason ?? 'unknown'}`);
@@ -272,16 +284,16 @@ Zama FHEVM supports two decryption paths, and the example tries them in this ord
272284

273285
### 1. Public Decryption
274286

275-
If an encrypted handle is marked as publicly decryptable on-chain, the Zama SDK can decrypt it directly, with no authorization from the relayer.
287+
If an encrypted handle is marked as publicly decryptable on-chain, the Zama Relayer SDK can decrypt it directly via the Zama Relayer, with no involvement from the OpenZeppelin Relayer.
276288

277289
```typescript
278290
const result = await instance.publicDecrypt([encryptedHandle]);
279291
const clear = result.clearValues[encryptedHandle as `0x${string}`];
280292
```
281293

282-
### 2. User Decryption (relayer-signed)
294+
### 2. User Decryption (OpenZeppelin Relayer's signed EIP-712)
283295

284-
When decryption requires authorization, the Zama SDK builds an EIP-712 payload and the relayer signs it via `signTypedData`. The signature is then passed to `userDecrypt`, which retrieves the cleartext from the Zama Gateway.
296+
When decryption requires authorization, the Zama Relayer SDK builds an EIP-712 payload and the **OpenZeppelin Relayer** signs it via `signTypedData`. The resulting signature is passed to `userDecrypt`, which uses it to authorize the Zama Relayer to return the cleartext.
285297

286298
```typescript
287299
import { TypedDataEncoder } from 'ethers';
@@ -299,7 +311,7 @@ const eip712 = instance.createEIP712(
299311
durationDays,
300312
);
301313

302-
// Hash the domain and struct, then ask the relayer to sign.
314+
// Hash the domain and struct, then ask the OpenZeppelin Relayer to sign.
303315
const domainSeparator = TypedDataEncoder.hashDomain(eip712.domain);
304316
const { EIP712Domain, ...structTypes } = eip712.types;
305317
const messageHash = TypedDataEncoder.hashStruct(
@@ -326,13 +338,13 @@ const decrypted = await instance.userDecrypt(
326338
);
327339
```
328340

329-
The EIP-712 domain separator and message hash are computed locally using `ethers` so the relayer only has to sign the final hashes via its `signTypedData` endpoint.
341+
The EIP-712 domain separator and message hash are computed locally using `ethers` so the OpenZeppelin Relayer only has to sign the final hashes via its `signTypedData` endpoint.
330342

331343
## Using on Mainnet
332344

333345
The example defaults to Sepolia. To run against Ethereum mainnet:
334346

335-
1. In `counter.ts`, use `MainnetConfig` instead of `SepoliaConfig` and provide the Zama Gateway API key:
347+
1. In `counter.ts`, use `MainnetConfig` instead of `SepoliaConfig` and provide the Zama Relayer API key:
336348

337349
```typescript
338350
import {
@@ -355,26 +367,26 @@ The example defaults to Sepolia. To run against Ethereum mainnet:
355367
RPC_URL=https://ethereum-rpc.publicnode.com
356368
```
357369

358-
3. Point `ZAMA_CONTRACT_ADDRESS` at your mainnet contract and configure the relayer for Ethereum mainnet.
370+
3. Point `ZAMA_CONTRACT_ADDRESS` at your mainnet contract and configure the OpenZeppelin Relayer for Ethereum mainnet.
359371

360372
<Callout>
361-
Mainnet requires API key authentication with the Zama Gateway. See the [Zama mainnet API key guide](https://docs.zama.org/protocol/relayer-sdk-guides/fhevm-relayer/mainnet-api-key) for instructions on obtaining one.
373+
Mainnet requires API key authentication with the Zama Relayer. See the [Zama mainnet API key guide](https://docs.zama.org/protocol/relayer-sdk-guides/fhevm-relayer/mainnet-api-key) for instructions on obtaining one.
362374
</Callout>
363375

364376
## Current Limitations
365377

366378
- The example is hardcoded for Sepolia via `SepoliaConfig` plus an explicit Sepolia RPC URL.
367379
- It assumes a counter contract shape compatible with the included ABI.
368380
- Logging and error handling are intentionally simple — this is a demo script, not production code.
369-
- It does not cover relayer creation or contract deployment.
381+
- It does not cover OpenZeppelin Relayer creation or contract deployment.
370382

371383
## Troubleshooting
372384

373385
- **`Missing required environment variable`**: one of the required values in `.env` is unset.
374-
- **`did not return an address`**: the configured relayer id is valid for the API, but the response did not include an EVM address. Check that the relayer is fully provisioned and the signer is reachable.
386+
- **`did not return an address`**: the configured `RELAYER_ID` is valid for the API, but the response did not include an EVM address. Check that the OpenZeppelin Relayer is fully provisioned and the signer is reachable.
375387
- **`Public decryption failed`**: this can be expected depending on the contract and permissions. The script will then try user decryption.
376-
- **`User decryption failed`**: check that the relayer can sign typed data correctly and that the contract address and decryption keypair are the ones you expect. Confirm the EIP-712 `startTimeStamp`/`durationDays` window is valid.
377-
- **Transaction polling timeout**: the transaction may still be pending, the relayer may be unhealthy, or the target chain may be slow. Inspect `getTransactionById` directly and check relayer logs.
388+
- **`User decryption failed`**: check that the OpenZeppelin Relayer can sign typed data correctly and that the contract address and decryption keypair are the ones you expect. Confirm the EIP-712 `startTimeStamp` / `durationDays` window is valid.
389+
- **Transaction polling timeout**: the transaction may still be pending, the OpenZeppelin Relayer may be unhealthy, or the target chain may be slow. Inspect `getTransactionById` directly and check OpenZeppelin Relayer logs.
378390

379391
## Additional Resources
380392

0 commit comments

Comments
 (0)