diff --git a/.gitbook/assets/coti-wallet-plugin-example-home.png b/.gitbook/assets/coti-wallet-plugin-example-home.png new file mode 100644 index 0000000..5894406 Binary files /dev/null and b/.gitbook/assets/coti-wallet-plugin-example-home.png differ diff --git a/SUMMARY.md b/SUMMARY.md index 9448ef4..d4fdfdd 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -26,6 +26,7 @@ * [COTI vs others](how-coti-works/advanced-topics/coti-vs-others.md) * [Build on COTI](build-on-coti/README.md) * [Core Concepts](build-on-coti/core-concepts/README.md) + * [What Is Onboarding?](build-on-coti/core-concepts/what-is-onboarding.md) * [Account Onboarding Procedure](build-on-coti/core-concepts/onboard-user.md) * [Private Data Types](build-on-coti/core-concepts/secure-data-types.md) * [Supported Operations on Private Data Types](build-on-coti/core-concepts/secure-operations-and-gas.md) @@ -62,6 +63,13 @@ * [Remix Plugin](build-on-coti/tools/remix-plugin.md) * [COTI MetaMask Snap](build-on-coti/tools/coti-metamask-snap/README.md) * [Snap Integration](build-on-coti/tools/coti-metamask-snap/snap-integration.md) + * [COTI Wallet Plugin](build-on-coti/tools/coti-wallet-plugin/README.md) + * [Integration Guide](build-on-coti/tools/coti-wallet-plugin/integration-guide.md) + * [Configuration](build-on-coti/tools/coti-wallet-plugin/configuration.md) + * [API Reference](build-on-coti/tools/coti-wallet-plugin/api-reference.md) + * [AES Key Onboarding](build-on-coti/tools/coti-wallet-plugin/aes-key-onboarding.md) + * [Onboard Modal Theming](build-on-coti/tools/coti-wallet-plugin/onboard-modal-theme.md) + * [Example App](build-on-coti/tools/coti-wallet-plugin/example-app.md) * [Developer Sandbox](build-on-coti/tools/developer-sandbox.md) * [Private Messaging](private-messaging/README.md) * [Quickstart](private-messaging/quickstart.md) @@ -112,6 +120,9 @@ * [Privacy on Demand (PoD) for Dummies](privacy-on-demand/pod-for-dummies.md) * [How a private request travels end to end](privacy-on-demand/how-a-private-request-travels-end-to-end.md) * [Architecture and main components](privacy-on-demand/architecture-and-components.md) + * [Networks](privacy-on-demand/networks/README.md) + * [COTI Testnet](privacy-on-demand/networks/coti-testnet.md) + * [Avalanche Fuji](privacy-on-demand/networks/fuji.md) * [Glossary](privacy-on-demand/glossary.md) * [Async private operations](privacy-on-demand/async-private-operations.md) * [How do PoA fees work?](privacy-on-demand/how-poa-fees-work.md) @@ -121,6 +132,24 @@ * [Cookbook: private investor allocations with PoD](privacy-on-demand/cookbook-private-investor-allocations.md) * [Tutorial: private Adder on Sepolia](privacy-on-demand/tutorial-private-adder-sepolia.md) * [Tutorial: custom privacy logic with PoD](privacy-on-demand/tutorial-custom-logic.md) +* [Privacy on Avalanche](privacy-on-avalanche/README.md) + * [Getting started on Avalanche Fuji (Day 0)](privacy-on-avalanche/getting-started-fuji.md) + * [What is Privacy on Avalanche?](privacy-on-avalanche/what-is-privacy-on-avalanche.md) + * [Privacy on Avalanche (PoD) for Dummies](privacy-on-avalanche/pod-for-dummies.md) + * [How a private request travels end to end](privacy-on-avalanche/how-a-private-request-travels-end-to-end.md) + * [Architecture and main components](privacy-on-avalanche/architecture-and-components.md) + * [Networks](privacy-on-avalanche/networks/README.md) + * [Avalanche Fuji](privacy-on-avalanche/networks/fuji.md) + * [COTI Testnet](privacy-on-avalanche/networks/coti-testnet.md) + * [Glossary](privacy-on-avalanche/glossary.md) + * [Async private operations](privacy-on-avalanche/async-private-operations.md) + * [How do PoA fees work?](privacy-on-avalanche/how-poa-fees-work.md) + * [For developers: mapping concepts to the SDK](privacy-on-avalanche/for-developers-mapping-to-the-sdk.md) + * [Tutorials: building PoD dApps on Avalanche](privacy-on-avalanche/tutorials-privacy-on-avalanche.md) + * [TypeScript PoD SDK (CotiPodCrypto, PodContract)](privacy-on-avalanche/typescript-pod-sdk.md) + * [Cookbook: private investor allocations with PoD](privacy-on-avalanche/cookbook-private-investor-allocations.md) + * [Tutorial: private Adder on Avalanche Fuji](privacy-on-avalanche/tutorial-private-adder-fuji.md) + * [Tutorial: custom privacy logic with PoD](privacy-on-avalanche/tutorial-custom-logic.md) ## Security diff --git a/build-on-coti/core-concepts/onboard-user.md b/build-on-coti/core-concepts/onboard-user.md index a344ed3..945c398 100644 --- a/build-on-coti/core-concepts/onboard-user.md +++ b/build-on-coti/core-concepts/onboard-user.md @@ -4,6 +4,8 @@ Account Onboarding is a procedure that is needed to acquire the EOA unique AES k Once the EOA is created and funded with native coin, it needs to be onboarded to the system to obtain an AES key. +For a plain-language explanation for end users, see [What Is Onboarding?](what-is-onboarding.md). + See the [guide for onboarding an account](../guides/account-onboard.md). After completing the Account Onboard step the wallet has a functional account with the necessary credentials (having own AES key) allowing it to start interacting with the gcEVM operations, including running smart contracts or performing other actions as needed. diff --git a/build-on-coti/core-concepts/what-is-onboarding.md b/build-on-coti/core-concepts/what-is-onboarding.md new file mode 100644 index 0000000..ee6239a --- /dev/null +++ b/build-on-coti/core-concepts/what-is-onboarding.md @@ -0,0 +1,35 @@ +# What Is Onboarding? + +Onboarding is the one-time setup that gives your wallet its encryption key for private on-chain data. A short COTI Network transaction **retrieves** that key (your AES key) for your wallet address so you can encrypt and decrypt private on-chain values. + +Until the key is available in your wallet session, you can still use the network normally (public transfers, gas, non-private contracts). You **cannot** read or write private data — for example private balances, amounts, or other encrypted contract variables. + +When a dApp needs access to private data, it will ask you to complete onboarding by approving that transaction in your wallet. + +{% hint style="info" %} +**Developers:** see [Account Onboarding Procedure](onboard-user.md) and [Account Onboard](../guides/account-onboard.md). +{% endhint %} + +## Why do I need it? + +Private values on COTI are stored encrypted. Encryption and decryption use a key that belongs only to **your** wallet account ([EOA](../../support-and-community/glossary.md#account)). + +Your wallet needs that key to: + +* **encrypt** inputs before private transactions +* **decrypt** outputs when you need to see private values + +Without it, private features stay locked. The network already associates a unique AES key with your address; onboarding is how your wallet **receives** that key (first time) or **recovers** it again (new device, cleared storage, reinstall). Recovering always returns the **same** key for that address — it does not issue a new one. + +## What is the encryption key? + +* One AES key per wallet address — not shared across accounts +* Used only for **your** private data on COTI +* Held by your wallet / dApp session (for MetaMask users, often inside the [COTI Snap](../tools/coti-metamask-snap/README.md)); you usually never need to copy it +* Delivered wrapped so only your wallet can open it — not left as plaintext for others to read on-chain + +{% hint style="danger" %} +Treat your AES key like your wallet private key. Anyone who has it can decrypt your private on-chain data. Never share your AES key, seed phrase, or private key. +{% endhint %} + +Technical detail: [AES Keys](../../how-coti-works/advanced-topics/aes-keys.md). diff --git a/build-on-coti/tools/README.md b/build-on-coti/tools/README.md index b410c0f..923f453 100644 --- a/build-on-coti/tools/README.md +++ b/build-on-coti/tools/README.md @@ -1,2 +1,22 @@ # Tools +Developer tools for building on COTI. + +## SDKs and libraries + +* [TypeScript SDK](typescript-sdk.md) +* [Ethers.js](ethers.js.md) +* [Python SDK](python-sdk.md) +* [Web3.py](web3.py.md) +* [Contracts Library](contracts-library/README.md) + +## IDE and wallet plugins + +* [Hardhat](hardhat.md) +* [Remix Plugin](remix-plugin.md) +* [COTI MetaMask Snap](coti-metamask-snap/README.md) +* [COTI Wallet Plugin](coti-wallet-plugin/README.md) — React library for adding private token support to wagmi/RainbowKit dApps + +## Other + +* [Developer Sandbox](developer-sandbox.md) diff --git a/build-on-coti/tools/coti-metamask-snap/README.md b/build-on-coti/tools/coti-metamask-snap/README.md index df0595e..1b1db01 100644 --- a/build-on-coti/tools/coti-metamask-snap/README.md +++ b/build-on-coti/tools/coti-metamask-snap/README.md @@ -30,6 +30,8 @@ Once the snap is installed, the MetaMask site will offer to continue to COTI's c #### **2. Onboard Account (Retrieve AES Key)** +For a plain-language overview, see [What Is Onboarding?](../../core-concepts/what-is-onboarding.md). + Once you are in the companion dApp site ([metamask.coti.io](https://metamask.coti.io)): 1. Click on the "**Connect Wallet**" button, follow the prompts on MetaMask. diff --git a/build-on-coti/tools/coti-metamask-snap/snap-integration.md b/build-on-coti/tools/coti-metamask-snap/snap-integration.md index 9201388..eb01939 100644 --- a/build-on-coti/tools/coti-metamask-snap/snap-integration.md +++ b/build-on-coti/tools/coti-metamask-snap/snap-integration.md @@ -1,5 +1,9 @@ # Snap Integration +{% hint style="info" %} +**Building a React/wagmi dApp?** Use the [COTI Wallet Plugin](../../coti-wallet-plugin/README.md) instead. It handles Snap RPC wiring, AES onboarding, and private balance decryption through a provider + hooks model. This guide is for low-level Snap integration without the wallet plugin. +{% endhint %} + ### Integrating your dApp with the COTI MetaMask Snap If you are building a dApp on the COTI network and want it to interact with the COTI MetaMask snap, follow these steps: diff --git a/build-on-coti/tools/coti-wallet-plugin/README.md b/build-on-coti/tools/coti-wallet-plugin/README.md new file mode 100644 index 0000000..e449659 --- /dev/null +++ b/build-on-coti/tools/coti-wallet-plugin/README.md @@ -0,0 +1,130 @@ +--- +description: React library for adding COTI private token support to wagmi/RainbowKit dApps. +--- + +# COTI Wallet Plugin + +The **COTI Wallet Plugin** (`@coti-io/coti-wallet-plugin`) is a React library that lets dApp developers add COTI private token (pToken) support to an existing **wagmi v2 + RainbowKit** stack — without building a custom wallet or hand-rolling AES key management. + +{% hint style="info" %} +**Important:** This library is a **plugin for existing dApps and wallets**, not a standalone wallet application. It is designed to be injected into your existing React/wagmi stack to seamlessly enhance standard wallets with COTI network privacy capabilities. +{% endhint %} + +## When to use it + +Use the COTI Wallet Plugin when you are building a **React dApp** that needs: + +* Wallet connection via MetaMask, Rabby, WalletConnect, and other EIP-1193 wallets +* AES key onboarding and unlock for private token balances +* Public ↔ private token bridging +* Private encrypt/decrypt/send operations + +If you are building without React or wagmi, use the lower-level [TypeScript SDK](../typescript-sdk.md), [Ethers.js](../ethers.js.md), or [COTI MetaMask Snap](../coti-metamask-snap/snap-integration.md) integration guides instead. + +## What it does + +| Capability | How | +| --- | --- | +| Wallet connection | `WagmiRainbowKitProvider` — MetaMask, Rabby, WalletConnect, and more | +| AES onboarding / unlock | Built-in `OnboardModal` — Snap, contract onboarding, encrypted backup restore | +| Private balance display | Auto-decrypt on-chain ciphertext balances | +| Public ↔ private bridging | Native COTI bridge on COTI chains; PoD Privacy Portal on Sepolia / Avalanche Fuji | +| Private crypto ops | `encryptPrivateValue`, `decryptPrivateValue`, `sendPrivateToken` | +| Network enforcement | `NetworkGuard` for supported chains | + +## Supported chains + +| Chain | Chain ID | Portal strategy | +| --- | --- | --- | +| COTI Testnet | 7082400 | COTI native bridge | +| COTI Mainnet | 2632500 | COTI native bridge | +| Sepolia | 11155111 | PoD Privacy Portal | +| Avalanche Fuji | 43113 | PoD Privacy Portal | + +Unlock is wallet-based (same on every supported chain), in preferred order: + +1. MetaMask Snap (when MetaMask + Snap is available) +2. Encrypted backup restore +3. Contract onboarding (on COTI) +4. Manual AES key input (if the host enables it) + +See [AES Key Onboarding](aes-key-onboarding.md) for routes and fallbacks. + +## Relationship to other COTI tools + +| Product | Role | +| --- | --- | +| [COTI MetaMask Snap](../coti-metamask-snap/README.md) | Key storage and Snap-backed crypto; the plugin calls it via RPC | +| [TypeScript SDK](../typescript-sdk.md) | Low-level encrypt/decrypt primitives; wrapped internally by the plugin | +| [Ethers.js](../ethers.js.md) | Contract onboarding (`generateOrRecoverAes`); used internally | +| [COTI Privacy Portal](../../../coti-privacy-portal/README.md) | A dApp that may consume the plugin; not required to use the library | + +## Installation + +```bash +npm install @coti-io/coti-wallet-plugin +``` + +### Peer dependencies + +```bash +npm install react react-dom ethers viem @coti-io/coti-sdk-typescript @metamask/providers @rainbow-me/rainbowkit wagmi @tanstack/react-query +``` + +{% hint style="warning" %} +This release is validated with `@rainbow-me/rainbowkit@2.2.0` and `wagmi@2.14.0`. Keep those two packages on compatible versions in the host app; installing mismatched latest peer versions can break RainbowKit before the plugin loads. +{% endhint %} + +## Quickstart + +```tsx +import { + PrivacyBridgeProvider, + WagmiRainbowKitProvider, + usePrivateUnlock, +} from '@coti-io/coti-wallet-plugin'; + +export function Root() { + return ( + + + + + + ); +} + +export function HeaderUnlockButton() { + const privateUnlock = usePrivateUnlock(); + + return ( + + ); +} +``` + +See the [Integration Guide](integration-guide.md) for the full provider setup, unlock flow, and private action guards. + +## dApp API contract + +dApps should **not** handle AES keys in the normal integration path. Use the plugin provider and hook APIs to connect wallets, onboard/unlock private balances, and execute private operations. The plugin owns AES retrieval, backup restore, Snap storage, and Snap-backed decrypt/encrypt calls internally. + +For MetaMask Snap wallets, runtime private operations use Snap RPCs without extracting the AES key from Snap. For non-Snap wallets, the plugin keeps any recovered key in plugin session state only as needed. + +## Example app + +See [Example App](example-app.md) for setup and run instructions. Source code lives in the [coti-wallet-plugin GitHub repository](https://github.com/coti-io/coti-wallet-plugin/tree/main/examples). + +## Next steps + +* [Integration Guide](integration-guide.md) — provider setup, unlock flow, lock semantics +* [Configuration](configuration.md) — `configureCotiPlugin()` and onboarding services +* [API Reference](api-reference.md) — hooks, providers, types, and error codes +* [AES Key Onboarding](aes-key-onboarding.md) — onboarding routes, contract flow, security +* [Onboard Modal Theming](onboard-modal-theme.md) — customize the onboarding UI +* [Example App](example-app.md) — runnable reference dApp diff --git a/build-on-coti/tools/coti-wallet-plugin/aes-key-onboarding.md b/build-on-coti/tools/coti-wallet-plugin/aes-key-onboarding.md new file mode 100644 index 0000000..bae3af2 --- /dev/null +++ b/build-on-coti/tools/coti-wallet-plugin/aes-key-onboarding.md @@ -0,0 +1,85 @@ +# AES Key Onboarding + +Onboarding retrieves or restores the wallet-bound AES key used to decrypt private token balances. The active key is kept in React session state, while optional restore/save services can store an encrypted backup outside the session. + +Application UI should use the provider-level private unlock controller: + +```tsx + + + +``` + +Then call `usePrivateUnlock().unlock()`, `usePrivateUnlock().lock()`, or `usePrivateUnlock().requireUnlock(action)` from app components. Do not orchestrate unlock directly with `refreshPrivateBalances({ restoreOnly: true })` or a locally owned `OnboardModal`. + +## Onboarding routes + +| Wallet | First route | Fallback | +| --- | --- | --- | +| MetaMask with Snap | Retrieve AES key from Snap | Contract onboarding if Snap is empty or unavailable | +| Other wallets / MetaMask without usable Snap | Encrypted backup restore, then contract onboarding | Manual AES key input in `OnboardModal` if the host supplies `onManualAesKeySubmit` | + +## Contract onboarding flow + +1. Caller invokes the AES key provider with the connected wallet address. +2. If `onboardingServices.fetchEncryptedAesBackup` is configured, the plugin emits `restoring-backup`, fetches the encrypted backup, asks the wallet for the EIP-712 restore signature, and decrypts it. +3. If `restoreOnly` is true and no backup is restored, the flow returns without contract onboarding. +4. The plugin switches to COTI mainnet or testnet when needed, muting chain-change reactions during the temporary switch. +5. If `grantNativeCoti` is configured and the wallet balance is below the minimum threshold, the plugin calls the grant service and polls native COTI balance. +6. The plugin creates a `@coti-io/coti-ethers` `BrowserProvider` and signer. +7. `signer.generateOrRecoverAes()` runs the onboarding flow. The wallet sees the message signature and, for first-time onboarding, the on-chain transaction. +8. The plugin reads and validates the AES key, then switches the wallet back to the original chain when applicable. +9. For MetaMask, the plugin attempts to persist the key into the Snap if the origin is allowed. +10. If **Save Locally** is enabled and backup save callbacks are configured, the plugin encrypts the AES key and calls `saveEncryptedAesBackup` or `replaceEncryptedAesBackup`. When Snap storage is used for onboarding, encrypted backup save is skipped (the Snap already holds the key). +11. The flow completes and returns the AES key. Unlock is only treated as successful after private balances refresh with the session key. + +## Save Locally and progress UI + +Non-Snap wallets show a **Save Locally** switch card in `OnboardModal` (encrypted backup blob; restore still needs a wallet signature). MetaMask Snap onboarding hides that switch because the Snap persists the key. + +| Situation | Persist step in progress UI | Encrypted backup save | +| --- | --- | --- | +| Non-Snap, **Save Locally** on | Shown (`persisting-key` / `saving-backup`) | Runs when backup services are configured | +| Non-Snap, **Save Locally** off | Hidden | Skipped | +| Snap onboarding | Shown (Snap always persists the key) | Skipped; key goes to Snap | + +## Example app services + +The [example app](example-app.md) configures `onboardingServices` with `mode: 'custom'`. + +| Env var | Behavior | +| --- | --- | +| `VITE_AES_BACKUP_API_URL` | Uses `GET /aes-backups/:chainId/:address` and `PUT /aes-backups/:chainId/:address`, and also keeps a localStorage encrypted backup copy | +| unset `VITE_AES_BACKUP_API_URL` | Uses `localStorage` only for encrypted backup blobs | +| `VITE_GRANT_API_URL_TESTNET` | Testnet `grantNativeCoti` POST URL | +| `VITE_GRANT_API_URL_MAINNET` | Mainnet `grantNativeCoti` POST URL | +| one grant URL unset | Skips grant API calls on that chain; onboarding falls through to the normal insufficient-funds path if the wallet has no native COTI | +| both grant URLs unset | Does not configure a grant service on either chain; onboarding requires the wallet to already have native COTI for gas | + +The example keeps the active AES key in memory. LocalStorage is only a sample backing store for the encrypted backup blob, not the live session key. + +Manual AES key input uses the same encrypted backup helper as contract onboarding when **Save Locally** is on: the user signs the backup context, the key is encrypted, and the encrypted blob is saved through the configured API or localStorage path. If the user rejects that backup signature, the plugin cancels local save, skips the AES key success screen, and still completes unlock with the session key when balance refresh succeeds. + +## Security properties + +1. The active AES key is session-only React state and is wallet-bound to prevent cross-account leakage. +2. Encrypted backups are optional and host-defined through `configureCotiPlugin`. The user can turn **Save Locally** off for non-Snap flows. +3. Backup restore requires a wallet signature, so a stored blob alone is not enough to recover the AES key. +4. Manual AES key input is session-only unless **Save Locally** (or Snap persistence) stores it. +5. Locking private balances clears plaintext session AES state and hides balances. Snap keys and encrypted backups remain intact for the next unlock. + +## Error handling + +* User rejection during Snap, restore signature, chain switch, or onboarding returns without storing a key. +* Backup restore failures fall through to contract onboarding and set a non-blocking warning. +* Backup save failures do not block a successful onboarding; the user receives a warning. +* Rejecting the manual **Save Locally** backup signature cancels local save and skips the success screen; unlock can still finish if private balances refresh. +* Unlock does not succeed if private balances fail to refresh after the AES key is available. +* Grant API HTTP errors are treated as skipped grants. The onboarding transaction still needs native COTI gas, so an unfunded wallet fails through the normal insufficient-balance path. +* Invalid AES key format sets an onboarding error state. + +## Related docs + +* [Integration Guide](integration-guide.md) +* [Configuration](configuration.md) +* [Example App](example-app.md) diff --git a/build-on-coti/tools/coti-wallet-plugin/api-reference.md b/build-on-coti/tools/coti-wallet-plugin/api-reference.md new file mode 100644 index 0000000..59e24f3 --- /dev/null +++ b/build-on-coti/tools/coti-wallet-plugin/api-reference.md @@ -0,0 +1,305 @@ +# API Reference + +Public exports from `@coti-io/coti-wallet-plugin`. + +## Providers + +### `WagmiRainbowKitProvider` + +Sets up wagmi v2, RainbowKit, and multi-wallet connectors (MetaMask EIP-6963, Rabby, OneKey, Zerion). + +```tsx + + {children} + +``` + +Also exports `getWagmiConfig()`, `wagmiConfig`, and `WagmiConfigOptions`. + +### `PrivacyBridgeProvider` + +Core provider for wallet, network, tokens, unlock, swap, and PoD state. Nests `PrivateUnlockProvider` internally. + +```tsx + + {children} + +``` + +**Props:** + +| Prop | Type | Description | +| --- | --- | --- | +| `children` | `React.ReactNode` | App content | +| `privateUnlock` | `PrivateUnlockProviderOptions` | Unlock modal theme, warning text, callbacks | + +### `PrivateUnlockProvider` + +Mounted internally by `PrivacyBridgeProvider`. Exposed for advanced use cases only. + +## Hooks — unlock + +### `usePrivateUnlock()` + +High-level unlock controller. Preferred API for app UI. + +| Property / method | Type | Description | +| --- | --- | --- | +| `isUnlocked` | `boolean` | Private balances are visible (requires successful balance refresh, not only an AES key) | +| `isUnlocking` | `boolean` | Unlock/onboarding in progress | +| `unlock()` | `() => Promise` | Start unlock flow; succeeds only after private balances refresh | +| `lock()` | `() => void` | Hide private balances | +| `toggleLock()` | `() => void` | Toggle lock state | +| `requireUnlock(action)` | `(action?) => Promise` | Ensure unlock, then run action | +| `reset()` | `() => void` | Reset unlock UI state | + +### `usePrivacyBridgeUnlock()` + +Low-level unlock and private crypto operations. Use inside or after `requireUnlock`. + +| Property / method | Type | Description | +| --- | --- | --- | +| `isPrivateUnlocked` | `boolean` | Private balances visible | +| `sessionAesKey` | `string \| null` | Session-bound AES key (avoid direct use) | +| `aesKeyChainId` | `number \| undefined` | COTI chain for AES state | +| `hasSnap` | `boolean` | Snap detected | +| `sendPrivateToken` | `(params) => Promise<{ txHash }>` | Send a private token transfer | +| `encryptPrivateValue` | `(params) => Promise<{ ciphertext }>` | Encrypt amount to ctUint256 JSON | +| `decryptPrivateValue` | `(params) => Promise<{ amount }>` | Decrypt ctUint256 JSON to amount | +| `lockPrivateBalances` | `() => void` | Hide balances, clear session key | +| `refreshPrivateBalances` | `(options?) => Promise` | Low-level balance refresh (do not use for unlock UI) | + +**`sendPrivateToken` params:** + +```typescript +{ symbol: string; recipient: string; amount: string } +``` + +**`encryptPrivateValue` / `decryptPrivateValue` params:** + +```typescript +{ amount: string; decimals?: number } // encrypt +{ ciphertext: string; decimals?: number } // decrypt +``` + +## Hooks — bounded context slices + +Prefer these over the legacy flat `usePrivacyBridgeContext()` for new code. + +### `usePrivacyBridgeWallet()` + +| Property / method | Type | Description | +| --- | --- | --- | +| `isConnected` | `boolean` | Wallet connected | +| `walletAddress` | `string` | Connected address | +| `handleConnect` | `() => Promise` | Open connect flow | +| `handleDisconnect` | `() => Promise` | Disconnect wallet | +| `metamaskDetected` | `boolean` | MetaMask provider detected | + +### `usePrivacyBridgeNetwork()` + +| Property / method | Type | Description | +| --- | --- | --- | +| `chainId` | `string \| null` | Current chain ID | +| `switchNetwork` | `(chainId) => Promise` | Switch wallet network | +| `networkName` | `string` | Human-readable network name | +| `isUnsupportedNetwork` | `boolean` | Chain not supported | +| `enforceNetwork` | `() => Promise` | Prompt network switch | + +### `usePrivacyBridgeTokens()` + +| Property | Type | Description | +| --- | --- | --- | +| `publicTokens` | `Token[]` | Public token balances | +| `privateTokens` | `Token[]` | Private token balances (after unlock) | + +### `usePrivacyBridgeSwap()` + +| Property / method | Type | Description | +| --- | --- | --- | +| `amount` | `string` | Bridge amount input | +| `direction` | `'to-private' \| 'to-public'` | Bridge direction | +| `selectedTokenIndex` | `number` | Selected token index | +| `handleSwap` | `(amount?, direction?, tokenIndex?, onProgress?) => Promise` | Execute bridge | +| `isBridgingLoading` | `boolean` | Bridge in progress | +| `isApprovalNeeded` | `boolean` | ERC20 approval required | +| `handleApprove` | `() => Promise` | Approve ERC20 spend | +| `estimatedGasFee` | `string \| null` | Estimated gas fee display | +| `portalFeeCoti` | `string \| null` | COTI bridge portal fee | +| `portalFee` | `string \| null` | PoD portal fee (ETH/AVAX) | +| `isPodChain` | `boolean` | Connected chain uses PoD portal | + +### `usePrivacyBridgePod()` + +| Property / method | Type | Description | +| --- | --- | --- | +| `podRequests` | `PodPortalRequest[]` | Tracked PoD portal requests | +| `refreshPodRequest` | `(request) => Promise` | Refresh request status | + +### `usePrivacyBridgeModals()` + +| Property | Type | Description | +| --- | --- | --- | +| `showInstallModal` | `boolean` | Snap install modal visible | +| `showMultipleWalletsModal` | `boolean` | Multiple wallets conflict modal | + +### `usePrivacyBridgeContext()` (legacy) + +Flat union of all slices. Existing consumers may continue using this; new code should prefer bounded hooks. + +## Hooks — utilities + +| Hook | Description | +| --- | --- | +| `useWalletType()` | Detect wallet type (MetaMask, Rabby, etc.) | +| `usePrivateTokenBalance()` | Fetch and decrypt a single private token balance | +| `useBalanceUpdater()` | Balance refresh utility | +| `useNetworkEnforcer()` | Network enforcement helper | +| `useMetamask()` | MetaMask provider helper | +| `useConnectModal()` | Re-exported from RainbowKit | + +## Components + +### `OnboardModal` + +Onboarding UI component. **Do not render directly** for unlock — `PrivateUnlockProvider` mounts it. + +Non-Snap flows show a **Save Locally** switch for optional encrypted backup; Snap onboarding hides it and still shows the persist progress step. Exports `onboardModalDefaultStyles` and `ONBOARD_MODAL_STYLE_KEYS` for theming. See [Onboard Modal Theming](onboard-modal-theme.md) and [AES Key Onboarding](aes-key-onboarding.md). + +### `NetworkGuard` + +Blocks children when the wallet is on an unsupported network. + +```tsx +}> + {children} + +``` + +## Configuration + +| Export | Description | +| --- | --- | +| `configureCotiPlugin(config)` | Set plugin configuration at startup | +| `getPluginConfig()` | Read current configuration | +| `getSnapRequestParams(snapId?, snapVersion?)` | Params for `wallet_requestSnaps` | +| `isSnapInstallEnabled()` | Whether Snap install is allowed | + +See [Configuration](configuration.md) for all `CotiPluginConfig` options. + +## Chain registry + +| Export | Description | +| --- | --- | +| `CHAIN_CONFIGS` | All supported chain configurations | +| `getChainConfig(chainId)` | Lookup chain config | +| `getTokensForChain(chainId)` | Token list for a chain | +| `getUnlockStrategyForChain(chainId)` | Chain-config metadata (`snap` or `manual-aes-key`); not the runtime unlock order — see [AES Key Onboarding](aes-key-onboarding.md) | +| `cotiMainnet`, `cotiTestnet`, `sepolia` | viem chain definitions | +| `COTI_MAINNET_CHAIN_ID`, `COTI_TESTNET_CHAIN_ID`, `SEPOLIA_CHAIN_ID` | Chain ID constants | + +## Contracts and tokens + +| Export | Description | +| --- | --- | +| `CONTRACT_ADDRESSES` | Contract addresses per chain | +| `SUPPORTED_TOKENS` | Supported token configurations | +| `getPublicTokensForChain(chainId)` | Public tokens for a chain | +| `getPrivateTokensForChain(chainId)` | Private tokens for a chain | +| `TOKEN_ABI`, `BRIDGE_ABI`, `ERC20_ABI` | Contract ABIs | +| `LIMITS` | Bridge amount limits | + +## Error handling + +### `CotiPluginError` + +```typescript +class CotiPluginError extends Error { + readonly code: CotiErrorCode; + readonly detail?: string; +} +``` + +### Type guards + +```typescript +isCotiPluginError(error: unknown): error is CotiPluginError +hasCotiErrorCode(error: unknown, code: CotiErrorCode): boolean +``` + +### Error codes + +#### Wallet / provider + +| Code | Description | +| --- | --- | +| `METAMASK_NOT_INSTALLED` | MetaMask or EIP-1193 provider not installed | +| `NO_PROVIDER` | No EIP-1193 provider on `window.ethereum` | +| `USER_REJECTED` | User rejected wallet request (EIP-1193 code 4001) | + +#### Snap + +| Code | Description | +| --- | --- | +| `SNAP_CONNECT_FAILED` | Snap not installed or connection failed | +| `SNAP_DIALOG_REJECTED` | User dismissed Snap dialog | +| `SNAP_REQUIRED` | Snap required but unavailable for wallet type | +| `SNAP_KEY_CHECK_FAILED` | Snap key existence check failed | + +#### AES key / onboarding + +| Code | Description | +| --- | --- | +| `AES_KEY_MISMATCH` | AES key does not match on-chain account | +| `AES_KEY_MISSING` | AES key missing or not provided | +| `ACCOUNT_NOT_ONBOARDED` | Account never onboarded to COTI | +| `ONBOARDING_INCOMPLETE` | Onboarding did not complete | + +#### Network + +| Code | Description | +| --- | --- | +| `UNSUPPORTED_NETWORK` | Connected to unsupported chain | +| `WALLETCONNECT_PROJECT_ID_MISSING` | WalletConnect project ID not configured | + +#### Bridge / transaction + +| Code | Description | +| --- | --- | +| `INSUFFICIENT_BALANCE` | Insufficient token balance | +| `INSUFFICIENT_ALLOWANCE` | ERC20 allowance too low | +| `CONTRACT_NOT_FOUND` | Contract address not found for chain | +| `TRANSACTION_REVERTED` | On-chain transaction reverted | +| `ORACLE_TIMESTAMP_MISMATCH` | Stale oracle price data | + +#### Other + +| Code | Description | +| --- | --- | +| `API_ERROR` | External API returned non-success status | +| `VALIDATION_ERROR` | Input validation failed | + +## Logging + +| Export | Description | +| --- | --- | +| `logger` | Plugin logger instance | +| `setDebugLogging(enabled)` | Toggle debug logging | + +Logging is silent by default. Enable via `configureCotiPlugin({ debug: true })`. Secrets are never logged. + +## Utilities + +| Export | Description | +| --- | --- | +| `formatTokenBalanceDisplay` | Format token balance for display | +| `truncateDecimalValue` | Truncate decimal values | +| `getEthereumProvider()` | Resolve EIP-1193 provider | +| `muteChainUpdates()` / `unmuteChainUpdates()` | Suppress UI during cross-chain onboarding | +| `isMultipleWalletsError(error)` | Detect multiple-wallet conflict | + +## Related docs + +* [Integration Guide](integration-guide.md) +* [Configuration](configuration.md) +* [Onboard Modal Theming](onboard-modal-theme.md) diff --git a/build-on-coti/tools/coti-wallet-plugin/configuration.md b/build-on-coti/tools/coti-wallet-plugin/configuration.md new file mode 100644 index 0000000..02f3461 --- /dev/null +++ b/build-on-coti/tools/coti-wallet-plugin/configuration.md @@ -0,0 +1,193 @@ +# Configuration + +Configure the COTI Wallet Plugin at initialization time via `configureCotiPlugin()`. Call this **before** rendering any plugin hooks. + +```tsx +import { configureCotiPlugin } from '@coti-io/coti-wallet-plugin'; + +configureCotiPlugin({ + snapId: 'npm:@coti-io/coti-snap', + aesKeyChainId: 7082400, + walletConnectProjectId: 'your-walletconnect-project-id', + debug: false, +}); +``` + +## `CotiPluginConfig` options + +| Option | Type | Default | Description | +| --- | --- | --- | --- | +| `snapId` | `string` | `'npm:@coti-io/coti-snap'` | COTI MetaMask Snap ID | +| `snapVersion` | `string` | — | Optional Snap version for `wallet_requestSnaps` | +| `snapInstallEnabled` | `boolean` | `true` | When `false`, skip `wallet_requestSnaps` install/connect | +| `defaultNetworkId` | `string` | — | Enforce a specific network chain ID | +| `sepoliaRpcUrl` | `string` | — | Sepolia RPC URL for PoD portal operations | +| `cotiTestnetRpcUrl` | `string` | — | COTI testnet RPC URL for PoD SDK tracking | +| `walletConnectProjectId` | `string` | — | WalletConnect Cloud project ID for RainbowKit | +| `debug` | `boolean` | `false` | Enable verbose internal logging (secrets are never logged) | +| `clearSessionKeyOnWagmiDisconnect` | `boolean` | `false` | Clear in-memory AES key on wagmi disconnect | +| `onboardingServices` | `OnboardingServices` | `{ mode: 'disabled' }` | Grant and encrypted backup service hooks | +| `aesKeyChainId` | `7082400 \| 2632500` | — | COTI chain that owns AES onboarding state | +| `onboardingGrantMinBalanceWei` | `BigNumberish` | `0` | Native COTI threshold before contract onboarding | +| `onboardingGrantPollIntervalMs` | `number` | `2000` | Polling interval after grant callback | +| `onboardingGrantTimeoutMs` | `number` | `60000` | Max wait time after grant callback | +| `additionalSnapAesWriteOrigins` | `string[]` | `[]` | Extra origins allowed to call Snap `set-aes-key` | + +{% hint style="info" %} +`aesKeyChainId` accepts only COTI Testnet (`7082400`) or COTI Mainnet (`2632500`). Only COTI chains can hold AES keys. +{% endhint %} + +## Onboarding services + +Optional host-implemented callbacks for encrypted AES backup storage and native COTI gas grants during contract onboarding. + +```tsx +configureCotiPlugin({ + onboardingServices: { + mode: 'custom', + fetchEncryptedAesBackup: async ({ address, chainId }) => { + const res = await fetch(`/aes-backups/${chainId}/${address}`); + if (res.status === 404) return null; + return res.json(); + }, + saveEncryptedAesBackup: async ({ address, chainId, backup }) => { + await fetch(`/aes-backups/${chainId}/${address}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(backup), + }); + }, + grantNativeCoti: async ({ address, chainId }) => { + const res = await fetch('https://your-grant-api.example.com', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ address, chainId }), + }); + return res.json(); + }, + }, +}); +``` + +### `OnboardingServices` modes + +| Mode | Behavior | +| --- | --- | +| `disabled` | No grant or backup features (default) | +| `custom` | Use the provided callback functions | +| `official` | Reserved for stable COTI-hosted APIs | + +### Callback reference + +| Callback | Purpose | +| --- | --- | +| `fetchEncryptedAesBackup` | `GET /aes-backups/:chainId/:address` — returns `EncryptedAesBackup` or `null` | +| `saveEncryptedAesBackup` | `PUT /aes-backups/:chainId/:address` — stores encrypted backup | +| `replaceEncryptedAesBackup` | Replace an existing encrypted backup | +| `grantNativeCoti` | `POST` with `{ address, chainId }` — funds wallet for onboarding gas | + +### `EncryptedAesBackup` shape + +```typescript +interface EncryptedAesBackup { + version: 1; + address: string; + chainId: number; + signatureKind: 'eip712'; + iv: string; + ciphertext: string; + createdAt: string; +} +``` + +Backup restore requires a wallet EIP-712 signature — a stored blob alone is not enough to recover the AES key. + +### `GrantResult` shape + +```typescript +interface GrantResult { + txHash?: string; + amountWei?: string; + status?: 'submitted' | 'funded' | 'skipped'; +} +``` + +## Snap configuration + +### Production Snap + +```tsx +configureCotiPlugin({ + snapId: 'npm:@coti-io/coti-snap', + snapInstallEnabled: true, +}); +``` + +### Local Snap development + +When developing against a local `coti-snap` server: + +```tsx +configureCotiPlugin({ + snapId: 'local:http://localhost:8080', + snapInstallEnabled: true, +}); +``` + +### Disable Snap install prompts + +If the Snap is pre-installed and you want to skip install/connect prompts: + +```tsx +configureCotiPlugin({ + snapInstallEnabled: false, +}); +``` + +An already-installed Snap can still be used for AES key retrieval. + +### Additional Snap write origins + +Whitelist dApp domains that call Snap `set-aes-key` outside the published COTI portals: + +```tsx +configureCotiPlugin({ + additionalSnapAesWriteOrigins: ['https://portal.example.com'], +}); +``` + +The Snap manifest's `allowedOrigins` must also include these domains. + +## Example app environment variables + +The [example app](https://github.com/coti-io/coti-wallet-plugin/tree/main/examples) uses Vite env vars for local development. Production dApps should use `configureCotiPlugin()` instead. + +| Variable | Purpose | +| --- | --- | +| `VITE_WALLETCONNECT_PROJECT_ID` | WalletConnect Cloud project ID | +| `VITE_SNAP_ID` | Local Snap dev (`local:http://localhost:8080`) | +| `VITE_COTI_SNAP_INSTALL_ENABLED` | Enable/disable Snap install | +| `VITE_AES_BACKUP_API_URL` | Remote encrypted backup API base URL | +| `VITE_GRANT_API_URL_TESTNET` | Testnet native COTI grant API | +| `VITE_GRANT_API_URL_MAINNET` | Mainnet native COTI grant API | +| `VITE_ONBOARDING_GRANT_MIN_BALANCE_COTI` | Grant threshold (default `0.2`) | + +Run the example with local Snap: + +```bash +npm run dev:local-snap +``` + +This starts the local `coti-snap` server, Snap companion dApp, and wallet example with `VITE_SNAP_ID=local:http://localhost:8080`. + +## Security notes + +* The active AES key lives in session-only React state, wallet-bound to prevent cross-account leakage. +* Encrypted backups are optional and host-defined. +* `debug: true` enables verbose logging but **never** logs secret material (AES keys, ciphertext, signatures). +* Set `clearSessionKeyOnWagmiDisconnect: true` for stricter shared-browser security at the cost of re-fetching the key on reconnect. + +## Related docs + +* [Integration Guide](integration-guide.md) +* [API Reference](api-reference.md) diff --git a/build-on-coti/tools/coti-wallet-plugin/example-app.md b/build-on-coti/tools/coti-wallet-plugin/example-app.md new file mode 100644 index 0000000..7650056 --- /dev/null +++ b/build-on-coti/tools/coti-wallet-plugin/example-app.md @@ -0,0 +1,69 @@ +# Example App + +A minimal React app ships in the [coti-wallet-plugin GitHub repository](https://github.com/coti-io/coti-wallet-plugin/tree/main/examples). It connects a wallet via RainbowKit and displays public and private token balances from the [COTI Token List](https://github.com/coti-io/coti-token-list). + +
COTI Wallet Plugin example dApp

Example dApp with Connect Wallet and unlock controls

+ +## Prerequisites + +* Node.js 18+ + +## Setup + +```bash +git clone https://github.com/coti-io/coti-wallet-plugin.git +cd coti-wallet-plugin/examples +cp .env.example .env +``` + +Edit `.env` and add your WalletConnect project ID (get one at https://cloud.walletconnect.com): + +``` +VITE_WALLETCONNECT_PROJECT_ID=your_project_id_here +``` + +## Run + +```bash +npm run dev +``` + +This installs dependencies in the plugin root and examples, rebuilds the wallet plugin, then starts Vite. Use `npm run dev:vite` to skip install/build and start Vite only. + +Opens at http://localhost:5173 + +Use the **Light mode / Dark mode** button in the header to preview onboard-modal theming, including the **Save Locally** switch card. The example passes `privateUnlock.theme` built from `src/onboardTheme.ts` — the same pattern host apps should use. + +### Local Snap development + +To run against a local `coti-snap` server: + +```bash +# Requires coti-snap cloned as ../coti-snap with yarn install done +npm run dev:local-snap +``` + +This starts: + +* `coti-snap` watch server at http://localhost:8080 +* Snap companion dApp at http://localhost:8000 +* Wallet example at http://localhost:5173 with `VITE_SNAP_ID=local:http://localhost:8080` + +Override the snap checkout path with `COTI_SNAP_ROOT` if needed. + +## What it does + +1. **Connect Wallet** — Opens the RainbowKit modal (MetaMask, Coinbase, WalletConnect, etc.) +2. **Public Balances** — Reads on-chain ERC20 `balanceOf` for all public tokens on the connected chain +3. **Native COTI** — Displays native COTI balance via wagmi +4. **Private Balances** — Click **Unlock Private Balances** to derive the AES key, then decrypted private token balances appear + +## Network + +The app targets **COTI Testnet** (chain ID 7082400) by default. Switch your wallet to COTI Testnet to see token balances. + +## Related docs + +* [Integration Guide](integration-guide.md) +* [Configuration](configuration.md) +* [AES Key Onboarding](aes-key-onboarding.md) diff --git a/build-on-coti/tools/coti-wallet-plugin/integration-guide.md b/build-on-coti/tools/coti-wallet-plugin/integration-guide.md new file mode 100644 index 0000000..953e14b --- /dev/null +++ b/build-on-coti/tools/coti-wallet-plugin/integration-guide.md @@ -0,0 +1,306 @@ +# Integration Guide + +This guide walks through integrating the COTI Wallet Plugin into a React/wagmi dApp. + +**Principle:** The provider owns the unlock flow. App pages call commands. Do not build custom unlock routing. + +## 1. Wrap your app once + +Mount `WagmiRainbowKitProvider` and `PrivacyBridgeProvider` near the root of your app: + +```tsx +import { + PrivacyBridgeProvider, + WagmiRainbowKitProvider, + type OnboardModalTheme, +} from '@coti-io/coti-wallet-plugin'; + +const onboardTheme: OnboardModalTheme = { + modal: { backgroundColor: '#ffffff', color: '#0f172a' }, + title: { color: '#0f172a' }, + description: { color: '#64748b' }, + primaryButton: { backgroundColor: '#1E29F6', color: '#ffffff' }, + saveOptionTitle: { color: '#0f172a' }, + saveOptionDescription: { color: '#64748b' }, + tooltipButton: { color: '#64748b' }, +}; + +export function Root() { + return ( + + { + // Optional: show "User canceled" toast. + }, + }} + > + + + + ); +} +``` + +{% hint style="info" %} +Do **not** render `` yourself. `PrivacyBridgeProvider` mounts the modal once internally via `PrivateUnlockProvider`. +{% endhint %} + +### Optional: network guard + +Wrap page content with `NetworkGuard` to show a fallback UI when the wallet is on an unsupported chain: + +```tsx +import { NetworkGuard } from '@coti-io/coti-wallet-plugin'; + +export function App() { + return ( + Please switch to a supported network.

}> + +
+ ); +} +``` + +## 2. Configure the plugin (before rendering hooks) + +Call `configureCotiPlugin()` once at app startup, before any plugin hooks run: + +```tsx +import { configureCotiPlugin } from '@coti-io/coti-wallet-plugin'; + +configureCotiPlugin({ + snapId: 'npm:@coti-io/coti-snap', + aesKeyChainId: 7082400, // COTI Testnet + walletConnectProjectId: 'your-project-id', + debug: false, +}); +``` + +See [Configuration](configuration.md) for all options. + +## 3. Add an unlock control + +Use `usePrivateUnlock()` for unlock orchestration in your UI: + +```tsx +import { usePrivateUnlock } from '@coti-io/coti-wallet-plugin'; + +export function HeaderUnlockButton() { + const privateUnlock = usePrivateUnlock(); + + return ( + + ); +} +``` + +### `usePrivateUnlock()` API + +| Method / property | Description | +| --- | --- | +| `isUnlocked` | `true` when private balances are visible | +| `isUnlocking` | `true` while unlock/onboarding is in progress | +| `unlock()` | Start the unlock flow (succeeds only after private balances refresh) | +| `lock()` | Hide private balances | +| `toggleLock()` | Toggle between locked and unlocked | +| `requireUnlock(action)` | Run `action` after ensuring unlock; returns `true` if unlocked | +| `reset()` | Reset unlock UI state | + +## 4. Guard private actions + +Use `requireUnlock(action)` for any operation that needs private balance or key access. It tries the cached session key first, then backup/Snap restore, then the onboarding modal only if needed. + +```tsx +import { + usePrivateUnlock, + usePrivacyBridgeUnlock, +} from '@coti-io/coti-wallet-plugin'; + +export function EncryptButton() { + const privateUnlock = usePrivateUnlock(); + const unlock = usePrivacyBridgeUnlock(); + + const encrypt = async () => { + const result = await unlock.encryptPrivateValue({ + amount: '1.0', + decimals: 18, + }); + console.log(result.ciphertext); + }; + + return ( + + ); +} +``` + +## 5. Display token balances + +Use the bounded context hooks to read wallet and token state: + +```tsx +import { + usePrivacyBridgeWallet, + usePrivacyBridgeTokens, +} from '@coti-io/coti-wallet-plugin'; + +export function BalancePanel() { + const { isConnected, walletAddress } = usePrivacyBridgeWallet(); + const { publicTokens, privateTokens } = usePrivacyBridgeTokens(); + + if (!isConnected) return

Connect your wallet

; + + return ( +
+

Address: {walletAddress}

+

Public tokens

+ {publicTokens.map((t) => ( +
{t.symbol}: {t.balance}
+ ))} +

Private tokens

+ {privateTokens.map((t) => ( +
{t.symbol}: {t.balance}
+ ))} +
+ ); +} +``` + +Private balances appear only after unlock. Public balances are always readable on-chain. + +## 6. Bridge public ↔ private tokens + +Use the swap context for portal operations: + +```tsx +import { usePrivacyBridgeSwap } from '@coti-io/coti-wallet-plugin'; + +export function BridgeForm() { + const { + amount, + setAmount, + direction, + setDirection, + handleSwap, + isBridgingLoading, + estimatedGasFee, + } = usePrivacyBridgeSwap(); + + return ( +
+ setAmount(e.target.value)} /> + + + {estimatedGasFee &&

Estimated gas: {estimatedGasFee}

} +
+ ); +} +``` + +On COTI chains, bridging uses the native COTI bridge. On Sepolia and Avalanche Fuji, bridging routes through the PoD Privacy Portal. + +## Lock semantics + +Understanding lock behavior is important for both UX and security: + +``` +lock() + → balances hidden + → session AES key kept in memory + +unlock() + → try cached session key + → try restore backup / Snap + → open onboarding modal only if restore fails + → succeed only after private balances refresh +``` + +{% hint style="warning" %} +`isUnlocked` (or `isPrivateUnlocked`) means **private balances are visible** — it does not guarantee a key exists in Snap or backup storage. Do not use it to infer onboarding state. Having an AES key alone is not enough; unlock requires a successful private balance refresh. +{% endhint %} + +Contract onboarding normally ends on the plugin success screen. The user can reveal/copy the raw AES key, then click Done. Any pending action passed to `requireUnlock` runs after Done. + +Non-Snap flows show a **Save Locally** switch for optional encrypted backup. Snap onboarding hides that switch and still shows the persist step because the Snap always stores the key. If the user rejects the manual backup signature while Save Locally is on, the plugin skips the success screen and completes unlock when balance refresh succeeds. See [AES Key Onboarding](aes-key-onboarding.md). + +## Onboarding routes + +| Wallet | First route | Fallback | +| --- | --- | --- | +| MetaMask with Snap | Retrieve AES key from Snap | Contract onboarding if Snap is empty | +| Other wallets / MetaMask without Snap | Encrypted backup restore | Contract onboarding, then manual AES key input | + +See [Configuration](configuration.md) for encrypted backup and grant service setup, and [AES Key Onboarding](aes-key-onboarding.md) for the full contract onboarding flow. + +## Do + +* Use `PrivacyBridgeProvider privateUnlock={...}` once near app root. +* Use `usePrivateUnlock()` for unlock orchestration: `unlock()`, `lock()`, `toggleLock()`, `requireUnlock(action)`. +* Use `usePrivacyBridgeUnlock()` inside or after that guard for private operations: `sendPrivateToken`, `encryptPrivateValue`, `decryptPrivateValue`. +* Let the plugin own `OnboardModal`, Snap install, restore-only flow, contract onboarding, and the AES key success screen. + +## Do not + +* Do not render `OnboardModal` for unlock in app pages. +* Do not call `refreshPrivateBalances({ restoreOnly: true })` as a custom unlock flow. +* Do not infer key existence from `isPrivateUnlocked`; it only means private balances are visible. +* Do not delete Snap-stored keys or encrypted backups on lock. Lock only clears plaintext session state. + +## Error handling + +The plugin throws typed `CotiPluginError` instances with structured `CotiErrorCode` values. See [API Reference — Error codes](api-reference.md#error-codes) for the full list. + +```tsx +import { + CotiPluginError, + CotiErrorCode, + isCotiPluginError, +} from '@coti-io/coti-wallet-plugin'; + +try { + await privateUnlock.unlock(); +} catch (error) { + if (isCotiPluginError(error)) { + switch (error.code) { + case CotiErrorCode.SNAP_CONNECT_FAILED: + // Prompt Snap install + break; + case CotiErrorCode.USER_REJECTED: + // User cancelled — no action needed + break; + case CotiErrorCode.AES_KEY_MISMATCH: + // Prompt re-onboarding + break; + } + } +} +``` + +## Related docs + +* [Configuration](configuration.md) +* [API Reference](api-reference.md) +* [Onboard Modal Theming](onboard-modal-theme.md) +* [COTI MetaMask Snap Integration](../coti-metamask-snap/snap-integration.md) — low-level Snap RPC details diff --git a/build-on-coti/tools/coti-wallet-plugin/onboard-modal-theme.md b/build-on-coti/tools/coti-wallet-plugin/onboard-modal-theme.md new file mode 100644 index 0000000..c32eb8f --- /dev/null +++ b/build-on-coti/tools/coti-wallet-plugin/onboard-modal-theme.md @@ -0,0 +1,140 @@ +# Onboard Modal Theming + +The unlock and onboarding modal is rendered by the plugin (`PrivateUnlockProvider`). **Colors and typography are owned by the host app**, not hard-coded to match a specific product skin. + +## Wire-up + +Pass a theme object when you mount `PrivacyBridgeProvider`: + +```tsx +import { + PrivacyBridgeProvider, + type OnboardModalTheme, +} from '@coti-io/coti-wallet-plugin'; + +const lightOnboardTheme: OnboardModalTheme = { + backdrop: { backgroundColor: 'rgba(4, 19, 61, 0.35)' }, + modal: { + backgroundColor: '#ffffff', + color: '#0f172a', + border: '1px solid #e2e8f0', + }, + title: { color: '#0f172a' }, + description: { color: '#64748b' }, + saveOptionTitle: { color: '#0f172a' }, + saveOptionDescription: { color: '#64748b' }, + tooltipButton: { color: '#64748b' }, + primaryButton: { backgroundColor: '#1E29F6', color: '#ffffff' }, + cancelButton: { color: '#64748b' }, +}; + +export function AppRoot() { + return ( + + + + ); +} +``` + +{% hint style="info" %} +Do **not** render `` yourself for the unlock flow — the provider mounts it once. +{% endhint %} + +## How it works + +1. `OnboardModal` starts from `onboardModalDefaultStyles` (dark palette). +2. Your `OnboardModalTheme` partial overrides are shallow-merged per style target. +3. When a theme is provided, the plugin fills common text targets (`saveOptionTitle`, `saveOptionDescription`, `tooltipButton`, icon buttons, etc.) from your `title` / `modal` / `description` tokens if you did not set them explicitly. +4. On light modal backgrounds, the plugin also fills interactive surfaces that still use dark defaults (including the **Save Locally** card and switch tracks) so controls stay visible. + +## Style targets + +Import `ONBOARD_MODAL_STYLE_KEYS` or `onboardModalDefaultStyles` from the package for the full list. + +| Key | Used for | +| --- | --- | +| `backdrop` | Overlay behind the dialog | +| `modal` | Dialog panel (`backgroundColor`, `color`, `border`) | +| `title` | Headings on every screen | +| `description` | Body copy under the title | +| `saveOptionCard` / `saveOptionCardActive` | **Save Locally** option card (idle / enabled) | +| `saveOptionIconWrap` | Icon badge on the Save Locally card | +| `saveOptionTitle` / `saveOptionDescription` | Save Locally title and helper text | +| `saveOptionSwitchTrack` / `saveOptionSwitchTrackOn` / `saveOptionSwitchTrackOff` | Switch track base / on / off states | +| `saveOptionSwitchKnob` | Switch knob | +| `tooltipButton` / `tooltipBubble` | `?` help control and tooltip | +| `primaryButton` / `primaryButtonDisabled` | Main CTA | +| `cancelButton` | Secondary dismiss action | +| `errorBox` / `errorText` | Failure screen | +| `stepLabel` / `stepDescription` | Progress stepper | +| `aesKeyBox` / `keyInput` | Success screen key display | +| `warningBox` / `warningText` | Non-blocking warning from `privateUnlock.warning` | + +Each value is a `React.CSSProperties` object (same as inline `style`). + +{% hint style="info" %} +The **Save Locally** control is a switch card, not a checkbox. Theme the `saveOption*` keys (or rely on palette gap-filling from `title` / `description` / `modal`). +{% endhint %} + +## Minimum for light mode + +Set at least `modal`, `title`, and `description`. The plugin fills other text and light-mode surface targets from those tokens when they still use the built-in dark defaults. + +For fuller light-mode control of Save Locally, also set `saveOptionCard`, `saveOptionSwitchTrackOff`, and `saveOptionSwitchTrackOn` (see the [example app](example-app.md) `onboardTheme.ts`). + +## Light / dark toggle + +The plugin has no opinion on your theme switcher. Typical pattern: + +1. Read your app theme (`next-themes`, CSS variables, etc.). +2. Map tokens to `OnboardModalTheme`. +3. Pass the result to `privateUnlock.theme` and refresh it when the user toggles light/dark. + +```tsx +import { useMemo } from 'react'; +import { useTheme } from 'next-themes'; +import type { OnboardModalTheme } from '@coti-io/coti-wallet-plugin'; + +function useOnboardModalTheme(): OnboardModalTheme { + const { resolvedTheme } = useTheme(); + + return useMemo(() => { + if (resolvedTheme === 'light') { + return { + modal: { backgroundColor: '#ffffff', color: '#0f172a' }, + title: { color: '#0f172a' }, + description: { color: '#64748b' }, + primaryButton: { backgroundColor: '#1E29F6', color: '#ffffff' }, + }; + } + return { + modal: { backgroundColor: '#0f172a', color: '#f8fafc' }, + title: { color: '#f8fafc' }, + description: { color: '#94a3b8' }, + primaryButton: { backgroundColor: '#1E29F6', color: '#ffffff' }, + }; + }, [resolvedTheme]); +} +``` + +## Optional warning text + +Show a non-blocking warning in the modal: + +```tsx + + + +``` + +## Related docs + +* [Integration Guide](integration-guide.md) +* [Configuration](configuration.md) +* [AES Key Onboarding](aes-key-onboarding.md) diff --git a/coti-privacy-portal/developer-guide/frontend-integration.md b/coti-privacy-portal/developer-guide/frontend-integration.md index fef4d28..bdf1bc4 100644 --- a/coti-privacy-portal/developer-guide/frontend-integration.md +++ b/coti-privacy-portal/developer-guide/frontend-integration.md @@ -1,5 +1,9 @@ # Frontend Integration +{% hint style="info" %} +**Building a React/wagmi dApp?** Use the [COTI Wallet Plugin](../../build-on-coti/tools/coti-wallet-plugin/README.md) as the recommended integration path. It handles AES onboarding, unlock, balance decryption, and private operations through `PrivacyBridgeProvider` and hooks — without your dApp handling raw AES keys. The manual examples below remain useful for non-React stacks. +{% endhint %} + ## Wallet Onboarding (AES Key) Before any encrypted operation, the user must be onboarded to get their AES key. **COTI MetaMask Snap** provides key management. @@ -249,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/coti-privacy-portal/user-guide/README.md b/coti-privacy-portal/user-guide/README.md index d9f95e4..d1bd7b2 100644 --- a/coti-privacy-portal/user-guide/README.md +++ b/coti-privacy-portal/user-guide/README.md @@ -25,6 +25,7 @@ This helps keep your financial data confidential. This guide will walk you through how to: * connect your wallet to the Privacy Portal +* [onboard](../../build-on-coti/core-concepts/what-is-onboarding.md) so private features can unlock * bridge public tokens into private tokens * send and receive private transactions * view your balances securely diff --git a/privacy-on-avalanche/README.md b/privacy-on-avalanche/README.md new file mode 100644 index 0000000..59e3e54 --- /dev/null +++ b/privacy-on-avalanche/README.md @@ -0,0 +1,89 @@ +# Privacy on Avalanche (PoD on Fuji) + +**Privacy on Avalanche** applies Privacy on Demand to **Avalanche Fuji C-Chain** (chain ID `43113`): keep wallets, assets, and dApp contracts on Avalanche, and send **encrypted private computation** to **COTI Testnet**. + +Fees on the host side are paid in **AVAX**. Private execution still happens on COTI; results return as ciphertext to your Fuji contracts for client-side decryption. + +> **Development status:** This Privacy on Avalanche 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. + +
+ +

Quick Access

+ +- **[Getting started on Avalanche Fuji (Day 0)](getting-started-fuji.md)** — Wallet, faucet, Hardhat/Foundry, SnowScan smoke-test. +- **[Tutorials: PoD dApps on Avalanche (choose your integration model)](tutorials-privacy-on-avalanche.md)** — Primitive-only vs custom COTI logic, then links to step-by-step Fuji guides. +- **[Architecture and design](architecture-and-components.md)** — Inbox, MPC executor, PodUser, PodLib, and how they connect. +- **[Avalanche Fuji network & addresses](networks/fuji.md)** — Fuji C-Chain RPC, SnowScan, Inbox, Privacy Portal, and sample MpcAdder. +- **[Networks](networks/README.md)** — Fuji (host) + COTI Testnet (private execution) parameters. +- **[Interactive PoD architecture on Fuji (pod.coti.io/avalanche)](https://pod.coti.io/avalanche/index.html)** — Live demo: MpcAdder journey across Avalanche Fuji, relayer, and COTI. +- **[Learn about fees](how-poa-fees-work.md)** — How PoA/PoD fees split across COTI and Fuji (paid in AVAX). +- **[Millionaires demo](https://millionaire.demo.coti.io)** — Live demo (includes an Avalanche Fuji route). + +

Further resources

+ +- **[Examples](https://github.com/coti-io/coti-contracts/tree/main/contracts/pod/examples)** — Contract examples in `@coti-io/coti-contracts`. +- **[PoD SDK documentation](https://github.com/cotitech-io/coti-pod-sdk/tree/main/docs)** — Full SDK docs on GitHub. +- **[General Privacy on Demand section](../privacy-on-demand/README.md)** — Multi-host PoD overview (not Avalanche-only). + +
+ +--- + +This section is the **Avalanche Fuji–first** companion to Privacy on Demand: same PoD pattern, with Fuji C-Chain network constants, `PodUserFuji` presets, SnowScan explorers, and AVAX fee context. For integration, use [`@coti-io/pod-sdk`](https://www.npmjs.com/package/@coti-io/pod-sdk) (TypeScript) and [`@coti-io/coti-contracts`](https://github.com/coti-io/coti-contracts) (Solidity), plus the links below. + +## Fuji at a glance + +| Parameter | Value | +| --- | --- | +| Network | Avalanche Fuji C-Chain | +| Chain ID | `43113` | +| Native token | AVAX | +| RPC | `https://api.avax-test.network/ext/bc/C/rpc` | +| Explorer | [testnet.snowscan.xyz](https://testnet.snowscan.xyz) | +| Inbox | `0xAb625bE229F603f6BBF964474AFf6d5487e364De` | +| Private execution | COTI Testnet (`7082400`) | +| Solidity preset | `PodUserFuji` | + +Full tables: [Avalanche Fuji](networks/fuji.md). + +## Who this documentation is for + +| Audience | What you will get here | +| --- | --- | +| **Product, compliance, and business readers** | Plain-language model of privacy on Avalanche + COTI, where data lives, and what “async” private operations mean in practice. | +| **Architects and technical leads** | End-to-end diagrams for the **Fuji ↔ COTI** path, component roles, and domain boundaries. | +| **Developers** | Fuji-focused map from concepts to Solidity/TypeScript (`PodUserFuji`, AVAX fees, SnowScan), plus pointers to the authoritative SDK docs. | + +## Table of contents + +### Start here + +1. [Getting started on Avalanche Fuji (Day 0)](getting-started-fuji.md) — Wallet, faucet, Hardhat/Foundry, SnowScan. + +### Understand first (readable without writing code) + +2. [What is Privacy on Avalanche?](what-is-privacy-on-avalanche.md) — Problem, promise, and constraints with Fuji as the host chain. +3. [How a private request travels end to end](how-a-private-request-travels-end-to-end.md) — Timeline from user action on Fuji to decrypted result. +4. [Architecture and main components](architecture-and-components.md) — Where **Inbox**, **MPC executor**, **PodUser**, and **PodLib** sit, with diagrams. +5. [Networks](networks/README.md) — [Avalanche Fuji](networks/fuji.md) (host) and [COTI Testnet](networks/coti-testnet.md) (execution). +6. [Glossary](glossary.md) — Short definitions of terms you will see in PoD and SDK docs. + +### Deeper context + +7. [Async private operations (why it is not instant)](async-private-operations.md) — What “pending” means and why UX must reflect it. +8. [How do PoA fees work?](how-poa-fees-work.md) — Two-way Inbox budgets in AVAX, oracle conversion, and a worked gas-unit example. +9. [For developers: mapping concepts to the SDK](for-developers-mapping-to-the-sdk.md) — Checklists and links to the [PoD SDK documentation on GitHub](https://github.com/cotitech-io/coti-pod-sdk/tree/main/docs). + +### Tutorials (hands-on) + +10. [Tutorials: building PoD dApps on Avalanche](tutorials-privacy-on-avalanche.md) — When to use **MpcLib / PodLib** primitives vs **custom COTI + Fuji** contracts. +11. [TypeScript PoD SDK (`CotiPodCrypto`, `PodContract`)](typescript-pod-sdk.md) — Encryption/decryption, fee estimation, method calls, and request ID extraction. +12. [Cookbook: private investor allocations with PoD](cookbook-private-investor-allocations.md) — Start from a familiar public Fuji allocation dApp, then make allocation reads and withdrawals private with PoD. +13. [Tutorial: private Adder on Avalanche Fuji](tutorial-private-adder-fuji.md) — Minimal primitive-only adder: `PodUserFuji`, AVAX fees, TypeScript crypto. +14. [Tutorial: custom privacy logic with PoD](tutorial-custom-logic.md) — Encrypted messaging shape: `DirectMessageCotiSide` + Fuji orchestrator. + +## Official technical reference + +The machine-readable contracts, types, and APIs live in the open-source SDK. Treat this book chapter as the **human-oriented companion**; treat the repository as the **source of truth** for signatures, fees, and network constants: + +- [COTI PoD SDK — documentation index](https://github.com/cotitech-io/coti-pod-sdk/tree/main/docs) diff --git a/privacy-on-avalanche/architecture-and-components.md b/privacy-on-avalanche/architecture-and-components.md new file mode 100644 index 0000000..629b938 --- /dev/null +++ b/privacy-on-avalanche/architecture-and-components.md @@ -0,0 +1,85 @@ +# Architecture and main components + +## Vocabulary: PoD versus “Pod” in code + +- **PoD** — **Privacy on Demand**, the **product pattern**: private work on COTI, orchestration on your EVM chain. +- **PodUser / PodLib** — **Solidity building blocks**. They help your **dApp contract** configure routing and call common private operations. They are **not** a separate blockchain; they are **libraries and base contracts** you inherit or use. + +## High-level deployment view + +Think of **three domains**: + +1. **User device** — keys, encryption, decryption, UX. +2. **Your EVM chain** — your app’s contracts, assets, and the **Inbox** contract that speaks cross-domain. +3. **COTI execution** — private computation and the **MPC executor** contract your integration targets. + +**Inbox** sits on **your chain** and is the **trusted bridge** between your contract logic and COTI. **MPC executor** sits on **COTI** and is the **entry point** for the SDK-configured private operation flow. + +## Component diagram: your dApp contract’s perspective + +The next diagram shows **logical modules** as engineers wire them. On Avalanche Fuji, the SDK inheritance preset is **`PodUserFuji`** (Inbox + COTI Testnet routing). + +```mermaid +flowchart TB + subgraph YourContract["Your application contract (EVM)"] + AppLogic["Your business rules & storage"] + PodInboxEvm["Inbox contract (EVM)"] + end + + + subgraph CotiSyetem["Privacy Application (EVM)"] + PodInboxCoti["Inbox contract (COTI)"] + Exec["MPC executor or Custom Executor (COTI)"] + end + + AppLogic --> PodInboxEvm + PodInboxEvm <-->|"cross-domain messages"| PodInboxCoti + PodInboxCoti --> Exec +``` + +### Inbox + +- **What it is**: An on-chain **message router** and **callback** mechanism between domains. +- **Why it matters**: Without it, your EVM contract cannot **reach** COTI private execution or **receive** structured answers. +- **Analogy**: A **certified courier** between two offices: it does not replace either office, but **only** it can hand off the parcel and bring the signed reply back. + +### MPC executor (COTI) + +- **What it is**: The **COTI-side contract address** your dApp is configured to call for a given deployment. The SDK’s network presets expose this as a constant you set during construction (for example `MPC_EXECUTOR_ADDRESS` alongside `COTI_CHAIN_ID` in [Getting started](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/04-getting-started.md)). +- **Why it matters**: It anchors **where** private execution is invoked in the COTI environment for **library-style** flows. + +### PodUser + +- **What it is**: A **configuration surface** for integrators: **which Inbox**, **which COTI chain**, **which executor**. Administrative changes are expected to be **access-controlled** (`onlyOwner` patterns in the SDK). +- **Why it matters**: Lets the same codebase target **different environments** (testnet vs mainnet, future routing updates) without rewriting core logic—**if** governance is handled responsibly. + +### PodLib + +- **What it is**: **High-level helpers** for **common private operations** (for example comparisons and arithmetic at fixed bit widths—see the SDK [features](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/03-features.md) table). +- **Why it matters**: Faster path than writing a **custom** COTI contract for every operation. If you outgrow it, you move to **custom** encoding and COTI-side contracts using `MpcAbiCodec`, described in the SDK’s [Writing privacy contracts](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/05-writing-privacy-contracts-on-ethereum.md). + +## Data shapes: a non-developer mental model + +Engineers talk about **`it*`**, **`gt*`**, and **`ct*`**. At a high level: + +| Symbol (in docs) | Think of it as | Where it “lives” | +| --- | --- | --- | +| **`it*`** | **Signed, encrypted user input** bundled for submission | Built on the **client**, consumed by **your EVM contract** | +| **`gt*`** | **Private compute representation** during the operation | **Inside COTI** private execution | +| **`ct*`** | **Encrypted output** you can **store on your chain** and **decrypt client-side** | **Returned** to your contract, **read** by the user’s app | + +A fuller table lives in the SDK’s [data types](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/contracts/01-it-ct-gt-data-types.md) page. + +## 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)). +- **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 + +- **[Interactive PoD architecture on Fuji (pod.coti.io/avalanche)](https://pod.coti.io/avalanche/index.html)** — Explore the Fuji ↔ COTI picture interactively: step through **MpcAdder**, open linked contracts on GitHub, and see how fees deplete across blocks. +- [Glossary](glossary.md) — quick term lookup. +- [For developers: mapping concepts to the SDK](for-developers-mapping-to-the-sdk.md) — concrete doc links and checklists. diff --git a/privacy-on-avalanche/async-private-operations.md b/privacy-on-avalanche/async-private-operations.md new file mode 100644 index 0000000..3d0554e --- /dev/null +++ b/privacy-on-avalanche/async-private-operations.md @@ -0,0 +1,75 @@ +# Async private operations + +Privacy on Demand private calls are **asynchronous**. That single sentence drives most product and operations decisions. + +## What “async” means for users + +On a normal smart contract call, many teams think in terms of: **submit transaction → see result in the same flow**. + +With PoD, the meaningful private result usually arrives in a **second step**: + +1. **Request transaction** — your dApp contract submits work **through the Inbox** and receives or emits a **request identifier**. +2. **Wait** — COTI performs private computation. +3. **Callback transaction** — the Inbox invokes your contract’s **callback** with **encrypted outputs** (`ct*`). + +So the user’s mental model should be closer to **“I submitted a job”** than **“I got the answer immediately.”** + +## Why the platform works this way + +Private execution happens **outside** your chain’s normal synchronous EVM frame. The **Inbox** pattern exists precisely to **carry a message out** and **bring a response back** through a **controlled channel**. + +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`** 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`. + +### 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** 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 | +| --- | --- | +| **UI states** | Show **Pending / Completed / Failed** (or equivalent) per request ID. | +| **Indexing** | Expect teams to use **events**, **subgraphs**, or internal indexers to connect callbacks to user actions. | +| **Errors** | Surface **structured failure** where the SDK exposes error callbacks and codes—users need actionable next steps. | +| **Support** | Train staff that **“stuck pending”** may be **fee**, **routing**, or **downstream execution** issues—not always “user error.” | + +## Relationship to decryption + +Even after **completion**, plaintext is **not** magically public on-chain. **Authorized clients** decrypt **`ct*` outputs** locally. Planning must cover **key recovery**, **device loss**, and **clear disclosure** of who can decrypt what. + +## Next steps + +- [How a private request travels end to end](how-a-private-request-travels-end-to-end.md) — full path diagram. +- [For developers: mapping concepts to the SDK](for-developers-mapping-to-the-sdk.md) — testing and callback checklists. diff --git a/privacy-on-avalanche/cookbook-private-investor-allocations.md b/privacy-on-avalanche/cookbook-private-investor-allocations.md new file mode 100644 index 0000000..e9e0fee --- /dev/null +++ b/privacy-on-avalanche/cookbook-private-investor-allocations.md @@ -0,0 +1,586 @@ +# Cookbook: private investor allocations with PoD + +Many token launches include an allocation contract: the project records how many tokens each investor may claim, then opens withdrawals after launch. This cookbook starts from that familiar **public Fuji dApp** pattern and turns it into a **Privacy on Demand (PoD)** dApp where investor allocation amounts stay private until they need to be revealed to the right party. + +> **Note:** This is an educational cookbook. The allocation unlocks **100% after launch** so the PoD flow is easier to follow. Production vesting contracts usually need cliffs, partial releases, revocation rules, admin controls, and a broader test suite. + +## What you will build + +You will build the same product twice: + +1. A public EVM version on Avalanche Fuji. +2. A PoD version where allocation amounts are encrypted, processed privately on COTI, and returned to Avalanche Fuji through callbacks. + +```mermaid +flowchart LR + Owner[Project owner] --> PublicDapp[Public Avalanche Fuji allocation dApp] + PublicDapp --> PrivacyProblem[Allocations visible before launch] + PrivacyProblem --> PodDapp[PoD allocation dApp] + PodDapp --> CotiPrivateLogic[COTI private allocation logic] + CotiPrivateLogic --> Callback[Fuji callback] + Callback --> Investor[Investor decrypts or withdraws] +``` + +The public version is useful because it gives you a known baseline: owner assigns allocations, investors read their allocation, launch happens, withdrawals open, and investors claim tokens. The private version preserves the same user story, but changes how allocation data moves through the system. + +## Parts in this cookbook + +1. [Build the public Fuji dApp](#part-1-build-the-public-fuji-dapp) +2. [Script allocation and withdrawal](#part-2-script-allocation-and-withdrawal) +3. [Make allocations private with PoD](#part-3-make-allocations-private-with-pod) +4. [Async calls and request IDs](#part-4-async-calls-and-request-ids) +5. [Understand fees](#part-5-understand-fees) +6. [Encrypt and decrypt](#part-6-encrypt-and-decrypt) +7. [Allocate and read private allocations](#part-7-allocate-and-read-private-allocations) +8. [Withdraw with execution callback](#part-8-withdraw-with-execution-callback) +9. [Follow the lifecycle in explorers](#part-9-follow-the-lifecycle-in-explorers) + +## Prerequisites + +- A Solidity toolchain such as Hardhat or Foundry. +- A Avalanche Fuji wallet with test AVAX for deploys, transactions, and PoD request fees. +- Node.js 18+ for scripts. +- Packages: `npm install @coti-io/pod-sdk ethers` and `npm install github:coti-io/coti-contracts#main` (Solidity). +- A way for users to complete PoD onboarding and obtain their account AES key for local decryption. + +Before implementing the private version, read: + +- [TypeScript PoD SDK (`CotiPodCrypto`, `PodContract`)](typescript-pod-sdk.md) +- [Async private operations](async-private-operations.md) +- [How do PoA fees work?](how-poa-fees-work.md) +- [Tutorial: custom privacy logic with PoD](tutorial-custom-logic.md) + +## Part 1: Build the public Fuji dApp + +Start with the simple contract most Solidity developers already understand: an owner writes allocation amounts into a public mapping, then opens withdrawals after launch. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +contract Allocations is Ownable { + using SafeERC20 for IERC20; + + mapping(address => uint256) public allocations; + bool public withdrawOpen; + IERC20 public immutable token; + + event AllocationSet(address indexed investor, uint256 amount); + event WithdrawalsOpened(); + event Withdrawn(address indexed investor, uint256 amount); + + constructor(IERC20 _token, address initialOwner) Ownable(initialOwner) { + token = _token; + } + + function openWithdrawals() external onlyOwner { + withdrawOpen = true; + emit WithdrawalsOpened(); + } + + function setAllocation(address investor, uint256 amount) external onlyOwner { + allocations[investor] = amount; + emit AllocationSet(investor, amount); + } + + function withdraw() external { + require(withdrawOpen, "withdrawals closed"); + + uint256 amount = allocations[msg.sender]; + require(amount > 0, "no allocation"); + + allocations[msg.sender] = 0; + token.safeTransfer(msg.sender, amount); + + emit Withdrawn(msg.sender, amount); + } +} +``` + +This is intentionally small, but it already includes habits you should keep in production examples: + +- `withdrawOpen` is checked before transfers. +- Allocation is cleared before `safeTransfer`, so an investor cannot withdraw twice. +- `onlyOwner` protects allocation writes and launch control. +- Events give scripts and indexers a stable way to follow state changes. + +> **Warning:** Do not ship this as a production vesting contract. Real investor flows need schedules, token decimal handling, funding checks, emergency behavior, accounting around partial claims, and tests for every state transition. + +## Part 2: Script allocation and withdrawal + +The owner script deploys the token and allocation contract, funds the allocation contract, and writes investor balances. + +```typescript +import { ethers } from "ethers"; + +const allocationsAbi = [ + "function setAllocation(address investor,uint256 amount)", + "function openWithdrawals()", + "function allocations(address investor) view returns (uint256)", +] as const; + +const allocations = new ethers.Contract(allocationsAddress, allocationsAbi, owner); + +await (await token.transfer(allocationsAddress, ethers.parseUnits("1000000", 18))).wait(); +await (await allocations.setAllocation(investorA, ethers.parseUnits("1000", 18))).wait(); +await (await allocations.setAllocation(investorB, ethers.parseUnits("2500", 18))).wait(); + +console.log("investor A:", await allocations.allocations(investorA)); +console.log("investor B:", await allocations.allocations(investorB)); +``` + +The investor script can read and withdraw without asking the project for any off-chain data. + +```typescript +const investorAllocations = allocations.connect(investor); + +const amount = await investorAllocations.allocations(await investor.getAddress()); +console.log("public allocation:", amount.toString()); + +await (await allocations.openWithdrawals()).wait(); // owner action in a real script +await (await investorAllocations.withdraw()).wait(); +``` + +That simplicity is also the privacy problem. A public mapping exposes every allocation amount to anyone who knows or guesses investor addresses. Even if the UI hides the numbers, archive nodes, explorers, logs, and direct RPC calls do not. + +## Part 3: Make allocations private with PoD + +Private allocations are a better fit for the **custom PoD dApp** model than the primitive-only model. The app needs private state and business logic, not just one arithmetic operation. + +Split the system into two contracts: + +| Layer | Responsibility | +| --- | --- | +| **Avalanche Fuji** | Holds public ERC-20 tokens, receives user transactions, checks public launch / withdrawal-open state, pays Inbox fees, tracks request state, and transfers tokens after authorized callbacks. | +| **COTI** | Stores or derives encrypted allocation amounts, checks private allocation / claimed state, off-boards ciphertext to the investor, and responds to Avalanche Fuji. | + +The high-level flow becomes: + +```mermaid +sequenceDiagram + participant Owner + participant Investor + participant Avalanche Fuji as Fuji allocations contract + participant Inbox + participant COTI as COTI private allocations contract + + Owner->>Avalanche Fuji: submit encrypted allocation request + Avalanche Fuji->>Inbox: sendTwoWayMessage + Inbox->>COTI: set private allocation + COTI-->>Inbox: respond + Inbox-->>Avalanche Fuji: allocation callback + Investor->>Avalanche Fuji: request allocation read or withdraw + Avalanche Fuji->>Inbox: sendTwoWayMessage + Inbox->>COTI: compute investor result + COTI-->>Inbox: respond with ciphertext or withdrawal amount + Inbox-->>Avalanche Fuji: execution callback +``` + +On Avalanche Fuji, the contract should be thin, but it still owns public business rules. It should check whether withdrawals are open before dispatching a withdraw request, submit requests, correlate callbacks by request ID, and transfer public tokens only after the COTI-side result says the investor may claim. It should not try to inspect private allocation amounts. + +On COTI, the contract owns the private allocation logic. Structurally, it follows the same pattern as the custom logic tutorial: the COTI contract receives Inbox calls, runs private computation, and calls `inbox.respond(...)` with an ABI-encoded result that the Fuji callback can decode. + +### Set trust relationships + +A custom PoD app has contracts on both sides of the Inbox. Do not accept callbacks from any remote contract that happens to return correctly shaped bytes. Configure the expected COTI peer on Avalanche Fuji, configure the expected Fuji peer on COTI where your COTI-side contract needs to authenticate inbound messages, and verify those peers in the callback path. + +On the Fuji side, store the trusted COTI contract address at deployment or through an owner-only setup function: + +```solidity +address public cotiAllocationPeer; + +event CotiAllocationPeerSet(address indexed peer); + +function setCotiAllocationPeer(address peer) external onlyOwner { + require(peer != address(0), "zero peer"); + cotiAllocationPeer = peer; + emit CotiAllocationPeerSet(peer); +} +``` + +On the COTI side, store the trusted Fuji contract as well. The exact base contract and constants depend on the SDK version you install, but the rule is the same: only process messages that came through the Inbox from your expected host-chain contract. + +```solidity +address public fujiAllocationPeer; + +function setFujiAllocationPeer(address peer) external onlyOwner { + require(peer != address(0), "zero peer"); + fujiAllocationPeer = peer; +} + +function _requireTrustedFujiPeer() internal view { + (uint256 callerChain, address callerContract) = inbox.inboxMsgSender(); + require(callerChain == AVALANCHE_FUJI_CHAIN_ID, "wrong source chain"); + require(callerContract == fujiAllocationPeer, "wrong source contract"); +} + +function receiveSetAllocation(bytes memory data) external onlyInbox { + _requireTrustedFujiPeer(); + + // Update private allocation state. +} +``` + +When sending a request, route it to that exact peer: + +```solidity +requestId = IInbox(inbox).sendTwoWayMessage{value: msg.value}( + COTI_TESTNET_CHAIN_ID, + cotiAllocationPeer, + methodCall, + this.onSetAllocationCompleted.selector, + this.onDefaultMpcError.selector, + callbackFeeLocalWei +); +``` + +When the Inbox calls your Fuji callback, verify both layers of trust: + +1. `onlyInbox` proves the immediate caller is the local Inbox. +2. `inboxMsgSender()` proves which remote chain and remote contract produced the message. + +```solidity +function _requireTrustedCotiPeer() internal view { + (uint256 callerChain, address callerContract) = IInbox(inbox).inboxMsgSender(); + require(callerChain == COTI_TESTNET_CHAIN_ID, "wrong source chain"); + require(callerContract == cotiAllocationPeer, "wrong source contract"); +} + +function onSetAllocationCompleted(bytes memory resultData) external onlyInbox { + _requireTrustedCotiPeer(); + + bytes32 requestId = IInbox(inbox).inboxSourceRequestId(); + address investor = abi.decode(resultData, (address)); + + allocationRequests[requestId] = AllocationRequest({ + investor: investor, + status: RequestStatus.Completed + }); + + emit PrivateAllocationCompleted(requestId, investor); +} +``` + +Apply the same check to every Fuji callback: allocation writes, allocation reads, withdraw approvals, and error handlers that change state. + +## Part 4: Async calls and request IDs + +PoD calls are asynchronous. You are already familiar with async callbacks in Javascript. Consider your async calls in solidity as jobs: + +1. The user or owner submits a Fuji transaction. +2. The Fuji contract sends a two-way Inbox message and emits a `requestId`. +3. COTI executes the private operation. +4. The Inbox invokes the Fuji callback in a later transaction. +5. The app marks the request as completed or failed. + +Your Fuji contract does **not** run the private computation or synchronously receive the answer. Its job is to **start** the PoD request and provide callback functions that the Inbox can call later. This is similar to the JavaScript async callback pattern: you submit work now, keep a handle you can use for correlation, and let a callback update your application state when the result is ready. + +As a reminder, this is how a javascript callback is defined: + +```typescript +startPrivateAllocationJob(input, (result) => { + // This runs later, after the private work completes. + storeResult(result); +}); +``` + +In PoD, the equivalent of `startPrivateAllocationJob` is the Fuji transaction that calls the Inbox, and the equivalent of the JavaScript callback is your Solidity callback selector, such as `onSetAllocationCompleted` or `onAllocationRead`. + +```solidity +enum RequestStatus { + None, + Pending, + Completed, + Failed +} + +struct AllocationRequest { + address investor; + RequestStatus status; +} + +mapping(bytes32 => AllocationRequest) public allocationRequests; + +event PrivateAllocationRequested(bytes32 indexed requestId, address indexed investor); +event PrivateAllocationCompleted(bytes32 indexed requestId, address indexed investor); +``` + +When the request is sent, store just enough correlation data for the later callback: + +```solidity +requestId = IInbox(inbox).sendTwoWayMessage{value: msg.value}( + COTI_TESTNET_CHAIN_ID, + cotiAllocationPeer, + methodCall, + this.onSetAllocationCompleted.selector, + this.onDefaultMpcError.selector, + callbackFeeLocalWei +); + +allocationRequests[requestId] = AllocationRequest({ + investor: investor, + status: RequestStatus.Pending +}); +``` + +When the Inbox calls back, verify the Inbox and the remote peer, then update state for that request: + +```solidity +function onSetAllocationCompleted(bytes memory resultData) external onlyInbox { + (uint256 callerChain, address callerContract) = IInbox(inbox).inboxMsgSender(); + require(callerChain == COTI_TESTNET_CHAIN_ID && callerContract == cotiAllocationPeer, "not allowed"); + + bytes32 requestId = IInbox(inbox).inboxSourceRequestId(); + address investor = abi.decode(resultData, (address)); + + allocationRequests[requestId] = AllocationRequest({ + investor: investor, + status: RequestStatus.Completed + }); + + emit PrivateAllocationCompleted(requestId, investor); +} +``` + +In TypeScript, use `PodContract.extractRequestIds(txHash)` after the transaction is mined. This reads Inbox `MessageSent` logs and gives your UI or script the request ID to poll. + +```typescript +const txResponse = await pod.encryptAndCallMethod("setPrivateAllocation", args, feeCfg); +const receipt = await txResponse.wait(); + +const requestIds = receipt?.hash ? await pod.extractRequestIds(receipt.hash) : []; +const requestId = requestIds[0]; +``` + +Until the callback lands, show `Pending`. Do not expect encrypted outputs, claimability, or token transfers to be visible in the same transaction that submitted the request. + +## Part 5: Understand fees + +Every two-way PoD message needs enough native token for both legs: + +- The forward leg from Avalanche Fuji to COTI. +- The return leg from COTI back to Avalanche Fuji. + +The callback leg is represented by `callbackFeeLocalWei`. If it is underfunded, the private operation may complete but the callback may not update Avalanche Fuji state. That usually looks like a request stuck in `Pending`. + +```typescript +import { + DataType, + PodContract, + type PodFeeEstimationConfig, + type PodMethodArgument, +} from "@coti-io/pod-sdk"; + +const args: PodMethodArgument[] = [ + { type: DataType.Address, value: investorAddress, isCallBackFee: false }, + { type: DataType.itUint256, value: allocationAmount, isCallBackFee: false }, + { type: DataType.Uint256, value: "0", isCallBackFee: true }, +]; + +const feeCfg: PodFeeEstimationConfig = { + forwardGasLimit: 500_000n, + gasPrice: (await signer.provider!.getFeeData()).gasPrice ?? 0n, + callBackGasLimit: 300_000n, + callBackDataSize: 512n, +}; + +const estimated = await pod.estimateFee("setPrivateAllocation", args, feeCfg); +console.log("total fee:", estimated.totalFee.toString()); +console.log("callback fee:", estimated.callBackFee.toString()); +``` + +Tune `forwardGasLimit`, `callBackGasLimit`, and `callBackDataSize` from real measurements. For the full model, see [How do PoA fees work?](how-poa-fees-work.md). + +## Part 6: Encrypt and decrypt + +The project owner encrypts allocation amounts before submitting them to the private flow. + +```typescript +import { CotiPodCrypto, DataType } from "@coti-io/pod-sdk"; + +const encryptedAllocation = await CotiPodCrypto.encrypt( + ethers.parseUnits("1000", 18).toString(), + "testnet", + DataType.itUint256 +); +``` + +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. + +```typescript +const ct = await fujiAllocations.readResultByRequest(requestId); +const ctHex = typeof ct === "bigint" ? "0x" + ct.toString(16) : String(ct); + +const plain = CotiPodCrypto.decrypt( + ctHex, + accountAesKeyFromOnboarding, + DataType.Uint256 +); + +console.log("private allocation:", plain); +``` + +> **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 + +The owner-facing operation stores an investor allocation privately. + +Conceptually, the Fuji function looks like this: + +```solidity +function setPrivateAllocation( + address investor, + itUint256 calldata encryptedAmount, + uint256 callbackFeeLocalWei +) external payable onlyOwner returns (bytes32 requestId) { + // Build an Inbox method call to the COTI-side allocation contract. + // Follow MpcAbiCodec argument construction from your installed SDK version. + + requestId = IInbox(inbox).sendTwoWayMessage{value: msg.value}( + COTI_TESTNET_CHAIN_ID, + cotiAllocationPeer, + methodCall, + this.onSetAllocationCompleted.selector, + this.onDefaultMpcError.selector, + callbackFeeLocalWei + ); + + allocationRequests[requestId] = AllocationRequest({ + investor: investor, + status: RequestStatus.Pending + }); + + emit PrivateAllocationRequested(requestId, investor); +} +``` + +The callback does not reveal the amount. It only confirms that private state was updated. + +```solidity +function onSetAllocationCompleted(bytes memory resultData) external onlyInbox { + (uint256 callerChain, address callerContract) = IInbox(inbox).inboxMsgSender(); + require(callerChain == COTI_TESTNET_CHAIN_ID && callerContract == cotiAllocationPeer, "not allowed"); + + bytes32 requestId = IInbox(inbox).inboxSourceRequestId(); + address investor = abi.decode(resultData, (address)); + + allocationRequests[requestId] = AllocationRequest({ + investor: investor, + status: RequestStatus.Completed + }); + + emit PrivateAllocationCompleted(requestId, investor); +} +``` + +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. + +```solidity +mapping(bytes32 => ctUint256) public allocationReadResults; + +function onAllocationRead(bytes memory resultData) external onlyInbox { + (uint256 callerChain, address callerContract) = IInbox(inbox).inboxMsgSender(); + require(callerChain == COTI_TESTNET_CHAIN_ID && callerContract == cotiAllocationPeer, "not allowed"); + + bytes32 requestId = IInbox(inbox).inboxSourceRequestId(); + ctUint256 allocation = abi.decode(resultData, (ctUint256)); + + allocationReadResults[requestId] = allocation; +} +``` + +## Part 8: Withdraw with execution callback + +Withdrawal is where public and private state meet. The investor submits a withdraw request on Avalanche Fuji. Avalanche Fuji checks the public `withdrawOpen` state before sending the PoD request. COTI checks the private allocation and private claimed state, then the callback returns the public transfer amount to Avalanche Fuji. + +The request entrypoint should keep the public launch check local: + +```solidity +function withdraw(uint256 callbackFeeLocalWei) external payable returns (bytes32 requestId) { + require(withdrawOpen, "withdrawals closed"); + + // Build an Inbox method call that asks the trusted COTI peer to approve + // msg.sender's private allocation withdrawal. + + requestId = IInbox(inbox).sendTwoWayMessage{value: msg.value}( + COTI_TESTNET_CHAIN_ID, + cotiAllocationPeer, + methodCall, + this.onWithdrawApproved.selector, + this.onDefaultMpcError.selector, + callbackFeeLocalWei + ); + + emit WithdrawRequested(requestId, msg.sender); +} +``` + +On Avalanche Fuji, keep the token transfer in the authorized callback: + +```solidity +mapping(address => bool) public claimed; + +function onWithdrawApproved(bytes memory resultData) external onlyInbox { + _requireTrustedCotiPeer(); + + bytes32 requestId = IInbox(inbox).inboxSourceRequestId(); + (address investor, uint256 amount) = abi.decode(resultData, (address, uint256)); + + require(!claimed[investor], "already claimed"); + claimed[investor] = true; + + token.safeTransfer(investor, amount); + + emit WithdrawApproved(requestId, investor, amount); +} +``` + +The exact COTI-side contract depends on the SDK version and how you model private state. The important invariant is that Avalanche Fuji transfers tokens only from a callback that: + +- Is protected by `onlyInbox`. +- Verifies `inboxMsgSender()` against the expected COTI chain and contract. +- Decodes the expected result shape. +- Marks the investor claimed before transferring tokens. + +This preserves the public contract’s safety habits while moving sensitive allocation logic into PoD. + +## Part 9: Follow the lifecycle in explorers + +When debugging a PoD allocation request, follow the same object through every step: + +| Step | Where to look | What to capture | +| --- | --- | --- | +| Request transaction | Fuji (SnowScan) explorer | User transaction hash and app event. | +| Inbox send | Fuji logs | `MessageSent` and the `requestId` extracted by `PodContract.extractRequestIds`. | +| COTI execution | COTI explorer / operator logs | Remote contract call and private execution status. | +| Callback transaction | Fuji (SnowScan) explorer | Inbox callback transaction into your app contract. | +| App completion | Fuji logs / UI indexer | `Completed`, `Failed`, `WithdrawApproved`, or equivalent app event. | + +For COTI network details and explorer links, start with the [COTI testnet page](../networks/testnet/README.md). For the end-to-end mental model, see [How a private request travels end to end](how-a-private-request-travels-end-to-end.md). + +## What to harden for production + +Before adapting this cookbook for a real launch, add: + +- A real vesting schedule: launch time, cliffs, partial claims, and released-but-unclaimed accounting. +- Strong request ownership: each request ID should map to the expected investor and operation. +- Replay protection and idempotent callbacks. +- Funding checks so the Fuji contract can satisfy approved withdrawals. +- Clear failure states and support tooling for stuck requests. +- Tests for unauthorized callbacks, wrong remote peers, underfunded fees, duplicate withdrawals, malformed callback data, and token transfer failures. +- Indexer support so users can see pending, completed, and failed private actions without manually inspecting logs. + +## Reference links + +- [Tutorials: building PoD dApps on Avalanche](tutorials-privacy-on-avalanche.md) +- [Tutorial: private Adder on Avalanche Fuji](tutorial-private-adder-fuji.md) +- [Tutorial: custom privacy logic with PoD](tutorial-custom-logic.md) +- [TypeScript PoD SDK (`CotiPodCrypto`, `PodContract`)](typescript-pod-sdk.md) +- [PoD SDK documentation](https://github.com/cotitech-io/coti-pod-sdk/tree/main/docs) diff --git a/privacy-on-avalanche/for-developers-mapping-to-the-sdk.md b/privacy-on-avalanche/for-developers-mapping-to-the-sdk.md new file mode 100644 index 0000000..a8293b7 --- /dev/null +++ b/privacy-on-avalanche/for-developers-mapping-to-the-sdk.md @@ -0,0 +1,61 @@ +# For developers: mapping concepts to the SDK + +This page is the **bridge** from [Architecture and main components](architecture-and-components.md) to the canonical [PoD SDK documentation on GitHub](https://github.com/cotitech-io/coti-pod-sdk/tree/main/docs). It repeats a few facts on purpose so engineers can verify mental models quickly. + +For a guided first implementation, read **[Tutorials: building Privacy on Avalanche (PoD) dApps](tutorials-privacy-on-avalanche.md)** to pick the integration model, then follow [Tutorial: private Adder on Avalanche Fuji](tutorial-private-adder-fuji.md) for a **primitive-only** Solidity + TypeScript walkthrough (Avalanche Fuji presets). + +## Official reading order (SDK) + +The upstream docs recommend: + +1. [Privacy dApps on any EVM chain with COTI PoD](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/01-privacy-decentralized-apps-on-any-evm-chain-with-coti-pod.md) +2. [Getting started](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/04-getting-started.md) +3. [Writing privacy contracts on Ethereum](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/05-writing-privacy-contracts-on-ethereum.md) +4. [TypeScript integration (UX development)](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/06-typescript-integration-ux-development.md) + +Then deep dives: + +- [Async execution](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/05a-async-execution.md) +- [MPC library (PodLib)](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/05b-multi-party-computing-library-mpclib.md) +- [Examples with description](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/05c-examples-with-description.md) +- Contract references: [Data types](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/contracts/01-it-ct-gt-data-types.md), [Patterns and checklist](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/contracts/02-contract-patterns-and-checklist.md), [Request builder and remote calls](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/contracts/03-request-builder-and-remote-calls.md), [Fees, gas, and oracle](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/contracts/04-fees-gas-and-oracle.md) + +## Component → source file map + +| Concept (this book) | Where it lives in the SDK docs / repo | +| --- | --- | +| **Inbox** | [IInbox.sol](https://github.com/cotitech-io/coti-pod-sdk/blob/main/contracts/IInbox.sol) and cross-domain flow in the [domain model](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/01-privacy-decentralized-apps-on-any-evm-chain-with-coti-pod.md) diagram. | +| **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/coti-io/coti-contracts/blob/main/contracts/pod/mpc/PodLib.sol) and width-specific libraries (`PodLib64`, `PodLib128`, `PodLib256`). | +| **PodUser / presets** | [PodUser.sol](https://github.com/coti-io/coti-contracts/blob/main/contracts/pod/mpc/PodUser.sol), network mixins such as [PodUserFuji.sol](https://github.com/coti-io/coti-contracts/blob/main/contracts/pod/mpc/PodUserFuji.sol). | +| **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). | +| **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/coti-io/coti-sdk-pod/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)). | + +## 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): + +1. **Classify data** — public metadata vs `it*` inputs vs `ct*` outputs vs internal `gt*` (COTI-only). +2. **Pick integration mode** — `PodLib` helpers vs custom `MpcAbiCodec` + COTI contract. +3. **Model async state** — persist `requestId`, track pending/completed/failed. +4. **Harden callbacks** — `onlyInbox`, correct `abi.decode` tuple, validate peer context when applicable. +5. **Configure routing safely** — gated `configure` / `configureCoti` / inbox updates. +6. **Budget fees** — understand `msg.value` and `callbackFeeLocalWei`; use Inbox fee views where available ([Fees doc](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/contracts/04-fees-gas-and-oracle.md)). +7. **Test failure paths** — spoofed callback must revert, error callbacks must mark failures, decrypt integration must match widths. + +## Relationship to native COTI “build” documentation + +If you build **directly on COTI V2** with precompiles and private types, start from **[Build on COTI](../build-on-coti/README.md)**. PoD adds the **Inbox-mediated cross-chain** angle; many **cryptographic ideas rhyme**, but **deployment and UX** differ. + +## Package install + +```bash +npm install @coti-io/pod-sdk ethers +npm install github:coti-io/coti-contracts#main +``` + +- **TypeScript:** `@coti-io/pod-sdk` +- **Solidity:** `@coti-io/coti-contracts` (`PodLib`, `PodUserFuji`, …) — the npm SDK does not ship contract sources + +Use `contract MyApp is PodLib, PodUserFuji` with imports from `@coti-io/coti-contracts/contracts/pod/mpc/...`. diff --git a/privacy-on-avalanche/getting-started-fuji.md b/privacy-on-avalanche/getting-started-fuji.md new file mode 100644 index 0000000..227843c --- /dev/null +++ b/privacy-on-avalanche/getting-started-fuji.md @@ -0,0 +1,156 @@ +# Getting started on Avalanche Fuji (Day 0) + +This page gets you ready to build **Privacy on Avalanche** on **Fuji C-Chain** before you touch PoD Solidity or the TypeScript SDK. + +You will: + +1. Add **Avalanche Fuji C-Chain** to your wallet +2. Fund the wallet with **test AVAX** +3. Configure **Hardhat** or **Foundry** for Fuji +4. Confirm a transaction on **SnowScan** +5. Jump into PoD with the right packages and next tutorial + +> **Scope:** This is the **C-Chain** (EVM) testnet — chain ID `43113`. It is not an Avalanche L1/subnet setup guide. + +## 1. Network parameters (copy into your wallet) + +| Field | Value | +| --- | --- | +| Network name | Avalanche Fuji C-Chain | +| RPC URL | `https://api.avax-test.network/ext/bc/C/rpc` | +| Chain ID | `43113` | +| Currency symbol | AVAX | +| Block explorer | `https://testnet.snowscan.xyz` | + +Full PoD contract tables: [Avalanche Fuji](networks/fuji.md). + +### Core + +[Core](https://core.app/) usually includes Fuji. Switch the network to **Fuji** / **Avalanche Fuji C-Chain**, or add the RPC above if you manage networks manually. + +### MetaMask (or any EIP-3085 wallet) + +1. Open **Settings → Networks → Add network → Add a network manually**. +2. Paste the table values above. +3. Save, then select **Avalanche Fuji C-Chain**. +4. Confirm the chain ID shows **`43113`** (not Avalanche mainnet `43114`). + +## 2. Get test AVAX + +PoD two-way requests pay fees in **AVAX** on Fuji (`msg.value`). You need enough for deploy gas **and** Inbox fees. + +Official options: + +- [Avalanche Core testnet faucet (C-Chain)](https://core.app/tools/testnet-faucet/?subnet=c&token=c) +- [Avalanche Builder Hub faucet](https://build.avax.network/console/primary-network/faucet) +- Avalanche support note on faucets: [Is there an AVAX faucet?](https://support.avax.network/en/articles/6110239-is-there-an-avax-faucet) + +Faucets may require a mainnet AVAX balance, a Builder Hub account, or a coupon. Request **C-Chain** test AVAX (not only P-Chain). + +After funding, your wallet AVAX balance on Fuji should be non-zero. + +## 3. Configure your toolchain for Fuji + +Use Node.js 18+. Install later when you start coding; for Day 0 you only need a network entry that targets Fuji. + +### Hardhat (`hardhat.config.ts` / `.js`) + +```ts +import { HardhatUserConfig } from "hardhat/config"; +import "@nomicfoundation/hardhat-ethers"; + +const FUJI_RPC = process.env.AVALANCHE_FUJI_RPC_URL + ?? "https://api.avax-test.network/ext/bc/C/rpc"; + +const config: HardhatUserConfig = { + solidity: "0.8.26", + networks: { + fuji: { + url: FUJI_RPC, + chainId: 43113, + accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [], + }, + }, +}; + +export default config; +``` + +Deploy later with: + +```bash +npx hardhat run scripts/deploy.ts --network fuji +``` + +### Foundry (`foundry.toml`) + +```toml +[profile.default] +src = "src" +out = "out" +libs = ["lib"] +solc_version = "0.8.26" + +[rpc_endpoints] +fuji = "${AVALANCHE_FUJI_RPC_URL}" +``` + +`.env` (example): + +```bash +AVALANCHE_FUJI_RPC_URL=https://api.avax-test.network/ext/bc/C/rpc +PRIVATE_KEY=0x... +``` + +Broadcast later with `--rpc-url fuji` (or the resolved RPC URL) and chain ID `43113`. + +### Packages you will install for PoD (not required yet) + +When you start the adder tutorial: + +```bash +npm install @coti-io/pod-sdk ethers +npm install github:coti-io/coti-contracts#main +``` + +- **TypeScript:** `@coti-io/pod-sdk` +- **Solidity:** `@coti-io/coti-contracts` (`PodLib`, `PodUserFuji`) + +## 4. Smoke-test on SnowScan + +1. From your funded Fuji wallet, send a small amount of AVAX to another address you control (or to yourself). +2. Open the tx on [SnowScan testnet](https://testnet.snowscan.xyz). +3. Confirm **status success**, network is Fuji, and the fee was paid in AVAX. + +Optional RPC check: + +```bash +curl -s -X POST https://api.avax-test.network/ext/bc/C/rpc \ + -H 'content-type: application/json' \ + --data '{"jsonrpc":"2.0","id":1,"method":"eth_chainId","params":[]}' +``` + +Expected result hex: `0xa869` (= decimal **`43113`**). + +## 5. PoD context (what comes next) + +On Fuji your dApp talks to the shared Inbox: + +`0xAb625bE229F603f6BBF964474AFf6d5487e364De` + +Private compute still runs on **COTI Testnet** (`7082400`). Solidity preset: **`PodUserFuji`**. + +| Next | Link | +| --- | --- | +| Network + contract addresses | [Avalanche Fuji](networks/fuji.md) | +| Fees in AVAX | [How do PoA fees work on Avalanche Fuji?](how-poa-fees-work.md) | +| First dApp | [Tutorial: private Adder on Avalanche Fuji](tutorial-private-adder-fuji.md) | +| Animated Fuji flow | [pod.coti.io/avalanche](https://pod.coti.io/avalanche/index.html) | + +## Checklist + +- [ ] Wallet on Fuji C-Chain (`43113`) +- [ ] Test AVAX balance > 0 +- [ ] Hardhat or Foundry has a `fuji` RPC / `chainId: 43113` +- [ ] At least one successful tx visible on SnowScan +- [ ] Ready for `@coti-io/pod-sdk` + `@coti-io/coti-contracts` diff --git a/privacy-on-avalanche/glossary.md b/privacy-on-avalanche/glossary.md new file mode 100644 index 0000000..81c91ba --- /dev/null +++ b/privacy-on-avalanche/glossary.md @@ -0,0 +1,28 @@ +# Glossary + +Short definitions for **Privacy on Demand** readers. Precise Solidity definitions and type tables are in the [PoD SDK contract types](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/contracts/01-it-ct-gt-data-types.md) document. + +| Term | Meaning | +| --- | --- | +| **Privacy on Demand (PoD)** | Pattern and tooling for **private computation on COTI** while **orchestrating** from **another EVM chain** via an **Inbox**. | +| **Privacy on Avalanche** | PoD applied with **Avalanche Fuji C-Chain** as the host (AVAX fees, SnowScan, `PodUserFuji`). | +| **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. | +| **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**—not that the application callback committed. | +| **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 AVAX into gas-unit budgets (bounded reference price—not raw tip manipulation). | +| **Account AES key** | User-side secret material used to **decrypt** many `ct*` outputs after onboarding; must be **handled like credentials**. | diff --git a/privacy-on-avalanche/how-a-private-request-travels-end-to-end.md b/privacy-on-avalanche/how-a-private-request-travels-end-to-end.md new file mode 100644 index 0000000..d6c2e5e --- /dev/null +++ b/privacy-on-avalanche/how-a-private-request-travels-end-to-end.md @@ -0,0 +1,110 @@ +# How a private request travels end to end + +This page describes **one full cycle** of Privacy on Demand **without assuming Solidity knowledge**. Names match what you will see in the [PoD SDK documentation](https://github.com/cotitech-io/coti-pod-sdk/tree/main/docs). + +## Cast of roles + +| Role | Plain description | +| --- | --- | +| **End user** | A person (or agent) using a wallet or app. They approve transactions and hold decryption capability for their own results. | +| **Client app** | Browser, mobile app, or script that prepares **encrypted inputs** and later **decrypts outputs** locally. | +| **Your dApp contract** | Smart contract on **your EVM chain** that encodes business rules and stores **correlation IDs** and **encrypted results** (`ct*` types in developer docs). | +| **Inbox (EVM)** | On-chain **messaging hub** on **your chain** that **forwards** jobs to the **Inbox (COTI)** and **calls back** into your contract when the answer is ready. | +| **Inbox (COTI)** | The **COTI-side Inbox contract**—the **counterpart** to the host Inbox. It receives cross-domain messages and routes work to the MPC Executor. | +| **COTI private execution** | The environment that performs **private computation** on **compute-domain values** (`gt*` in developer docs). | +| **MPC Executor** | The **COTI-side contract** your integration targets for a given network (see SDK presets such as `PodUserFuji` in [Getting started](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/04-getting-started.md)). The **Inbox (COTI)** invokes it; it is **not** the same contract as either Inbox. | + +## The journey in seven steps + +1. **User chooses an action** (for example “compare two private numbers”, “run a private scoring step”, and so on). +2. **Client encrypts inputs** into **`it*` payloads**: ciphertext plus the cryptographic material the protocol expects (signatures). The plaintext never has to hit the chain in the clear. +3. **User submits an EVM transaction** to your dApp contract, which validates public rules (permissions, payment, scheduling). +4. **Your contract asks the Inbox (EVM)** to send a **two-way message**: outbound leg to **Inbox (COTI)**, inbound **callback** to your contract when finished. This step is **not** the same as a normal internal function return—see [Async private operations](async-private-operations.md). +5. **Inbox (COTI)** hands the job to the **MPC Executor**, which runs the private logic on **`gt*` values**—the internal representation used during computation. +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 (Avalanche Fuji)**—**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. + +**Mermaid quirk:** on a `box` line, do not put **ASCII** parentheses in the title after `rgb(r, g, b)` (for example `(COTI)`). The parser treats everything from `rgb(` through the **last** `)` on that line as the color string, which breaks the fill. Use an em dash in the box title instead—`(Avalanche Fuji)` / `(COTI)` still appear on participant labels below. + +```mermaid +%%{init: { + 'theme': 'dark', + 'themeVariables': { + 'background': '#0b0d11', + 'actorBkg': '#151922', + 'actorBorder': '#2a3140', + 'actorTextColor': '#e8eaef', + 'actorLineColor': '#6b7280', + 'signalColor': '#9ca6b8', + 'sequenceNumberColor': '#9ca6b8', + 'labelBoxBkgColor': '#151922', + 'labelTextColor': '#e8eaef', + 'loopTextColor': '#e8eaef', + 'noteTextColor': '#e8eaef', + 'noteBkgColor': '#1c2230', + 'noteBorderColor': '#2a3140' + } +}}%% +sequenceDiagram + participant User as User + participant Client as Client app + box rgb(28, 32, 44) Avalanche Fuji + participant Dapp as Your Fuji dApp contract + participant InboxEvm as Inbox + end + box rgb(44, 34, 28) Coti + participant InboxCoti as Inbox + participant Exec as MPC Executor + end + + User->>Client: Approve action + Client->>Client: Build encrypted input (it*) + Client->>Dapp: Transaction with it* args + Dapp->>InboxEvm: Request two-way private job + InboxEvm->>InboxCoti: Cross-domain: forward job + InboxCoti->>Exec: Invoke configured private op + Exec->>Exec: Private compute (gt*) + Exec->>InboxCoti: Return encoded ct* + InboxCoti->>InboxEvm: Cross-domain: relay result + InboxEvm->>Dapp: Callback with result bytes + Dapp->>Dapp: Store ct* by request ID + Client->>Dapp: Read ct* (view / indexer) + Client->>Client: Decrypt with account AES key + Client->>User: Show plaintext result (if entitled) +``` + +## 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**, **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 + +- **[Interactive PoD architecture on Fuji (pod.coti.io/avalanche)](https://pod.coti.io/avalanche/index.html)** — Walk through the Fuji ↔ COTI path as an animated demo with contract links and fee consumption. +- [Architecture and main components](architecture-and-components.md) — how **PodUser**, **PodLib**, and the **Inbox** relate in a component picture. +- [Async private operations](async-private-operations.md) — why the UI must show **pending** states. diff --git a/privacy-on-avalanche/how-poa-fees-work.md b/privacy-on-avalanche/how-poa-fees-work.md new file mode 100644 index 0000000..05e9b7d --- /dev/null +++ b/privacy-on-avalanche/how-poa-fees-work.md @@ -0,0 +1,152 @@ +# How do PoA fees work on Avalanche Fuji? + +**PoA fees** are the **Privacy on Demand** fees you pay in **AVAX** on **Avalanche Fuji C-Chain** (`43113`) for a **two-way Inbox** message. Your Fuji contract typically calls something like **`add(itUint64 a, itUint64 b)`**, and the success path returns **`ctUint64 c`** in a later **callback** on Fuji. The same payment model applies to other `PodLib` operations that take **`msg.value`** and **`callbackFeeLocalWei`**. + +You always pay on Fuji in AVAX. Oracles convert the **COTI-bound slice** into a remote execution budget; the **callback slice** stays denominated for Fuji gas. + +> **ABI note:** The parameter is named `callbackFeeLocalWei` in Solidity. On Fuji that means the **18-decimal AVAX subunit** (same decimal layout as wei on Ethereum), not ETH. Avalanche gas prices are usually quoted in **nAVAX** (\(10^{-9}\) AVAX = \(10^{9}\) of those subunits). + +## What you are paying for + +A two-way PoD request does work on **two chains, in order**: + +1. **COTI (outbound leg)** — relay the request and run private compute (for example MPC `add`). +2. **Avalanche Fuji (return leg)** — relay the response and run your **`onlyInbox` callback** so `c` is written on Fuji. + +If either leg is underfunded, the job can stall as **pending** even when the other leg would have succeeded (see [Async private operations](async-private-operations.md)). + +`callbackFeeLocalWei` is a **slice of** `msg.value`, not an extra payment on top. + +## How Fuji AVAX becomes gas-unit budgets + +The Inbox fee manager (conceptually) does this when you call `sendTwoWayMessage` / a `PodLib` helper: + +| Symbol | Meaning on Fuji | +| --- | --- | +| \(V\) | `msg.value` in AVAX subunits | +| \(C\) | `callbackFeeLocalWei` (Fuji / callback slice), \(C \le V\) | +| \(R\) | Remote slice \(R = V - C\) (funds COTI via oracle) | +| \(G\) | **Bounded reference** gas price used on-chain when converting AVAX fees into gas-unit budgets (floor / ceiling / min priority via `setGasPriceBounds`)—**not** an unbounded raw `tx.gasprice` tip | + +**Fuji callback budget (gas units):** + +\[ +\texttt{callerFee} = \left\lfloor \frac{C}{G} \right\rfloor +\] + +**COTI remote budget (gas units), after oracle scaling:** + +\[ +\texttt{targetFee} = \left\lfloor \frac{R}{G} \right\rfloor \times \frac{P_{\mathrm{AVAX}}}{P_{\mathrm{COTI}}} +\] + +where \(P_{\mathrm{AVAX}}\) and \(P_{\mathrm{COTI}}\) are the Fuji Inbox **price oracle** USD prices for local (AVAX) and remote (COTI) tokens. + +Because AVAX is typically much more valuable than COTI per token, a modest AVAX remote slice often converts into a **large** COTI gas-unit budget. That is expected on Fuji; do not copy Sepolia ETH numbers. + +Live conversions and minima come from the deployed Fuji Inbox / fee manager / oracle — always prefer on-chain views over this page’s arithmetic. + +## Worked example (Avalanche Fuji, pedagogical) + +Assumptions for one readable walkthrough (not live oracle quotes): + +| Input | Value | +| --- | --- | +| `msg.value` | **0.02 AVAX** | +| `callbackFeeLocalWei` | **0.008 AVAX** (Fuji callback slice) | +| Remote slice \(R\) | **0.012 AVAX** (pays for COTI leg) | +| Fuji gas price \(G\) | **25 nAVAX** | +| Oracle \(P_{\mathrm{AVAX}}\) | **\$25** / AVAX | +| Oracle \(P_{\mathrm{COTI}}\) | **\$0.017** / COTI | + +### Step A — Split the AVAX payment + +| Leg | AVAX | Role | +| --- | --- | --- | +| Callback (Fuji) | 0.008 | Relayer response + `addCallback` on C-Chain | +| Remote (COTI) | 0.012 | Relay + private `add` on COTI | +| **Total** | **0.02** | Attached as `msg.value` on the Fuji tx | + +USD value of the remote slice: \(0.012 \times 25 = \$0.30\), or about **17.65 COTI** at \$0.017 / COTI. That intuition check is optional; the Inbox stores **gas-unit** budgets, not a COTI balance in your wallet. + +### Step B — Convert to gas-unit ledgers + +**Fuji `callerFee`:** + +\[ +\frac{0.008\ \mathrm{AVAX}}{25\ \mathrm{nAVAX}} = 320{,}000\ \text{gas units} +\] + +**Pre-oracle remote units** (AVAX paid ÷ Fuji gas price): + +\[ +\frac{0.012\ \mathrm{AVAX}}{25\ \mathrm{nAVAX}} = 480{,}000 +\] + +**COTI `targetFee`** (scale by AVAX/COTI oracle prices): + +\[ +480{,}000 \times \frac{25}{0.017} \approx 705{,}882{,}353\ \text{gas units} +\] + +| Ledger | Opening balance (gas units) | +| --- | --- | +| COTI (`targetFee`) | **705,882,353** | +| Avalanche Fuji (`callerFee`) | **320,000** | + +### Step C — Spend the ledgers in order + +COTI runs first; Fuji callback runs only after the encrypted result exists. + +| Step | Where | What happens | Gas used | COTI remaining | Fuji remaining | +| --- | --- | --- | --- | --- | --- | +| — | — | Opening balances | — | **705,882,353** | **320,000** | +| 1 | COTI | Relayers relay request | **12,000** | **705,870,353** | **320,000** | +| 2 | COTI | Private **add** → ciphertext **c** | **492,902** | **705,377,451** | **320,000** | +| 3 | Avalanche Fuji | Miner relays response | **8,000** | **705,377,451** | **312,000** | +| 4 | Avalanche Fuji | **`addCallback`** stores **`ctUint64 c`** | **230,000** | **705,377,451** | **82,000** | + +Remainders are illustrative. Production refunds / keep policies depend on **InboxMiner** / **InboxFeeManager** configuration. + +### Oracle cache refresh (Fuji) + +Inbox fee math reads **cached** USD prices for the configured local (AVAX) / remote (COTI) legs. Operators call **`refreshCache()`** (both legs). Configure legs with **`setInboxTokens`** (Uniswap oracles may set them from pair token0/token1 at construction). Prefer trusted Chainlink/Band feeds over spot Uniswap for production. + +## Why underfunding fails differently on each leg + +- **Too little `msg.value` overall** (or too little remote slice) → COTI relay / private execution may not clear minima → outbound path fails. +- **`callbackFeeLocalWei` too small** → COTI may finish, but Fuji never successfully writes **`c`** → UI stays **pending**. +- **Gas price matters:** budgets are \({\approx}\,\mathrm{fee}/G\). On-chain, \(G\) is a **bounded reference** (see `setGasPriceBounds`), so tip inflation cannot arbitrarily shrink gas-unit budgets. Off-chain estimators should still use a realistic gas price so UI quotes match what users will pay. + +## How to budget fees in practice on Fuji + +1. Prefer the deployed Fuji Inbox view: + + `calculateTwoWayFeeRequiredInLocalToken(...)` + +2. Or estimate from TypeScript with `@coti-io/pod-sdk`: + +```typescript +const fee = await pod.estimateFee("add", podArgs, { + forwardGasLimit: 400_000n, + callBackGasLimit: 250_000n, + callBackDataSize: 512n, + gasPrice: (await signer.provider!.getFeeData()).gasPrice!, +}); +// fee.totalFee → msg.value +// fee.callBackFee → callbackFeeLocalWei (must be ≤ totalFee) +``` + +3. Attach `fee.totalFee` as `msg.value` and pass `fee.callBackFee` as `callbackFeeLocalWei` (mark that argument with `isCallBackFee: true` when using `PodContract`). + +4. Confirm the Fuji price oracle and Inbox addresses on [Avalanche Fuji](networks/fuji.md). + +## Where this shows up in Solidity + +- Payable **`add`** (or other `PodLib` helpers) with **`msg.value`** and **`callbackFeeLocalWei`** — see [Tutorial: private Adder on Avalanche Fuji](tutorial-private-adder-fuji.md). +- Integration model context: [Tutorials overview](tutorials-privacy-on-avalanche.md). +- Contract-level detail: SDK [Fees, gas, and oracle](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/contracts/04-fees-gas-and-oracle.md). + +## Disclaimer + +Oracle prices, gas prices, minimum fee templates, and remainders on this page are **pedagogical**. They show the **Fuji AVAX → dual gas-unit ledger** story with arithmetic that matches the Inbox fee-manager shape. They **do not** replace live Fuji Inbox / oracle configuration for your deployment. diff --git a/privacy-on-avalanche/images/pod-infographics-2.png b/privacy-on-avalanche/images/pod-infographics-2.png new file mode 100644 index 0000000..81ab028 Binary files /dev/null and b/privacy-on-avalanche/images/pod-infographics-2.png differ diff --git a/privacy-on-avalanche/images/pod-infographics.png b/privacy-on-avalanche/images/pod-infographics.png new file mode 100644 index 0000000..6e14e98 Binary files /dev/null and b/privacy-on-avalanche/images/pod-infographics.png differ diff --git a/privacy-on-avalanche/networks/README.md b/privacy-on-avalanche/networks/README.md new file mode 100644 index 0000000..0bf0b59 --- /dev/null +++ b/privacy-on-avalanche/networks/README.md @@ -0,0 +1,28 @@ +# Networks (Privacy on Avalanche) + +Privacy on Avalanche spans **two domains**: + +1. **Avalanche Fuji C-Chain** — where your dApp contracts, assets (paid in **AVAX**), and the local **Inbox** live. +2. **COTI Testnet** — where private computation runs via the **MPC executor** and the COTI-side **Inbox**. + +The pages below list network parameters and deployed contract addresses for the current Fuji ↔ COTI Testnet pairing. Addresses can change after redeploys; treat [`@coti-io/pod-sdk`](https://github.com/coti-io/coti-sdk-pod) / [`@coti-io/coti-contracts`](https://github.com/coti-io/coti-contracts) and your environment config as the live source of truth when building against a specific release. + +| Network | Chain ID | Role | +| --- | --- | --- | +| [Avalanche Fuji](fuji.md) | `43113` | **Host chain** — dApp + Inbox (native token: AVAX) | +| [COTI Testnet](coti-testnet.md) | `7082400` | **Private execution** — MPC executor + COTI Inbox | + +The shared CREATE3 **Inbox** address is the same on every supported PoD chain: + +`0xAb625bE229F603f6BBF964474AFf6d5487e364De` + +**Quick links for Fuji builders** + +- RPC: `https://api.avax-test.network/ext/bc/C/rpc` +- Explorer: [testnet.snowscan.xyz](https://testnet.snowscan.xyz) +- Solidity preset: `PodUserFuji` +- SDK constant: `FUJI_DEFAULT_INBOX_ADDRESS` + +Start here if you need wallet + faucet + Hardhat/Foundry first: **[Getting started on Avalanche Fuji (Day 0)](../getting-started-fuji.md)**. + +For general COTI chain RPC and faucet details (not PoD-specific), see the top-level [Networks](../../networks/README.md) section. diff --git a/privacy-on-avalanche/networks/coti-testnet.md b/privacy-on-avalanche/networks/coti-testnet.md new file mode 100644 index 0000000..313f001 --- /dev/null +++ b/privacy-on-avalanche/networks/coti-testnet.md @@ -0,0 +1,62 @@ +# COTI Testnet + +COTI Testnet is the **private execution** side of Privacy on Demand. Host-chain dApps (for example on [Avalanche Fuji](fuji.md)) send encrypted work through the cross-chain Inbox; COTI runs the private computation and returns encrypted results. + +> **Note:** Addresses below reflect the current PoD test deployment. They may change after contract redeploys. Confirm against your SDK release or environment before production use. + +## Network details + +| Parameter | Value | +| --- | --- | +| Network name | COTI Testnet | +| Chain ID | `7082400` | +| Currency | COTI | +| RPC URL | `https://testnet.coti.io/rpc` | +| WebSocket URL | `wss://testnet.coti.io/ws` | +| Block explorer | [https://testnet.cotiscan.io](https://testnet.cotiscan.io) | +| Status | [https://uptime.coti.io](https://uptime.coti.io) | + +Also see the general COTI [TestNet](../../networks/testnet/README.md) page for MetaMask setup and the [Testnet faucet](../../networks/testnet/faucet.md). + +## PoD contracts + +| Contract | Address | Description | +| --- | --- | --- | +| Inbox | [`0xAb625bE229F603f6BBF964474AFf6d5487e364De`](https://testnet.cotiscan.io/address/0xAb625bE229F603f6BBF964474AFf6d5487e364De) | Cross-chain message router (CREATE3; same address on every PoD chain) | +| MPC executor | [`0x68e151b78d51cea01eef6ee354579e044606a739`](https://testnet.cotiscan.io/address/0x68e151b78d51cea01eef6ee354579e044606a739) | Entry point for library-style private operations (`configureCoti` target on host dApps) | +| Price oracle | [`0xe1afeda542d1b9003df0e133187f845dea8b1ba8`](https://testnet.cotiscan.io/address/0xe1afeda542d1b9003df0e133187f845dea8b1ba8) | Local/remote token prices used by Inbox fee conversion | +| Pod ERC20 mother | [`0x293daf267bf657b0bae870a0ce8cd59f1e5eb32a`](https://testnet.cotiscan.io/address/0x293daf267bf657b0bae870a0ce8cd59f1e5eb32a) | COTI-side registry for Privacy Portal / pToken flows | + +## Protocol helpers (COTI Testnet) + +These are not PoD-specific, but most PoD clients need them for account onboarding and chain tooling: + +| Contract | Address | +| --- | --- | +| AccountOnboard | [`0x536A67f0cc46513E7d27a370ed1aF9FDcC7A5095`](https://testnet.cotiscan.io/address/0x536A67f0cc46513E7d27a370ed1aF9FDcC7A5095) | +| MPCInterface (precompile) | `0x0000000000000000000000000000000000000064` | + +Full COTI Testnet protocol addresses: [Contracts Addresses](../../networks/testnet/contracts-addresses.md). + +## How this network fits PoD + +| Piece | Role on COTI Testnet | +| --- | --- | +| Inbox | Receives mined requests from host chains and routes callbacks back | +| MPC executor | Target address host dApps pass to `configureCoti(..., COTI_TESTNET_CHAIN_ID)` | +| Price oracle | Converts fee budgets between host gas units and COTI | + +Typical pairing for this section: + +- **Host:** [Avalanche Fuji C-Chain](fuji.md) · **Execution:** COTI Testnet (see the [private Adder tutorial](../tutorial-private-adder-fuji.md)) + +## SDK constants + +In `@coti-io/pod-sdk` and `@coti-io/coti-contracts`: + +| Constant | Value | +| --- | --- | +| Chain ID | `7082400` | +| Inbox | `0xAb625bE229F603f6BBF964474AFf6d5487e364De` (`COTI_TESTNET_DEFAULT_INBOX_ADDRESS`) | + +Confirm the MPC executor against your deployed release — presets in `PodNetworkConstants` can lag a redeploy; wire the address from the table above (or your env) in `configureCoti`. diff --git a/privacy-on-avalanche/networks/fuji.md b/privacy-on-avalanche/networks/fuji.md new file mode 100644 index 0000000..75d749d --- /dev/null +++ b/privacy-on-avalanche/networks/fuji.md @@ -0,0 +1,75 @@ +# Avalanche Fuji C-Chain + +Avalanche Fuji C-Chain is the **primary host chain** for this Privacy on Avalanche section. Your dApp contracts and the local **Inbox** live here; fees are paid in **AVAX**; private computation still runs on [COTI Testnet](coti-testnet.md). + +Use the Solidity preset **`PodUserFuji`** (or configure the Inbox + COTI executor manually) so Fuji dApps route to COTI Testnet chain ID `7082400`. + +> **Note:** Addresses below reflect the current PoD test deployment. They may change after contract redeploys. Confirm against your SDK release or environment before production use. + +## Network details + +| Parameter | Value | +| --- | --- | +| Network name | Avalanche Fuji C-Chain | +| Chain ID | `43113` | +| Currency | AVAX | +| RPC URL | `https://api.avax-test.network/ext/bc/C/rpc` | +| Block explorer | [https://testnet.snowscan.xyz](https://testnet.snowscan.xyz) | + +Private execution for Fuji dApps targets **COTI Testnet** (`7082400`). See [COTI Testnet](coti-testnet.md) for the MPC executor and COTI-side Inbox. + +## PoD contracts + +| Contract | Address | Description | +| --- | --- | --- | +| Inbox | [`0xAb625bE229F603f6BBF964474AFf6d5487e364De`](https://testnet.snowscan.xyz/address/0xAb625bE229F603f6BBF964474AFf6d5487e364De) | Cross-chain message router (CREATE3; same address on every PoD chain) | +| Price oracle | [`0xbf615045803edb0dbb2c9b9afedcd782c383a09b`](https://testnet.snowscan.xyz/address/0xbf615045803edb0dbb2c9b9afedcd782c383a09b) | Local/remote token prices used by Inbox fee conversion | +| MpcAdder (example) | [`0xf51c789e4c60d33a2abcfd656e607724f56aaf0d`](https://testnet.snowscan.xyz/address/0xf51c789e4c60d33a2abcfd656e607724f56aaf0d) | Reference primitive-only adder dApp on Fuji | + +## Privacy Portal (Fuji) + +Deployed factory and implementations for private-token flows on Fuji: + +| Contract | Address | +| --- | --- | +| Privacy Portal factory | [`0x5230856b5ce21ee5efdd113f8d4849a4b02d3a57`](https://testnet.snowscan.xyz/address/0x5230856b5ce21ee5efdd113f8d4849a4b02d3a57) | +| Portal implementation | [`0x01dee3b8046a94896fe97d531aa2019ea2557ce3`](https://testnet.snowscan.xyz/address/0x01dee3b8046a94896fe97d531aa2019ea2557ce3) | +| Pod token implementation | [`0x0c8ec0f93cae026db214755388d2c41c8b5be08d`](https://testnet.snowscan.xyz/address/0x0c8ec0f93cae026db214755388d2c41c8b5be08d) | + +### Privacy Portal tokens + +| Token | Underlying | Portal | pToken | +| --- | --- | --- | --- | +| pMTT | `0x328e70e1c52662cd5f19f824fcb8b463d77a6686` | `0x9a82B356c9f7F59aE2c04200358122eb99aE6364` | `0xc1258E5C04A6933940105FE94cC9A4C2439d3402` | +| pUSDC | `0x5425890298aed601595a70AB815c96711a31Bc65` | `0x32146Ce8a96F2b0102c5C0f27e45E53eA30ef18B` | `0xAd40C2E98ef9d37827F5206D0a6641c29f2247c5` | +| pWAVAX | `0xd00ae08403B9bbb9124bB305C09058E32C39A48c` | `0xdf75091D4AF11b94FA318eeBC6CEBC106899E25C` | `0x44392E67f6Fc5179BD25302519b1B85B87afc5D8` | + +## How this network fits PoD + +| Piece | Role on Fuji | +| --- | --- | +| Inbox | Host-side courier: accepts encrypted requests from your dApp and delivers COTI callbacks | +| Your dApp | Configures Inbox + COTI chain ID `7082400` + [MPC executor on COTI Testnet](coti-testnet.md#pod-contracts) | +| Price oracle | Converts Fuji fee budgets against COTI-side costs | + +Flow at a glance: **user / dApp on Fuji → Fuji Inbox → (relayer) → COTI Inbox → MPC executor → callback to Fuji**. + +## SDK constants + +In `@coti-io/pod-sdk` / `@coti-io/coti-contracts`: + +| Constant | Value | +| --- | --- | +| Chain ID | `43113` (`AVALANCHE_FUJI_CHAIN_ID`) | +| Inbox | `0xAb625bE229F603f6BBF964474AFf6d5487e364De` (`FUJI_DEFAULT_INBOX_ADDRESS`) | +| Solidity preset | `PodUserFuji` — sets Fuji Inbox and configures COTI Testnet MPC executor | + +Point `configureCoti` at the [COTI Testnet MPC executor](coti-testnet.md#pod-contracts) and chain ID `7082400` if you are not using `PodUserFuji`. + +## Getting Fuji AVAX and exploring txs + +Step-by-step wallet, faucet, and toolchain setup: **[Getting started on Avalanche Fuji (Day 0)](../getting-started-fuji.md)**. + +- Fund a wallet with **test AVAX**, then deploy and call your dApp with enough `msg.value` for two-way Inbox fees. +- Verify contracts and transactions on [SnowScan testnet](https://testnet.snowscan.xyz). +- Hands-on next step: [Tutorial: private Adder on Avalanche Fuji](../tutorial-private-adder-fuji.md). diff --git a/privacy-on-avalanche/pod-for-dummies.md b/privacy-on-avalanche/pod-for-dummies.md new file mode 100644 index 0000000..dfc37c4 --- /dev/null +++ b/privacy-on-avalanche/pod-for-dummies.md @@ -0,0 +1,124 @@ +# Privacy on Avalanche (PoD) for Dummies + +![Pod for Dummies Infographics](../.gitbook/assets/pod-infographics.png) + +## Why care about privacy on a public blockchain? + +Public blockchains such as **Avalanche** are designed for transparency. Every transaction, contract call and balance is visible to anyone. This openness provides agreement, auditability and open participation, but it comes at a cost: sensitive information can leak. There are many situations where you **do not** want to expose the details of a transaction or an internal computation – for example, when handling salaries, bidding in an auction, running a confidential vote or computing a credit score. Traditional options are limited: you either keep the sensitive data off‑chain (breaking composability) or move everything to a specialized privacy chain (which may not fit your liquidity or regulatory needs). + +**Privacy on Avalanche** applies **Privacy on Demand (PoD)** to **Avalanche Fuji C-Chain**. Keep wallets, AVAX, and dApp contracts on Avalanche while off‑loading **sensitive computation** to COTI. Applications send **encrypted inputs** to COTI, receive **encrypted outputs** back on Fuji, and decrypt them locally. In other words: **Avalanche for coordination**, **COTI for the sensitive math and data handling**. + +## What is PoD? + +PoD is a pattern for **private computation on COTI** while continuing to use your existing blockchain for tokens and business logic. In simple terms: + +* **Privacy on Avalanche / PoD** – A way to do calculations secretly on COTI while keeping accounts and assets on **Avalanche Fuji**. +* **Inbox** – An on‑chain mailbox. It forwards your encrypted request to COTI and brings back the answer. Think of it as a certified courier carrying sealed letters. +* **MPC executor** – COTI’s private calculator. It runs the secret maths and returns encrypted results. + +The sequence is straightforward: + +1. **Stay on Avalanche.** Your application continues to run on **Avalanche Fuji C-Chain** just as it does today. +2. **Encrypt sensitive data.** The user’s app turns the input into an encrypted parcel before sending it. The cleartext never appears on the chain. +3. **Send via the Inbox.** The application forwards the parcel through the Inbox to COTI. +4. **Compute privately on COTI.** COTI performs the secret computation and produces an encrypted answer. +5. **Get the result and decrypt it.** The encrypted answer comes back through the Inbox. Only the user can open it with their key. + +This process hides amounts, scores or business rules while still benefiting from the public blockchain’s security and composability. + +## How does a PoD request travel end to end? + +To understand PoD, let’s follow a request from the user’s perspective without getting into programming details. There are a few key actors in the story: the **End user**, the **client app**, **your dApp contract**, and the **PoD infrastructure** that connects your chain with COTI. Each plays a different part in the journey: + +| Actor | Role | +| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| **End user** | A person using a wallet or app who approves transactions and holds the decryption key. | +| **Client app** | A browser or mobile app that encrypts inputs and later decrypts the result on the user’s device. | +| **Your dApp contract** | A smart contract on your chain that defines business rules, stores correlation IDs and holds encrypted results. | +| **PoD infrastructure** | The combination of the Inbox and the private compute engine on COTI. It forwards your encrypted request to COTI, performs the secret calculation and returns an encrypted result back to your contract. In other words, it is the bridge and calculator that connects two chains together. | + +The **journey** happens in seven steps: + +1. **User chooses an action.** This could be comparing two numbers, making a private payment or running a private score. +2. **Client encrypts inputs.** The app turns the user’s data into an encrypted `it*` payload; the plaintext never hits the chain. +3. **User submits an EVM transaction.** The dApp contract checks permissions, payments and other rules. +4. **Contract talks to the PoD infrastructure.** Your contract sends the encrypted job through the PoD infrastructure – the message is forwarded to COTI and a promise to return the answer is recorded. +5. **PoD infrastructure computes and returns.** On the COTI side, the private engine performs the secret computation and then the PoD infrastructure delivers the encrypted result back to your contract. You don’t see the intermediate steps; it’s all handled behind the scenes. +6. **User decrypts the result.** The client fetches the ciphertext and decrypts it locally with their account key. + +Because private execution happens on another chain, PoD is **asynchronous**: the result does not arrive in the same transaction. Users should think of it like submitting a job and waiting for a callback. UI designers should show _Pending_, _Completed_ and _Failed_ states, and teams need indexing to match callbacks to requests. Decryption always happens client‑side; no plaintext is ever written on‑chain. + +![Infographics](../.gitbook/assets/pod-infographics-2.png) + +## Behind the scenes + +You don’t need to be a developer to understand the basic parts involved. Think of the system as three rooms connected by a mail slot: + +1. **Your device.** This is your wallet or app. It holds your keys and encrypts the information you want to keep private. +2. **Avalanche Fuji C-Chain.** This is the host network where your tokens and smart contracts live (fees in **AVAX**). It includes the **Inbox**, which is just a mailbox that forwards your encrypted request and later delivers the reply. +3. **The private compute room (COTI).** This is COTI’s secure environment. Inside, a private engine performs the computation and produces an encrypted answer. + +When you use PoD, you send a sealed envelope through the mailbox. The envelope travels to the private room, where a trusted party reads the encrypted contents, performs the requested calculation and seals the result. The sealed reply comes back through the mailbox. Only you can open it because you hold the key. All of the routing and encryption details are handled by the underlying tools – there’s no need to understand smart contracts or special libraries. + +### Optional: Your dApp’s private execution on COTI + +For many applications, the built‑in PoD operations are enough – you can compare values, add numbers or perform common checks without writing any special code. However, if your dApp needs unique functionality (for example, a bespoke scoring algorithm, a secret auction mechanism or a complex data transformation), there is an option to run your **own private logic** on COTI. In this section – **Your dApp’s private execution on COTI** – we outline how developers can extend PoD beyond the standard operations: + +* **Custom private code:** Developers can deploy a program on COTI that defines the logic for their private computation. It only accepts encrypted inputs and returns encrypted outputs, so its inner workings and data remain hidden from everyone else. +* **Connected by PoD infrastructure:** Your dApp contract still sends encrypted requests through the PoD infrastructure. The infrastructure routes them to your private program on COTI and brings back the result. + +From a user’s perspective, nothing changes – you still submit a job and receive an encrypted answer. Behind the scenes, though, your dApp’s “secret sauce” runs privately on COTI. This option allows businesses to implement privacy‑preserving features tailored to their needs without exposing sensitive logic or data. In short, **PoD infrastructure** remains your bridge between chains, but you can choose to host your app’s private components on COTI rather than relying solely on the built‑in operations. + +## What can you use PoD for? + +PoD unlocks a range of applications that were previously difficult or impossible on public blockchains. Here are a few examples: + +* **Confidential payments:** Private transfers of tokens or stablecoins where only the sender and receiver know the amount. Useful for payroll, royalties and peer‑to‑peer payments. +* **Fair auctions and secret bids:** Run sealed‑bid auctions where bids remain hidden until the auction closes. This prevents front‑running and bid collusion. +* **Private DeFi strategies:** Execute trades or calculate liquidations without revealing positions or strategies to competitors. PoD can shield orders from MEV bots and protect traders from being sandwiched. +* **Secure credit scoring and KYC checks:** Perform scoring or identity verification on encrypted data. A dApp can check that a user’s credit score exceeds a threshold without seeing the raw score. +* **Confidential voting and governance:** Cast votes privately and prove that votes were counted correctly without exposing individual choices. Ideal for DAOs, corporate boards or community governance. +* **Secret games and NFTs:** Reveal the outcome of a game or the attributes of an NFT only to the owner, keeping the randomness or rarity hidden until needed. + +These examples share a common pattern: sensitive inputs stay encrypted, computation happens in a private environment and only authorized parties see the outputs. PoD makes these patterns accessible to developers building on EVM chains. + +## Why PoD operations are asynchronous + +In a typical smart‑contract interaction you send a transaction and immediately see the result. PoD is different because private execution happens off‑chain in COTI’s secure environment. The call is **asynchronous**: you submit a job, then wait for a callback containing the encrypted result. There are three phases: + +1. **Request transaction:** your dApp contract submits the encrypted job through the Inbox and receives a request ID. +2. **Wait:** COTI performs the private computation. This may take a few seconds or longer, depending on network conditions. +3. **Callback transaction:** the Inbox calls your contract’s callback function with the encrypted output. + +Because of this asynchronous nature, the user experience must include pending/completed/failed states and error handling. After completion, the plaintext result is **not** public on‑chain; the user’s client decrypts it locally. Thinking of PoD as “send a job, get a sealed reply” helps set expectations. + +## Why is there a fee? + +Every PoD call uses resources on **two** networks: **Avalanche Fuji** and the COTI chain. When you send a two‑way message through the Inbox, you pay a fee in **AVAX**. This fee funds two things: + +1. **Private computation on COTI.** The MPC executor executes the garbled‑circuit logic and relays the request. This consumes gas and relayer work on the COTI side. +2. **Callback on Fuji.** After computation, the Inbox must relay the result back and execute your contract’s callback function, consuming gas on Avalanche C-Chain. + +In the worked Fuji example, a user sends **0.02 AVAX** as `msg.value` and reserves **0.008 AVAX** as `callbackFeeLocalWei` for the Fuji return path; the remaining **0.012 AVAX** funds the COTI leg after oracle conversion (AVAX/COTI prices). Because AVAX is typically much more valuable per token than COTI, even a small AVAX remote slice can become a large COTI gas budget — that is expected. Underfunding either side can leave the request stuck in a pending state: too little `msg.value` (or remote slice) hurts the outbound leg; too little `callbackFeeLocalWei` prevents the Fuji callback from writing the result. Details and arithmetic: [How do PoA fees work on Avalanche Fuji?](how-poa-fees-work.md). + +## Benefits and limitations + +**Benefits:** + +* **Seamless integration:** PoD lets you add privacy to existing dApps without migrating assets or users. You keep your tokens and workflows on your favourite chain. +* **Strong confidentiality:** Sensitive inputs and outputs stay encrypted; computation happens in a secure enclave and only authorized clients decrypt results. +* **Flexible and composable:** Future tools let developers build simple or complex private operations without needing to rebuild everything from scratch. You won’t notice a difference; your app simply turns privacy on when needed. +* **Regulatory alignment:** PoD enables compliant confidentiality – data can be audited on the public chain while personal or business details remain protected. +* **Cross‑chain future:** COTI is building cross‑chain capabilities so that privacy can be turned on or off across many chains, further expanding its reach (as announced on Medium in Nov 2024). + +**Limitations to consider:** + +* **Not full anonymity:** PoD does not hide metadata such as gas costs, timing or which contract was called. A determined observer might infer patterns unless you design around them. +* **Asynchronous UX:** Users must wait for callbacks. UIs need to handle pending states and errors gracefully. +* **Fee planning:** Each leg of the job requires funding. Underestimating fees can leave jobs hanging. +* **Key management:** Users need to manage client‑side encryption keys. Losing a device or key can mean losing access to decrypted outputs. +* **Not legal advice:** PoD is a technology, not a substitute for regulatory compliance. Teams must consult legal experts for their specific use case. + +## Conclusion + +Privacy on Demand brings a pragmatic approach to confidential computing on public blockchains. By combining the openness of EVM chains with the privacy of COTI’s garbled‑circuit engine, builders can create applications that protect users’ confidentiality without sacrificing composability or liquidity. The pattern is simple: **encrypt, send via Inbox, compute privately, receive and decrypt**. The underlying developer tools handle the routing and cryptography so you don’t need to understand the underlying smart‑contract details. While PoD introduces new considerations (fees, asynchronous UX, key management), it unlocks a world of private payments, fair auctions, confidential DeFi and secure voting – all from the comfort of your existing blockchain stack. diff --git a/privacy-on-avalanche/tutorial-custom-logic.md b/privacy-on-avalanche/tutorial-custom-logic.md new file mode 100644 index 0000000..afae6e2 --- /dev/null +++ b/privacy-on-avalanche/tutorial-custom-logic.md @@ -0,0 +1,159 @@ +# Tutorial: custom privacy logic with PoD + +This tutorial describes the **second PoD integration model**: a **custom COTI-side contract** paired with a **host-chain (for example Avalanche Fuji) contract**. Use this path when [primitive-only `PodLib` / MpcLib operations](tutorials-privacy-on-avalanche.md#1-primitive-only-dapps-mpclib--podlib) are not enough for your business logic. + +For the conceptual split between the two models and a comparison diagram, start at **[Tutorials: building Privacy on Avalanche (PoD) dApps](tutorials-privacy-on-avalanche.md)**. + +## Why two contracts? + +The **host EVM chain** is excellent for accounts, assets, and user-facing flows, but your Fuji (or mainnet) contract **cannot** host the full garbled-circuit lifecycle: it is not the right place to keep **intermediate encrypted MPC state** (`gt*`) and arbitrarily compose it the way the **COTI** execution environment does. + +So you **partition** the design: + +- **Fuji side** — Collect **encrypted user inputs** (`it*`), pay Inbox fees, and issue **remote calls** to COTI. In the success path, receive **encrypted outputs** (`ct*`) in an **`onlyInbox` callback** and store or emit them for clients. Think of this layer as a **thin, client-facing orchestrator**. +- **COTI side** — Implement the **actual private logic**: receive `gt*` / `it*` payloads from the Inbox, run `MpcCore` operations, optionally **off-board** ciphertext to a specific user (`offBoardToUser`), and **`inbox.respond`** with an ABI-encoded blob the Fuji callback decodes. + +The example below mirrors an **encrypted messaging** flow: the message is handled privately on COTI so the recipient-bound ciphertext is produced in a controlled way; Avalanche Fuji only routes the call and persists **`ctString`** for the UI. + +## Security habits on the host chain + +Any **`onlyInbox` callback** that mutates state must **verify the caller** is the Inbox **and** that the **source COTI contract and chain** match your deployment. Otherwise a malicious remote peer could try to impersonate your COTI logic. + +Always use something equivalent to: + +```solidity +(uint256 callerChain, address callerContract) = IInbox(inbox).inboxMsgSender(); +require(callerChain == EXPECTED_COTI_CHAIN_ID && callerContract == myCotiPeer, "unauthorized peer"); +``` + +Match **`EXPECTED_COTI_CHAIN_ID`** and **`myCotiPeer`** to the values you configured at deploy time (the SDK presets expose constants such as `COTI_TESTNET_CHAIN_ID` on `PodUserFuji`). + +## COTI-side contract: `DirectMessageCotiSide` + +The COTI contract **inherits `InboxUser`**, so only the Inbox can enter `receiveMessage`. It: + +1. Accepts the garbled string and routing metadata (`sender`, `recipient`). +2. Uses **`MpcCore.offBoardToUser`** to convert the payload into **`ctString` ciphertext that only `recipient` can decrypt**. +3. Returns that ciphertext to the host chain via **`inbox.respond`**, ABI-encoding `sender`, `recipient`, and the ciphertext so the Fuji callback can update storage deterministically. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "../InboxUserCotiTestnet.sol"; +import "@coti-io/coti-contracts/contracts/utils/mpc/MpcCore.sol"; + +contract DirectMessageCotiSide is InboxUserCotiTestnet { + function receiveMessage(gtString calldata message, address sender, address recipient) external onlyInbox { + // Off-board the garbled string into a cipher state exclusively for the recipient + ctString memory cipherForRecipient = MpcCore.offBoardToUser(message, recipient); + inbox.respond(abi.encode(sender, recipient, cipherForRecipient)); + } +} +``` + +Paths like `../InboxUser.sol` assume you follow the SDK’s example layout; adjust imports to your repo. + +## Avalanche Fuji-side contract: orchestration only + +The Fuji contract **inherits `PodUserFuji`** (or your network’s `PodUser` preset), tracks the **COTI peer address**, and: + +1. **`sendMessage`** — Wraps encrypted input (`itString`) and public addresses in an **`IInbox.MpcMethodCall`** built with **`MpcAbiCodec`** (see the SDK’s [Request builder and remote calls](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/contracts/03-request-builder-and-remote-calls.md)). It sends a **two-way** message so the result comes back asynchronously. +2. **`onMessageReceived`** — Decodes the tuple produced on COTI, **re-checks `inboxMsgSender()`**, and stores **`ctString`** keyed by conversation participants (or whatever your product needs). + +The Solidity below is **structurally** correct; wire **`MpcAbiCodec`**’s `create` / `addArgument` / `build` steps exactly as in your installed `@coti-io/coti-contracts` version (argument order and `gt`/`it` interface types **must** match the COTI method signature). + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {PodUserFuji} from "../mpc/PodUserFuji.sol"; +import {MpcAbiCodec} from "../mpccodec/MpcAbiCodec.sol"; +import {IInbox} from "../IInbox.sol"; + +// Interface MUST match the COTI `DirectMessageCotiSide.receiveMessage` ABI +// (including `gt` vs `it` types on each parameter). +interface IDirectMessagePod { + function receiveMessage(gtString calldata message, address sender, address recipient) external; +} + +contract DirectMessageEvm is PodUserFuji { + using MpcAbiCodec for MpcAbiCodec.MpcMethodCallContext; + + mapping(address => mapping(address => ctString)) public encryptedMessages; + address public myCotiPeer; + + event MessageDispatched(bytes32 indexed requestId, address indexed sender, address indexed recipient); + event MessageReply(bytes32 indexed requestId, address indexed sender, address indexed recipient); + + constructor(address _cotiPeer) { + myCotiPeer = _cotiPeer; + } + + /// @notice Dispatch encrypted `itString` to COTI; fee semantics match other two-way Pod calls. + function sendMessage( + itString memory encryptedMessage, + address recipient, + uint256 callbackFeeLocalWei + ) external payable { + MpcAbiCodec.MpcMethodCallContext memory callCtx = + MpcAbiCodec.create(IDirectMessagePod.receiveMessage.selector, 3); + callCtx = callCtx.addArgument(encryptedMessage); + callCtx = callCtx.addArgument(msg.sender); + callCtx = callCtx.addArgument(recipient); + IInbox.MpcMethodCall memory methodCall = callCtx.build(); + + bytes32 requestId = IInbox(inbox).sendTwoWayMessage{value: msg.value}( + COTI_TESTNET_CHAIN_ID, + myCotiPeer, + methodCall, + this.onMessageReceived.selector, + this.onDefaultMpcError.selector, + callbackFeeLocalWei + ); + emit MessageDispatched(requestId, msg.sender, recipient); + } + + /// @notice COTI contract executed `inbox.respond(abi.encode(sender, recipient, cipherForRecipient))`. + function onMessageReceived(bytes memory resultData) external onlyInbox { + (uint256 callerChain, address callerContract) = IInbox(inbox).inboxMsgSender(); + require(callerChain == COTI_TESTNET_CHAIN_ID && callerContract == myCotiPeer, "not allowed"); + + bytes32 requestId = IInbox(inbox).inboxSourceRequestId(); + (address sender, address recipient, ctString memory message) = + abi.decode(resultData, (address, address, ctString)); + + encryptedMessages[sender][recipient] = message; + emit MessageReply(requestId, sender, recipient); + } +} +``` + +### TypeScript and deployment + +This chapter stops at **Solidity** to highlight the **chain split**. For **encryption**, **Inbox fee estimation**, and **client-side decryption** of `ctString`, continue with: + +- [Tutorial: private Adder on Avalanche Fuji](tutorial-private-adder-fuji.md) — includes **`PodContract`**, **`encryptAndCallMethod`**, **`estimateFee`**, and **`extractRequestIds`** so you can copy the same client pattern to **`sendMessage`** (build **`PodMethodArgument[]`** with types that match your ABI: **`itString`** for the ciphertext argument, plain types for addresses and the callback-fee slot, **`isCallBackFee: true`** on the fee parameter). +- [TypeScript PoD SDK (`CotiPodCrypto`, `PodContract`)](typescript-pod-sdk.md) — short reference for **`CotiPodCrypto`** and **`PodContract`** with links to [`coti-pod-crypto.ts`](https://github.com/coti-io/coti-sdk-pod/blob/main/src/coti-pod-crypto.ts) and [`pod-method-call.ts`](https://github.com/coti-io/coti-sdk-pod/blob/main/src/pod-method-call.ts). +- [TypeScript integration — SDK](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/06-typescript-integration-ux-development.md) +- [Writing privacy contracts on Ethereum — SDK](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/05-writing-privacy-contracts-on-ethereum.md) (custom mode) + +After **`encryptAndCallMethod("sendMessage", args, feeCfg)`** (or a raw **`ethers.Contract`** send), use **`await pod.extractRequestIds(receipt.hash)`** on the same **`PodContract`** instance so your UI stores the **`requestId`** emitted in the Inbox **`MessageSent`** logs—same helper as in the adder walkthrough. + +## Summary + +| Piece | Responsibility | +| --- | --- | +| **`DirectMessageCotiSide`** | Private transformation (`gtString` → recipient-bound `ctString`) and **`inbox.respond`**. | +| **`DirectMessageEvm`** | Fee-bearing Inbox send, **peer verification**, and storing **`ctString`** for the app. | +| **Client** | Encrypt inputs, submit transactions, decrypt outputs using the account key flow from the SDK. | + +When your feature fits this shape—**heavy private logic on COTI**, **Avalanche Fuji as router and ciphertext cache**—you are in the **custom PoD dApp** model described on the [tutorials overview](tutorials-privacy-on-avalanche.md#2-custom-pod-dapps-host-chain--coti-contracts). + + diff --git a/privacy-on-avalanche/tutorial-private-adder-fuji.md b/privacy-on-avalanche/tutorial-private-adder-fuji.md new file mode 100644 index 0000000..0c5e03d --- /dev/null +++ b/privacy-on-avalanche/tutorial-private-adder-fuji.md @@ -0,0 +1,259 @@ +# Tutorial: private Adder on Avalanche Fuji + +This walkthrough is the **primitive-only** path: your host-chain contract calls **`PodLib`** helpers (the SDK surface for **MpcLib**-style primitives) and never deploys custom Solidity on COTI. If you are unsure whether that is enough for your product, read **[Tutorials: building Privacy on Avalanche (PoD) dApps](tutorials-privacy-on-avalanche.md)** first. + +This guide shows how to build a minimal **Privacy on Demand** dApp that **adds two encrypted integers** on COTI and stores the **encrypted sum** on your EVM contract. It follows the same ideas as the SDK’s [MpcAdder.sol](https://github.com/cotitech-io/coti-pod-sdk/blob/main/contracts/examples/MpcAdder.sol) example, extended with **Avalanche Fuji routing presets** and **request correlation** suitable for a real UI. + +For background on async flows and fees, see [Async private operations](async-private-operations.md), [How do PoA fees work?](how-poa-fees-work.md), and 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. + +## Writing a PoD example + +In this example we will do the following: + +1. **Use one built-in executor operation** from `PodLib` (the sample below uses **`add256`**) so you do not write a custom COTI contract yet. +2. **Submit a payable request** that forwards `msg.value` and splits out `callbackFeeLocalWei` for the return leg (two-way Inbox message). +3. **Implement a success callback** that decodes `abi.encode(ctUint256)` and stores the ciphertext. +4. **Wire `onDefaultMpcError.selector`** so failed remote runs surface through the SDK’s default error path (and emit `ErrorRemoteCall` from `PodUser`). + +After that works, you harden for production: per-user request ownership, explicit `pending / completed / failed` state, fee estimation via the Inbox, and tests for under-funded sends. The SDK’s [Examples with description](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/05c-examples-with-description.md) lists what the shipped `MpcAdder` omits on purpose. + +## Prerequisites + +Complete **[Getting started on Avalanche Fuji (Day 0)](getting-started-fuji.md)** first (wallet on `43113`, test AVAX, Hardhat/Foundry Fuji network, SnowScan smoke-test). + +- **Solidity toolchain** (Foundry or Hardhat) targeting **Avalanche Fuji C-Chain** (where the SDK’s `PodUserFuji` Inbox is deployed). +- **Node.js 18+** for scripts and `fetch` used by encryption helpers. +- **Fuji AVAX** for deployment and for **`msg.value`** on each `add` call (plus gas). +- **User onboarding** so your client can obtain an **account AES key** for decryption (see the SDK’s [TypeScript integration](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/06-typescript-integration-ux-development.md) and [Onboarding / account AES key](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/06c-onboarding-account-account-aes-key.md) docs). + +Always confirm **Inbox**, **COTI chain id**, and **MPC executor** against `PodUserFuji.sol` / `PodNetworkConstants.sol` in your installed `@coti-io/coti-contracts` package; constants can change between releases. + +## Step 1: Install packages + +```bash +# TypeScript helpers (encrypt / fees / send) +npm install @coti-io/pod-sdk ethers + +# Solidity (PodLib, PodUserFuji, MpcCore) — currently install from GitHub main +npm install github:coti-io/coti-contracts#main +``` + +`@coti-io/pod-sdk` is **TypeScript only**. Solidity imports come from `@coti-io/coti-contracts`. + +## Step 2: Create the `PrivateAdder` contract + +Save as `PrivateAdder.sol`. The contract: + +- Inherits **`PodLib`** and **`PodUserFuji`** (Fuji Inbox + COTI Testnet routing are set in the `PodUserFuji` constructor — do not call `setInbox` / `configureCoti` again). +- Calls **`add256`** with the caller’s encrypted inputs and your callback selector. +- Resolves **`requestId`** in the callback the same way as the SDK’s [Getting started](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/04-getting-started.md) example. + +```solidity +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.26; + +import "@coti-io/coti-contracts/contracts/pod/mpc/PodLib.sol"; +import "@coti-io/coti-contracts/contracts/pod/mpc/PodUserFuji.sol"; +import "@coti-io/coti-contracts/contracts/utils/mpc/MpcCore.sol"; + +/// @title PrivateAdder +/// @notice Adds two encrypted uint64 values via PoD on Avalanche Fuji (SDK preset addresses). +contract PrivateAdder is PodLib, PodUserFuji { + enum RequestStatus { + None, + Pending, + Completed + } + + mapping(bytes32 => ctUint256) public sumByRequest; + mapping(bytes32 => RequestStatus) public statusByRequest; + + event AddRequested(bytes32 indexed requestId, address indexed caller); + event AddCompleted(bytes32 indexed requestId); + + constructor() PodLibBase(msg.sender) {} + + /// @param callbackFeeLocalWei AVAX subunits (18 decimals) reserved for the Fuji callback leg; must be <= msg.value. + function add( + itUint256 calldata a, + itUint256 calldata b, + uint256 callbackFeeLocalWei + ) external payable returns (bytes32 requestId) { + requestId = add256( // Add two encrypted 256 bit integers + a, + b, + msg.sender, // Who can decrypt the result + this.addCallback.selector, + this.onDefaultMpcError.selector, + msg.value, + callbackFeeLocalWei + ); + statusByRequest[requestId] = RequestStatus.Pending; + emit AddRequested(requestId, msg.sender); + } + + function addCallback(bytes memory data) external onlyInbox { + bytes32 requestId = inbox.inboxSourceRequestId(); + if (requestId == bytes32(0)) { + requestId = inbox.inboxRequestId(); + } + + ctUint256 sum = abi.decode(data, (ctUint256)); + sumByRequest[requestId] = sum; + statusByRequest[requestId] = RequestStatus.Completed; + emit AddCompleted(requestId); + } +} +``` + +**Notes:** + +- **`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. + +## Step 3: Compile and deploy on Avalanche Fuji + +Configure remappings / `paths` so `@coti-io/coti-contracts` resolves (Hardhat or Foundry), then compile and deploy `PrivateAdder` to **Avalanche Fuji C-Chain**. Record the deployed address for scripts. + +## Step 4: Budget `msg.value` and `callbackFeeLocalWei` + +Two-way Inbox traffic needs enough native token to cover **outbound execution** and the **callback**. Use the deployed Inbox’s **`calculateTwoWayFeeRequiredInLocalToken`** (or your operator’s runbook) to pick `msg.value` and `callbackFeeLocalWei`. Undershooting typically leaves the request stuck or failing. + +## Step 5: Encrypt the two summands (TypeScript) + +`CotiPodCrypto.encrypt` calls the PoD encryption service. For Fuji-style test usage, pass **`"testnet"`** as the network key (see [`coti-pod-crypto.ts`](https://github.com/coti-io/coti-sdk-pod/blob/main/src/coti-pod-crypto.ts) in the SDK: `testnet` maps to the COTI testnet encryption endpoint). + +Use **`DataType.itUint256`** when you build **`itUint256`** calldata yourself (for example with **`ethers.Contract`**). If you use **`PodContract.encryptAndCallMethod`** in the next step, you can skip manual encryption: pass **plaintext numeric strings** and **`DataType.itUint256`** in each `PodMethodArgument`, and the SDK encrypts before encoding the transaction. + +```typescript +import { CotiPodCrypto, DataType } from "@coti-io/pod-sdk"; + +const plainA = "10"; +const plainB = "20"; + +const encA = await CotiPodCrypto.encrypt(plainA, "testnet", DataType.itUint256); +const encB = await CotiPodCrypto.encrypt(plainB, "testnet", DataType.itUint256); + +``` + +## Step 6: Submit the `add` transaction (`PodContract`, fees, `extractRequestIds`) + +[`PodContract`](https://github.com/coti-io/coti-sdk-pod/blob/main/src/pod-method-call.ts) wraps your **`ethers.Contract`**: it **`estimateFee`**s against the Inbox, maps **`PodMethodArgument`** values (including **`encryptAndCallMethod`** encryption for **`it*`** types), injects the **`callBackFee`** into the slot marked **`isCallBackFee: true`**, sends **`value: totalFee`** on payable functions, and exposes **`extractRequestIds(txHash)`** to read **`requestId`** values from **`MessageSent`** logs on the Inbox (reliable across layouts where parsing logs from the app contract alone is brittle). + +```typescript +import { + PodContract, + DataType, + type PodFeeEstimationConfig, + type PodMethodArgument, +} from "@coti-io/pod-sdk"; +import { ethers } from "ethers"; + +// Minimal ABI fragment — prefer the full artifact from your build (Hardhat / Foundry). +const privateAdderAbi = [ + "function add((uint256 ciphertext,bytes signature),(uint256 ciphertext,bytes signature),uint256) payable returns (bytes32)", +] as const; + +const pod = new PodContract( + privateAdderAddress, + privateAdderAbi, + signer, + { encryptionNetwork: "testnet" } +); + +const args: PodMethodArgument[] = [ + { type: DataType.itUint256, value: "10", isCallBackFee: false }, + { type: DataType.itUint256, value: "20", isCallBackFee: false }, + { type: DataType.Uint256, value: "0", isCallBackFee: true }, +]; + +const feeCfg: PodFeeEstimationConfig = { + forwardGasLimit: 400_000n, + gasPrice: (await signer.provider!.getFeeData()).gasPrice ?? 0n, + callBackGasLimit: 250_000n, + callBackDataSize: 512n, +}; + +const estimated = await pod.estimateFee("add", args, feeCfg); +// estimated.totalFee — forward + callback legs in local wei (see SDK fee docs) + +const txResponse = await pod.encryptAndCallMethod("add", args, feeCfg); +const receipt = await (txResponse as ethers.ContractTransactionResponse).wait(); + +const requestIds = receipt?.hash ? await pod.extractRequestIds(receipt.hash) : []; +const requestId = requestIds[0]; +// requestId is 0x-prefixed bytes32 — keep it for status polling and decryption +``` + +Tune **`forwardGasLimit`**, **`callBackGasLimit`**, and **`callBackDataSize`** from measurements on your contract; **`estimateFee`** requires **`callBackGasLimit`** and **`callBackDataSize`** together or neither. For **`callMethod`** instead of **`encryptAndCallMethod`**, supply **JSON ciphertext strings** for **`it*`** arguments (what the encryption service returns), for example when the browser already called **`CotiPodCrypto.encrypt`**. + +**Alternative (raw `ethers.Contract`)** — If you are not using **`PodContract`**, call **`add`** with **`toitUint256(encA)`**, **`toitUint256(encB)`**, **`callbackFeeLocalWei`**, and **`{ value: totalWei }`**, then still use **`extractRequestIds`** for correlation: + +```typescript +const podRead = new PodContract( + privateAdderAddress, + privateAdderAbi, + signer, + { encryptionNetwork: "testnet" } +); + +const tx = await privateAdder.add( + toitUint256(encA), + toitUint256(encB), + callbackFeeWei, + { value: totalWei } +); +const receipt = await tx.wait(); +const requestIds = receipt?.hash ? await podRead.extractRequestIds(receipt.hash) : []; +``` + +Private addition is **asynchronous**: the sum appears only after the Inbox invokes **`addCallback`** in a later transaction. Poll **`statusByRequest(requestId)`** or wait for **`AddCompleted`**. + +## Step 7: Read the encrypted sum and decrypt locally + +After status is **Completed**, read **`sumByRequest(requestId)`**. The value is **`ctUint256`** (ciphertext), not plaintext. + +```typescript +import { CotiPodCrypto, DataType } from "@coti-io/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 decryptedString = CotiPodCrypto.decrypt( + ctHex, + accountAesKey, + DataType.Uint64 +); + +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/coti-io/coti-sdk-pod/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. +- **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 + +- [`pod-method-call.ts` (`PodContract`, fees, `extractRequestIds`)](https://github.com/coti-io/coti-sdk-pod/blob/main/src/pod-method-call.ts) +- [MpcAdder.sol (minimal repo example)](https://github.com/cotitech-io/coti-pod-sdk/blob/main/contracts/examples/MpcAdder.sol) +- [Examples with description](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/05c-examples-with-description.md) +- [Getting started (PodUserFuji pattern)](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/04-getting-started.md) +- [Async execution](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/05a-async-execution.md) + + diff --git a/privacy-on-avalanche/tutorials-privacy-on-avalanche.md b/privacy-on-avalanche/tutorials-privacy-on-avalanche.md new file mode 100644 index 0000000..d371bcd --- /dev/null +++ b/privacy-on-avalanche/tutorials-privacy-on-avalanche.md @@ -0,0 +1,146 @@ +# Tutorials: building Privacy on Avalanche (PoD) dApps + +PoD applications on **Avalanche Fuji C-Chain** fall into **two integration models**. Choosing the right one early saves rework: most simple flows stay entirely on **Fuji** using shipped primitives (`PodLib` + `PodUserFuji`); richer private logic needs a **custom COTI-side contract** and a thin **Fuji host contract** that only orchestrates encrypted I/O. + +## 1. Primitive-only dApps (MpcLib / PodLib) + +COTI ships a large set of **primitive MPC operations**. For many dApps, composing a few of these is enough: you never author Solidity on COTI, and the **MPC executor** on the COTI network already knows how to run each primitive. + +In the **PoD Solidity SDK**, these primitives are exposed as helpers on **`PodLib`** (and width-specific variants such as **`PodLib64`**, **`PodLib128`**, **`PodLib256`**). The upstream documentation often refers to this layer as the **MPC library**; method names mirror the underlying primitives (for example **`add`**, **`mul`**, …) for each supported width. + +If your business logic **only** needs these operations, and only **once or twice** per user action (or in a similarly small composition), you can usually implement the whole flow from your **host-chain contract** by calling **`PodLib`** helpers, which package the Inbox round-trip to the executor. + +### Available primitive families (per width) + +For **64-, 128-, and 256-bit** lanes, the library surface includes (names may be width-suffixed in Solidity, for example `add64` / `add128` / `add256`): + +**Arithmetic and bitwise:** `add`, `mul`, `div`, `rem`, `and`, `or`, `xor` + +**Min / max:** `min`, `max` + +**Comparisons:** `eq`, `ne`, `ge`, `le`, `lt` + +**Control:** `mux` + +**Shifts:** `shl`, `shr` + +**Randomness:** `randBoundedBits` + +For the authoritative list, signatures, and gas notes, use the SDK’s **[MPC library (PodLib)](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/05b-multi-party-computing-library-mpclib.md)** and **[PodLib.sol](https://github.com/coti-io/coti-contracts/blob/main/contracts/pod/mpc/PodLib.sol)** in your installed `@coti-io/coti-contracts` package. + +### Example tutorial (simple PoD dApp) + +Same topic as the **blue callout at the top of this page**: one primitive (`add`), **`msg.value`** / **`callbackFeeLocalWei`**, encrypted sum stored on the host chain. Follow the link below for the full walkthrough. + + + +### TypeScript SDK guide + +TypeScript usage is documented on a dedicated page so this overview stays short: + +- [TypeScript PoD SDK (`CotiPodCrypto`, `PodContract`)](typescript-pod-sdk.md) — encryption/decryption, `estimateFee`, `encryptAndCallMethod`, `callMethod`, and `extractRequestIds`. + +### Business dApp cookbook + +If you want a walkthrough that starts from a familiar public Solidity app, use the investor-allocation cookbook. It begins with a simple Fuji token-allocation contract, shows why public mappings leak sensitive launch data, then evolves the app into a custom PoD flow with private allocation reads, async callbacks, fee estimation, decryption, withdrawal execution, and explorer-based lifecycle checks. + + + +--- + +## 2. Custom PoD dApps (host chain + COTI contracts) + +When your logic **goes beyond** what the primitives cover, or you need **non-trivial composition**, **custom types**, or **stateful private processing** on ciphertext, you design a **custom PoD dApp** with **two contracts**: + +| Layer | Role | +| --- | --- | +| **Host EVM (Avalanche Fuji C-Chain)** | **Client-facing orchestration**: accept encrypted user inputs (`it*`), forward work to COTI via the Inbox, receive encrypted outputs (`ct*`) in callbacks. It does **not** hold or meaningfully process **intermediate garbled / MPC-internal state** (`gt*`) the way the COTI execution environment can. | +| **COTI** | **Private compute and private state**: run garbled-circuit logic, store or transform encrypted data off the host chain’s MPC limitations, and return selectively **user-bound** ciphertext (for example via `MpcCore.offBoardToUser`) or other encoded results. | + +Typical **Avalanche Fuji** flow in this model: + +1. Obtain **encrypted user data** on the client and pass it to your host-chain contract. +2. Send a **two-way Inbox** message that invokes a **method on your deployed COTI-side contract** (built with `MpcAbiCodec` and the patterns in the SDK’s “custom mode” docs). +3. The COTI contract runs the private logic and calls **`inbox.respond`** with an ABI-encoded payload your Fuji callback understands. +4. The host chain stores or forwards **encrypted** results (`ct*`) for the user or for follow-up calls. + +### Example tutorial (custom logic, Solidity excerpts) + +**Encrypted messaging** sketch: an **Avalanche Fuji** `PodUserFuji` contract dispatches calls, and a **COTI-side** `DirectMessageCotiSide` contract uses `MpcCore.offBoardToUser` so only the recipient receives a decryptable ciphertext. That page focuses on the **split responsibility** between chains and on **verifying `inboxMsgSender()`** in callbacks. Follow the link below for the Solidity walkthrough. + + + +--- + +## How the two models differ (diagram) + +At a high level, the **adder** stays on **primitives + executor**; **custom messaging** adds a **first-class COTI contract** you maintain. + +On GitHub and other narrow layouts, one tall diagram with stacked subgraphs is often **compressed**. The same model is easier to read as **two left-to-right flows** (scroll horizontally if needed). + +### Path 1 — Primitives only (e.g. private adder) + +```mermaid +%%{init: {"flowchart": {"htmlLabels": false, "useMaxWidth": false, "diagramPadding": 12, "nodeSpacing": 45, "rankSpacing": 40}}}%% +flowchart LR + classDef youAuthor fill:#ffedd5,stroke:#c2410c,stroke-width:4px,color:#431407 + classDef platform fill:#f1f5f9,stroke:#94a3b8,stroke-width:1px,color:#475569 + + U1[User / wallet] --> SA[Your Fuji dApp] + SA --> PL[PodLib SDK] + PL --> C1[Inbox plus MPC executor on COTI] + C1 --> X1[Inbox callback delivers ct*] + + class SA youAuthor + class U1,PL,C1,X1 platform +``` + +The callback runs **in the same Fuji contract** you deployed (`SA`); there is no separate COTI Solidity dApp in this path. + +### Path 2 — Custom COTI logic (e.g. encrypted messaging) + +```mermaid +%%{init: {"flowchart": {"htmlLabels": false, "useMaxWidth": false, "diagramPadding": 12, "nodeSpacing": 45, "rankSpacing": 40}}}%% +flowchart LR + classDef youAuthor fill:#ffedd5,stroke:#c2410c,stroke-width:4px,color:#431407 + classDef platform fill:#f1f5f9,stroke:#94a3b8,stroke-width:1px,color:#475569 + + U2[User / wallet] --> SB[Your Fuji dApp] + SB --> IB[Inbox] + IB --> DC[Your COTI dApp] + DC --> X2[Inbox callback on Avalanche Fuji] + + class SB,DC youAuthor + class U2,IB,X2 platform +``` + +`inbox.respond` and the rest of your private logic run **inside** `DC` before the return message is delivered to the callback on Avalanche Fuji. + +**Legend:** **Amber / thick orange border** — Solidity **you** write and deploy (**one** host-chain contract in Path 1; **host + COTI** contracts in Path 2). **Gray** — wallets, SDK helpers you import, and COTI network infrastructure you do not author as application code. + +**Intuition:** in Path 1, **COTI-side custom code is optional**—the executor already implements **`add`** (and the other primitives). In Path 2, **`DirectMessageCotiSide`** (or your own contract) **is** the program: the executor runs *your* compiled private contract logic, not only a single named primitive. + +--- + +## Where to read next + +| Your situation | Start here | +| --- | --- | +| Logic fits the primitive list and a small number of MPC steps | [Tutorial: private Adder on Avalanche Fuji](tutorial-private-adder-fuji.md), [TypeScript PoD SDK (`CotiPodCrypto`, `PodContract`)](typescript-pod-sdk.md), then [MPC library (PodLib) — SDK](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/05b-multi-party-computing-library-mpclib.md) | +| You want a business-oriented public-to-private migration | [Cookbook: private investor allocations with PoD](cookbook-private-investor-allocations.md), then [Tutorial: custom privacy logic with PoD](tutorial-custom-logic.md) | +| Logic needs custom COTI processing, `gt*` handling, or richer state | [Tutorial: custom privacy logic with PoD](tutorial-custom-logic.md), then [Writing privacy contracts on Ethereum — SDK](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/05-writing-privacy-contracts-on-ethereum.md) and [Request builder and remote calls — SDK](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/contracts/03-request-builder-and-remote-calls.md) | +| Fees, async UX, and components | [How do PoA fees work?](how-poa-fees-work.md), [Async private operations](async-private-operations.md), [Architecture and main components](architecture-and-components.md) | + +Return to the [Privacy on Avalanche section index](README.md). diff --git a/privacy-on-avalanche/typescript-pod-sdk.md b/privacy-on-avalanche/typescript-pod-sdk.md new file mode 100644 index 0000000..37769f9 --- /dev/null +++ b/privacy-on-avalanche/typescript-pod-sdk.md @@ -0,0 +1,87 @@ +# TypeScript PoD SDK (`CotiPodCrypto`, `PodContract`) + +The npm package **`@coti-io/pod-sdk`** ships **TypeScript** helpers for **encrypting and decrypting** PoD payloads and for **encoding, fee estimation, and sending** calls against your host-chain contract through [`coti-pod-crypto.ts`](https://github.com/coti-io/coti-sdk-pod/blob/main/src/coti-pod-crypto.ts) and [`pod-method-call.ts`](https://github.com/coti-io/coti-sdk-pod/blob/main/src/pod-method-call.ts). Solidity (`PodLib`, `PodUserFuji`) lives in **`@coti-io/coti-contracts`**, not in this package. + +Use these helpers from a wallet script, backend service, or dApp frontend once you have a **`Signer`** (or **`Provider`** for read-only helpers) and your contract **ABI**. + +## Encrypt and decrypt data (`CotiPodCrypto`) + +`CotiPodCrypto.encrypt` POSTs to the PoD encryption service: + +- `"testnet"` and `"mainnet"` resolve to SDK defaults. +- 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. + +```typescript +import { CotiPodCrypto, DataType } from "@coti-io/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); +``` + +## Gas estimation and method calls (`PodContract`) + +`PodContract` wraps `ethers.Contract` with PoD-aware helpers: + +- `estimateFee` for Inbox two-way fee calculation. +- `encryptAndCallMethod` to encrypt `it*` plaintext args and send the tx. +- `callMethod` to send pre-encrypted JSON ciphertext values. +- `extractRequestIds(txHash)` to parse Inbox `MessageSent` logs and recover request IDs for async tracking. + +```typescript +import { + PodContract, + DataType, + type PodFeeEstimationConfig, + type PodMethodArgument, +} from "@coti-io/pod-sdk"; +import { ethers } from "ethers"; + +const pod = new PodContract(contractAddress, abi, signer, { + encryptionNetwork: "testnet", +}); + +const args: PodMethodArgument[] = [ + { type: DataType.itUint256, value: "10", isCallBackFee: false }, + { type: DataType.itUint256, value: "20", isCallBackFee: false }, + { type: DataType.Uint256, value: "0", isCallBackFee: true }, // auto-replaced with callback fee +]; + +const feeCfg: PodFeeEstimationConfig = { + forwardGasLimit: 400_000n, + gasPrice: (await signer.provider!.getFeeData()).gasPrice ?? 0n, + callBackGasLimit: 250_000n, + callBackDataSize: 512n, +}; + +const fee = await pod.estimateFee("add", args, feeCfg); +// fee.totalFee, fee.remoteFee, fee.callBackFee + +const txResponse = await pod.encryptAndCallMethod("add", args, feeCfg); +const receipt = await (txResponse as ethers.ContractTransactionResponse).wait(); + +const requestIds = receipt?.hash ? await pod.extractRequestIds(receipt.hash) : []; +``` + +### Important fee config rules + +- `forwardGasLimit` and `gasPrice` are required. +- `callBackGasLimit` and `callBackDataSize` must be provided together (or both omitted). +- `forwardDataSize` is optional; SDK can estimate from argument string sizes. + +## Choosing `encryptAndCallMethod` vs `callMethod` + +- Use **`encryptAndCallMethod`** when your app currently has plaintext user input. +- Use **`callMethod`** when the app already has ciphertext JSON from a prior `CotiPodCrypto.encrypt` step and you want to submit it directly. + +## See also + +- [Tutorial: private Adder on Avalanche Fuji](tutorial-private-adder-fuji.md) — full walkthrough including `PodContract` and `extractRequestIds`. +- [Tutorial: custom privacy logic with PoD](tutorial-custom-logic.md) — custom COTI-side pattern. +- [TypeScript integration (SDK docs)](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/06-typescript-integration-ux-development.md) +- [PoD SDK docs index](https://github.com/cotitech-io/coti-pod-sdk/tree/main/docs) diff --git a/privacy-on-avalanche/what-is-privacy-on-avalanche.md b/privacy-on-avalanche/what-is-privacy-on-avalanche.md new file mode 100644 index 0000000..d752cec --- /dev/null +++ b/privacy-on-avalanche/what-is-privacy-on-avalanche.md @@ -0,0 +1,44 @@ +# What is Privacy on Avalanche? + +## The problem PoD addresses + +Public blockchains are excellent for **agreement**, **auditability**, and **open participation**. They are weaker when an application must hide **amounts**, **scores**, **personal fields**, or **business logic inputs** from everyone who can read the chain. + +Classic options include “only encrypt off-chain” (which weakens composability) or “move everything to a specialized chain” (which may not fit your custody, liquidity, or regulatory story). + +**Privacy on Avalanche** applies the **Privacy on Demand** middle path to **Avalanche Fuji C-Chain**: + +- Keep **accounts, tokens, and workflow** on **Avalanche** (Fuji for test; the same pattern targets Avalanche C-Chain style EVM hosts). +- Send **encrypted inputs** into a **private computation** environment on **COTI**. +- Receive **encrypted outputs** back on Fuji, which **only authorized users** can decrypt on their own devices. + +So: **Avalanche for coordination**, **COTI for the sensitive math and data handling**. + +## What “privacy” means in this model + +PoD is not one magic switch. In practice, privacy here means: + +1. **Sensitive values are not stored on Fuji in plaintext** in the way ordinary `uint256` balances are. +2. **Computation over those values** happens in COTI’s private execution path, not in a trace everyone can replay from Fuji calldata. +3. **Users decrypt only what they are entitled to see**, using **client-side keys** (for example an account AES key managed through your wallet or onboarding flow), as described in the SDK’s client integration guides. + +What PoD does **not** automatically guarantee by itself: + +- Perfect **metadata privacy** (timing, gas, which Fuji contract was called, and linkage patterns may still leak information unless you design around them). +- **Legal or regulatory** classification of your use case; you still need product and counsel review. + +## What ships in the SDK versus what your team builds + +The **COTI PoD stack** provides the pattern: TypeScript helpers in [`@coti-io/pod-sdk`](https://www.npmjs.com/package/@coti-io/pod-sdk) ([GitHub](https://github.com/coti-io/coti-sdk-pod)), and Solidity (`PodLib`, **`PodUserFuji`**, …) in [`@coti-io/coti-contracts`](https://github.com/coti-io/coti-contracts). Your project still typically supplies: + +- **Application-specific** Fuji contracts and state machines. +- **User experience** for onboarding, showing **pending / completed / failed** private operations, and **safe key handling**. +- **Operations**: monitoring, indexing, or internal tools for stuck requests and AVAX fee configuration, as appropriate for your deployment. + +The SDK’s own [documentation README](https://github.com/cotitech-io/coti-pod-sdk/blob/main/docs/README.md) states scope clearly: it does not replace deployment scripts, indexers, or backend services for you. + +## Next steps + +- For **Fuji network parameters and addresses**, read [Avalanche Fuji](networks/fuji.md). +- For a **walkthrough of one request**, read [How a private request travels end to end](how-a-private-request-travels-end-to-end.md). +- For **boxes-and-arrows** views of Inbox, executor, PodUser, and PodLib, read [Architecture and main components](architecture-and-components.md). diff --git a/privacy-on-demand/README.md b/privacy-on-demand/README.md index 34ecb96..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.
@@ -10,6 +10,7 @@ Privacy on Demand lets applications use **strong privacy for data and computatio - **[Tutorials: PoD dApps (choose your integration model)](tutorials-privacy-on-demand.md)** — Primitive-only vs custom COTI logic, then links to step-by-step guides. - **[Architecture and design](architecture-and-components.md)** — Inbox, MPC executor, PodUser, PodLib, and how they connect. +- **[Networks](networks/README.md)** — COTI Testnet and Avalanche Fuji parameters plus PoD contract addresses. - **[Interactive PoD architecture (pod.coti.io)](https://pod.coti.io/)** — Live demo: play the MpcAdder journey across Sepolia, relayer, and COTI, with GitHub source links and gas/fee visualization. - **[Learn about fees](how-poa-fees-work.md)** — How PoA/PoD fees split across COTI and your host chain. - **[Millionaires demo](https://millionaire.demo.coti.io)** — Live demo (external). @@ -42,21 +43,22 @@ This section explains **what PoD is**, **how it feels to users and operators**, 1. [What is Privacy on Demand?](what-is-privacy-on-demand.md) — Problem, promise, and constraints in everyday language. 2. [How a private request travels end to end](how-a-private-request-travels-end-to-end.md) — Timeline from user action to decrypted result. 3. [Architecture and main components](architecture-and-components.md) — Where **Inbox**, **MPC executor**, **PodUser**, and **PodLib** sit, with diagrams. -4. [Glossary](glossary.md) — Short definitions of terms you will see in PoD and SDK docs. +4. [Networks](networks/README.md) — Supported PoD test networks and contract addresses ([COTI Testnet](networks/coti-testnet.md), [Avalanche Fuji](networks/fuji.md)). +5. [Glossary](glossary.md) — Short definitions of terms you will see in PoD and SDK docs. ### Deeper context -5. [Async private operations (why it is not instant)](async-private-operations.md) — What “pending” means and why UX must reflect it. -6. [How do PoA fees work?](how-poa-fees-work.md) — Two-way Inbox budgets, oracle conversion, and step-by-step gas-unit consumption (worked example). -7. [For developers: mapping concepts to the SDK](for-developers-mapping-to-the-sdk.md) — Checklists and links to the [PoD SDK documentation on GitHub](https://github.com/cotitech-io/coti-pod-sdk/tree/main/docs). +6. [Async private operations (why it is not instant)](async-private-operations.md) — What “pending” means and why UX must reflect it. +7. [How do PoA fees work?](how-poa-fees-work.md) — Two-way Inbox budgets, oracle conversion, and step-by-step gas-unit consumption (worked example). +8. [For developers: mapping concepts to the SDK](for-developers-mapping-to-the-sdk.md) — Checklists and links to the [PoD SDK documentation on GitHub](https://github.com/cotitech-io/coti-pod-sdk/tree/main/docs). ### Tutorials (hands-on) -8. [Tutorials: building PoD dApps](tutorials-privacy-on-demand.md) — When to use **MpcLib / PodLib** primitives vs **custom COTI + host** contracts, with links to focused walkthroughs. -9. [TypeScript PoD SDK (`CotiPodCrypto`, `PodContract`)](typescript-pod-sdk.md) — Encryption/decryption, fee estimation, method calls, and request ID extraction. -10. [Cookbook: private investor allocations with PoD](cookbook-private-investor-allocations.md) — Start from a familiar public Sepolia allocation dApp, then make allocation reads and withdrawals private with PoD. -11. [Tutorial: private Adder on Sepolia](tutorial-private-adder-sepolia.md) — Minimal primitive-only adder: `PodUserSepolia`, fees, TypeScript crypto. -12. [Tutorial: custom privacy logic with PoD](tutorial-custom-logic.md) — Encrypted messaging shape: `DirectMessageCotiSide` + Sepolia orchestrator. +9. [Tutorials: building PoD dApps](tutorials-privacy-on-demand.md) — When to use **MpcLib / PodLib** primitives vs **custom COTI + host** contracts, with links to focused walkthroughs. +10. [TypeScript PoD SDK (`CotiPodCrypto`, `PodContract`)](typescript-pod-sdk.md) — Encryption/decryption, fee estimation, method calls, and request ID extraction. +11. [Cookbook: private investor allocations with PoD](cookbook-private-investor-allocations.md) — Start from a familiar public Sepolia allocation dApp, then make allocation reads and withdrawals private with PoD. +12. [Tutorial: private Adder on Sepolia](tutorial-private-adder-sepolia.md) — Minimal primitive-only adder: `PodUserSepolia`, fees, TypeScript crypto. +13. [Tutorial: custom privacy logic with PoD](tutorial-custom-logic.md) — Encrypted messaging shape: `DirectMessageCotiSide` + Sepolia orchestrator. ## Official technical reference 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** |
  • 0.007 ETH (msg.value)
  • 0.0035 ETH (callbackFeeLocalWei, Sepolia)
  • 0.0035 ETH (COTI-side)
  • 5 gwei (reference)
| +| **User-provided gas** |
  • 0.007 ETH (msg.value)
  • 0.0035 ETH (callbackFeeLocalWei, Sepolia)
  • 0.0035 ETH (COTI-side)
  • Bounded reference gas price (see below)—not the caller’s raw tip
| | **Price at oracle** (same quote, e.g. USD) |
  • COTI: 0.017 per 1 COTI.
  • ETH: 2,170 per 1 ETH.
| | **Converted to the chain gas (COTI — ETH)** |
  • COTI: 446.76 COTI
  • ETH: 0.0035 ETH
| | **Convert to gas units (COTI — ETH)** |
  • COTI: 16,029,096
  • ETH: 615,701
| @@ -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/networks/README.md b/privacy-on-demand/networks/README.md new file mode 100644 index 0000000..4005092 --- /dev/null +++ b/privacy-on-demand/networks/README.md @@ -0,0 +1,19 @@ +# Networks + +Privacy on Demand spans **two domains**: + +1. **Host chain** — where your dApp contracts, assets, and the local **Inbox** live (for example Avalanche Fuji). +2. **COTI** — where private computation runs via the **MPC executor** and the COTI-side **Inbox**. + +The pages below list network parameters and deployed contract addresses for current test environments. Addresses can change after redeploys; treat the [PoD SDK](https://github.com/cotitech-io/coti-pod-sdk) and your environment config as the live source of truth when building against a specific release. + +| Network | Chain ID | Role in PoD | +| --- | --- | --- | +| [COTI Testnet](coti-testnet.md) | `7082400` | Private execution (MPC executor, COTI Inbox) | +| [Avalanche Fuji](fuji.md) | `43113` | Host chain (dApp + Inbox paired with COTI Testnet) | + +The shared CREATE3 **Inbox** address is the same on every supported chain: + +`0xAb625bE229F603f6BBF964474AFf6d5487e364De` + +For general COTI chain RPC and faucet details (not PoD-specific), see the top-level [Networks](../../networks/README.md) section. diff --git a/privacy-on-demand/networks/coti-testnet.md b/privacy-on-demand/networks/coti-testnet.md new file mode 100644 index 0000000..c4d0875 --- /dev/null +++ b/privacy-on-demand/networks/coti-testnet.md @@ -0,0 +1,63 @@ +# COTI Testnet + +COTI Testnet is the **private execution** side of Privacy on Demand. Host-chain dApps (for example on [Avalanche Fuji](fuji.md)) send encrypted work through the cross-chain Inbox; COTI runs the private computation and returns encrypted results. + +> **Note:** Addresses below reflect the current PoD test deployment. They may change after contract redeploys. Confirm against your SDK release or environment before production use. + +## Network details + +| Parameter | Value | +| --- | --- | +| Network name | COTI Testnet | +| Chain ID | `7082400` | +| Currency | COTI | +| RPC URL | `https://testnet.coti.io/rpc` | +| WebSocket URL | `wss://testnet.coti.io/ws` | +| Block explorer | [https://testnet.cotiscan.io](https://testnet.cotiscan.io) | +| Status | [https://uptime.coti.io](https://uptime.coti.io) | + +Also see the general COTI [TestNet](../../networks/testnet/README.md) page for MetaMask setup and the [Testnet faucet](../../networks/testnet/faucet.md). + +## PoD contracts + +| Contract | Address | Description | +| --- | --- | --- | +| Inbox | [`0xAb625bE229F603f6BBF964474AFf6d5487e364De`](https://testnet.cotiscan.io/address/0xAb625bE229F603f6BBF964474AFf6d5487e364De) | Cross-chain message router (CREATE3; same address on every PoD chain) | +| MPC executor | [`0x68e151b78d51cea01eef6ee354579e044606a739`](https://testnet.cotiscan.io/address/0x68e151b78d51cea01eef6ee354579e044606a739) | Entry point for library-style private operations (`configureCoti` target on host dApps) | +| Price oracle | [`0xe1afeda542d1b9003df0e133187f845dea8b1ba8`](https://testnet.cotiscan.io/address/0xe1afeda542d1b9003df0e133187f845dea8b1ba8) | Local/remote token prices used by Inbox fee conversion | +| Pod ERC20 mother | [`0x293daf267bf657b0bae870a0ce8cd59f1e5eb32a`](https://testnet.cotiscan.io/address/0x293daf267bf657b0bae870a0ce8cd59f1e5eb32a) | COTI-side registry for Privacy Portal / pToken flows | + +## Protocol helpers (COTI Testnet) + +These are not PoD-specific, but most PoD clients need them for account onboarding and chain tooling: + +| Contract | Address | +| --- | --- | +| AccountOnboard | [`0x536A67f0cc46513E7d27a370ed1aF9FDcC7A5095`](https://testnet.cotiscan.io/address/0x536A67f0cc46513E7d27a370ed1aF9FDcC7A5095) | +| MPCInterface (precompile) | `0x0000000000000000000000000000000000000064` | + +Full COTI Testnet protocol addresses: [Contracts Addresses](../../networks/testnet/contracts-addresses.md). + +## How this network fits PoD + +| Piece | Role on COTI Testnet | +| --- | --- | +| Inbox | Receives mined requests from host chains and routes callbacks back | +| MPC executor | Target address host dApps pass to `configureCoti(..., COTI_TESTNET_CHAIN_ID)` | +| Price oracle | Converts fee budgets between host gas units and COTI | + +Typical pairings today: + +- **Host:** [Avalanche Fuji](fuji.md) · **Execution:** COTI Testnet +- **Host:** Ethereum Sepolia · **Execution:** COTI Testnet (see the [private Adder tutorial](../tutorial-private-adder-sepolia.md)) + +## SDK constants + +In `@coti-io/pod-sdk` and `@coti-io/coti-contracts`: + +| Constant | Value | +| --- | --- | +| Chain ID | `7082400` | +| Inbox | `0xAb625bE229F603f6BBF964474AFf6d5487e364De` (`COTI_TESTNET_DEFAULT_INBOX_ADDRESS`) | + +Confirm the MPC executor against your deployed release — presets in `PodNetworkConstants` can lag a redeploy; wire the address from the table above (or your env) in `configureCoti`. diff --git a/privacy-on-demand/networks/fuji.md b/privacy-on-demand/networks/fuji.md new file mode 100644 index 0000000..60aa1cb --- /dev/null +++ b/privacy-on-demand/networks/fuji.md @@ -0,0 +1,64 @@ +# Avalanche Fuji + +Avalanche Fuji is a supported **host chain** for Privacy on Demand. Your dApp contracts and the local **Inbox** live here; private computation still runs on [COTI Testnet](coti-testnet.md). + +> **Note:** Addresses below reflect the current PoD test deployment. They may change after contract redeploys. Confirm against your SDK release or environment before production use. + +## Network details + +| Parameter | Value | +| --- | --- | +| Network name | Avalanche Fuji C-Chain | +| Chain ID | `43113` | +| Currency | AVAX | +| RPC URL | `https://api.avax-test.network/ext/bc/C/rpc` | +| Block explorer | [https://testnet.snowscan.xyz](https://testnet.snowscan.xyz) | + +Private execution for Fuji dApps targets **COTI Testnet** (`7082400`). See [COTI Testnet](coti-testnet.md) for the MPC executor and COTI-side Inbox. + +## PoD contracts + +| Contract | Address | Description | +| --- | --- | --- | +| Inbox | [`0xAb625bE229F603f6BBF964474AFf6d5487e364De`](https://testnet.snowscan.xyz/address/0xAb625bE229F603f6BBF964474AFf6d5487e364De) | Cross-chain message router (CREATE3; same address on every PoD chain) | +| Price oracle | [`0xbf615045803edb0dbb2c9b9afedcd782c383a09b`](https://testnet.snowscan.xyz/address/0xbf615045803edb0dbb2c9b9afedcd782c383a09b) | Local/remote token prices used by Inbox fee conversion | +| MpcAdder (example) | [`0xf51c789e4c60d33a2abcfd656e607724f56aaf0d`](https://testnet.snowscan.xyz/address/0xf51c789e4c60d33a2abcfd656e607724f56aaf0d) | Reference primitive-only adder dApp on Fuji | + +## Privacy Portal (Fuji) + +Deployed factory and implementations for private-token flows on Fuji: + +| Contract | Address | +| --- | --- | +| Privacy Portal factory | [`0x5230856b5ce21ee5efdd113f8d4849a4b02d3a57`](https://testnet.snowscan.xyz/address/0x5230856b5ce21ee5efdd113f8d4849a4b02d3a57) | +| Portal implementation | [`0x01dee3b8046a94896fe97d531aa2019ea2557ce3`](https://testnet.snowscan.xyz/address/0x01dee3b8046a94896fe97d531aa2019ea2557ce3) | +| Pod token implementation | [`0x0c8ec0f93cae026db214755388d2c41c8b5be08d`](https://testnet.snowscan.xyz/address/0x0c8ec0f93cae026db214755388d2c41c8b5be08d) | + +### Privacy Portal tokens + +| Token | Underlying | Portal | pToken | +| --- | --- | --- | --- | +| pMTT | `0x328e70e1c52662cd5f19f824fcb8b463d77a6686` | `0x9a82B356c9f7F59aE2c04200358122eb99aE6364` | `0xc1258E5C04A6933940105FE94cC9A4C2439d3402` | +| pUSDC | `0x5425890298aed601595a70AB815c96711a31Bc65` | `0x32146Ce8a96F2b0102c5C0f27e45E53eA30ef18B` | `0xAd40C2E98ef9d37827F5206D0a6641c29f2247c5` | +| pWAVAX | `0xd00ae08403B9bbb9124bB305C09058E32C39A48c` | `0xdf75091D4AF11b94FA318eeBC6CEBC106899E25C` | `0x44392E67f6Fc5179BD25302519b1B85B87afc5D8` | + +## How this network fits PoD + +| Piece | Role on Fuji | +| --- | --- | +| Inbox | Host-side courier: accepts encrypted requests from your dApp and delivers COTI callbacks | +| Your dApp | Configures Inbox + COTI chain ID `7082400` + [MPC executor on COTI Testnet](coti-testnet.md#pod-contracts) | +| Price oracle | Converts Fuji fee budgets against COTI-side costs | + +Flow at a glance: **user / dApp on Fuji → Fuji Inbox → (relayer) → COTI Inbox → MPC executor → callback to Fuji**. + +## SDK constants + +In `@coti-io/pod-sdk`: + +| Constant | Value | +| --- | --- | +| Chain ID | `43113` | +| Inbox | `0xAb625bE229F603f6BBF964474AFf6d5487e364De` (`FUJI_DEFAULT_INBOX_ADDRESS`) | + +Point `configureCoti` at the [COTI Testnet MPC executor](coti-testnet.md#pod-contracts) and chain ID `7082400`. 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).