|
23 | 23 | .foot { margin-top:48px; border-top:1px solid var(--bd); padding-top:18px; color:var(--mut); font-size:14px; } |
24 | 24 | hr { border:0; border-top:1px solid var(--bd); margin:34px 0; } |
25 | 25 | em { color:var(--fg); } |
| 26 | + figure { margin:30px 0; } |
| 27 | + figure svg { width:100%; height:auto; display:block; background:#fff; } |
| 28 | + figcaption { color:var(--mut); font-size:13.5px; font-family:var(--mono); text-align:center; margin-top:10px; line-height:1.5; } |
| 29 | + .svgtxt { font-family:var(--mono); } |
26 | 30 | </style> |
27 | 31 | </head> |
28 | 32 | <body> |
@@ -87,6 +91,46 @@ <h2>Building the node, one "but how do you…" at a time</h2> |
87 | 91 | because the tab does the checking. With it, the tab reached the live tip — and caught blocks that were mined |
88 | 92 | <em>while it was running</em>.</p> |
89 | 93 |
|
| 94 | +<figure> |
| 95 | +<svg viewBox="0 0 680 290" class="svgtxt" role="img" aria-label="The node lives entirely in the browser tab. A thin WS-to-TCP bridge relays Bitcoin p2p to public peers; WebTorrent delivers the snapshot directly."> |
| 96 | + <defs> |
| 97 | + <marker id="ar" markerWidth="9" markerHeight="9" refX="7.5" refY="4.5" orient="auto"><path d="M0,0 L8,4.5 L0,9 z" fill="#5b6470"/></marker> |
| 98 | + <marker id="arb" markerWidth="9" markerHeight="9" refX="7.5" refY="4.5" orient="auto"><path d="M0,0 L8,4.5 L0,9 z" fill="#0969da"/></marker> |
| 99 | + </defs> |
| 100 | + <!-- tab --> |
| 101 | + <rect x="18" y="46" width="250" height="208" rx="12" fill="#fafbfc" stroke="#e6e8eb"/> |
| 102 | + <line x1="18" y1="76" x2="268" y2="76" stroke="#e6e8eb"/> |
| 103 | + <circle cx="36" cy="61" r="4" fill="#e6e8eb"/><circle cx="50" cy="61" r="4" fill="#e6e8eb"/><circle cx="64" cy="61" r="4" fill="#e6e8eb"/> |
| 104 | + <text x="156" y="66" text-anchor="middle" font-size="13" font-weight="700" fill="#16181d">Browser tab — the node</text> |
| 105 | + <g font-size="11" fill="#16181d" text-anchor="middle"> |
| 106 | + <rect x="32" y="90" width="100" height="32" rx="6" fill="#fff" stroke="#e6e8eb"/><text x="82" y="110">consensus engine</text> |
| 107 | + <rect x="146" y="90" width="106" height="32" rx="6" fill="#fff" stroke="#e6e8eb"/><text x="199" y="110">UTXO · 32-byte Σ</text> |
| 108 | + <rect x="32" y="130" width="100" height="32" rx="6" fill="#fff" stroke="#e6e8eb"/><text x="82" y="150">WASM secp256k1</text> |
| 109 | + <rect x="146" y="130" width="106" height="32" rx="6" fill="#fff" stroke="#e6e8eb"/><text x="199" y="150">OPFS storage</text> |
| 110 | + </g> |
| 111 | + <text x="143" y="188" text-anchor="middle" font-size="11.5" fill="#1a7f37">every rule checked in the tab ✓</text> |
| 112 | + <text x="143" y="208" text-anchor="middle" font-size="10.5" fill="#5b6470">no server trusted</text> |
| 113 | + <!-- upper path: bridge -> peers --> |
| 114 | + <line x1="268" y1="110" x2="320" y2="110" stroke="#5b6470" marker-end="url(#ar)"/> |
| 115 | + <text x="385" y="73" text-anchor="middle" font-size="10" fill="#e8830c">the only non-browser piece ↓</text> |
| 116 | + <rect x="326" y="88" width="118" height="44" rx="8" fill="#fff" stroke="#e8830c" stroke-width="1.5"/> |
| 117 | + <text x="385" y="106" text-anchor="middle" font-size="11.5" font-weight="700" fill="#16181d">WS↔TCP bridge</text> |
| 118 | + <text x="385" y="121" text-anchor="middle" font-size="10" fill="#5b6470">≈40-line relay</text> |
| 119 | + <line x1="444" y1="110" x2="496" y2="110" stroke="#5b6470" marker-end="url(#ar)"/> |
| 120 | + <rect x="502" y="88" width="160" height="44" rx="8" fill="#fafbfc" stroke="#e6e8eb"/> |
| 121 | + <text x="582" y="106" text-anchor="middle" font-size="11.5" font-weight="700" fill="#16181d">public testnet4 peers</text> |
| 122 | + <text x="582" y="121" text-anchor="middle" font-size="10" fill="#5b6470">Bitcoin p2p</text> |
| 123 | + <text x="385" y="150" text-anchor="middle" font-size="10" fill="#5b6470">headers · blocks</text> |
| 124 | + <!-- lower path: webtorrent, direct --> |
| 125 | + <path d="M268,210 C 330,210 340,214 392,214" fill="none" stroke="#0969da" stroke-dasharray="4 3" marker-end="url(#arb)"/> |
| 126 | + <text x="330" y="203" font-size="9.5" fill="#0969da">WebRTC, direct</text> |
| 127 | + <rect x="398" y="192" width="264" height="46" rx="8" fill="#fff" stroke="#0969da" stroke-opacity=".5"/> |
| 128 | + <text x="530" y="211" text-anchor="middle" font-size="11.5" font-weight="700" fill="#16181d">WebTorrent swarm</text> |
| 129 | + <text x="530" y="227" text-anchor="middle" font-size="9.5" fill="#5b6470">snapshot + hints — browser-native, no relay</text> |
| 130 | +</svg> |
| 131 | +<figcaption>Everything that validates lives in the tab. The bridge is the one piece a browser can't be — a dumb<br>byte-relay that can withhold but never forge. The snapshot comes peer-to-peer, straight into the page.</figcaption> |
| 132 | +</figure> |
| 133 | + |
90 | 134 | <p><b>But the snapshot is in Bitcoin Core's format.</b> So you write a parser for it — Core's <code>dumptxoutset</code> |
91 | 135 | v2 layout, with its compressed amounts and script encodings. The proof that the parser is correct is delicious: |
92 | 136 | feed a real block's real signatures against coins decoded from the file, and they verify. A segwit signature |
@@ -139,10 +183,45 @@ <h2>The punchline: 32 bytes</h2> |
139 | 183 | end is a fingerprint of precisely the coins still unspent: the UTXO set, distilled to a constant |
140 | 184 | <b>32 bytes</b>.</p> |
141 | 185 |
|
| 186 | +<figure> |
| 187 | +<svg viewBox="0 0 680 132" class="svgtxt" role="img" aria-label="Add a hash for every created output, subtract one for every spent input. Created-then-spent coins cancel; the unspent survivors are the whole UTXO set, held as 32 bytes."> |
| 188 | + <defs><marker id="ar2" markerWidth="9" markerHeight="9" refX="7.5" refY="4.5" orient="auto"><path d="M0,0 L8,4.5 L0,9 z" fill="#5b6470"/></marker></defs> |
| 189 | + <text x="196" y="20" text-anchor="middle" font-size="10.5" fill="#5b6470">created, then later spent → cancels</text> |
| 190 | + <path d="M76,52 C 120,28 232,28 272,52" fill="none" stroke="#5b6470" stroke-opacity=".35" stroke-dasharray="3 3"/> |
| 191 | + <path d="M136,52 C 188,36 296,36 332,52" fill="none" stroke="#5b6470" stroke-opacity=".3" stroke-dasharray="3 3"/> |
| 192 | + <g font-size="12" font-weight="700" text-anchor="middle"> |
| 193 | + <g opacity=".45"><rect x="54" y="52" width="44" height="30" rx="6" fill="#fff" stroke="#1a7f37"/><text x="76" y="72" fill="#1a7f37">+A</text></g> |
| 194 | + <g opacity=".45"><rect x="114" y="52" width="44" height="30" rx="6" fill="#fff" stroke="#1a7f37"/><text x="136" y="72" fill="#1a7f37">+B</text></g> |
| 195 | + <rect x="174" y="50" width="44" height="34" rx="6" fill="#eaf6ee" stroke="#1a7f37" stroke-width="1.6"/><text x="196" y="72" fill="#1a7f37">+C</text> |
| 196 | + <g opacity=".45"><rect x="250" y="52" width="44" height="30" rx="6" fill="#fff" stroke="#e8830c"/><text x="272" y="72" fill="#e8830c">−A</text></g> |
| 197 | + <g opacity=".45"><rect x="310" y="52" width="44" height="30" rx="6" fill="#fff" stroke="#e8830c"/><text x="332" y="72" fill="#e8830c">−B</text></g> |
| 198 | + </g> |
| 199 | + <text x="196" y="103" text-anchor="middle" font-size="9.5" fill="#1a7f37">never spent → survives</text> |
| 200 | + <line x1="362" y1="67" x2="416" y2="67" stroke="#5b6470" marker-end="url(#ar2)"/> |
| 201 | + <rect x="422" y="44" width="210" height="46" rx="9" fill="#fafbfc" stroke="#e6e8eb"/> |
| 202 | + <text x="527" y="64" text-anchor="middle" font-size="12" font-weight="700" fill="#16181d">= the unspent set · 32 bytes</text> |
| 203 | + <text x="527" y="80" text-anchor="middle" font-size="10" fill="#5b6470">two 128-bit lanes, always</text> |
| 204 | +</svg> |
| 205 | +<figcaption>Add a hash for every output created, subtract one for every input spent. Created-then-spent pairs<br>vanish; the survivors are the UTXO set — and the running total is always just 32 bytes.</figcaption> |
| 206 | +</figure> |
| 207 | + |
142 | 208 | <div class="big">We streamed the full testnet4 UTXO set — all <b>14,129,063 coins</b> — through this accumulator. |
143 | 209 | Memory stayed flat at the size of the input, never the set. The set-state never left <b>32 bytes</b>. |
144 | 210 | What was 25 GB is now a number you could write on a napkin.</div> |
145 | 211 |
|
| 212 | +<figure> |
| 213 | +<svg viewBox="0 0 680 100" class="svgtxt" role="img" aria-label="25 gigabytes held in RAM versus a 32-byte number — the same set, the same guarantee."> |
| 214 | + <defs><marker id="ar3" markerWidth="9" markerHeight="9" refX="7.5" refY="4.5" orient="auto"><path d="M0,0 L8,4.5 L0,9 z" fill="#5b6470"/></marker></defs> |
| 215 | + <rect x="18" y="26" width="470" height="50" rx="8" fill="#fdecec" stroke="#cf222e" stroke-opacity=".55"/> |
| 216 | + <text x="253" y="48" text-anchor="middle" font-size="13" font-weight="700" fill="#16181d">25 GB held in RAM</text> |
| 217 | + <text x="253" y="65" text-anchor="middle" font-size="10.5" fill="#5b6470">14,129,063 coins · crashed a 24 GB heap</text> |
| 218 | + <line x1="494" y1="51" x2="526" y2="51" stroke="#5b6470" marker-end="url(#ar3)"/> |
| 219 | + <rect x="532" y="35" width="130" height="32" rx="8" fill="#eaf6ee" stroke="#1a7f37"/> |
| 220 | + <text x="597" y="55" text-anchor="middle" font-size="13" font-weight="700" fill="#1a7f37">32 bytes</text> |
| 221 | +</svg> |
| 222 | +<figcaption>The same set, the same guarantee against double-spends and forgeries. Only one of them fits in a tab.</figcaption> |
| 223 | +</figure> |
| 224 | + |
146 | 225 | <p>And it's not a trust shortcut. If a single block tried to spend a coin that never existed, or spend one twice, |
147 | 226 | the sums wouldn't cancel — the final number wouldn't match the known commitment, and validation fails. The hints |
148 | 227 | that make it fast (a tiny file marking which outputs survive — about <b>25 bytes per block</b>, a few megabytes |
|
0 commit comments