diff --git a/privacy-on-demand/cookbook-private-investor-allocations.md b/privacy-on-demand/cookbook-private-investor-allocations.md index 91b774e..cd732a7 100644 --- a/privacy-on-demand/cookbook-private-investor-allocations.md +++ b/privacy-on-demand/cookbook-private-investor-allocations.md @@ -40,7 +40,8 @@ The public version is useful because it gives you a known baseline: owner assign - A Solidity toolchain such as Hardhat or Foundry. - A Sepolia wallet with test ETH for deploys, transactions, and PoD request fees. - Node.js 18+ for scripts. -- The PoD SDK package: `npm install "@coti/pod-sdk"`. +- The PoD SDK package: `npm install "@coti/pod-sdk"` (ships the vendored `MpcCore.sol` under `@coti/pod-sdk/contracts/utils/mpc/`, so the COTI‑side contract no longer needs the `@coti-io/coti-contracts` package). +- The COTI client crypto package: `npm install "@coti-io/coti-sdk-typescript@^1.0.7"` (provides `decryptUint256({ ciphertextHigh, ciphertextLow }, key)` for the 256‑bit ciphertext shape). - A way for users to complete PoD onboarding and obtain their account AES key for local decryption. Before implementing the private version, read: @@ -411,14 +412,17 @@ const encryptedAllocation = await CotiPodCrypto.encrypt( If you use `PodContract.encryptAndCallMethod`, you can pass the plaintext string plus `DataType.itUint256`; the SDK encrypts and encodes the argument before sending the transaction. If the browser or backend already encrypted the value, use `callMethod` with the ciphertext JSON. -Investors decrypt only the ciphertext that was off-boarded to them. +Investors decrypt only the ciphertext that was off-boarded to them. Because `ctUint256` is a struct, the contract read returns a tuple `{ ciphertextHigh, ciphertextLow }`: ```typescript -const ct = await sepoliaAllocations.readResultByRequest(requestId); -const ctHex = typeof ct === "bigint" ? "0x" + ct.toString(16) : String(ct); +const raw = await sepoliaAllocations.readResultByRequest(requestId); +const ct = { + ciphertextHigh: BigInt(raw.ciphertextHigh ?? raw[0]), + ciphertextLow: BigInt(raw.ciphertextLow ?? raw[1]), +}; const plain = CotiPodCrypto.decrypt( - ctHex, + ct, accountAesKeyFromOnboarding, DataType.Uint256 ); @@ -426,6 +430,8 @@ const plain = CotiPodCrypto.decrypt( console.log("private allocation:", plain); ``` +Under the hood, the 256‑bit decrypt path calls `decryptUint256({ ciphertextHigh, ciphertextLow }, key)` from `@coti-io/coti-sdk-typescript` (`^1.0.7`). Narrower lanes (`Uint64`, `Uint128`) still take a single ciphertext word. + > **Warning:** Never log, persist, or transmit the account AES key as ordinary application data. Treat it as user-controlled key material. ## Part 7: Allocate and read private allocations @@ -482,6 +488,8 @@ function onSetAllocationCompleted(bytes memory resultData) external onlyInbox { For investor reads, the investor asks COTI to off-board their allocation to their address. The callback stores `ctUint256`, and the investor decrypts locally with their account AES key. +`ctUint256` is a Solidity **struct** with two `ctUint128` limbs (`ciphertextHigh`, `ciphertextLow`), so the decoded local needs a `memory` location and the storage mapping holds the two‑limb tuple. + ```solidity mapping(bytes32 => ctUint256) public allocationReadResults; @@ -490,7 +498,7 @@ function onAllocationRead(bytes memory resultData) external onlyInbox { require(callerChain == COTI_TESTNET_CHAIN_ID && callerContract == cotiAllocationPeer, "not allowed"); bytes32 requestId = IInbox(inbox).inboxSourceRequestId(); - ctUint256 allocation = abi.decode(resultData, (ctUint256)); + ctUint256 memory allocation = abi.decode(resultData, (ctUint256)); allocationReadResults[requestId] = allocation; } diff --git a/privacy-on-demand/for-developers-mapping-to-the-sdk.md b/privacy-on-demand/for-developers-mapping-to-the-sdk.md index 96750f6..5140f46 100644 --- a/privacy-on-demand/for-developers-mapping-to-the-sdk.md +++ b/privacy-on-demand/for-developers-mapping-to-the-sdk.md @@ -28,10 +28,21 @@ Then deep dives: | **Callback guard** | [InboxUser.sol](https://github.com/cotitech-io/coti-pod-sdk/blob/main/contracts/InboxUser.sol) (`onlyInbox`) — see [Features](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/03-features.md). | | **PodLib** | [PodLib.sol](https://github.com/cotitech-io/coti-pod-sdk/blob/main/contracts/mpc/PodLib.sol) and width-specific libraries (`PodLib64`, `PodLib128`, `PodLib256`). | | **PodUser / presets** | [PodUser.sol](https://github.com/cotitech-io/coti-pod-sdk/blob/main/contracts/mpc/PodUser.sol), network mixins such as [PodUserSepolia.sol](https://github.com/cotitech-io/coti-pod-sdk/blob/main/contracts/mpc/PodUserSepolia.sol) in [Getting started](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/04-getting-started.md). | -| **Types (`it*`, `ct*`, `gt*`)** | [MpcCore.sol](https://github.com/cotitech-io/coti-pod-sdk/blob/main/contracts/utils/mpc/MpcCore.sol) and [Data types](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/contracts/01-it-ct-gt-data-types.md). | +| **Types (`it*`, `ct*`, `gt*`)** | Vendored [MpcCore.sol](https://github.com/cotitech-io/coti-pod-sdk/blob/main/contracts/utils/mpc/MpcCore.sol) (and `MpcInterface.sol`) inside the PoD SDK — no separate `@coti-io/coti-contracts` package is needed for PoD apps. See [Data types](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/contracts/01-it-ct-gt-data-types.md). | | **Custom COTI calls** | [MpcAbiCodec.sol](https://github.com/cotitech-io/coti-pod-sdk/blob/main/contracts/mpccodec/MpcAbiCodec.sol) and the **custom mode** section of [Writing privacy contracts](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/05-writing-privacy-contracts-on-ethereum.md). | | **Client crypto** | [coti-pod-crypto.ts](https://github.com/cotitech-io/coti-pod-sdk/blob/main/src/coti-pod-crypto.ts) via `CotiPodCrypto` ([TypeScript integration](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/06-typescript-integration-ux-development.md)). | +## Type model at a glance + +The PoD SDK ships **`MpcCore.sol`** under `@coti/pod-sdk/contracts/utils/mpc/`. In the current revision: + +- **`gtUint8` … `gtUint256` and `gtBool`** are **user‑defined value types** (`type gtUint256 is uint256`). Pass and assign them like `uint256` — **no `memory` / `calldata` on `gt*` parameters or locals**. +- **`ctUint8` … `ctUint128`** are also user‑defined value types (single `uint256` word). +- **`ctUint256`** is a **struct** `{ ctUint128 ciphertextHigh; ctUint128 ciphertextLow; }` — decoded locals and callback variables must use a `memory` location, and off‑chain reads return the two limbs as a tuple. +- **`itUint*`** (user encrypted inputs, `ciphertext + signature`), **`utUint*`** (dual‑ciphertext), **`gtString`** and **`ctString`** remain structs — keep their `calldata` / `memory` locations. + +If you previously imported from **`@coti-io/coti-contracts`** in PoD code, switch the import to **`@coti/pod-sdk/contracts/utils/mpc/MpcCore.sol`**. Off‑chain decryption uses **`@coti-io/coti-sdk-typescript@^1.0.7`**, which exposes `decryptUint256({ ciphertextHigh, ciphertextLow }, accountAesKey)` for the 256‑bit lane. + ## Implementation checklist (condensed) Derived from the SDK’s [Writing privacy contracts](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/05-writing-privacy-contracts-on-ethereum.md) and [Async execution](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/05a-async-execution.md): diff --git a/privacy-on-demand/tutorial-custom-logic.md b/privacy-on-demand/tutorial-custom-logic.md index 15fd783..21ea805 100644 --- a/privacy-on-demand/tutorial-custom-logic.md +++ b/privacy-on-demand/tutorial-custom-logic.md @@ -41,7 +41,7 @@ The COTI contract **inherits `InboxUser`**, so only the Inbox can enter `receive pragma solidity ^0.8.19; import "../InboxUserCotiTestnet.sol"; -import "@coti-io/coti-contracts/contracts/utils/mpc/MpcCore.sol"; +import "@coti/pod-sdk/contracts/utils/mpc/MpcCore.sol"; contract DirectMessageCotiSide is InboxUserCotiTestnet { function receiveMessage(gtString calldata message, address sender, address recipient) external onlyInbox { diff --git a/privacy-on-demand/tutorial-private-adder-sepolia.md b/privacy-on-demand/tutorial-private-adder-sepolia.md index 04bb85b..6de6e67 100644 --- a/privacy-on-demand/tutorial-private-adder-sepolia.md +++ b/privacy-on-demand/tutorial-private-adder-sepolia.md @@ -93,7 +93,7 @@ contract PrivateAdder is PodLib, PodUserSepolia { requestId = inbox.inboxRequestId(); } - ctUint256 sum = abi.decode(data, (ctUint256)); + ctUint256 memory sum = abi.decode(data, (ctUint256)); sumByRequest[requestId] = sum; statusByRequest[requestId] = RequestStatus.Completed; emit AddCompleted(requestId); @@ -105,6 +105,7 @@ contract PrivateAdder is PodLib, PodUserSepolia { - **`onDefaultMpcError`** is implemented on `PodLibBase` and forwards failures to **`ErrorRemoteCall`** on `PodUser`. Your UI can listen for that event to mark a request failed. - **`addCallback`** must stay **`onlyInbox`** so random accounts cannot forge results. +- **`ctUint256`** is a Solidity **struct** `{ ctUint128 ciphertextHigh; ctUint128 ciphertextLow; }`, so the decoded local must use a `memory` location and the storage mapping holds the two‑limb tuple. The narrower garbled / ciphertext types (`gtUint8…gtUint256`, `gtBool`, and `ctUint8…ctUint128`) are **user‑defined value types** — pass and assign them like `uint256` (no `memory` / `calldata`). Encrypted-input wrappers such as **`itUint256`** stay structs and keep their `calldata` / `memory` location as before. ## Step 3: Compile and deploy on Sepolia @@ -145,8 +146,10 @@ import { import { ethers } from "ethers"; // Minimal ABI fragment — prefer the full artifact from your build (Hardhat / Foundry). +// itUint256 = { ctUint256 ciphertext; bytes signature }, and ctUint256 = { uint256 ciphertextHigh; uint256 ciphertextLow } +// so each `itUint256` parameter encodes as ((uint256,uint256),bytes). const privateAdderAbi = [ - "function add((uint256 ciphertext,bytes signature),(uint256 ciphertext,bytes signature),uint256) payable returns (bytes32)", + "function add(((uint256,uint256),bytes),((uint256,uint256),bytes),uint256) payable returns (bytes32)", ] as const; const pod = new PodContract( @@ -206,35 +209,37 @@ Private addition is **asynchronous**: the sum appears only after the Inbox invok ## Step 7: Read the encrypted sum and decrypt locally -After status is **Completed**, read **`sumByRequest(requestId)`**. The value is **`ctUint256`** (ciphertext), not plaintext. +After status is **Completed**, read **`sumByRequest(requestId)`**. The value is **`ctUint256`** (ciphertext), not plaintext. Because **`ctUint256`** is a Solidity **struct** with two `ctUint128` limbs, the contract read returns a tuple `{ ciphertextHigh, ciphertextLow }` (each is a single `uint256`). ```typescript import { CotiPodCrypto, DataType } from "@coti/pod-sdk"; // accountAesKey: hex string from your app’s COTI onboarding flow (never log it) -const ct = await privateAdder.sumByRequest(requestId); // bytes32 from extractRequestIds or return value -const ctHex = - typeof ct === "bigint" - ? "0x" + ct.toString(16) - : String(ct); +const raw = await privateAdder.sumByRequest(requestId); +// ethers / viem return the struct as a tuple — normalize to { ciphertextHigh, ciphertextLow } +const ct = { + ciphertextHigh: BigInt(raw.ciphertextHigh ?? raw[0]), + ciphertextLow: BigInt(raw.ciphertextLow ?? raw[1]), +}; const decryptedString = CotiPodCrypto.decrypt( - ctHex, + ct, accountAesKey, - DataType.Uint64 + DataType.Uint256 ); console.log("sum (plaintext string):", decryptedString); // Expect "30" for plainA=10 and plainB=20 ``` -`CotiPodCrypto.decrypt` delegates to `@coti-io/coti-sdk-typescript` and expects a **scalar ciphertext** as a **hex string** for `Uint64`, plus the user’s **AES key** (see SDK source [coti-pod-crypto.ts](https://github.com/cotitech-io/coti-pod-sdk/blob/main/src/coti-pod-crypto.ts)). +`CotiPodCrypto.decrypt` delegates to **`@coti-io/coti-sdk-typescript`** (`^1.0.7`), which now exposes `decryptUint256({ ciphertextHigh, ciphertextLow }, accountAesKey)` for the 256‑bit lane. Narrower lanes (`Uint64`, `Uint128`, …) still take a single `uint256` ciphertext as a `bigint` or `0x`‑prefixed hex string (see SDK source [coti-pod-crypto.ts](https://github.com/cotitech-io/coti-pod-sdk/blob/main/src/coti-pod-crypto.ts)). ## Step 8: Sanity checks and next steps -- **Callback decode** must stay **`(ctUint256)`** — changing the executor op or COTI-side behavior without updating the decode tuple will corrupt storage reads. -- **Type lane** — This contract uses **`add256`** with **`itUint256`** / **`ctUint256`** on chain. **`CotiPodCrypto.decrypt`** still takes a **`DataType`** for the scalar decode; keep **`DataType.Uint64`** (or **`Uint256`**, etc.) aligned with how your app and onboarding produce the ciphertext for this flow, per your installed SDK. +- **Callback decode** must stay **`(ctUint256)`** — and the local must use `memory` because `ctUint256` is a struct. Changing the executor op or COTI-side behavior without updating the decode tuple will corrupt storage reads. +- **Type lane** — This contract uses **`add256`** with **`itUint256`** / **`ctUint256`** on chain, so pass **`DataType.Uint256`** to `CotiPodCrypto.decrypt` and feed it the **`{ ciphertextHigh, ciphertextLow }`** tuple read from the contract. Narrower lanes (`Uint64`, `Uint128`) still take a single ciphertext word. +- **Type model** — In the current `MpcCore.sol`, `gtUint*`, `gtBool`, and `ctUint8…ctUint128` are **user‑defined value types** (`type X is uint256`) — drop `memory` / `calldata` on them. `ctUint256` is a struct (two `ctUint128` limbs); `itUint*` / `utUint*` are also still structs, so keep `calldata` / `memory` on those. - **Production**: add tests for non-Inbox callers on `addCallback`, under-funded `msg.value`, and decrypt failures; follow the [first production checklist](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/04-getting-started.md) in Getting started. ## Reference links diff --git a/privacy-on-demand/typescript-pod-sdk.md b/privacy-on-demand/typescript-pod-sdk.md index 5a2f4af..9851588 100644 --- a/privacy-on-demand/typescript-pod-sdk.md +++ b/privacy-on-demand/typescript-pod-sdk.md @@ -12,7 +12,7 @@ Use these helpers from a wallet script, backend service, or dApp frontend once y - You can also pass a full encryption service URL. - `DataType` distinguishes plaintext types (`Uint64`, `String`, etc.) from **`it*`** types that become ciphertext tuples on chain. -`CotiPodCrypto.decrypt` uses the user's **account AES key** and `@coti-io/coti-sdk-typescript` under the hood. +`CotiPodCrypto.decrypt` uses the user's **account AES key** and **`@coti-io/coti-sdk-typescript`** (`^1.0.7`) under the hood. ```typescript import { CotiPodCrypto, DataType } from "@coti/pod-sdk"; @@ -20,10 +20,31 @@ import { CotiPodCrypto, DataType } from "@coti/pod-sdk"; // Encrypt plaintext for Solidity itUint256 parameters const enc = await CotiPodCrypto.encrypt("42", "testnet", DataType.itUint256); -// Decrypt scalar ciphertext read from contract storage -const plain = CotiPodCrypto.decrypt("0x...", accountAesKeyFromOnboarding, DataType.Uint64); +// Decrypt a narrow scalar ciphertext (one uint256 word) read from contract storage +const plain64 = CotiPodCrypto.decrypt("0x...", accountAesKeyFromOnboarding, DataType.Uint64); + +// Decrypt a ctUint256 value (a struct with two ctUint128 limbs) +const plain256 = CotiPodCrypto.decrypt( + { ciphertextHigh, ciphertextLow }, + accountAesKeyFromOnboarding, + DataType.Uint256 +); ``` +### Ciphertext shape in the current `MpcCore.sol` + +After the gt‑type upgrade, the on‑chain types you read back have these shapes: + +| Type | Solidity | Off‑chain shape | +| -------------------------- | --------------------------------------------------------- | ---------------------------------------------- | +| `gtUint8` … `gtUint256`, `gtBool` | User‑defined value type (`type … is uint256`), no `memory`/`calldata` | n/a — never crosses the chain boundary | +| `ctUint8` … `ctUint128` | User‑defined value type | single `uint256` word | +| `ctUint256` | Struct `{ ctUint128 ciphertextHigh; ctUint128 ciphertextLow; }` | tuple `{ ciphertextHigh, ciphertextLow }` (each `bigint`) | +| `itUint*` / `utUint*` | Struct (ciphertext + signature, unchanged) | tuple / object | +| `gtString` / `ctString` | Struct (array of `gtUint64` / `ctUint64`, unchanged) | array of words | + +When you read a `ctUint256` value via ethers or viem, the storage getter returns the two limbs as a tuple — normalize it to `{ ciphertextHigh, ciphertextLow }` before passing it to `CotiPodCrypto.decrypt` (or to `decryptUint256` from `@coti-io/coti-sdk-typescript`). The narrower `ct*` lanes can still be passed as a `bigint` or `0x`‑prefixed hex string. + ## Gas estimation and method calls (`PodContract`) `PodContract` wraps `ethers.Contract` with PoD-aware helpers: