Communication Contracts
Bilateral cryptographic agreements that establish secure channels between identities — no data flows without mutual consent.
Formal spec: DCTRL-0003 — Communication Contracts defines the contract schema, creation/acceptance flows, root secret derivation, key epochs, and revocation.
Communication contracts are the core innovation of Decentrl. They are bilateral cryptographic agreements that establish secure channels between two identities. Unlike platforms that decide who can message whom, Decentrl requires explicit mutual consent before any private communication can occur.
What's in a Contract
Each contract contains:
- Both parties' DIDs — who is in this relationship
- Ephemeral encryption keys — fresh X25519 key pairs, generated specifically for this contract, used to derive a shared root secret via Diffie-Hellman
- Creation timestamp — orders contracts and anchors the daily key epochs
- Bilateral Ed25519 signatures — both parties must sign under identical terms
There's no expiry. A contract is consent, and it stays valid until either side revokes it.
The dual-signature requirement prevents coercion, spam, and unauthorized communication. You can't send someone a message unless they've explicitly agreed to receive messages from you.
How Contracts Are Established
The full contract establishment flow is walked through step-by-step in the Alice and Bob protocol walkthrough. The key insight:
- Alice generates a fresh ephemeral X25519 key pair and signs a contract request
- The request is encrypted with Bob's pre-key — only Bob can read it, not even his mediator
- Bob decrypts, verifies, generates his own ephemeral key pair, and counter-signs
- Both parties independently derive an identical root secret via
X25519(own_ephemeral_private, their_ephemeral_public)— no secret is ever transmitted - Each side turns the root secret into the first key of a daily key chain and wipes it, along with its ephemeral private key
- The contract ID is a deterministic
SHA256hash of the contract data
Bob only accepts a request whose timestamp is at most 5 minutes in the future and at most 30 days old.
What Contracts Solve
Contracts solve multiple problems simultaneously:
- Spam prevention — you can't message someone without a mutual contract. The mediator rejects messages from identities the recipient holds no contract with.
- Shared encryption — both parties derive a shared root secret without exposing it to any server.
- Proof of consent — bilateral signatures provide cryptographic proof that both parties explicitly agreed to communicate.
- Bounded exposure — traffic keys change daily along a one-way chain (key epochs), so old transit ciphertext stays unreadable after its key is deleted.
- Revocability — either party can revoke a contract at any time, silently cutting off communication.
Contracts Are Universal
The same contract primitive handles every relationship in the network:
- User-to-user — Alice and Bob establish a contract to chat
- User-to-mediator — Alice establishes a contract with her mediator (this is registration)
- User-to-group — a group's DID invites Alice with a contract request; accepting it is joining
- User-to-service — Alice establishes a contract with an automated news feed
One primitive, every relationship.
One Contract per Pair
A mediator keeps at most one contract between its user and any one counterpart. When a new one arrives, the newer wins: the later timestamp, and for a tie the greater contract ID. Older or equal ones are ignored without an error. If Alice and Bob request each other at the same moment, both mediators and both clients end up keeping the same contract.
Requesting again is how you reconnect, for example from a device that has no keys for the current contract. The new contract replaces the old one.
Revocation
Either side can withdraw consent on its own, without asking and without telling the other. Revoking sends REVOKE_COMMUNICATION_CONTRACT to your own mediator only, which then:
- deletes your contract with them
- deletes events and contract requests from them that are still waiting for you
- remembers the revocation, so the old contract can never be stored again, even if someone replays it
Nothing is sent to the other side. Their next message gets COMMUNICATION_CONTRACT_NOT_FOUND, exactly what a stranger gets. They can send a new request, but only as a stranger, subject to the mediator's cap on pending requests from strangers. Revoking is idempotent, and revoking someone you never had a contract with also succeeds.
Revocation stops what comes next. It doesn't delete what the other side already received, or their copy of the contract.
await client.contracts.revoke(bobDid)Contract Schema
type CommunicationContract = {
requestor_did: string
recipient_did: string
requestor_signing_key_id: string
recipient_signing_key_id: string
requestor_encryption_public_key: string // ephemeral X25519
recipient_encryption_public_key: string | null // null until recipient accepts
timestamp: number // creation time; anchors key epochs
}
type SignedCommunicationContract = {
communication_contract: CommunicationContract
requestor_signature: string // Ed25519 (signs contract with recipient key = null)
recipient_signature: string // Ed25519 (signs completed contract)
}The asymmetric signature scope is intentional — the requestor signs before the recipient's key is known, and the recipient signs the complete contract. This prevents either party from modifying the terms after the other has signed.