diff --git a/client-sdk/examples/end-to-end.mdx b/client-sdk/examples/end-to-end.mdx index a8a3e68..746688a 100644 --- a/client-sdk/examples/end-to-end.mdx +++ b/client-sdk/examples/end-to-end.mdx @@ -18,8 +18,8 @@ import '@fhenixprotocol/cofhe-contracts/FHE.sol'; contract ConfidentialVault { mapping(address => euint64) private _balances; - function deposit(InEuint64 calldata encryptedAmount) external { - euint64 amount = FHE.asEuint64(encryptedAmount); + function deposit(externalEuint64 encryptedAmount, bytes calldata inputProof) external { + euint64 amount = FHE.asEuint64(encryptedAmount, inputProof); _balances[msg.sender] = FHE.add(_balances[msg.sender], amount); FHE.allowThis(_balances[msg.sender]); FHE.allowSender(_balances[msg.sender]); @@ -67,18 +67,19 @@ const walletClient = createWalletClient({ await client.connect(publicClient, walletClient); -// 2. Create a permit -await client.permits.getOrCreateSelfPermit(); +// 2. Create an ACP +await client.acp.getOrCreateSelfACP(); // 3. Encrypt and deposit -const [encryptedAmount] = await client +const [amountHash, signature] = await client .encryptInputs([Encryptable.uint64(100n)]) + .setConsumingContract(contract.address) .onStep((step, ctx) => { if (ctx?.isStart) console.log(`Encrypting: ${step}...`); }) .execute(); -await contract.deposit(encryptedAmount); +await contract.deposit(amountHash, signature); // 4. Decrypt for UI display const ctHash = await contract.getBalance(); @@ -91,7 +92,7 @@ console.log('Balance:', balance); // 100n // 5. Decrypt for on-chain verification const { decryptedValue, signature } = await client .decryptForTx(ctHash) - .withPermit() + .withACP() .execute(); await contract.publishBalance(ctHash, decryptedValue, signature); @@ -116,15 +117,16 @@ const client = createCofheClient(config); const { publicClient, walletClient } = await Ethers6Adapter(provider, wallet); await client.connect(publicClient, walletClient); -// 2. Create a permit -await client.permits.getOrCreateSelfPermit(); +// 2. Create an ACP +await client.acp.getOrCreateSelfACP(); // 3. Encrypt and deposit -const [encryptedAmount] = await client +const [amountHash, signature] = await client .encryptInputs([Encryptable.uint64(100n)]) + .setConsumingContract(contract.address) .execute(); -await contract.deposit(encryptedAmount); +await contract.deposit(amountHash, signature); // 4. Decrypt for UI display const ctHash = await contract.getBalance(); @@ -137,7 +139,7 @@ console.log('Balance:', balance); // 100n // 5. Decrypt for on-chain verification const { decryptedValue, signature } = await client .decryptForTx(ctHash) - .withPermit() + .withACP() .execute(); await contract.publishBalance(ctHash, decryptedValue, signature); @@ -161,10 +163,11 @@ describe('ConfidentialVault', () => { const vault = await Factory.deploy(); // Encrypt and deposit - const [encrypted] = await cofheClient + const [amountHash, proof] = await cofheClient .encryptInputs([Encryptable.uint64(100n)]) + .setConsumingContract(await vault.getAddress()) .execute(); - await (await vault.deposit(encrypted)).wait(); + await (await vault.deposit(amountHash, proof)).wait(); // Decrypt and verify const ctHash = await vault.getBalance(); @@ -180,16 +183,17 @@ describe('ConfidentialVault', () => { const vault = await Factory.deploy(); // Encrypt and deposit - const [encrypted] = await cofheClient + const [amountHash, proof] = await cofheClient .encryptInputs([Encryptable.uint64(42n)]) + .setConsumingContract(await vault.getAddress()) .execute(); - await (await vault.deposit(encrypted)).wait(); + await (await vault.deposit(amountHash, proof)).wait(); // Decrypt for on-chain verification const ctHash = await vault.getBalance(); const { decryptedValue, signature } = await cofheClient .decryptForTx(ctHash) - .withPermit() + .withACP() .execute(); // Publish on-chain with Threshold Network proof diff --git a/client-sdk/examples/templates.mdx b/client-sdk/examples/templates.mdx index faafe71..81d0835 100644 --- a/client-sdk/examples/templates.mdx +++ b/client-sdk/examples/templates.mdx @@ -25,7 +25,7 @@ npx hardhat test - `@cofhe/hardhat-plugin` and `@cofhe/sdk` pre-installed - `hardhat.config.ts` with `evmVersion: 'cancun'` and the plugin imported - A sample FHE contract -- A test demonstrating the encrypt → store → decrypt flow +- A test demonstrating the encrypt, store, and decrypt flow - Pre-configured network settings for local development and testnets ## Foundry Starter @@ -50,5 +50,5 @@ forge test -vvv - `foundry.toml` with `evm_version = "cancun"`, `solc_version = "0.8.25"`, and `code_size_limit = 100000` - `remappings.txt` configured for the plugin and mocks - A sample `Counter` contract using FHE -- Tests demonstrating `expectPlaintext`, `decryptForTx_withoutPermit`, permit-based unseal, ACL deny assertions, and fuzz tests +- Tests demonstrating `expectPlaintext`, `decryptForTx_withoutACP`, ACP-based unseal, ACL deny assertions, and fuzz tests - Deploy scripts for `eth-sepolia`, `arb-sepolia`, and `base-sepolia` diff --git a/client-sdk/foundry-plugin/cofhe-client.mdx b/client-sdk/foundry-plugin/cofhe-client.mdx index c519810..b464060 100644 --- a/client-sdk/foundry-plugin/cofhe-client.mdx +++ b/client-sdk/foundry-plugin/cofhe-client.mdx @@ -1,9 +1,9 @@ --- title: CofheClient -description: "The in-Solidity SDK shim: one client per user, produces encrypted inputs and signed permits" +description: "The in-Solidity SDK shim: one client per user, produces encrypted inputs and signed ACPs" --- -`CofheClient` is the Foundry plugin's in-Solidity SDK shim. One client per "user" in your scenario. The client carries a private key and produces encrypted inputs / signed permits **as if it were that user's frontend SDK**, with no JS bridge required. +`CofheClient` is the Foundry plugin's in-Solidity SDK shim. One client per "user" in your scenario. The client carries a private key and produces encrypted inputs and signed ACPs **as if it were that user's frontend SDK**, with no JS bridge required. ## Creating and connecting @@ -14,13 +14,15 @@ CofheClient bob = createCofheClient(); bob.connect(0xB0B); // bob.account() == vm.addr(0xB0B) ``` -After `connect`, the client knows which address to sign as. All `createInEuintN` and `permit_*` calls use that account automatically; there's no `account` argument to pass. +After `connect`, the client knows which address to sign as. All `createExternalEuintN` and `ACP_*` calls use that account automatically; there's no `account` argument to pass. To act onchain as that user, prank with `client.account()`: ```solidity +(externalEuint32 hash, bytes memory proof) = bob.createExternalEuint32(2000, address(counter)); + vm.prank(bob.account()); -counter.reset(bob.createInEuint32(2000)); +counter.reset(hash, proof); ``` @@ -29,38 +31,40 @@ A mismatch between the prank address and the client that produced the input will ## Encrypting inputs -The client mirrors the JS SDK's `encryptInputs` API, one method per encrypted Solidity type: +The client mirrors the JS SDK's `encryptInputs` API, one method per encrypted Solidity type. Each takes the plaintext plus the contract that will consume it, and returns the handle and its proof as a pair: | Method | Returns | | --- | --- | -| `createInEbool(bool)` | `InEbool` | -| `createInEuint8(uint8)` | `InEuint8` | -| `createInEuint16(uint16)` | `InEuint16` | -| `createInEuint32(uint32)` | `InEuint32` | -| `createInEuint64(uint64)` | `InEuint64` | -| `createInEuint128(uint128)` | `InEuint128` | -| `createInEaddress(address)` | `InEaddress` | +| `createExternalEbool(bool, address)` | `(externalEbool, bytes)` | +| `createExternalEuint8(uint8, address)` | `(externalEuint8, bytes)` | +| `createExternalEuint16(uint16, address)` | `(externalEuint16, bytes)` | +| `createExternalEuint32(uint32, address)` | `(externalEuint32, bytes)` | +| `createExternalEuint64(uint64, address)` | `(externalEuint64, bytes)` | +| `createExternalEuint128(uint128, address)` | `(externalEuint128, bytes)` | +| `createExternalEaddress(address, address)` | `(externalEaddress, bytes)` | -All produce signed `EncryptedInput` shapes that drop straight into the `InEuintN` parameter on the contract under test. +The second argument is the consuming contract. The verifier binds it into the signature, so a proof made for one contract will not verify in another. ```solidity -InEuint32 memory encrypted = bob.createInEuint32(42); +(externalEuint32 hash, bytes memory proof) = bob.createExternalEuint32(42, address(counter)); vm.prank(bob.account()); -counter.reset(encrypted); +counter.reset(hash, proof); ``` +For several values under one signature, use `createEncryptedInputsBatch`. + ## Decrypting The plugin exposes both decryption flows the SDK supports: | Method | Returns | Use for | | --- | --- | --- | -| `decryptForTx_withoutPermit(ctHash)` | `(bytes32 ctHash, uint256 plaintext, bytes signature)` | Globally-allowed (`FHE.allowPublic`) ciphertexts. Pass `signature` to `FHE.publishDecryptResult`. | -| `decryptForTx_withPermit(ctHash, permit)` | `(bytes32, uint256, bytes)` | ACL-gated `decryptForTx` flow. | -| `decryptForView(ctHash, permit)` | `uint256 plaintext` | Offchain seal/unseal flow. **Reverts on deny**, so use the mock directly to assert deny. | +| `decryptForTx_withoutACP(ctHash)` | `(bytes32 ctHash, uint256 plaintext, bytes signature)` | Globally-allowed (`FHE.allowPublic`) ciphertexts. Pass `signature` to `FHE.publishDecryptResult`. | +| `decryptForTx_withACP(ctHash, acp)` | `(bytes32, uint256, bytes)` | ACL-gated `decryptForTx` flow. | +| `decryptForView(ctHash, acp)` | `uint256 plaintext` | Offchain seal/unseal flow. **Reverts on deny**, so use the mock directly to assert deny. | -### Public-decrypt 3-step flow with `decryptForTx_withoutPermit` +### Public-decrypt 3-step flow with `decryptForTx_withoutACP` Mirrors the production flow when a contract calls `FHE.publishDecryptResult`: @@ -71,7 +75,7 @@ counter.allowCounterPublicly(); // calls FHE.allowPublic(handle) // Step 2: SDK fetches plaintext + threshold-network signature bytes32 ctHash = euint32.unwrap(counter.count()); -(, uint256 plaintext, bytes memory sig) = bob.decryptForTx_withoutPermit(ctHash); +(, uint256 plaintext, bytes memory sig) = bob.decryptForTx_withoutACP(ctHash); // Step 3: contract verifies signature and stores plaintext counter.revealCounter(uint32(plaintext), sig); @@ -79,57 +83,57 @@ counter.revealCounter(uint32(plaintext), sig); The same shape runs unmodified against real CoFHE on testnet. The mock signature is produced by the same `MockThresholdNetworkSigner` that `FHE.verifyDecryptResult` accepts. -### Permit-based unseal with `decryptForView` +### ACP-based unseal with `decryptForView` ```solidity -Permission memory bobPermit = bob.permit_createSelf(); -uint256 value = bob.decryptForView(ctHash, bobPermit); +ACP memory bobAcp = bob.ACP_createSelf(); +uint256 value = bob.decryptForView(ctHash, bobAcp); assertEq(value, 42); ``` `decryptForView` reverts when the caller isn't on the ACL. To **assert** the deny path (e.g. "Alice should NOT be able to decrypt Bob's value"), drop down to the mock directly. See [Testing: Deny path](/client-sdk/foundry-plugin/testing#deny-path). -## Permits +## Access Control Permissions -The client signs EIP-712 permits against the ACL's domain. Two flavors: +The client signs EIP-712 ACPs against the ACL's domain. Two flavors: | Method | Purpose | | --- | --- | -| `permit_createSelf()` | Self-permit for the connected account; sealing key is auto-derived (`keccak(address)`). | -| `permit_createShared(recipient)` | Issuer half of a shared permit (no sealing key; the recipient adds it on import). | -| `permit_exportShared(perm)` | Strip sensitive fields to produce `SharedPermitExport` (safe to transmit out-of-band). | -| `permit_importShared(export)` | Recipient-side completion: adds sealing key + recipient signature. Reverts unless `export.recipient == account()`. | -| `createSealingKey(seed)` | Custom sealing key. Rarely needed: `permit_createSelf` derives one for you. | +| `ACP_createSelf()` | Self-ACP for the connected account; sealing key is auto-derived (`keccak(address)`). | +| `ACP_createShared(recipient)` | Issuer half of a shared ACP (no sealing key; the recipient adds it on import). | +| `ACP_exportShared(acp)` | Strip sensitive fields to produce `SharedACPExport` (safe to transmit out-of-band). | +| `ACP_importShared(export)` | Recipient-side completion: adds sealing key and recipient signature. Reverts unless `export.recipient == account()`. | +| `createSealingKey(seed)` | Custom sealing key. Rarely needed: `ACP_createSelf` derives one for you. | -### Self-permit (most common) +### Self-ACP (most common) ```solidity -Permission memory bobPermit = bob.permit_createSelf(); -uint256 plaintext = bob.decryptForView(ctHash, bobPermit); +ACP memory bobAcp = bob.ACP_createSelf(); +uint256 plaintext = bob.decryptForView(ctHash, bobAcp); ``` -`permit_createSelf` builds the EIP-712 typed-data, derives a sealing key from the connected account, and signs, all in one call. +`ACP_createSelf` builds the EIP-712 typed-data, derives a sealing key from the connected account, and signs, all in one call. -### Shared permits (issuer to recipient) +### Shared ACPs (issuer to recipient) ```solidity -// Bob (issuer) creates a permit shared to Alice (recipient) -Permission memory shared = bob.permit_createShared(alice.account()); +// Bob (issuer) creates an ACP shared to Alice (recipient) +ACP memory shared = bob.ACP_createShared(alice.account()); // Bob exports it (strips bob's sealing key) for transmission -SharedPermitExport memory exported = bob.permit_exportShared(shared); +SharedACPExport memory exported = bob.ACP_exportShared(shared); // Alice imports it — adds her sealing key and recipient signature -Permission memory aliceImported = alice.permit_importShared(exported); +ACP memory aliceImported = alice.ACP_importShared(exported); ``` -`permit_importShared` reverts unless the calling client's `account()` matches `export.recipient`, preventing Alice from importing a permit shared to someone else. +`ACP_importShared` reverts unless the calling client's `account()` matches `export.recipient`, preventing Alice from importing an ACP shared to someone else. ## Common pitfalls -`vm.prank(bob.account())` while the input came from `alice.createInEuintN(...)` fails ZK verification. The input was signed for Alice's address, not Bob's. Match the client to the prank. +`vm.prank(bob.account())` while the input came from `alice.createExternalEuintN(...)` fails ZK verification. The input was signed for Alice's address, not Bob's. Match the client to the prank. @@ -146,8 +150,8 @@ expectPlaintext(counter.count(), uint32(1)); // ✅ fetch the new handle Re-fetch after each state change. - -The `pkey` passed to `connect` must derive the address used as `permit.issuer`. If you call `bob.permit_createSelf()` after `bob.connect(0xB0B)`, the issuer is `vm.addr(0xB0B)`. Trying to forge an issuer mismatch will fail signature verification. + +The `pkey` passed to `connect` must derive the address used as `acp.issuer`. If you call `bob.ACP_createSelf()` after `bob.connect(0xB0B)`, the issuer is `vm.addr(0xB0B)`. Trying to forge an issuer mismatch will fail signature verification. @@ -155,7 +159,7 @@ Useful default: most tests want a hard failure when the caller isn't permitted. ```solidity (bool allowed, string memory err, ) = mockThresholdNetwork.querySealOutput( - uint256(ctHash), block.chainid, alicePermit + uint256(ctHash), block.chainid, aliceAcp ); assertFalse(allowed); assertEq(err, "NotAllowed"); diff --git a/client-sdk/foundry-plugin/cofhe-test.mdx b/client-sdk/foundry-plugin/cofhe-test.mdx index d724bdb..5c68fe1 100644 --- a/client-sdk/foundry-plugin/cofhe-test.mdx +++ b/client-sdk/foundry-plugin/cofhe-test.mdx @@ -55,7 +55,7 @@ One client per scenario address. Connecting with a deterministic plaintext priva ## Reading plaintext values -Because `MockTaskManager` stores plaintext values onchain, you can read the underlying plaintext of any encrypted handle directly, with no permit needed. +Because `MockTaskManager` stores plaintext values onchain, you can read the underlying plaintext of any encrypted handle directly, with no ACP needed. ### `getPlaintext(handle)` @@ -76,7 +76,7 @@ Reverts if the handle isn't in mock storage. ### `expectPlaintext(handle, value)` and `(handle, value, "msg")` -Assertion variant, with typed overloads for the same type set. Faster than `decryptForView` (no SDK round-trip, no permit needed). Use it whenever you only care about the value, not the SDK code path. +Assertion variant, with typed overloads for the same type set. Faster than `decryptForView` (no SDK round-trip, no ACP needed). Use it whenever you only care about the value, not the SDK code path. ```solidity expectPlaintext(counter.count(), uint32(42)); @@ -138,5 +138,5 @@ The remappings are relative to the foundry package's root. Running `forge test` ## Next steps -- [CofheClient](/client-sdk/foundry-plugin/cofhe-client): per-user encrypt / decrypt / permit shim. +- [CofheClient](/client-sdk/foundry-plugin/cofhe-client): per-user encrypt, decrypt, and ACP shim. - [Testing](/client-sdk/foundry-plugin/testing): canonical test-writing patterns and migration mapping. diff --git a/client-sdk/foundry-plugin/getting-started.mdx b/client-sdk/foundry-plugin/getting-started.mdx index 1c67a5b..eacf6fd 100644 --- a/client-sdk/foundry-plugin/getting-started.mdx +++ b/client-sdk/foundry-plugin/getting-started.mdx @@ -3,7 +3,7 @@ title: Getting Started description: "Set up @cofhe/foundry-plugin for local FHE contract development and testing under Forge" --- -`@cofhe/foundry-plugin` is the Foundry counterpart to [`@cofhe/hardhat-plugin`](/client-sdk/hardhat-plugin/getting-started). It provides two abstract Solidity contracts, `CofheTest` (test base, deploys all CoFHE mocks) and `CofheClient` (per-account encrypt/decrypt/permit shim), that let you exercise FHE contracts under `forge test` with **no JS SDK required**. +`@cofhe/foundry-plugin` is the Foundry counterpart to [`@cofhe/hardhat-plugin`](/client-sdk/hardhat-plugin/getting-started). It provides two abstract Solidity contracts, `CofheTest` (test base, deploys all CoFHE mocks) and `CofheClient` (per-account encrypt, decrypt, and ACP shim), that let you exercise FHE contracts under `forge test` with **no JS SDK required**. Want to skip the setup? Clone the [cofhe-foundry-starter](https://github.com/FhenixProtocol/cofhe-foundry-starter) template to get a pre-configured project ready to go. @@ -12,8 +12,8 @@ Want to skip the setup? Clone the [cofhe-foundry-starter](https://github.com/Fhe ## What the plugin provides - **`CofheTest`**: abstract test base that inherits `forge-std/Test` and deploys the full CoFHE mock stack (`MockTaskManager`, `MockACL`, `MockZkVerifier`, `MockThresholdNetwork`). -- **`CofheClient`**: in-Solidity SDK shim. One client per "user" in your scenario; each client carries a private key and produces encrypted inputs and signed permits as if it were that user's frontend SDK. -- **Plaintext assertions**: `expectPlaintext(handle, value)` reads the onchain plaintext from the mock task manager. Faster than `decryptForView` and needs no permit. +- **`CofheClient`**: in-Solidity SDK shim. One client per "user" in your scenario; each client carries a private key and produces encrypted inputs and signed ACPs as if it were that user's frontend SDK. +- **Plaintext assertions**: `expectPlaintext(handle, value)` reads the onchain plaintext from the mock task manager. Faster than `decryptForView` and needs no ACP. ## Prerequisites @@ -31,15 +31,15 @@ The plugin and its dependencies are distributed via npm. Install them as dev dep ```bash npm -npm install -D @cofhe/foundry-plugin@^0.5.2 @cofhe/mock-contracts@^0.5.2 @fhenixprotocol/cofhe-contracts@^0.1.3 @openzeppelin/contracts +npm install -D @cofhe/foundry-plugin@^0.7.1 @cofhe/mock-contracts@^0.7.1 @fhenixprotocol/cofhe-contracts@^0.2.0 @openzeppelin/contracts ``` ```bash pnpm -pnpm add -D @cofhe/foundry-plugin@^0.5.2 @cofhe/mock-contracts@^0.5.2 @fhenixprotocol/cofhe-contracts@^0.1.3 @openzeppelin/contracts +pnpm add -D @cofhe/foundry-plugin@^0.7.1 @cofhe/mock-contracts@^0.7.1 @fhenixprotocol/cofhe-contracts@^0.2.0 @openzeppelin/contracts ``` ```bash yarn -yarn add -D @cofhe/foundry-plugin@^0.5.2 @cofhe/mock-contracts@^0.5.2 @fhenixprotocol/cofhe-contracts@^0.1.3 @openzeppelin/contracts +yarn add -D @cofhe/foundry-plugin@^0.7.1 @cofhe/mock-contracts@^0.7.1 @fhenixprotocol/cofhe-contracts@^0.2.0 @openzeppelin/contracts ``` @@ -76,7 +76,7 @@ code_size_limit = 100000 # mocks exceed 24 KB -`evm_version = "cancun"` is no longer required as of `@cofhe/mock-contracts@0.5.0`. `MockACL` was migrated off transient storage (`tstore`/`tload`) to block-number-based storage, and the pragma was lowered to `>=0.8.19`. Set it only if your own contracts need cancun-specific opcodes. +`evm_version = "cancun"` is no longer required as of `@cofhe/mock-contracts@0.7.1`. `MockACL` was migrated off transient storage (`tstore`/`tload`) to block-number-based storage, and the pragma was lowered to `>=0.8.19`. Set it only if your own contracts need cancun-specific opcodes. @@ -129,9 +129,9 @@ Known-aligned tuple as of writing: | Package | Version | | --- | --- | -| `@cofhe/foundry-plugin` | `0.5.2` | -| `@cofhe/mock-contracts` | `0.5.2` | -| `@fhenixprotocol/cofhe-contracts` | `0.1.3` | +| `@cofhe/foundry-plugin` | `0.7.1` | +| `@cofhe/mock-contracts` | `0.7.1` | +| `@fhenixprotocol/cofhe-contracts` | `0.2.0` | See the [Compatibility](/get-started/introduction/compatibility) page for the canonical table. @@ -141,7 +141,7 @@ The mocks are the same `@cofhe/mock-contracts` package the [Hardhat plugin](/cli - Plaintext lives onchain in `MockTaskManager.mockStorage` (so `expectPlaintext` and `getPlaintext` work). - No real ZK proving; encrypted inputs are signed by `MockZkVerifierSigner`. -- Decryption is synchronous. `decryptForTx_withoutPermit` returns the result immediately. +- Decryption is synchronous. `decryptForTx_withoutACP` returns the result immediately. - Mock signatures are accepted by the same `FHE.verifyDecryptResult` your contract uses on testnet. The same test code runs unchanged against real CoFHE on a deployed network. @@ -149,5 +149,5 @@ The same test code runs unchanged against real CoFHE on a deployed network. ## Next steps - [CofheTest](/client-sdk/foundry-plugin/cofhe-test): the test base contract: `deployMocks`, `expectPlaintext`, `getPlaintext`, log toggles. -- [CofheClient](/client-sdk/foundry-plugin/cofhe-client): per-user shim: `createInEuintN`, `decryptForTx_withoutPermit`, `decryptForView`, permits. +- [CofheClient](/client-sdk/foundry-plugin/cofhe-client): per-user shim: `createExternalEuintN`, `decryptForTx_withoutACP`, `decryptForView`, ACPs. - [Testing](/client-sdk/foundry-plugin/testing): canonical test patterns and the migration mapping from the old `@cofhe/mock-contracts/foundry/CoFheTest.sol` API. diff --git a/client-sdk/foundry-plugin/testing.mdx b/client-sdk/foundry-plugin/testing.mdx index 256d50f..ea47406 100644 --- a/client-sdk/foundry-plugin/testing.mdx +++ b/client-sdk/foundry-plugin/testing.mdx @@ -10,7 +10,7 @@ This page shows the load-bearing patterns for writing FHE contract tests under F ```solidity test/Counter.t.sol import { CofheTest } from "@cofhe/foundry-plugin/contracts/CofheTest.sol"; import { CofheClient } from "@cofhe/foundry-plugin/contracts/CofheClient.sol"; -import { InEuint32 } from "@fhenixprotocol/cofhe-contracts/FHE.sol"; +import { externalEuint32 } from "@fhenixprotocol/cofhe-contracts/FHE.sol"; import { Counter } from "../src/Counter.sol"; contract CounterTest is CofheTest { @@ -49,20 +49,21 @@ That's the load-bearing shape. Everything below is what to add as the contract g ### 2. One `CofheClient` per scenario address -Each user with their own permit/encrypted inputs gets their own client. Connect with a deterministic plaintext private key. Don't recycle real keys; these are visible in test output. +Each user with their own ACP and encrypted inputs gets their own client. Connect with a deterministic plaintext private key. Don't recycle real keys; these are visible in test output. ```solidity CofheClient bob = createCofheClient(); bob.connect(0xB0B); // bob.account() == vm.addr(0xB0B) ``` -You don't pass `account` to `createInEuintN`; the client is bound at `connect`. +You don't pass `account` to `createExternalEuintN`; the client is bound at `connect`. ### 3. `vm.prank(client.account())` to act onchain as that user ```solidity vm.prank(bob.account()); -counter.reset(bob.createInEuint32(2000)); +(externalEuint32 hash, bytes memory proof) = bob.createExternalEuint32(2000, address(counter)); +counter.reset(hash, proof); ``` Mismatching the prank address and the client that produced the input fails the ZK-verifier signature check. @@ -73,9 +74,9 @@ Mismatching the prank address and the client that produced the input fails the Z expectPlaintext(counter.count(), uint32(2000)); // typed overload ``` -Faster than `decryptForView` and needs no permit. Reserve the SDK path for tests where the SDK behavior itself is under test. +Faster than `decryptForView` and needs no ACP. Reserve the SDK path for tests where the SDK behavior itself is under test. -### 5. Test the public-decrypt 3-step flow with `decryptForTx_withoutPermit` +### 5. Test the public-decrypt 3-step flow with `decryptForTx_withoutACP` When the contract calls `FHE.publishDecryptResult`: @@ -86,7 +87,7 @@ counter.allowCounterPublicly(); // FHE.allowPublic(handle) // Step 2: SDK fetches plaintext + threshold-network signature bytes32 ctHash = euint32.unwrap(counter.count()); -(, uint256 plaintext, bytes memory sig) = bob.decryptForTx_withoutPermit(ctHash); +(, uint256 plaintext, bytes memory sig) = bob.decryptForTx_withoutACP(ctHash); // Step 3: contract verifies signature and stores plaintext counter.revealCounter(uint32(plaintext), sig); @@ -95,26 +96,26 @@ assertEq(counter.getDecryptedValue(), plaintext); Same shape runs unmodified against real CoFHE on testnet. The mock signature is produced by the same `MockThresholdNetworkSigner` that `FHE.verifyDecryptResult` accepts. -### 6. Permit-based unseal: `decryptForView` for the success path +### 6. ACP-based unseal: `decryptForView` for the success path ```solidity -import { Permission } from "@cofhe/mock-contracts/contracts/Permissioned.sol"; +import { ACP } from "@cofhe/mock-contracts/contracts/Permissioned.sol"; -Permission memory bobPermit = bob.permit_createSelf(); -uint256 value = bob.decryptForView(ctHash, bobPermit); +ACP memory bobAcp = bob.ACP_createSelf(); +uint256 value = bob.decryptForView(ctHash, bobAcp); assertEq(value, expected); ``` -`permit_createSelf` builds the EIP-712 typed-data, derives a sealing key from the connected account, and signs, with no manual `signPermissionSelf` boilerplate. +`ACP_createSelf` builds the EIP-712 typed-data, derives a sealing key from the connected account, and signs, with no manual `signPermissionSelf` boilerplate. ### 7. Deny path: when the caller is not on the ACL {#deny-path} `decryptForView` reverts when the caller isn't on the ACL. To assert the deny path, drop down to the mock directly: ```solidity -Permission memory alicePermit = alice.permit_createSelf(); +ACP memory aliceAcp = alice.ACP_createSelf(); (bool allowed, string memory err, ) = mockThresholdNetwork.querySealOutput( - uint256(ctHash), block.chainid, alicePermit + uint256(ctHash), block.chainid, aliceAcp ); assertFalse(allowed, "Alice should NOT be allowed"); assertEq(err, "NotAllowed"); @@ -126,14 +127,14 @@ assertEq(err, "NotAllowed"); ```solidity function testFuzz_Reset(uint32 v) public { - InEuint32 memory enc = bob.createInEuint32(v); + (externalEuint32 enc, bytes memory proof) = bob.createExternalEuint32(v, address(counter)); vm.prank(bob.account()); counter.reset(enc); expectPlaintext(counter.count(), v); } ``` -`createInEuintN` accepts the full `uintN` range, so no shaping needed. +`createExternalEuintN` accepts the full `uintN` range, so no shaping needed. ## Migration from the old `mock-contracts` API @@ -145,13 +146,13 @@ If you're upgrading from `@cofhe/mock-contracts@0.4.x` (where `CoFheTest` lived | `is Test, CoFheTest` | `is CofheTest` (Test already inherited) | | `assertHashValue(handle, value)` | `expectPlaintext(handle, value)` | | `mockStorage(ctHash)` | `getPlaintext(ctHash)` | -| `createInEuint32(v, bob)` | `bob.createInEuint32(v)` | -| `createPermissionSelf(bob)` + `signPermissionSelf(perm, bobKey)` | `bob.permit_createSelf()` (auto-signs) | -| `createSealingKey(seed)` | `bob.createSealingKey(seed)` (rarely needed: `permit_createSelf` derives one) | -| `queryDecrypt(hash, chainId, permit)` | `bob.decryptForView(hash, permit)` (reverts on deny) | -| Same, asserting deny | `mockThresholdNetwork.querySealOutput(hash, block.chainid, permit)` | +| `createInEuint32(v, bob)` | `bob.createExternalEuint32(v, address(target))` | +| `createPermissionSelf(bob)` + `signPermissionSelf(perm, bobKey)` | `bob.ACP_createSelf()` (auto-signs) | +| `createSealingKey(seed)` | `bob.createSealingKey(seed)` (rarely needed: `ACP_createSelf` derives one) | +| `queryDecrypt(hash, chainId, permit)` | `bob.decryptForView(hash, acp)` (reverts on deny) | +| Same, asserting deny | `mockThresholdNetwork.querySealOutput(hash, block.chainid, acp)` | | `querySealOutput` + `unseal` | `bob.decryptForView` (does both) | -| `decryptForTxWithoutPermit(ct)` returns `(allowed, error, plaintext)` | `bob.decryptForTx_withoutPermit(ct)` returns `(ctHash, plaintext, signature)` | +| `decryptForTxWithoutPermit(ct)` returns `(allowed, error, plaintext)` | `bob.decryptForTx_withoutACP(ct)` returns `(ctHash, plaintext, signature)` | ## Common pitfalls @@ -165,7 +166,7 @@ Tests pass on the first op, then a second op reverts with `ACLNotAllowed` becaus -`vm.prank(bob.account())` while the input came from `alice.createInEuintN(...)` fails ZK verification. Always match the client to the prank. +`vm.prank(bob.account())` while the input came from `alice.createExternalEuintN(...)` fails ZK verification. Always match the client to the prank. @@ -181,8 +182,8 @@ expectPlaintext(counter.count(), uint32(1)); // ✅ re-fetch Re-fetch after each state change. - -The `pkey` passed to `connect` must derive the address used as `permit.issuer`. `bob.permit_createSelf()` after `bob.connect(0xB0B)` produces `issuer == vm.addr(0xB0B)`. Trying to forge a mismatch fails signature verification. + +The `pkey` passed to `connect` must derive the address used as `acp.issuer`. `bob.ACP_createSelf()` after `bob.connect(0xB0B)` produces `issuer == vm.addr(0xB0B)`. Trying to forge a mismatch fails signature verification. diff --git a/client-sdk/hardhat-plugin/client.mdx b/client-sdk/hardhat-plugin/client.mdx index 1d95150..759874d 100644 --- a/client-sdk/hardhat-plugin/client.mdx +++ b/client-sdk/hardhat-plugin/client.mdx @@ -12,7 +12,7 @@ The plugin extends `hre` with `hre.cofhe`, providing three ways to create and co 1. Creates a CoFHE config with `environment: 'hardhat'` and `supportedChains: [hardhat]` 2. Creates a `CofheClient` 3. Connects it using the provided Hardhat signer (defaults to the first signer) -4. Generates a self-usage permit for the signer +4. Generates a self-usage ACP for the signer ```typescript import hre from 'hardhat'; @@ -24,14 +24,14 @@ cofheClient.connected; // true ``` -`createClientWithBatteries` generates a self-permit automatically, so most test operations (encrypting inputs, decrypting for view, decrypting for tx) work immediately without any extra setup. +`createClientWithBatteries` generates a self-ACP automatically, so most test operations (encrypting inputs, decrypting for view, decrypting for tx) work immediately without any extra setup. If `signer` is omitted, the first signer from `hre.ethers.getSigners()` is used. ## Manual setup -For more control — custom config options, multiple signers, or adjusting `encryptDelay` — you can set up the client step by step. +For more control (custom config options, multiple signers, or adjusting `encryptDelay`) you can set up the client step by step. @@ -95,4 +95,4 @@ Once connected, the client works identically to the standard SDK client. See: - [Encrypting Inputs](/client-sdk/guides/encrypting-inputs) - [Decrypt to View](/client-sdk/guides/decrypt-to-view) - [Decrypt to Transact](/client-sdk/guides/decrypt-to-tx) -- [Permits](/client-sdk/guides/permits) +- [Access Control Permissions](/client-sdk/guides/acps) diff --git a/client-sdk/hardhat-plugin/mock-contracts.mdx b/client-sdk/hardhat-plugin/mock-contracts.mdx index db64735..0143398 100644 --- a/client-sdk/hardhat-plugin/mock-contracts.mdx +++ b/client-sdk/hardhat-plugin/mock-contracts.mdx @@ -63,7 +63,7 @@ const testBed = await hre.cofhe.mocks.getTestBed(); ## Reading plaintext values -Because `MockTaskManager` stores plaintext values onchain, you can read the underlying plaintext of any encrypted handle directly in tests, no permit needed. +Because `MockTaskManager` stores plaintext values onchain, you can read the underlying plaintext of any encrypted handle directly in tests, no ACP needed. ### `getPlaintext(ctHash)` diff --git a/client-sdk/hardhat-plugin/testing.mdx b/client-sdk/hardhat-plugin/testing.mdx index 9ffd6e6..5594850 100644 --- a/client-sdk/hardhat-plugin/testing.mdx +++ b/client-sdk/hardhat-plugin/testing.mdx @@ -7,7 +7,7 @@ This page shows the common patterns for writing Hardhat tests with the CoFHE plu ## Test setup -Use `hre.cofhe.createClientWithBatteries` in a `before` hook. It creates and connects a fully configured `CofheClient`, including a self-permit, so the client is ready for every test in the suite: +Use `hre.cofhe.createClientWithBatteries` in a `before` hook. It creates and connects a fully configured `CofheClient`, including a self-ACP, so the client is ready for every test in the suite: ```typescript import hre from 'hardhat'; @@ -33,13 +33,14 @@ The core test loop: encrypt a value, pass it to a contract, then decrypt the sto import { Encryptable, FheTypes } from '@cofhe/sdk'; import { expect } from 'chai'; -// 1. Encrypt the input -const encrypted = await cofheClient +// 1. Encrypt the input, bound to the contract that will consume it +const [valueHash, signature] = await cofheClient .encryptInputs([Encryptable.uint32(100n)]) + .setConsumingContract(await testContract.getAddress()) .execute(); // 2. Send to contract -const tx = await testContract.setValue(encrypted[0]); +const tx = await testContract.setValue(valueHash, signature); await tx.wait(); // 3. Read the stored handle @@ -55,7 +56,7 @@ expect(decrypted).to.equal(100n); ## Reading plaintext directly -In tests you can bypass the normal decrypt flow and read the raw plaintext stored by the mock contracts. This is useful for asserting contract state without needing a permit: +In tests you can bypass the normal decrypt flow and read the raw plaintext stored by the mock contracts. This is useful for asserting contract state without needing an ACP: ```typescript import hre from 'hardhat'; @@ -69,21 +70,21 @@ await hre.cofhe.mocks.expectPlaintext(ctHash, 100n); See [Mock Contracts](/client-sdk/hardhat-plugin/mock-contracts) for details. -## Permits +## Access Control Permissions -`createClientWithBatteries` pre-generates a self-permit, so `decryptForView` and `decryptForTx().withPermit()` work immediately. For tests that need named permits or multiple signers, create them explicitly: +`createClientWithBatteries` pre-generates a self-ACP, so `decryptForView` and `decryptForTx().withACP()` work immediately. For tests that need named ACPs or multiple signers, create them explicitly: ```typescript -import { PermitUtils } from '@cofhe/sdk/permits'; +import { ACPUtils } from '@cofhe/sdk/acps'; -const permit = await cofheClient.permits.createSelf({ +const acp = await cofheClient.acp.createSelf({ issuer: signer.address, - name: 'My Test Permit', + name: 'My Test ACP', }); -// Select it as the active permit -const permitHash = PermitUtils.getHash(permit); -cofheClient.permits.selectActivePermit(permitHash); +// Select it as the active ACP +const acpHash = ACPUtils.getHash(acp); +cofheClient.acp.selectActiveACP(acpHash); ``` Alternatively, create a separate client for each signer: @@ -98,43 +99,43 @@ const aliceClient = await hre.cofhe.createClientWithBatteries(alice); ## `decryptForTx` patterns -[`decryptForTx`](/client-sdk/guides/decrypt-to-tx) returns a `{ ctHash, decryptedValue, signature }` tuple for onchain submission. The permit mode must be selected explicitly. +[`decryptForTx`](/client-sdk/guides/decrypt-to-tx) returns a `{ ctHash, decryptedValue, signature }` tuple for onchain submission. The ACP mode must be selected explicitly. -### Globally allowed values (`.withoutPermit()`) +### Globally allowed values (`.withoutACP()`) -When a contract calls `FHE.allowPublic(handle)`, anyone can decrypt without a permit: +When a contract calls `FHE.allowPublic(handle)`, anyone can decrypt without an ACP: ```typescript import { expect } from 'chai'; const result = await cofheClient .decryptForTx(publicCtHash) - .withoutPermit() + .withoutACP() .execute(); expect(result.decryptedValue).to.equal(55n); ``` -### Access-controlled values (`.withPermit()`) +### Access-controlled values (`.withACP()`) -For handles restricted by ACL policy, supply a permit: +For handles restricted by ACL policy, supply an ACP: -```typescript Explicit permit +```typescript Explicit ACP const result = await cofheClient .decryptForTx(ctHash) - .withPermit(permit) + .withACP(acp) .execute(); expect(result.decryptedValue).to.equal(99n); ``` -```typescript Active permit -// resolves the active permit automatically +```typescript Active ACP +// resolves the active ACP automatically const result = await cofheClient .decryptForTx(ctHash) - .withPermit() + .withACP() .execute(); expect(result.decryptedValue).to.equal(99n); diff --git a/client-sdk/introduction/installation.mdx b/client-sdk/introduction/installation.mdx index 78bdd61..875afbf 100644 --- a/client-sdk/introduction/installation.mdx +++ b/client-sdk/introduction/installation.mdx @@ -7,33 +7,33 @@ description: "Install and configure @cofhe/sdk for your project" - Node.js 18+ - TypeScript 5+ -- Viem 2+ +- viem 2.38.6+ ## Install packages ```bash npm -npm install @cofhe/sdk@^0.5.2 @fhenixprotocol/cofhe-contracts@^0.1.3 +npm install @cofhe/sdk@^0.7.1 @fhenixprotocol/cofhe-contracts@^0.2.0 ``` ```bash pnpm -pnpm add @cofhe/sdk@^0.5.2 @fhenixprotocol/cofhe-contracts@^0.1.3 +pnpm add @cofhe/sdk@^0.7.1 @fhenixprotocol/cofhe-contracts@^0.2.0 ``` ```bash yarn -yarn add @cofhe/sdk@^0.5.2 @fhenixprotocol/cofhe-contracts@^0.1.3 +yarn add @cofhe/sdk@^0.7.1 @fhenixprotocol/cofhe-contracts@^0.2.0 ``` | Package | Version | Purpose | | --- | --- | --- | -| `@cofhe/sdk` | `^0.5.2` | Client-side encryption, decryption, and permit management | -| `@fhenixprotocol/cofhe-contracts` | `^0.1.3` | `FHE.sol` — the Solidity library imported by your contracts | +| `@cofhe/sdk` | `^0.7.1` | Client-side encryption, decryption, and ACP management | +| `@fhenixprotocol/cofhe-contracts` | `^0.2.0` | `FHE.sol`, the Solidity library imported by your contracts | -`@fhenixprotocol/cofhe-contracts@0.1.3` requires `@cofhe/sdk` version `>= 0.5.1`. Latest published is `0.5.2`. +`@cofhe/sdk` `0.7.x` pairs with `@fhenixprotocol/cofhe-contracts` `0.2.0`. Keep every `@cofhe/*` package on the same version. They are released together and pin each other exactly. The [compatibility page](/get-started/introduction/compatibility) carries the full table. ## For Hardhat projects @@ -43,24 +43,24 @@ If you are using Hardhat for development and testing, also install the plugin: ```bash npm -npm install @cofhe/hardhat-plugin@^0.5.2 @cofhe/sdk@^0.5.2 @fhenixprotocol/cofhe-contracts@^0.1.3 +npm install @cofhe/hardhat-plugin@^0.7.1 @cofhe/sdk@^0.7.1 @fhenixprotocol/cofhe-contracts@^0.2.0 ``` ```bash pnpm -pnpm add @cofhe/hardhat-plugin@^0.5.2 @cofhe/sdk@^0.5.2 @fhenixprotocol/cofhe-contracts@^0.1.3 +pnpm add @cofhe/hardhat-plugin@^0.7.1 @cofhe/sdk@^0.7.1 @fhenixprotocol/cofhe-contracts@^0.2.0 ``` ```bash yarn -yarn add @cofhe/hardhat-plugin@^0.5.2 @cofhe/sdk@^0.5.2 @fhenixprotocol/cofhe-contracts@^0.1.3 +yarn add @cofhe/hardhat-plugin@^0.7.1 @cofhe/sdk@^0.7.1 @fhenixprotocol/cofhe-contracts@^0.2.0 ``` | Package | Version | Purpose | | --- | --- | --- | -| `@cofhe/hardhat-plugin` | `^0.5.2` | Extends Hardhat with `hre.cofhe`, deploys mock contracts automatically | -| `@cofhe/sdk` | `^0.5.2` | Client-side encryption, decryption, and permit management | -| `@fhenixprotocol/cofhe-contracts` | `^0.1.3` | `FHE.sol` — the Solidity library imported by your contracts | +| `@cofhe/hardhat-plugin` | `^0.7.1` | Extends Hardhat with `hre.cofhe`, deploys mock contracts automatically | +| `@cofhe/sdk` | `^0.7.1` | Client-side encryption, decryption, and ACP management | +| `@fhenixprotocol/cofhe-contracts` | `^0.2.0` | `FHE.sol`, the Solidity library imported by your contracts | See the [Hardhat Plugin Getting Started](/client-sdk/hardhat-plugin/getting-started) guide for configuration details. @@ -71,17 +71,17 @@ If you are using Foundry for development and testing, install the Foundry plugin ```bash npm -npm install -D @cofhe/foundry-plugin@^0.5.2 @cofhe/mock-contracts@^0.5.2 @fhenixprotocol/cofhe-contracts@^0.1.3 @openzeppelin/contracts +npm install -D @cofhe/foundry-plugin@^0.7.1 @cofhe/mock-contracts@^0.7.1 @fhenixprotocol/cofhe-contracts@^0.2.0 @openzeppelin/contracts forge install foundry-rs/forge-std ``` ```bash pnpm -pnpm add -D @cofhe/foundry-plugin@^0.5.2 @cofhe/mock-contracts@^0.5.2 @fhenixprotocol/cofhe-contracts@^0.1.3 @openzeppelin/contracts +pnpm add -D @cofhe/foundry-plugin@^0.7.1 @cofhe/mock-contracts@^0.7.1 @fhenixprotocol/cofhe-contracts@^0.2.0 @openzeppelin/contracts forge install foundry-rs/forge-std ``` ```bash yarn -yarn add -D @cofhe/foundry-plugin@^0.5.2 @cofhe/mock-contracts@^0.5.2 @fhenixprotocol/cofhe-contracts@^0.1.3 @openzeppelin/contracts +yarn add -D @cofhe/foundry-plugin@^0.7.1 @cofhe/mock-contracts@^0.7.1 @fhenixprotocol/cofhe-contracts@^0.2.0 @openzeppelin/contracts forge install foundry-rs/forge-std ``` @@ -89,12 +89,12 @@ forge install foundry-rs/forge-std | Package | Version | Purpose | | --- | --- | --- | -| `@cofhe/foundry-plugin` | `^0.5.2` | `CofheTest` and `CofheClient` — Solidity test base and per-user SDK shim | -| `@cofhe/mock-contracts` | `^0.5.2` | Mock CoFHE contracts used by the Foundry plugin | -| `@fhenixprotocol/cofhe-contracts` | `^0.1.3` | `FHE.sol` — the Solidity library imported by your contracts | +| `@cofhe/foundry-plugin` | `^0.7.1` | `CofheTest` and `CofheClient`, the Solidity test base and per-user SDK shim | +| `@cofhe/mock-contracts` | `^0.7.1` | Mock CoFHE contracts used by the Foundry plugin | +| `@fhenixprotocol/cofhe-contracts` | `^0.2.0` | `FHE.sol`, the Solidity library imported by your contracts | -The Foundry plugin uses Solidity-only abstractions — no `@cofhe/sdk` (JS) needed for tests. +The Foundry plugin uses Solidity-only abstractions, so tests need no `@cofhe/sdk` (JS). See the [Foundry Plugin Getting Started](/client-sdk/foundry-plugin/getting-started) guide for `foundry.toml` and `remappings.txt` setup. @@ -116,5 +116,5 @@ import { Encryptable, FheTypes } from '@cofhe/sdk'; ## Next steps -- [Quick Start](/client-sdk/quick-start) — write your first FHE contract and test -- [Client Setup](/client-sdk/guides/client-setup) — configure and connect the SDK client +- [Quick start](/client-sdk/quick-start/javascript): write your first FHE contract and test. +- [Client setup](/client-sdk/guides/client-setup): configure and connect the SDK client. diff --git a/client-sdk/introduction/mental-model.mdx b/client-sdk/introduction/mental-model.mdx index 1623165..4e879ec 100644 --- a/client-sdk/introduction/mental-model.mdx +++ b/client-sdk/introduction/mental-model.mdx @@ -24,9 +24,9 @@ When a user wants to add `5` to their counter, the data must first be encrypted **What happens:** -1. The user's plaintext value `5` is encrypted using `client.encryptInputs([Encryptable.uint32(5n)]).execute()` -2. The SDK generates a ZK proof and submits the encrypted value to the CoFHE verifier -3. The returned `EncryptedItemInput` is sent to the smart contract onchain +1. The user's plaintext value `5` is encrypted using `client.encryptInputs([Encryptable.uint32(5n)]).setConsumingContract(address).execute()` +2. The SDK generates a ZK proof and submits the encrypted value to the CoFHE verifier's batch endpoint +3. The returned ciphertext handle and its batch signature are sent to the smart contract onchain 4. The blockchain sees only encrypted data, never the actual value `5` ### The "Locked Box" Analogy @@ -64,8 +64,8 @@ When a user wants to read their counter value, they use one of the SDK's two dec **For UI display (`decryptForView`):** 1. The user reads the encrypted handle (`ctHash`) from the contract -2. A permit authorizes decryption. Created via `client.permits.getOrCreateSelfPermit()` -3. The SDK requests re-encryption from the Threshold Network using the permit's sealing key +2. An ACP authorizes decryption. Created via `client.acp.getOrCreateSelfACP()` +3. The SDK requests re-encryption from the Threshold Network using the ACP's sealing key 4. The plaintext is returned locally for display **For onchain use (`decryptForTx`):** @@ -79,7 +79,7 @@ When a user wants to read their counter value, they use one of the SDK's two dec This is like exchanging locks on the box: - The box starts locked with the CoFHE co-processor's lock -- The user sends their own lock to the co-processor (via the permit's sealing key) +- The user sends their own lock to the co-processor (via the ACP's sealing key) - The co-processor removes its lock and applies the user's lock - The box remains locked throughout, but now only the user can open it - The data remains private at every step @@ -102,9 +102,9 @@ sequenceDiagram User->>SDK: Add value 5 to counter SDK->>SDK: encryptInputs([Encryptable.uint32(5n)]) SDK->>CoFHE: Submit ZK proof + encrypted value - CoFHE-->>SDK: Signed EncryptedItemInput - SDK->>Blockchain: Send transaction with encrypted value - Blockchain->>Contract: increment(encryptedValue) + CoFHE-->>SDK: Ciphertext handle + batch signature + SDK->>Blockchain: Send transaction with handle and proof + Blockchain->>Contract: increment(handle, proof) Note over Contract,Blockchain: Step 2: Performing Computations Contract->>Contract: Retrieve encrypted counter @@ -118,7 +118,7 @@ sequenceDiagram Blockchain->>Contract: getCounter(userAddress) Contract-->>SDK: ctHash (encrypted handle) - SDK->>SDK: Use permit with sealing key + SDK->>SDK: Use ACP with sealing key SDK->>CoFHE: decryptForView(ctHash, FheTypes.Uint32) CoFHE->>CoFHE: Re-encrypt with user's sealing key CoFHE-->>SDK: Re-encrypted data @@ -131,7 +131,7 @@ sequenceDiagram 1. **Encryption happens client-side**: The SDK encrypts data with ZK proofs before it reaches the blockchain 2. **Computation happens onchain**: Smart contracts perform operations on encrypted data via `FHE.sol` 3. **FHE enables privacy-preserving computation**: The blockchain never sees plaintext values -4. **Permits enable access control**: EIP-712 signed permits authorize who can decrypt specific data +4. **ACPs enable access control**: EIP-712 signed ACPs authorize who can decrypt specific data 5. **Two decryption paths**: `decryptForView` for UI display, `decryptForTx` for onchain verification This architecture ensures that sensitive data remains private throughout its entire lifecycle, from input to computation to output, while still enabling decentralized applications. diff --git a/client-sdk/introduction/migrating-from-cofhejs.mdx b/client-sdk/introduction/migrating-from-cofhejs.mdx index f79905f..d37c17d 100644 --- a/client-sdk/introduction/migrating-from-cofhejs.mdx +++ b/client-sdk/introduction/migrating-from-cofhejs.mdx @@ -3,11 +3,11 @@ title: Migrating from cofhejs description: "Side-by-side migration guide from cofhejs to @cofhe/sdk" --- -`@cofhe/sdk` is the successor to `cofhejs`, redesigned around an explicit, builder-pattern API that gives you full control over encryption, decryption, and permit management. +`@cofhe/sdk` is the successor to `cofhejs`, redesigned around an explicit, builder-pattern API that gives you full control over encryption, decryption, and ACP management. ## Why migrate? -- **Explicit API**: no more implicit initialization or auto-generated permits. Every action is opt-in. +- **Explicit API**: no more implicit initialization or auto-generated ACPs. Every action is opt-in. - **Builder pattern**: `encryptInputs`, `decryptForView`, and `decryptForTx` use a chainable builder so you can set overrides (account, chain, callbacks) before calling `.execute()`. - **`decryptForTx` feature**: `cofhejs` does not provide an API for generating decryption signatures for onchain usage. - **Deferred key loading**: FHE keys and TFHE WASM are fetched lazily on the first `encryptInputs` call, not during initialization. @@ -119,13 +119,13 @@ await client.connect(publicClient, walletClient); ## 2. Encrypting inputs -`cofhejs.encrypt(...)` is replaced by a builder: `client.encryptInputs([...]).execute()`. +`cofhejs.encrypt(...)` is replaced by a builder: `client.encryptInputs([...]).setConsumingContract(address).execute()`. `setConsumingContract` is required, because the verifier binds that address into the batch signature. | | `cofhejs` | `@cofhe/sdk` | | --- | --- | --- | -| **Function** | `cofhejs.encrypt([...], callback)` | `client.encryptInputs([...]).execute()` | +| **Function** | `cofhejs.encrypt([...], callback)` | `client.encryptInputs([...]).setConsumingContract(addr).execute()` | | **Return value** | `Result` with `.success` / `.data` / `.error` | Direct value (throws `CofheError` on failure) | | **Progress callback** | Second argument to `encrypt` | `.onStep(callback)` on the builder | | **Overrides** | Not available | `.setAccount(...)`, `.setChainId(...)`, `.setUseWorker(...)` | @@ -155,8 +155,9 @@ const [eAmount, eFlag] = result.data; ```typescript import { Encryptable, EncryptStep } from '@cofhe/sdk'; -const [eAmount, eFlag] = await client +const [amountHash, flagHash, signature] = await client .encryptInputs([Encryptable.uint64(42n), Encryptable.bool(true)]) + .setConsumingContract(contractAddress) .onStep((step, ctx) => { if (ctx?.isStart) console.log(`Starting: ${step}`); }) @@ -179,7 +180,7 @@ The `Encryptable` factory functions (`Encryptable.uint32(...)`, `Encryptable.boo | | `cofhejs` | `@cofhe/sdk` | | --- | --- | --- | | **Function** | `cofhejs.unseal(sealed, type)` | `client.decryptForView(ctHash, type)` or `client.decryptForTx(ctHash)` | -| **Permit handling** | Automatic (uses most recent permit) | Explicit. `.withPermit()` / `.withoutPermit()` | +| **ACP handling** | Automatic (uses most recent permit) | Explicit. `.withACP()` / `.withoutACP()` | | **Return value** | `Result` | Direct value for view; `{ ctHash, decryptedValue, signature }` for tx | | **Onchain verification** | Not built in | `decryptForTx` returns a signature for `FHE.publishDecryptResult(...)` | @@ -246,18 +247,18 @@ contract MyContract { -## 4. Permits +## 4. Access Control Permissions -Permits are no longer auto-generated during initialization. All permit operations are now explicit through `client.permits`. +ACPs, which `cofhejs` called permits, are no longer auto-generated during initialization. All ACP operations are now explicit through `client.acp`. | | `cofhejs` | `@cofhe/sdk` | | --- | --- | --- | | **Auto-generation** | `generatePermit: true` (default) | Never, always explicit | -| **Creation** | `cofhejs.createPermit({ type, issuer })` | `client.permits.createSelf(...)`, `client.permits.createSharing(...)` | -| **Return type** | `Result` | Direct `Permit` object | -| **Active permit** | Implicitly used by `unseal` | `getOrCreateSelfPermit()` sets active; used automatically by decrypt methods | +| **Creation** | `cofhejs.createPermit({ type, issuer })` | `client.acp.createSelf(...)`, `client.acp.createSharing(...)` | +| **Return type** | `Result` | Direct `ACP` object | +| **Active ACP** | Implicitly used by `unseal` | `getOrCreateSelfACP()` sets active; used automatically by decrypt methods | @@ -280,16 +281,16 @@ const result = await cofhejs.createPermit({ ### After (@cofhe/sdk) ```typescript -// Create a self permit (prompts for wallet signature) -const permit = await client.permits.createSelf({ +// Create a self ACP (prompts for wallet signature) +const acp = await client.acp.createSelf({ issuer: account, - name: 'My dApp permit', + name: 'My dApp ACP', }); // Or use the convenience method that creates one only if needed -const permit2 = await client.permits.getOrCreateSelfPermit(); +const acp2 = await client.acp.getOrCreateSelfACP(); -// Use with decryptForView (active permit is used automatically) +// Use with decryptForView (the active ACP is used automatically) const value = await client .decryptForView(ctHash, FheTypes.Uint32) .execute(); @@ -318,6 +319,7 @@ import { isCofheError, CofheErrorCode } from '@cofhe/sdk'; try { const encrypted = await client .encryptInputs([Encryptable.uint32(42n)]) + .setConsumingContract(contractAddress) .execute(); } catch (err) { if (isCofheError(err)) { @@ -334,19 +336,16 @@ try { | `cofhejs/node` | `@cofhe/sdk/node` | | `cofhejs/web` | `@cofhe/sdk/web` | | N/A | `@cofhe/sdk` (core types, `Encryptable`, `FheTypes`) | -| N/A | `@cofhe/sdk/permits` | +| N/A | `@cofhe/sdk/acps` | | N/A | `@cofhe/sdk/adapters` | | N/A | `@cofhe/sdk/chains` | ## 7. Type renames +`cofhejs` had one input type per encrypted value. `@cofhe/sdk` `0.7` has none. `encryptInputs` returns plain ciphertext handles, and the signature that authenticates them covers the whole batch. + | `cofhejs` | `@cofhe/sdk` | | --- | --- | -| `CoFheInItem` | `EncryptedItemInput` | -| `CoFheInBool` | `EncryptedBoolInput` | -| `CoFheInUint8` | `EncryptedUint8Input` | -| `CoFheInUint16` | `EncryptedUint16Input` | -| `CoFheInUint32` | `EncryptedUint32Input` | -| `CoFheInUint64` | `EncryptedUint64Input` | -| `CoFheInUint128` | `EncryptedUint128Input` | -| `CoFheInAddress` | `EncryptedAddressInput` | +| `CoFheInItem`, `CoFheInBool`, `CoFheInUint8` through `CoFheInAddress` | A ciphertext handle, typed `` `0x${string}` `` | + +The `EncryptedItemInput` family that `0.5` and `0.6` used as the replacement was itself removed in `0.7`, along with `asHashPlusProof()`. diff --git a/client-sdk/introduction/overview.mdx b/client-sdk/introduction/overview.mdx index e943f45..95d2f12 100644 --- a/client-sdk/introduction/overview.mdx +++ b/client-sdk/introduction/overview.mdx @@ -3,7 +3,7 @@ title: Overview description: "Introduction to @cofhe/sdk - the TypeScript client SDK for building FHE-enabled applications on Fhenix" --- -`@cofhe/sdk` is the TypeScript client SDK for [CoFHE](/deep-dive/cofhe-components/overview). It handles the client-side operations required to interact with FHE-enabled smart contracts: encrypting inputs with ZK proofs, decrypting ciphertext handles via the Threshold Network, and managing EIP-712 permits for access control. +`@cofhe/sdk` is the TypeScript client SDK for [CoFHE](/deep-dive/cofhe-components/overview). It handles the client-side work of interacting with FHE-enabled smart contracts: encrypting inputs with ZK proofs, and decrypting ciphertext handles via the Threshold Network. It also manages the EIP-712 Access Control Permissions (ACPs) that authorize those decryptions. Onchain, contracts use [`FHE.sol`](/fhe-library/introduction/overview) to operate on encrypted data. Offchain, this SDK prepares the inputs and reads the outputs. @@ -12,19 +12,19 @@ Onchain, contracts use [`FHE.sol`](/fhe-library/introduction/overview) to operat - Packs plaintext values, generates a ZKPoK, and submits them to the CoFHE verifier. Returns signed EncryptedItemInput objects for use in contract calls. + Packs plaintext values, generates a ZKPoK, and submits them to the CoFHE verifier's batch endpoint. Returns one ciphertext handle per input plus a single shared signature, for use in contract calls. - Requests decryption of a ciphertext handle via the Threshold Network using a permit. Returns the plaintext locally, not published onchain. + Requests decryption of a ciphertext handle via the Threshold Network using an ACP. Returns the plaintext locally, not published onchain. Requests decryption and returns the plaintext with a Threshold Network signature for onchain verification. - - Creates, stores, and manages EIP-712 permits that authorize decryption of specific ciphertext handles. + + Creates, stores, and manages EIP-712 ACPs that authorize decryption of specific ciphertext handles. @@ -36,7 +36,7 @@ Onchain, contracts use [`FHE.sol`](/fhe-library/introduction/overview) to operat | `@cofhe/sdk` | Core types (`Encryptable`, `FheTypes`, `EncryptStep`, `CofheError`), shared across runtimes | | `@cofhe/sdk/web` | `createCofheConfig` / `createCofheClient` with browser defaults (IndexedDB storage, TFHE WASM, Web Workers) | | `@cofhe/sdk/node` | `createCofheConfig` / `createCofheClient` with Node.js defaults (filesystem storage, `node-tfhe`) | -| `@cofhe/sdk/permits` | Permit creation, validation, serialization, and storage utilities | +| `@cofhe/sdk/acps` | ACP creation, validation, serialization, and storage utilities | | `@cofhe/sdk/adapters` | `Ethers5Adapter`, `Ethers6Adapter`, `WagmiAdapter`, `HardhatSignerAdapter` | | `@cofhe/sdk/chains` | Built-in chain definitions and `getChainById` / `getChainByName` helpers | @@ -75,14 +75,15 @@ const config = createCofheConfig({ const client = createCofheClient(config); await client.connect(publicClient, walletClient); -// Encrypt and send -const [encrypted] = await client +// Encrypt and send: one handle per input, then the batch signature +const [hash, signature] = await client .encryptInputs([Encryptable.uint64(100n)]) + .setConsumingContract(contract.address) .execute(); -await contract.deposit(encrypted); +await contract.deposit(hash, signature); // Decrypt for UI -await client.permits.getOrCreateSelfPermit(); +await client.acp.getOrCreateSelfACP(); const ctHash = await contract.getBalance(); const balance = await client .decryptForView(ctHash, FheTypes.Uint64) @@ -101,12 +102,12 @@ const balance = await client Encrypt plaintext values with ZK proofs before passing them to your smart contract. - - Create and manage EIP-712 permits that authorize decryption of confidential data. + + Create and manage the EIP-712 ACPs that authorize decryption of confidential data. - Reveal encrypted values locally for UI display using permits. + Reveal encrypted values locally for UI display using ACPs. diff --git a/client-sdk/quick-start/foundry.mdx b/client-sdk/quick-start/foundry.mdx index da31615..9761291 100644 --- a/client-sdk/quick-start/foundry.mdx +++ b/client-sdk/quick-start/foundry.mdx @@ -21,17 +21,17 @@ The plugin and its CoFHE dependencies are distributed via npm. Install them as d ```bash npm -npm install -D @cofhe/foundry-plugin@^0.5.2 @cofhe/mock-contracts@^0.5.2 @fhenixprotocol/cofhe-contracts@^0.1.3 @openzeppelin/contracts +npm install -D @cofhe/foundry-plugin@^0.7.1 @cofhe/mock-contracts@^0.7.1 @fhenixprotocol/cofhe-contracts@^0.2.0 @openzeppelin/contracts forge install foundry-rs/forge-std ``` ```bash pnpm -pnpm add -D @cofhe/foundry-plugin@^0.5.2 @cofhe/mock-contracts@^0.5.2 @fhenixprotocol/cofhe-contracts@^0.1.3 @openzeppelin/contracts +pnpm add -D @cofhe/foundry-plugin@^0.7.1 @cofhe/mock-contracts@^0.7.1 @fhenixprotocol/cofhe-contracts@^0.2.0 @openzeppelin/contracts forge install foundry-rs/forge-std ``` ```bash yarn -yarn add -D @cofhe/foundry-plugin@^0.5.2 @cofhe/mock-contracts@^0.5.2 @fhenixprotocol/cofhe-contracts@^0.1.3 @openzeppelin/contracts +yarn add -D @cofhe/foundry-plugin@^0.7.1 @cofhe/mock-contracts@^0.7.1 @fhenixprotocol/cofhe-contracts@^0.2.0 @openzeppelin/contracts forge install foundry-rs/forge-std ``` @@ -56,7 +56,7 @@ code_size_limit = 100000 -`evm_version = "cancun"` is no longer required as of `@cofhe/mock-contracts@0.5.0`. `MockACL` was migrated off transient storage to block-number-based storage. Set it only if your own contracts need cancun-specific opcodes. +`evm_version = "cancun"` is no longer required as of `@cofhe/mock-contracts@0.7.1`. `MockACL` was migrated off transient storage to block-number-based storage. Set it only if your own contracts need cancun-specific opcodes. ## 3. Configure `remappings.txt` @@ -83,8 +83,8 @@ import "@fhenixprotocol/cofhe-contracts/FHE.sol"; contract MyContract { euint32 public storedValue; - function setValue(InEuint32 memory inValue) external { - storedValue = FHE.asEuint32(inValue); + function setValue(externalEuint32 inValue, bytes calldata inputProof) external { + storedValue = FHE.asEuint32(inValue, inputProof); FHE.allowThis(storedValue); FHE.allowSender(storedValue); } @@ -92,7 +92,7 @@ contract MyContract { ``` - `euint32`: an encrypted `uint32` stored onchain as a ciphertext handle. -- `InEuint32`: the encrypted input struct produced by `CofheClient`. +- `externalEuint32`: the encrypted input handle produced by `CofheClient`, paired with a `bytes` proof. - `FHE.allowThis` / `FHE.allowSender`: grant the contract and caller permission to read the encrypted value (required by the ACL). ## 5. Write a test @@ -103,7 +103,7 @@ pragma solidity ^0.8.25; import { CofheTest } from "@cofhe/foundry-plugin/contracts/CofheTest.sol"; import { CofheClient } from "@cofhe/foundry-plugin/contracts/CofheClient.sol"; -import { InEuint32 } from "@fhenixprotocol/cofhe-contracts/FHE.sol"; +import { externalEuint32 } from "@fhenixprotocol/cofhe-contracts/FHE.sol"; import { MyContract } from "../src/MyContract.sol"; contract MyContractTest is CofheTest { @@ -123,14 +123,14 @@ contract MyContractTest is CofheTest { } function test_StoresAndDecryptsAnEncryptedValue() public { - // 1. Encrypt the input - InEuint32 memory encrypted = bob.createInEuint32(42); + // 1. Encrypt the input, bound to the contract that will consume it + (externalEuint32 hash, bytes memory proof) = bob.createExternalEuint32(42, address(myContract)); // 2. Send to contract vm.prank(bob.account()); - myContract.setValue(encrypted); + myContract.setValue(hash, proof); - // 3. Assert the stored plaintext directly (mock-only, no permit needed) + // 3. Assert the stored plaintext directly (mock-only, no ACP needed) expectPlaintext(myContract.storedValue(), uint32(42)); } } @@ -152,13 +152,13 @@ Test result: ok. 1 passed; 0 failed; 0 skipped; finished in … 1. **`deployMocks()`** deployed the full CoFHE coprocessor mock stack (TaskManager, ACL, ZK verifier, threshold network) to the in-process EVM. 2. **`createCofheClient()` + `bob.connect(BOB_PKEY)`** spun up an in-Solidity SDK shim bound to `vm.addr(BOB_PKEY)`. -3. **`bob.createInEuint32(42)`** produced a signed `InEuint32`, the same shape your contract receives on testnet, signed by `MockZkVerifierSigner`. +3. **`bob.createExternalEuint32(42, address(myContract))`** produced a signed `externalEuint32` and its proof, the same shape your contract receives on testnet, signed by `MockZkVerifierSigner`. 4. **`vm.prank(bob.account())` + `setValue(...)`** called the contract as Bob. The contract stored the ciphertext handle and granted ACL access to itself and Bob. -5. **`expectPlaintext(myContract.storedValue(), 42)`** read the onchain plaintext directly from `MockTaskManager.mockStorage`, no permit, no SDK round-trip. +5. **`expectPlaintext(myContract.storedValue(), 42)`** read the onchain plaintext directly from `MockTaskManager.mockStorage`, no ACP, no SDK round-trip. ## Next steps - [Foundry Plugin to Getting Started](/client-sdk/foundry-plugin/getting-started): full plugin configuration and features. - [CofheTest](/client-sdk/foundry-plugin/cofhe-test): `deployMocks`, `expectPlaintext`, log toggles. -- [CofheClient](/client-sdk/foundry-plugin/cofhe-client): encrypt inputs, decrypt for view / tx, permits. +- [CofheClient](/client-sdk/foundry-plugin/cofhe-client): encrypt inputs, decrypt for view or tx, and ACPs. - [Testing](/client-sdk/foundry-plugin/testing): canonical test patterns for ACL, public-decrypt, and fuzzing. diff --git a/client-sdk/quick-start/hardhat.mdx b/client-sdk/quick-start/hardhat.mdx index 9b6a070..7deeea0 100644 --- a/client-sdk/quick-start/hardhat.mdx +++ b/client-sdk/quick-start/hardhat.mdx @@ -20,15 +20,15 @@ Want to skip the setup? Clone the [cofhe-hardhat-starter](https://github.com/Fhe ```bash npm -npm install @cofhe/hardhat-plugin@^0.5.2 @cofhe/sdk@^0.5.2 @fhenixprotocol/cofhe-contracts@^0.1.3 +npm install @cofhe/hardhat-plugin@^0.7.1 @cofhe/sdk@^0.7.1 @fhenixprotocol/cofhe-contracts@^0.2.0 ``` ```bash pnpm -pnpm add @cofhe/hardhat-plugin@^0.5.2 @cofhe/sdk@^0.5.2 @fhenixprotocol/cofhe-contracts@^0.1.3 +pnpm add @cofhe/hardhat-plugin@^0.7.1 @cofhe/sdk@^0.7.1 @fhenixprotocol/cofhe-contracts@^0.2.0 ``` ```bash yarn -yarn add @cofhe/hardhat-plugin@^0.5.2 @cofhe/sdk@^0.5.2 @fhenixprotocol/cofhe-contracts@^0.1.3 +yarn add @cofhe/hardhat-plugin@^0.7.1 @cofhe/sdk@^0.7.1 @fhenixprotocol/cofhe-contracts@^0.2.0 ``` @@ -71,8 +71,8 @@ import '@fhenixprotocol/cofhe-contracts/FHE.sol'; contract MyContract { euint32 public storedValue; - function setValue(InEuint32 memory inValue) external { - storedValue = FHE.asEuint32(inValue); + function setValue(externalEuint32 inValue, bytes calldata inputProof) external { + storedValue = FHE.asEuint32(inValue, inputProof); FHE.allowThis(storedValue); FHE.allowSender(storedValue); } @@ -80,12 +80,12 @@ contract MyContract { ``` - `euint32`: an encrypted `uint32` stored onchain as a ciphertext handle. -- `InEuint32`: the encrypted input struct produced by the SDK. +- `externalEuint32`: the encrypted input handle produced by the SDK, paired with a `bytes` proof. - `FHE.allowThis` / `FHE.allowSender`: grant the contract and caller permission to read the encrypted value (required by the ACL). ## 4. Write a test -Use `hre.cofhe.createClientWithBatteries` to get a fully configured SDK client with a self-permit, then encrypt to send to decrypt. +Use `hre.cofhe.createClientWithBatteries` to get a fully configured SDK client with a self-ACP, then encrypt to send to decrypt. ```typescript test/MyContract.test.ts import hre from 'hardhat'; @@ -106,13 +106,14 @@ describe('MyContract', () => { const Factory = await hre.ethers.getContractFactory('MyContract'); const contract = await Factory.deploy(); - // 1. Encrypt the input - const [encrypted] = await cofheClient + // 1. Encrypt the input, bound to the contract that will consume it + const [valueHash, signature] = await cofheClient .encryptInputs([Encryptable.uint32(42n)]) + .setConsumingContract(await contract.getAddress()) .execute(); // 2. Send to contract - await (await contract.setValue(encrypted)).wait(); + await (await contract.setValue(valueHash, signature)).wait(); // 3. Read the stored handle and decrypt const ctHash = await contract.storedValue(); @@ -143,10 +144,10 @@ The plugin deploys mock contracts automatically, no extra setup needed. ## What just happened? 1. The **Hardhat plugin** deployed mock versions of the CoFHE coprocessor contracts (TaskManager, ACL, ZK verifier, threshold network) before the test ran. -2. `createClientWithBatteries` created an SDK client connected to the Hardhat network, with a self-permit ready to go. +2. `createClientWithBatteries` created an SDK client connected to the Hardhat network, with a self-ACP ready to go. 3. `encryptInputs` encrypted the plaintext `42` into an FHE ciphertext with a ZK proof (simulated by the mock verifier). 4. The contract stored the ciphertext handle onchain and set ACL permissions. -5. `decryptForView` used the permit to decrypt the handle back to `42n` locally. +5. `decryptForView` used the ACP to decrypt the handle back to `42n` locally. ## Next steps diff --git a/client-sdk/quick-start/javascript.mdx b/client-sdk/quick-start/javascript.mdx index ec5b828..fcd3b1d 100644 --- a/client-sdk/quick-start/javascript.mdx +++ b/client-sdk/quick-start/javascript.mdx @@ -16,15 +16,15 @@ Connect to an FHE-enabled contract, encrypt a value, send it onchain, and decryp ```bash npm -npm install @cofhe/sdk@^0.5.2 viem +npm install @cofhe/sdk@^0.7.1 viem ``` ```bash pnpm -pnpm add @cofhe/sdk@^0.5.2 viem +pnpm add @cofhe/sdk@^0.7.1 viem ``` ```bash yarn -yarn add @cofhe/sdk@^0.5.2 viem +yarn add @cofhe/sdk@^0.7.1 viem ``` @@ -101,13 +101,14 @@ If you use Ethers.js instead of viem, see the [Client Setup](/client-sdk/guides/ ```typescript import { Encryptable } from '@cofhe/sdk'; -// Encrypt a uint32 value -const [encrypted] = await client +// Encrypt a uint32 value, naming the contract that will consume it +const [valueHash, signature] = await client .encryptInputs([Encryptable.uint32(42n)]) + .setConsumingContract(contract.address) .execute(); -// Pass it to your contract -await contract.setValue(encrypted); +// Pass the handle and its proof to your contract +await contract.setValue(valueHash, signature); ``` ## 4. Decrypt for display @@ -115,8 +116,8 @@ await contract.setValue(encrypted); ```typescript import { FheTypes } from '@cofhe/sdk'; -// Create a permit (one-time per account + chain) -await client.permits.getOrCreateSelfPermit(); +// Create an ACP (one-time per account + chain) +await client.acp.getOrCreateSelfACP(); // Read the encrypted handle from your contract const ctHash = await contract.storedValue(); @@ -133,6 +134,6 @@ console.log(plaintext); // 42n - [Client Setup](/client-sdk/guides/client-setup): adapters, connection management, and config options. - [Encrypting Inputs](/client-sdk/guides/encrypting-inputs): supported types, builder API, and progress callbacks. -- [Permits](/client-sdk/guides/permits): create, share, and manage decryption authorization. +- [Access Control Permissions](/client-sdk/guides/acps): create, share, and manage decryption authorization. - [Decrypt to View](/client-sdk/guides/decrypt-to-view): reveal encrypted state in your UI. - [Decrypt to Transact](/client-sdk/guides/decrypt-to-tx): decrypt with a verifiable signature for onchain use. diff --git a/client-sdk/reference/foundry-reference.mdx b/client-sdk/reference/foundry-reference.mdx index 315942e..1adac13 100644 --- a/client-sdk/reference/foundry-reference.mdx +++ b/client-sdk/reference/foundry-reference.mdx @@ -63,20 +63,20 @@ import { CofheClient } from "@cofhe/foundry-plugin/contracts/CofheClient.sol"; | Function | Description | | --- | --- | -| `connect(uint256 pkey)` | Set the connected account to `vm.addr(pkey)`. Required before any `createIn*` or `permit_*` call. | +| `connect(uint256 pkey)` | Set the connected account to `vm.addr(pkey)`. Required before any `createExternal*` or `ACP_*` call. | | `account()` | Returns the connected `address`. | ### Encrypt inputs | Function | Returns | | --- | --- | -| `createInEbool(bool)` | `InEbool` | -| `createInEuint8(uint8)` | `InEuint8` | -| `createInEuint16(uint16)` | `InEuint16` | -| `createInEuint32(uint32)` | `InEuint32` | -| `createInEuint64(uint64)` | `InEuint64` | -| `createInEuint128(uint128)` | `InEuint128` | -| `createInEaddress(address)` | `InEaddress` | +| `createExternalEbool(bool, address)` | `(externalEbool, bytes)` | +| `createExternalEuint8(uint8, address)` | `(externalEuint8, bytes)` | +| `createExternalEuint16(uint16, address)` | `(externalEuint16, bytes)` | +| `createExternalEuint32(uint32, address)` | `(externalEuint32, bytes)` | +| `createExternalEuint64(uint64, address)` | `(externalEuint64, bytes)` | +| `createExternalEuint128(uint128, address)` | `(externalEuint128, bytes)` | +| `createExternalEaddress(address, address)` | `(externalEaddress, bytes)` | All produce signed `EncryptedInput` shapes signed for `account()`. @@ -84,18 +84,18 @@ All produce signed `EncryptedInput` shapes signed for `account()`. | Function | Returns | Notes | | --- | --- | --- | -| `decryptForTx_withoutPermit(bytes32 ctHash)` | `(bytes32, uint256, bytes)` | `(ctHash, plaintext, signature)`. Signature consumable by `FHE.publishDecryptResult`. Requires `FHE.allowPublic(handle)` to have been called. | -| `decryptForTx_withPermit(bytes32 ctHash, Permission permit)` | `(bytes32, uint256, bytes)` | ACL-gated `decryptForTx`. | -| `decryptForView(bytes32 ctHash, Permission permit)` | `uint256` | Offchain seal/unseal. **Reverts on deny**, to assert deny use `mockThresholdNetwork.querySealOutput(...)`. | +| `decryptForTx_withoutACP(bytes32 ctHash)` | `(bytes32, uint256, bytes)` | `(ctHash, plaintext, signature)`. Signature consumable by `FHE.publishDecryptResult`. Requires `FHE.allowPublic(handle)` to have been called. | +| `decryptForTx_withACP(bytes32 ctHash, ACP acp)` | `(bytes32, uint256, bytes)` | ACL-gated `decryptForTx`. | +| `decryptForView(bytes32 ctHash, ACP acp)` | `uint256` | Offchain seal/unseal. **Reverts on deny**, to assert deny use `mockThresholdNetwork.querySealOutput(...)`. | ### Permits | Function | Description | | --- | --- | -| `permit_createSelf()` | Self-permit for the connected account; sealing key auto-derived. | -| `permit_createShared(address recipient)` | Issuer-side shared permit. | -| `permit_exportShared(Permission perm)` | Strip sensitive fields to `SharedPermitExport`. | -| `permit_importShared(SharedPermitExport export)` | Recipient-side completion. Reverts unless `export.recipient == account()`. | +| `ACP_createSelf()` | Self-ACP for the connected account; sealing key auto-derived. | +| `ACP_createShared(address recipient)` | Issuer-side shared ACP. | +| `ACP_exportShared(ACP acp)` | Strip sensitive fields to `SharedACPExport`. | +| `ACP_importShared(SharedACPExport export)` | Recipient-side completion. Reverts unless `export.recipient == account()`. | | `createSealingKey(bytes32 seed)` | Custom sealing key (rarely needed). | ## Mock storage layout @@ -113,7 +113,7 @@ libs = ["node_modules"] ``` -`evm_version = "cancun"` is no longer required as of `@cofhe/mock-contracts@0.5.0`. `MockACL` was migrated off transient storage to block-number-based storage. Set it only if your own contracts need cancun-specific opcodes. +`evm_version = "cancun"` is no longer required as of `@cofhe/mock-contracts@0.7.1`. `MockACL` was migrated off transient storage to block-number-based storage. Set it only if your own contracts need cancun-specific opcodes. ## `remappings.txt` (canonical shape) @@ -136,8 +136,8 @@ hardhat/=node_modules/forge-std/src/ | Package | Version | | --- | --- | -| `@cofhe/foundry-plugin` | `0.5.2` | -| `@cofhe/mock-contracts` | `0.5.2` | -| `@fhenixprotocol/cofhe-contracts` | `0.1.3` | +| `@cofhe/foundry-plugin` | `0.7.1` | +| `@cofhe/mock-contracts` | `0.7.1` | +| `@fhenixprotocol/cofhe-contracts` | `0.2.0` | See the [Compatibility](/get-started/introduction/compatibility) page for the canonical table. diff --git a/client-sdk/reference/hardhat-reference.mdx b/client-sdk/reference/hardhat-reference.mdx index 9b9d18a..bdb2309 100644 --- a/client-sdk/reference/hardhat-reference.mdx +++ b/client-sdk/reference/hardhat-reference.mdx @@ -15,7 +15,7 @@ The plugin extends the Hardhat Runtime Environment with a `cofhe` namespace. | Method | Description | | --- | --- | -| `hre.cofhe.createClientWithBatteries(signer?)` | Creates a fully configured `CofheClient` with a self-permit. Uses the first signer if none provided. | +| `hre.cofhe.createClientWithBatteries(signer?)` | Creates a fully configured `CofheClient` with a self-ACP. Uses the first signer if none provided. | | `hre.cofhe.createConfig(options)` | Wraps `createCofheConfig` with Hardhat defaults (`environment: 'hardhat'`, `encryptDelay: 0`). | | `hre.cofhe.createClient(config)` | Creates a `CofheClient` from a config object. | | `hre.cofhe.connectWithHardhatSigner(client, signer)` | Connects a client using a `HardhatEthersSigner`. | @@ -62,6 +62,6 @@ export default { | Network | URL | Chain ID | | --- | --- | --- | -| `localcofhe` | `http://127.0.0.1:42069` | — | +| `localcofhe` | `http://127.0.0.1:42069` | `420105` | | `eth-sepolia` | Ethereum Sepolia public RPC | `11155111` | | `arb-sepolia` | Arbitrum Sepolia public RPC | `421614` | diff --git a/client-sdk/reference/sdk-reference.mdx b/client-sdk/reference/sdk-reference.mdx index a69f679..aada080 100644 --- a/client-sdk/reference/sdk-reference.mdx +++ b/client-sdk/reference/sdk-reference.mdx @@ -11,10 +11,10 @@ This page is under construction. A full API reference documenting all functions, | Entrypoint | Contents | | --- | --- | -| `@cofhe/sdk` | Core types: `Encryptable`, `FheTypes`, `EncryptStep`, `CofheError`, `CofheErrorCode`, `isCofheError`, `assertCorrectEncryptedItemInput` | +| `@cofhe/sdk` | Core types: `Encryptable`, `FheTypes`, `EncryptStep`, `CofheError`, `CofheErrorCode`, `isCofheError` | | `@cofhe/sdk/web` | `createCofheConfig`, `createCofheClient` (browser defaults) | | `@cofhe/sdk/node` | `createCofheConfig`, `createCofheClient` (Node.js defaults) | -| `@cofhe/sdk/permits` | `PermitUtils`, `setPermit`, `setActivePermitHash`, `getPermit`, `getActivePermitHash` | +| `@cofhe/sdk/acps` | `ACPUtils`, `setACP`, `setActiveACPHash`, `getACP`, `getActiveACPHash` | | `@cofhe/sdk/adapters` | `Ethers5Adapter`, `Ethers6Adapter`, `WagmiAdapter`, `HardhatSignerAdapter` | | `@cofhe/sdk/chains` | `chains`, `getChainById`, `getChainByName`, `hardhat` | @@ -26,13 +26,13 @@ Factory for creating encryptable input items. | Method | Input type | Solidity param | | --- | --- | --- | -| `Encryptable.bool(value)` | `boolean` | `InEbool` | -| `Encryptable.uint8(value)` | `bigint \| string` | `InEuint8` | -| `Encryptable.uint16(value)` | `bigint \| string` | `InEuint16` | -| `Encryptable.uint32(value)` | `bigint \| string` | `InEuint32` | -| `Encryptable.uint64(value)` | `bigint \| string` | `InEuint64` | -| `Encryptable.uint128(value)` | `bigint \| string` | `InEuint128` | -| `Encryptable.address(value)` | `bigint \| string` | `InEaddress` | +| `Encryptable.bool(value)` | `boolean` | `externalEbool` | +| `Encryptable.uint8(value)` | `bigint \| string` | `externalEuint8` | +| `Encryptable.uint16(value)` | `bigint \| string` | `externalEuint16` | +| `Encryptable.uint32(value)` | `bigint \| string` | `externalEuint32` | +| `Encryptable.uint64(value)` | `bigint \| string` | `externalEuint64` | +| `Encryptable.uint128(value)` | `bigint \| string` | `externalEuint128` | +| `Encryptable.address(value)` | `bigint \| string` | `externalEaddress` | | `Encryptable.create(type, value)` | varies | varies | ### `FheTypes` @@ -49,17 +49,17 @@ Enum of supported FHE types used with `decryptForView`. | `FheTypes.Uint128` | `bigint` | | `FheTypes.Uint160` | `string` (checksummed address) | -### `EncryptedItemInput` +### The `encryptInputs` result + +`execute()` returns one ciphertext handle per input, in order, followed by a single signature covering the whole batch: ```typescript -type EncryptedItemInput = { - ctHash: bigint; - securityZone: number; - utype: FheTypes; - signature: string; -}; +type EncryptInputsResult = readonly `0x${string}`[]; +// [ ...hashes, signature ] -> inputs.length + 1 elements ``` +The per-item `EncryptedItemInput` struct and its typed variants were removed in `0.7`, along with `asHashPlusProof()`. + ### `EncryptStep` Enum values fired during the encryption pipeline: diff --git a/fhe-library/core-concepts/access-control.mdx b/fhe-library/core-concepts/access-control.mdx index b96f6b1..9861eda 100644 --- a/fhe-library/core-concepts/access-control.mdx +++ b/fhe-library/core-concepts/access-control.mdx @@ -9,8 +9,8 @@ Consider the following scenario: Your contract receives an encrypted input that ```solidity // Contract A -function submitSecretBid(InEuint32 bid) public { - euint32 handle = FHE.asEuint32(bid); +function submitSecretBid(externalEuint32 bid, bytes calldata inputProof) public { + euint32 handle = FHE.asEuint32(bid, inputProof); // Perform some operations on the encrypted input } ``` @@ -54,7 +54,7 @@ FHE.publishDecryptResult(seenHandle, plaintext, signature); `publishDecryptResult` (and `verifyDecryptResult`) only check that `signature` is a valid ECDSA signature from the Threshold Network over that exact `(ctHash, result, chainId, encType)` tuple. Publication is deliberately permissionless so clients and relayers can settle results onchain. -What protects the value is that an attacker cannot *obtain* that signature. The signature is only issued by the Threshold Network in response to a decryption request, and that request is ACL-gated: the requester must either hold permission on the handle (with a valid permit) or the handle must have been marked with `FHE.allowPublic()`. Without permission, the request is denied and no signature is ever produced, so there is nothing valid to publish. +What protects the value is that an attacker cannot *obtain* that signature. The signature is only issued by the Threshold Network in response to a decryption request, and that request is ACL-gated: the requester must either hold permission on the handle (with a valid ACP) or the handle must have been marked with `FHE.allowPublic()`. Without permission, the request is denied and no signature is ever produced, so there is nothing valid to publish. Treat `FHE.allowPublic()` as the real decision point. Once a handle is marked public, anyone can request its decryption and publish the plaintext onchain, permanently. @@ -137,8 +137,8 @@ Decryption is a multi-step process: a client requests the plaintext and a thresh Access control governs who can request decryption: -- If the ciphertext was marked with `FHE.allowPublic()`, anyone can request decryption without a permit (`.withoutPermit()`). -- Otherwise, only addresses with explicit permission on the handle can request decryption, and must provide a valid permit (`.withPermit()`). +- If the ciphertext was marked with `FHE.allowPublic()`, anyone can request decryption without an ACP (`.withoutACP()`). +- Otherwise, only addresses with explicit permission on the handle can request decryption, and must provide a valid ACP (`.withACP()`). If the requester does not have permission on the ciphertext handle, the decryption request will be denied by the access control system. Grant appropriate permissions before attempting to decrypt, use `FHE.allowPublic()` for values intended to become public, or `FHE.allow()` / `FHE.allowSender()` for restricted access. @@ -191,8 +191,8 @@ For detailed examples on how to explicitly manage ciphertext allowances in contr ### Quick Example: Token Transfer ```solidity -function transfer(address to, InEuint32 memory inAmount) public { - euint32 amount = FHE.asEuint32(inAmount); +function transfer(address to, externalEuint32 inAmount, bytes calldata inputProof) public { + euint32 amount = FHE.asEuint32(inAmount, inputProof); euint32 fromBalance = _balances[msg.sender]; euint32 toBalance = _balances[to]; diff --git a/fhe-library/core-concepts/conditions.mdx b/fhe-library/core-concepts/conditions.mdx index 5530506..bc8a231 100644 --- a/fhe-library/core-concepts/conditions.mdx +++ b/fhe-library/core-concepts/conditions.mdx @@ -205,8 +205,8 @@ contract EncryptedAuction { euint32 public highestBid; address public highestBidder; - function placeBid(InEuint32 memory encryptedBid) public { - euint32 bid = FHE.asEuint32(encryptedBid); + function placeBid(externalEuint32 encryptedBid, bytes calldata inputProof) public { + euint32 bid = FHE.asEuint32(encryptedBid, inputProof); // Compare new bid with current highest (encrypted comparison) ebool isHigher = bid.gt(highestBid); diff --git a/fhe-library/core-concepts/decryption-operations.mdx b/fhe-library/core-concepts/decryption-operations.mdx index 25407d0..e6dd926 100644 --- a/fhe-library/core-concepts/decryption-operations.mdx +++ b/fhe-library/core-concepts/decryption-operations.mdx @@ -86,24 +86,24 @@ See [Access Control](/fhe-library/core-concepts/access-control) for the full lis ### Step 2: Request decryption offchain (client-side) -The client calls `decryptForTx(ctHash)` to obtain the plaintext and a Threshold Network signature. Choose the permit mode that matches the contract's ACL policy: +The client calls `decryptForTx(ctHash)` to obtain the plaintext and a Threshold Network signature. Choose the ACP mode that matches the contract's ACL policy: -```typescript No permit (allowPublic) +```typescript No ACP (allowPublic) const decryptResult = await client .decryptForTx(ctHash) - .withoutPermit() + .withoutACP() .execute(); -// decryptResult.ctHash — the ciphertext handle -// decryptResult.decryptedValue — the plaintext (bigint) -// decryptResult.signature — the Threshold Network signature +// decryptResult.ctHash is the ciphertext handle +// decryptResult.decryptedValue is the plaintext (bigint) +// decryptResult.signature is the Threshold Network signature ``` -```typescript With permit (restricted access) +```typescript With ACP (restricted access) const decryptResult = await client .decryptForTx(ctHash) - .withPermit() + .withACP() .execute(); ``` @@ -187,10 +187,10 @@ contract EncryptedAuction { } // Place an encrypted bid - function placeBid(InEuint64 memory encryptedBid) external { + function placeBid(externalEuint64 encryptedBid, bytes calldata inputProof) external { require(!auctionClosed, "Auction is closed"); - euint64 bid = FHE.asEuint64(encryptedBid); + euint64 bid = FHE.asEuint64(encryptedBid, inputProof); ebool isHigher = bid.gt(highestBid); // Update highest bid if this bid is higher @@ -231,10 +231,10 @@ The client-side flow to reveal the winner: // 1. Read the encrypted highest bid from the contract const ctHash = await auctionContract.highestBid(); -// 2. Request decryption off-chain (no permit needed since allowPublic was used) +// 2. Request decryption offchain (no ACP needed since allowPublic was used) const decryptResult = await client .decryptForTx(ctHash) - .withoutPermit() + .withoutACP() .execute(); // 3. Submit the result on-chain @@ -279,7 +279,7 @@ Both functions accept type-specific overloads for `ebool`, `euint8`, `euint16`, -When a value is intended to become public (e.g. unshielding, auction reveals), use `FHE.allowPublic()` so anyone can trigger the decryption without needing a permit. +When a value is intended to become public (e.g. unshielding, auction reveals), use `FHE.allowPublic()` so anyone can trigger the decryption without needing an ACP. @@ -300,8 +300,8 @@ If you only need to display a value in your UI and don't need an onchain-verifia ## Common Pitfalls - **Missing ACL permissions**: If no `allow*` was called for the ciphertext handle, decryption requests will be denied. Make sure to grant permissions before the client requests decryption. -- **Permit mode must be selected**: When using `decryptForTx`, you must call exactly one of `.withPermit(...)` or `.withoutPermit()` before `.execute()`. -- **Wrong chain/account**: Permits are scoped to `chainId + account`. If you get an ACL/permit error, double-check you're connected to the expected chain and account. +- **ACP mode must be selected**: When using `decryptForTx`, you must call exactly one of `.withACP(...)` or `.withoutACP()` before `.execute()`. +- **Wrong chain/account**: ACPs are scoped to `chainId + account`. If you get an ACL or ACP error, double-check you're connected to the expected chain and account. - **Type mismatch**: `decryptedValue` is always a `bigint`. If your Solidity function expects a smaller integer type (e.g. `uint32`), make sure the value is within range. --- diff --git a/fhe-library/core-concepts/encrypted-operations.mdx b/fhe-library/core-concepts/encrypted-operations.mdx index 100082c..9e47141 100644 --- a/fhe-library/core-concepts/encrypted-operations.mdx +++ b/fhe-library/core-concepts/encrypted-operations.mdx @@ -5,7 +5,7 @@ description: "Complete guide to FHE types and operations for confidential smart ## Overview -The library exposes utility functions for FHE operations. The goal of the library is to provide a seamless developer experience for writing smart contracts that can operate on confidential data. +The library exposes utility functions for FHE operations. The goal of the library is to let you write smart contracts that operate on confidential data. ## Types @@ -38,17 +38,33 @@ In the back-end, encrypted integers are FHE ciphertexts. The library abstracts a | Name | Bit Size | Usage | |--------------|----------|-------| -| `InEuint8` | 8 | Input | -| `InEuint16` | 16 | Input | -| `InEuint32` | 32 | Input | -| `InEuint64` | 64 | Input | -| `InEuint128` | 128 | Input | -| `InEbool` | 8 | Input | -| `InEaddress` | 160 | Input | +| `externalEuint8` | 8 | Input | +| `externalEuint16` | 16 | Input | +| `externalEuint32` | 32 | Input | +| `externalEuint64` | 64 | Input | +| `externalEuint128` | 128 | Input | +| `externalEbool` | 8 | Input | +| `externalEaddress` | 160 | Input | + + + + + +| Name | Bit Size | Usage | +|--------------------|----------|----------------------| +| `sharedEuint8` | 8 | Contract to contract | +| `sharedEuint16` | 16 | Contract to contract | +| `sharedEuint32` | 32 | Contract to contract | +| `sharedEuint64` | 64 | Contract to contract | +| `sharedEuint128` | 128 | Contract to contract | +| `sharedEbool` | 8 | Contract to contract | +| `sharedEaddress` | 160 | Contract to contract | +Each family records where a value came from. A **compute** type is yours, held in storage or produced by an operation. An **input** type arrived from a user offchain and travels with a `bytes` proof that authenticates it. A **shared** type was handed over by another contract during this transaction. See [inputs](/fhe-library/core-concepts/inputs) for how to convert between them. + The `ebool` type is not a real boolean type. It is implemented as a `euint8` for compatibility with FHE operations. diff --git a/fhe-library/core-concepts/inputs.mdx b/fhe-library/core-concepts/inputs.mdx index 17a6450..6c4fb5a 100644 --- a/fhe-library/core-concepts/inputs.mdx +++ b/fhe-library/core-concepts/inputs.mdx @@ -10,28 +10,31 @@ One of the key aspects of writing confidential smart contracts is receiving encr ```solidity function transfer( address to, - InEuint32 memory inAmount // <------ encrypted input here + externalEuint32 inAmount, // <------ encrypted input here + bytes calldata inputProof // <------ the proof that authenticates it ) public virtual returns (euint32 transferred) { - euint32 amount = FHE.asEuint32(inAmount); + euint32 amount = FHE.asEuint32(inAmount, inputProof); } ``` -Notice in the example above the distinction between **`InEuint32`** and **`euint32`**. +Notice in the example above the distinction between **`externalEuint32`** and **`euint32`**. ## Input Types Conversion -The **input types** `InEuintxx` (and `InEbool`, `InEaddress`) are special encrypted types that represent **user input**. Input types contain additional information required to authenticate and validate ciphertexts. For more on that, read about the [ZK-Verifier](/deep-dive/cofhe-components/zk-verifier). +The **input types** `externalEuintXX` (and `externalEbool`, `externalEaddress`) represent **user input**. Each one is a ciphertext handle that travels with a separate `bytes` proof, and the pair is what lets the contract authenticate the value. For more on that, read about the [ZK-Verifier](/deep-dive/cofhe-components/zk-verifier). -Before you can use an encrypted input, you need to convert it to a regular **encrypted type**: +Before you can use an encrypted input, convert it to a regular **encrypted type**, passing the proof alongside the handle: ```solidity -euint32 amount = FHE.asEuint32(inAmount); +euint32 amount = FHE.asEuint32(inAmount, inputProof); ``` +One signature covers every encrypted input in a call, so the proof is shared. It follows the handle it authenticates rather than sitting last in the parameter list. + -Avoid storing encrypted input types in contract state. These types carry extra metadata, which increases gas costs and may cause unexpected behavior. Always convert them using `FHE.asE...()`. +Avoid storing `externalEuintXX` values in contract state. They are unverified until you convert them, so storing one keeps a handle the contract has not authenticated. Always convert with `FHE.asE...()` first. Now that `amount` is of type `euint32`, you can store or manipulate it: @@ -51,9 +54,10 @@ Here's a complete example showing how to handle encrypted inputs in a transfer f ```solidity function transfer( address to, - InEuint32 memory inAmount + externalEuint32 inAmount, + bytes calldata inputProof ) public virtual returns (euint32 transferred) { - euint32 amount = FHE.asEuint32(inAmount); + euint32 amount = FHE.asEuint32(inAmount, inputProof); toBalance = _balances[to]; fromBalance = _balances[msg.sender]; @@ -67,20 +71,54 @@ function transfer( For the example above to work correctly, you will also need to manage access to the newly created ciphertexts in the `_updateBalance()` function. Learn more about access control in the [ACL Mechanism](/fhe-library/core-concepts/access-control) guide. +## Passing encrypted values between contracts + +`externalEuintXX` is for values arriving from a user. A value arriving from **another contract** uses `sharedEuintXX` instead. + +The distinction matters for security. FHE operations check the permission of the contract performing them, not of whoever called it. A function that accepts a bare `euintXX` from outside can therefore be handed any handle that contract is allowed on, including one read out of its own storage. It can then be made to return something derived from it. + +`sharedEuintXX` closes that. The sharer grants access and records itself in the same step, and the receiver checks who handed the value over: + +```solidity +// Sender: grants access and directs the value at one receiver +token.pull(FHE.shareEuint64(amount, address(token))); + +// Receiver: unwraps it, checking the sharer is the caller +function pull(sharedEuint64 shared) external { + euint64 amount = FHE.receiveEuint64Param(shared); + ... +} +``` + +Pick the receive form by how the value reached you: + +| How it arrived | Use | Sharer is checked against | +| --- | --- | --- | +| An argument to your function | `receiveEuintXXParam(shared)` | your caller (`msg.sender`) | +| The return value of a call you made | `receiveEuintXXFromCall(shared, callee)` | the contract you called | + +For `receiveEuintXXFromCall`, `callee` must be the address called in that same expression. Naming a merely trusted address checks who *created* the share rather than who *handed it over*. + +A share is single-use and lasts one transaction, so it cannot be stored, replayed, or rebuilt from an event. To keep a received value, call `FHE.allowThis` on the unwrapped `euintXX`. Expect `NotShared` when nothing was shared with you, `UnexpectedSharer` when the share came from someone other than the party you named, and `SenderNotAllowed` when the sharer does not hold the handle. + + +The old spelling, an `FHE.allowTransient` grant plus a bare `euintXX` parameter, still compiles and still runs. The compiler will not find these for you, so they have to be found by search. See [migrating to 0.7](/client-sdk/introduction/migrating-to-0-7) for the full pass. + + ## Additional Examples ### Voting in a Poll ```solidity -function castEncryptedVote(address poll, InEbool calldata encryptedVote) public { - _submitVote(poll, FHE.asEbool(encryptedVote)); +function castEncryptedVote(address poll, externalEbool encryptedVote, bytes calldata inputProof) public { + _submitVote(poll, FHE.asEbool(encryptedVote, inputProof)); } ``` ### Setting Encrypted User Preferences ```solidity -function updateUserSetting(address user, InEuint8 calldata encryptedSetting) public { - _applyUserSetting(user, FHE.asEuint8(encryptedSetting)); +function updateUserSetting(address user, externalEuint8 encryptedSetting, bytes calldata inputProof) public { + _applyUserSetting(user, FHE.asEuint8(encryptedSetting, inputProof)); } ``` diff --git a/fhe-library/core-concepts/trivial-encryption.mdx b/fhe-library/core-concepts/trivial-encryption.mdx index 912c389..9ef2c30 100644 --- a/fhe-library/core-concepts/trivial-encryption.mdx +++ b/fhe-library/core-concepts/trivial-encryption.mdx @@ -29,7 +29,7 @@ When two trivially-encrypted numbers are combined in an FHE operation, the resul ## Example ```solidity -function doSomeCalculations(InEuint16 calldata input) { +function doSomeCalculations(externalEuint16 input, bytes calldata inputProof) { // public euint16 number2 = FHE.asEuint16(2); euint16 number3 = FHE.asEuint16(3); diff --git a/fhe-library/examples/auction-example.mdx b/fhe-library/examples/auction-example.mdx index 81539c7..f536186 100644 --- a/fhe-library/examples/auction-example.mdx +++ b/fhe-library/examples/auction-example.mdx @@ -83,8 +83,8 @@ FHE.allowPublic(highestBidder); **Step 2:** Request decryption offchain (client-side) ```typescript -const bidResult = await client.decryptForTx(bidCtHash).withoutPermit().execute(); -const bidderResult = await client.decryptForTx(bidderCtHash).withoutPermit().execute(); +const bidResult = await client.decryptForTx(bidCtHash).withoutACP().execute(); +const bidderResult = await client.decryptForTx(bidderCtHash).withoutACP().execute(); ``` **Step 3:** Publish results onchain with proof @@ -265,7 +265,7 @@ function closeBidding() external onlyAuctioneer { } ``` -Since `FHE.allowPublic` is used, anyone can request decryption offchain without needing a permit. The values are not revealed until someone submits the proof onchain. +Since `FHE.allowPublic` is used, anyone can request decryption offchain without needing an ACP. The values are not revealed until someone submits the proof onchain. ### Revealing the Winner @@ -328,15 +328,15 @@ await auction.connect(auctioneer).closeBidding(); const bidCtHash = await auction.highestBid(); const bidderCtHash = await auction.highestBidder(); -// Request decryption off-chain (no permit needed since allowPublic was used) +// Request decryption offchain (no ACP needed since allowPublic was used) const bidResult = await client .decryptForTx(bidCtHash) - .withoutPermit() + .withoutACP() .execute(); const bidderResult = await client .decryptForTx(bidderCtHash) - .withoutPermit() + .withoutACP() .execute(); // Submit the proofs on-chain to reveal the winner diff --git a/fhe-library/introduction/overview.mdx b/fhe-library/introduction/overview.mdx index de673e8..27a8999 100644 --- a/fhe-library/introduction/overview.mdx +++ b/fhe-library/introduction/overview.mdx @@ -37,10 +37,16 @@ The library supports multiple encrypted data types, each representing an encrypt - Type indicator: Specifies the data type of the encrypted value (e.g. euint8, euint16) to ensure correct handling. - Cryptographic signature: A signature proving that the data and its metadata were generated and verified by an authorized entity. -- **Type-specific input structures:** - - `InEuint8`, `InEuint16`, `InEuint32` - - `InEuint64`, `InEuint128` - - `InEbool`, `InEaddress` +- **`UnsignedEncryptedInput`**: The same shape without the signature, used when a batch of inputs is verified together under one signature. + +- **Type-specific input types:** + - `externalEuint8`, `externalEuint16`, `externalEuint32` + - `externalEuint64`, `externalEuint128` + - `externalEbool`, `externalEaddress` + + Each is a `bytes32` handle rather than a struct. It arrives with a separate `bytes` proof, and `FHE.asEuintXX(handle, proof)` converts the pair. + +- **Type-specific shared types:** `sharedEuint8` through `sharedEaddress`, for encrypted values handed from one contract to another. See [inputs](/fhe-library/core-concepts/inputs). --- @@ -61,7 +67,7 @@ Performs encrypted comparisons (eq, gt, lt, etc.) that return an eboolan encryp Includes conditionals like `select` to allowing encrypted branching without revealing decision paths. #### 5. Data and Access Management -Provides functions for sealing outputs, decrypting values securely, and managing user access via permits, ensuring only authorized parties can access decrypted data. +Provides functions for sealing outputs, decrypting values securely, and managing user access via ACPs, ensuring only authorized parties can access decrypted data. --- diff --git a/fhe-library/introduction/quick-start.mdx b/fhe-library/introduction/quick-start.mdx index 0ea4dc6..081a564 100644 --- a/fhe-library/introduction/quick-start.mdx +++ b/fhe-library/introduction/quick-start.mdx @@ -161,7 +161,7 @@ it('Full Client SDK flow', async function () { // Decrypt the value (view decryption) const result = await client .decryptForView(encryptedValue, FheTypes.Uint32) - .withPermit() + .withACP() .execute() // Check the decrypted result diff --git a/fhe-library/reference/fhe-sol.mdx b/fhe-library/reference/fhe-sol.mdx index 44828b8..60e2297 100644 --- a/fhe-library/reference/fhe-sol.mdx +++ b/fhe-library/reference/fhe-sol.mdx @@ -170,118 +170,148 @@ Converts a plaintext address value to an encrypted address. eaddress encrypted = FHE.asEaddress(0x1234567890123456789012345678901234567890); ``` -### From Encrypted Input Structures +### From encrypted inputs -#### asEbool (from InEbool) +Each of these takes the input handle and the `bytes` proof that authenticates it. One signature covers every encrypted input in a call, so if a function receives more than one, convert them together with the plural form (`asEuint8s`, `asEuint32s`, and so on) rather than calling the single form per value. - -Encrypted input structure containing boolean data +#### asEbool (from externalEbool) + + +Encrypted input handle containing boolean data + + + +Signature authenticating the input batch Encrypted boolean value -Converts an encrypted input structure to an encrypted boolean. +Converts an encrypted input into an encrypted boolean. ```solidity -ebool encrypted = FHE.asEbool(encryptedInput); +ebool encrypted = FHE.asEbool(encryptedInput, inputProof); ``` -#### asEuint8 (from InEuint8) +#### asEuint8 (from externalEuint8) + + +Encrypted input handle containing 8-bit integer data + - -Encrypted input structure containing 8-bit integer data + +Signature authenticating the input batch Encrypted 8-bit unsigned integer -Converts an encrypted input structure to an encrypted 8-bit unsigned integer. +Converts an encrypted input into an encrypted 8-bit unsigned integer. ```solidity -euint8 encrypted = FHE.asEuint8(encryptedInput); +euint8 encrypted = FHE.asEuint8(encryptedInput, inputProof); ``` -#### asEuint16 (from InEuint16) +#### asEuint16 (from externalEuint16) + + +Encrypted input handle containing 16-bit integer data + - -Encrypted input structure containing 16-bit integer data + +Signature authenticating the input batch Encrypted 16-bit unsigned integer -Converts an encrypted input structure to an encrypted 16-bit unsigned integer. +Converts an encrypted input into an encrypted 16-bit unsigned integer. ```solidity -euint16 encrypted = FHE.asEuint16(encryptedInput); +euint16 encrypted = FHE.asEuint16(encryptedInput, inputProof); ``` -#### asEuint32 (from InEuint32) +#### asEuint32 (from externalEuint32) - -Encrypted input structure containing 32-bit integer data + +Encrypted input handle containing 32-bit integer data + + + +Signature authenticating the input batch Encrypted 32-bit unsigned integer -Converts an encrypted input structure to an encrypted 32-bit unsigned integer. +Converts an encrypted input into an encrypted 32-bit unsigned integer. ```solidity -euint32 encrypted = FHE.asEuint32(encryptedInput); +euint32 encrypted = FHE.asEuint32(encryptedInput, inputProof); ``` -#### asEuint64 (from InEuint64) +#### asEuint64 (from externalEuint64) + + +Encrypted input handle containing 64-bit integer data + - -Encrypted input structure containing 64-bit integer data + +Signature authenticating the input batch Encrypted 64-bit unsigned integer -Converts an encrypted input structure to an encrypted 64-bit unsigned integer. +Converts an encrypted input into an encrypted 64-bit unsigned integer. ```solidity -euint64 encrypted = FHE.asEuint64(encryptedInput); +euint64 encrypted = FHE.asEuint64(encryptedInput, inputProof); ``` -#### asEuint128 (from InEuint128) +#### asEuint128 (from externalEuint128) - -Encrypted input structure containing 128-bit integer data + +Encrypted input handle containing 128-bit integer data + + + +Signature authenticating the input batch Encrypted 128-bit unsigned integer -Converts an encrypted input structure to an encrypted 128-bit unsigned integer. +Converts an encrypted input into an encrypted 128-bit unsigned integer. ```solidity -euint128 encrypted = FHE.asEuint128(encryptedInput); +euint128 encrypted = FHE.asEuint128(encryptedInput, inputProof); ``` -#### asEaddress (from InEaddress) +#### asEaddress (from externalEaddress) + + +Encrypted input handle containing address data + - -Encrypted input structure containing address data + +Signature authenticating the input batch Encrypted Ethereum address -Converts an encrypted input structure to an encrypted address. +Converts an encrypted input into an encrypted address. ```solidity -eaddress encrypted = FHE.asEaddress(encryptedInput); +eaddress encrypted = FHE.asEaddress(encryptedInput, inputProof); ``` ### Type Conversion Between Encrypted Types @@ -1161,7 +1191,7 @@ FHE.allowThis(counter); // Required for future access ### allowPublic -Grants public permission to access the encrypted value. Once called, anyone can request decryption of this value offchain via `decryptForTx` without needing a permit. +Grants public permission to access the encrypted value. Once called, anyone can request decryption of this value offchain via `decryptForTx` without needing an ACP. Encrypted value to grant public access to @@ -1176,7 +1206,7 @@ Once `allowPublic` is called, the value can be decrypted by anyone. Only use thi FHE.allowPublic(highestBid); FHE.allowPublic(highestBidder); -// Now anyone can call decryptForTx off-chain without a permit +// Now anyone can call decryptForTx offchain without an ACP ``` ### allowSender @@ -1318,12 +1348,14 @@ Each encrypted type has a corresponding binding library that includes all the op ### Example with Bindings ```solidity -// Use secure encrypted input -InEuint8 encryptedInputA; // Provided by client-side encryption -InEuint8 encryptedInputB; // Provided by client-side encryption +// Two inputs share one batch signature, so convert them together +externalEuint8[] memory inputs = new externalEuint8[](2); +inputs[0] = encryptedInputA; // Provided by client-side encryption +inputs[1] = encryptedInputB; // Provided by client-side encryption -euint8 a = FHE.asEuint8(encryptedInputA); -euint8 b = FHE.asEuint8(encryptedInputB); +euint8[] memory values = FHE.asEuint8s(inputs, inputProof); +euint8 a = values[0]; +euint8 b = values[1]; // Arithmetic operations euint8 sum = a.add(b); // Addition @@ -1398,12 +1430,14 @@ Always consider security implications when working with encrypted data. This example shows how to perform private computations while keeping all intermediate values encrypted: ```solidity -// Use secure encrypted input -InEuint8 encryptedInputA; // Provided by client-side encryption -InEuint8 encryptedInputB; // Provided by client-side encryption +// Two inputs share one batch signature, so convert them together +externalEuint8[] memory inputs = new externalEuint8[](2); +inputs[0] = encryptedInputA; // Provided by client-side encryption +inputs[1] = encryptedInputB; // Provided by client-side encryption -euint8 a = FHE.asEuint8(encryptedInputA); -euint8 b = FHE.asEuint8(encryptedInputB); +euint8[] memory values = FHE.asEuint8s(inputs, inputProof); +euint8 a = values[0]; +euint8 b = values[1]; // Perform operations euint8 sum = FHE.add(a, b); // Encrypted addition @@ -1425,12 +1459,14 @@ FHE.allowPublic(result); ### Example with Bindings ```solidity -// Use secure encrypted input -InEuint8 encryptedInputA; // Provided by client-side encryption -InEuint8 encryptedInputB; // Provided by client-side encryption +// Two inputs share one batch signature, so convert them together +externalEuint8[] memory inputs = new externalEuint8[](2); +inputs[0] = encryptedInputA; // Provided by client-side encryption +inputs[1] = encryptedInputB; // Provided by client-side encryption -euint8 a = FHE.asEuint8(encryptedInputA); -euint8 b = FHE.asEuint8(encryptedInputB); +euint8[] memory values = FHE.asEuint8s(inputs, inputProof); +euint8 a = values[0]; +euint8 b = values[1]; // Perform operations using dot notation euint8 sum = a.add(b); // Encrypted addition diff --git a/fhe-library/reference/fhe-sol/access-control.mdx b/fhe-library/reference/fhe-sol/access-control.mdx index 0d13fe0..faad681 100644 --- a/fhe-library/reference/fhe-sol/access-control.mdx +++ b/fhe-library/reference/fhe-sol/access-control.mdx @@ -30,7 +30,7 @@ FHE.allowThis(counter); ## allowPublic -Grants public permission. Anyone can request decryption offchain via `decryptForTx` without a permit. +Grants public permission. Anyone can request decryption offchain via `decryptForTx` without an ACP. Once called, the value can be decrypted by anyone. Only use this when you intend to reveal the value publicly (e.g., after an auction closes or when unwrapping tokens). diff --git a/fhe-library/reference/fhe-sol/bindings.mdx b/fhe-library/reference/fhe-sol/bindings.mdx index b1bf24b..05daaf3 100644 --- a/fhe-library/reference/fhe-sol/bindings.mdx +++ b/fhe-library/reference/fhe-sol/bindings.mdx @@ -3,7 +3,7 @@ title: "Bindings (Dot Notation)" description: "Use encrypted types with dot notation instead of FHE.* prefix" --- -The FHE library provides binding libraries that enable dot notation for all operations. Import them alongside FHE.sol — no extra setup required. +The FHE library provides binding libraries that enable dot notation for all operations. Import them alongside FHE.sol, with no extra setup required. ```solidity // Without bindings @@ -16,11 +16,16 @@ euint8 sum = a.add(b); ## Full Example ```solidity -InEuint8 encryptedInputA; -InEuint8 encryptedInputB; +// Two inputs share one batch signature, so convert them together. +// Calling asEuint8(hash, signature) per value would verify each hash +// against a signature that covers both, and revert. +externalEuint8[] memory inputs = new externalEuint8[](2); +inputs[0] = encryptedInputA; +inputs[1] = encryptedInputB; -euint8 a = FHE.asEuint8(encryptedInputA); -euint8 b = FHE.asEuint8(encryptedInputB); +euint8[] memory values = FHE.asEuint8s(inputs, inputProof); +euint8 a = values[0]; +euint8 b = values[1]; // Arithmetic euint8 sum = a.add(b); diff --git a/fhe-library/reference/fhe-sol/overview.mdx b/fhe-library/reference/fhe-sol/overview.mdx index 47dfdf7..ff14ada 100644 --- a/fhe-library/reference/fhe-sol/overview.mdx +++ b/fhe-library/reference/fhe-sol/overview.mdx @@ -29,17 +29,33 @@ All functions are prefixed with `FHE.` when called. For example: `FHE.add(a, b)` ## Input Types -Each encrypted type has a corresponding input struct used when receiving encrypted data from the client SDK: +Each encrypted type has a corresponding input type, used when receiving encrypted data from the client SDK. An input arrives as a handle plus a `bytes` proof, and `FHE.asEuintXX(handle, proof)` converts the pair into the encrypted type: | Input Type | Encrypted Type | |------------|---------------| -| `InEbool` | `ebool` | -| `InEuint8` | `euint8` | -| `InEuint16` | `euint16` | -| `InEuint32` | `euint32` | -| `InEuint64` | `euint64` | -| `InEuint128` | `euint128` | -| `InEaddress` | `eaddress` | +| `externalEbool` | `ebool` | +| `externalEuint8` | `euint8` | +| `externalEuint16` | `euint16` | +| `externalEuint32` | `euint32` | +| `externalEuint64` | `euint64` | +| `externalEuint128` | `euint128` | +| `externalEaddress` | `eaddress` | + +## Shared Types + +A value handed over by another contract arrives as a shared type instead. `FHE.shareEuintXX(value, receiver)` produces one, and the receiver unwraps it with `FHE.receiveEuintXXParam` or `FHE.receiveEuintXXFromCall`: + +| Shared Type | Encrypted Type | +|-------------|---------------| +| `sharedEbool` | `ebool` | +| `sharedEuint8` | `euint8` | +| `sharedEuint16` | `euint16` | +| `sharedEuint32` | `euint32` | +| `sharedEuint64` | `euint64` | +| `sharedEuint128` | `euint128` | +| `sharedEaddress` | `eaddress` | + +See [inputs](/fhe-library/core-concepts/inputs) for when to use each. ## Security Considerations diff --git a/fhe-library/reference/fhe-sol/type-conversion.mdx b/fhe-library/reference/fhe-sol/type-conversion.mdx index abb52fc..8b45fc9 100644 --- a/fhe-library/reference/fhe-sol/type-conversion.mdx +++ b/fhe-library/reference/fhe-sol/type-conversion.mdx @@ -40,8 +40,8 @@ eaddress encrypted = FHE.asEaddress(0x1234567890123456789012345678901234567890); Convert client-side encrypted inputs (received as function parameters) into encrypted types. ```solidity -function deposit(InEuint64 calldata encryptedAmount) external { - euint64 amount = FHE.asEuint64(encryptedAmount); +function deposit(externalEuint64 encryptedAmount, bytes calldata inputProof) external { + euint64 amount = FHE.asEuint64(encryptedAmount, inputProof); } ``` diff --git a/get-started/introduction/what-is-cofhe.mdx b/get-started/introduction/what-is-cofhe.mdx index 7e71b65..f5ccadb 100644 --- a/get-started/introduction/what-is-cofhe.mdx +++ b/get-started/introduction/what-is-cofhe.mdx @@ -25,7 +25,7 @@ Because computation happens on encrypted values, you can build applications wher - **Confidential balances and transfers**: token amounts and balances stay hidden while transfers still settle correctly. - **Private state in contracts**: per-user values (counters, scores, bids, positions) that no one, not even the contract or CoFHE, can read in the clear. - **Sealed inputs**: users submit encrypted inputs (votes, bids, orders) that are computed on without ever being revealed. -- **Selective disclosure**: results are decrypted only for authorized parties, gated by signed permits. +- **Selective disclosure**: results are decrypted only for authorized parties, gated by signed ACPs. ## How it works @@ -41,8 +41,8 @@ The user's plaintext is encrypted in the client using the [`@cofhe/sdk`](/client The smart contract uses [`FHE.sol`](/fhe-library/introduction/overview) to operate on encrypted handles, adding, comparing, selecting, as if they were ordinary numbers. Each operation deterministically derives a new result handle and is recorded onchain; the CoFHE server independently computes the matching ciphertext offchain. Nothing returns to the contract, and plaintext is never exposed at any point. - -When an authorized user wants a result, they present a signed [permit](/client-sdk/guides/permits). The Threshold Network decrypts via multi-party computation, either re-encrypting the value so only that user can read it (for display), or returning a verifiable plaintext with a signature (for onchain use). + +When an authorized user wants a result, they present a signed [Access Control Permission](/client-sdk/guides/acps). The Threshold Network decrypts via multi-party computation, either re-encrypting the value so only that user can read it (for display), or returning a verifiable plaintext with a signature (for onchain use). @@ -67,7 +67,7 @@ sequenceDiagram CoFHE->>CoFHE: pick up operation, compute ciphertext for the same handle Note over User,CoFHE: Decrypt - User->>SDK: read result (+ permit) + User->>SDK: read result (+ ACP) SDK->>CoFHE: decrypt request CoFHE-->>SDK: re-encrypted / verifiable plaintext SDK-->>User: revealed value @@ -81,7 +81,7 @@ Developers only interact directly with **two** parts of CoFHE; the rest runs beh | Component | Where | Role | | --- | --- | --- | -| **[`@cofhe/sdk`](/client-sdk/introduction/overview)** | Client | Encrypt inputs, manage permits, decrypt outputs | +| **[`@cofhe/sdk`](/client-sdk/introduction/overview)** | Client | Encrypt inputs, manage ACPs, decrypt outputs | | **[`FHE.sol`](/fhe-library/introduction/overview)** | Onchain | Solidity API for operating on encrypted handles | ### What runs behind the scenes @@ -90,7 +90,8 @@ Developers only interact directly with **two** parts of CoFHE; the rest runs beh | --- | --- | | **Task Manager** | Onchain gateway that validates FHE requests and enforces access control | | **Slim Listener** | Watches onchain events and forwards operations to the offchain layer | -| **FheOS Server** | Executes the actual FHE computations and holds encrypted state | +| **FheOS Server** | Verifies incoming work and queues it. It does not execute FHE operations | +| **FHE Engine** | Executes the FHE operations on encrypted data | | **Result Processor** | Publishes verified results back onchain | | **Threshold Network** | Decrypts via multi-party computation, no single party holds the key | | **Registries** | Track ciphertexts and record result commitments so integrity can be verified before any decryption | @@ -102,7 +103,7 @@ For a component-by-component breakdown, see the [CoFHE Architecture deep dive](/ - **Encrypted end-to-end**: values are encrypted client-side and stay encrypted through computation; only handles touch the chain. - **Verified inputs**: zero-knowledge proofs ensure every encrypted input is well-formed before it enters the system. - **Verified results**: the coprocessor commits to each result onchain, and the Threshold Network checks integrity before it will decrypt anything. -- **No single point of trust for decryption**: decryption requires the Threshold Network's multi-party computation, gated by signed permits. +- **No single point of trust for decryption**: decryption requires the Threshold Network's multi-party computation, gated by signed ACPs. ## Next steps diff --git a/index.mdx b/index.mdx index 6b9da22..437a0c3 100644 --- a/index.mdx +++ b/index.mdx @@ -24,9 +24,8 @@ Learn the fundamentals by building a simple encrypted counter contract. ## Key Resources -{/* @SDK */} - -JavaScript library for encrypting inputs, managing permits, and decrypting outputs. + +JavaScript library for encrypting inputs, managing ACPs, and decrypting outputs.