|
| 1 | +# ripple — Fugger-classic trustlines as a JSS plugin |
| 2 | + |
| 3 | +Ryan Fugger's **original Ripple (2004)** — the pre-XRP design: a web of |
| 4 | +bilateral credit between real identities, payments routed through chains of |
| 5 | +trust. No blockchain, no native token, no consensus. Spec/issue: |
| 6 | +[webcontracts#4 — trustline.v1 profile](https://github.com/webcontracts/webcontracts.github.io/issues/4); |
| 7 | +background: [classic.ripplepay.com](https://classic.ripplepay.com/), Fugger, |
| 8 | +*"Money as IOUs in Social Trust Networks"*. |
| 9 | + |
| 10 | +```js |
| 11 | +plugins: [{ module: 'ripple/plugin.js', prefix: '/ripple' }] // zero config |
| 12 | +``` |
| 13 | + |
| 14 | +Open `/ripple` for the UI (trust graph, pay/settle forms, live hash-chained |
| 15 | +log); everything it does rides the JSON API below. |
| 16 | + |
| 17 | +## The model |
| 18 | + |
| 19 | +| Concept | Here | |
| 20 | +|---|---| |
| 21 | +| **Trustline** | unilateral: creditor extends debtor up to `limit` of `currency`. Only the creditor creates/resizes/removes it — it's their risk. The authenticated agent **is** the creditor, never a parameter. | |
| 22 | +| **Balance** | ONE signed number per unordered pair+currency ("lo owes hi"). Never two mirrored entries — the two directions are the same number, so the books *cannot* desync. | |
| 23 | +| **Payment** | BFS shortest path where every hop carries the full amount; `capacity(x→y) = limit(y→x) − debt(x→y)`. Balances shift atomically along the path. | |
| 24 | +| **Settle** | the creditor records out-of-band repayment ("peer paid me"), shrinking only a claim they hold. | |
| 25 | +| **Log** | every transition hash-chained (`seq`, `prev`, sha256 of the RFC 8785-canonical entry) — trustline.v1's contract-state framing. `GET /api/log/verify` recomputes the chain. | |
| 26 | + |
| 27 | +The two properties that make Fugger's design sing, both asserted in tests: |
| 28 | + |
| 29 | +- **Intermediaries need no per-payment consent.** Alice→Carol via Bob only |
| 30 | + consumes credit Bob and Carol *already granted* (Bob trusts Alice, Carol |
| 31 | + trusts Bob). Pre-authorization is the routing permission; the only |
| 32 | + per-request auth is the sender's — which is exactly `getAgent`. |
| 33 | +- **Signed debt gives clearing for free.** If Bob owes Carol, Carol can "pay" |
| 34 | + Bob with **zero trustline** from Bob — the payment forgives existing debt |
| 35 | + first. Reverse capacity falls out of the sign, not special-casing. |
| 36 | + |
| 37 | +Amounts cross the API as decimals (≤ 6 dp); all arithmetic is integer |
| 38 | +micro-units (bigint) — no float drift in anyone's ledger. |
| 39 | + |
| 40 | +## Endpoints |
| 41 | + |
| 42 | +``` |
| 43 | +GET /ripple UI |
| 44 | +GET /ripple/api/whoami caller's agent id |
| 45 | +GET /ripple/api/graph trustlines + IOUs (public — see Findings) |
| 46 | +GET /ripple/api/balances?agent= net + per-peer positions |
| 47 | +GET /ripple/api/path?from&to¤cy&amount dry-run pathfind |
| 48 | +POST /ripple/api/trustlines { peer, currency, limit } [creditor] |
| 49 | +POST /ripple/api/trustlines/remove { peer, currency } [creditor] |
| 50 | +POST /ripple/api/payments { to, currency, amount } [sender] |
| 51 | +POST /ripple/api/settle { peer, currency, amount } [creditor] |
| 52 | +GET /ripple/api/log?limit=N hash-chained transition log |
| 53 | +GET /ripple/api/log/verify recompute + check the chain |
| 54 | +``` |
| 55 | + |
| 56 | +## Findings |
| 57 | + |
| 58 | +- **Another whole protocol with zero seam gaps** — like recordweb/, this needs |
| 59 | + none of the open seams: `getAgent` + `pluginDir` + plain routes carry |
| 60 | + trustlines, routing, and the chained log end to end. The single-server |
| 61 | + framing is what makes that true (see the federation finding). |
| 62 | + |
| 63 | +- **Single-server atomicity is the honest MVP, and it's free.** Node's |
| 64 | + single-threaded handlers make a multi-hop payment atomic by construction: |
| 65 | + the balance shifts and the chained log append happen with no `await` |
| 66 | + between them. The HARD problem in 2004 Ripple — atomic commit of a payment |
| 67 | + crossing *hosts* — is exactly what this sidesteps, and exactly where the |
| 68 | + federation flavour begins. A cross-server hop needs a two-phase |
| 69 | + hold/commit between nodes (or an in-protocol hashlock, LN-style); nothing |
| 70 | + in the plugin api blocks trying it over loopback-style HTTP between two |
| 71 | + JSS instances (the `federation-demo/` scaffold), but the protocol design — |
| 72 | + holds, timeouts, unwind — is the real work, not the plumbing. |
| 73 | + |
| 74 | +- **Credit-graph privacy is the one real tension with Fugger's design.** |
| 75 | + Classic RipplePay showed a user only their *own* lines; this MVP serves |
| 76 | + the whole graph publicly (`/api/graph`), which the routing engine needs |
| 77 | + and which a demo wants — but a production posture wants per-agent |
| 78 | + visibility ("my lines, my balances, paths that touch me"). That is an |
| 79 | + authorization question the plugin api can't yet delegate (`api.authorize`, |
| 80 | + #604): WAC governs *pod resources*, and this state isn't pod resources. |
| 81 | + A later wave could mirror each agent's lines into their pod (the recordweb |
| 82 | + pod-delivery pattern) and let WAC govern the copies. |
| 83 | + |
| 84 | +- **The trustline is unilateral; the IOU is bilateral.** A subtle modelling |
| 85 | + point: removing a trustline doesn't erase the balance (the claim lives in |
| 86 | + the pair's signed number, keyed independently). Removal is refused (409) |
| 87 | + only while the peer still owes on that line — freeze at `limit: 0` first, |
| 88 | + settle, then remove. This keeps "you can always withdraw unused credit" |
| 89 | + and "you can never vaporize a debt record" simultaneously true. |
| 90 | + |
| 91 | +- **State-file growth**: every transition rewrites `state.json` including the |
| 92 | + full log — the same O(n) append cost class as plugins#6 (relay). Fine for |
| 93 | + the MVP scale; an NDJSON append-log is the obvious fix when it matters. |
| 94 | + |
| 95 | +## Flavours (the sequence) |
| 96 | + |
| 97 | +1. **Fugger classic** — this plugin. |
| 98 | +2. **Web-contract flavour** — the chained log *is* trustline.v1 contract |
| 99 | + state; anchor its `tip` via the forge/ Blocktrails path (Bitcoin |
| 100 | + timestamp over the whole credit history). |
| 101 | +3. **Federated flavour** — cross-pod routing between JSS instances |
| 102 | + (two-phase hold/commit or hashlocks; the real 2004 dream). |
| 103 | +4. An XRPL API shim would fit the mastodon/bluesky pattern but inherits the |
| 104 | + post-Fugger design this plugin exists to predate — skipped on purpose. |
| 105 | + |
| 106 | +## Test |
| 107 | + |
| 108 | +```bash |
| 109 | +node --test --test-concurrency=1 ripple/test.js |
| 110 | +``` |
| 111 | + |
| 112 | +14 tests: micro-unit + signed-balance + clearing units; the canonical |
| 113 | +scenario (bob trusts alice 1000, carol trusts bob 500, alice pays carol 300 |
| 114 | +through bob); capacity refusal with untouched books; currency isolation; |
| 115 | +debt-clearing reverse payments; creditor-only settle bounded by the debt; |
| 116 | +removal semantics; the chain verifying end-to-end; the UI page. |
0 commit comments