Skip to content

Commit 5b077b8

Browse files
federation first light + live Bitcoin anchoring
Spec §13 (Draft): sovereign nodes / global agents; the gateway pattern (implemented — tools/xnode-demo.js ran alice@melvin.me → carol@ melvincarvalho.com through a did:nostr gateway live between the two public nodes, signatures as the only coordination, gateway books balancing across nodes); atomic hold/commit as the named level-2 work with the griefing dragon documented; and settlement assurance as a pluggable interface with Blocktrails as the first backend — LIVE: a testnet4 federation trail carrying both nodes' audited tips as chained P2TR marks (4 marks broadcast), served at GET /api/anchors, with the dispute rule stated. Roadmap + landing updated to match reality. 38 tests green.
1 parent 2913fd9 commit 5b077b8

4 files changed

Lines changed: 160 additions & 14 deletions

File tree

docs/roadmap.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,18 @@
3030

3131
## v2 — federation
3232

33-
- [ ] Node-to-node peering: cross-node trustlines and routed payments
34-
(two-phase hold/commit; evaluate hashlocks à la LN)
33+
- [x] TWO public testnet nodes (melvin.me:3480 + melvincarvalho.com:3480)
34+
- [x] Gateway-pattern cross-node payments (spec §13.2, tools/xnode-demo.js):
35+
alice@A → carol@B through a did:nostr gateway — signatures are the
36+
only coordination; demonstrated live between the two nodes
37+
- [ ] Atomic routes: hold/commit transitions with TTL + hashlocks (spec §13.3)
3538
- [ ] Signed transitions as nostr events over relays (discovery + transport)
36-
- [ ] Multiple public testnets, then peering between them
3739

3840
## v3 — anchoring & beyond
3941

40-
- [ ] Anchor chain tips to Bitcoin via Blocktrails (public timestamps)
42+
- [x] Anchor chain tips to Bitcoin via Blocktrails (spec §13.4,
43+
tools/anchor.js + GET /api/anchors) — LIVE on testnet4: a federation
44+
trail carrying both nodes' audited tips as chained P2TR marks
4145
- [ ] Pod-delivered statements (Solid pods as citizen-controlled copies,
4246
the recordweb pattern)
4347
- [ ] Agents as participants (autonomous trust management, routing fees?)

docs/spec/index.html

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ <h2 id="sotd">Status of This Document<a class="self" href="#sotd">§</a></h2>
145145
<li><a href="#http"><span class="n">10</span>HTTP API</a></li>
146146
<li><a href="#security"><span class="n">11</span>Security considerations</a></li>
147147
<li><a href="#privacy"><span class="n">12</span>Privacy considerations</a></li>
148+
<li><a href="#federation"><span class="n">13</span>Federation (draft)</a></li>
148149
<li><a href="#vectors"><span class="n">A</span>Test vectors</a></li>
149150
<li><a href="#references"><span class="n">B</span>References</a></li>
150151
</ol></nav>
@@ -563,6 +564,59 @@ <h2 id="privacy"><span class="secno">12.</span>Privacy considerations<a class="s
563564
expose the full graph publicly. Usernames are public by construction (they are URIs); agents who
564565
need unlinkability <span class="rfc">should</span> use pseudonymous names or DIDs.</p>
565566

567+
<h2 id="federation"><span class="secno">13.</span>Federation
568+
<span class="pill" style="vertical-align:middle;margin-left:8px">Draft</span><a class="self" href="#federation">§</a></h2>
569+
<h3 id="fed-model"><span class="secno">13.1</span>Model — sovereign nodes, global agents<a class="self" href="#fed-model">§</a></h3>
570+
<p>Nodes are sovereign: each holds its own ledger and answers for its own history. Agents are
571+
global: an identity is a URI or DID, so the same agent can hold trustlines on many nodes at once.
572+
A trustline (and its balance) lives on exactly <em>one</em> node — the node both parties accepted
573+
when the line was created. There is no global state and no consensus between nodes; there are only
574+
per-node ledgers whose entries are signed by their actors (§&nbsp;9) and anchored histories that
575+
make lying provable (§&nbsp;13.3).</p>
576+
<h3 id="fed-gateway"><span class="secno">13.2</span>The gateway pattern (implemented)<a class="self" href="#fed-gateway">§</a></h3>
577+
<p>Cross-node value transfer composes from per-node payments through a <dfn>gateway</dfn> — an
578+
agent with credit relationships on both nodes. For alice@A → carol@B via gateway G:</p>
579+
<ol class="algo">
580+
<li>G holds (or creates, with one signed transition to each node) trust relationships:
581+
G→alice on A, carol→G on B.</li>
582+
<li>alice pays G on node A — consuming credit G already granted.</li>
583+
<li>G pays carol on node B — consuming credit carol already granted.</li>
584+
</ol>
585+
<p>Net: value crossed nodes; G's books balance across them (+x owed to it on A, −x owed by it on
586+
B). The gateway's signatures are the <em>only</em> coordination — no node-to-node protocol is
587+
required, which is why this level ships already (the repository's
588+
<code>tools/xnode-demo.js</code> runs it against two live nodes). The honest caveat: steps 2 and 3
589+
are <em>not atomic</em>; between them the gateway bears the risk, which is precisely a gateway's
590+
business (and Fugger's original inter-server design assumption).</p>
591+
<h3 id="fed-atomic"><span class="secno">13.3</span>Toward atomic routes (level 2, unimplemented)<a class="self" href="#fed-atomic">§</a></h3>
592+
<p>Removing the gateway's window of risk needs conditional transitions: a
593+
<code>hold</code> reserving capacity on each leg with a TTL, released by a <code>commit</code>
594+
carrying a hashlock preimage (HTLC-shaped), else unwound at expiry. Known dragon, named here so it
595+
is not rediscovered: holds lock capacity, so hold-griefing and liquidity-lockup are real costs —
596+
the reason Interledger moved to packetized amounts. Level 2 will specify hold/commit transitions
597+
as two further signed event kinds; nothing in the level-1 data model changes.</p>
598+
<h3 id="fed-anchor"><span class="secno">13.4</span>Settlement assurance — anchoring (implemented)<a class="self" href="#fed-anchor">§</a></h3>
599+
<p>Because state is a pure function of the log (§&nbsp;8.2), the only lie available to a node is
600+
presenting <em>different histories to different parties</em>. Anchoring makes that refutable. The
601+
interface is deliberately pluggable:</p>
602+
<pre><code>anchor(state) → proof <span class="c">// commit to a state externally</span>
603+
verify(proof) → attested time <span class="c">// check a commitment</span>
604+
resolve(histA, histB) → … <span class="c">// which history was committed to, when</span></code></pre>
605+
<p>The first backend is <strong>Blocktrails</strong> [<a href="#ref-blocktrails">BLOCKTRAILS</a>]
606+
(implemented; <code>tools/anchor.js</code>): the anchored state is the string
607+
<code>{"node":"&lt;origin&gt;","seq":n,"tip":"sha256:…"}</code> (literal key order); each anchor
608+
tweaks the trail key by the state hash (chained BIP-341 TapTweak) yielding a fresh P2TR address,
609+
and the mark transaction pays the trail's balance forward to it. The chain of spends <em>is</em>
610+
the anchor history — ordered and timestamped by Bitcoin. One trail <span class="rfc">may</span>
611+
carry many nodes' tips (a <dfn>federation trail</dfn>); the state names the node. A node
612+
<span class="rfc">must not</span> anchor a tip whose own audit fails. Anchors are served read-only
613+
at <code>GET /api/anchors</code>. Dispute rule: a history whose (seq, tip) was anchored at
614+
Bitcoin-attested time T refutes any competing history presented later that diverges before T —
615+
the node is caught by its own commitment.</p>
616+
<div class="note"><span class="lbl">Note</span><p>This is not consensus, on purpose. The network
617+
keeps no global state to agree on; anchoring makes each node's <em>own</em> statements
618+
progressively harder to retract. Trust stays bilateral; evidence becomes public.</p></div>
619+
566620
<h2 id="vectors"><span class="secno">A.</span>Test vectors<a class="self" href="#vectors">§</a></h2>
567621
<p>An implementation is expected to reproduce these values exactly. Agents:
568622
<code>A = https://n.example/u/alice#me</code>, <code>B = …/u/bob#me</code>,
@@ -637,8 +691,9 @@ <h3>B.2 Informative</h3>
637691
HTTP Auth</a></li>
638692
<li>[TRUSTLINE-V1] — <a href="https://github.com/webcontracts/webcontracts.github.io/issues/4">trustline.v1
639693
profile — Ryan Fugger's original Ripple as a web contract</a></li>
640-
<li>[BLOCKTRAILS] — <a href="https://blocktrails.org">Blocktrails: Bitcoin-anchored history</a>
641-
<em>(anchoring, level 3)</em></li>
694+
<li id="ref-blocktrails">[BLOCKTRAILS] — <a href="https://blocktrails.org">Blocktrails: Bitcoin-anchored
695+
history</a> — reference implementation <a href="https://www.npmjs.com/package/blocktrails">npm
696+
<code>blocktrails</code></a></li>
642697
</ul>
643698

644699
<footer>

index.html

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -188,10 +188,11 @@ <h2>What 2004 didn't have</h2>
188188
nostr key (extension or xlogin), <code>POST /api/tx</code>, done. The node verifies, never
189189
attributes: authorship is provable by anyone replaying the log. The server is a coordinator,
190190
not an authority — verify, don't trust.</p></div>
191-
<div class="card"><h3><span class="ic"></span>A network of nodes <small style="color:var(--warn);font-weight:600">v2</small></h3>
192-
<p>Signed transitions make federation possible: routes that cross nodes, relayed and verified
193-
anywhere — the part that was genuinely hard in 2004. Chain tips can anchor to Bitcoin via
194-
<a href="https://blocktrails.org">Blocktrails</a> for public timestamps.</p></div>
191+
<div class="card"><h3><span class="ic"></span>A network of nodes</h3>
192+
<p><strong>Two nodes live.</strong> Cross-node payments flow through did:nostr gateway agents —
193+
signatures are the only coordination — and both nodes' audited tips are anchored to Bitcoin
194+
testnet4 on a <a href="https://blocktrails.org">Blocktrails</a> trail. Atomic routes
195+
(hold/commit) are the next protocol level.</p></div>
195196
</div>
196197
</div></section>
197198

@@ -230,10 +231,13 @@ <h2>Testnet first, then the hard parts</h2>
230231
<p>Shipped: schnorr-signed transitions as nostr events (kinds 8801–8804) — the ledger is
231232
verify-don't-trust, and <a href="docs/spec/#signatures">spec §9</a> is normative. Remaining in
232233
v1: per-agent privacy views, WebID agents, custody export.</p></div></li>
233-
<li><span class="v">v2</span><div><b>Federation</b>
234-
<p>Cross-node routing over signed transitions — hold/commit or relay gossip. Multiple testnets, then peering.</p></div></li>
235-
<li><span class="v">v3</span><div><b>Anchoring &amp; beyond</b>
236-
<p>Chain tips anchored to Bitcoin (Blocktrails), pod-delivered statements, agent participants.</p></div></li>
234+
<li><span class="v">v2</span><div><b>Federation<span class="now">first light</span></b>
235+
<p>Two live nodes; cross-node payments through did:nostr gateways — demonstrated on the open
236+
internet (<a href="docs/spec/#federation">spec §13</a>). Next: atomic routes (hold/commit).</p></div></li>
237+
<li><span class="v">v3</span><div><b>Anchoring<span class="now">live</span></b>
238+
<p>Both nodes' audited tips anchored to Bitcoin testnet4 on a
239+
<a href="https://blocktrails.org">Blocktrails</a> federation trail —
240+
chained P2TR marks, served at <code>/api/anchors</code>.</p></div></li>
237241
</ul>
238242
</div></section>
239243

tools/xnode-demo.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Cross-node payment demo — federation with today's primitives.
2+
//
3+
// node tools/xnode-demo.js http://nodeA:3480 http://nodeB:3480
4+
//
5+
// alice@A pays carol@B through a GATEWAY agent G — a did:nostr key that
6+
// exists on both nodes at once (identity is a URI/DID, so "which node" is
7+
// just where a trustline happens to live):
8+
//
9+
// 1. G signs set-trustline(alice@A) on A — G trusts alice on A
10+
// 2. carol@B extends trust to G on B — carol trusts G on B
11+
// 3. alice pays G 25 USD on A — uses credit G granted
12+
// 4. G signs send-payment(carol@B) on B — uses credit carol granted
13+
//
14+
// Net: value moved alice@A → carol@B. G's books balance across nodes:
15+
// +25 owed to it on A, −25 owed by it on B. No server-to-server protocol —
16+
// the gateway's signatures ARE the coordination. (Atomic two-node commit —
17+
// holds/hashlocks — is the level-2 protocol this demo motivates; see
18+
// docs/spec/ § 13.)
19+
20+
import crypto from 'node:crypto';
21+
import { buildTxEvent } from '../lib/tx.js';
22+
23+
const [A, B] = [process.argv[2] || 'http://localhost:3480', process.argv[3] || 'http://localhost:3481']
24+
.map((u) => u.replace(/\/$/, ''));
25+
const PASS = 'solidpay-demo';
26+
const say = (s) => console.log(s);
27+
const jsonH = { 'content-type': 'application/json' };
28+
29+
const post = (base, p, token, body) => fetch(base + p, {
30+
method: 'POST', headers: { ...jsonH, ...(token ? { authorization: `Bearer ${token}` } : {}) },
31+
body: JSON.stringify(body),
32+
}).then(async (r) => ({ status: r.status, body: await r.json().catch(() => ({})) }));
33+
const tx = (base, ev) => post(base, '/api/tx', null, ev);
34+
async function login(base, name) {
35+
let r = await post(base, '/api/login', null, { username: name, password: PASS });
36+
if (r.status !== 200) r = await post(base, '/api/register', null, { username: name, password: PASS });
37+
if (!r.body.token) throw new Error(`${name}@${base}: ${r.body.error}`);
38+
return r.body;
39+
}
40+
41+
// The gateway: a fresh throwaway did:nostr key.
42+
const gPriv = crypto.randomBytes(32).toString('hex');
43+
44+
say(`\n— SolidPay cross-node payment —\n node A: ${A}\n node B: ${B}\n`);
45+
46+
const alice = await login(A, 'alice');
47+
const carol = await login(B, 'carol');
48+
49+
// 1. G trusts alice on A (signed by G, submitted to A).
50+
const t1 = await tx(A, buildTxEvent(gPriv, 'set-trustline', { peer: alice.agent, currency: 'USD', limit: 100 }));
51+
if (t1.status !== 201 && t1.status !== 200) throw new Error(`step1: ${t1.body.error}`);
52+
const G = t1.body.trustline.creditor;
53+
say(`1. gateway ${G.slice(0, 30)}… trusts alice@A for 100 USD [node A, signed by G]`);
54+
55+
// 2. carol trusts G on B.
56+
const t2 = await post(B, '/api/trustlines', carol.token, { peer: G, currency: 'USD', limit: 100 });
57+
if (t2.status !== 201 && t2.status !== 200) throw new Error(`step2: ${t2.body.error}`);
58+
say(`2. carol@B trusts the gateway for 100 USD [node B, custodially signed]`);
59+
60+
// 3. alice pays G 25 on A.
61+
const p1 = await post(A, '/api/payments', alice.token, { to: G, currency: 'USD', amount: 25 });
62+
if (p1.status !== 200) throw new Error(`step3: ${p1.body.error}`);
63+
say(`3. alice@A pays the gateway 25 USD [node A, ${p1.body.payment.path.length - 1} hop(s)]`);
64+
65+
// 4. G pays carol 25 on B (signed by G, submitted to B).
66+
const p2 = await tx(B, buildTxEvent(gPriv, 'send-payment', { to: carol.agent, currency: 'USD', amount: 25 }));
67+
if (p2.status !== 200) throw new Error(`step4: ${p2.body.error}`);
68+
say(`4. gateway pays carol@B 25 USD [node B, signed by G]`);
69+
70+
// ---- the books, both sides ------------------------------------------------
71+
const gA = await (await fetch(`${A}/api/balances?agent=${encodeURIComponent(G)}`)).json();
72+
const gB = await (await fetch(`${B}/api/balances?agent=${encodeURIComponent(G)}`)).json();
73+
const cB = await (await fetch(`${B}/api/balances?agent=${encodeURIComponent(carol.agent)}`)).json();
74+
const vA = await (await fetch(`${A}/api/log/verify`)).json();
75+
const vB = await (await fetch(`${B}/api/log/verify`)).json();
76+
77+
say(`\n— result —`);
78+
say(` gateway on A: ${gA.net.USD > 0 ? '+' : ''}${gA.net.USD ?? 0} USD (owed to it)`);
79+
say(` gateway on B: ${gB.net.USD > 0 ? '+' : ''}${gB.net.USD ?? 0} USD (it owes)`);
80+
say(` carol@B net: +${cB.net.USD} USD — value crossed nodes`);
81+
say(` audit A: valid=${vA.valid} signed=${vA.signatures.signed} invalid=${vA.signatures.invalid}`);
82+
say(` audit B: valid=${vB.valid} signed=${vB.signatures.signed} invalid=${vB.signatures.invalid}`);
83+
say(`\nThe gateway's signatures were the only coordination. Level 2 makes\nsteps 3+4 atomic (hold/commit); the anchors make either side's history\nindisputable. See docs/spec/ § 13.\n`);

0 commit comments

Comments
 (0)