morphit/docs/adr/0010-key-custody.md

458 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ADR-0010 — Key custody for Morphit's Blurt accounts
**Status:** Accepted (Phase 4) — **amended 2026-06 (beta.28)**, see banner below
**Date:** 2026-04-19
**Deciders:** project maintainer
**Supersedes:** none
**Superseded by:** none
**Related:** ADR-0011 (dynamic fee model)
> **⚠ AMENDMENT (2026-06, beta.28):** Blurt disabled BOTH `claim_account`
> and `create_claimed_account` at hard fork 2 — the chain evaluators now
> assert *"This operation is disable since hard fork 2."* The Account-
> Creation-Token (ACT) model this ADR originally specified is therefore
> dead on Blurt. The relay now creates accounts with a **direct
> `account_create` op**, paying the `account_creation_fee` (~100 BLURT)
> **inline** from its liquid BLURT, which the chain burns to the null
> account. Consequences: the **ACT auto-minter (§5) and the weekly
> `mint-acts.ts` ceremony are removed**; signup readiness gates on the
> relay's **liquid BLURT balance** (≥ fee + margin) rather than an ACT
> buffer; and the relay must hold enough liquid BLURT to fund signups
> inline (it can no longer run at near-zero balance — see the amended §4).
> The key-custody posture is otherwise unchanged: the same online active
> key that signed `create_claimed_account` now signs `account_create`, so
> the blast-radius analysis (§6) still holds. Sections below describing
> ACT minting are retained for historical context but are **superseded by
> this amendment.**
## Context
Morphit operates three Blurt accounts on behalf of the project:
- `@morphit` — signs release announcements (`morphit_release_v1`
custom_json ops). Signed infrequently; posting authority is
sufficient.
- `@morphit-relay` — creates Blurt accounts for new Morphit
users and sends them a small BLURT "dust" to enable their
first chain op. Account-creation ops on Blurt require
**active** authority.
- `@morphit-fees` — receives listing fee transfers in BLURT,
BTC, or XMR. Purely passive receipt of BLURT; BTC/XMR fees
flow to externally-controlled addresses documented separately.
No keys needed on any server to receive BLURT.
The project owner's closeout note raised the core concern
plainly:
> *"the morphit.io master server will have to hold the private
> keys of the morphit blurt account. that's a MASSIVE honeypot.
> ... so maybe we can keep both the owner AND the active
> private keys OFF of the server(s)."*
The project owner later clarified the key-custody posture
through a design discussion. This ADR documents the outcome.
## Chain mechanics (for reference)
Blurt forked from Steem and inherits Steem's two-phase account
creation:
1. **`claim_account`** — the claiming account pays the witness-
set account creation fee in BLURT to mint an Account Creation
Token (ACT). Requires **active** authority. ACTs don't
expire, aren't transferable between accounts, and there's no
upper limit per claiming account.
2. **`create_claimed_account`** — consumes one ACT to create a
new account. Costs no BLURT beyond the original claim fee.
Requires **active** authority.
Both ops need the claiming account's active key. Pre-minting
ACTs does NOT eliminate the need for an online active key —
`create_claimed_account` still needs it. But pre-minting
decouples the relay's BLURT BALANCE from the account creation
rate, which is the point.
A note on terminology: **"voucher" in Blurt community usage
(e.g., blurtplugin.online) refers to an application-layer
redemption code, NOT the chain ACT.** A community voucher
service issues redemption codes to users and internally
consumes its own ACTs when a code is redeemed. Morphit does
NOT use the community voucher system; we issue ACTs on-chain
directly.
Current fee levels (as observed on blocks.blurtwallet.com at
the time of this ADR):
- `account_creation_fee`: **100.000 BLURT per ACT**
- Operation flat fee: 0.050 BLURT
- Bandwidth fee: 0.200 BLURT/KB
These are witness-controlled and subject to change. ADR-0011
documents how Morphit tracks witness changes and adjusts the
listing fee to preserve margin.
## Decision
### 1. Three accounts, three key-custody postures
**`@morphit-fees`** — no online keys. Owner and active keys
exist on cold paper backup only. Memo key generated but not
used. The account accumulates BLURT from listing fees over
time; periodic ceremonial withdrawal from cold storage moves
funds as needed.
**`@morphit`** — posting key only, online. Release announcements
use `morphit_release_v1` custom_json ops which take posting
authority. Owner key and active key are paper-only. The posting
key sits on a dedicated release-signer machine, decrypted into
memory only when a release is being signed. If this machine is
compromised, an attacker can publish false release announcements
— but the frontend pins `MORPHIT_OFFICIAL_POSTING_PUBKEY` at
build time, so key rotation via the owner key (paper) recovers
cleanly.
**`@morphit-relay`** — active key online, with the blast-radius
mitigations below. Owner key paper-only.
### 2. Registration flow (new Morphit user)
The user-facing flow, landed in its final form:
1. User visits Morphit. Signup is **free** and requires no
crypto. Users are explicitly encouraged to use VPN/Tor; no
browser fingerprinting, no captchas.
2. Frontend generates user keys locally and calls the relay
with a signed request to create their Blurt account.
3. Relay creates the user's account on-chain with a direct
`account_create` op, paying the chain's `account_creation_fee`
inline from its liquid BLURT (the ACT model was disabled at HF2 —
see the amendment banner).
4. Relay sends the user **2 BLURT dust** via `transfer` so their
fresh account has enough on-chain bandwidth to post their
first Morphit order.
5. User's first order is a BUY (any supported asset). The
listing fee is **waived** for the first buy order per
account — this is grandma's onboarding: she posts her
first buy-BLURT order and acquires her first crypto through
Morphit. See ADR-0011 for the multi-asset fee mechanics.
6. When the user's first order successfully completes a trade
(detected by the indexer), the relay transfers the welcome
bonus: **10 BLURT liquid + 10 BLURT Power** (via
`transfer_to_vesting`). This bonus is delayed until first
successful trade — not granted at signup.
7. Thereafter, the user pays the standard listing fee (in
BLURT, BTC, or XMR — their choice, BLURT default) for each
new order.
This flow is deliberately designed so that **grifters cannot
profitably spam-register accounts.** A grifter who signs up
N accounts and never completes a trade receives N × (1 BLURT
dust) = N × $0.002 of extractable value. The 10+10 welcome
bonus is only paid on successful trade completion, which
requires real counterparty interaction and a paid listing fee
on any subsequent orders. The economics are negative-sum for
grifters and are disclosed publicly in the FAQ.
### 3. Low-balance auto-refill
Users who remain active on Morphit but exhaust their BLURT on
chain-bandwidth fees get an automatic **1 BLURT dust refill**
from the relay whenever their balance drops below 0.5 BLURT,
gated on recent Morphit activity (they must have a Morphit op
in the last N days; exact threshold operator-configured). This
keeps grandma from getting stuck mid-session without BLURT to
pay bandwidth.
Grifter exploitation: a bot signing up many accounts to farm
the 1 BLURT dust still needs real counterparty trades (or
Morphit ops) to trigger the refill. Dust value is $0.002 per
refill; extraction is slow.
### 4. Active-key blast-radius mitigations for `@morphit-relay`
- **Use the direct `account_create` op.** *(Amended: Blurt disabled
`claim_account`/`create_claimed_account` at HF2, so this is the only
creation path.)* The relay pays the `account_creation_fee` inline,
read live from the chain per broadcast — the evaluator asserts it
equals the witnesses' median exactly.
- **Size `@morphit-relay`'s liquid balance to ~one week of signups.**
*(Amended: the relay can no longer run at near-zero balance — it pays
the ~100 BLURT `account_creation_fee` inline per signup, whereas the
ACT model pre-paid it at mint time.)* The working balance covers one
week of: per-signup creation fees (~100 BLURT each) + 2 BLURT dust
each + the week's expected welcome bonuses (~10 BLURT liquid per
completed-trade signup). At 500 signups/week that is on the order of
50,000 BLURT in fees + ~500 dust + ~5,000 welcome bonuses. Signup
readiness is gated on this balance: the relay refuses signups
(`relay_out_of_funds`) when it can't cover the fee + a small margin.
- **Passphrase-at-boot for the active key.** The relay service
does NOT hold the decrypted active key on disk. At startup,
the operator SSHes in and enters a passphrase interactively;
the service holds the decrypted scalar in memory only. A
service restart requires a human.
- **BLURT top-ups via `recurrent_transfer`.** Blurt supports
native recurrent transfers. The operator configures a
recurrent transfer from a funding account (cold-storage-
replenished) to `@morphit-relay` on a weekly cadence. The
funding account's active key is NOT on any server — the
recurrent_transfer is set up once with the funding account's
active key (entered from cold), then runs autonomously
on-chain for its configured duration. No manual ACT minting is
needed (the relay funds signups inline from this balance).
- **Dedicated host.** `@morphit-relay`'s key material lives
only on the relay VM — not on the indexer host, not on
developer machines, not in backups. Owner-key backup is
paper, stored in a physical location separate from the
maintainer's home office.
- **Nginx rate limit: 2 signups per IP per day.** Weak — defeated
by VPN/Tor/mobile carrier NAT — but establishes a floor
against casual abuse. No fingerprinting; no captchas.
Privacy-positive posture is a deliberate tradeoff: we accept
higher grifting risk in exchange for VPN/Tor-friendly UX.
- **Operator velocity alerts.** Indexer monitors signup rate per
hour and per day. Signups exceeding configured thresholds
auto-pause registration and page the operator.
- **Operator incident response:** if velocity alerts fire, the
operator can pause the relay at nginx while investigating,
rotate keys from paper backup if compromise is suspected.
### 5. ACT auto-minter — REMOVED (superseded by the 2026-06 amendment)
> This section originally described an in-process auto-minter that kept
> the relay's Account-Creation-Token buffer topped up. Blurt disabled the
> ACT model at hard fork 2 (see the amendment banner), so there are no
> tokens to mint: the auto-minter, the `MORPHIT_RELAY_AUTOMINT_*` config,
> and the weekly `mint-acts.ts` ceremony have all been removed. The relay
> now pays the `account_creation_fee` inline per `account_create` and
> gates signups on its liquid BLURT balance (≥ fee + margin). The
> low-balance operator alert this machinery fed (a Matrix DM via the
> matrix-bot) is retained, now triggered by the relay's
> `relay_low_balance_for_signups` journal event.
### 6. Blast radius analysis
In the worst case — full compromise of the relay VM while it
holds the decrypted active key in memory — the attacker can:
1. **Consume all pre-minted ACTs** to create junk accounts. At
~1 week of ACT buffer (say 50 tickets early, 500 later), the
attacker can create that many junk accounts. Morphit loses
the prepaid claim cost (~$10-100 of BLURT). Each junk account
also triggers a 2 BLURT dust, increasing the loss.
2. **Transfer the working liquid BLURT balance** to an attacker-
controlled account. At the sizing above, ~$0.10-$11 of direct
theft depending on weekly rate.
3. **Transfer BLURT received via the `recurrent_transfer`**
during the compromise window. The recurrent_transfer is
one-way (funding → relay); the attacker can steal each
weekly disbursement until the operator revokes the recurrent
transfer. Bounded by detection time.
Total direct monetary damage in a worst-case full compromise:
**one week of Morphit's operating float plus pre-minted ACT
value.** Under $25 early, under $150 at 500 signups/week. This
is not zero, but it is bounded and affordable.
What the attacker CANNOT do (because those keys are paper-only):
- Drain `@morphit-fees` accumulated revenue
- Steal owner-key authority over any Morphit account
- Publish false release announcements signed by `@morphit` (the
release-signer machine is a separate host; its compromise is
independent and is mitigated by frontend pubkey pinning)
- Change the registered recovery account on `@morphit-relay`
without the owner key (Blurt's recover_account mechanism
requires owner-or-recovery-partner authority)
### 7. Owner keys — always paper, always air-gapped
For all three accounts, the owner key is generated in an
air-gapped environment, printed to paper, and stored in a
physical safe. A copy exists at a second physical location
controlled by a trusted agent for disaster recovery.
Owner keys are NEVER:
- Stored on any internet-connected machine, even encrypted
- Transmitted over any network, even end-to-end encrypted
- Photographed with a device that has ever been online
- Stored in any password manager, cloud or otherwise
- Typed on any machine that has ever accessed the internet
Owner-key events are rare: the initial account creation (once),
key rotations after a suspected compromise (hopefully never),
and periodic withdrawals from `@morphit-fees` to pay operating
costs (maybe quarterly). Each event is a one-time ceremony with
deliberate friction.
### 8. "Super-encrypt everything" is not the right mental model
The original closeout note raised the possibility of "super-
encrypt[ing], salt[ing], hash[ing], whatever you call it with
the strongest security known to mankind." This is a common
instinct, but layered disk encryption adds zero marginal
security if the decryption key lives on the same machine. The
correct mental model is:
- **Minimize keys present online.** Biggest win. `@morphit-fees`
and paper-only owner keys follow this.
- **If a key MUST be online, use passphrase-at-boot.** The
decryption input lives in the operator's head, not on the
machine. Disk compromise yields an encrypted blob;
running-process-memory compromise yields the decrypted key.
The attack surface is running memory, not disk.
- **If a key must be online AND must survive reboots without a
human, use a hardware security module.** YubiHSM2 or
equivalent holds key material in tamper-resistant hardware;
the service can sign via the HSM but cannot extract the key.
This is overkill for Morphit's current scale but is the right
answer if operational needs change.
Morphit uses the first two. HSM stays as the escalation path.
## Alternatives considered
### Charge a small upfront registration fee
**Rejected by project owner.** Grandma has no crypto yet;
Morphit's value proposition is helping her acquire her first
crypto. Any upfront fee defeats the core use case.
### Keep `@morphit-relay` always well-funded
**Rejected.** A compromised relay with a large BLURT balance
can be drained for real money. Near-zero balance + weekly
top-up bounds the damage.
### Browser fingerprinting to detect multi-account signup abuse
**Rejected by project owner.** Conflicts with the privacy-
positive posture (VPN/Tor encouragement). We accept more
grifting exposure for a cleaner privacy story — and we
compensate with delayed-bonus economics that make spam-signup
unprofitable.
### CAPTCHAs at signup
**Rejected by project owner.** Bad UX, privacy concerns, and
defeated by modern CAPTCHA-solving services anyway.
### Hardware security module for `@morphit-relay`
**Reserved as escalation path.** Current passphrase-at-boot is
sufficient for the projected load. Revisit if the relay needs
to run unattended.
### Multi-sig account auths on @morphit-relay
**Rejected.** Adds operational complexity (multiple operators
must coordinate each ACT mint) without meaningful security
gain at our scale.
## Consequences
### Positive
- Grandma can sign up for Morphit with no crypto, receive 1
BLURT of dust to pay chain fees, post her first buy order for
free, and complete her first crypto trade — the core use case
works.
- Grifter economics are negative-sum: welcome bonuses are
delayed until first paid trade, so spam-signup yields at most
the 2 BLURT dust ($0.004 per attempt) until real trades are
completed.
- `@morphit-relay`'s BLURT balance is kept near-zero during
normal operation; worst-case compromise damage is ~$25-150
depending on traffic rate.
- Owner keys never touch online systems; full service compromise
does not escalate to account takeover.
- The privacy-friendly posture (no fingerprinting, no captcha,
VPN/Tor welcomed) is a marketing differentiator, disclosed
prominently in the FAQ.
### Negative
- Relay service restarts require a human operator to enter a
passphrase. Unattended reboots leave the relay dark.
- Once a week, a human must run the ACT-minting script. If
skipped, the ticket pool eventually drains and new user
registration fails.
- Paper owner-key backups require physical security discipline.
- The first-order-free-for-new-accounts policy means Morphit
eats the listing fee for every first buy. This is bounded:
limited to one per account, one-time.
- Grifters can still create junk Blurt accounts via Morphit at
the cost of the ACT fee (100 BLURT per account), paid by
Morphit. Operator monitors velocity; nginx rate limits set a
floor; the FAQ discloses the economics publicly as a
disincentive.
### Neutral
- `@morphit-fees` accumulates BLURT over time; periodic
withdrawal ceremonies move funds to cold storage or pay
operating costs.
- The BTC and XMR listing-fee-receipt addresses are managed
outside this ADR (they're regular Bitcoin and Monero addresses,
not Blurt accounts).
## Implementation plan
This ADR is a design document; the code and operational changes
it implies land over multiple Phase 4 turns:
### Code
1. **Relay: ACT pre-minting script** (`scripts/mint-acts.ts`)
— standalone tool; prompts for active-key passphrase;
mints N tickets; logs operation.
2. **Relay: passphrase-at-boot flow.** Replace env-var active
key with stdin prompt at service start.
3. **Relay: signup flow updated.** Use `create_claimed_account`
(not `account_create`). Send 1 BLURT dust after account
creation.
4. **Relay: low-balance auto-refill.** Indexer detects active
users whose BLURT balance drops below 0.5 BLURT; relay sends
1 BLURT dust.
5. **Indexer: velocity monitor.** Track signups per hour/day;
pause relay if thresholds exceeded; page operator.
6. **Indexer: delayed welcome bonus trigger.** On first
successful trade completion, instruct relay to send 10
BLURT + 10 BP to the user.
7. **Nginx: 2 signups/IP/day rate limit.**
8. **Frontend: pubkey pin audit.** Verify all trust-anchor
pubkeys are build-time-pinned.
### Documentation
9. **Operator runbook** (`docs/OPERATIONS.md`) — weekly ACT
minting procedure, reboot procedure, owner-key rotation
ceremony, incident response for suspected key compromise,
`recurrent_transfer` setup instructions.
10. **FAQ: attack-vector disclosure.** Publicly documents why
self-trading and multi-accounting don't pay.
### One-time setup (pre-launch)
11. Physical owner-key ceremony for `@morphit`,
`@morphit-relay`, `@morphit-fees`.
12. Fund a cold-controlled source account for the
`recurrent_transfer` to `@morphit-relay`.
13. Configure recurrent_transfer from funding account →
`@morphit-relay`, weekly cadence, appropriate amount
based on projected signup rate.
14. Deploy release-signer machine; transfer `@morphit`
posting key to it; pin the corresponding pubkey in the
frontend build.
Items 18 and 10 are per-turn deliverables during Phase 4. Item
9 is written incrementally as pieces land. Items 1114 are
operational milestones coordinated with deployment.
## Non-goals
- Multi-sig account auths. Out of scope.
- HSM deployment. Reserved as escalation path.
- Key rotation automation. Rare ceremonial events only.
- Replacement of the release-signer host with a cloud HSM
service (e.g., AWS KMS). Operator-choice decision outside
this ADR.