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 Sepolia) contract. Use this path when primitive-only PodLib / MpcLib operations 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 Demand (PoD) dApps.
Why two contracts?
The host EVM chain is excellent for accounts, assets, and user-facing flows, but your Sepolia (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:
Sepolia side — Collect encrypted user inputs (
it*), pay Inbox fees, and issue remote calls to COTI. In the success path, receive encrypted outputs (ct*) in anonlyInboxcallback 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, runMpcCoreoperations, optionally off-board ciphertext to a specific user (offBoardToUser), andinbox.respondwith an ABI-encoded blob the Sepolia 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; Sepolia 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:
(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 PodUserSepolia).
COTI-side contract: DirectMessageCotiSide
DirectMessageCotiSideThe COTI contract inherits InboxUser, so only the Inbox can enter receiveMessage. It:
Accepts the garbled string and routing metadata (
sender,recipient).Uses
MpcCore.offBoardToUserto convert the payload intoctStringciphertext that onlyrecipientcan decrypt.Returns that ciphertext to the host chain via
inbox.respond, ABI-encodingsender,recipient, and the ciphertext so the Sepolia callback can update storage deterministically.
Paths like ../InboxUser.sol assume you follow the SDK’s example layout; adjust imports to your repo.
Sepolia-side contract: orchestration only
The Sepolia contract inherits PodUserSepolia (or your network’s PodUser preset), tracks the COTI peer address, and:
sendMessage— Wraps encrypted input (itString) and public addresses in anIInbox.MpcMethodCallbuilt withMpcAbiCodec(see the SDK’s Request builder and remote calls). It sends a two-way message so the result comes back asynchronously.onMessageReceived— Decodes the tuple produced on COTI, re-checksinboxMsgSender(), and storesctStringkeyed 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/pod-sdk version (argument order and gt/it interface types must match the COTI method signature).
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 Sepolia — includes
PodContract,encryptAndCallMethod,estimateFee, andextractRequestIdsso you can copy the same client pattern tosendMessage(buildPodMethodArgument[]with types that match your ABI:itStringfor the ciphertext argument, plain types for addresses and the callback-fee slot,isCallBackFee: trueon the fee parameter).TypeScript PoD SDK (
CotiPodCrypto,PodContract) — short reference forCotiPodCryptoandPodContractwith links tocoti-pod-crypto.tsandpod-method-call.ts.Writing privacy contracts on Ethereum — SDK (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
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, Sepolia as router and ciphertext cache—you are in the custom PoD dApp model described on the tutorials overview.
Last updated
Was this helpful?