diff --git a/coti-privacy-portal/developer-guide/frontend-integration.md b/coti-privacy-portal/developer-guide/frontend-integration.md index 3f8186b..bdf1bc4 100644 --- a/coti-privacy-portal/developer-guide/frontend-integration.md +++ b/coti-privacy-portal/developer-guide/frontend-integration.md @@ -253,3 +253,15 @@ async function encryptedApprove( return txHash; } ``` + +## Polling failed PoD / Privacy Portal requests + +When a pToken request leaves `Pending`, read `requests(requestId).status` and decode `failedRequests(requestId)`: + +1. If status is `SystemFailed` (or `failedRequests` ABI-decodes to Inbox `{ErrorData}` with `errorCode == 2`), show a system-error message. Do not retry the same request on COTI. +2. For portal deposits, call `refundFailedDeposit(mintRequestId)` **only** after `SystemFailed`. App `raise` / `Failed` is not refundable. +3. For portal withdrawals, call `cancelFailedWithdrawal(withdrawalId)` after the transfer request is `Failed` or `SystemFailed`. +4. If the mint is still `Pending` but COTI Inbox `errors[id].errorCode == 1`, show an **execution failure** and offer / await **`retryFailedRequest`** (permissionless). Use **`getOutboxError(id)`** for the capped returndata and decode in the client. Do **not** refund while a retry can still mint. +5. After `createPortal`, poll mother `isRegistered(sourceChainId, pToken)` before enabling deposits. A prior `FactoryNotAllowed` registration is recovered with allowlist + `retryFailedRequest`, not by assuming the portal is ready. + +For offline fee accounting, index `PrivacyPortal.OperationFeesPaid`: `portalFee` (protocol, retained), `podFee` (forwarded to inbox), and `podCallbackFee` (callback slice). `isDeposit` / `isNativeWrap` distinguish the operation. diff --git a/coti-privacy-portal/developer-guide/troubleshooting.md b/coti-privacy-portal/developer-guide/troubleshooting.md index cfa7e00..6b09a3c 100644 --- a/coti-privacy-portal/developer-guide/troubleshooting.md +++ b/coti-privacy-portal/developer-guide/troubleshooting.md @@ -11,6 +11,30 @@ | `AES key mismatch` | Wrong AES key used to decrypt | Re-onboard wallet to get correct key | | `invalid BytesLike value` | Passing HardhatEthersSigner to `prepareIT256` | Use `new ethers.Wallet(privateKey, provider)` instead | | `PRIVATE_AES_KEY_TESTNET not set` | Missing env var | Add 32 hex char key to `.env` (no `0x` prefix) | +| Deposit stuck `Pending` forever | Several distinct causes (see below) | Diagnose with pToken `requests` / `failedRequests` and Inbox `errors` / `getOutboxError` | +| Withdraw stuck `TransferPending` | Transfer-to-portal request Failed | Call `PrivacyPortal.cancelFailedWithdrawal(withdrawalId)`; do not expect underlying release | +| Encode/`validateCiphertext` fail (`failedRequests` as `{ErrorData}` with `errorCode == 2`) | Wrong `it*` signer / encode fail | Clear UI pending; submit a **new** op (not COTI `retryFailedRequest`) | +| Mother registration / `FactoryNotAllowed` | Factory not allowlisted on COTI mother before registration mined | Allowlist factory (`setAllowedFactory`), then permissionless COTI `retryFailedRequest` for the failed registration id | +| Deposits before mother confirms | Portal created but mother `isRegistered` still false | Keep deposits disabled / pause until mother registration confirms | + +### Stuck `Pending` deposits — diagnose before refunding + +A deposit mint can remain **`Pending`** for different reasons. Do **not** assume every stuck mint is a system-failed encode: + +| Observation | Likely cause | Action | +| --- | --- | --- | +| `requests(id).status == SystemFailed` / `failedRequests` decodes to Inbox `{ErrorData}` code `2` | Encode / `validateCiphertext` failed before COTI mint logic | Clear UI pending; call `refundFailedDeposit` (permissionless) after SystemFailed | +| COTI Inbox `errors[id].errorCode == 1` | Target execution reverted (retryable) | Call permissionless `retryFailedRequest` on COTI; do **not** refund while mint may still succeed | +| No COTI incoming request / never mined | Relayer lag or miner not ingesting | Wait for miner; do not treat as SystemFailed | +| Mother never registered / factory not allowlisted | Registration one-way failed or never confirmed | Fix allowlist + `retryFailedRequest`; keep user deposits off until `isRegistered` | + +**Break-glass:** some deployments expose an admin refund for still-`Pending` escrows. That is operationally dangerous if COTI mint can still succeed afterward—prefer SystemFailed refunds and retries. + +### Privacy Portal recovery + +* **System-failed deposit (mint):** underlying stays escrowed until anyone calls `refundFailedDeposit(mintRequestId)` after `pToken.requests(id).status == SystemFailed` (funds always return to the depositor). App `raise` / `Failed` is **not** refundable (mint should not raise). Portal protocol fee is kept. +* **Failed withdraw (transfer):** after the transfer request is `Failed` or `SystemFailed`, call `cancelFailedWithdrawal(withdrawalId)` to mark the withdrawal `Failed`. Underlying is **not** released; the user still holds pTokens; portal fee is kept. +* **Factory → mother registration:** `createPortal` **submits** a one-way registration message; it does **not** wait for the mother to confirm. Keep deposits disabled until `PodErc20CotiMother.isRegistered(sourceChainId, pToken)` is true. If registration failed with `FactoryNotAllowed` before allowlist, allowlist the factory then call COTI `retryFailedRequest`. ### Security Notes @@ -20,3 +44,4 @@ * Self-transfers (`from == to`) are explicitly blocked at the contract level. * `transferAndCall` is protected by `nonReentrant` but the callback contract must be trusted. * `totalSupply()` always returns `0` for privacy — do not rely on it for supply accounting. +* Inbox `executed` / compact response events mean the return leg was **received**, not that your app callback committed—confirm via pToken / portal status. diff --git a/privacy-on-demand/README.md b/privacy-on-demand/README.md index 57c028c..1f792f9 100644 --- a/privacy-on-demand/README.md +++ b/privacy-on-demand/README.md @@ -2,7 +2,7 @@ Privacy on Demand lets applications use **strong privacy for data and computation** while still using **ordinary EVM chains** (Ethereum, L2s, and other compatible networks) for accounts, assets, and business workflows. -> **Development status:** This Privacy on Demand material and the **COTI PoD SDK** it describes are **under active development**. Treat them accordingly: on-chain and client code **may not yet be fully audited**, and **breaking changes** (APIs, ABIs, addresses, presets, or documentation) can occur as the stack matures. Pin versions, follow release notes, and perform your own review before relying on anything in production. +> **Development status:** This Privacy on Demand material and the **COTI PoD SDK** it describes are **under active development**. Treat them accordingly: pin versions, follow release notes, and perform your own review before relying on anything in production. External audits for related COTI components are listed under [Audit Reports](../security/audit-reports.md); the PoD Inbox and Privacy Portal stacks also received an **internal security review** with documented hardening (see that page). **Breaking changes** (APIs, ABIs, addresses, presets, or documentation) can still occur as the stack matures.
diff --git a/privacy-on-demand/architecture-and-components.md b/privacy-on-demand/architecture-and-components.md index 788dbf7..43f5627 100644 --- a/privacy-on-demand/architecture-and-components.md +++ b/privacy-on-demand/architecture-and-components.md @@ -73,7 +73,9 @@ A fuller table lives in the SDK’s [data types](https://github.com/cotitech-io/ ## Trust and security highlights (for architects) - **Callback authentication**: Your contract should only accept **Inbox-originated** callbacks for private results—otherwise anyone could try to spoof answers. The SDK’s `onlyInbox` pattern exists for this boundary ([features](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/03-features.md)). -- **Request correlation**: Private work completes **later**; your system must track **request IDs** and statuses honestly in UX and backends ([Async private operations](async-private-operations.md)). +- **Trusted miner / relayer**: Cross-domain delivery is operated by **registered Inbox miners**. Payload authenticity for private results still rests on **`onlyInbox` callbacks** and your application’s request-status accounting—not on an on-chain proof of the remote execution transcript. Treat miner set ownership (multisig / timelock) as part of the threat model. +- **Request correlation**: Private work completes **later**; your system must track **request IDs** and statuses honestly in UX and backends ([Async private operations](async-private-operations.md)). Do **not** treat Inbox `executed` / compact response events alone as proof that your callback committed. +- **Failure surfaces**: Distinguish **system error** (code `2`, not retryable), **app `raise`**, and **execution failure** (code `1`, permissionless `retryFailedRequest`). Use **`getOutboxError`** for the capped returndata bytes (decode in the client). - **Key stewardship**: Client-side AES material is powerful; treat it like **credentials**, not analytics metadata ([TypeScript integration](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/06-typescript-integration-ux-development.md)). ## Next steps diff --git a/privacy-on-demand/async-private-operations.md b/privacy-on-demand/async-private-operations.md index 1a7e05b..dc0764a 100644 --- a/privacy-on-demand/async-private-operations.md +++ b/privacy-on-demand/async-private-operations.md @@ -20,6 +20,42 @@ Private execution happens **outside** your chain’s normal synchronous EVM fram The SDK’s [Async execution](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/05a-async-execution.md) page lists the canonical lifecycle and common mistakes (wrong decode shape, missing `onlyInbox`, expecting same-block completion). +### System errors vs application `raise` + +Both are delivered to the **same** source `errorSelector(bytes data)` (same path as `inbox.raise`). Branch with `inbox.inboxErrorType()` (`SystemError` vs `Exception`). + +| Kind | When | COTI target ran? | `data` layout | Retryable via `retryFailedRequest`? | +| --- | --- | --- | --- | --- | +| **System error** | Encode / `validateCiphertext` fails before the COTI app runs | No | Inbox `{ErrorData}`: `abi.encode(uint64 code, bytes message)` (code `2`). Sender is `SYSTEM_SENDER` | **No** | +| **App `raise`** | COTI app calls `inbox.raise(...)` | Yes (started) | **dApp-defined** (e.g. `abi.encode(from, to, reason)`) | Submit a **new** request after pending clears | +| **Execution failure** | Target reverts without `raise` (code `1`) | Yes | No automatic source callback; error stored on COTI Inbox | **Yes** on COTI (permissionless) | + +**Handler pattern** (see PodERC20 error callbacks): + +1. `onlyInbox`; `_errorCallbackContext()` **reverts** unless `inboxErrorType()` is `SystemError`/`Exception`, `sourceRequestId` is linked, and status is Pending. +2. Branch on type: `SystemError` → decode Inbox `{ErrorData}`; `Exception` → decode your app `raise` layout. + +### One-way vs two-way error handling + +- **`sendOneWayMessage` rejects a non-zero `errorSelector`.** One-way jobs have no return / error callback leg. If you need an `errorSelector` handler, use a **two-way** message. +- System-error and app-`raise` callbacks therefore apply to **two-way** flows that registered an `errorSelector`. + +### Execution failure, capped returndata, and `getOutboxError` + +When the COTI target reverts without `raise`, the miner records **error code `1`** (`ERROR_CODE_EXECUTION_FAILED`) and stores the first ≤**256** bytes of returndata in `errors[requestId].errorMessage`. + +- **`getOutboxError(requestId)`** returns `(code, data)` where `data` is those same raw bytes. Decode `Error(string)` / custom errors in your client (JS/TS). +- If `data.length == 256`, the original returndata may have been longer (cap truncated it). + +Anyone may call permissionless **`retryFailedRequest(requestId)`** on COTI while the stored code is still `1`. A retry that fails to **re-encode** the call **reverts** and **keeps** code `1` (it does not flip the request to encode-failed / code `2`). + +### What `executed` and response events mean + +Inbox flags such as **`executed`** on an incoming request, and compact events such as **`IncomingResponseReceived`**, mean the **return / error leg was ingested** by the Inbox—not that your application callback **committed** successfully. + +- A return leg can still leave a **retryable** execution error (`errors[id]` with code `1`) if the callback reverted. +- Product and indexers should treat **application events / request status** (for example pToken `requests(id).status`) as the source of truth for user-visible success or failure—not Inbox `executed` alone. + ## What product and support teams should plan for | Topic | Recommendation | 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/glossary.md b/privacy-on-demand/glossary.md index cbf6577..66a5dae 100644 --- a/privacy-on-demand/glossary.md +++ b/privacy-on-demand/glossary.md @@ -5,14 +5,28 @@ Short definitions for **Privacy on Demand** readers. Precise Solidity definition | Term | Meaning | | --- | --- | | **Privacy on Demand (PoD)** | Pattern and tooling for **private computation on COTI** while **orchestrating** from **another EVM chain** via an **Inbox**. | -| **Inbox** | **On-chain router** on your EVM chain that **forwards** private jobs toward COTI and **delivers callbacks** with results to your contract. | -| **MPC executor** | **COTI-side contract** configured as the execution target for **library-style** PoD flows; referenced from your dApp’s routing configuration. | -| **PodUser** | Solidity **configuration mixin** for **Inbox address**, **COTI chain id**, and **executor address**; changes should be **governed** (for example `onlyOwner`). | -| **PodLib** | Solidity **helper library** for **common private operations** (fixed-width arithmetic and comparisons in supported paths). | -| **`it*` (input types)** | **Encrypted user input** with required **signature material**, prepared client-side and sent to your contract. | -| **`gt*` (garbled / compute types)** | **Internal private representation** during computation on **COTI**—not something your EVM contract should expose as a public API. | -| **`ct*` (ciphertext types)** | **Encrypted outputs** suitable to **store on your chain**; users **decrypt locally** with **account AES** keys where applicable. | -| **PoA fees** | In this book, **Privacy on Demand (PoD) fees** for **two-way Inbox** traffic: native token on your chain that funds **COTI-side** and **callback** execution budgets. See [How do PoA fees work?](how-poa-fees-work.md). | -| **Two-way message** | Inbox flow: **outbound** request to COTI plus **inbound callback** to your contract; typically needs **fee** planning for both legs. | -| **Request ID** | Correlator tying a **submission** to a **callback**; essential for **async** UX and troubleshooting. | -| **Account AES key** | User-side secret material used to **decrypt** many `ct*` outputs after onboarding; must be **handled like credentials**. | +| **Inbox** | On-chain **message router** on each chain that forwards private jobs toward COTI and delivers callbacks. Implementation in [`coti-pod-inbox-contracts`](https://github.com/coti-io/coti-pod-inbox-contracts). | +| **MPC executor** | COTI-side contract configured as the execution target for **library-style** PoD flows; referenced from your dApp’s routing configuration. | +| **PodUser** | Solidity **configuration mixin** for Inbox address, COTI chain id, and executor address; changes should be **governed** (for example `onlyOwner`). | +| **PodUserSepolia / PodUserFuji** | Network presets that auto-wire inbox and COTI routing in the constructor. | +| **PodLib** | Solidity **helper library** for built-in private operations at 64/128/256-bit widths. | +| **`it*` (input types)** | Encrypted user input with signature material, prepared client-side and sent to your contract. | +| **`gt*` (garbled / compute types)** | Internal private representation during computation on **COTI**—not something your EVM contract should expose as a public API. | +| **`ct*` (ciphertext types)** | Encrypted outputs suitable to store on your chain; users decrypt locally with the account AES key. | +| **PoA / PoD fees** | Native token on your chain funding COTI-side and callback execution budgets for **two-way Inbox** traffic. See [How do PoA fees work?](how-poa-fees-work.md). | +| **Two-way message** | Outbound request to COTI plus inbound callback to your contract; typically needs fee planning for both legs. | +| **Request ID** | 32-byte correlator tying submission to callback; indexed in compact `MessageSent` events. Essential for async UX and troubleshooting. | +| **System error** | Pre-execution Inbox failure (encode / `validateCiphertext`). Delivered on the same `errorSelector(bytes)` as app `raise`. Attributed to `SYSTEM_SENDER`. Detect via `inboxErrorType() == SystemError`. **Not** eligible for `retryFailedRequest`. | +| **`SYSTEM_SENDER`** | Placeholder `originalSender` / `inboxMsgSender()` for system-error return legs. Not a real contract; do not require it to equal your COTI peer. | +| **`inboxErrorType()`** | Inbox view returning `NotErrorContext`, `SystemError`, or `Exception` for the active execution — preferred way for error handlers to branch. | +| **Execution failure (code `1`)** | Target ran and reverted without `raise`. Stored on COTI with capped returndata; **permissionless** `retryFailedRequest` while code remains `1`. | +| **`getOutboxError`** | View that returns `(code, data)` — for execution failures, the **raw capped returndata** (≤256 bytes). Decode in the client. | +| **`executed` / `IncomingResponseReceived`** | Mean the **return or error leg was ingested** by the Inbox—not that the application callback committed. Confirm UX success via app status / events. | +| **One-way message** | Outbound-only Inbox send. **Cannot** register a non-zero `errorSelector` (use two-way if you need error callbacks). | +| **`retryFailedRequest`** | Permissionless COTI Inbox call that re-executes a request still marked execution-failed (code `1`). Encode failure on retry **reverts** and preserves code `1`. | +| **Gas-price bounds** | Operator-configured floor / ceiling / min priority used when converting fee wei into gas-unit budgets (bounded reference price—not raw tip manipulation). | +| **Account AES key** | 32-hex-character user secret for decrypting `ct*` outputs after onboarding; must be handled like credentials. See [Account Onboard](../build-on-coti/guides/account-onboard.md). | +| **PodRequest** | TypeScript helper (`@coti/pod-sdk`) that polls inbox state across chains for async UX. | +| **PodSdkConfig** | JSON config (chains, inbox addresses, RPCs, encryption network) shared by `PodContract` and `PodRequest`. | +| **`@coti-io/coti-contracts`** | npm package with PoD Solidity libraries, interfaces, and examples. | +| **`@coti-io/coti-pod-inbox-contracts`** | npm package with Inbox implementation, fee manager, and miner contracts. | diff --git a/privacy-on-demand/how-a-private-request-travels-end-to-end.md b/privacy-on-demand/how-a-private-request-travels-end-to-end.md index 5008a90..ea51971 100644 --- a/privacy-on-demand/how-a-private-request-travels-end-to-end.md +++ b/privacy-on-demand/how-a-private-request-travels-end-to-end.md @@ -24,6 +24,28 @@ This page describes **one full cycle** of Privacy on Demand **without assuming S 6. **The MPC Executor returns through Inbox (COTI) and Inbox (EVM)**, delivering an **ABI-encoded payload** of **`ct*` ciphertext**: encrypted outputs suitable to store on your chain. 7. **Your contract records the result** keyed by a **request ID**. The **user reads ciphertext from chain or API**, then **decrypts locally** with their **account AES key** (after proper onboarding). +### Encode / `validateCiphertext` failure (system error) + +If Inbox encoding fails **before** the MPC Executor runs (most commonly a bad `it*` signature), COTI never calls your target contract. Instead the Inbox: + +1. Records error code `2` (`ERROR_CODE_ENCODE_FAILED`) and emits `ErrorReceived`. +2. Automatically sends a return leg to your source `errorSelector` with Inbox `{ErrorData}` (`abi.encode(uint64 code, bytes message)`, code `2`) attributed to `SYSTEM_SENDER` — see [Async private operations](async-private-operations.md). +3. Marks the incoming request executed so it is **not** eligible for `retryFailedRequest`. + +Your source error callback must clear pending state and surface failure to the UI. Users submit a **new** request after pending clears. + +### Execution failure (retryable) and reading errors + +If the COTI target **runs** and **reverts** without `raise`: + +1. The Inbox stores error code `1` with a **capped** returndata blob (see [Async private operations](async-private-operations.md)). +2. There is **no** automatic source callback for this path—operators or anyone may call **`retryFailedRequest`** on COTI while the code remains `1`. +3. Use **`getOutboxError(requestId)`** for the capped returndata bytes (same as `errors[requestId].errorMessage`); decode in the client. + +### Completion signals + +When the diagram says “Callback with result bytes,” that is the **application** success path. Inbox-level `executed` / compact response events only mean the **return leg was received**—confirm success via your contract’s status / events (see [Async private operations](async-private-operations.md)). + ## Sequence diagram (conceptual) **Colors:** the **cool charcoal** panel is **contracts on your host chain (Ethereum)**—**Your dApp contract** and **Inbox (EVM)**. The **warm charcoal** panel is **contracts on COTI**—**Inbox (COTI)** and **MPC Executor**. **User** and **Client app** are off-chain (default styling). Arrows **between** the two dark panels are **cross-chain / cross-domain** handoffs. The diagram uses a **dark theme** so labels stay light-on-dark and readable. @@ -79,7 +101,7 @@ sequenceDiagram ## Fees and gas -Private jobs that cross from your chain to COTI and back incur **network and execution costs**. Integrations typically attach **native token value** on the request and split it between **remote execution** and the **callback** leg. Operators configure **fee parameters** and **oracle** behavior on supporting contracts (see the SDK’s [Fees, gas, and oracle](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/contracts/04-fees-gas-and-oracle.md) page). +Private jobs that cross from your chain to COTI and back incur **network and execution costs**. Integrations typically attach **native token value** on the request and split it between **remote execution** and the **callback** leg. Operators configure **fee parameters**, **gas-price bounds**, and **oracle** behavior on supporting contracts (see the SDK’s [Fees, gas, and oracle](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/contracts/04-fees-gas-and-oracle.md) page and [How do PoA fees work?](how-poa-fees-work.md)). ## Next steps diff --git a/privacy-on-demand/how-poa-fees-work.md b/privacy-on-demand/how-poa-fees-work.md index 9a0f9b9..f0dc0f4 100644 --- a/privacy-on-demand/how-poa-fees-work.md +++ b/privacy-on-demand/how-poa-fees-work.md @@ -21,7 +21,7 @@ Solidity shape (conceptually): | Stage | Amounts | | --- | --- | -| **User-provided gas** | | +| **User-provided gas** | | | **Price at oracle** (same quote, e.g. USD) | | | **Converted to the chain gas (COTI — ETH)** | | | **Convert to gas units (COTI — ETH)** | | @@ -40,6 +40,18 @@ Solidity shape (conceptually): Read the **first table top to bottom:** user ETH is split by leg, oracles supply **COTI** and **ETH** prices, the COTI leg is expressed as **quote → COTI tokens**, then policy turns each leg into **gas-unit budgets**. The **second table** spends **COTI** first, then **Sepolia** after the result exists. Underspend remains are illustrative; production behavior depends on **InboxMiner** / **InboxFeeManager** and operator policy (see the SDK [Fees, gas, and oracle](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/contracts/04-fees-gas-and-oracle.md) doc). +### Reference gas price and bounds + +When the Inbox converts fee wei into **gas-unit** budgets, it uses a **bounded reference gas price**—not an unbounded reading of the caller’s tip: + +- Operators configure **`setGasPriceBounds`** (min priority fee, min gas price, optional max). +- On EIP-1559 chains the reference is typically derived from **`basefee` + min priority** (and clamped by the configured floor / ceiling). +- Clients should still pass a realistic `gasPrice` into **off-chain** fee estimates so the UI matches what users will pay; on-chain conversion remains **bounded** so tip inflation cannot shrink budgets arbitrarily. + +### Oracle cache refresh + +Inbox fee math reads **cached** USD prices for the configured local / remote legs. Operators (or keepers) call **`refreshCache()`**, which refreshes **both** inbox legs together. Configure legs with **`setInboxTokens`**. Uniswap-based oracles derive those leg addresses from the configured pairs at construction; spot Uniswap prices remain manipulable—prefer trusted feeds or TWAP for production. + ## Why this matters for `add(a, b) → ctUint64 c` - **Private execution** and **callback execution** happen in **different environments**, as **separate steps in time**: COTI runs **first**; the Sepolia relay and **callback** run **after** the result exists. Each leg needs its **own** paid work (compute + relay). 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: diff --git a/security/README.md b/security/README.md index 1ce7218..df0228d 100644 --- a/security/README.md +++ b/security/README.md @@ -1,10 +1,8 @@ -# Audit Roports +# Audit Reports -Security is a top priority for the COTI network. Below you’ll find a list of independent security audits conducted on different components of the COTI ecosystem. Each report outlines the scope, focus areas, and includes a link to the full audit documentation. +Security is a top priority for the COTI network. Below you’ll find a list of independent security audits conducted on different components of the COTI ecosystem. -These audits play a critical role in ensuring that the COTI protocol remains secure, reliable, and ready for real-world adoption. - -You can find all published audit reports here: +> **Canonical page:** Prefer [Audit Reports](audit-reports.md) for the full table (including Privacy Portal) and the **PoD Inbox / Privacy Portal internal hardening** notes. diff --git a/security/audit-reports.md b/security/audit-reports.md index 0c41d48..b407aa1 100644 --- a/security/audit-reports.md +++ b/security/audit-reports.md @@ -16,3 +16,19 @@ You can find all published audit reports here: | 2026 - 3 | Sayfer | Private ERC20 | [2026\_ERC20\_Sayfer](Sayfer-2026-03-Smart-Contract-Audit-Report-for-Coti.pdf) | | 2026 - 3 | Sayfer | Metamask Snap | [2026\_Metamask](https://github.com/coti-io/coti-snap/blob/main/docs/Sayfer-2026-03-Metamask-Snap-Audit-for-Coti.pdf) | | 2026 - 5 | Sayfer | Privacy Portal | [2026\_PrivacyPortal](Sayfer-2026-05-Smart-Contract-Audit-Report-for-Coti.pdf) | + +## PoD Inbox and Privacy Portal hardening (internal review) + +In addition to the external reports above, the PoD **Inbox** (`coti-pod-inbox-contracts`) and **PoD Privacy Portal** stack received an internal security review with follow-up fixes. Integrators should treat the following as current behavior (also reflected in the PoD / Avalanche books): + +| Area | What changed for integrators | +| --- | --- | +| **Capped returndata** | Execution failures store at most 256 bytes of returndata. **`getOutboxError`** returns `(code, data)` with those raw bytes for client-side decoding. | +| **`retryFailedRequest`** | Permissionless while error code is `1`. Encode failure on retry **reverts** and preserves code `1` (does not flip to encode-failed). | +| **One-way `errorSelector`** | `sendOneWayMessage` rejects non-zero `errorSelector`—use two-way for error callbacks. | +| **`executed` / response events** | Mean the return leg was **ingested**, not that the app callback committed. | +| **Fee gas price** | On-chain budgets use **bounded reference** gas price (`setGasPriceBounds`), not unbounded tip manipulation. | +| **Oracle cache** | `refreshCache()` refreshes **both** inbox legs; configure with `setInboxTokens` (Uniswap oracles set legs from pairs at construction). | +| **Portal deposits** | Prefer `refundFailedDeposit` only after **SystemFailed**. Stuck `Pending` may be retryable execution failure or miner lag—diagnose before refunding. Keep deposits off until mother registration confirms. | + +See [Async private operations](../privacy-on-demand/async-private-operations.md), [How do PoA fees work?](../privacy-on-demand/how-poa-fees-work.md), and [Privacy Portal troubleshooting](../coti-privacy-portal/developer-guide/troubleshooting.md).