Skip to content

Commit ba2ca00

Browse files
ripple: Fugger-classic trustlines — credit routing, settle, chained log
Ryan Fugger's ORIGINAL Ripple (2004) as a plugin (webcontracts#4, the trustline.v1 profile): unilateral trustlines (the authenticated agent IS the creditor), payments BFS-routed through chains of pre-granted credit with no per-payment consent from intermediaries, creditor-only settle bounded by the debt, and a hash-chained transition log (seq/prev over RFC 8785-canonical entries) ready for Blocktrails anchoring. Bookkeeping is one SIGNED balance per unordered pair+currency — the two directions are the same number, so the books cannot desync, and clearing falls out free (owed-to-you credit is spendable with zero trustline). All arithmetic in bigint micro-units; decimals only at the API edge. Single-server atomicity is the honest MVP; cross-host commit is the named federation flavour. Zero deps, zero seam gaps; graph privacy documented as the api.authorize-shaped finding. UI page + JSON API; 14 tests; full suite 605 green.
1 parent 7155eee commit ba2ca00

6 files changed

Lines changed: 840 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ README and NOTES.md, ship the closest honest approximation.
9494
| `gallery/` || pod photo/media gallery — first `api.mountApp` (#583) consumer; zero required config |
9595
| `forge/` | #322-next | git forge — hosting, GitHub-light UI + JSON API, forks/PRs with real merges, issues as pod resources, did:nostr + xlogin, Bitcoin-anchored history (Blocktrails); zero deps, zero build |
9696
| `recordweb/` || [RecordWeb](https://github.com/recordweb) (RWP) node — institutional Records with `did:rwp`, content-addressed immutable snapshots in a version DAG (RFC 8785 + SHA-256 + Ed25519), one-way finalization, Merkle-rooted Cases; DID resolver + verify; **zero deps, zero seam gaps** — the four landed seams suffice for a from-scratch protocol |
97+
| `ripple/` | [webcontracts#4](https://github.com/webcontracts/webcontracts.github.io/issues/4) | Ryan Fugger's ORIGINAL Ripple (2004) — bilateral trustlines, BFS payment routing through chains of pre-granted credit, creditor-only settle, hash-chained transition log (trustline.v1); signed single-balance bookkeeping, bigint micro-units, UI page; single-server atomicity honest, federation is the named next flavour |
9798

9899
Plus one directory that is a scenario, not a plugin:
99100
[`federation-demo/`](./federation-demo/)**two** JSS instances federating

compose.test.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ describe('composition: every plugin on one server', () => {
9898
{ module: at('gallery/plugin.js'), prefix: '/gallery' },
9999
{ module: at('forge/plugin.js'), prefix: '/forge' },
100100
{ module: at('recordweb/plugin.js'), prefix: '/recordweb', config: { baseUrl: base, loopbackUrl: base } },
101+
{ module: at('ripple/plugin.js'), prefix: '/ripple' },
101102
{
102103
module: at('remotestorage/plugin.js'),
103104
prefix: '/remotestorage',
@@ -371,6 +372,17 @@ describe('composition: every plugin on one server', () => {
371372
assert.ok([401, 403].includes(up.status), `anon upload: ${up.status}`);
372373
});
373374

375+
it('ripple: the UI answers; an anonymous trustline is refused', async () => {
376+
const page = await fetch(`${base}/ripple`);
377+
assert.strictEqual(page.status, 200);
378+
assert.match(page.headers.get('content-type') || '', /text\/html/);
379+
const tl = await fetch(`${base}/ripple/api/trustlines`, {
380+
method: 'POST', headers: { 'content-type': 'application/json' },
381+
body: JSON.stringify({ peer: 'x', currency: 'USD', limit: 1 }),
382+
});
383+
assert.strictEqual(tl.status, 401, `anon trustline: ${tl.status}`);
384+
});
385+
374386
it('recordweb: the resolver-discovery doc answers; an anonymous Record create is refused', async () => {
375387
const wk = await fetch(`${base}/.well-known/rwp-resolver.json`);
376388
assert.strictEqual(wk.status, 200);

ripple/README.md

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
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&currency&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

Comments
 (0)