morphit/docs/adr/0014-chat-and-counterparty-reputation.md

355 lines
15 KiB
Markdown

# ADR-0014 — Chat and counterparty reputation
**Status:** Partially superseded. The cipher and key-exchange
decision (Component A below) was REPLACED by ADR-0015 — see
"Cipher decision superseded" call-out below. Component B
(message transport via on-chain `morphit_chat_v1` ops) and the
chat-reputation framing in Component C remain in effect.
**Date:** 2026-04-19 (updated 2026-04-23 with ADR-0015 cross-ref)
**Deciders:** project maintainer
**Superseded by:** ADR-0015 (cipher + key derivation only)
**Related:** ADR-0010 (key custody), ADR-0009 (order posting),
ADR-0011 (dynamic fee model), ADR-0001 (custom_json replacement)
> **Cipher decision superseded.** ADR-0014 originally proposed
> a forward-secrecy protocol using a heavy WASM library.
> ADR-0015 (2026-04-23) reviewed that decision and chose
> simpler per-message ECIES instead: X25519 ECDH for key
> agreement + ChaCha20-Poly1305-IETF for the AEAD, with
> message keys derived per-message via BLAKE2b. Rationale:
> the project's threat model didn't actually require forward
> secrecy or post-compromise security (a posting-key
> compromise already ends the account's security story), and
> the WASM library's ~2MB bundle weight was a real cost
> against the "fast first paint" requirement. The
> implementation lives in `apps/web/src/lib/chat/crypto.ts`.
> Read ADR-0015 for the full derivation, security
> properties, and the four requirements that drove the
> choice.
>
> Everything below this call-out predates ADR-0015 and is kept
> for historical context. All references to specific
> forward-secrecy protocols are obsolete — the implementation
> uses the ADR-0015 ECIES design.
## Context
Current state: counterparties in a trade communicate via an
in-memory chat in the Morphit frontend, routed through a
websocket bridge. This has three serious problems:
1. **No persistence.** If a user reloads the page, chat
history is lost. Users report this as a major friction
point ("I lost my counterparty's bank details because I
refreshed").
2. **No authenticity.** The websocket bridge is operator-
run. A compromised operator could impersonate either
counterparty or man-in-the-middle the conversation. We
don't rely on this for anything critical (addresses should
be shared out-of-band), but the attack surface is real.
3. **No reputation carry-over.** A counterparty who has had
many successful trades has feedback on-chain, but their
chat style and responsiveness — real trust signals — don't
inform future interactions.
Phase 5 proposes to fix all three with an on-chain message
protocol: ciphertexts stored as `custom_json` ops, keys
derived from Blurt keys, rendering via authenticated
encryption with key derivation per-message (see ADR-0015 for
the resolved cipher choice).
### Constraints from Morphit principles
- **No custody of plaintext messages.** Messages must be
end-to-end encrypted between counterparties. No operator,
not even Agorise, should be able to read them.
- **Forward secrecy.** Compromise of long-term keys (a user's
posting key, say) must not reveal past messages.
- **Post-compromise security.** After a compromise event, future
messages should recover to being private once keys rotate.
- **Counterparty verifiability.** Each message must be
provably from the account it claims (via signature), so a
malicious operator can't inject messages.
- **Non-custody stays non-custody.** Chat must be signed with
the user's own keys, not a Morphit-held key.
- **No dispute resolution via chat.** Per PHASE-5-BACKLOG:
Morphit does not arbitrate disputes. Chat is a
communication tool; it is not evidence in a Morphit-run
dispute process, because there is no Morphit-run dispute
process.
### Technical state of play
- Each Morphit account has a Blurt posting key and active key.
Posting keys are used on every Morphit op; active keys for
transfers only. Both are long-term; neither is suitable
for ephemeral chat session keys.
- `custom_json` ops are the transport for all Morphit activity.
The chain enforces signature validity per op; our indexer
enforces payload shape.
- An order has a permlink. This is a natural "channel
identifier" — two users are in a chat scoped to an order.
## Decision
**Component A — Cipher and key exchange.**
**SUPERSEDED by ADR-0015.** The original ADR-0014 proposal
named a heavy WASM forward-secrecy library; ADR-0015
replaced this with per-message ECIES (X25519 ECDH +
ChaCha20-Poly1305-IETF, keys derived per-message via
BLAKE2b). Read ADR-0015 for the chosen design. The
implementation lives in `apps/web/src/lib/chat/crypto.ts`.
**Component B — Message transport.**
Encrypted messages flow as `morphit_chat_v1` custom_json
ops on the Blurt chain. The op payload contains:
- `order_permlink`: the order this chat is scoped to
- `recipient`: the counterparty account name
- `ciphertext`: base64-encoded encrypted message body
- `header`: a JSONB object carrying envelope fields the
indexer / frontend may need to read without decrypting
the body — including the ephemeral pubkey for the
per-message ECDH (the part of the message not encrypted
under the derived message key) and a `client_tag` for
client-side replay deduplication. Indexer treats the
header as opaque JSONB (size-bounded), narrowing only
defensively at read sites.
The indexer records these ops into a `chat_messages` table
indexed by `(order_permlink, recipient, block_num)`. Ciphertext
is stored; plaintext never lands in the indexer.
**UNDECIDED 1:** Per-account chat identity keys — how are
they generated, stored, rotated? See Open Q1.
**RESOLVED 2026-04-23 by ADR-0015:** derive from posting key
via HKDF-SHA256; no separate chat identity published.
**UNDECIDED 2:** On-chain vs. hybrid storage. See Open Q2.
**UNDECIDED 3:** Replay / relay logic. See Open Q3.
**RESOLVED 2026-04-23 by ADR-0015:** source_trx_id unique
constraint handles replay; no extra client state needed.
**Component C — Reputation integration (SEPARABLE).**
After a trade completes (feedback signed by both parties), the
following signals become available for display in the UI:
- Number of chat messages exchanged before trade close
- Response time statistics (first response, average turn-
around)
- Whether the chat hit a lull > N hours before trade close
(often an indicator of trouble)
Reputation signals are DISPLAYED in the UI (on an order-
detail page and a counterparty profile page) and optionally
included in the chain-visible feedback op as additional
context. Display-only use is cheap; chain-included signals
require an additive extension to `morphit_feedback_v1`.
**UNDECIDED 4:** Reputation chain-inclusion depth. See Open Q4.
**What Morphit does NOT provide:**
- Arbitration. Chat is communication, not evidence for a
Morphit-run dispute process.
- Automated moderation. No content scanning, no auto-summarization,
no auto-flagging. End-to-end encryption means even the operator
can't read the messages.
- Analytics. No aggregated "average response time across
all trades" metrics visible to anyone but the participants.
- Chat across multiple orders. Each chat is scoped to one
order's permlink. Counterparties who trade repeatedly
establish new chat sessions per order.
## Alternatives considered
- **Keep in-memory chat.** The current state. Rejected
because the friction and security concerns above are
real and increasing as user count grows.
- **Hosted chat server (Matrix, XMPP).** Morphit runs a
Matrix homeserver; users authenticate via their Blurt
keys. Rejected because it imports operational complexity
(running a mature chat service), creates a single
point-of-failure outside Morphit's control, and
centralizes a component that the current architecture
keeps distributed.
- **Roll our own crypto.** Write a forward-secrecy
protocol from scratch for better library ergonomics and
smaller bundle size. Rejected because rolling
cryptographic protocols is how bugs land in production.
Audited libraries are the only safe option for this layer.
Note: ADR-0015 ultimately landed on a much simpler
per-message ECIES design, which avoids both this
alternative AND the heavy WASM library that the
superseded Component A originally proposed.
- **Use Blurt's "private message" custom_json convention
(if one exists).** Checked: it doesn't. No Blurt app uses
a standard encrypted-messaging format we could adopt.
- **Peer-to-peer chat (libp2p, nostr).** Users' browsers
establish direct connections and skip the Blurt chain
entirely. Rejected because (a) browsers can't NAT-punch
reliably; (b) users typically aren't online simultaneously
during a trade window; (c) we'd lose the audit property
that every message is chain-addressable.
## Consequences
### Positive
- End-to-end encrypted chat: operators can't read messages.
Morphit can't read messages. Only the counterparties can.
- Cryptographically-authenticated senders: a hostile
operator cannot impersonate a counterparty. (Protection
that the current websocket-based chat lacks entirely.)
- Persistence: chat history survives page reloads and
device swaps (if key material is preserved).
- Reputation signals from chat data: in-UI display makes
response time and engagement visible to prospective
counterparties.
- Defensible as privacy-preserving: a subpoena request
for "the chat between alice and bob about order
sell-btc-usd-xxxx" returns only ciphertexts; the
operator can't decrypt them.
### Negative
- On-chain storage bloat. Every message is a Blurt op,
and the chain grows forever. For chatty counterparties
this could add up. (Quantify in Q2.)
- Blurt op throughput is finite. Can the chain support a
trade that sends 50 messages in a minute? Probably yes
(Blurt's fee_status allows many ops), but worth testing.
- Keystone for Blurt-network-dependent: if Blurt goes
down or forks disruptively, chat goes down.
- Implementation complexity: cipher port, session
management, key rotation, replay protection, message
ordering. This is the largest feature in Phase 5.
- Key-loss consequences. If a user loses their chat
identity key, all past chats on that account are
permanently unreadable. Must be communicated clearly.
### Follow-up work
- Sub-ADR: chat identity-key management (Open Q1 is
big enough to deserve its own ADR — RESOLVED by
ADR-0015).
- Sub-ADR: message storage model (Open Q2 is the other
big decision).
- `morphit_chat_v1` payload spec.
- Indexer handler for the new op.
- Frontend chat UI replacement.
- Key backup flow for users (posting key backup already
exists; chat keys are new material).
- 10-locale copy for chat error states, key-loss warnings,
"older messages may be unreadable" prompts.
## Open questions
**Q1 — Per-account chat identity keys.** Each account needs
a long-term keypair to seed encrypted-chat sessions. Two
options:
- **Derive from posting key.** A deterministic derivation
(HKDF, domain-separated) gives every account a chat key
without new user action. Pros: no new key material to
lose. Cons: posting-key compromise compromises past chats;
derivation exposes the chat key's address space to
anyone who knows the posting pubkey.
- **Publish a chat identity as a custom_json.** User
generates an ephemeral long-term chat keypair, publishes
the pubkey via `morphit_chat_identity_v1`. Pros: chat
compromise doesn't follow from posting-key compromise.
Cons: new key material to lose; UI complexity around
"publish your chat identity" (users who don't publish
can't chat); key rotation needs a separate op.
- **Per-session ephemeral keys via dlog-signed handshake.**
No persistent chat identity; each order's chat starts
with a fresh handshake signed by posting keys. Pros:
minimal additional infrastructure. Cons: handshake is
a full round-trip (two messages) before content flows,
costing latency and an extra on-chain op.
Leaning toward option 2 (published long-term chat identity)
but this deserves its own sub-ADR.
**Q2 — Storage model.** Three options, each with real
tradeoffs:
- **On-chain only.** Every message is a Blurt op. Storage
is forever; retrieval is chain-scan. Works if message
volume stays modest (~100 msgs/order avg). But a
counterparty pair who trades actively for years could
accumulate thousands of on-chain messages, contributing
meaningfully to chain size. Blurt community reaction to
this depends on scale.
- **Off-chain + on-chain integrity hash.** Ciphertext is
stored on operator-run servers; a commitment hash goes
on-chain. Retrieval is from any operator; integrity
checked against on-chain hash. Pros: minimal chain load.
Cons: operator-liveness-dependent for retrieval; new
infrastructure to run.
- **Hybrid: recent on-chain, historic on cold storage.**
Last N days on-chain (fast, always available), older
messages archived to operator storage with on-chain
pointers. Pros: best of both. Cons: implementation
complexity, chat UX shows "archived — loading" delays.
Leaning toward on-chain-only for Phase 5 MVP, with explicit
plan to revisit if chain-load becomes problematic.
**Q3 — Replay protection.** Blurt ops are eventually-final;
an op submitted twice should be idempotent at the chat
layer. How do we detect replays?
- Per-message counter: each message embeds a counter
derivable from session state. Replays are detected
because the counter doesn't match current state.
- Timestamp window: reject messages with timestamps > N
seconds in the past. Simple but crude.
**RESOLVED 2026-04-23 by ADR-0015.** ADR-0015's per-message
ECIES design uses the chain transaction id as a natural
nonce; replay detection is handled at the indexer layer by
the `(trx_id, op_index)` uniqueness already enforced by
Blurt + the indexer's own dedup on `source_trx_id`.
**Q4 — Reputation chain-inclusion.** The display-only
reputation signals (Component C) are cheap. If we want any
of them to be chain-visible (i.e., included in the
`morphit_feedback_v1` payload or a new companion op),
which should it be?
- Nothing: keep all reputation display-only based on
client-side counting of on-chain messages.
- Message count: an integer field in feedback.
- Response time: aggregated metric in feedback.
- Boolean "was responsive": user-judged, bool in feedback.
The question is whether cross-client consistency matters
(i.e., does Alice see the same stats as Bob does?). If yes,
chain-include. If clients can compute independently from the
same on-chain data, don't chain-include.
Leaning toward nothing in chain — let clients compute from
ciphertext-counts (knowable without decryption) and the
feedback op itself.
## References
- PHASE-5-BACKLOG.md item 6
- ADR-0010 — key custody (constrains identity-key handling)
- ADR-0001 — custom_json replacement (transport foundation)
- ADR-0015 — chat crypto (resolved Component A)
- X3DH Key Agreement: https://signal.org/docs/specifications/x3dh/