Message Routing
How encrypted events are routed between identities through the mediator network using dual-delivery.
Formal specs: DCTRL-0004 — Mediator Protocol and DCTRL-0005 — Event Encryption and Storage
The Dual-Delivery Pattern
When Alice sends Bob a message, two things happen:
- The message is encrypted with the contract's root secret and sent to Bob's mediator for delivery
- The message is re-encrypted with Alice's storage key and saved on Alice's mediator for her records
// 1. Build the event
event = { type: "chat.message", data: { chatId: "abc", content: "Hello Bob" } }
// 2. Sign the event envelope
envelope = { contract_id, event: JSON.stringify(event), timestamp: now }
signature = Ed25519_sign(envelope, alice_signing_private)
// 3. Encrypt for transit → send to Bob's mediator
transit_payload = AES_GCM_encrypt(JSON.stringify(signed_envelope), root_secret)
send_command(bob_mediator, TWO_WAY_PRIVATE, { payload: transit_payload })
// 4. Encrypt for storage → save on Alice's mediator
storage_payload = AES_GCM_encrypt(JSON.stringify(event), alice_storage_key)
encrypted_tags = [sign("chat"), sign("chat.abc")]
send_command(alice_mediator, SAVE_EVENTS, { payload: storage_payload, tags: encrypted_tags })This ensures both parties always have complete records, regardless of which mediator they use or whether they later switch providers.
Receiving Events
When Bob connects to his mediator (HTTP poll or WebSocket push):
// 1. Query pending events
pending = query_pending_events(bob_mediator)
for each pending_event:
// 2. Find the matching contract
contract = find_contract(pending_event.sender_did)
root_secret = X25519(bob_ephemeral.private, alice_ephemeral.public)
// 3. Decrypt and verify
envelope = AES_GCM_decrypt(pending_event.payload, root_secret)
verify(envelope.signature, envelope, alice_signing_public)
// 4. Re-encrypt for storage
storage_payload = AES_GCM_encrypt(envelope.event, bob_storage_key)
save_events(bob_mediator, { payload: storage_payload, tags: [...] })
// 5. Acknowledge
acknowledge_pending_events(bob_mediator, { ids: [...] })Bob decrypts with the root secret, verifies Alice's signature, then re-encrypts with his own storage key. The mediator never sees plaintext at any point.
Real-Time Delivery
Mediators support WebSocket connections for push-based event delivery:
1. Client connects via WebSocket
2. Client sends: { type: AUTHENTICATE, did, signing_key_id, timestamp, nonce, signature }
3. Mediator verifies: timestamp drift < 5 min, resolves DID, verifies signature
4. Mediator responds: { type: AUTH_SUCCESS }
5. On new events: mediator pushes { type: PENDING_EVENTS, events: [...] } directlyThis eliminates polling. The mediator pushes notifications for new pending events, contract updates, and other state changes to connected clients in real time.
Cross-Mediator Delivery
Alice on mediator A, Bob on mediator B — no problem. The protocol doesn't require mediators to coordinate. Alice's client simply sends the transit-encrypted payload directly to Bob's mediator's HTTP endpoint. Bob's mediator stores it as a pending event. Bob's client picks it up on next sync.
There is no mediator-to-mediator protocol. Each mediator is an independent server that accepts signed commands from registered identities and stores encrypted data. The "cross-mediator" magic is simply that clients know where to deliver messages (the mediator endpoint is in the recipient's DID).