Skip to content

Commit c5cfea7

Browse files
Melvin Carvalhoclaude
andcommitted
Wire SwiftSync into the running node (node.html)
node-worker.js gains a 'swiftsync' command: a 32-byte accumulator runs the set-consistency check (add created, subtract spent; closed against start/terminal snapshots -> zero) alongside script validation, in the worker. node.html shows a 'set consistency' card. Honest copy: for the bounded range it's a demonstration; its load-bearing value is at scale (it removes the ~25 GB UTXO-set requirement). Verified in-browser: set consistency ✓ reconciled, 32-byte state, in the running node. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 7ce47a1 commit c5cfea7

2 files changed

Lines changed: 43 additions & 5 deletions

File tree

‎node-worker.js‎

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import { loadEngine, coinviewOf } from './validate-forward.js';
77
import { ShardedUtxo } from './sharded-utxo-browser.js';
88
import { followChain } from './follow-chain.js';
99
import { setVerifyBackend } from './engine/codec/secp256k1.js';
10+
import { sha256 } from './engine/codec/hash.js';
11+
import { Accumulator } from './swiftsync/accumulator.js';
12+
import { encodeOutpoint } from './swiftsync/outpoint.js';
1013

1114
let codec, be;
1215
let snap = null; // the worker's RAM-resident coin view
@@ -35,6 +38,31 @@ async function followRange() {
3538
return { validated: r.validated, total: r.total, utxoStart: start, utxoEnd: snap.size, start: range.start, end: range.end, ms: performance.now() - t0 };
3639
}
3740

41+
// SwiftSync set-consistency over the same range: a constant 32-byte accumulator
42+
// (add created outputs, subtract spent inputs) replaces holding the UTXO set for
43+
// double-spend / fabrication checks. Closed against the start (seed) + terminal
44+
// (survivors) snapshots, a valid range cancels to zero.
45+
async function swiftsync() {
46+
const NULL = '00'.repeat(32);
47+
const acc = new Accumulator({ sha256 });
48+
const seed = await (await fetch('data/range-seed.ndjson')).text();
49+
let first = true, seedN = 0;
50+
for (const l of seed.split('\n')) { if (!l) continue; if (first) { first = false; continue; } const [k] = JSON.parse(l); const i = k.lastIndexOf(':'); acc.add(encodeOutpoint({ txid: k.slice(0, i), vout: +k.slice(i + 1) })); seedN++; }
51+
const range = await (await fetch('data/range.json')).json();
52+
const survivors = new Set();
53+
let created = 0, spent = 0; const t0 = performance.now();
54+
for (const hex of range.blocks) {
55+
const block = codec.decode('Block', hex);
56+
for (const tx of block.transactions) {
57+
const txid = codec.txid(tx);
58+
for (const inp of tx.inputs) { if (inp.prevout.txid === NULL) continue; acc.spend(encodeOutpoint({ txid: inp.prevout.txid, vout: inp.prevout.vout })); spent++; survivors.delete(`${inp.prevout.txid}:${inp.prevout.vout}`); }
59+
for (let v = 0; v < tx.outputs.length; v++) { const s = tx.outputs[v].scriptPubKey; if (typeof s === 'string' && s.startsWith('6a')) continue; acc.add(encodeOutpoint({ txid, vout: v })); created++; survivors.add(`${txid}:${v}`); }
60+
}
61+
}
62+
for (const k of survivors) { const i = k.lastIndexOf(':'); acc.spend(encodeOutpoint({ txid: k.slice(0, i), vout: +k.slice(i + 1) })); }
63+
return { zero: acc.isZero(), seedN, created, spent, terminal: survivors.size, stateBytes: 32, ms: performance.now() - t0 };
64+
}
65+
3866
// Checkpoint the coin view via an OPFS *synchronous access handle* (Worker-only).
3967
async function checkpoint() {
4068
if (!snap) throw new Error('no coin view — run followRange first');
@@ -64,7 +92,7 @@ async function resume() {
6492
return { coins: snap.size };
6593
}
6694

67-
const handlers = { init, followRange, checkpoint, resume };
95+
const handlers = { init, followRange, checkpoint, resume, swiftsync };
6896
self.onmessage = async (e) => {
6997
const { id, cmd } = e.data;
7098
try { self.postMessage({ id, ok: true, result: await handlers[cmd]() }); }

‎node.html‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
<h1>bitcoin-kernel / browser-node — running node <span class="dim">(testnet4)</span></h1>
2727
<p class="dim">The capstone: one continuous pipeline. Headers stream from a real peer over the bridge; the UTXO set is
2828
bootstrapped from a snapshot; blocks are validated forward and applied — the engine, WASM secp, UTXO set and OPFS
29-
persistence all run in a <b>Web Worker</b>. <a href="index.html">← the eleven acts, explained</a></p>
29+
persistence all run in a <b>Web Worker</b>, and set-consistency is checked with a <b>SwiftSync 32-byte accumulator</b>
30+
(not the full UTXO set). <a href="index.html">← the eleven acts, explained</a></p>
3031

3132
<p><button id="start">▶ Start node</button> <span id="status" class="dim">idle</span></p>
3233

@@ -37,16 +38,19 @@ <h1>bitcoin-kernel / browser-node — running node <span class="dim">(testnet4)<
3738
<div class="card"><div class="k">UTXO set</div><div class="v" id="utxo">—</div><div class="s">RAM, checkpointed to OPFS</div></div>
3839
<div class="card"><div class="k">blocks validated</div><div class="v" id="nblocks">0</div><div class="s" id="bps"></div></div>
3940
<div class="card"><div class="k">persistence</div><div class="v" id="ckpt">—</div><div class="s">OPFS sync access handle</div></div>
41+
<div class="card"><div class="k">set consistency</div><div class="v" id="swift">—</div><div class="s" id="swiftsub">SwiftSync · 32-byte accumulator</div></div>
4042
</div>
4143
<div id="log"></div>
4244

4345
<p class="dim" style="margin-top:14px; border-top:1px solid var(--bd); padding-top:10px">
4446
Honest scope: this runs the node's real loop over a bounded range of real testnet4 blocks. Reaching the live tip
4547
with the <b>full</b> UTXO set hits a memory wall — the whole 14.1M-coin set is ~25 GB, over a tab's limit.
4648
<a href="index.html#">SwiftSync (act ⑪)</a> removes it: validate set-consistency with a <b>32-byte accumulator</b> instead of
47-
the set. Wiring SwiftSync into this worker loop (with a WebTorrent hints file) is the next step;
48-
<code>tools/reach-tip.mjs</code> already reaches the live tip today by keeping only the coins the forward blocks
49-
spend. Header sync needs the local bridge (<code>node bridge.mjs</code> + a testnet4 node); without it the
49+
the set — <b>now wired into this loop</b> (the "set consistency" card above). What remains is applying it at
50+
<i>scale</i>: a large range / full IBD with a WebTorrent hints file + streamed prevout data for scripts, so the node
51+
validates to the live tip holding nothing but the accumulator. <code>tools/reach-tip.mjs</code> already reaches the
52+
live tip today by keeping only the coins the forward blocks spend. Header sync needs the local bridge
53+
(<code>node bridge.mjs</code> + a testnet4 node); without it the
5054
validation pipeline still runs.</p>
5155

5256
<script type="module">
@@ -119,6 +123,12 @@ <h1>bitcoin-kernel / browser-node — running node <span class="dim">(testnet4)<
119123
$('ckpt').textContent = (ck.bytes/1048576).toFixed(2) + ' MB';
120124
log(`UTXO set checkpointed to OPFS (${(ck.bytes/1048576).toFixed(2)} MB, sync access handle)`, 'ok');
121125

126+
// SwiftSync set-consistency check in the worker — 32-byte state, no UTXO set
127+
const ss = await wcall('swiftsync');
128+
$('swift').innerHTML = ss.zero ? '<span class="ok">✓ reconciled</span>' : '<span class="err">✗ non-zero</span>';
129+
$('swiftsub').textContent = `SwiftSync · ${ss.stateBytes} bytes · ${fmt(ss.created)}+ ${fmt(ss.spent)}− · ${ss.ms.toFixed(0)}ms`;
130+
log(`SwiftSync set-consistency: ${ss.zero ? 'reconciles to ZERO' : 'NON-ZERO'} with ${ss.stateBytes} bytes — no UTXO set held for double-spend checks (the path past the 25 GB ceiling at scale)`, ss.zero ? 'ok' : 'err');
131+
122132
setStatus(`● running — fully validated to #${fmt(r.end)}; watching the tip`, 'ok');
123133
log('node running. Headers live from the network; UTXO bootstrapped + validated forward in the worker.', 'ok');
124134
} catch (e) { setStatus('error: ' + e.message, 'err'); log('node error: ' + e.message, 'err'); console.error(e); $('start').disabled = false; }

0 commit comments

Comments
 (0)