githubEdit

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 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 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

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 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:

  1. sendMessage — Wraps encrypted input (itString) and public addresses in an IInbox.MpcMethodCall built with MpcAbiCodec (see the SDK’s Request builder and remote callsarrow-up-right). 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/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:

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, Sepolia as router and ciphertext cache—you are in the custom PoD dApp model described on the tutorials overview.

Tutorial: private Adder on Sepolia

Tutorial: custom privacy logic with PoD

Last updated

Was this helpful?