458 lines
20 KiB
Markdown
458 lines
20 KiB
Markdown
# 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 1–8 and 10 are per-turn deliverables during Phase 4. Item
|
||
9 is written incrementally as pieces land. Items 11–14 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.
|