-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathmesh.html
More file actions
189 lines (175 loc) · 12.2 KB
/
Copy pathmesh.html
File metadata and controls
189 lines (175 loc) · 12.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>bitcoin-kernel/browser-node — mesh: browser↔browser block propagation</title>
<style>
:root { --bg:#fff; --fg:#16181d; --mut:#5b6470; --bd:#e6e8eb; --pan:#fafbfc; --ac:#e8830c; --ac2:#0969da; --good:#1a7f37; --bad:#cf222e; --mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; color-scheme:light; }
body { background:var(--bg); color:var(--fg); font:14px/1.5 var(--mono); max-width:760px; margin:24px auto; padding:0 16px; }
h1 { font-size:18px; } a { color:var(--ac2); }
button { background:var(--pan); color:var(--fg); border:1px solid var(--bd); border-radius:6px; padding:9px 16px; cursor:pointer; font:inherit; }
button:hover { background:#f0f2f4; border-color:var(--ac); } button:disabled { opacity:.45; cursor:default; }
.grid { display:grid; grid-template-columns:repeat(3,1fr); gap:10px; margin:14px 0; }
.card { border:1px solid var(--bd); border-radius:8px; padding:10px 12px; background:var(--pan); }
.card .k { color:var(--mut); font-size:12px; text-transform:uppercase; letter-spacing:.04em; }
.card .v { font-size:20px; color:var(--fg); margin-top:3px; }
.card .s { color:var(--mut); font-size:12px; }
#log { background:var(--pan); border:1px solid var(--bd); border-radius:6px; padding:12px; height:200px; overflow:auto; white-space:pre-wrap; margin-top:12px; }
.ok{color:var(--good)} .warn{color:var(--ac)} .err{color:var(--bad)} .dim{color:var(--mut)}
.dot{ display:inline-block; width:9px; height:9px; border-radius:50%; background:var(--mut); margin-right:6px; vertical-align:middle; }
.dot.live{ background:var(--good); animation:pulse 2s ease-in-out infinite; }
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.35} }
</style>
</head>
<body>
<h1>bitcoin-kernel / browser-node — mesh <span class="dim">(block propagation · PoC)</span></h1>
<p class="dim"><b>Open this page in two or more tabs</b> (or send the link to a friend). The tabs become Bitcoin peers to each other
over <b>WebRTC — no server in the data path</b> — every tab connects to every other (a full mesh) in a room on a signaling server.
Hit <b>Seed 5000</b> in one tab: it pulls <b>5000 real blocks</b> from the network (via the bridge) and <b>propagates them to the
other peers</b>, which <b>validate every one</b> (proof-of-work + merkle root). That's block propagation at volume, in a browser mesh.
<a href="node.html">the node</a> · <a href="how-it-works.html">how it works</a></p>
<p>
<button id="connect">▶ Connect to the mesh</button>
<button id="seed" disabled>🌱 Seed 5000 blocks from the network → peer</button>
<span id="status" class="dim">idle</span>
</p>
<div class="grid">
<div class="card"><div class="k"><span class="dot" id="dot"></span>mesh</div><div class="v" id="mesh">—</div><div class="s" id="role">browser ↔ browser</div></div>
<div class="card"><div class="k">seeded from network</div><div class="v" id="seeded">—</div><div class="s">bridge → mesh</div></div>
<div class="card"><div class="k">blocks received + validated</div><div class="v" id="relayed">0</div><div class="s">from the mesh peer</div></div>
<div class="card"><div class="k">last block</div><div class="v" id="last">—</div><div class="s" id="lasthash">PoW + merkle</div></div>
</div>
<div id="log"></div>
<p class="dim" style="margin-top:14px; border-top:1px solid var(--bd); padding-top:10px">
PoC scope: <b>Seed 5000</b> pulls real blocks from the network (via the bridge) and relays them to the peer, each validated for
structure + proof-of-work (no UTXO set, so not full-consensus). Every tab connects to every other (full mesh), and a received
block is <b>gossiped onward</b> to other peers (deduped by a seen-set), so it propagates across the mesh, multi-hop. Next: stream the full chain.
Default room <code>c0ffeebabe123456</code>; override with
<code>?room=<hex></code>, the signaling server with <code>?signal=wss://your.pod/.webrtc</code>, and the block count with <code>?blocks=N</code> (default 5000).
See gossip in action with <code>?gossip=1</code>: the source sends each block to <b>one</b> neighbor and the mesh carries it to the rest (so a tab the source never sent to still gets every block).</p>
<script type="module">
import { Codec } from './engine/codec/codec.js';
import { P2pEngine } from './engine/codec/p2p.js';
import { BlockEngine } from './engine/codec/blocks.js';
import { MeshPeer } from './peer-mesh.js';
import { RtcPeer } from './peer-rtc.js';
const $ = (id) => document.getElementById(id);
const log = (m, c='') => { const t = new Date().toISOString().slice(11,19); $('log').innerHTML += `<span class="dim">${t}</span> <span class="${c}">${m}</span>\n`; $('log').scrollTop = $('log').scrollHeight; };
const fmt = (n) => Number(n).toLocaleString();
const setStatus = (m, c='') => $('status').innerHTML = `<span class="${c}">${m}</span>`;
const MSG_WITNESS_BLOCK = 1073741826;
const toHex = (b) => Array.from(b, (x) => x.toString(16).padStart(2, '0')).join('');
const q = new URLSearchParams(location.search);
const signalUrl = q.get('signal') || 'wss://melvincarvalho.com/.webrtc';
const room = q.get('room') || 'c0ffeebabe123456'; // mesh's own room (distinct from the bridge's room)
const seedCount = Math.max(1, parseInt(q.get('blocks'), 10) || 5000); // ?blocks=N (default 5000)
const gossipMode = q.get('gossip') === '1'; // ?gossip=1 → seed sends each block to ONE neighbor; the mesh gossips it onward
let codec, p2p, be, peer = null, genesisHash = null;
let relayed = 0;
async function loadEngine() {
const jl = (n) => fetch(`./engine/schema/${n}.jsonld`).then(r => r.json());
const [core, proof, p2pS, chain, validate, script] = await Promise.all(['core','proof','p2p','chain','validate','script'].map(jl));
codec = new Codec(core, proof, p2pS);
p2p = P2pEngine.fromSchemas(codec, p2pS, chain, 'btc:testnet4');
be = BlockEngine.fromSchemas(codec, chain, validate, script, 'btc:testnet4');
const vectors = await (await fetch('./data/testnet4.json')).json();
genesisHash = codec.blockHash(codec.decode('BlockHeader', vectors.genesisHeader));
}
// Backpressure: don't overrun any connected peer's send buffer during a big burst.
async function drain(p, max = 4 * 1024 * 1024) { while (p.bufferedHigh(max)) await new Promise((r) => setTimeout(r, 50)); }
// SEED: pull N real blocks from the network (via the bridge), validate each, and
// relay them to the mesh peer — block propagation at volume, browser→browser.
async function seedBlocks(n = 5000) {
$('seed').disabled = true;
if (gossipMode) log('gossip mode: each block goes to ONE neighbor — the mesh carries it to the rest', 'ok');
const bridgeSignal = q.get('signal') || 'wss://melvincarvalho.com/.webrtc';
const bridgeRoom = q.get('bridgeRoom') || 'b17c0192abad1deacafe'; // the bridge's room (its network source)
const net = new RtcPeer(p2p, codec);
const MAX_MSG = 200 * 1024; // single data-channel message safety cap (bytes)
try {
setStatus('seed: connecting to the network via the bridge…', 'warn');
log(`seed: connecting to the network (bridge room ${bridgeRoom.slice(0, 10)}…)`);
await net.connect(bridgeSignal, { room: bridgeRoom });
log('seed: connected to a testnet4 peer — fetching headers', 'ok');
// getheaders returns ≤2000/msg → paginate, continuing the locator from the last hash.
const hashes = []; let locator = [genesisHash];
while (hashes.length < n) {
net.send('getheaders', { version: 70016, blockLocator: locator, hashStop: '0'.repeat(64) });
const hmsg = await net.waitFor(['headers'], 30000);
const hdrs = (hmsg.payload?.entries ?? []).map((e) => e.header);
if (!hdrs.length) break;
for (const h of hdrs) { hashes.push(codec.blockHash(h)); if (hashes.length >= n) break; }
locator = [hashes[hashes.length - 1]];
setStatus(`seed: fetched ${fmt(hashes.length)} headers…`, 'warn');
}
log(`seed: have ${fmt(hashes.length)} block hashes — fetching + relaying`, 'ok');
const BATCH = 50; let got = 0, skipped = 0; const t0 = performance.now();
for (let i = 0; i < hashes.length; i += BATCH) {
const batch = hashes.slice(i, i + BATCH);
net.send('getdata', { items: batch.map((hash) => ({ type: MSG_WITNESS_BLOCK, hash })) });
const out = [];
await net.collect('block', batch.length, { onItem: (msg) => {
if (be.validateBlockStructure(msg.payload).results.some((r) => r.ok === false)) return; // validate before relay
out.push(codec.encode('Block', msg.payload));
}, timeoutMs: 30000 });
for (const bytes of out) {
if (bytes.length > MAX_MSG) { skipped++; continue; } // too big for one DC message (chunking is a TODO)
await drain(peer);
if (gossipMode) peer.sendToOne('block', toHex(bytes)); else peer.send('block', toHex(bytes)); // gossip: one neighbor; the mesh carries it onward
got++; $('seeded').textContent = fmt(got);
}
setStatus(`seed: relaying ${fmt(got)}/${fmt(hashes.length)}${skipped ? ` (${skipped} too large, skipped)` : ''}…`, 'warn');
}
log(`seed: fetched + relayed ${fmt(got)} blocks${skipped ? `, skipped ${skipped} over ${MAX_MSG / 1024}KB` : ''} in ${((performance.now() - t0) / 1000).toFixed(1)}s`, 'ok');
setStatus(`● seeded ${fmt(got)} blocks to the peer`, 'ok');
net.close();
} catch (e) {
setStatus('seed error: ' + e.message, 'err');
log('seed: ' + e.message + ' — is the bridge running? (the seed pulls blocks from the network through it)', 'err');
$('seed').disabled = false; try { net.close(); } catch {}
}
}
// Handle a Bitcoin message from a mesh peer, validate it, then gossip it onward.
const seen = new Set(); // block hashes already processed (gossip dedup / loop stop)
function onMessage(msg, from) {
if (msg.command !== 'block') return;
const block = msg.payload;
const hash = codec.blockHash(block.header);
if (seen.has(hash)) return; // already have it — don't re-validate, re-count, or re-forward
const failed = be.validateBlockStructure(block).results.filter(r => r.ok === false).map(r => r.rule);
if (failed.length) { log(`block ${hash.slice(0,16)}… ✗ INVALID: ${failed.join(', ')}`, 'err'); return; }
seen.add(hash);
relayed++; $('relayed').textContent = fmt(relayed);
$('last').textContent = hash.slice(0, 10) + '…'; $('lasthash').textContent = `${block.transactions.length} txs · PoW + merkle ✓`;
if (relayed <= 3 || relayed % 100 === 0) log(`✅ received + validated block ${relayed} (${hash.slice(0,16)}…, ${block.transactions.length} txs) from the mesh — no server`, 'ok');
peer.forward('block', block, from); // gossip onward to other peers (not the sender)
}
$('connect').onclick = async () => {
$('connect').disabled = true;
try {
setStatus('loading engine…', 'warn');
if (!be) await loadEngine();
setStatus(`joining the mesh room ${room.slice(0,10)}…`, 'warn');
log(`connecting to the mesh (signaling ${signalUrl})…`);
peer = new MeshPeer(p2p, codec, { log, onMessage, onPeers: (count) => {
$('dot').classList.toggle('live', count > 0); $('mesh').textContent = count;
$('role').textContent = `${count} peer${count === 1 ? '' : 's'} connected`;
$('seed').disabled = count < 1;
setStatus(count > 0 ? `● connected · ${count} peer${count === 1 ? '' : 's'} — seed blocks` : '● in the mesh room — waiting for a peer', 'ok');
log(`mesh: connected to ${count} peer${count === 1 ? '' : 's'} (direct browser↔browser, no server)`, 'ok');
} });
await peer.connect(signalUrl, { room });
if (peer.peerCount === 0) { $('mesh').textContent = '0'; setStatus('● in the mesh room — open this page in another tab to connect', 'warn'); log('in the mesh room — waiting for another tab to join…'); }
} catch (e) {
setStatus('mesh error: ' + e.message, 'err');
log('mesh: ' + e.message + ' — is the page open in a second tab? (one tab waits for the other)', 'err');
$('connect').disabled = false;
}
};
$('seed').textContent = `🌱 Seed ${fmt(seedCount)} blocks from the network → peer`;
$('seed').onclick = () => seedBlocks(seedCount);
log(`idle — press ▶ Connect, then open this page in a second tab. Room ${room.slice(0,10)}… via ${signalUrl}.`, 'dim');
</script>
</body>
</html>