diff --git a/docs/pages/sdk.mdx b/docs/pages/sdk.mdx index 92522ff639..b4076dc31d 100644 --- a/docs/pages/sdk.mdx +++ b/docs/pages/sdk.mdx @@ -37,9 +37,16 @@ export default defineConfig({ ```ts import { EnclaveSDK, EnclaveEventType, RegistryEventType } from '@enclave-e3/sdk' import { createPublicClient, createWalletClient, http, custom } from 'viem' +import { sepolia } from 'viem/chains' -const publicClient = createPublicClient({ transport: http(import.meta.env.VITE_RPC_URL) }) -const walletClient = createWalletClient({ transport: custom(window.ethereum) }) +const publicClient = createPublicClient({ + chain: sepolia, + transport: http(import.meta.env.VITE_RPC_URL), +}) +const walletClient = createWalletClient({ + chain: sepolia, + transport: custom(window.ethereum), +}) const sdk = new EnclaveSDK({ publicClient, @@ -47,11 +54,30 @@ const sdk = new EnclaveSDK({ contracts: { enclave: import.meta.env.VITE_ENCLAVE_ADDRESS, ciphernodeRegistry: import.meta.env.VITE_REGISTRY_ADDRESS, + feeToken: import.meta.env.VITE_FEE_TOKEN_ADDRESS, }, - chainId: 11155111, + chain: sepolia, + thresholdBfvParamsPresetName: 'INSECURE_THRESHOLD_512', }) +``` + +For server-side or simpler setups, use the factory method: -await sdk.initialize() +```ts +import { EnclaveSDK } from '@enclave-e3/sdk' +import { sepolia } from 'viem/chains' + +const sdk = EnclaveSDK.create({ + rpcUrl: import.meta.env.VITE_RPC_URL, + contracts: { + enclave: import.meta.env.VITE_ENCLAVE_ADDRESS, + ciphernodeRegistry: import.meta.env.VITE_REGISTRY_ADDRESS, + feeToken: import.meta.env.VITE_FEE_TOKEN_ADDRESS, + }, + chain: sepolia, + privateKey: '0x...', // optional — omit for read-only + thresholdBfvParamsPresetName: 'INSECURE_THRESHOLD_512', +}) ``` ### Requesting computations @@ -59,8 +85,10 @@ await sdk.initialize() ```ts const hash = await sdk.requestE3({ threshold: [2, 3], - startWindow: [BigInt(Date.now()), BigInt(Date.now() + 5 * 60 * 1000)], - duration: BigInt(1800), + inputWindow: [ + BigInt(Math.floor(Date.now() / 1000)), + BigInt(Math.floor(Date.now() / 1000) + 5 * 60), + ], e3Program: '0x...', e3ProgramParams: '0x', computeProviderParams: '0x', @@ -91,31 +119,65 @@ sdk.off(EnclaveEventType.E3_REQUESTED, e3Handler) import { useEnclaveSDK } from '@enclave-e3/react' export function Dashboard() { - const { isInitialized, requestE3, onEnclaveEvent, EnclaveEventType } = useEnclaveSDK({ + const { isInitialized, requestE3, onEnclaveEvent, off, EnclaveEventType } = useEnclaveSDK({ autoConnect: true, contracts: { enclave: import.meta.env.VITE_ENCLAVE_ADDRESS, ciphernodeRegistry: import.meta.env.VITE_REGISTRY_ADDRESS, + feeToken: import.meta.env.VITE_FEE_TOKEN_ADDRESS, }, - chainId: 31337, + thresholdBfvParamsPresetName: 'INSECURE_THRESHOLD_512', }) useEffect(() => { if (!isInitialized) return - const handle = (event) => console.log('Activated', event.data) - onEnclaveEvent(EnclaveEventType.E3_ACTIVATED, handle) - return () => off(EnclaveEventType.E3_ACTIVATED, handle) + const handle = (event) => console.log('E3 Requested', event.data) + onEnclaveEvent(EnclaveEventType.E3_REQUESTED, handle) + return () => off(EnclaveEventType.E3_REQUESTED, handle) }, [isInitialized]) return } ``` +The hook uses wagmi's `usePublicClient` and `useWalletClient` internally, so your app must be inside +a wagmi provider. + +## Encryption functions + +The SDK includes standalone FHE encryption functions that can be called via the SDK instance or +imported directly: + +```ts +// Via SDK instance (uses the configured preset name automatically) +const publicKey = await sdk.generatePublicKey() +const encrypted = await sdk.encryptNumber(42n, publicKey) +const { encryptedData, proof } = await sdk.encryptNumberAndGenProof(42n, publicKey) + +// Standalone import (pass preset name explicitly) +import { generatePublicKey, encryptNumber } from '@enclave-e3/sdk' + +const pk = await generatePublicKey('INSECURE_THRESHOLD_512') +const ct = await encryptNumber(42n, pk, 'INSECURE_THRESHOLD_512') +``` + +## Modular sub-module imports + +The SDK is organized into three sub-modules (`contracts`, `events`, `encryption`) that can be +imported independently for tree-shaking: + +```ts +import { generatePublicKey, encryptNumber } from '@enclave-e3/sdk/crypto' +import { ContractClient } from '@enclave-e3/sdk/contracts' +import { EventListener, EnclaveEventType } from '@enclave-e3/sdk/events' +``` + +All exports are also available from the main `@enclave-e3/sdk` entry point. + ## Configuration contract map -Provide both the Enclave and CiphernodeRegistry addresses. The template exposes these via `.env` / -`enclave.config.yaml`. For multi-chain apps, instantiate multiple SDKs or call -`sdk.updateConfig({ contracts: { ... }, chainId })` whenever wallets switch networks. +Provide the Enclave, CiphernodeRegistry, and FeeToken addresses. The template exposes these via +`.env` / `enclave.config.yaml`. For multi-chain apps, instantiate separate SDK instances per chain. ## Working with the template @@ -151,20 +213,24 @@ that wraps or complements the core Enclave SDK. - **Historical events**: `sdk.getHistoricalEvents(type, fromBlock, toBlock)` fetches logs without a WebSocket provider; useful for CRON jobs or health checks. -- **Gas controls**: pass `gasLimit` to `requestE3`, `activateE3`, or `publishInput` if you want - fixed limits when interacting with Enclave on L2s. +- **Gas controls**: pass `gasLimit` to `requestE3`, `publishCiphertextOutput`, or other write + methods if you want fixed limits when interacting with Enclave on L2s. - **Event polling**: `sdk.startEventPolling()` is handy in serverless environments where websockets are unavailable. - **Custom transports**: on the server, call `createWalletClient({ account, transport: http(rpc) })` and load the private key from Vaults/KMS. +- **Get E3 quote**: call `sdk.getE3Quote(requestParams)` to get the fee amount required for an E3 + request. +- **Fee token approval**: call `sdk.approveFeeToken(amount)` before `requestE3` to approve the fee + token spend. ## Troubleshooting -| Symptom | Triage | -| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | -| `MISSING_PUBLIC_CLIENT` errors | Ensure `createPublicClient` uses an HTTP or WebSocket RPC reachable from the app. | -| `INVALID_ADDRESS` | Double-check contract addresses passed into the SDK match the current chain. | -| Hooks never initialize | Confirm `autoConnect` is true, wallet is connected, and React component is inside a provider that renders on the client. | -| No events arrive | Switch to a WebSocket RPC or call `sdk.startEventPolling()` so logs are polled over HTTP. | +| Symptom | Triage | +| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ | +| `MISSING_PUBLIC_CLIENT` errors | Ensure `createPublicClient` uses an HTTP or WebSocket RPC reachable from the app. | +| `INVALID_ADDRESS` | Double-check contract addresses passed into the SDK match the current chain. | +| Hooks never initialize | Confirm `autoConnect` is true, wallet is connected, and React component is inside a wagmi provider that renders on the client. | +| No events arrive | Switch to a WebSocket RPC or call `sdk.startEventPolling()` so logs are polled over HTTP. | For additional API surface details see `packages/enclave-sdk/README.md` inside this repo. diff --git a/examples/CRISP/packages/crisp-sdk/tests/vote.test.ts b/examples/CRISP/packages/crisp-sdk/tests/vote.test.ts index 9b7c2fdb1c..52544a8bd9 100644 --- a/examples/CRISP/packages/crisp-sdk/tests/vote.test.ts +++ b/examples/CRISP/packages/crisp-sdk/tests/vote.test.ts @@ -35,8 +35,7 @@ describe('Vote', () => { json: async () => ({ ciphertext: previousCiphertext }), }) as Response - const mockPreviousCiphertextNotFoundResponse = () => - ({ ok: false, status: 404 }) as Response + const mockPreviousCiphertextNotFoundResponse = () => ({ ok: false, status: 404 }) as Response beforeEach(() => { vi.clearAllMocks() diff --git a/packages/enclave-react/src/useEnclaveSDK.ts b/packages/enclave-react/src/useEnclaveSDK.ts index 9f47a26544..28be4dde3e 100644 --- a/packages/enclave-react/src/useEnclaveSDK.ts +++ b/packages/enclave-react/src/useEnclaveSDK.ts @@ -23,22 +23,18 @@ export interface UseEnclaveSDKConfig { ciphernodeRegistry: `0x${string}` feeToken: `0x${string}` } - chainId?: number autoConnect?: boolean - thresholdBfvParamsPresetName: ThresholdBfvParamsPresetName + thresholdBfvParamsPresetName?: ThresholdBfvParamsPresetName } export interface UseEnclaveSDKReturn { sdk: EnclaveSDK | null isInitialized: boolean error: string | null - // Contract interaction methods (only the ones commonly used) requestE3: typeof EnclaveSDK.prototype.requestE3 getThresholdBfvParamsSet: typeof EnclaveSDK.prototype.getThresholdBfvParamsSet - // Event handling onEnclaveEvent: (eventType: T, callback: EventCallback) => void off: (eventType: T, callback: EventCallback) => void - // Event types for convenience EnclaveEventType: typeof EnclaveEventType RegistryEventType: typeof RegistryEventType } @@ -64,7 +60,8 @@ export interface UseEnclaveSDKReturn { * autoConnect: true, * contracts: { * enclave: '0x...', - * ciphernodeRegistry: '0x...' + * ciphernodeRegistry: '0x...', + * feeToken: '0x...', * }, * thresholdBfvParamsPresetName: 'INSECURE_THRESHOLD_512', * }); @@ -102,12 +99,10 @@ export const useEnclaveSDK = (config: UseEnclaveSDKConfig): UseEnclaveSDKReturn ciphernodeRegistry: '0x0000000000000000000000000000000000000000', feeToken: '0x0000000000000000000000000000000000000000', }, - chainId: config.chainId, thresholdBfvParamsPresetName: config.thresholdBfvParamsPresetName, } const newSdk = new EnclaveSDK(sdkConfig) - await newSdk.initialize() setSdk(newSdk) sdkRef.current = newSdk setIsInitialized(true) @@ -116,7 +111,7 @@ export const useEnclaveSDK = (config: UseEnclaveSDKConfig): UseEnclaveSDKReturn setError(errorMessage) console.error('SDK initialization failed:', err) } - }, [publicClient, walletClient, config.contracts, config.chainId, config.thresholdBfvParamsPresetName]) + }, [publicClient, walletClient, config.contracts, config.thresholdBfvParamsPresetName]) // Initialize SDK when wagmi clients are available useEffect(() => { @@ -146,7 +141,6 @@ export const useEnclaveSDK = (config: UseEnclaveSDKConfig): UseEnclaveSDKReturn return sdk.getThresholdBfvParamsSet() }, [sdk]) - // Contract interaction methods const requestE3 = useCallback( (...args: Parameters) => { if (!sdk) throw new Error('SDK not initialized') @@ -155,7 +149,6 @@ export const useEnclaveSDK = (config: UseEnclaveSDKConfig): UseEnclaveSDKReturn [sdk], ) - // Event handling methods const onEnclaveEvent = useCallback( (eventType: T, callback: EventCallback) => { if (!sdk) throw new Error('SDK not initialized') diff --git a/packages/enclave-sdk/README.md b/packages/enclave-sdk/README.md index 140fdc149b..d069b55c0f 100644 --- a/packages/enclave-sdk/README.md +++ b/packages/enclave-sdk/README.md @@ -8,7 +8,10 @@ real-time event listening, contract interaction methods, and comprehensive error - **Event-driven architecture**: Listen to smart contract events in real-time - **Type-safe**: Built with TypeScript and uses generated types from contracts - **Easy contract interactions**: Simple methods for reading from and writing to contracts -- **React integration**: Includes React hooks for easy frontend integration +- **React integration**: Includes React hooks for easy frontend integration (via + `@enclave-e3/react`) +- **Modular architecture**: Tree-shakeable sub-modules for contracts, events, and encryption +- **Encryption helpers**: Standalone FHE encryption functions with optional ZK proof generation - **Error handling**: Comprehensive error handling with custom error types - **Gas estimation**: Built-in gas estimation for transactions - **Event polling**: Support for both WebSocket and polling-based event listening @@ -24,13 +27,16 @@ pnpm add @enclave-e3/sdk ```typescript import { EnclaveSDK, EnclaveEventType, RegistryEventType } from '@enclave-e3/sdk' import { createPublicClient, createWalletClient, http, custom } from 'viem' +import { sepolia } from 'viem/chains' // Initialize clients const publicClient = createPublicClient({ + chain: sepolia, transport: http('YOUR_RPC_URL'), }) const walletClient = createWalletClient({ + chain: sepolia, transport: custom(window.ethereum), }) @@ -41,27 +47,25 @@ const sdk = new EnclaveSDK({ contracts: { enclave: '0x...', // Your Enclave contract address ciphernodeRegistry: '0x...', // Your CiphernodeRegistry contract address + feeToken: '0x...', // Your ERC-20 fee token address }, - chainId: 1, // Optional + chain: sepolia, + thresholdBfvParamsPresetName: 'INSECURE_THRESHOLD_512', }) -// Initialize the SDK -await sdk.initialize() - // Listen to events with the unified event system sdk.onEnclaveEvent(EnclaveEventType.E3_REQUESTED, (event) => { console.log('E3 Requested:', event.data) }) -sdk.onEnclaveEvent(RegistryEventType.CIPHERNODE_ADDED, (event) => { - console.log('Ciphernode Added:', event.data) +sdk.onEnclaveEvent(RegistryEventType.COMMITTEE_REQUESTED, (event) => { + console.log('Committee Requested:', event.data) }) // Interact with contracts const hash = await sdk.requestE3({ threshold: [1, 3], - startWindow: [BigInt(0), BigInt(100)], - duration: BigInt(3600), + inputWindow: [BigInt(0), BigInt(100)], e3Program: '0x...', e3ProgramParams: '0x...', computeProviderParams: '0x...', @@ -69,6 +73,29 @@ const hash = await sdk.requestE3({ }) ``` +### Factory Method + +For a simpler setup (especially on the server), use the static `EnclaveSDK.create()` factory: + +```typescript +import { EnclaveSDK } from '@enclave-e3/sdk' +import { sepolia } from 'viem/chains' + +const sdk = EnclaveSDK.create({ + rpcUrl: 'wss://sepolia.example.com', + contracts: { + enclave: '0x...', + ciphernodeRegistry: '0x...', + feeToken: '0x...', + }, + chain: sepolia, + privateKey: '0x...', // optional — omit for read-only + thresholdBfvParamsPresetName: 'INSECURE_THRESHOLD_512', +}) +``` + +The factory auto-detects HTTP vs WebSocket transports and creates the appropriate viem clients. + ## Usage within a browser Usage within a typescript project should work out of the box, however in order to use wasm related @@ -105,8 +132,6 @@ The SDK uses a unified event system with TypeScript enums for type safety: enum EnclaveEventType { // E3 Lifecycle E3_REQUESTED = 'E3Requested', - E3_ACTIVATED = 'E3Activated', - INPUT_PUBLISHED = 'InputPublished', CIPHERTEXT_OUTPUT_PUBLISHED = 'CiphertextOutputPublished', PLAINTEXT_OUTPUT_PUBLISHED = 'PlaintextOutputPublished', @@ -114,10 +139,16 @@ enum EnclaveEventType { E3_PROGRAM_ENABLED = 'E3ProgramEnabled', E3_PROGRAM_DISABLED = 'E3ProgramDisabled', + // Encryption Scheme Management + ENCRYPTION_SCHEME_ENABLED = 'EncryptionSchemeEnabled', + ENCRYPTION_SCHEME_DISABLED = 'EncryptionSchemeDisabled', + // Configuration CIPHERNODE_REGISTRY_SET = 'CiphernodeRegistrySet', MAX_DURATION_SET = 'MaxDurationSet', - // ... more events + ALLOWED_E3_PROGRAMS_PARAMS_SET = 'AllowedE3ProgramsParamsSet', + OWNERSHIP_TRANSFERRED = 'OwnershipTransferred', + INITIALIZED = 'Initialized', } ``` @@ -125,13 +156,12 @@ enum EnclaveEventType { ```typescript enum RegistryEventType { - CIPHERNODE_ADDED = 'CiphernodeAdded', - CIPHERNODE_REMOVED = 'CiphernodeRemoved', COMMITTEE_REQUESTED = 'CommitteeRequested', COMMITTEE_PUBLISHED = 'CommitteePublished', COMMITTEE_FINALIZED = 'CommitteeFinalized', ENCLAVE_SET = 'EnclaveSet', - // ... more events + OWNERSHIP_TRANSFERRED = 'OwnershipTransferred', + INITIALIZED = 'Initialized', } ``` @@ -152,51 +182,120 @@ interface EnclaveEvent { ## React Integration -The SDK includes a React hook for easy integration: +The SDK includes a React hook via the `@enclave-e3/react` package: + +```bash +pnpm add @enclave-e3/react +``` ```typescript -import { useEnclaveSDK } from "@enclave-e3/contracts/sdk"; +import { useEnclaveSDK } from '@enclave-e3/react' function MyComponent() { const { sdk, isInitialized, - isConnecting, error, - connectWallet, requestE3, onEnclaveEvent, + off, EnclaveEventType, + RegistryEventType, } = useEnclaveSDK({ contracts: { - enclave: "0x...", - ciphernodeRegistry: "0x...", + enclave: '0x...', + ciphernodeRegistry: '0x...', + feeToken: '0x...', }, - rpcUrl: "YOUR_RPC_URL", autoConnect: true, - }); + thresholdBfvParamsPresetName: 'INSECURE_THRESHOLD_512', + }) useEffect(() => { if (isInitialized) { - onEnclaveEvent(EnclaveEventType.E3_REQUESTED, (event) => { - console.log("New E3 request:", event); - }); + const handler = (event) => { + console.log('New E3 request:', event) + } + onEnclaveEvent(EnclaveEventType.E3_REQUESTED, handler) + return () => off(EnclaveEventType.E3_REQUESTED, handler) } - }, [isInitialized]); + }, [isInitialized]) return (
- {!isInitialized && ( - - )} + {error &&

Error: {error}

} + {!isInitialized &&

Initializing...

} {/* Your UI */}
- ); + ) } ``` +The hook uses wagmi's `usePublicClient` and `useWalletClient` under the hood, so your app must be +wrapped in a wagmi provider. + +## Encryption Functions + +The SDK provides standalone encryption functions for FHE (Fully Homomorphic Encryption) operations. +These can be used via the SDK instance or imported directly for tree-shaking: + +### Via the SDK instance + +```typescript +// Generate a public key +const publicKey = await sdk.generatePublicKey() + +// Encrypt a single number +const encrypted = await sdk.encryptNumber(42n, publicKey) + +// Encrypt a vector +const encryptedVec = await sdk.encryptVector(BigUint64Array.from([1n, 2n, 3n]), publicKey) + +// Encrypt with ZK proof generation +const { encryptedData, proof } = await sdk.encryptNumberAndGenProof(42n, publicKey) +``` + +### Standalone imports + +```typescript +import { + generatePublicKey, + encryptNumber, + encryptVector, + encryptNumberAndGenProof, + encryptVectorAndGenProof, + encryptNumberAndGenInputs, + encryptVectorAndGenInputs, + computePublicKeyCommitment, + getThresholdBfvParamsSet, +} from '@enclave-e3/sdk' + +const presetName = 'INSECURE_THRESHOLD_512' + +const publicKey = await generatePublicKey(presetName) +const encrypted = await encryptNumber(42n, publicKey, presetName) +const { encryptedData, proof } = await encryptNumberAndGenProof(42n, publicKey, presetName) +``` + +## Modular Imports + +The SDK is organized into three sub-modules that can be imported independently for tree-shaking: + +```typescript +// Encryption functions and types +import { generatePublicKey, encryptNumber } from '@enclave-e3/sdk/crypto' + +// Contract client and types +import { ContractClient } from '@enclave-e3/sdk/contracts' +import type { ContractAddresses, E3 } from '@enclave-e3/sdk/contracts' + +// Event listener and types +import { EventListener, EnclaveEventType, RegistryEventType } from '@enclave-e3/sdk/events' +``` + +All sub-module exports are also re-exported from the main `@enclave-e3/sdk` entry point for +convenience. + ## API Reference ### Core Methods @@ -204,11 +303,13 @@ function MyComponent() { #### Contract Interactions ```typescript +// Approve fee token spending +await sdk.approveFeeToken(amount: bigint); + // Request a new E3 computation await sdk.requestE3({ threshold: [number, number], - startWindow: [bigint, bigint], - duration: bigint, + inputWindow: [bigint, bigint], e3Program: `0x${string}`, e3ProgramParams: `0x${string}`, computeProviderParams: `0x${string}`, @@ -216,14 +317,12 @@ await sdk.requestE3({ gasLimit?: bigint }); -// Activate an E3 computation -await sdk.activateE3(e3Id: bigint, gasLimit?: bigint); - -// Publish input data -await sdk.publishInput(e3Id: bigint, data: `0x${string}`, gasLimit?: bigint); +// Publish ciphertext output +await sdk.publishCiphertextOutput(e3Id: bigint, ciphertextOutput: `0x${string}`, proof: `0x${string}`, gasLimit?: bigint); // Read operations const e3Data = await sdk.getE3(e3Id: bigint); +const publicKey = await sdk.getE3PublicKey(e3Id: bigint); ``` #### Event Handling @@ -233,6 +332,8 @@ sdk.onEnclaveEvent(eventType: AllEventTypes, callback: EventCallback); sdk.off(eventType: AllEventTypes, callback: EventCallback); +sdk.once(eventType: AllEventTypes, callback: EventCallback); + const logs = await sdk.getHistoricalEvents( eventType: AllEventTypes, fromBlock?: bigint, @@ -244,6 +345,29 @@ await sdk.startEventPolling(); sdk.stopEventPolling(); ``` +#### Encryption + +```typescript +// Get BFV parameter set +const params = await sdk.getThresholdBfvParamsSet(); + +// Key generation +const publicKey = await sdk.generatePublicKey(); +const commitment = await sdk.computePublicKeyCommitment(publicKey); + +// Encrypt data +const encrypted = await sdk.encryptNumber(data: bigint, publicKey: Uint8Array); +const encryptedVec = await sdk.encryptVector(data: BigUint64Array, publicKey: Uint8Array); + +// Encrypt with proof inputs (for ZK verification) +const { encryptedData, circuitInputs } = await sdk.encryptNumberAndGenInputs(data, publicKey); +const { encryptedData, circuitInputs } = await sdk.encryptVectorAndGenInputs(data, publicKey); + +// Encrypt with full ZK proof generation +const { encryptedData, proof } = await sdk.encryptNumberAndGenProof(data, publicKey); +const { encryptedData, proof } = await sdk.encryptVectorAndGenProof(data, publicKey); +``` + #### Utilities ```typescript @@ -253,9 +377,6 @@ const gas = await sdk.estimateGas(functionName, args, contractAddress, abi, valu // Transaction waiting const receipt = await sdk.waitForTransaction(hash); -// Configuration updates -sdk.updateConfig(newConfig: Partial); - // Cleanup sdk.cleanup(); ``` @@ -269,11 +390,16 @@ interface SDKConfig { contracts: { enclave: `0x${string}` ciphernodeRegistry: `0x${string}` + feeToken: `0x${string}` } - chainId?: number + chain?: Chain + thresholdBfvParamsPresetName: ThresholdBfvParamsPresetName } ``` +`thresholdBfvParamsPresetName` must be one of: `'INSECURE_THRESHOLD_512'` or +`'SECURE_THRESHOLD_8192'`. + ## Error Handling The SDK includes comprehensive error handling: @@ -297,8 +423,8 @@ try { ### Building the SDK ```bash -cd packages/enclave-contracts -pnpm compile +cd packages/enclave-sdk +pnpm build ``` ### Running the Demo @@ -314,19 +440,24 @@ The demo showcases all SDK features including real-time event listening and cont ### Testing ```bash -cd packages/enclave-contracts +cd packages/enclave-sdk pnpm test ``` ## Architecture -The SDK consists of several key components: +The SDK is organized into a modular architecture with three domain-specific sub-modules: + +- **EnclaveSDK** (`enclave-sdk.ts`): Main orchestrator class that delegates to sub-modules +- **Contracts** (`contracts/`): `ContractClient` for contract read/write operations, type + definitions for contract addresses and E3 data +- **Events** (`events/`): `EventListener` for real-time and historical event subscriptions, typed + event enums and data interfaces +- **Encryption** (`encryption/`): Standalone FHE encryption functions, BFV parameter management, ZK + proof generation +- **Utils** (`utils.ts`): Helper functions, error classes, encoding utilities -- **EnclaveSDK**: Main orchestrator class -- **ContractClient**: Handles contract read/write operations -- **EventListener**: Manages real-time event listening -- **Types**: TypeScript definitions with full type safety -- **Utils**: Helper functions and error classes +Each sub-module has its own `index.ts` entry point and can be imported independently. ## License diff --git a/packages/enclave-sdk/package.json b/packages/enclave-sdk/package.json index e47fbc43fa..2e9775383f 100644 --- a/packages/enclave-sdk/package.json +++ b/packages/enclave-sdk/package.json @@ -8,6 +8,21 @@ "import": "./dist/index.js", "require": "./dist/index.cjs", "default": "./dist/index.js" + }, + "./crypto": { + "types": "./dist/crypto/index.d.ts", + "import": "./dist/crypto/index.js", + "require": "./dist/crypto/index.cjs" + }, + "./contracts": { + "types": "./dist/contracts/index.d.ts", + "import": "./dist/contracts/index.js", + "require": "./dist/contracts/index.cjs" + }, + "./events": { + "types": "./dist/events/index.d.ts", + "import": "./dist/events/index.js", + "require": "./dist/events/index.cjs" } }, "main": "./dist/index.cjs", diff --git a/packages/enclave-sdk/src/constants.ts b/packages/enclave-sdk/src/constants.ts new file mode 100644 index 0000000000..8be49ca04b --- /dev/null +++ b/packages/enclave-sdk/src/constants.ts @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +import type { ThresholdBfvParamsPresetName } from './crypto/types' + +export const DEFAULT_THRESHOLD_BFV_PARAMS_PRESET_NAME: ThresholdBfvParamsPresetName = 'INSECURE_THRESHOLD_512' diff --git a/packages/enclave-sdk/src/contract-client.ts b/packages/enclave-sdk/src/contract-client.ts deleted file mode 100644 index 50ff5c16a1..0000000000 --- a/packages/enclave-sdk/src/contract-client.ts +++ /dev/null @@ -1,301 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -// -// This file is provided WITHOUT ANY WARRANTY; -// without even the implied warranty of MERCHANTABILITY -// or FITNESS FOR A PARTICULAR PURPOSE. - -import { Abi, Hash, PublicClient, TransactionReceipt, WalletClient } from 'viem' - -import { CiphernodeRegistryOwnable__factory, Enclave__factory, EnclaveToken__factory } from '@enclave-e3/contracts/types' -import { type E3 } from './types' -import { SDKError, isValidAddress } from './utils' - -export class ContractClient { - private contractInfo: { - enclave: { address: `0x${string}`; abi: Abi } - ciphernodeRegistry: { address: `0x${string}`; abi: Abi } - feeToken: { address: `0x${string}`; abi: Abi } - } | null = null - - constructor( - private publicClient: PublicClient, - private walletClient?: WalletClient, - private addresses: { - enclave: `0x${string}` - ciphernodeRegistry: `0x${string}` - feeToken: `0x${string}` - } = { - enclave: '0x0000000000000000000000000000000000000000', - ciphernodeRegistry: '0x0000000000000000000000000000000000000000', - feeToken: '0x0000000000000000000000000000000000000000', - }, - ) { - if (!isValidAddress(addresses.enclave)) { - throw new SDKError('Invalid Enclave contract address', 'INVALID_ADDRESS') - } - if (!isValidAddress(addresses.ciphernodeRegistry)) { - throw new SDKError('Invalid CiphernodeRegistry contract address', 'INVALID_ADDRESS') - } - if (!isValidAddress(addresses.feeToken)) { - throw new SDKError('Invalid FeeToken contract address', 'INVALID_ADDRESS') - } - } - - /** - * Initialize contract instances - */ - public async initialize(): Promise { - try { - this.contractInfo = { - enclave: { - address: this.addresses.enclave, - abi: Enclave__factory.abi, - }, - ciphernodeRegistry: { - address: this.addresses.ciphernodeRegistry, - abi: CiphernodeRegistryOwnable__factory.abi, - }, - feeToken: { - address: this.addresses.feeToken, - abi: EnclaveToken__factory.abi, - }, - } - } catch (error) { - throw new SDKError(`Failed to initialize contracts: ${error}`, 'INITIALIZATION_FAILED') - } - } - - /** - * Approve the fee token for the Enclave - * approve(address spender, uint256 amount) - */ - public async approveFeeToken(amount: bigint): Promise { - if (!this.walletClient) { - throw new SDKError('Wallet client required for write operations', 'NO_WALLET') - } - - if (!this.contractInfo) { - await this.initialize() - } - - try { - const account = this.walletClient.account - if (!account) { - throw new SDKError('No account connected', 'NO_ACCOUNT') - } - - const { request } = await this.publicClient.simulateContract({ - address: this.addresses.feeToken, - abi: EnclaveToken__factory.abi, - functionName: 'approve', - args: [this.addresses.enclave, amount], - account, - }) - - const hash = await this.walletClient.writeContract(request) - - return hash - } catch (error) { - throw new SDKError(`Failed to approve fee token: ${error}`, 'APPROVE_FEE_TOKEN_FAILED') - } - } - - /** - * Request a new E3 computation - * request(uint32[2] threshold, uint256[2] inputWindow, IE3Program e3Program, bytes e3ProgramParams, bytes computeProviderParams, bytes customParams) - */ - public async requestE3( - threshold: [number, number], - inputWindow: [bigint, bigint], - e3Program: `0x${string}`, - e3ProgramParams: `0x${string}`, - computeProviderParams: `0x${string}`, - customParams?: `0x${string}`, - gasLimit?: bigint, - ): Promise { - if (!this.walletClient) { - throw new SDKError('Wallet client required for write operations', 'NO_WALLET') - } - - if (!this.contractInfo) { - await this.initialize() - } - - try { - const account = this.walletClient.account - if (!account) { - throw new SDKError('No account connected', 'NO_ACCOUNT') - } - - // Simulate transaction - const { request } = await this.publicClient.simulateContract({ - address: this.addresses.enclave, - abi: Enclave__factory.abi, - functionName: 'request', - args: [ - { - threshold, - inputWindow, - e3Program, - e3ProgramParams, - computeProviderParams, - customParams: customParams || '0x', - }, - ], - account, - gas: gasLimit, - }) - - // Execute transaction - const hash = await this.walletClient.writeContract(request) - - return hash - } catch (error) { - throw new SDKError(`Failed to request E3: ${error}`, 'REQUEST_E3_FAILED') - } - } - - /** - * Publish ciphertext output for an E3 computation - * publishCiphertextOutput(uint256 e3Id, bytes memory ciphertextOutput, bytes memory proof) - */ - public async publishCiphertextOutput( - e3Id: bigint, - ciphertextOutput: `0x${string}`, - proof: `0x${string}`, - gasLimit?: bigint, - ): Promise { - if (!this.walletClient) { - throw new SDKError('Wallet client required for write operations', 'NO_WALLET') - } - - if (!this.contractInfo) { - await this.initialize() - } - - try { - const account = this.walletClient.account - if (!account) { - throw new SDKError('No account connected', 'NO_ACCOUNT') - } - - // Simulate transaction - const { request } = await this.publicClient.simulateContract({ - address: this.addresses.enclave, - abi: Enclave__factory.abi, - functionName: 'publishCiphertextOutput', - args: [e3Id, ciphertextOutput, proof], - account, - gas: gasLimit, - }) - - // Execute transaction - const hash = await this.walletClient.writeContract(request) - - return hash - } catch (error) { - throw new SDKError(`Failed to publish ciphertext output: ${error}`, 'PUBLISH_CIPHERTEXT_OUTPUT_FAILED') - } - } - - /** - * Get E3 information - * Based on the contract: getE3(uint256 e3Id) returns (E3 memory e3) - */ - public async getE3(e3Id: bigint): Promise { - if (!this.contractInfo) { - await this.initialize() - } - - try { - const result: E3 = await this.publicClient.readContract({ - address: this.addresses.enclave, - abi: Enclave__factory.abi, - functionName: 'getE3', - args: [e3Id], - }) - - return result - } catch (error) { - throw new SDKError(`Failed to get E3: ${error}`, 'GET_E3_FAILED') - } - } - - /** - * Get the public key for an E3 computation - * Based on the contract: committeePublicKey(uint256 e3Id) returns (bytes32 publicKeyHash) - * @param e3Id - * @returns The public key - */ - public async getE3PublicKey(e3Id: bigint): Promise<`0x${string}`> { - if (!this.contractInfo) { - await this.initialize() - } - - try { - const result: `0x${string}` = await this.publicClient.readContract({ - address: this.addresses.ciphernodeRegistry, - abi: CiphernodeRegistryOwnable__factory.abi, - functionName: 'committeePublicKey', - args: [e3Id], - }) - - return result - } catch (error) { - throw new SDKError(`Failed to get E3 public key: ${error}`, 'GET_E3_PUBLIC_KEY_FAILED') - } - } - - /** - * Estimate gas for a transaction - */ - public async estimateGas( - functionName: string, - args: readonly unknown[], - contractAddress: `0x${string}`, - abi: Abi, - value?: bigint, - ): Promise { - if (!this.walletClient) { - throw new SDKError('Wallet client required for gas estimation', 'NO_WALLET') - } - - try { - const account = this.walletClient.account - if (!account) { - throw new SDKError('No account connected', 'NO_ACCOUNT') - } - - const estimateParams = { - address: contractAddress, - abi, - functionName, - args, - account, - ...(value !== undefined && { value }), - } - - const gas = await this.publicClient.estimateContractGas(estimateParams) - - return gas - } catch (error) { - throw new SDKError(`Failed to estimate gas: ${error}`, 'GAS_ESTIMATION_FAILED') - } - } - - /** - * Wait for transaction confirmation - */ - public async waitForTransaction(hash: Hash): Promise { - try { - const receipt = await this.publicClient.waitForTransactionReceipt({ - hash, - confirmations: 1, - }) - - return receipt - } catch (error) { - throw new SDKError(`Failed to wait for transaction: ${error}`, 'TRANSACTION_WAIT_FAILED') - } - } -} diff --git a/packages/enclave-sdk/src/contracts/contract-client.ts b/packages/enclave-sdk/src/contracts/contract-client.ts new file mode 100644 index 0000000000..b22f988790 --- /dev/null +++ b/packages/enclave-sdk/src/contracts/contract-client.ts @@ -0,0 +1,321 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +import { + type Abi, + type Chain, + type Hash, + type PublicClient, + type TransactionReceipt, + type WalletClient, + createPublicClient, + createWalletClient, + http, + webSocket, +} from 'viem' +import { privateKeyToAccount } from 'viem/accounts' + +import { CiphernodeRegistryOwnable__factory, Enclave__factory, EnclaveToken__factory } from '@enclave-e3/contracts/types' +import type { ContractAddresses, E3, E3RequestParams, E3Stage, FailureReason } from './types' +import { SDKError, isValidAddress } from '../utils' + +export interface ContractClientConfig { + publicClient: PublicClient + walletClient?: WalletClient + contracts: ContractAddresses +} + +export class ContractClient { + private publicClient: PublicClient + private walletClient?: WalletClient + private contracts: ContractAddresses + private contractInfo: { + enclave: { address: `0x${string}`; abi: Abi } + ciphernodeRegistry: { address: `0x${string}`; abi: Abi } + feeToken: { address: `0x${string}`; abi: Abi } + } + + constructor(config: ContractClientConfig) { + const { publicClient, walletClient, contracts } = config + + if (!isValidAddress(contracts.enclave)) { + throw new SDKError('Invalid Enclave contract address', 'INVALID_ADDRESS') + } + if (!isValidAddress(contracts.ciphernodeRegistry)) { + throw new SDKError('Invalid CiphernodeRegistry contract address', 'INVALID_ADDRESS') + } + if (!isValidAddress(contracts.feeToken)) { + throw new SDKError('Invalid FeeToken contract address', 'INVALID_ADDRESS') + } + + this.publicClient = publicClient + this.walletClient = walletClient + this.contracts = contracts + + this.contractInfo = { + enclave: { + address: contracts.enclave, + abi: Enclave__factory.abi, + }, + ciphernodeRegistry: { + address: contracts.ciphernodeRegistry, + abi: CiphernodeRegistryOwnable__factory.abi, + }, + feeToken: { + address: contracts.feeToken, + abi: EnclaveToken__factory.abi, + }, + } + } + + public static create(options: { + rpcUrl: string + contracts: ContractAddresses + privateKey?: `0x${string}` + chain: Chain + }): ContractClient { + const isWebSocket = options.rpcUrl.startsWith('ws://') || options.rpcUrl.startsWith('wss://') + const transport = isWebSocket + ? webSocket(options.rpcUrl, { + keepAlive: { interval: 30_000 }, + reconnect: { attempts: 5, delay: 2_000 }, + }) + : http(options.rpcUrl) + + const publicClient = createPublicClient({ + chain: options.chain, + transport, + }) as PublicClient + + let walletClient: WalletClient | undefined + if (options.privateKey) { + const account = privateKeyToAccount(options.privateKey) + walletClient = createWalletClient({ + account, + chain: options.chain, + transport, + }) + } + + return new ContractClient({ publicClient, walletClient, contracts: options.contracts }) + } + + public getPublicClient(): PublicClient { + return this.publicClient + } + + public async approveFeeToken(amount: bigint): Promise { + if (!this.walletClient) { + throw new SDKError('Wallet client required for write operations', 'NO_WALLET') + } + + try { + const account = this.walletClient.account + if (!account) { + throw new SDKError('No account connected', 'NO_ACCOUNT') + } + + const { request } = await this.publicClient.simulateContract({ + address: this.contracts.feeToken, + abi: EnclaveToken__factory.abi, + functionName: 'approve', + args: [this.contracts.enclave, amount], + account, + }) + + return await this.walletClient.writeContract(request) + } catch (error) { + throw new SDKError(`Failed to approve fee token: ${error}`, 'APPROVE_FEE_TOKEN_FAILED') + } + } + + public async requestE3(params: E3RequestParams): Promise { + if (!this.walletClient) { + throw new SDKError('Wallet client required for write operations', 'NO_WALLET') + } + + try { + const account = this.walletClient.account + if (!account) { + throw new SDKError('No account connected', 'NO_ACCOUNT') + } + + const { request } = await this.publicClient.simulateContract({ + address: this.contracts.enclave, + abi: Enclave__factory.abi, + functionName: 'request', + args: [ + { + threshold: params.threshold, + inputWindow: params.inputWindow, + e3Program: params.e3Program, + e3ProgramParams: params.e3ProgramParams, + computeProviderParams: params.computeProviderParams, + customParams: params.customParams || '0x', + }, + ], + account, + gas: params.gasLimit, + }) + + return await this.walletClient.writeContract(request) + } catch (error) { + throw new SDKError(`Failed to request E3: ${error}`, 'REQUEST_E3_FAILED') + } + } + + public async publishCiphertextOutput( + e3Id: bigint, + ciphertextOutput: `0x${string}`, + proof: `0x${string}`, + gasLimit?: bigint, + ): Promise { + if (!this.walletClient) { + throw new SDKError('Wallet client required for write operations', 'NO_WALLET') + } + + try { + const account = this.walletClient.account + if (!account) { + throw new SDKError('No account connected', 'NO_ACCOUNT') + } + + const { request } = await this.publicClient.simulateContract({ + address: this.contracts.enclave, + abi: Enclave__factory.abi, + functionName: 'publishCiphertextOutput', + args: [e3Id, ciphertextOutput, proof], + account, + gas: gasLimit, + }) + + return await this.walletClient.writeContract(request) + } catch (error) { + throw new SDKError(`Failed to publish ciphertext output: ${error}`, 'PUBLISH_CIPHERTEXT_OUTPUT_FAILED') + } + } + + public async getE3(e3Id: bigint): Promise { + try { + const result: E3 = await this.publicClient.readContract({ + address: this.contracts.enclave, + abi: Enclave__factory.abi, + functionName: 'getE3', + args: [e3Id], + }) + + return result + } catch (error) { + throw new SDKError(`Failed to get E3: ${error}`, 'GET_E3_FAILED') + } + } + + public async getE3Quote(requestParams: E3RequestParams): Promise { + try { + return this.publicClient.readContract({ + address: this.contracts.enclave, + abi: Enclave__factory.abi, + functionName: 'getE3Quote', + args: [ + { + threshold: requestParams.threshold, + inputWindow: requestParams.inputWindow, + e3Program: requestParams.e3Program, + e3ProgramParams: requestParams.e3ProgramParams, + computeProviderParams: requestParams.computeProviderParams, + customParams: requestParams.customParams || '0x', + }, + ], + }) + } catch (error) { + throw new SDKError(`Failed to get E3 quote: ${error}`, 'GET_E3_QUOTE_FAILED') + } + } + + public async getFailureReason(e3Id: bigint): Promise { + try { + return this.publicClient.readContract({ + address: this.contracts.enclave, + abi: Enclave__factory.abi, + functionName: 'getFailureReason', + args: [e3Id], + }) + } catch (error) { + throw new SDKError(`Failed to get failure reason: ${error}`, 'GET_FAILURE_REASON_FAILED') + } + } + + public async getE3PublicKey(e3Id: bigint): Promise<`0x${string}`> { + try { + const result: `0x${string}` = await this.publicClient.readContract({ + address: this.contracts.ciphernodeRegistry, + abi: CiphernodeRegistryOwnable__factory.abi, + functionName: 'committeePublicKey', + args: [e3Id], + }) + + return result + } catch (error) { + throw new SDKError(`Failed to get E3 public key: ${error}`, 'GET_E3_PUBLIC_KEY_FAILED') + } + } + + public async getE3Stage(e3Id: bigint): Promise { + try { + return this.publicClient.readContract({ + address: this.contracts.enclave, + abi: Enclave__factory.abi, + functionName: 'getE3Stage', + args: [e3Id], + }) + } catch (error) { + throw new SDKError(`Failed to get E3 stage: ${error}`, 'GET_E3_STAGE_FAILED') + } + } + + public async estimateGas( + functionName: string, + args: readonly unknown[], + contractAddress: `0x${string}`, + abi: Abi, + value?: bigint, + ): Promise { + if (!this.walletClient) { + throw new SDKError('Wallet client required for gas estimation', 'NO_WALLET') + } + + try { + const account = this.walletClient.account + if (!account) { + throw new SDKError('No account connected', 'NO_ACCOUNT') + } + + const estimateParams = { + address: contractAddress, + abi, + functionName, + args, + account, + ...(value !== undefined && { value }), + } + + return await this.publicClient.estimateContractGas(estimateParams) + } catch (error) { + throw new SDKError(`Failed to estimate gas: ${error}`, 'GAS_ESTIMATION_FAILED') + } + } + + public async waitForTransaction(hash: Hash): Promise { + try { + return await this.publicClient.waitForTransactionReceipt({ + hash, + confirmations: 1, + }) + } catch (error) { + throw new SDKError(`Failed to wait for transaction: ${error}`, 'TRANSACTION_WAIT_FAILED') + } + } +} diff --git a/packages/enclave-sdk/src/contracts/index.ts b/packages/enclave-sdk/src/contracts/index.ts new file mode 100644 index 0000000000..aef581427c --- /dev/null +++ b/packages/enclave-sdk/src/contracts/index.ts @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +export { ContractClient } from './contract-client' +export type { ContractClientConfig } from './contract-client' +export type { ContractAddresses, E3, E3RequestParams } from './types' +export { E3Stage, FailureReason } from './types' diff --git a/packages/enclave-sdk/src/contracts/types.ts b/packages/enclave-sdk/src/contracts/types.ts new file mode 100644 index 0000000000..6bdf06fc53 --- /dev/null +++ b/packages/enclave-sdk/src/contracts/types.ts @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +export interface ContractAddresses { + enclave: `0x${string}` + ciphernodeRegistry: `0x${string}` + feeToken: `0x${string}` +} + +export interface E3 { + seed: bigint + threshold: readonly [number, number] + requestBlock: bigint + inputWindow: readonly [bigint, bigint] + encryptionSchemeId: string + e3Program: string + e3ProgramParams: string + decryptionVerifier: string + committeePublicKey: string + ciphertextOutput: string + plaintextOutput: string +} + +export interface RequestParams { + gasLimit?: bigint +} + +export interface E3RequestParams extends RequestParams { + threshold: readonly [number, number] + inputWindow: readonly [bigint, bigint] + e3Program: `0x${string}` + e3ProgramParams: `0x${string}` + computeProviderParams: `0x${string}` + customParams?: `0x${string}` +} + +export enum E3Stage { + None, + Requested, + CommitteeFinalized, + KeyPublished, + CiphertextReady, + Complete, + Failed, +} + +export enum FailureReason { + None, + CommitteeFormationTimeout, + InsufficientCommitteeMembers, + DKGTimeout, + DKGInvalidShares, + NoInputsReceived, + ComputeTimeout, + ComputeProviderExpired, + ComputeProviderFailed, + RequesterCancelled, + DecryptionTimeout, + DecryptionInvalidShares, + VerificationFailed, +} diff --git a/packages/enclave-sdk/src/crypto/encrypt.ts b/packages/enclave-sdk/src/crypto/encrypt.ts new file mode 100644 index 0000000000..8f92dc2c9c --- /dev/null +++ b/packages/enclave-sdk/src/crypto/encrypt.ts @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +import initializeWasm from '@enclave-e3/wasm/init' +import { + bfv_encrypt_number, + bfv_encrypt_vector, + generate_public_key, + bfv_verifiable_encrypt_number, + bfv_verifiable_encrypt_vector, + compute_pk_commitment, + get_bfv_params, +} from '@enclave-e3/wasm' +import { generateProof } from './user-data-encryption' +import type { BfvParams, EncryptedValueAndPublicInputs, ThresholdBfvParamsPresetName, VerifiableEncryptionResult } from './types' + +async function resolveParams(presetName: ThresholdBfvParamsPresetName): Promise { + await initializeWasm() + const params = get_bfv_params(presetName) + return { + degree: Number(params.degree), + plaintextModulus: params.plaintext_modulus as bigint, + moduli: params.moduli as bigint[], + error1Variance: params.error1_variance, + } +} + +export async function getThresholdBfvParamsSet(presetName: ThresholdBfvParamsPresetName): Promise { + return resolveParams(presetName) +} + +export async function generatePublicKey(presetName: ThresholdBfvParamsPresetName): Promise { + const params = await resolveParams(presetName) + return generate_public_key(params.degree, params.plaintextModulus, BigUint64Array.from(params.moduli)) +} + +export async function computePublicKeyCommitment(pk: Uint8Array, presetName: ThresholdBfvParamsPresetName): Promise { + const params = await resolveParams(presetName) + return compute_pk_commitment(pk, params.degree, params.plaintextModulus, BigUint64Array.from(params.moduli)) +} + +export async function encryptNumber(data: bigint, pk: Uint8Array, presetName: ThresholdBfvParamsPresetName): Promise { + const params = await resolveParams(presetName) + return bfv_encrypt_number(data, pk, params.degree, params.plaintextModulus, BigUint64Array.from(params.moduli)) +} + +export async function encryptVector(data: BigUint64Array, pk: Uint8Array, presetName: ThresholdBfvParamsPresetName): Promise { + const params = await resolveParams(presetName) + return bfv_encrypt_vector(data, pk, params.degree, params.plaintextModulus, BigUint64Array.from(params.moduli)) +} + +export async function encryptNumberAndGenInputs( + data: bigint, + pk: Uint8Array, + presetName: ThresholdBfvParamsPresetName, +): Promise { + const params = await resolveParams(presetName) + const [encryptedData, circuitInputs] = bfv_verifiable_encrypt_number( + data, + pk, + params.degree, + params.plaintextModulus, + BigUint64Array.from(params.moduli), + ) + return { encryptedData, circuitInputs: JSON.parse(circuitInputs) } +} + +export async function encryptNumberAndGenProof( + data: bigint, + pk: Uint8Array, + presetName: ThresholdBfvParamsPresetName, +): Promise { + const { circuitInputs, encryptedData } = await encryptNumberAndGenInputs(data, pk, presetName) + const proof = await generateProof(circuitInputs) + return { encryptedData, proof } +} + +export async function encryptVectorAndGenInputs( + data: BigUint64Array, + pk: Uint8Array, + presetName: ThresholdBfvParamsPresetName, +): Promise { + const params = await resolveParams(presetName) + const [encryptedData, circuitInputs] = bfv_verifiable_encrypt_vector( + data, + pk, + params.degree, + params.plaintextModulus, + BigUint64Array.from(params.moduli), + ) + return { encryptedData, circuitInputs: JSON.parse(circuitInputs) } +} + +export async function encryptVectorAndGenProof( + data: BigUint64Array, + pk: Uint8Array, + presetName: ThresholdBfvParamsPresetName, +): Promise { + const { circuitInputs, encryptedData } = await encryptVectorAndGenInputs(data, pk, presetName) + const proof = await generateProof(circuitInputs) + return { encryptedData, proof } +} diff --git a/packages/enclave-sdk/src/crypto/index.ts b/packages/enclave-sdk/src/crypto/index.ts new file mode 100644 index 0000000000..b2a1261b1c --- /dev/null +++ b/packages/enclave-sdk/src/crypto/index.ts @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +export { + getThresholdBfvParamsSet, + generatePublicKey, + computePublicKeyCommitment, + encryptNumber, + encryptVector, + encryptNumberAndGenInputs, + encryptNumberAndGenProof, + encryptVectorAndGenInputs, + encryptVectorAndGenProof, +} from './encrypt' + +export { generateProof } from './user-data-encryption' + +export type { BfvParams, ThresholdBfvParamsPresetName, VerifiableEncryptionResult, EncryptedValueAndPublicInputs } from './types' + +export { ThresholdBfvParamsPresetNames } from './types' diff --git a/packages/enclave-sdk/src/crypto/types.ts b/packages/enclave-sdk/src/crypto/types.ts new file mode 100644 index 0000000000..0a0ff40853 --- /dev/null +++ b/packages/enclave-sdk/src/crypto/types.ts @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +import type { ProofData } from '@aztec/bb.js' +import type { CircuitInputs } from './user-data-encryption' + +export interface BfvParams { + degree: number + plaintextModulus: bigint + moduli: bigint[] + error1Variance: string | undefined +} + +export type ThresholdBfvParamsPresetName = 'INSECURE_THRESHOLD_512' | 'SECURE_THRESHOLD_8192' + +export const ThresholdBfvParamsPresetNames = [ + 'INSECURE_THRESHOLD_512', + 'SECURE_THRESHOLD_8192', +] as const satisfies ReadonlyArray + +export interface VerifiableEncryptionResult { + encryptedData: Uint8Array + proof: ProofData +} + +export interface EncryptedValueAndPublicInputs { + encryptedData: Uint8Array + circuitInputs: CircuitInputs +} diff --git a/packages/enclave-sdk/src/greco.ts b/packages/enclave-sdk/src/crypto/user-data-encryption.ts similarity index 91% rename from packages/enclave-sdk/src/greco.ts rename to packages/enclave-sdk/src/crypto/user-data-encryption.ts index 14b261ca17..ecbf389aeb 100644 --- a/packages/enclave-sdk/src/greco.ts +++ b/packages/enclave-sdk/src/crypto/user-data-encryption.ts @@ -5,11 +5,11 @@ // or FITNESS FOR A PARTICULAR PURPOSE. import { Barretenberg, UltraHonkBackend, type ProofData } from '@aztec/bb.js' -import userDataEncryptionCt0Circuit from '../../../circuits/bin/threshold/target/user_data_encryption_ct0.json' -import userDataEncryptionCt1Circuit from '../../../circuits/bin/threshold/target/user_data_encryption_ct1.json' -import userDataEncryptionCircuit from '../../../circuits/bin/recursive_aggregation/wrapper/threshold/target/user_data_encryption.json' +import userDataEncryptionCt0Circuit from '../../../../circuits/bin/threshold/target/user_data_encryption_ct0.json' +import userDataEncryptionCt1Circuit from '../../../../circuits/bin/threshold/target/user_data_encryption_ct1.json' +import userDataEncryptionCircuit from '../../../../circuits/bin/recursive_aggregation/wrapper/threshold/target/user_data_encryption.json' import { CompiledCircuit, Noir } from '@noir-lang/noir_js' -import { proofToFields } from './utils' +import { proofToFields } from '../utils' // Conversion to Noir types export type Field = string diff --git a/packages/enclave-sdk/src/enclave-sdk.ts b/packages/enclave-sdk/src/enclave-sdk.ts index 4424a01560..e17843c164 100644 --- a/packages/enclave-sdk/src/enclave-sdk.ts +++ b/packages/enclave-sdk/src/enclave-sdk.ts @@ -4,53 +4,49 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -import { type Abi, type Hash, type Log, PublicClient, WalletClient, createPublicClient, createWalletClient, http, webSocket } from 'viem' +import { + type Abi, + type Chain, + type Hash, + type Log, + type PublicClient, + type WalletClient, + createPublicClient, + createWalletClient, + http, + webSocket, +} from 'viem' import { privateKeyToAccount } from 'viem/accounts' -import { hardhat, mainnet, monadTestnet, sepolia } from 'viem/chains' -import initializeWasm from '@enclave-e3/wasm/init' -import { CiphernodeRegistryOwnable__factory, Enclave__factory } from '@enclave-e3/contracts/types' -import { ContractClient } from './contract-client' -import { EventListener } from './event-listener' -import { EnclaveEventType, ThresholdBfvParamsPresetNames } from './types' +import { ContractClient } from './contracts/contract-client' +import { EventListener } from './events/event-listener' +import { + getThresholdBfvParamsSet, + generatePublicKey, + computePublicKeyCommitment, + encryptNumber, + encryptVector, + encryptNumberAndGenInputs, + encryptNumberAndGenProof, + encryptVectorAndGenInputs, + encryptVectorAndGenProof, +} from './crypto/encrypt' +import { ThresholdBfvParamsPresetNames } from './crypto/types' import { SDKError, isValidAddress } from './utils' +import { DEFAULT_THRESHOLD_BFV_PARAMS_PRESET_NAME } from './constants' -import type { - AllEventTypes, - E3, - EventCallback, - SDKConfig, - BfvParams, - VerifiableEncryptionResult, - EncryptedValueAndPublicInputs, - ThresholdBfvParamsPresetName, -} from './types' -import { - bfv_encrypt_number, - bfv_encrypt_vector, - generate_public_key, - bfv_verifiable_encrypt_number, - bfv_verifiable_encrypt_vector, - compute_pk_commitment, - get_bfv_params, -} from '@enclave-e3/wasm' -import { generateProof } from './greco' +import type { SDKConfig } from './types' +import type { AllEventTypes, EventCallback } from './events/types' +import type { E3, E3RequestParams } from './contracts/types' +import { E3Stage, FailureReason } from './contracts/types' +import type { BfvParams, EncryptedValueAndPublicInputs, ThresholdBfvParamsPresetName, VerifiableEncryptionResult } from './crypto/types' export class EnclaveSDK { - public static readonly chains = { - 1: mainnet, - 11155111: sepolia, - 41454: monadTestnet, - 31337: hardhat, - } as const - private eventListener: EventListener private contractClient: ContractClient - private initialized = false - private thresholdBfvParamsPresetName?: ThresholdBfvParamsPresetName + private thresholdBfvParamsPresetName: ThresholdBfvParamsPresetName private publicClient: PublicClient - // TODO: use zod for config validation constructor(private config: SDKConfig) { if (!config.publicClient) { throw new SDKError('Public client is required', 'MISSING_PUBLIC_CLIENT') @@ -68,213 +64,75 @@ export class EnclaveSDK { throw new SDKError('Invalid FeeToken contract address', 'INVALID_ADDRESS') } - if (!config.thresholdBfvParamsPresetName) { - throw new SDKError('Threshold BFV parameters preset name is required', 'MISSING_THRESHOLD_BFV_PARAMS_PRESET_NAME') - } + const presetName = config.thresholdBfvParamsPresetName ?? DEFAULT_THRESHOLD_BFV_PARAMS_PRESET_NAME - if (!Object.values(ThresholdBfvParamsPresetNames).includes(config.thresholdBfvParamsPresetName)) { - throw new SDKError( - `Invalid threshold BFV parameters preset name: ${config.thresholdBfvParamsPresetName}`, - 'INVALID_THRESHOLD_BFV_PARAMS_PRESET_NAME', - ) + if (!Object.values(ThresholdBfvParamsPresetNames).includes(presetName)) { + throw new SDKError(`Invalid threshold BFV parameters preset name: ${presetName}`, 'INVALID_THRESHOLD_BFV_PARAMS_PRESET_NAME') } - this.thresholdBfvParamsPresetName = config.thresholdBfvParamsPresetName - this.eventListener = new EventListener(config.publicClient) - this.contractClient = new ContractClient(config.publicClient, config.walletClient, config.contracts) + this.thresholdBfvParamsPresetName = presetName this.publicClient = config.publicClient - } - /** - * Initialize the SDK - */ - // TODO: Delete this it is redundant - public async initialize(): Promise { - if (this.initialized) return - - try { - await this.contractClient.initialize() - this.initialized = true - } catch (error) { - throw new SDKError(`Failed to initialize SDK: ${error}`, 'SDK_INITIALIZATION_FAILED') - } + this.contractClient = new ContractClient({ + publicClient: config.publicClient, + walletClient: config.walletClient, + contracts: config.contracts, + }) + + this.eventListener = new EventListener({ + publicClient: config.publicClient, + contracts: config.contracts, + }) } - /** - * Get the public client used by the SDK - * @returns The public client - */ - public getPublicClient = (): PublicClient => { + // --- Encryption (delegates to standalone functions) --- + + public getPublicClient(): PublicClient { return this.publicClient } public async getThresholdBfvParamsSet(): Promise { - await initializeWasm() - let params = get_bfv_params(this.thresholdBfvParamsPresetName as ThresholdBfvParamsPresetName) - return { - degree: Number(params.degree), // degree is returned as a bigint from wasm - plaintextModulus: params.plaintext_modulus as bigint, - moduli: params.moduli as bigint[], - error1Variance: params.error1_variance, - } + return getThresholdBfvParamsSet(this.thresholdBfvParamsPresetName) } public async generatePublicKey(): Promise { - await initializeWasm() - const protocolParams = await this.getThresholdBfvParamsSet() - return generate_public_key(protocolParams.degree, protocolParams.plaintextModulus, BigUint64Array.from(protocolParams.moduli)) + return generatePublicKey(this.thresholdBfvParamsPresetName) } public async computePublicKeyCommitment(publicKey: Uint8Array): Promise { - await initializeWasm() - const protocolParams = await this.getThresholdBfvParamsSet() - - return compute_pk_commitment( - publicKey, - protocolParams.degree, - protocolParams.plaintextModulus, - BigUint64Array.from(protocolParams.moduli), - ) + return computePublicKeyCommitment(publicKey, this.thresholdBfvParamsPresetName) } - /** - * Encrypt a number using the configured protocol - * @param data - The number to encrypt - * @param publicKey - The public key to use for encryption - * @returns The encrypted number - */ public async encryptNumber(data: bigint, publicKey: Uint8Array): Promise { - await initializeWasm() - const protocolParams = await this.getThresholdBfvParamsSet() - - return bfv_encrypt_number( - data, - publicKey, - protocolParams.degree, - protocolParams.plaintextModulus, - BigUint64Array.from(protocolParams.moduli), - ) + return encryptNumber(data, publicKey, this.thresholdBfvParamsPresetName) } - /** - * Encrypt a vector using the configured protocol - * @param data - The vector to encrypt - * @param publicKey - The public key to use for encryption - * @returns The ciphertext - */ public async encryptVector(data: BigUint64Array, publicKey: Uint8Array): Promise { - await initializeWasm() - const protocolParams = await this.getThresholdBfvParamsSet() - - return bfv_encrypt_vector( - data, - publicKey, - protocolParams.degree, - protocolParams.plaintextModulus, - BigUint64Array.from(protocolParams.moduli), - ) + return encryptVector(data, publicKey, this.thresholdBfvParamsPresetName) } - /** - * This function encrypts a number using the configured FHE protocol - * and generates the necessary public inputs for a zk-SNARK proof. - * @param data The number to encrypt - * @param publicKey The public key to use for encryption - * @returns The encrypted number and the inputs for the zk-SNARK proof - */ public async encryptNumberAndGenInputs(data: bigint, publicKey: Uint8Array): Promise { - await initializeWasm() - const protocolParams = await this.getThresholdBfvParamsSet() - - const [encryptedData, circuitInputs] = bfv_verifiable_encrypt_number( - data, - publicKey, - protocolParams.degree, - protocolParams.plaintextModulus, - BigUint64Array.from(protocolParams.moduli), - ) - - return { - encryptedData, - circuitInputs: JSON.parse(circuitInputs), - } + return encryptNumberAndGenInputs(data, publicKey, this.thresholdBfvParamsPresetName) } - /** - * Encrypt a number using the configured protocol and generate a zk-SNARK proof using Greco - * @param data - The number to encrypt - * @param publicKey - The public key to use for encryption - * @returns The encrypted number and the proof - */ public async encryptNumberAndGenProof(data: bigint, publicKey: Uint8Array): Promise { - const { circuitInputs: publicInputs, encryptedData } = await this.encryptNumberAndGenInputs(data, publicKey) - const proof = await generateProof(publicInputs) - - return { - encryptedData, - proof, - } + return encryptNumberAndGenProof(data, publicKey, this.thresholdBfvParamsPresetName) } - /** - * Encrypt a vector and generate inputs for an E3 computation - * @param data - The vector to encrypt - * @param publicKey - The public key to use for encryption - * @returns The encrypted vector and the inputs for the E3 computation - */ public async encryptVectorAndGenInputs(data: BigUint64Array, publicKey: Uint8Array): Promise { - await initializeWasm() - const protocolParams = await this.getThresholdBfvParamsSet() - - const [encryptedData, circuitInputs] = bfv_verifiable_encrypt_vector( - data, - publicKey, - protocolParams.degree, - protocolParams.plaintextModulus, - BigUint64Array.from(protocolParams.moduli), - ) - - return { - encryptedData, - circuitInputs: JSON.parse(circuitInputs), - } + return encryptVectorAndGenInputs(data, publicKey, this.thresholdBfvParamsPresetName) } - /** - * Encrypt a vector using the configured protocol and generate a zk-SNARK proof using Greco - * @param data - The vector to encrypt - * @param publicKey - The public key to use for encryption - * @returns The encrypted vector and the proof - */ public async encryptVectorAndGenProof(data: BigUint64Array, publicKey: Uint8Array): Promise { - const { circuitInputs: publicInputs, encryptedData } = await this.encryptVectorAndGenInputs(data, publicKey) - - const proof = await generateProof(publicInputs) - - return { - encryptedData, - proof, - } + return encryptVectorAndGenProof(data, publicKey, this.thresholdBfvParamsPresetName) } - /** - * Approve the fee token for the Enclave - * @param amount - The amount to approve - * @returns The approval transaction hash - */ - public async approveFeeToken(amount: bigint): Promise { - console.log('>>> APPROVE FEE TOKEN') - - if (!this.initialized) { - await this.initialize() - } + // --- Contracts (delegates to ContractClient) --- + public async approveFeeToken(amount: bigint): Promise { return this.contractClient.approveFeeToken(amount) } - /** - * Request a new E3 computation - */ public async requestE3(params: { threshold: [number, number] inputWindow: [bigint, bigint] @@ -284,128 +142,38 @@ export class EnclaveSDK { customParams?: `0x${string}` gasLimit?: bigint }): Promise { - console.log('>>> REQUEST') - - if (!this.initialized) { - await this.initialize() - } - - return this.contractClient.requestE3( - params.threshold, - params.inputWindow, - params.e3Program, - params.e3ProgramParams, - params.computeProviderParams, - params.customParams, - params.gasLimit, - ) + return this.contractClient.requestE3(params) } - /** - * Get the public key for an E3 computation - * @param e3Id - The ID of the E3 computation - * @returns The public key - */ public async getE3PublicKey(e3Id: bigint): Promise<`0x${string}`> { - if (!this.initialized) { - await this.initialize() - } - return this.contractClient.getE3PublicKey(e3Id) } - /** - * Publish ciphertext output for an E3 computation - */ public async publishCiphertextOutput( e3Id: bigint, ciphertextOutput: `0x${string}`, proof: `0x${string}`, gasLimit?: bigint, ): Promise { - if (!this.initialized) { - await this.initialize() - } - return this.contractClient.publishCiphertextOutput(e3Id, ciphertextOutput, proof, gasLimit) } - /** - * Get E3 information - */ public async getE3(e3Id: bigint): Promise { - if (!this.initialized) { - await this.initialize() - } - return this.contractClient.getE3(e3Id) } - /** - * Unified Event Listening - Listen to any Enclave or Registry event - */ - public onEnclaveEvent(eventType: T, callback: EventCallback): void { - // Determine which contract to listen to based on event type - const isEnclaveEvent = Object.values(EnclaveEventType).includes(eventType as EnclaveEventType) - const contractAddress = isEnclaveEvent ? this.config.contracts.enclave : this.config.contracts.ciphernodeRegistry - const abi = isEnclaveEvent ? Enclave__factory.abi : CiphernodeRegistryOwnable__factory.abi - - void this.eventListener.watchContractEvent(contractAddress, eventType, abi, callback) - } - - /** - * Remove event listener - */ - public off(eventType: T, callback: EventCallback): void { - this.eventListener.off(eventType, callback) - } - - /** - * Handle an event only once - */ - public once(type: T, callback: EventCallback): void { - const handler: EventCallback = (event) => { - this.off(type, handler) - const prom = callback(event) - if (prom) { - prom.catch((e) => console.log(e)) - } - } - this.onEnclaveEvent(type, handler) - } - - /** - * Get historical events - */ - public async getHistoricalEvents(eventType: AllEventTypes, fromBlock?: bigint, toBlock?: bigint): Promise { - const isEnclaveEvent = Object.values(EnclaveEventType).includes(eventType as EnclaveEventType) - const contractAddress = isEnclaveEvent ? this.config.contracts.enclave : this.config.contracts.ciphernodeRegistry - const abi = isEnclaveEvent ? Enclave__factory.abi : CiphernodeRegistryOwnable__factory.abi - - return this.eventListener.getHistoricalEvents(contractAddress, eventType, abi, fromBlock, toBlock) + public async getE3Quote(params: E3RequestParams): Promise { + return this.contractClient.getE3Quote(params) } - /** - * Start polling for events - */ - public async startEventPolling(): Promise { - void this.eventListener.startPolling() + public async getFailureReason(e3Id: bigint): Promise { + return this.contractClient.getFailureReason(e3Id) } - /** - * Stop polling for events - */ - public stopEventPolling(): void { - this.eventListener.stopPolling() + public async getE3Stage(e3Id: bigint): Promise { + return this.contractClient.getE3Stage(e3Id) } - /** - * Utility methods - */ - - /** - * Estimate gas for a transaction - */ public async estimateGas( functionName: string, args: readonly unknown[], @@ -416,50 +184,42 @@ export class EnclaveSDK { return this.contractClient.estimateGas(functionName, args, contractAddress, abi, value) } - /** - * Wait for transaction confirmation - */ public async waitForTransaction(hash: Hash): Promise { return this.contractClient.waitForTransaction(hash) } - /** - * Clean up resources - */ - public cleanup(): void { - this.eventListener.cleanup() + // --- Events (delegates to EventListener) --- + + public async onEnclaveEvent(eventType: T, callback: EventCallback): Promise { + return this.eventListener.onEnclaveEvent(eventType, callback) } - /** - * Update SDK configuration - */ - // TODO: We should delete this as we don't want a stateful client. - public updateConfig(newConfig: Partial): void { - if (newConfig.publicClient) { - this.config.publicClient = newConfig.publicClient - this.eventListener = new EventListener(newConfig.publicClient) - } + public off(eventType: T, callback: EventCallback): void { + this.eventListener.off(eventType, callback) + } - if (newConfig.walletClient) { - this.config.walletClient = newConfig.walletClient - } + public async once(type: T, callback: EventCallback): Promise { + return this.eventListener.once(type, callback) + } - if (newConfig.contracts) { - this.config.contracts = { - ...this.config.contracts, - ...newConfig.contracts, - } - } + public async getHistoricalEvents(eventType: AllEventTypes, fromBlock?: bigint, toBlock?: bigint): Promise { + return this.eventListener.getHistoricalEvents(eventType, fromBlock, toBlock) + } - if (newConfig.chainId) { - this.config.chainId = newConfig.chainId - } + public startEventPolling(): Promise { + return this.eventListener.startPolling() + } - this.contractClient = new ContractClient(this.config.publicClient, this.config.walletClient, this.config.contracts) + public stopEventPolling(): void { + this.eventListener.stopPolling() + } - this.initialized = false + public cleanup(): void { + this.eventListener.cleanup() } + // --- Factory --- + public static create(options: { rpcUrl: string contracts: { @@ -468,11 +228,9 @@ export class EnclaveSDK { feeToken: `0x${string}` } privateKey?: `0x${string}` - chainId: keyof typeof EnclaveSDK.chains - thresholdBfvParamsPresetName: ThresholdBfvParamsPresetName + chain: Chain + thresholdBfvParamsPresetName?: ThresholdBfvParamsPresetName }): EnclaveSDK { - const chain = EnclaveSDK.chains[options.chainId] - const isWebSocket = options.rpcUrl.startsWith('ws://') || options.rpcUrl.startsWith('wss://') const transport = isWebSocket ? webSocket(options.rpcUrl, { @@ -480,16 +238,18 @@ export class EnclaveSDK { reconnect: { attempts: 5, delay: 2_000 }, }) : http(options.rpcUrl) + const publicClient = createPublicClient({ - chain, + chain: options.chain, transport, }) as SDKConfig['publicClient'] - let walletClient: WalletClient | undefined = undefined + + let walletClient: WalletClient | undefined if (options.privateKey) { const account = privateKeyToAccount(options.privateKey) walletClient = createWalletClient({ account, - chain, + chain: options.chain, transport, }) } @@ -498,7 +258,7 @@ export class EnclaveSDK { publicClient, walletClient, contracts: options.contracts, - chainId: options.chainId, + chain: options.chain, thresholdBfvParamsPresetName: options.thresholdBfvParamsPresetName, }) } diff --git a/packages/enclave-sdk/src/event-listener.ts b/packages/enclave-sdk/src/events/event-listener.ts similarity index 67% rename from packages/enclave-sdk/src/event-listener.ts rename to packages/enclave-sdk/src/events/event-listener.ts index 949c86bf4e..c736a37c71 100644 --- a/packages/enclave-sdk/src/event-listener.ts +++ b/packages/enclave-sdk/src/events/event-listener.ts @@ -5,34 +5,79 @@ // or FITNESS FOR A PARTICULAR PURPOSE. import { type Abi, type Log, type PublicClient } from 'viem' +import { CiphernodeRegistryOwnable__factory, Enclave__factory } from '@enclave-e3/contracts/types' import { + RegistryEventType, type AllEventTypes, type EnclaveEvent, type EnclaveEventData, - type EnclaveEventType, + type EnclaveEventType as EnclaveEventTypeT, type EventCallback, type EventListenerConfig, type RegistryEventData, - type RegistryEventType, + type RegistryEventType as RegistryEventTypeT, type SDKEventEmitter, } from './types' -import { SDKError, sleep } from './utils' +import type { ContractAddresses } from '../contracts/types' +import { SDKError, sleep } from '../utils' + +export interface EventListenerOptions { + publicClient: PublicClient + contracts: ContractAddresses + config?: EventListenerConfig +} export class EventListener implements SDKEventEmitter { private listeners: Map> = new Map() private activeWatchers: Map void> = new Map() private isPolling = false private lastBlockNumber: bigint = BigInt(0) + private publicClient: PublicClient + private contracts: ContractAddresses + private config: EventListenerConfig + + constructor(options: EventListenerOptions) { + this.publicClient = options.publicClient + this.contracts = options.contracts + this.config = options.config || {} + } - constructor( - private publicClient: PublicClient, - private config: EventListenerConfig = {}, - ) {} + // Registry-exclusive event names that don't collide with EnclaveEventType. + // Shared names like 'OwnershipTransferred' and 'Initialized' exist in both + // enums with the same string value, so they cannot be disambiguated at + // runtime; those default to the Enclave contract. + private static readonly REGISTRY_ONLY_EVENTS: ReadonlySet = new Set([ + RegistryEventType.COMMITTEE_REQUESTED, + RegistryEventType.COMMITTEE_PUBLISHED, + RegistryEventType.COMMITTEE_FINALIZED, + RegistryEventType.ENCLAVE_SET, + ]) + + private resolveContract(eventType: AllEventTypes): { address: `0x${string}`; abi: Abi } { + const isRegistryEvent = EventListener.REGISTRY_ONLY_EVENTS.has(eventType as string) + return { + address: isRegistryEvent ? this.contracts.ciphernodeRegistry : this.contracts.enclave, + abi: isRegistryEvent ? CiphernodeRegistryOwnable__factory.abi : Enclave__factory.abi, + } + } + + public async onEnclaveEvent(eventType: T, callback: EventCallback): Promise { + const { address, abi } = this.resolveContract(eventType) + return this.watchContractEvent(address, eventType, abi, callback) + } + + public async once(type: T, callback: EventCallback): Promise { + const handler: EventCallback = (event) => { + this.off(type, handler) + const prom = callback(event) + if (prom) { + prom.catch((e) => console.error(e)) + } + } + return this.onEnclaveEvent(type, handler) + } - /** - * Listen to specific contract events - */ public async watchContractEvent( address: `0x${string}`, eventType: T, @@ -40,21 +85,16 @@ export class EventListener implements SDKEventEmitter { callback: EventCallback, ): Promise { const watcherKey = `${address}:${eventType}` - console.log(`watchContractEvent: ${watcherKey}`) if (!this.listeners.has(eventType)) { this.listeners.set(eventType, new Set()) } - console.log('Added callback') this.listeners.get(eventType)!.add(callback as EventCallback) // eslint-disable-next-line @typescript-eslint/no-this-alias const emitter = this - // If we don't have an active watcher for this event, create one if (!this.activeWatchers.has(watcherKey)) { - console.log('Adding active watcher for ' + watcherKey) - try { const unwatch = this.publicClient.watchContractEvent({ address, @@ -64,15 +104,12 @@ export class EventListener implements SDKEventEmitter { onLogs(logs: Log[]) { for (let i = 0; i < logs.length; i++) { const log = logs[i] - if (!log) { - console.log('warning: Log was falsy when a log was expected!') - break - } + if (!log) break const event: EnclaveEvent = { type: eventType, - data: (log as unknown as { args: unknown }).args as T extends EnclaveEventType + data: (log as unknown as { args: unknown }).args as T extends EnclaveEventTypeT ? EnclaveEventData[T] - : T extends RegistryEventType + : T extends RegistryEventTypeT ? RegistryEventData[T] : unknown, log, @@ -80,9 +117,7 @@ export class EventListener implements SDKEventEmitter { blockNumber: log.blockNumber ?? BigInt(0), transactionHash: log.transactionHash ?? '0x', } - console.log('Created event, now emitting event...') emitter.emit(event) - console.log('Event emitted') } }, }) @@ -94,9 +129,6 @@ export class EventListener implements SDKEventEmitter { } } - /** - * Listen to all logs from a specific address - */ public async watchLogs(address: `0x${string}`, callback: (log: Log) => void): Promise { const watcherKey = `logs:${address}` @@ -118,9 +150,6 @@ export class EventListener implements SDKEventEmitter { } } - /** - * Start polling for historical events - */ public async startPolling(): Promise { if (this.isPolling) return @@ -128,7 +157,6 @@ export class EventListener implements SDKEventEmitter { try { this.lastBlockNumber = await this.publicClient.getBlockNumber() - void this.pollForEvents() } catch (error) { this.isPolling = false @@ -136,41 +164,26 @@ export class EventListener implements SDKEventEmitter { } } - /** - * Stop polling for events - */ public stopPolling(): void { this.isPolling = false } - /** - * Get historical events - */ - public async getHistoricalEvents( - address: `0x${string}`, - eventType: AllEventTypes, - abi: Abi, - fromBlock?: bigint, - toBlock?: bigint, - ): Promise { + public async getHistoricalEvents(eventType: AllEventTypes, fromBlock?: bigint, toBlock?: bigint): Promise { + const { address, abi } = this.resolveContract(eventType) + try { - const logs = await this.publicClient.getContractEvents({ + return await this.publicClient.getContractEvents({ address, abi, eventName: eventType as string, - fromBlock: fromBlock || this.config.fromBlock, - toBlock: toBlock || this.config.toBlock, + fromBlock: fromBlock ?? this.config.fromBlock, + toBlock: toBlock ?? this.config.toBlock, }) - - return logs } catch (error) { throw new SDKError(`Failed to get historical events: ${error}`, 'HISTORICAL_EVENTS_FAILED') } } - /** - * SDKEventEmitter implementation - */ public on(eventType: T, callback: EventCallback): void { if (!this.listeners.has(eventType)) { this.listeners.set(eventType, new Set()) @@ -184,7 +197,6 @@ export class EventListener implements SDKEventEmitter { callbacks.delete(callback as EventCallback) if (callbacks.size === 0) { this.listeners.delete(eventType) - // Find and stop corresponding watchers const watchersToRemove: string[] = [] this.activeWatchers.forEach((unwatch, key) => { if (key.endsWith(`:${eventType}`)) { @@ -202,12 +214,9 @@ export class EventListener implements SDKEventEmitter { } public emit(event: EnclaveEvent): void { - console.log('emit() called for ' + event.type) const callbacks = this.listeners.get(event.type) if (callbacks) { - console.log('Have ' + callbacks.size + ' callbacks') callbacks.forEach((callback) => { - console.log('Running callback...') try { void (callback as EventCallback)(event) } catch (error) { @@ -217,13 +226,9 @@ export class EventListener implements SDKEventEmitter { } } - /** - * Clean up all listeners and watchers - */ public cleanup(): void { this.stopPolling() - // Stop all active watchers this.activeWatchers.forEach((unwatch) => { try { unwatch() @@ -232,8 +237,6 @@ export class EventListener implements SDKEventEmitter { } }) this.activeWatchers.clear() - - // Clear all listeners this.listeners.clear() } diff --git a/packages/enclave-sdk/src/events/index.ts b/packages/enclave-sdk/src/events/index.ts new file mode 100644 index 0000000000..5b2d35a021 --- /dev/null +++ b/packages/enclave-sdk/src/events/index.ts @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +export { EventListener } from './event-listener' +export type { EventListenerOptions } from './event-listener' + +export { EnclaveEventType, RegistryEventType } from './types' + +export type { + AllEventTypes, + EnclaveEvent, + EventCallback, + EventFilter, + SDKEventEmitter, + EventListenerConfig, + E3RequestedData, + E3ActivatedData, + CiphertextOutputPublishedData, + PlaintextOutputPublishedData, + CiphernodeAddedData, + CiphernodeRemovedData, + CommitteeRequestedData, + CommitteePublishedData, + CommitteeFinalizedData, + EnclaveEventData, + RegistryEventData, +} from './types' diff --git a/packages/enclave-sdk/src/events/types.ts b/packages/enclave-sdk/src/events/types.ts new file mode 100644 index 0000000000..bd82143c5c --- /dev/null +++ b/packages/enclave-sdk/src/events/types.ts @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +import type { Log } from 'viem' + +export enum EnclaveEventType { + E3_REQUESTED = 'E3Requested', + CIPHERTEXT_OUTPUT_PUBLISHED = 'CiphertextOutputPublished', + PLAINTEXT_OUTPUT_PUBLISHED = 'PlaintextOutputPublished', + E3_PROGRAM_ENABLED = 'E3ProgramEnabled', + E3_PROGRAM_DISABLED = 'E3ProgramDisabled', + ENCRYPTION_SCHEME_ENABLED = 'EncryptionSchemeEnabled', + ENCRYPTION_SCHEME_DISABLED = 'EncryptionSchemeDisabled', + CIPHERNODE_REGISTRY_SET = 'CiphernodeRegistrySet', + MAX_DURATION_SET = 'MaxDurationSet', + ALLOWED_E3_PROGRAMS_PARAMS_SET = 'AllowedE3ProgramsParamsSet', + OWNERSHIP_TRANSFERRED = 'OwnershipTransferred', + INITIALIZED = 'Initialized', +} + +export enum RegistryEventType { + COMMITTEE_REQUESTED = 'CommitteeRequested', + COMMITTEE_PUBLISHED = 'CommitteePublished', + COMMITTEE_FINALIZED = 'CommitteeFinalized', + ENCLAVE_SET = 'EnclaveSet', + OWNERSHIP_TRANSFERRED = 'OwnershipTransferred', + INITIALIZED = 'Initialized', +} + +export type AllEventTypes = EnclaveEventType | RegistryEventType + +export interface E3RequestedData { + e3Id: bigint + e3: { + seed: bigint + threshold: readonly [number, number] + requestBlock: bigint + inputWindow: readonly [bigint, bigint] + encryptionSchemeId: string + e3Program: string + e3ProgramParams: string + decryptionVerifier: string + committeePublicKey: string + ciphertextOutput: string + plaintextOutput: string + } + filter: string + e3Program: string +} + +export interface E3ActivatedData { + e3Id: bigint + expiration: bigint + committeePublicKey: string +} + +export interface CiphertextOutputPublishedData { + e3Id: bigint + ciphertextOutput: string +} + +export interface PlaintextOutputPublishedData { + e3Id: bigint + plaintextOutput: string +} + +export interface CiphernodeAddedData { + node: string + index: bigint + numNodes: bigint + size: bigint +} + +export interface CiphernodeRemovedData { + node: string + index: bigint + numNodes: bigint + size: bigint +} + +export interface CommitteeRequestedData { + e3Id: bigint + seed: bigint + threshold: [bigint, bigint] + requestBlock: bigint + committeeDeadline: bigint +} + +export interface CommitteePublishedData { + e3Id: bigint + nodes: string[] + publicKey: string +} + +export interface CommitteeFinalizedData { + e3Id: bigint + nodes: string[] +} + +export interface EnclaveEventData { + [EnclaveEventType.E3_REQUESTED]: E3RequestedData + [EnclaveEventType.CIPHERTEXT_OUTPUT_PUBLISHED]: CiphertextOutputPublishedData + [EnclaveEventType.PLAINTEXT_OUTPUT_PUBLISHED]: PlaintextOutputPublishedData + [EnclaveEventType.E3_PROGRAM_ENABLED]: { e3Program: string } + [EnclaveEventType.E3_PROGRAM_DISABLED]: { e3Program: string } + [EnclaveEventType.ENCRYPTION_SCHEME_ENABLED]: { encryptionSchemeId: string } + [EnclaveEventType.ENCRYPTION_SCHEME_DISABLED]: { encryptionSchemeId: string } + [EnclaveEventType.CIPHERNODE_REGISTRY_SET]: { ciphernodeRegistry: string } + [EnclaveEventType.MAX_DURATION_SET]: { maxDuration: bigint } + [EnclaveEventType.ALLOWED_E3_PROGRAMS_PARAMS_SET]: { e3ProgramParams: string[] } + [EnclaveEventType.OWNERSHIP_TRANSFERRED]: { previousOwner: string; newOwner: string } + [EnclaveEventType.INITIALIZED]: { version: bigint } +} + +export interface RegistryEventData { + [RegistryEventType.COMMITTEE_REQUESTED]: CommitteeRequestedData + [RegistryEventType.COMMITTEE_PUBLISHED]: CommitteePublishedData + [RegistryEventType.COMMITTEE_FINALIZED]: CommitteeFinalizedData + [RegistryEventType.ENCLAVE_SET]: { enclave: string } + [RegistryEventType.OWNERSHIP_TRANSFERRED]: { previousOwner: string; newOwner: string } + [RegistryEventType.INITIALIZED]: { version: bigint } +} + +export interface EnclaveEvent { + type: T + data: T extends EnclaveEventType ? EnclaveEventData[T] : T extends RegistryEventType ? RegistryEventData[T] : unknown + log: Log + timestamp: Date + blockNumber: bigint + transactionHash: string +} + +export type EventCallback = (event: EnclaveEvent) => void | Promise + +export interface EventFilter { + address?: `0x${string}` + fromBlock?: bigint + toBlock?: bigint + args?: Partial +} + +export interface SDKEventEmitter { + on(eventType: T, callback: EventCallback): void + off(eventType: T, callback: EventCallback): void + emit(event: EnclaveEvent): void +} + +export interface EventListenerConfig { + fromBlock?: bigint + toBlock?: bigint + polling?: boolean + pollingInterval?: number +} diff --git a/packages/enclave-sdk/src/index.ts b/packages/enclave-sdk/src/index.ts index 671847ebb3..a1b14d5cbe 100644 --- a/packages/enclave-sdk/src/index.ts +++ b/packages/enclave-sdk/src/index.ts @@ -8,21 +8,37 @@ export { EnclaveSDK } from './enclave-sdk' // Core classes -export { EventListener } from './event-listener' -export { ContractClient } from './contract-client' +export { EventListener } from './events/event-listener' +export { ContractClient } from './contracts/contract-client' +export type { ContractClientConfig } from './contracts/contract-client' +export type { EventListenerOptions } from './events/event-listener' -// Types and interfaces +// Standalone encryption functions +export { + getThresholdBfvParamsSet, + generatePublicKey, + computePublicKeyCommitment, + encryptNumber, + encryptVector, + encryptNumberAndGenInputs, + encryptNumberAndGenProof, + encryptVectorAndGenInputs, + encryptVectorAndGenProof, + generateProof, +} from './crypto' + +// Types and interfaces (re-exported from sub-modules via types.ts) export type { - E3, SDKConfig, + ContractAddresses, + E3, + E3RequestParams, EventListenerConfig, - ContractInstances, EventFilter, EventCallback, SDKEventEmitter, AllEventTypes, EnclaveEvent, - // Event data types E3RequestedData, E3ActivatedData, CiphertextOutputPublishedData, @@ -37,10 +53,12 @@ export type { BfvParams, VerifiableEncryptionResult, EncryptedValueAndPublicInputs, + ThresholdBfvParamsPresetName, } from './types' -// enums and constants -export { EnclaveEventType, RegistryEventType, ThresholdBfvParamsPresetName, ThresholdBfvParamsPresetNames } from './types' +// Enums and constants +export { EnclaveEventType, RegistryEventType, ThresholdBfvParamsPresetNames, E3Stage, FailureReason } from './types' +export { DEFAULT_THRESHOLD_BFV_PARAMS_PRESET_NAME } from './constants' // Export utilities export { @@ -54,7 +72,6 @@ export { generateEventId, sleep, getCurrentTimestamp, - // BFV and E3 utilities DEFAULT_COMPUTE_PROVIDER_PARAMS, DEFAULT_E3_CONFIG, encodeBfvParams, @@ -64,5 +81,3 @@ export { decodePlaintextOutput, type ComputeProviderParams, } from './utils' - -export { generateProof } from './greco' diff --git a/packages/enclave-sdk/src/types.ts b/packages/enclave-sdk/src/types.ts index 5ab38010e2..a116fdd139 100644 --- a/packages/enclave-sdk/src/types.ts +++ b/packages/enclave-sdk/src/types.ts @@ -4,300 +4,44 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -import type { Log, PublicClient, WalletClient } from 'viem' -import type { ProofData } from '@aztec/bb.js' -import type { CiphernodeRegistryOwnable, Enclave, MockCiphernodeRegistry, MockUSDC, EnclaveToken } from '@enclave-e3/contracts/types' +import type { Chain, PublicClient, WalletClient } from 'viem' +import type { ContractAddresses } from './contracts/types' +import type { ThresholdBfvParamsPresetName } from './crypto/types' + +// Re-export all sub-module types for backward compatibility +export type { BfvParams, ThresholdBfvParamsPresetName, VerifiableEncryptionResult, EncryptedValueAndPublicInputs } from './crypto/types' + +export { ThresholdBfvParamsPresetNames } from './crypto/types' + +export type { ContractAddresses, E3, E3RequestParams } from './contracts/types' +export { E3Stage, FailureReason } from './contracts/types' + +export { EnclaveEventType, RegistryEventType } from './events/types' + +export type { + AllEventTypes, + EnclaveEvent, + EventCallback, + EventFilter, + SDKEventEmitter, + EventListenerConfig, + E3RequestedData, + E3ActivatedData, + CiphertextOutputPublishedData, + PlaintextOutputPublishedData, + CiphernodeAddedData, + CiphernodeRemovedData, + CommitteeRequestedData, + CommitteePublishedData, + CommitteeFinalizedData, + EnclaveEventData, + RegistryEventData, +} from './events/types' -import type { CircuitInputs } from './greco' - -/** - * SDK configuration - */ export interface SDKConfig { - /** - * The public client to use to interact with the blockchain - */ publicClient: PublicClient - - /** - * The wallet client to use to send/sign transactions - */ walletClient?: WalletClient - - /** - * The Enclave contracts - */ - contracts: { - /** - * The Enclave contract address - */ - enclave: `0x${string}` - - /** - * The CiphernodeRegistry contract address - */ - ciphernodeRegistry: `0x${string}` - - /** - * The FeeToken contract address - */ - feeToken: `0x${string}` - } - - /** - * The chain ID to which the contracts are deployed - */ - chainId?: number - - /** - * The threshold BFV parameters preset name to use for the Enclave requests - */ - thresholdBfvParamsPresetName: ThresholdBfvParamsPresetName -} - -export interface EventListenerConfig { - fromBlock?: bigint - toBlock?: bigint - polling?: boolean - pollingInterval?: number -} - -export interface ContractInstances { - enclave: Enclave - ciphernodeRegistry: CiphernodeRegistryOwnable | MockCiphernodeRegistry - feeToken: EnclaveToken | MockUSDC -} - -// Unified Event System -export enum EnclaveEventType { - // E3 Lifecycle Events - E3_REQUESTED = 'E3Requested', - CIPHERTEXT_OUTPUT_PUBLISHED = 'CiphertextOutputPublished', - PLAINTEXT_OUTPUT_PUBLISHED = 'PlaintextOutputPublished', - - // E3 Program Management - E3_PROGRAM_ENABLED = 'E3ProgramEnabled', - E3_PROGRAM_DISABLED = 'E3ProgramDisabled', - - // Encryption Scheme Management - ENCRYPTION_SCHEME_ENABLED = 'EncryptionSchemeEnabled', - ENCRYPTION_SCHEME_DISABLED = 'EncryptionSchemeDisabled', - - // Configuration - CIPHERNODE_REGISTRY_SET = 'CiphernodeRegistrySet', - MAX_DURATION_SET = 'MaxDurationSet', - ALLOWED_E3_PROGRAMS_PARAMS_SET = 'AllowedE3ProgramsParamsSet', - - // Ownership - OWNERSHIP_TRANSFERRED = 'OwnershipTransferred', - INITIALIZED = 'Initialized', -} - -export enum RegistryEventType { - // Committee Management - COMMITTEE_REQUESTED = 'CommitteeRequested', - COMMITTEE_PUBLISHED = 'CommitteePublished', - COMMITTEE_FINALIZED = 'CommitteeFinalized', - - // Configuration - ENCLAVE_SET = 'EnclaveSet', - - // Ownership - OWNERSHIP_TRANSFERRED = 'OwnershipTransferred', - INITIALIZED = 'Initialized', -} - -// Union type for all events -export type AllEventTypes = EnclaveEventType | RegistryEventType - -// Event data interfaces based on TypeChain types -export interface E3 { - seed: bigint - threshold: readonly [number, number] - requestBlock: bigint - inputWindow: readonly [bigint, bigint] - encryptionSchemeId: string - e3Program: string - e3ProgramParams: string - decryptionVerifier: string - committeePublicKey: string - ciphertextOutput: string - plaintextOutput: string -} - -export interface E3RequestedData { - e3Id: bigint - e3: E3 - filter: string - e3Program: string -} - -export interface E3ActivatedData { - e3Id: bigint - expiration: bigint - committeePublicKey: string -} - -export interface CiphertextOutputPublishedData { - e3Id: bigint - ciphertextOutput: string -} - -export interface PlaintextOutputPublishedData { - e3Id: bigint - plaintextOutput: string -} - -export interface CiphernodeAddedData { - node: string - index: bigint - numNodes: bigint - size: bigint -} - -export interface CiphernodeRemovedData { - node: string - index: bigint - numNodes: bigint - size: bigint -} - -export interface CommitteeRequestedData { - e3Id: bigint - seed: bigint - threshold: [bigint, bigint] - requestBlock: bigint - committeeDeadline: bigint -} - -export interface CommitteePublishedData { - e3Id: bigint - nodes: string[] - publicKey: string -} - -export interface CommitteeFinalizedData { - e3Id: bigint - nodes: string[] -} - -// Event data mapping -export interface EnclaveEventData { - [EnclaveEventType.E3_REQUESTED]: E3RequestedData - [EnclaveEventType.CIPHERTEXT_OUTPUT_PUBLISHED]: CiphertextOutputPublishedData - [EnclaveEventType.PLAINTEXT_OUTPUT_PUBLISHED]: PlaintextOutputPublishedData - [EnclaveEventType.E3_PROGRAM_ENABLED]: { e3Program: string } - [EnclaveEventType.E3_PROGRAM_DISABLED]: { e3Program: string } - [EnclaveEventType.ENCRYPTION_SCHEME_ENABLED]: { encryptionSchemeId: string } - [EnclaveEventType.ENCRYPTION_SCHEME_DISABLED]: { encryptionSchemeId: string } - [EnclaveEventType.CIPHERNODE_REGISTRY_SET]: { ciphernodeRegistry: string } - [EnclaveEventType.MAX_DURATION_SET]: { maxDuration: bigint } - [EnclaveEventType.ALLOWED_E3_PROGRAMS_PARAMS_SET]: { - e3ProgramParams: string[] - } - [EnclaveEventType.OWNERSHIP_TRANSFERRED]: { - previousOwner: string - newOwner: string - } - [EnclaveEventType.INITIALIZED]: { version: bigint } -} - -export interface RegistryEventData { - [RegistryEventType.COMMITTEE_REQUESTED]: CommitteeRequestedData - [RegistryEventType.COMMITTEE_PUBLISHED]: CommitteePublishedData - [RegistryEventType.COMMITTEE_FINALIZED]: CommitteeFinalizedData - [RegistryEventType.ENCLAVE_SET]: { enclave: string } - [RegistryEventType.OWNERSHIP_TRANSFERRED]: { - previousOwner: string - newOwner: string - } - [RegistryEventType.INITIALIZED]: { version: bigint } -} - -// Generic event structure -export interface EnclaveEvent { - type: T - data: T extends EnclaveEventType ? EnclaveEventData[T] : T extends RegistryEventType ? RegistryEventData[T] : unknown - log: Log - timestamp: Date - blockNumber: bigint - transactionHash: string -} - -export type EventCallback = (event: EnclaveEvent) => void | Promise - -export interface EventFilter { - address?: `0x${string}` - fromBlock?: bigint - toBlock?: bigint - args?: Partial -} - -export interface SDKEventEmitter { - on(eventType: T, callback: EventCallback): void - off(eventType: T, callback: EventCallback): void - emit(event: EnclaveEvent): void -} - -/** - * Result of verifiable encryption using BFV - */ -export interface VerifiableEncryptionResult { - /** - * The encrypted data - */ - encryptedData: Uint8Array - /** - * The proof generated by Greco - */ - proof: ProofData -} - -/** - * BFV parameters for an Enclave program request - * Example for BFV - * 512, // degree - * 10, // plaintext_modulus - * 0xffffee001, // moduli - * 0xffffc4001, // moduli - */ -export interface BfvParams { - /** - * The degree of the polynomial - */ - degree: number - /** - * The plaintext modulus - */ - plaintextModulus: bigint - /** - * The moduli - */ - moduli: bigint[] - /** - * error1 - */ - error1Variance: string | undefined -} - -export type ThresholdBfvParamsPresetName = 'INSECURE_THRESHOLD_512' | 'SECURE_THRESHOLD_8192' - -export const ThresholdBfvParamsPresetNames = [ - 'INSECURE_THRESHOLD_512', - 'SECURE_THRESHOLD_8192', -] as const satisfies ReadonlyArray - -/** - * The result of encrypting a value and generating a proof - */ -export interface EncryptedValueAndPublicInputs { - /** - * The encrypted data - */ - encryptedData: Uint8Array - - /** - * The public inputs for the proof - */ - circuitInputs: CircuitInputs + contracts: ContractAddresses + chain?: Chain + thresholdBfvParamsPresetName?: ThresholdBfvParamsPresetName } diff --git a/packages/enclave-sdk/tests/sdk.test.ts b/packages/enclave-sdk/tests/sdk.test.ts index c3b64ada36..ce5339789e 100644 --- a/packages/enclave-sdk/tests/sdk.test.ts +++ b/packages/enclave-sdk/tests/sdk.test.ts @@ -8,12 +8,14 @@ import { describe, expect, it } from 'vitest' import { EnclaveSDK } from '../src/enclave-sdk' import { zeroAddress } from 'viem' +import { hardhat } from 'viem/chains' +import { generatePublicKey, encryptNumber as standaloneEncryptNumber, encryptVector as standaloneEncryptVector } from '../src/crypto' describe('encryptNumber', () => { describe('trbfv', () => { // create SDK with default config const sdk = EnclaveSDK.create({ - chainId: 31337, + chain: hardhat, contracts: { enclave: zeroAddress, ciphernodeRegistry: zeroAddress, @@ -58,4 +60,20 @@ describe('encryptNumber', () => { expect(value.proof).to.be.an.instanceOf(Object) }, 9999999) }) + + describe('standalone encryption (no blockchain setup)', () => { + it('should encrypt a number using standalone functions', async () => { + const pk = await generatePublicKey('INSECURE_THRESHOLD_512') + const ct = await standaloneEncryptNumber(10n, pk, 'INSECURE_THRESHOLD_512') + expect(ct).to.be.an.instanceof(Uint8Array) + expect(ct.length).to.equal(9_242) + }) + + it('should encrypt a vector using standalone functions', async () => { + const pk = await generatePublicKey('INSECURE_THRESHOLD_512') + const ct = await standaloneEncryptVector(new BigUint64Array([1n, 2n]), pk, 'INSECURE_THRESHOLD_512') + expect(ct).to.be.an.instanceof(Uint8Array) + expect(ct.length).to.equal(9_242) + }) + }) }) diff --git a/packages/enclave-sdk/tsup.config.js b/packages/enclave-sdk/tsup.config.js index 0849fad7aa..b607d9cf52 100644 --- a/packages/enclave-sdk/tsup.config.js +++ b/packages/enclave-sdk/tsup.config.js @@ -7,9 +7,12 @@ import { defineConfig } from 'tsup' import { baseConfig } from '@enclave-e3/config/tsup' +const entry = ['src/index.ts', 'src/crypto/index.ts', 'src/contracts/index.ts', 'src/events/index.ts'] + export default defineConfig([ { ...baseConfig, + entry, include: ['./src/**/*.ts'], format: ['esm'], outExtension: () => ({ @@ -23,6 +26,7 @@ export default defineConfig([ }, { ...baseConfig, + entry, include: ['./src/**/*.ts'], format: ['cjs'], outExtension: () => ({ diff --git a/scripts/build-circuits.ts b/scripts/build-circuits.ts index 09fa4b2f78..39c3017485 100644 --- a/scripts/build-circuits.ts +++ b/scripts/build-circuits.ts @@ -9,7 +9,7 @@ import { execSync } from 'child_process' import { createHash } from 'crypto' import { appendFileSync, copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'fs' import { basename, join, resolve } from 'path' -import { ALL_GROUPS, CIRCUIT_GROUPS, CIRCUIT_VARIANTS, type CircuitGroup, type CircuitVariant } from './circuit-constants' +import { ALL_GROUPS, CIRCUIT_GROUPS, CIRCUIT_VARIANTS, type CircuitGroup } from './circuit-constants' interface CircuitInfo { name: string diff --git a/templates/default/server/index.ts b/templates/default/server/index.ts index c477cc70e8..a27acb2cc9 100644 --- a/templates/default/server/index.ts +++ b/templates/default/server/index.ts @@ -5,8 +5,10 @@ // or FITNESS FOR A PARTICULAR PURPOSE. import express, { Request, Response } from 'express' -import { EnclaveSDK, RegistryEventType, CommitteePublishedData } from '@enclave-e3/sdk' +import { EnclaveSDK } from '@enclave-e3/sdk' +import { RegistryEventType, type CommitteePublishedData } from '@enclave-e3/sdk/events' import { Log, PublicClient } from 'viem' +import { hardhat } from 'viem/chains' import { handleTestInteraction } from './testHandler' import { getCheckedEnvVars } from './utils' import { callFheRunner } from './runner' @@ -24,14 +26,14 @@ interface E3Session { const e3Sessions = new Map() +let sdkInstance: EnclaveSDK | null = null + async function createPrivateSDK() { - const { CHAIN_ID, PRIVATE_KEY, CIPHERNODE_REGISTRY_CONTRACT, ENCLAVE_CONTRACT, FEE_TOKEN_CONTRACT, RPC_URL } = getCheckedEnvVars() + if (sdkInstance) return sdkInstance - if (!isSupportedChain(CHAIN_ID)) { - throw new Error(`Unsupported CHAIN_ID: ${CHAIN_ID}`) - } + const { PRIVATE_KEY, CIPHERNODE_REGISTRY_CONTRACT, ENCLAVE_CONTRACT, FEE_TOKEN_CONTRACT, RPC_URL } = getCheckedEnvVars() - const sdk = EnclaveSDK.create({ + sdkInstance = EnclaveSDK.create({ rpcUrl: RPC_URL, privateKey: PRIVATE_KEY as `0x${string}`, contracts: { @@ -39,12 +41,11 @@ async function createPrivateSDK() { ciphernodeRegistry: CIPHERNODE_REGISTRY_CONTRACT as `0x${string}`, feeToken: FEE_TOKEN_CONTRACT as `0x${string}`, }, - chainId: CHAIN_ID, + chain: hardhat, thresholdBfvParamsPresetName: 'INSECURE_THRESHOLD_512', }) - await sdk.initialize() - return sdk + return sdkInstance } async function runProgram(e3Id: bigint): Promise { @@ -69,7 +70,7 @@ async function runProgram(e3Id: bigint): Promise { let e3ProgramParams = session.e3ProgramParams if (!e3ProgramParams) { const sdk = await createPrivateSDK() - const e3Details = (await sdk.getE3(e3Id)) as any + const e3Details = await sdk.getE3(e3Id) e3ProgramParams = e3Details.e3ProgramParams session.e3ProgramParams = e3ProgramParams } @@ -214,7 +215,7 @@ async function setupEventListeners() { console.log('📡 Setting up event listeners...') // we need to listen to CommitteePublished to know when an E3 is ready - sdk.onEnclaveEvent(RegistryEventType.COMMITTEE_PUBLISHED, handleCommitteePublishedEvent) + await sdk.onEnclaveEvent(RegistryEventType.COMMITTEE_PUBLISHED, handleCommitteePublishedEvent) await listenToInputPublishedEvents(sdk.getPublicClient(), PROGRAM_ADDRESS as `0x${string}`, 0n) @@ -225,10 +226,6 @@ function isValidHexString(value: string): value is `0x${string}` { return value.startsWith('0x') && /^0x[a-fA-F0-9]*$/.test(value) } -function isSupportedChain(value: any): value is keyof typeof EnclaveSDK.chains { - return value in EnclaveSDK.chains -} - async function handleWebhookRequest(req: Request, res: Response) { try { console.log('📨 Webhook received:') @@ -291,8 +288,6 @@ if (process.env.TEST_MODE) { app.get('/test', handleTestInteraction) } -app.get('/sessions', handleGetSessions) - async function startServer() { try { await setupEventListeners() diff --git a/templates/default/server/types.ts b/templates/default/server/types.ts index b40c156349..306a712401 100644 --- a/templates/default/server/types.ts +++ b/templates/default/server/types.ts @@ -4,7 +4,7 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -import { AllEventTypes } from '@enclave-e3/sdk' +import type { AllEventTypes } from '@enclave-e3/sdk/events' export enum ProgramEventType { INPUT_PUBLISHED = 'InputPublished', diff --git a/templates/default/tests/integration.spec.ts b/templates/default/tests/integration.spec.ts index d80e68a3a7..b44f08aded 100644 --- a/templates/default/tests/integration.spec.ts +++ b/templates/default/tests/integration.spec.ts @@ -5,18 +5,18 @@ // or FITNESS FOR A PARTICULAR PURPOSE. import { - AllEventTypes, + EnclaveSDK, calculateInputWindow, DEFAULT_COMPUTE_PROVIDER_PARAMS, DEFAULT_E3_CONFIG, - E3, - EnclaveEvent, - EnclaveEventType, - EnclaveSDK, encodeBfvParams, encodeComputeProviderParams, - RegistryEventType, + decodePlaintextOutput, } from '@enclave-e3/sdk' +import { EnclaveEventType, RegistryEventType } from '@enclave-e3/sdk/events' +import type { AllEventTypes, EnclaveEvent } from '@enclave-e3/sdk/events' +import { E3Stage } from '@enclave-e3/sdk/contracts' +import type { E3 } from '@enclave-e3/sdk/contracts' import { createWalletClient, hexToBytes, http } from 'viem' import assert from 'assert' @@ -35,12 +35,6 @@ export function getContractAddresses() { } } -function hexToUint8Array(hexString: string) { - const hex = hexString.startsWith('0x') ? hexString.slice(2) : hexString - const m = hex.match(/.{2}/g)?.map((byte) => parseInt(byte, 16)) ?? [] - return new Uint8Array(m) -} - type E3Shared = { e3Id: bigint e3Program: string @@ -66,14 +60,14 @@ type E3State = E3StateRequested | E3StatePublished | E3StateOutputPublished async function setupEventListeners(sdk: EnclaveSDK, store: Map) { async function waitForEvent(type: T, trigger?: () => Promise): Promise> { return new Promise((resolve, reject) => { - sdk.once(type, resolve) + sdk.once(type, resolve).catch(reject) if (trigger) { trigger().catch(reject) } }) } - sdk.onEnclaveEvent(EnclaveEventType.E3_REQUESTED, (event) => { + await sdk.onEnclaveEvent(EnclaveEventType.E3_REQUESTED, (event) => { const id = event.data.e3Id if (store.has(id)) { @@ -86,7 +80,7 @@ async function setupEventListeners(sdk: EnclaveSDK, store: Map) }) }) - sdk.onEnclaveEvent(RegistryEventType.COMMITTEE_PUBLISHED, (event) => { + await sdk.onEnclaveEvent(RegistryEventType.COMMITTEE_PUBLISHED, (event) => { const id = event.data.e3Id const state = store.get(id) @@ -106,7 +100,7 @@ async function setupEventListeners(sdk: EnclaveSDK, store: Map) }) }) - sdk.onEnclaveEvent(EnclaveEventType.PLAINTEXT_OUTPUT_PUBLISHED, (event) => { + await sdk.onEnclaveEvent(EnclaveEventType.PLAINTEXT_OUTPUT_PUBLISHED, (event) => { const id = event.data.e3Id const state = store.get(id) @@ -137,13 +131,13 @@ describe('Integration', () => { const store = new Map() const sdk = EnclaveSDK.create({ - chainId: 31337, contracts: { enclave: contracts.enclave, ciphernodeRegistry: contracts.ciphernodeRegistry, feeToken: contracts.feeToken, }, rpcUrl: 'ws://localhost:8545', + chain: anvil, thresholdBfvParamsPresetName: 'INSECURE_THRESHOLD_512', privateKey: testPrivateKey, }) @@ -162,7 +156,7 @@ describe('Integration', () => { const { waitForEvent } = await setupEventListeners(sdk, store) const threshold: [number, number] = [DEFAULT_E3_CONFIG.threshold_min, DEFAULT_E3_CONFIG.threshold_max] - const duration = 225 + const duration = 250 const inputWindow = await calculateInputWindow(publicClient, duration) const thresholdBfvParams = await sdk.getThresholdBfvParamsSet() const e3ProgramParams = encodeBfvParams(thresholdBfvParams) @@ -175,9 +169,21 @@ describe('Integration', () => { let state let event + // Verify fee quoting works + const requestParams = { + threshold, + inputWindow, + e3Program: contracts.e3Program, + e3ProgramParams, + computeProviderParams, + } + const quote = await sdk.getE3Quote(requestParams) + console.log('E3 quote:', quote) + assert(quote >= 0n, 'E3 quote should be a non-negative bigint') + // Approve fee token console.log('Approving fee token...') - const hash = await sdk.approveFeeToken(100000000000n) + const hash = await sdk.approveFeeToken(quote) console.log('Fee token approved:', hash) await new Promise((resolve) => setTimeout(resolve, 1000)) @@ -185,13 +191,7 @@ describe('Integration', () => { // REQUEST phase await waitForEvent(EnclaveEventType.E3_REQUESTED, async () => { console.log('Requested E3...') - await sdk.requestE3({ - threshold, - inputWindow, - e3Program: contracts.e3Program, - e3ProgramParams, - computeProviderParams, - }) + await sdk.requestE3(requestParams) }) state = store.get(0n) @@ -200,17 +200,23 @@ describe('Integration', () => { assert.strictEqual(state.type, 'requested') console.log('E3 Sucessfully Requested!') + // Verify E3 stage after request + const stageAfterRequest = await sdk.getE3Stage(state.e3Id) + assert.strictEqual(stageAfterRequest, E3Stage.Requested, 'E3 stage should be Requested after requestE3') + // Ciphernodes will publish a public key within the COMMITTEE_PUBLISHED event event = await waitForEvent(RegistryEventType.COMMITTEE_PUBLISHED) const publicKeyBytes = hexToBytes(event.data.publicKey as `0x${string}`) - state = store.get(0n) + state = store.get(state.e3Id) assert(state, 'store should have E3State but it was falsey') assert.strictEqual(state.type, 'committee_published') assert.strictEqual(state.publicKey, event.data.publicKey) - let { e3Id } = state + // Verify E3 stage after committee published + const stageAfterCommittee = await sdk.getE3Stage(state.e3Id) + assert.strictEqual(stageAfterCommittee, E3Stage.KeyPublished, 'E3 stage should be KeyPublished after committee published') // INPUT PUBLISHING phase console.log('PUBLISHING PRIVATE INPUT') @@ -223,26 +229,27 @@ describe('Integration', () => { let txHash = await publishInput( walletClient, - e3Id, + state.e3Id, `0x${Array.from(enc1, (b) => b.toString(16).padStart(2, '0')).join('')}` as `0x${string}`, account.address, contracts.e3Program, ) - await publicClient.waitForTransactionReceipt({ hash: txHash }) + await sdk.waitForTransaction(txHash) txHash = await publishInput( walletClient, - e3Id, + state.e3Id, `0x${Array.from(enc2, (b) => b.toString(16).padStart(2, '0')).join('')}` as `0x${string}`, account.address, contracts.e3Program, ) - await publicClient.waitForTransactionReceipt({ hash: txHash }) + await sdk.waitForTransaction(txHash) const plaintextEvent = await waitForEvent(EnclaveEventType.PLAINTEXT_OUTPUT_PUBLISHED) - const parsed = hexToUint8Array(plaintextEvent.data.plaintextOutput) + const result = decodePlaintextOutput(plaintextEvent.data.plaintextOutput) + assert(result !== null, 'Failed to decode plaintext output') - expect(BigInt(parsed[0])).toBe(num1 + num2) + expect(BigInt(result)).toBe(num1 + num2) console.log('Answer was correct') }, 9999999) })