From 098c1dc66e47db6fb9aa8c241ed73e18f1a11849 Mon Sep 17 00:00:00 2001 From: Ruediger Klaehn Date: Wed, 24 Jun 2026 10:08:23 +0300 Subject: [PATCH 1/4] Add python svg generator scripts --- .gitignore | 3 + scripts/README.md | 42 +++ scripts/connect-by-key.gen.py | 227 +++++++++++++++ scripts/endpoint-startup.gen.py | 280 ++++++++++++++++++ scripts/endpoint-startup.land.txt | 1 + scripts/hole-punching-lan.gen.py | 366 +++++++++++++++++++++++ scripts/hole-punching.gen.py | 469 ++++++++++++++++++++++++++++++ scripts/publish-relay-dht.gen.py | 308 ++++++++++++++++++++ scripts/publish-relay.gen.py | 284 ++++++++++++++++++ scripts/routing-moves.gen.py | 316 ++++++++++++++++++++ 10 files changed, 2296 insertions(+) create mode 100644 .gitignore create mode 100644 scripts/README.md create mode 100644 scripts/connect-by-key.gen.py create mode 100644 scripts/endpoint-startup.gen.py create mode 100644 scripts/endpoint-startup.land.txt create mode 100644 scripts/hole-punching-lan.gen.py create mode 100644 scripts/hole-punching.gen.py create mode 100644 scripts/publish-relay-dht.gen.py create mode 100644 scripts/publish-relay.gen.py create mode 100644 scripts/routing-moves.gen.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5947688 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +# Python +__pycache__/ +*.py[cod] diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..619ddcc --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,42 @@ +# Diagram generators + +Python generators for the animated SVGs on +[`/what-is-iroh`](../what-is-iroh.mdx). Each `*.gen.py` writes one SVG into +`../images/how-iroh-works/`. + +Regenerate one: + +```sh +python3 connect-by-key.gen.py +``` + +Regenerate all: + +```sh +for f in *.gen.py; do python3 "$f"; done +``` + +Scripts are self-contained (stdlib only). `endpoint-startup.gen.py` additionally +reads `endpoint-startup.land.txt` (world-map path data). + +## SMIL only — no CSS animations + +The SVGs are embedded via ``. In production, Mintlify serves image +assets with the HTTP header `Content-Security-Policy: default-src 'none'`. With +no `style-src 'unsafe-inline'`, the browser **blocks the SVG's inline ` + + + + + + + + + + 10.0.0.42 + +{chr(10).join(ip_rows)} + + +{alice_iphone()} + + +{bob_android()} + + + Alice + Bob + + + + + connect + + + + + + + + + + +''' + +open(OUT_PATH, "w").write(svg) +print(f"wrote {len(svg)} bytes -> {OUT_PATH}") diff --git a/scripts/endpoint-startup.gen.py b/scripts/endpoint-startup.gen.py new file mode 100644 index 0000000..61df1f5 --- /dev/null +++ b/scripts/endpoint-startup.gen.py @@ -0,0 +1,280 @@ +#!/usr/bin/env python3 +"""Generator for endpoint-startup.svg (the "Endpoint startup" figure in how-iroh-works). + +This SVG is generated, not hand-edited. To change it, edit this script and run: + + python3 endpoint-startup.gen.py + +It reads the projected land outline from endpoint-startup.land.txt (Natural Earth +110m coastline, plate carrée with standard parallel 45°, clipped to a US + Europe +window) and writes ../images/how-iroh-works/endpoint-startup.svg. + +NOTE: this lives in scripts/, not in src/app/, because anything under the Next.js +app router directory gets bundled by webpack (which chokes on .py files). + +The animation: the three relay wires fade in at 2s, then an equal-speed probe +packet round-trips Bob -> relay -> Bob on each wire. Because speed is constant and +distance differs, us-east returns first, then us-west, then eu-west. A readout to +the right of the phone gains a line each time a packet returns. +""" + +import math +import os + +CYCLE = 14.0 # one loop; everything resets afterwards + +HERE = os.path.dirname(os.path.abspath(__file__)) +LAND_PATH = os.path.join(HERE, "endpoint-startup.land.txt") +OUT_PATH = os.path.normpath(os.path.join( + HERE, "..", + "images", "how-iroh-works", "endpoint-startup.svg")) + +def key_label(cx, y, text, size, color): + """A tiny drawn gold key (bow + shaft + teeth) + monospace label, centered on cx + (replaces the 🔑 emoji, which no pure-vector renderer can draw).""" + s = size + gold = "#eab308" + w = s * 1.3 # key glyph width + gap = s * 0.3 + xl = cx - (w + gap + s * 0.6 * len(text)) / 2 + cy = y - s * 0.30 + rb, rh = s * 0.34, s * 0.15 # bow outer / hole radius + bx = xl + rb # bow center x + hs = s * 0.18 # shaft thickness + xr = xl + w # right end + top, bot = cy - hs / 2, cy + hs / 2 + tw = s * 0.13 # tooth width + mono = "'Space Mono', monospace" + return ( + f'' + f'' + f'' + f'' + f'{text}' + ) + + +land = open(LAND_PATH).read() + +# ---- projection: plate carrée, standard parallel 45°, US + Europe window ---- +lon0, lon1 = -130, 42 +lat0, lat1 = 16, 72 +cosp = math.cos(math.radians(45)) +scale = 820.0 / ((lon1 - lon0) * cosp) +def proj(lon, lat): + return ((lon - lon0) * cosp * scale, (lat1 - lat) * scale) + +# city coordinates (lon, lat) +seattle = (-122.33, 47.61) +delaware = (-75.55, 39.16) # Dover/Wilmington area +frankfurt = (8.68, 50.11) +florida = (-80.2, 25.8) # Miami area + + +def relay(cx, cy, label, ip): + # pizzabox 1U server icon centered horizontally on cx, sitting above the location dot + bx, by, bw, bh = cx-23, cy-30, 46, 15 + s = [] + s.append(f' ') + s.append(f' ') + s.append(f' {label}') + # connector from box bottom to dot + s.append(f' ') + # location dot + s.append(f' ') + # pizzabox body + s.append(f' ') + # front bezel split line + s.append(f' ') + # LEDs + s.append(f' ') + s.append(f' ') + # vent slits on the right + for i in range(5): + vx = bx+22+i*4 + s.append(f' ') + # public IPv4 below the location dot + s.append(f' {ip}') + s.append(f' ') + return "\n".join(s) + + +def arc(x1, y1, x2, y2, k=0.16): + # great-circle-style curved wire: quadratic bezier bowing "up" (toward the top). + # Returns (path_d, arc_length). Larger k => more northern bow. + mx, my = (x1+x2)/2, (y1+y2)/2 + dx, dy = x2-x1, y2-y1 + L = math.hypot(dx, dy) + px, py = -dy/L, dx/L + if py > 0: # always bow upward + px, py = -px, -py + off = k*L + cx, cy = mx+px*off, my+py*off + d = f"M {x1:.0f} {y1:.0f} Q {cx:.0f} {cy:.0f} {x2:.0f} {y2:.0f}" + def q(t, a, b, c): return (1-t)**2*a + 2*(1-t)*t*b + t**2*c + N, length, prev = 60, 0.0, (x1, y1) + for i in range(1, N+1): + t = i/N + cur = (q(t, x1, cx, x2), q(t, y1, cy, y2)) + length += math.hypot(cur[0]-prev[0], cur[1]-prev[1]) + prev = cur + return d, length + + +# ---- Bob: small Android phone in the Atlantic, leader line to his FL location ---- +bx0, by0 = 300, 244 # phone top-left +bw, bh = 48, 88 +bcx, bcy = bx0+bw/2, by0+bh/2 +flx, fly = proj(*florida) +bob = f''' + + + + + + + + + + + + iroh + {key_label(bcx, by0+58, "8e2b…", 8, "#d97706")} + ''' + +uwx, uwy = round(proj(*seattle)[0]), round(proj(*seattle)[1]) +uex, uey = round(proj(*delaware)[0]), round(proj(*delaware)[1]) +ewx, ewy = round(proj(*frankfurt)[0]), round(proj(*frankfurt)[1]) +bx_, by_ = round(flx), round(fly) # Bob's location dot + +# made-up public IPv4 addresses for the relays +ip_uw, ip_ue, ip_ew = "52.10.18.7", "44.208.61.5", "18.196.142.9" +# Bob's public IP — reflected back by the first relay to respond +ip_bob = "73.118.42.9" + +# wire paths (us-east kept nearly straight); keep lengths for equal-speed packets +d_uw, L_uw = arc(bx_, by_, uwx, uwy, k=0.16) +d_ue, L_ue = arc(bx_, by_, uex, uey, k=0.05) +d_ew, L_ew = arc(bx_, by_, ewx, ewy, k=0.10) + +wires = f''' + + + + + ''' + +# Equal-speed probe packets: round-trip Bob -> relay -> Bob. +# Duration is proportional to path length so speed is constant => us-east returns first. +SPEED = 120.0 # px per second (slow enough that the short us-east hop is still watchable) +START = 3.0 # packets launch after the wires have faded in +def packet(wid, length): + dur = 2*length/SPEED # there and back + t0 = START / CYCLE + t1 = (START + dur*0.04) / CYCLE + tmid = (START + dur*0.5) / CYCLE + t2 = (START + dur*0.96) / CYCLE + t3 = (START + dur) / CYCLE + opa = f'values="0;0;1;1;1;0;0" keyTimes="0;{t0:.4f};{t1:.4f};{tmid:.4f};{t2:.4f};{t3:.4f};1"' + mot = f'keyTimes="0;{t0:.4f};{tmid:.4f};{t3:.4f};1" keyPoints="0;0;1;0;0"' + return f''' + + + + + ''' + +packets = f''' + +{packet("w-uw", L_uw)} +{packet("w-ue", L_ue)} +{packet("w-ew", L_ew)} + ''' + +# Latency readout, right of the phone: one line appears each time a packet returns. +def ret_time(length): + return START + 2*length/SPEED # packet is back at Bob at START + round-trip dur +def readline(y, label, ms, begin): + t0 = begin / CYCLE + t1 = (begin + 0.4) / CYCLE + return (f' ' + f'{label}: {ms} ms' + f'') +readout = f''' + + +{readline(268, "us-east", 19, ret_time(L_ue))} +{readline(294, "us-west", 71, ret_time(L_uw))} +{readline(320, "eu-west", 102, ret_time(L_ew))} + + + + ''' + +# When the last response is in, us-east wins: overlay its wire in connection-blue, +# then keep it gently pulsing to show the persistent home-relay connection. +_rt = ret_time(L_ew) +home = f''' + + + ''' + +# The endpoint's public IP appears just below the phone on the first response. +_pt0 = ret_time(L_ue) / CYCLE +_pt1 = (ret_time(L_ue) + 0.4) / CYCLE +phone_ip = f''' + {ip_bob}''' + +# Cropped viewport: trim the empty Arctic (top) and eastern Europe (right) so the +# relevant US <-> western-Europe band fills the frame. The land path is unchanged; +# this just clips it. The MDX embeds this as ; +# the intrinsic aspect ratio comes from the SVG viewBox (VB_W x VB_H). +VB_X, VB_Y, VB_W, VB_H = 0, 92, 705, 280 +svg = f''' + + + + + + + + +{wires} + +{packets} + +{relay(uwx, uwy, "us-west", ip_uw)} + +{relay(uex, uey, "us-east", ip_ue)} + +{relay(ewx, ewy, "eu-west", ip_ew)} + +{bob} + +{phone_ip} + +{home} + +{readout} + + + + + + +''' + +open(OUT_PATH, "w").write(svg) +print(f"wrote {len(svg)} bytes -> {OUT_PATH}") diff --git a/scripts/endpoint-startup.land.txt b/scripts/endpoint-startup.land.txt new file mode 100644 index 0000000..a746ad2 --- /dev/null +++ b/scripts/endpoint-startup.land.txt @@ -0,0 +1 @@ +M 329,417 L 324,417 L 326,416 L 326,413 L 329,412 L 329,417 Z M 307,363 L 306,364 L 299,364 L 299,362 L 300,361 L 304,361 L 307,363 Z M 253,365 L 252,366 L 249,365 L 246,363 L 247,361 L 253,361 L 256,365 L 253,365 Z M 274,351 L 278,353 L 278,351 L 282,351 L 286,353 L 287,355 L 290,355 L 290,357 L 292,357 L 294,360 L 292,363 L 290,361 L 286,361 L 284,363 L 283,361 L 281,362 L 279,367 L 278,364 L 275,363 L 270,363 L 267,364 L 265,362 L 265,360 L 273,361 L 275,360 L 273,357 L 273,354 L 270,353 L 271,351 L 274,351 Z M 240,332 L 242,334 L 246,334 L 255,342 L 259,344 L 259,346 L 263,346 L 266,349 L 266,350 L 249,352 L 252,348 L 250,346 L 247,346 L 244,340 L 242,340 L 236,337 L 230,336 L 228,334 L 230,333 L 225,332 L 222,336 L 220,336 L 219,338 L 215,338 L 220,332 L 228,329 L 235,330 L 240,332 Z M 250,325 L 249,326 L 246,320 L 247,315 L 248,316 L 250,325 Z M 249,306 L 244,307 L 243,305 L 249,304 L 249,306 Z M 253,306 L 252,311 L 249,303 L 253,306 Z M 785,245 L 781,248 L 782,250 L 777,252 L 775,251 L 774,249 L 785,245 Z M 733,245 L 735,247 L 743,247 L 743,248 L 745,247 L 745,249 L 738,250 L 738,249 L 732,248 L 733,245 Z M 694,228 L 692,239 L 679,232 L 680,228 L 685,229 L 694,228 Z M 664,208 L 667,212 L 666,221 L 664,221 L 662,223 L 660,221 L 659,209 L 661,210 L 664,208 Z M 665,201 L 664,206 L 662,205 L 661,201 L 661,198 L 665,195 L 665,201 Z M 316,172 L 324,172 L 320,176 L 314,173 L 313,170 L 315,168 L 316,172 Z M 325,154 L 317,152 L 312,149 L 320,150 L 325,153 L 325,154 Z M 31,158 L 29,159 L 21,156 L 14,150 L 9,148 L 7,145 L 8,143 L 20,146 L 24,152 L 29,155 L 31,158 Z M 352,144 L 349,150 L 352,147 L 355,149 L 354,151 L 358,153 L 360,151 L 365,153 L 363,158 L 367,157 L 369,165 L 367,171 L 361,170 L 363,164 L 361,163 L 356,169 L 353,169 L 356,166 L 352,164 L 337,164 L 336,162 L 339,160 L 337,158 L 341,154 L 346,143 L 349,140 L 353,137 L 356,138 L 352,144 Z M -13,121 L -8,121 L -10,128 L -6,134 L -8,134 L -15,125 L -15,120 L -13,121 Z M 587,133 L 579,137 L 572,136 L 576,129 L 574,122 L 584,114 L 588,113 L 593,118 L 590,122 L 591,127 L 587,133 Z M 680,111 L 677,116 L 672,112 L 672,109 L 679,107 L 680,111 Z M 605,90 L 600,97 L 610,97 L 609,102 L 605,108 L 610,108 L 614,117 L 618,118 L 622,129 L 628,130 L 627,134 L 625,136 L 627,140 L 622,143 L 616,143 L 608,145 L 606,144 L 603,147 L 598,146 L 595,149 L 592,147 L 599,140 L 603,139 L 596,138 L 595,135 L 600,133 L 597,129 L 598,125 L 605,125 L 606,121 L 602,117 L 597,116 L 596,114 L 597,111 L 596,109 L 593,113 L 593,106 L 590,103 L 592,96 L 596,90 L 605,90 Z M 242,66 L 240,70 L 238,69 L 237,67 L 239,65 L 242,66 Z M 229,63 L 224,66 L 220,66 L 219,64 L 223,61 L 229,61 L 229,63 Z M 214,43 L 215,46 L 217,45 L 231,51 L 231,54 L 234,54 L 238,56 L 234,58 L 226,56 L 224,53 L 212,60 L 210,56 L 204,57 L 208,54 L 210,42 L 214,43 Z M 551,37 L 549,42 L 555,46 L 549,51 L 531,57 L 511,54 L 516,51 L 506,48 L 514,47 L 514,45 L 504,43 L 507,39 L 514,38 L 522,42 L 529,39 L 535,40 L 543,37 L 551,37 Z M 258,33 L 253,33 L 252,30 L 254,26 L 258,25 L 262,27 L 261,31 L 258,33 Z M 164,20 L 161,22 L 154,20 L 150,21 L 144,18 L 152,13 L 159,16 L 164,20 Z M 188,17 L 188,24 L 194,18 L 200,23 L 199,28 L 203,32 L 208,27 L 212,22 L 212,14 L 226,16 L 232,19 L 233,22 L 229,26 L 232,30 L 232,33 L 222,38 L 216,39 L 211,37 L 203,49 L 198,53 L 191,54 L 187,57 L 187,61 L 181,62 L 176,67 L 170,75 L 169,80 L 168,88 L 175,89 L 180,101 L 186,99 L 195,102 L 203,108 L 214,113 L 228,114 L 227,119 L 228,126 L 232,134 L 239,140 L 242,138 L 245,131 L 243,120 L 239,117 L 247,114 L 252,109 L 255,104 L 254,100 L 251,94 L 245,89 L 251,82 L 247,65 L 251,64 L 264,66 L 268,64 L 278,71 L 279,73 L 288,74 L 289,88 L 294,89 L 297,93 L 304,89 L 312,79 L 327,101 L 325,106 L 336,113 L 343,115 L 346,117 L 348,123 L 352,124 L 354,126 L 354,134 L 347,139 L 340,141 L 334,147 L 326,148 L 315,146 L 303,147 L 299,152 L 293,155 L 281,170 L 285,169 L 292,160 L 302,154 L 310,153 L 314,157 L 309,161 L 312,174 L 319,177 L 326,176 L 331,169 L 332,173 L 335,176 L 329,180 L 313,187 L 308,192 L 305,191 L 304,186 L 313,180 L 300,181 L 301,183 L 285,191 L 282,196 L 282,200 L 284,204 L 286,204 L 285,201 L 287,203 L 286,205 L 272,208 L 268,209 L 275,208 L 277,209 L 270,212 L 267,212 L 267,211 L 266,213 L 267,213 L 266,218 L 263,223 L 260,219 L 262,227 L 258,235 L 259,230 L 256,227 L 256,221 L 255,224 L 256,229 L 253,228 L 256,230 L 256,236 L 258,237 L 259,246 L 256,251 L 251,253 L 248,257 L 245,257 L 243,260 L 242,262 L 237,266 L 232,273 L 232,283 L 238,304 L 237,315 L 235,316 L 233,316 L 232,313 L 230,311 L 225,297 L 226,293 L 224,289 L 219,283 L 214,286 L 208,280 L 195,281 L 193,282 L 194,288 L 190,289 L 187,289 L 183,285 L 179,286 L 175,285 L 172,285 L 168,287 L 164,292 L 159,295 L 156,301 L 157,311 L 154,322 L 153,334 L 156,346 L 161,355 L 163,358 L 168,360 L 170,363 L 181,359 L 187,355 L 189,344 L 198,341 L 205,340 L 206,342 L 206,345 L 202,353 L 203,354 L 201,362 L 199,361 L 200,362 L 199,374 L 196,378 L 200,380 L 201,378 L 205,379 L 210,378 L 215,378 L 222,382 L 223,384 L 223,389 L 222,394 L 222,402 L 220,411 L 228,425 L 230,425 L 232,426 L 239,423 L 240,421 L 246,422 L 251,427 L 253,427 L 257,422 L 259,422 L 260,414 L 263,411 L 266,411 L 266,409 L 270,410 L 278,402 L 281,404 L 280,408 L 277,408 L 278,411 L 278,415 L 276,419 L 278,424 L 280,424 L 281,419 L 280,417 L 279,411 L 285,409 L 285,406 L 286,403 L 288,408 L 291,408 L 294,412 L 295,414 L 304,414 L 307,417 L 310,417 L 313,415 L 313,414 L 325,413 L 321,415 L 322,418 L 326,419 L 330,422 L 331,428 L 333,427 L 338,432 L 341,436 L 341,439 L 343,440 L 347,445 L 353,447 L 354,445 L 357,445 L 363,447 L 368,449 L 373,455 L 373,457 L 375,457 L 379,473 L 382,474 L 382,478 L 378,484 L 380,486 L 388,487 L 388,494 L 392,489 L 406,496 L 408,500 L 407,504 L 413,502 L 422,505 L 429,505 L 436,510 L 442,518 L 446,520 L 450,520 L 452,522 L 454,535 L 452,546 L 443,560 L 437,573 L 435,573 L 434,578 L 434,591 L 433,606 L 431,609 L 430,618 L 425,626 L 425,633 L 421,636 L 420,640 L 414,640 L 407,643 L 404,646 L 398,648 L 393,653 L 389,660 L 389,669 L 387,679 L 383,682 L 378,694 L 371,703 L 368,709 L 363,717 L 358,721 L 354,720 L 352,720 L 347,718 L 344,718 L 341,714 L 341,718 L 347,723 L 346,728 L 349,731 L 349,734 L 344,743 L 337,746 L 328,748 L 323,747 L 323,760 L 321,762 L 316,763 L 311,761 L 309,762 L 310,769 L 313,771 L 316,769 L 317,772 L 313,774 L 309,779 L 307,789 L 303,789 L 299,793 L 298,798 L 302,803 L 307,804 L 305,810 L 300,814 L 296,822 L 292,824 L 290,827 L 292,834 L 295,838 L 289,838 L 282,842 L 281,848 L 274,846 L 262,838 L 261,834 L 262,830 L 260,825 L 259,814 L 261,807 L 266,802 L 259,800 L 264,794 L 265,783 L 271,785 L 273,771 L 270,769 L 268,778 L 265,777 L 269,755 L 271,750 L 269,736 L 271,736 L 279,704 L 278,694 L 280,688 L 279,680 L 282,672 L 286,630 L 285,619 L 284,609 L 279,605 L 279,603 L 270,596 L 257,584 L 255,579 L 256,577 L 240,534 L 232,527 L 234,524 L 232,517 L 233,513 L 239,503 L 238,500 L 237,504 L 234,501 L 235,499 L 234,493 L 236,492 L 238,483 L 238,480 L 244,476 L 243,474 L 245,474 L 246,468 L 248,467 L 252,459 L 250,458 L 251,454 L 250,440 L 247,435 L 246,431 L 247,429 L 243,425 L 240,425 L 236,431 L 238,435 L 234,437 L 233,433 L 233,434 L 231,433 L 230,431 L 225,430 L 225,431 L 222,428 L 221,424 L 216,421 L 215,417 L 214,421 L 211,418 L 211,413 L 210,412 L 211,411 L 202,398 L 202,397 L 203,398 L 203,396 L 201,395 L 201,397 L 198,397 L 188,392 L 185,392 L 180,387 L 175,380 L 168,376 L 159,380 L 139,370 L 134,365 L 126,362 L 124,359 L 119,355 L 117,351 L 116,348 L 117,347 L 118,341 L 114,332 L 103,316 L 99,313 L 98,311 L 99,307 L 93,302 L 92,298 L 90,297 L 85,290 L 80,275 L 73,271 L 72,274 L 73,282 L 88,306 L 92,322 L 95,322 L 98,328 L 96,332 L 91,324 L 85,319 L 84,310 L 79,305 L 78,306 L 74,302 L 71,299 L 74,298 L 75,296 L 76,293 L 69,286 L 61,263 L 57,259 L 55,258 L 55,256 L 45,252 L 44,248 L 40,242 L 36,231 L 30,223 L 29,217 L 27,214 L 28,202 L 26,197 L 28,191 L 29,179 L 28,169 L 25,161 L 26,159 L 33,162 L 35,168 L 37,166 L 34,155 L 21,146 L 12,143 L 10,137 L 10,133 L 4,130 L 3,124 L -2,119 L -3,116 L -9,111 L -11,105 L -17,100 L -19,94 L -32,93 L -37,91 L -47,84 L -60,80 L -67,81 L -82,75 L -87,76 L -86,81 L -104,87 L -104,83 L -102,76 L -97,74 L -98,72 L -104,76 L -108,80 L -115,85 L -111,89 L -116,93 L -125,98 L -127,101 L -134,105 L -136,108 L -141,111 L -144,110 L -158,117 L -166,119 L -167,118 L -152,109 L -146,108 L -132,97 L -131,92 L -129,88 L -134,90 L -136,89 L -139,92 L -142,88 L -143,91 L -145,87 L -149,90 L -152,90 L -152,83 L -155,81 L -161,82 L -169,77 L -169,74 L -172,71 L -165,60 L -161,59 L -158,60 L -154,57 L -150,58 L -147,56 L -148,52 L -150,51 L -147,49 L -155,50 L -156,52 L -160,50 L -167,51 L -174,49 L -176,47 L -182,43 L -164,37 L -160,37 L -161,40 L -151,40 L -155,35 L -161,33 L -169,27 L -175,25 L -173,21 L -164,21 L -158,18 L -157,14 L -152,11 L -138,7 L -134,8 L -127,4 L -120,6 L -116,9 L -114,7 L -106,8 L -106,9 L -99,11 L -94,10 L -71,14 L -65,12 L -31,21 L -21,16 L -14,17 L 1,12 L 4,15 L 8,13 L 9,10 L 12,11 L 20,17 L 27,12 L 27,18 L 33,16 L 35,14 L 41,15 L 59,20 L 70,21 L 77,24 L 70,28 L 79,29 L 96,27 L 101,31 L 106,28 L 101,25 L 104,23 L 114,22 L 122,27 L 128,26 L 136,29 L 150,28 L 150,24 L 154,23 L 162,25 L 161,32 L 165,26 L 168,27 L 171,20 L 160,13 L 160,5 L 166,1 L 172,2 L 177,5 L 183,12 L 179,16 L 188,17 Z M 75,-8 L 73,-4 L 84,-6 L 90,-3 L 96,-6 L 100,-4 L 104,2 L 106,-0 L 103,-7 L 107,-8 L 112,-7 L 117,-5 L 122,7 L 138,13 L 138,16 L 130,17 L 133,19 L 131,22 L 115,19 L 100,22 L 80,23 L 77,20 L 70,18 L 66,19 L 60,14 L 84,11 L 75,9 L 58,10 L 55,7 L 66,5 L 59,5 L 51,3 L 58,-5 L 71,-9 L 75,-8 Z M 122,-10 L 117,-5 L 110,-10 L 118,-11 L 122,-10 Z M 256,-7 L 256,-6 L 241,-5 L 234,-9 L 234,-11 L 248,-11 L 256,-7 Z M 207,-8 L 211,-4 L 215,-9 L 227,-12 L 236,-5 L 235,-0 L 244,-2 L 249,-5 L 266,2 L 267,5 L 275,3 L 280,7 L 292,10 L 296,13 L 300,19 L 292,22 L 303,27 L 311,28 L 317,34 L 325,35 L 323,39 L 315,47 L 309,44 L 302,38 L 296,39 L 295,43 L 308,51 L 311,58 L 310,63 L 292,56 L 304,66 L 304,68 L 291,65 L 281,61 L 275,58 L 277,56 L 263,49 L 263,51 L 249,52 L 245,50 L 248,45 L 267,44 L 266,42 L 273,32 L 272,29 L 270,27 L 263,23 L 253,21 L 256,19 L 251,15 L 247,15 L 243,12 L 241,14 L 232,15 L 197,11 L 193,8 L 198,5 L 191,5 L 190,-2 L 193,-8 L 198,-10 L 211,-12 L 207,-8 Z M 141,-12 L 147,-11 L 156,-12 L 157,-10 L 152,-7 L 160,-4 L 159,2 L 151,5 L 146,4 L 131,-3 L 131,-6 L 141,-5 L 136,-9 L 141,-12 Z M 175,-5 L 170,-0 L 165,-0 L 162,-6 L 162,-10 L 164,-13 L 169,-14 L 188,-13 L 181,-7 L 175,-5 Z M 45,4 L 33,7 L 30,4 L 19,1 L 29,-11 L 24,-15 L 40,-17 L 59,-15 L 69,-10 L 51,-4 L 45,1 L 45,4 Z M 173,-20 L 171,-17 L 158,-20 L 161,-23 L 168,-25 L 173,-20 Z M 150,-32 L 154,-29 L 154,-25 L 152,-20 L 144,-20 L 139,-21 L 139,-25 L 131,-24 L 131,-29 L 136,-29 L 143,-31 L 150,-31 L 150,-32 Z M 104,-28 L 106,-26 L 115,-27 L 116,-23 L 113,-20 L 97,-19 L 85,-16 L 78,-16 L 77,-18 L 87,-21 L 65,-21 L 59,-22 L 65,-28 L 70,-30 L 83,-28 L 91,-24 L 100,-23 L 93,-30 L 97,-32 L 102,-32 L 104,-28 Z M 168,-34 L 174,-32 L 183,-32 L 187,-30 L 186,-27 L 195,-24 L 208,-23 L 216,-25 L 233,-25 L 238,-22 L 239,-20 L 229,-16 L 223,-17 L 200,-16 L 179,-19 L 177,-26 L 172,-29 L 162,-30 L 157,-32 L 159,-35 L 168,-34 Z M 66,-38 L 65,-33 L 61,-31 L 41,-26 L 34,-28 L 52,-37 L 66,-38 Z M 1130,-34 L 1131,-30 L 1135,-32 L 1149,-32 L 1160,-28 L 1164,-26 L 1163,-22 L 1141,-15 L 1154,-12 L 1159,-13 L 1161,-9 L 1163,-11 L 1171,-12 L 1186,-11 L 1187,-8 L 1207,-7 L 1207,-12 L 1225,-11 L 1233,-7 L 1235,-3 L 1232,0 L 1238,5 L 1246,8 L 1250,1 L 1258,4 L 1266,2 L 1275,4 L 1279,3 L 1287,3 L 1283,-3 L 1289,-6 L 1333,-1 L 1337,3 L 1349,8 L 1368,7 L 1378,8 L 1382,10 L 1381,15 L 1387,17 L 1393,16 L 1411,17 L 1420,16 L 1428,22 L 1434,20 L 1430,16 L 1432,13 L 1448,15 L 1458,14 L 1471,18 L 1478,20 L 1478,47 L 1472,50 L 1466,50 L 1470,53 L 1475,61 L 1475,64 L 1474,65 L 1465,64 L 1448,70 L 1434,79 L 1432,82 L 1425,77 L 1413,82 L 1410,80 L 1406,83 L 1399,82 L 1398,86 L 1392,93 L 1392,95 L 1398,97 L 1397,107 L 1393,107 L 1391,113 L 1393,116 L 1384,119 L 1383,127 L 1376,128 L 1374,135 L 1367,142 L 1361,112 L 1363,103 L 1367,99 L 1367,96 L 1375,94 L 1391,79 L 1400,73 L 1404,64 L 1398,64 L 1395,70 L 1383,77 L 1379,69 L 1367,71 L 1355,83 L 1359,87 L 1341,89 L 1341,84 L 1334,83 L 1328,87 L 1313,85 L 1298,87 L 1264,116 L 1271,117 L 1274,122 L 1278,123 L 1282,120 L 1287,120 L 1294,127 L 1294,133 L 1290,140 L 1288,159 L 1280,169 L 1279,173 L 1263,193 L 1256,197 L 1253,197 L 1250,194 L 1244,199 L 1243,201 L 1241,200 L 1239,203 L 1238,205 L 1238,210 L 1228,217 L 1227,221 L 1232,225 L 1237,237 L 1237,245 L 1235,249 L 1227,253 L 1223,254 L 1223,245 L 1221,238 L 1225,237 L 1221,231 L 1219,230 L 1217,231 L 1214,229 L 1217,225 L 1217,219 L 1212,216 L 1206,218 L 1202,221 L 1197,223 L 1199,220 L 1198,217 L 1202,213 L 1200,209 L 1190,216 L 1187,221 L 1183,221 L 1180,224 L 1183,229 L 1187,230 L 1187,233 L 1190,235 L 1196,230 L 1200,233 L 1203,233 L 1204,236 L 1197,238 L 1195,242 L 1190,245 L 1188,250 L 1193,254 L 1195,260 L 1201,272 L 1201,277 L 1198,279 L 1199,282 L 1202,284 L 1200,295 L 1197,296 L 1185,320 L 1172,332 L 1167,333 L 1164,336 L 1162,333 L 1160,337 L 1153,340 L 1148,341 L 1146,348 L 1144,349 L 1142,344 L 1144,341 L 1137,339 L 1129,346 L 1125,352 L 1124,357 L 1132,373 L 1136,377 L 1139,382 L 1141,395 L 1140,407 L 1131,416 L 1121,427 L 1119,423 L 1121,419 L 1117,415 L 1113,414 L 1109,403 L 1105,400 L 1100,400 L 1101,395 L 1097,395 L 1097,402 L 1092,418 L 1093,423 L 1096,423 L 1099,435 L 1101,439 L 1104,440 L 1111,448 L 1113,453 L 1113,467 L 1115,468 L 1117,477 L 1113,477 L 1103,467 L 1097,450 L 1098,445 L 1097,442 L 1089,429 L 1089,433 L 1088,429 L 1091,408 L 1089,404 L 1089,397 L 1087,393 L 1085,377 L 1083,371 L 1074,379 L 1069,377 L 1070,369 L 1069,363 L 1066,355 L 1066,352 L 1064,352 L 1060,346 L 1056,332 L 1051,332 L 1052,334 L 1050,338 L 1048,337 L 1047,338 L 1044,337 L 1044,339 L 1034,341 L 1035,346 L 1032,350 L 1025,354 L 1020,362 L 1012,371 L 1012,374 L 1003,378 L 1001,383 L 1003,398 L 1001,404 L 1000,416 L 998,416 L 996,421 L 997,423 L 993,425 L 989,432 L 985,425 L 981,409 L 977,400 L 975,387 L 970,378 L 967,356 L 966,341 L 959,345 L 956,345 L 950,337 L 952,334 L 950,331 L 945,326 L 941,324 L 940,319 L 936,314 L 913,316 L 893,312 L 891,304 L 889,302 L 881,307 L 875,305 L 870,299 L 865,298 L 859,282 L 856,283 L 853,281 L 851,284 L 848,283 L 852,299 L 859,305 L 859,311 L 862,319 L 862,314 L 863,310 L 864,309 L 866,311 L 865,319 L 867,323 L 870,322 L 877,323 L 888,307 L 889,317 L 891,322 L 893,324 L 900,327 L 905,335 L 899,348 L 896,347 L 895,349 L 895,358 L 893,358 L 890,360 L 888,365 L 885,365 L 883,367 L 883,369 L 881,371 L 878,371 L 870,375 L 868,380 L 856,386 L 852,391 L 848,391 L 846,394 L 837,396 L 834,400 L 827,400 L 823,383 L 824,383 L 823,372 L 816,360 L 815,354 L 810,348 L 806,342 L 806,333 L 803,326 L 798,322 L 796,313 L 787,296 L 785,296 L 786,287 L 781,299 L 778,294 L 774,284 L 776,292 L 782,309 L 790,324 L 789,330 L 796,337 L 798,360 L 803,364 L 807,378 L 816,388 L 826,402 L 826,405 L 823,406 L 827,409 L 830,415 L 832,415 L 842,413 L 850,409 L 855,408 L 863,404 L 863,414 L 856,440 L 847,457 L 842,466 L 825,483 L 818,497 L 815,499 L 814,502 L 812,503 L 809,515 L 807,517 L 804,525 L 805,529 L 808,532 L 807,543 L 813,558 L 814,584 L 811,594 L 808,598 L 798,604 L 786,619 L 785,624 L 788,635 L 789,634 L 789,648 L 775,659 L 775,662 L 777,662 L 773,679 L 769,684 L 763,695 L 754,706 L 751,709 L 743,712 L 743,714 L 740,713 L 737,715 L 732,713 L 727,714 L 713,720 L 708,715 L 707,716 L 707,714 L 705,705 L 707,704 L 707,699 L 692,668 L 688,646 L 688,635 L 683,626 L 680,614 L 676,607 L 676,592 L 679,577 L 685,567 L 685,558 L 681,547 L 683,543 L 677,519 L 665,500 L 662,493 L 665,479 L 664,478 L 666,465 L 665,460 L 662,459 L 660,453 L 652,457 L 648,457 L 644,448 L 640,443 L 629,444 L 610,454 L 606,452 L 598,451 L 592,452 L 584,456 L 582,456 L 577,453 L 565,440 L 561,436 L 558,433 L 557,425 L 550,417 L 549,412 L 545,408 L 543,408 L 541,403 L 540,394 L 538,389 L 536,386 L 538,385 L 541,377 L 541,373 L 543,363 L 542,350 L 538,344 L 539,338 L 542,333 L 544,325 L 548,320 L 551,308 L 554,306 L 557,299 L 560,296 L 564,296 L 574,284 L 573,275 L 575,266 L 578,261 L 587,255 L 591,244 L 595,244 L 598,247 L 602,247 L 609,248 L 614,245 L 619,243 L 627,239 L 643,237 L 645,238 L 650,235 L 660,236 L 665,234 L 668,234 L 668,238 L 672,235 L 673,237 L 670,240 L 670,243 L 672,245 L 671,251 L 668,254 L 669,258 L 672,258 L 673,261 L 675,262 L 680,264 L 686,265 L 692,268 L 695,274 L 706,278 L 711,281 L 715,277 L 714,271 L 716,268 L 719,265 L 722,264 L 729,265 L 731,268 L 739,270 L 740,273 L 746,272 L 758,277 L 763,273 L 767,273 L 771,274 L 772,277 L 773,275 L 781,277 L 785,273 L 786,264 L 791,252 L 791,247 L 792,244 L 790,241 L 792,238 L 789,239 L 785,237 L 782,241 L 775,242 L 771,238 L 766,238 L 765,241 L 761,242 L 757,238 L 752,238 L 749,232 L 745,228 L 748,223 L 745,219 L 750,213 L 757,213 L 759,208 L 768,208 L 774,204 L 780,202 L 787,202 L 803,209 L 812,209 L 818,205 L 819,203 L 817,198 L 795,180 L 798,179 L 802,174 L 799,171 L 806,168 L 806,167 L 791,171 L 786,173 L 787,178 L 789,179 L 794,179 L 793,181 L 788,182 L 781,186 L 779,185 L 780,182 L 774,180 L 780,176 L 779,175 L 771,173 L 771,171 L 766,171 L 761,180 L 761,182 L 759,183 L 757,183 L 756,191 L 753,194 L 752,198 L 754,205 L 758,207 L 757,209 L 751,209 L 745,215 L 744,210 L 739,209 L 733,211 L 736,215 L 734,216 L 731,216 L 729,213 L 728,214 L 731,221 L 729,223 L 734,228 L 734,232 L 730,230 L 731,233 L 728,234 L 730,240 L 727,240 L 723,237 L 720,227 L 715,216 L 712,214 L 713,204 L 710,200 L 696,192 L 692,187 L 693,187 L 691,184 L 691,182 L 688,180 L 686,183 L 685,181 L 685,179 L 686,178 L 682,177 L 679,179 L 678,185 L 680,188 L 684,192 L 687,197 L 692,203 L 696,203 L 697,204 L 696,205 L 703,210 L 708,215 L 707,217 L 704,214 L 700,213 L 698,217 L 702,220 L 701,223 L 699,224 L 697,229 L 695,230 L 697,223 L 693,215 L 685,208 L 681,207 L 677,204 L 670,196 L 668,189 L 662,186 L 651,195 L 641,193 L 635,195 L 634,203 L 630,207 L 624,209 L 618,220 L 620,224 L 618,227 L 617,232 L 613,233 L 610,238 L 599,238 L 594,243 L 592,243 L 589,236 L 584,235 L 582,237 L 580,236 L 577,237 L 578,227 L 575,227 L 574,224 L 578,211 L 577,198 L 575,195 L 582,190 L 599,193 L 611,193 L 613,189 L 614,175 L 606,165 L 598,162 L 598,157 L 604,156 L 612,157 L 611,150 L 615,153 L 626,147 L 628,142 L 638,137 L 642,127 L 649,125 L 653,125 L 654,123 L 658,123 L 658,125 L 662,121 L 658,111 L 658,104 L 660,100 L 665,100 L 670,96 L 669,102 L 669,104 L 672,105 L 671,107 L 669,107 L 666,111 L 667,117 L 672,119 L 672,121 L 679,118 L 687,123 L 704,116 L 709,117 L 709,118 L 713,118 L 715,116 L 721,113 L 720,103 L 723,98 L 727,96 L 731,101 L 735,101 L 736,92 L 734,93 L 731,90 L 731,86 L 743,84 L 753,84 L 759,81 L 754,78 L 745,78 L 729,82 L 726,78 L 721,76 L 722,69 L 720,63 L 722,59 L 727,55 L 741,46 L 740,44 L 734,40 L 726,42 L 721,47 L 722,51 L 705,62 L 701,72 L 709,80 L 705,88 L 700,90 L 698,101 L 695,107 L 690,107 L 687,112 L 681,112 L 680,106 L 672,89 L 669,84 L 660,92 L 653,94 L 647,90 L 644,68 L 648,63 L 661,58 L 670,51 L 690,28 L 711,15 L 722,12 L 730,12 L 737,7 L 745,7 L 754,5 L 769,10 L 763,12 L 768,16 L 773,14 L 781,18 L 794,20 L 812,27 L 816,31 L 816,35 L 811,39 L 803,40 L 781,35 L 778,36 L 786,41 L 786,51 L 796,55 L 797,52 L 794,49 L 797,46 L 809,50 L 813,49 L 809,44 L 820,37 L 825,38 L 829,40 L 832,35 L 828,31 L 830,27 L 827,23 L 840,25 L 843,29 L 837,30 L 837,34 L 841,36 L 848,34 L 849,30 L 876,21 L 879,22 L 875,26 L 881,26 L 884,24 L 893,24 L 900,21 L 906,25 L 911,21 L 906,17 L 908,14 L 923,17 L 946,26 L 950,23 L 945,19 L 945,18 L 939,17 L 940,14 L 938,7 L 947,0 L 950,-6 L 953,-7 L 966,-5 L 967,-1 L 962,4 L 965,6 L 967,11 L 966,20 L 971,24 L 969,29 L 960,38 L 965,39 L 967,37 L 972,35 L 973,32 L 978,29 L 975,25 L 977,20 L 972,20 L 971,16 L 974,9 L 968,4 L 977,-1 L 976,-6 L 978,-6 L 981,-2 L 979,4 L 984,6 L 982,1 L 990,-2 L 1000,-2 L 1008,2 L 1004,-4 L 1004,-11 L 1012,-12 L 1034,-13 L 1030,-17 L 1035,-21 L 1041,-21 L 1050,-25 L 1063,-25 L 1064,-27 L 1077,-28 L 1081,-26 L 1091,-30 L 1100,-30 L 1101,-33 L 1106,-36 L 1117,-38 L 1125,-36 L 1119,-35 L 1130,-34 Z M 854,207 L 856,212 L 859,212 L 860,214 L 856,215 L 854,222 L 853,224 L 854,232 L 859,233 L 862,237 L 869,238 L 876,236 L 877,223 L 873,221 L 874,216 L 871,216 L 872,210 L 877,211 L 881,209 L 877,205 L 876,201 L 872,203 L 872,208 L 870,202 L 871,199 L 870,197 L 865,195 L 862,189 L 860,187 L 860,185 L 864,185 L 864,180 L 868,179 L 873,180 L 873,174 L 873,170 L 868,170 L 864,168 L 854,173 L 852,177 L 847,178 L 842,185 L 847,191 L 846,196 L 854,207 Z M 172,-37 L 161,-37 L 160,-39 L 170,-39 L 173,-38 L 172,-37 Z M 94,-38 L 86,-36 L 78,-39 L 82,-41 L 89,-41 L 96,-40 L 94,-38 Z M 738,-39 L 727,-37 L 719,-38 L 722,-40 L 719,-42 L 729,-44 L 731,-41 L 738,-39 Z M 163,-41 L 156,-39 L 152,-41 L 150,-44 L 150,-46 L 158,-46 L 164,-43 L 163,-41 Z M 143,-43 L 145,-40 L 129,-43 L 118,-43 L 123,-45 L 117,-47 L 117,-49 L 139,-46 L 143,-43 Z M 707,-52 L 722,-47 L 710,-44 L 708,-39 L 704,-38 L 701,-32 L 696,-32 L 685,-36 L 690,-39 L 683,-41 L 673,-46 L 670,-52 L 683,-54 L 685,-52 L 692,-52 L 694,-54 L 701,-54 L 707,-52 Z M 293,-75 L 316,-73 L 325,-72 L 325,-70 L 297,-64 L 308,-64 L 289,-58 L 280,-53 L 268,-50 L 253,-49 L 260,-49 L 256,-47 L 260,-44 L 240,-35 L 240,-34 L 248,-34 L 248,-32 L 236,-28 L 223,-30 L 209,-29 L 193,-30 L 193,-33 L 201,-35 L 199,-40 L 202,-40 L 215,-37 L 208,-42 L 200,-43 L 204,-46 L 213,-47 L 214,-50 L 207,-52 L 205,-56 L 222,-55 L 230,-57 L 202,-57 L 194,-60 L 184,-64 L 183,-67 L 205,-69 L 212,-72 L 218,-71 L 223,-70 L 227,-73 L 242,-75 L 293,-75 Z M 491,-78 L 520,-72 L 512,-70 L 468,-69 L 470,-68 L 487,-68 L 501,-66 L 511,-68 L 515,-66 L 509,-62 L 545,-67 L 559,-66 L 562,-63 L 539,-56 L 524,-55 L 535,-55 L 526,-46 L 526,-38 L 532,-34 L 524,-33 L 516,-31 L 525,-28 L 526,-22 L 521,-21 L 527,-15 L 517,-15 L 522,-12 L 521,-10 L 507,-9 L 513,-4 L 513,-1 L 504,-4 L 502,-2 L 508,-1 L 514,4 L 516,9 L 508,10 L 498,4 L 500,8 L 494,12 L 513,13 L 487,24 L 468,26 L 463,29 L 457,36 L 446,41 L 430,44 L 426,48 L 426,53 L 423,57 L 416,63 L 418,68 L 413,80 L 406,81 L 399,75 L 390,75 L 385,71 L 382,65 L 374,56 L 371,52 L 371,46 L 364,40 L 366,35 L 362,32 L 367,25 L 374,22 L 376,19 L 377,14 L 365,18 L 359,16 L 359,12 L 361,8 L 375,10 L 362,3 L 358,4 L 354,2 L 359,-4 L 346,-18 L 340,-21 L 340,-24 L 328,-28 L 293,-27 L 279,-34 L 301,-36 L 281,-38 L 270,-41 L 271,-43 L 306,-50 L 308,-52 L 295,-55 L 300,-57 L 316,-62 L 323,-63 L 321,-66 L 347,-69 L 362,-69 L 367,-67 L 380,-70 L 408,-65 L 396,-69 L 397,-72 L 413,-76 L 430,-75 L 436,-78 L 491,-78 Z \ No newline at end of file diff --git a/scripts/hole-punching-lan.gen.py b/scripts/hole-punching-lan.gen.py new file mode 100644 index 0000000..fbd464c --- /dev/null +++ b/scripts/hole-punching-lan.gen.py @@ -0,0 +1,366 @@ +#!/usr/bin/env python3 +"""Generator for hole-punching-lan.svg ("Direct connections on the same network"). + +The easy, very common case: Alice and Bob are two devices on the *same* home +network, behind the *same* router. They still discover each other over the relay +(ADD_ADDRESS / REACH_OUT), but then the *local* candidate addresses are directly +reachable across the LAN — so the very first PATH_CHALLENGE to the local address +succeeds. No NAT hole to punch, no sacrificial probe, no public-address probes. + +Layout is a "Y": the relay sits on top of the stem (the router's uplink), the +router is the junction, Alice and Bob hang off the two legs. Data first flows up +the stem to the relay and back; once the LAN path validates, the stem fades to +gray and the two legs (the direct LAN path) carry everything. + +Same look as the other how-iroh-works figures. + +Edit this script and run: python3 hole-punching-lan.gen.py +Writes ../images/how-iroh-works/hole-punching-lan.svg. +The MDX embeds this as ; the +intrinsic aspect ratio comes from the SVG viewBox (VB_W x VB_H). +""" + +import os + +HERE = os.path.dirname(os.path.abspath(__file__)) +OUT_PATH = os.path.normpath(os.path.join( + HERE, "..", "images", "how-iroh-works", "hole-punching-lan.svg")) + +MONO = "'Space Mono', monospace" +INDIGO, AMBER, GRAY, INK, BLUE, RED, GREEN = "#6366f1", "#d97706", "#888", "#111", "#2563eb", "#dc2626", "#15803d" +FADED = "#aab2bd" # inactive/secondary connection — clearly lighter than the structure gray +CYCLE = 27.0 +T_POPUP_FADE = 23.5 # popups fade a little after the LAN path is validated + + +def anim_opacity(points): + times = ";".join(f"{t/CYCLE:.4f}" for t, _ in points) + vals = ";".join(str(v) for _, v in points) + return (f'') + + +def anim_motion(wire_id, points): + times = ";".join(f"{t/CYCLE:.4f}" for t, _ in points) + kp = ";".join(str(k) for _, k in points) + return (f'') + + +def dot(color, opa_pts, mot_pts, wire): + return f''' + + {anim_opacity(opa_pts)} + {anim_motion(wire, mot_pts)} + ''' + + +def key_label(cx, y, text, size, color): + """A tiny drawn gold key (bow + shaft + teeth) + monospace label, centered on cx + (replaces the 🔑 emoji, which no pure-vector renderer can draw).""" + s = size + gold = "#eab308" + w = s * 1.3 # key glyph width + gap = s * 0.3 + xl = cx - (w + gap + s * 0.6 * len(text)) / 2 + cy = y - s * 0.30 + rb, rh = s * 0.34, s * 0.15 # bow outer / hole radius + bx = xl + rb # bow center x + hs = s * 0.18 # shaft thickness + xr = xl + w # right end + top, bot = cy - hs / 2, cy + hs / 2 + tw = s * 0.13 # tooth width + mono = "'Space Mono', monospace" + return ( + f'' + f'' + f'' + f'' + f'{text}' + ) + + +def iphone_screen(px0, py0, pw, ph, indigo): + """iPhone-style screen outline path (with notch) sized to fit inside an outer body of pw x ph.""" + sx0, sx1 = px0+3, px0+pw-3 + sy0, sy1 = py0+3, py0+ph-3 + cx = px0 + pw/2 + no = 7 # outer notch half-width + ni = 4 # inner notch half-width + nd = 5 # notch depth + ny = sy0 + nd + r = 6 # screen corner radius + return ( + f'' + ) + +def phone(px0, py0, keytext, iphone=False): + pw, ph = 50, 92 + cx = px0+pw/2 + if iphone: + screen = iphone_screen(px0, py0, pw, ph, INDIGO) + else: + screen = f'\n ' + return f''' + {screen} + + iroh + {key_label(cx, py0+60, keytext, 8, AMBER)}''' + + +def router_lan(cx, cy, lan_ip): + # single home router; the stem (uplink to the relay) leaves the top center, + # so the LAN gateway IP goes below and there is no label above. + bx, by, bw, bh = cx-30, cy-20, 60, 40 + return f''' + + + + + + + {lan_ip} + ''' + + +def relay(cx, cy, label): + bx, by, bw, bh = cx-32, cy-9, 64, 18 + s = [' ', + f' ', + f' ', + f' ', + f' '] + for i in range(6): + vx = bx+24+i*5 + s.append(f' ') + s.append(f' {label}') + s.append(' ') + return "\n".join(s) + + +def facts(x, lines, anchor="start"): + out = [f' '] + for y, label, val in lines: + out.append(f' {label} {val}') + out.append(' ') + return "\n".join(out) + + +# ---- positions (a "Y": relay on top, router at the junction, phones on the legs) ---- +RELAY = (440, 66) +ROUTER = (440, 200) +A_PH = (175, 312) # Alice phone top-left +B_PH = (655, 312) # Bob phone top-left +acx, bcx = 200, 680 +PH = 92 +JX, JY = 440, 220 # junction (router bottom center) where the legs meet the stem +RELAY_B = 78 # relay box bottom + +# The legs curve: they leave each phone vertically and arrive at the router vertically +# (from below). They enter the router box at two *separate* points tucked inside it, so +# the legs never cross; the vertical stem leaves the box top center up to the relay. +DV, DV2 = 64, 88 # control-point offsets: vertical out of the phone / into the router +OJ = 16 # legs enter the box this far either side of center +JYH = 213 # internal junction, hidden inside the router box +JXA, JXB = JX-OJ, JX+OJ +_legA = f"C {acx} {A_PH[1]-DV} {JXA} {JYH+DV2} {JXA} {JYH}" +_legB = f"C {bcx} {B_PH[1]-DV} {JXB} {JYH+DV2} {JXB} {JYH}" +_legA_r = f"C {JXA} {JYH+DV2} {acx} {A_PH[1]-DV} {acx} {A_PH[1]}" +_legB_r = f"C {JXB} {JYH+DV2} {bcx} {B_PH[1]-DV} {bcx} {B_PH[1]}" + +# stem (uplink to the relay); lan path (Alice -> router -> Bob) +stem_d = f"M {JX} {JYH} L {JX} {RELAY_B}" +lan_d = f"M {acx} {A_PH[1]} {_legA} L {JXB} {JYH} {_legB_r}" +# relay round-trips: up the stem to the relay and back down (the latency made visible) +relay_b2a = f"M {bcx} {B_PH[1]} {_legB} L {JX} {JYH} L {JX} {RELAY_B} L {JX} {JYH} L {JXA} {JYH} {_legA_r}" +relay_a2b = f"M {acx} {A_PH[1]} {_legA} L {JX} {JYH} L {JX} {RELAY_B} L {JX} {JYH} L {JXB} {JYH} {_legB_r}" + +alice_facts = facts(A_PH[0]+62, [ + (A_PH[1]+18, "EndpointId:", "1a9c…"), + (A_PH[1]+36, "Addr:", "192.168.0.3:2104"), + (A_PH[1]+54, "Addr:", "4.9.8.2:2104"), + (A_PH[1]+72, "relay:", "us-east"), +]) +bob_facts = facts(B_PH[0]+62, [ + (B_PH[1]+18, "EndpointId:", "8e2b…"), + (B_PH[1]+36, "Addr:", "192.168.0.5:4153"), + (B_PH[1]+54, "Addr:", "4.9.8.2:4153"), + (B_PH[1]+72, "relay:", "us-east"), +]) + +# ---- beat 1: ADD_ADDRESS (Bob advertises his candidates to Alice, over the relay) ---- +addaddr_pkt = dot(BLUE, + [(0, 0), (1.0, 0), (1.3, 1), (4.3, 1), (4.6, 0), (CYCLE, 0)], + [(0, 1), (1.0, 1), (4.5, 0), (CYCLE, 0)], "relay-b2a") +addaddr_co_pts = [(0, 0), (1.0, 0), (1.4, 1), (4.4, 1), (4.8, 0), (CYCLE, 0)] +addaddr_co = f''' + {anim_opacity(addaddr_co_pts)} + + ADD_ADDRESS 192.168.0.5:4153 + ADD_ADDRESS 4.9.8.2:4153 + ''' + +# Alice now knows Bob's candidates — ~2s popup beside Alice +ak_pts = [(0, 0), (4.4, 0), (4.7, 1), (6.6, 1), (6.9, 0), (CYCLE, 0)] +alice_knows = f''' + {anim_opacity(ak_pts)} + + + + + Bob + Candidate: 192.168.0.5:4153 + Candidate: 4.9.8.2:4153 + ''' + +# ---- beat 2: REACH_OUT (Alice -> Bob over the relay), carries Alice's candidates ---- +reachout_pkt = dot(BLUE, + [(0, 0), (7.5, 0), (7.8, 1), (10.8, 1), (11.1, 0), (CYCLE, 0)], + [(0, 0), (7.5, 0), (11.0, 1), (CYCLE, 1)], "relay-a2b") +reachout_co_pts = [(0, 0), (7.5, 0), (7.9, 1), (10.9, 1), (11.2, 0), (CYCLE, 0)] +reachout_co = f''' + {anim_opacity(reachout_co_pts)} + + REACH_OUT192.168.0.3:2104 + REACH_OUT4.9.8.2:2104 + ''' + +# Bob now knows Alice's candidates — ~2s popup beside Bob +bk_pts = [(0, 0), (11.0, 0), (11.3, 1), (13.2, 1), (13.5, 0), (CYCLE, 0)] +bob_knows = f''' + {anim_opacity(bk_pts)} + + + + + Alice + Candidate: 192.168.0.3:2104 + Candidate: 4.9.8.2:2104 + ''' + +# ============================ LAN hole punch (no NAT in the way) ============================ +# Same subnet, so the local candidate is directly reachable: the first PATH_CHALLENGE to the +# local address gets through. No sacrificial probe, no public probes, no drops. +# Alice is the client → she probes first. Each step gets a clear window. +T_C1 = 14.2 # Alice PATH_CHALLENGE -> Bob (local) — gets through +T_C2 = 16.8 # Bob PATH_CHALLENGE + PATH_RESPONSE -> Alice +T_R = 19.4 # Alice PATH_RESPONSE -> Bob — both directions validated +T_BLUE = T_R + 2.2 + + +def hp_pkt(t0, dur, kp0, kp1, color=BLUE): + arr = t0 + dur + return f''' + + {anim_opacity([(0, 0), (t0, 0), (t0+0.15, 1), (arr-0.12, 1), (arr+0.06, 0), (CYCLE, 0)])} + {anim_motion("lan", [(0, kp0), (t0, kp0), (arr, kp1), (CYCLE, kp1)])} + ''' + + +def callout(side, lines, t0, t1, w=170): + x = 232 if side == "L" else (648 - w) + h = 18 + len(lines)*19 + pts = [(0, 0), (t0-0.3, 0), (t0, 1), (t1, 1), (t1+0.3, 0), (CYCLE, 0)] + rows = "\n".join( + f' {txt}' + for i, (txt, bold) in enumerate(lines)) + return f''' + {anim_opacity(pts)} + +{rows} + ''' + + +# beat 3 — Alice's PATH_CHALLENGE to Bob's local address: directly reachable, gets through +c1_pkt = hp_pkt(T_C1, 2.0, 0, 1) +c1_co = callout("L", [("PATH_CHALLENGE", True), ("192.168.0.5:4153", False)], T_C1-0.3, T_C1+2.1) + +# beat 4 — Bob answers: PATH_RESPONSE (to Alice's challenge) + his own PATH_CHALLENGE +c2_pkt = hp_pkt(T_C2, 2.0, 1, 0) +c2_co = callout("R", [("PATH_RESPONSE", True), ("PATH_CHALLENGE", True), ("192.168.0.3:2104", False)], T_C2-0.3, T_C2+2.1) + +# beat 5 — Alice's PATH_RESPONSE to Bob — both directions validated +r_pkt = hp_pkt(T_R, 2.0, 0, 1) +r_co = callout("L", [("PATH_RESPONSE", True)], T_R-0.3, T_R+2.1, w=150) + +# the stem (relay uplink) is blue/active until the LAN path validates, then fades to gray +stem_stroke = (f'') + +# "direct" confirmation appears when the path is validated +direct_pts = [(0, 0), (T_BLUE, 0), (T_BLUE+0.4, 1), (CYCLE, 1)] +direct_badge = f''' + {anim_opacity(direct_pts)} + + direct (LAN) + ''' + +VB_X, VB_Y, VB_W, VB_H = 0, 30, 940, 430 +svg = f''' + + + {stem_stroke} + + + + + + +{relay(*RELAY, "relay")} + +{router_lan(*ROUTER, "192.168.0.1")} + + + +{phone(*A_PH, "1a9c…", iphone=True)} + Alice + +{alice_facts} + + + +{phone(*B_PH, "8e2b…")} + Bob + +{bob_facts} + + +{addaddr_pkt} +{reachout_pkt} +{c1_pkt} +{c2_pkt} +{r_pkt} +{direct_badge} + + +{addaddr_co} +{alice_knows} +{reachout_co} +{bob_knows} +{c1_co} +{c2_co} +{r_co} + + + + + + +''' + +open(OUT_PATH, "w").write(svg) +print(f"wrote {len(svg)} bytes -> {OUT_PATH}") diff --git a/scripts/hole-punching.gen.py b/scripts/hole-punching.gen.py new file mode 100644 index 0000000..6f88316 --- /dev/null +++ b/scripts/hole-punching.gen.py @@ -0,0 +1,469 @@ +#!/usr/bin/env python3 +"""Generator for hole-punching.svg ("Establishing direct connections"). + +Initial state of the hole-punching story: Alice and Bob, each behind a normal +home router, are connected *through the relay* (us-east) — all data flows up to +the relay and back down. Later steps (CallMeMaybe, address discovery, the ping, +and the direct path) build on this. + +Continues the discovery story: Bob = 8e2b…, home relay us-east; Alice = 1a9c…. +Same look as the other how-iroh-works figures; all connections blue. + +Edit this script and run: python3 hole-punching.gen.py +Writes ../images/how-iroh-works/hole-punching.svg. +The MDX embeds this as ; the +intrinsic aspect ratio comes from the SVG viewBox (VB_W x VB_H). +""" + +import os + +HERE = os.path.dirname(os.path.abspath(__file__)) +OUT_PATH = os.path.normpath(os.path.join( + HERE, "..", "images", "how-iroh-works", "hole-punching.svg")) + +MONO = "'Space Mono', monospace" +INDIGO, AMBER, GRAY, INK, BLUE, RED, GREEN = "#6366f1", "#d97706", "#888", "#111", "#2563eb", "#dc2626", "#15803d" +FADED = "#aab2bd" # inactive/secondary connection — clearly lighter than the structure gray +CYCLE = 39.0 # one loop; popups fade after both checks are green, then the direct path stands alone +T_POPUP_FADE = 29.5 # NAT bubbles fade out here (a few seconds after both mappings are validated) + + +def anim_opacity(points): + times = ";".join(f"{t/CYCLE:.4f}" for t, _ in points) + vals = ";".join(str(v) for _, v in points) + return (f'') + + +def anim_motion(wire_id, points): + times = ";".join(f"{t/CYCLE:.4f}" for t, _ in points) + kp = ";".join(str(k) for _, k in points) + return (f'') + + +def dot(color, opa_pts, mot_pts, wire="relay-path"): + return f''' + + {anim_opacity(opa_pts)} + {anim_motion(wire, mot_pts)} + ''' + + +def key_label(cx, y, text, size, color): + """A tiny drawn gold key (bow + shaft + teeth) + monospace label, centered on cx + (replaces the 🔑 emoji, which no pure-vector renderer can draw).""" + s = size + gold = "#eab308" + w = s * 1.3 # key glyph width + gap = s * 0.3 + xl = cx - (w + gap + s * 0.6 * len(text)) / 2 + cy = y - s * 0.30 + rb, rh = s * 0.34, s * 0.15 # bow outer / hole radius + bx = xl + rb # bow center x + hs = s * 0.18 # shaft thickness + xr = xl + w # right end + top, bot = cy - hs / 2, cy + hs / 2 + tw = s * 0.13 # tooth width + mono = "'Space Mono', monospace" + return ( + f'' + f'' + f'' + f'' + f'{text}' + ) + + +def iphone_screen(px0, py0, pw, ph, indigo): + """iPhone-style screen outline path (with notch) sized to fit inside an outer body of pw x ph.""" + sx0, sx1 = px0+3, px0+pw-3 + sy0, sy1 = py0+3, py0+ph-3 + cx = px0 + pw/2 + no = 7 # outer notch half-width + ni = 4 # inner notch half-width + nd = 5 # notch depth + ny = sy0 + nd + r = 6 # screen corner radius + return ( + f'' + ) + +def phone(px0, py0, keytext, iphone=False): + pw, ph = 50, 92 + cx = px0+pw/2 + if iphone: + screen = iphone_screen(px0, py0, pw, ph, INDIGO) + else: + screen = f'\n ' + return f''' + {screen} + + iroh + {key_label(cx, py0+60, keytext, 8, AMBER)}''' + + +def router(cx, cy, pub_ip, lan_ip): + # same design as the "Guaranteed connections" diagram: public IP above, + # L-shaped antennas, body, LAN IP below + bx, by, bw, bh = cx-30, cy-20, 60, 40 + return f''' + {pub_ip} + + + + + + + {lan_ip} + ''' + + +def relay(cx, cy, label): + bx, by, bw, bh = cx-32, cy-9, 64, 18 + s = [' ', + f' ', + f' ', + f' ', + f' '] + for i in range(6): + vx = bx+24+i*5 + s.append(f' ') + s.append(f' {label}') + s.append(' ') + return "\n".join(s) + + +def facts(x, lines, anchor="start"): + out = [f' '] + for y, label, val in lines: + out.append(f' {label} {val}') + out.append(' ') + return "\n".join(out) + + +# ---- positions ---- +RELAY = (440, 66) +R1 = (200, 250) # Alice's router +R2 = (680, 250) # Bob's router +A_PH = (175, 312) # Alice phone top-left +B_PH = (655, 312) # Bob phone top-left +acx, bcx = 200, 680 +PH = 92 + +# relay legs: Alice <-> relay (attach left of box) and relay <-> Bob (attach right of box), +# so the "relay" label below the box stays clear +# one continuous relay pipe: rises through each router, flattens and passes +# horizontally *through* the relay (in the left edge, out the right), then drops. +# The relay clearly sits on the pipe; the "relay" label stays clear below it. +rx, ry = RELAY +relay_d = (f"M {acx} {A_PH[1]} L {acx} {R1[1]-20} " + f"C {acx} 120 {rx-160} {ry} {rx-32} {ry} " + f"L {rx+32} {ry} " + f"C {rx+160} {ry} {bcx} 120 {bcx} {R2[1]-20} " + f"L {bcx} {B_PH[1]}") + +alice_facts = facts(A_PH[0]+62, [ + (A_PH[1]+18, "EndpointId:", "1a9c…"), + (A_PH[1]+36, "Addr:", "10.0.0.3:2104"), + (A_PH[1]+54, "Addr:", "8.3.1.9:2104"), + (A_PH[1]+72, "relay:", "us-west"), +]) +bob_facts = facts(B_PH[0]+62, [ + (B_PH[1]+18, "EndpointId:", "8e2b…"), + (B_PH[1]+36, "Addr:", "192.168.0.3:4153"), + (B_PH[1]+54, "Addr:", "4.9.8.2:4153"), + (B_PH[1]+72, "relay:", "us-east"), +]) + +# ---- beat 1: ADD_ADDRESS (Bob advertises his candidate addresses to Alice, over the relay) ---- +# one packet carrying both ADD_ADDRESS frames travels Bob -> relay -> Alice. Not time-critical, +# and it does nothing to the routers' forwarding tables. +addaddr_pkt = dot(BLUE, + [(0, 0), (1.0, 0), (1.3, 1), (3.9, 1), (4.2, 0), (CYCLE, 0)], + [(0, 1), (1.0, 1), (4.0, 0), (CYCLE, 0)]) +addaddr_co_pts = [(0, 0), (1.0, 0), (1.4, 1), (4.0, 1), (4.4, 0), (CYCLE, 0)] +addaddr_co = f''' + {anim_opacity(addaddr_co_pts)} + + ADD_ADDRESS 192.168.0.3:4153 + ADD_ADDRESS 4.9.8.2:4153 + ''' + +# when the ADD_ADDRESS packet lands, Alice now knows Bob's candidates — ~2s popup +# beside Alice (briefly covering her own info panel) +ak_pts = [(0, 0), (4.0, 0), (4.3, 1), (6.2, 1), (6.5, 0), (CYCLE, 0)] +alice_knows = f''' + {anim_opacity(ak_pts)} + + + + + Bob + Candidate: 192.168.0.3:4153 + Candidate: 4.9.8.2:4153 + ''' + +# ---- beat 2: REACH_OUT (Alice -> Bob over the relay) — carries Alice's candidate addresses +# and is the trigger that starts probing; shown here decoupled from the probes for clarity ---- +reachout_pkt = dot(BLUE, + [(0, 0), (6.8, 0), (7.1, 1), (9.2, 1), (9.4, 0), (CYCLE, 0)], + [(0, 0), (6.8, 0), (9.3, 1), (CYCLE, 1)]) +reachout_co_pts = [(0, 0), (6.8, 0), (7.1, 1), (9.3, 1), (9.6, 0), (CYCLE, 0)] +reachout_co = f''' + {anim_opacity(reachout_co_pts)} + + REACH_OUT10.0.0.3:2104 + REACH_OUT8.3.1.9:2104 + ''' + +# Bob now knows Alice's candidates — ~2s popup beside Bob (toward center) +bk_pts = [(0, 0), (9.3, 0), (9.6, 1), (11.6, 1), (11.9, 0), (CYCLE, 0)] +bob_knows = f''' + {anim_opacity(bk_pts)} + + + + + Alice + Candidate: 10.0.0.3:2104 + Candidate: 8.3.1.9:2104 + I need to reach out + ''' + +# ============================ hole punch (Alice is the client → she probes first) ============================ +# Locals fail (not routable). Then the public chain on ONE shared direct route: Alice's sacrificial +# probe opens her hole but is rejected at Bob's NAT; Bob's probe opens his hole and gets through; +# Alice replies PATH_RESPONSE+PATH_CHALLENGE; Bob replies PATH_RESPONSE → both paths validated. +# No timeouts. Each step gets a clear window before the next. +T_AL, T_BL = 12.4, 14.6 # local probes (fail) +T_AP = 16.9 # Alice public — sacrificial +T_BP = 19.9 # Bob public — succeeds +T_AR = 22.9 # Alice reply: PATH_RESPONSE + PATH_CHALLENGE +T_BR = 25.7 # Bob reply: PATH_RESPONSE → validated +T_BLUE = T_BR + 2.4 # direct path turns blue once the response validates it + +DROP_A, DROP_B = (acx, 165), (bcx, 165) # local drops — straight up above each router (no bend toward the other side, which read as if the packet was trying to reach the peer). Kept high enough that the "not routable" label clears the router's public IP below. +probe_local_a_d = f"M {acx} {A_PH[1]} L {acx} {DROP_A[1]}" +probe_local_b_d = f"M {bcx} {B_PH[1]} L {bcx} {DROP_B[1]}" + +# the one direct (internet) route, Alice(0) -> Bob(1). Every public packet rides it. +ARC = 158 +direct_d = f"M {acx} {A_PH[1]} L {acx} {R1[1]-20} C {acx} {ARC} {bcx} {ARC} {bcx} {R2[1]-20} L {bcx} {B_PH[1]}" + + +def cubic_len(p0, p1, p2, p3, n=120): + def b(t, a, bb, c, d): return (1-t)**3*a + 3*(1-t)**2*t*bb + 3*(1-t)*t*t*c + t**3*d + prev, L = p0, 0.0 + for i in range(1, n+1): + t = i/n + cur = (b(t, p0[0], p1[0], p2[0], p3[0]), b(t, p0[1], p1[1], p2[1], p3[1])) + L += ((cur[0]-prev[0])**2 + (cur[1]-prev[1])**2)**0.5 + prev = cur + return L + + +_seg = A_PH[1] - (R1[1]-20) +_arc = cubic_len((acx, R1[1]-20), (acx, ARC), (bcx, ARC), (bcx, R2[1]-20)) +F_BR = (_seg + _arc) / (_seg + _arc + _seg) # fraction at Bob's router — where the sacrificial bounces +REJECT = (bcx, R2[1]-20) # Bob's router WAN — sacrificial probe rejected here + + +def hp_pkt(wire, t0, dur, kp0=0, kp1=1, color=BLUE): + arr = t0 + dur + return f''' + + {anim_opacity([(0, 0), (t0, 0), (t0+0.15, 1), (arr-0.12, 1), (arr+0.06, 0), (CYCLE, 0)])} + {anim_motion(wire, [(0, kp0), (t0, kp0), (arr, kp1), (CYCLE, kp1)])} + ''' + + +def drop_x(p, t_arr, label, hold=1.9, dx=0): + pts = [(0, 0), (t_arr-0.05, 0), (t_arr+0.15, 1), (t_arr+hold, 1), (t_arr+hold+0.3, 0), (CYCLE, 0)] + if dx: # label to the side instead of below + lbl = f'{label}' + else: + lbl = f'{label}' + return f''' + {anim_opacity(pts)} + + + {lbl} + ''' + + +def callout(side, lines, t0, t1, w=164): + x = 240 if side == "L" else (656 - w) + h = 18 + len(lines)*19 + pts = [(0, 0), (t0-0.3, 0), (t0, 1), (t1, 1), (t1+0.3, 0), (CYCLE, 0)] + rows = "\n".join( + f' {txt}' + for i, (txt, bold) in enumerate(lines)) + return f''' + {anim_opacity(pts)} + +{rows} + ''' + + +def pc_co(side, addr, t0, t1): + return callout(side, [("PATH_CHALLENGE", True), (addr, False)], t0, t1) + + +def nat_bubble(side, l1, l2, t_on, t_used): + # thinking bubble on a router: the NAT mapping (remote public -> local private). A green + # check appears next to the mapping the moment a packet actually uses it. + out0, out1 = T_POPUP_FADE, T_POPUP_FADE+1.0 + pts = [(0, 0), (t_on, 0), (t_on+0.4, 1), (out0, 1), (out1, 0), (CYCLE, 0)] + hg_pts = [(0, 0), (t_on+0.4, 0), (t_on+0.7, 1), (t_used, 1), (t_used+0.3, 0), (CYCLE, 0)] # hourglass: created, temporary + chk_pts = [(0, 0), (t_used, 0), (t_used+0.3, 1), (CYCLE, 1)] # check: used / validated + if side == "a": # to the right of Alice's router, trail back to it + bx, trail = 240, [(236, 251, 3), (231, 250, 2.5), (227, 249, 2)] + else: # to the right of Bob's router + bx, trail = 720, [(716, 251, 3), (711, 250, 2.5), (707, 249, 2)] + dots = "\n".join(f' ' for c in trail) + cx = bx+138 + return f''' + {anim_opacity(pts)} +{dots} + + {l1} + {l2} + + + + + + {anim_opacity(hg_pts)} + + + + {anim_opacity(chk_pts)} + + ''' + + +# beat 3 — sequential locals (Alice first), not routable +loc_a_pkt = hp_pkt("probe-local-a", T_AL, 1.4) +loc_b_pkt = hp_pkt("probe-local-b", T_BL, 1.4) +loc_a_drop = drop_x(DROP_A, T_AL+1.4, "not routable", hold=0.5) +loc_b_drop = drop_x(DROP_B, T_BL+1.4, "not routable", hold=0.5) +loc_a_co = pc_co("L", "192.168.0.3:4153", T_AL-0.3, T_AL+1.5) +loc_b_co = pc_co("R", "10.0.0.3:2104", T_BL-0.3, T_BL+1.5) + +# beat 4 — Alice's sacrificial public probe: opens her hole, rejected at Bob's NAT (bounces) +pub_a_pkt = hp_pkt("direct", T_AP, 2.0, kp0=0, kp1=F_BR) +pub_a_drop = drop_x(REJECT, T_AP+2.0, "no mapping", hold=0.6, dx=16) +pub_a_co = pc_co("L", "4.9.8.2:4153", T_AP-0.3, T_AP+2.1) +nat_a = nat_bubble("a", "4.9.8.2:4153 →", "10.0.0.3:2104", T_AP+0.6, t_used=21.9) + +# beat 5 — Bob's public probe: opens his hole, through Alice's hole, reaches Alice (success) +pub_b_pkt = hp_pkt("direct", T_BP, 2.2, kp0=1, kp1=0) +pub_b_co = pc_co("R", "8.3.1.9:2104", T_BP-0.3, T_BP+2.3) +nat_b = nat_bubble("b", "8.3.1.9:2104 →", "192.168.0.3:4153", T_BP+0.6, t_used=24.9) + +# beat 6 — Alice's bundled reply (PATH_RESPONSE + PATH_CHALLENGE) → Bob +reply_pkt = hp_pkt("direct", T_AR, 2.2, kp0=0, kp1=1) +reply_co = callout("L", [("PATH_RESPONSE", True), ("PATH_CHALLENGE", True)], T_AR-0.3, T_AR+2.3, w=164) + +# beat 7 — Bob's PATH_RESPONSE → Alice; both directions validated +resp_pkt = hp_pkt("direct", T_BR, 2.2, kp0=1, kp1=0) +resp_co = callout("R", [("PATH_RESPONSE", True)], T_BR-0.3, T_BR+2.3, w=150) + +# the direct path itself: appears faded-gray as Bob's packet goes through, turns blue once validated +direct_opa = [(0, 0), (T_BP, 0), (T_BP+0.3, 1), (CYCLE-0.5, 1), (CYCLE-0.2, 0), (CYCLE, 0)] +direct_stroke = (f'') +direct_path = (f' ' + f'{anim_opacity(direct_opa)}{direct_stroke}') + +# the relay stays blue/active (user data flows over it during hole punching) and only fades to +# gray at the moment the direct path becomes blue — i.e. when the handover happens +relay_stroke = (f'') + +VB_X, VB_Y, VB_W, VB_H = 0, 30, 940, 430 +svg = f''' + + + {relay_stroke} + +{direct_path} + + + + + + + +{relay(*RELAY, "relay")} + +{router(*R1, "8.3.1.9", "10.0.0.1")} + +{router(*R2, "4.9.8.2", "192.168.0.1")} + + + +{phone(*A_PH, "1a9c…", iphone=True)} + Alice + +{alice_facts} + + + +{phone(*B_PH, "8e2b…")} + Bob + +{bob_facts} + + +{addaddr_pkt} +{reachout_pkt} +{loc_a_pkt} +{loc_b_pkt} +{pub_a_pkt} +{pub_b_pkt} +{reply_pkt} +{resp_pkt} +{loc_a_drop} +{loc_b_drop} +{pub_a_drop} +{nat_a} +{nat_b} + + +{addaddr_co} +{alice_knows} +{reachout_co} +{bob_knows} +{loc_a_co} +{loc_b_co} +{pub_a_co} +{pub_b_co} +{reply_co} +{resp_co} + + + + + + +''' + +open(OUT_PATH, "w").write(svg) +print(f"wrote {len(svg)} bytes -> {OUT_PATH}") diff --git a/scripts/publish-relay-dht.gen.py b/scripts/publish-relay-dht.gen.py new file mode 100644 index 0000000..b37a098 --- /dev/null +++ b/scripts/publish-relay-dht.gen.py @@ -0,0 +1,308 @@ +#!/usr/bin/env python3 +"""Generator for publish-relay-dht.svg ("Mainline DHT based address lookup"). + +The fully peer-to-peer variant of the DNS discovery figure. Instead of one +server (dns.iroh.link) there is a cloud (the Mainline DHT) holding many nodes. +Bob publishes his signed record to several random DHT nodes (green); Alice +resolves it by querying several random nodes (blue). + +All dots travel at the same speed, so they reach their nodes (and return) at +different times depending on distance. No arrowheads — the moving dots show the +direction. The answer is revealed as soon as the FIRST query response returns. + +One master SMIL loop (CYCLE seconds): publish -> Alice resolves -> arrows fade +out -> final state holds ~10s -> loop. + +Edit this script and run: python3 publish-relay-dht.gen.py +Writes ../images/how-iroh-works/publish-relay-dht.svg. +The MDX embeds this as ; the +intrinsic aspect ratio comes from the SVG viewBox (VB_W x VB_H). +""" + +import math +import os + +HERE = os.path.dirname(os.path.abspath(__file__)) +OUT_PATH = os.path.normpath(os.path.join( + HERE, "..", "images", "how-iroh-works", "publish-relay-dht.svg")) + +MONO = "'Space Mono', monospace" +INDIGO, AMBER, GRAY, INK = "#6366f1", "#d97706", "#888", "#111" +GREEN, BLUE, RED = "#15803d", "#2563eb", "#dc2626" +SPEED = 150.0 # px/second — same for every dot + + +def arc(x1, y1, x2, y2, k=0.16): + mx, my = (x1+x2)/2, (y1+y2)/2 + dx, dy = x2-x1, y2-y1 + L = math.hypot(dx, dy) + px, py = -dy/L, dx/L + if py > 0: + px, py = -px, -py + cx, cy = mx+px*k*L, my+py*k*L + d = f"M {x1:.0f} {y1:.0f} Q {cx:.0f} {cy:.0f} {x2:.0f} {y2:.0f}" + def q(t, a, b, c): return (1-t)**2*a + 2*(1-t)*t*b + t**2*c + N, length, prev = 50, 0.0, (x1, y1) + for i in range(1, N+1): + t = i/N + cur = (q(t, x1, cx, x2), q(t, y1, cy, y2)) + length += math.hypot(cur[0]-prev[0], cur[1]-prev[1]) + prev = cur + return d, length + + +def key_label(cx, y, text, size, color): + """A tiny drawn gold key (bow + shaft + teeth) + monospace label, centered on cx + (replaces the 🔑 emoji, which no pure-vector renderer can draw).""" + s = size + gold = "#eab308" + w = s * 1.3 # key glyph width + gap = s * 0.3 + xl = cx - (w + gap + s * 0.6 * len(text)) / 2 + cy = y - s * 0.30 + rb, rh = s * 0.34, s * 0.15 # bow outer / hole radius + bx = xl + rb # bow center x + hs = s * 0.18 # shaft thickness + xr = xl + w # right end + top, bot = cy - hs / 2, cy + hs / 2 + tw = s * 0.13 # tooth width + mono = "'Space Mono', monospace" + return ( + f'' + f'' + f'' + f'' + f'{text}' + ) + + +def iphone_screen(px0, py0, pw, ph, indigo): + """iPhone-style screen outline path (with notch) sized to fit inside an outer body of pw x ph.""" + sx0, sx1 = px0+3, px0+pw-3 + sy0, sy1 = py0+3, py0+ph-3 + cx = px0 + pw/2 + no = 7 # outer notch half-width + ni = 4 # inner notch half-width + nd = 5 # notch depth + ny = sy0 + nd + r = 6 # screen corner radius + return ( + f'' + ) + +def phone(px0, py0, keytext, iphone=False): + pw, ph = 48, 88 + cx = px0+pw/2 + if iphone: + screen = iphone_screen(px0, py0, pw, ph, INDIGO) + else: + screen = f'\n ' + return f''' + {screen} + + iroh + {key_label(cx, py0+58, keytext, 8, AMBER)}''' + + +# ============================ geometry ============================ +# DHT cloud (soft, fill-only overlapping circles) +cloud_circles = [(320, 150, 56), (392, 124, 62), (462, 130, 58), (524, 156, 50), + (360, 172, 56), (442, 178, 56), (292, 174, 40), (548, 182, 38)] +nodes = [(320, 162), (376, 138), (432, 172), (484, 142), (528, 168), (402, 196)] +pub_nodes = nodes[0:5] # Bob publishes to these +look_nodes = nodes[1:6] # Alice queries these + +bx0, by0 = 96, 296 +bcx = bx0+24 +PH = 88 +bob_top = (bcx, by0) + +ax0, ay0 = 664, 296 +acx = ax0+24 +alice_top = (acx, ay0) + +# ============================ timeline (constant speed) ============================ +PUB_LAUNCH = 1.5 +pub = [] # (path_d, arrival_time) +for n in pub_nodes: + d, L = arc(bob_top[0], bob_top[1], n[0], n[1], k=0.16) + pub.append((d, PUB_LAUNCH + L/SPEED)) +PUB_DONE = max(t for _, t in pub) + +ALICE_IN = PUB_DONE + 0.3 +LOOK_LAUNCH = ALICE_IN + 1.2 +look = [] # (path_d, reach_node_time, back_at_alice_time, has_data) +for n in look_nodes: + d, L = arc(n[0], n[1], alice_top[0], alice_top[1], k=0.16) + half = L/SPEED + look.append((d, LOOK_LAUNCH + half, LOOK_LAUNCH + 2*half, n in pub_nodes)) +FIRST_VALID = min(b for _, _, b, ok in look if ok) # first node that actually has the data +LAST_RETURN = max(b for _, _, b, _ in look) + +HOLD = 5.0 +ARROWS_GONE = LAST_RETURN + 0.6 +CYCLE = ARROWS_GONE + HOLD + 0.6 +OUT0, OUT1 = CYCLE - 0.6, CYCLE - 0.2 # persistent elements fade out before loop + + +def anim_opacity(points): + times = ";".join(f"{t/CYCLE:.4f}" for t, _ in points) + vals = ";".join(str(v) for _, v in points) + return (f'') + + +def anim_motion(wire_id, points): + times = ";".join(f"{t/CYCLE:.4f}" for t, _ in points) + kp = ";".join(str(k) for _, k in points) + return (f'') + + +def dot(wire_id, color, opa_pts, mot_pts): + return f''' + + {anim_opacity(opa_pts)} + {anim_motion(wire_id, mot_pts)} + ''' + + +def answer_dot(wire_id, color_down, launch, reach, back): + # query goes up blue, answer comes back coloured (green=valid, red=no data) + up = [(0, 0), (launch, 0), (launch+0.15, 1), (reach-0.05, 1), (reach, 0), (CYCLE, 0)] + down = [(0, 0), (reach, 0), (reach+0.05, 1), (back-0.05, 1), (back+0.1, 0), (CYCLE, 0)] + mot = [(0, 1), (launch, 1), (reach, 0), (back, 1), (CYCLE, 1)] + return f''' + {anim_motion(wire_id, mot)} + {anim_opacity(up)} + {anim_opacity(down)} + ''' + + +# ============================ static backdrop ============================ +cloud = [' ', ' '] +for cx, cy, r in cloud_circles: + cloud.append(f' ') +cloud.append(f' Mainline DHT') +cloud.append(' ') +cloud = "\n".join(cloud) + +# nodes are black; a published node turns green when its message arrives (one is never reached) +nd = [' ', ' '] +for x, y in nodes: + nd.append(f' ') +for i, (x, y) in enumerate(pub_nodes): + arr = pub[i][1] + g_pts = [(0, 0), (arr, 0), (arr+0.3, 1), (OUT0, 1), (OUT1, 0), (CYCLE, 0)] + nd.append(f' {anim_opacity(g_pts)}') +nd.append(' ') +node_dots = "\n".join(nd) + +bob = f''' + +{phone(bx0, by0, "8e2b…")} + Bob + + + NodeId: 8e2b… + Home relay: us-east + ''' + +alice_pts = [(0, 0), (ALICE_IN, 0), (ALICE_IN+0.6, 1), (OUT0, 1), (OUT1, 0), (CYCLE, 0)] +result_pts = [(0, 0), (FIRST_VALID, 0), (FIRST_VALID+0.5, 1), (OUT0, 1), (OUT1, 0), (CYCLE, 0)] +alice = f''' + + {anim_opacity(alice_pts)} +{phone(ax0, ay0, "1a9c…", iphone=True)} + Alice + + 8e2b… is at relay us-east{anim_opacity(result_pts)}''' + +# ============================ wires + dots ============================ +pub_wire_pts = [(0, 0), (0.6, 0), (1.0, 1), (PUB_DONE, 1), (PUB_DONE+0.5, 0), (CYCLE, 0)] +look_wire_pts = [(0, 0), (LOOK_LAUNCH-0.6, 0), (LOOK_LAUNCH, 1), (LAST_RETURN, 1), (LAST_RETURN+0.6, 0), (CYCLE, 0)] +wires, packets = [], [] +for i, (d, arr) in enumerate(pub): + wires.append(f' {anim_opacity(pub_wire_pts)}') + opa = [(0, 0), (PUB_LAUNCH, 0), (PUB_LAUNCH+0.15, 1), (arr-0.05, 1), (arr+0.1, 0), (CYCLE, 0)] + mot = [(0, 0), (PUB_LAUNCH, 0), (arr, 1), (CYCLE, 1)] + packets.append(dot(f"pw{i}", BLUE, opa, mot)) +for i, (d, reach, back, ok) in enumerate(look): + wires.append(f' {anim_opacity(look_wire_pts)}') + packets.append(answer_dot(f"lw{i}", GREEN if ok else RED, LOOK_LAUNCH, reach, back)) +wires = "\n".join(wires) +packets = "\n".join(packets) + +# ============================ callouts ============================ +pub_co_pts = [(0, 0), (1.6, 0), (2.0, 1), (PUB_DONE, 1), (PUB_DONE+0.5, 0), (CYCLE, 0)] +pub_co = f''' + + {anim_opacity(pub_co_pts)} + + DHT put + 8e2b… + Relay: us-east + ''' + +# DHT get callout (read request) — same box as the answer, shown until the answer arrives +read_pts = [(0, 0), (LOOK_LAUNCH-0.3, 0), (LOOK_LAUNCH+0.1, 1), (FIRST_VALID-0.1, 1), (FIRST_VALID+0.3, 0), (CYCLE, 0)] +read_co = f''' + + {anim_opacity(read_pts)} + + DHT get + 8e2b… + ''' + +# answer popup is visible while the dots are still travelling, then vanishes +ans_pts = [(0, 0), (FIRST_VALID, 0), (FIRST_VALID+0.4, 1), (LAST_RETURN, 1), (LAST_RETURN+0.6, 0), (CYCLE, 0)] +ans_co = f''' + + {anim_opacity(ans_pts)} + + ;; ANSWER SECTION: + TXT "relay=https://us-east" + ''' + +VB_X, VB_Y, VB_W, VB_H = 0, 44, 820, 376 +svg = f''' + +{cloud} + +{wires} + +{node_dots} + +{bob} + +{alice} + +{packets} + +{pub_co} + +{read_co} + +{ans_co} + + + + + + +''' + +open(OUT_PATH, "w").write(svg) +print(f"wrote {len(svg)} bytes -> {OUT_PATH} (CYCLE={CYCLE:.1f}s, first valid answer at {FIRST_VALID:.1f}s)") diff --git a/scripts/publish-relay.gen.py b/scripts/publish-relay.gen.py new file mode 100644 index 0000000..e4a3d3c --- /dev/null +++ b/scripts/publish-relay.gen.py @@ -0,0 +1,284 @@ +#!/usr/bin/env python3 +"""Generator for publish-relay.svg (the "Publishing the home relay" figure). + +Adapted from rklaehn's "DNS discovery" slide, restyled to match the other +how-iroh-works figures. No map. Our device "Bob" (left) publishes its current +home relay to the vanilla DNS server dns.iroh.link with a signed DNS packet sent +as an HTTPS PUT (green). Alice (right) resolves it with a DNS lookup (blue). +Continues the endpoint-startup story (Bob's home relay = us-east, key 8e2b...). + +One master loop (CYCLE seconds), driven entirely by SMIL so every element stays +in sync: publish once -> record added -> Alice resolves once -> arrows fade out +-> final state holds ~10s -> loop. + +To change it, edit this script and run: python3 publish-relay.gen.py +It writes ../images/how-iroh-works/publish-relay.svg. +The MDX embeds this as ; the +intrinsic aspect ratio comes from the SVG viewBox (VB_W x VB_H). +""" + +import math +import os + +HERE = os.path.dirname(os.path.abspath(__file__)) +OUT_PATH = os.path.normpath(os.path.join( + HERE, "..", "images", "how-iroh-works", "publish-relay.svg")) + +MONO = "'Space Mono', monospace" +INDIGO, AMBER, GRAY, INK = "#6366f1", "#d97706", "#888", "#111" +GREEN, BLUE = "#15803d", "#2563eb" + +CYCLE = 14.5 # seconds for one full loop (~5s static hold) + + +def anim_opacity(points): + times = ";".join(f"{t/CYCLE:.4f}" for t, _ in points) + vals = ";".join(str(v) for _, v in points) + return (f'') + + +def anim_motion(wire_id, points): + times = ";".join(f"{t/CYCLE:.4f}" for t, _ in points) + kp = ";".join(str(k) for _, k in points) + return (f'') + + +def arc(x1, y1, x2, y2, k=0.16): + mx, my = (x1+x2)/2, (y1+y2)/2 + dx, dy = x2-x1, y2-y1 + L = math.hypot(dx, dy) + px, py = -dy/L, dx/L + if py > 0: + px, py = -px, -py + cx, cy = mx+px*k*L, my+py*k*L + return f"M {x1:.0f} {y1:.0f} Q {cx:.0f} {cy:.0f} {x2:.0f} {y2:.0f}" + + +def key_label(cx, y, text, size, color): + """A tiny drawn gold key (bow + shaft + teeth) + monospace label, centered on cx + (replaces the 🔑 emoji, which no pure-vector renderer can draw).""" + s = size + gold = "#eab308" + w = s * 1.3 # key glyph width + gap = s * 0.3 + xl = cx - (w + gap + s * 0.6 * len(text)) / 2 + cy = y - s * 0.30 + rb, rh = s * 0.34, s * 0.15 # bow outer / hole radius + bx = xl + rb # bow center x + hs = s * 0.18 # shaft thickness + xr = xl + w # right end + top, bot = cy - hs / 2, cy + hs / 2 + tw = s * 0.13 # tooth width + mono = "'Space Mono', monospace" + return ( + f'' + f'' + f'' + f'' + f'{text}' + ) + + +def iphone_screen(px0, py0, pw, ph, indigo): + """iPhone-style screen outline path (with notch) sized to fit inside an outer body of pw x ph.""" + sx0, sx1 = px0+3, px0+pw-3 + sy0, sy1 = py0+3, py0+ph-3 + cx = px0 + pw/2 + no = 7 # outer notch half-width + ni = 4 # inner notch half-width + nd = 5 # notch depth + ny = sy0 + nd + r = 6 # screen corner radius + return ( + f'' + ) + +def phone(px0, py0, keytext, iphone=False): + pw, ph = 48, 88 + cx = px0+pw/2 + if iphone: + screen = iphone_screen(px0, py0, pw, ph, INDIGO) + else: + screen = f'\n ' + return f''' + {screen} + + iroh + {key_label(cx, py0+58, keytext, 8, AMBER)}''' + + +def doc_packet(wire_id, color, opa_pts, mot_pts): + # a simple dot riding a wire (the popup explains what the request is) + return f''' + + {anim_opacity(opa_pts)} + {anim_motion(wire_id, mot_pts)} + ''' + + +# ============================ static backdrop ============================ + +# dns.iroh.link server, top-center (vanilla 3U rack, gray LEDs) +sx, sy, sw, sh = 378, 56, 64, 66 +scx = sx+sw/2 +srv = [' ', ' ', + f' '] +for i in range(3): + ry = sy + 4 + i*21 + srv.append(f' ') + srv.append(f' ') + for j in range(6): + vx = sx+24+j*5 + srv.append(f' ') + if i < 2: + yy = sy+4+(i+1)*21-2 + srv.append(f' ') +srv.append(f' dns.iroh.link') +srv.append(' ') +server = "\n".join(srv) + +# Bob (left, publisher) — always present +bx0, by0 = 96, 296 +bcx = bx0+24 +PH = 88 +bob = f''' + +{phone(bx0, by0, "8e2b…")} + Bob + + + NodeId: 8e2b… + Home relay: us-east + ''' + +# ============================ records on the server ============================ +rec_x = sx+sw+30 +existing = [("1f3a…", "eu-west"), ("7c0e…", "us-west"), + ("b48d…", "us-east"), ("2a91…", "eu-west")] +rlh, ry0 = 16, 60 +rec_lines = [f' {k} → {r}' + for i, (k, r) in enumerate(existing)] +ynew = ry0 + len(existing)*rlh +rec_pts = [(0, 0), (3.5, 0), (4.0, 1), (13.8, 1), (14.2, 0), (CYCLE, 0)] +record = f''' + + +{chr(10).join(rec_lines)} + 8e2b… → us-east{anim_opacity(rec_pts)} + ''' + +# ============================ Alice (right, resolver) ============================ +ax0, ay0 = 664, 296 +acx = ax0+24 +alice_pts = [(0, 0), (3.8, 0), (4.4, 1), (13.8, 1), (14.2, 0), (CYCLE, 0)] +result_pts = [(0, 0), (8.0, 0), (8.5, 1), (13.8, 1), (14.2, 0), (CYCLE, 0)] +alice = f''' + + {anim_opacity(alice_pts)} +{phone(ax0, ay0, "1a9c…", iphone=True)} + Alice + + + 8e2b… is at relay us-east{anim_opacity(result_pts)}''' + +# ============================ wires ============================ +put_d = arc(bcx, by0, sx+18, sy+sh, k=0.18) # Bob -> server +lookup_d = arc(sx+sw-18, sy+sh, acx, ay0, k=0.18) # server -> Alice (answer direction) +put_wire_pts = [(0, 0), (0.6, 0), (1.0, 1), (4.0, 1), (4.5, 0), (CYCLE, 0)] +lookup_wire_pts = [(0, 0), (4.4, 0), (5.0, 1), (8.3, 1), (8.8, 0), (CYCLE, 0)] +wires = f''' {anim_opacity(put_wire_pts)} + {anim_opacity(lookup_wire_pts)}''' + +# ============================ packets ============================ +# PUT: Bob(0) -> server(1), fades out as it lands (~3.5s) +put_pkt = doc_packet("put-wire", BLUE, + [(0, 0), (1.5, 0), (1.7, 1), (3.4, 1), (3.6, 0), (CYCLE, 0)], + [(0, 0), (1.5, 0), (3.5, 1), (CYCLE, 1)]) +# LOOKUP: on lookup-wire 0=server,1=Alice. Query Alice(1)->server(0), answer server(0)->Alice(1). +lookup_pkt = doc_packet("lookup-wire", BLUE, + [(0, 0), (5.0, 0), (5.2, 1), (7.9, 1), (8.1, 0), (CYCLE, 0)], + [(0, 1), (5.0, 1), (6.5, 0), (8.0, 1), (CYCLE, 1)]) + +# ============================ callouts ============================ +put_co_pts = [(0, 0), (1.6, 0), (2.0, 1), (4.0, 1), (4.5, 0), (CYCLE, 0)] +put_co = f''' + + {anim_opacity(put_co_pts)} + + HTTPS PUT + Relay: us-east + Signed by: 8e2b… + ''' + +lookup_co_pts = [(0, 0), (4.6, 0), (5.0, 1), (8.3, 1), (8.8, 0), (CYCLE, 0)] +# the query content is shown on the way up, then replaced by the answer as the dot heads back (~6.5s) +query_pts = [(0, 0), (4.6, 0), (5.0, 1), (6.3, 1), (6.6, 0), (CYCLE, 0)] +answer_pts = [(0, 0), (6.4, 0), (6.7, 1), (8.3, 1), (8.7, 0), (CYCLE, 0)] +lookup_co = f''' + + {anim_opacity(lookup_co_pts)} + + + {anim_opacity(query_pts)} + DNS LOOKUP + TXT _iroh.8e2b….dns.iroh.link + + + {anim_opacity(answer_pts)} + ;; ANSWER SECTION: + TXT "relay=https://us-east" + + ''' + +VB_X, VB_Y, VB_W, VB_H = 0, 44, 820, 376 +svg = f''' + + + + + + + + + + +{wires} + +{server} + +{record} + +{bob} + +{alice} + +{put_pkt} +{lookup_pkt} + +{put_co} + +{lookup_co} + + + + + + +''' + +open(OUT_PATH, "w").write(svg) +print(f"wrote {len(svg)} bytes -> {OUT_PATH}") diff --git a/scripts/routing-moves.gen.py b/scripts/routing-moves.gen.py new file mode 100644 index 0000000..64cd38d --- /dev/null +++ b/scripts/routing-moves.gen.py @@ -0,0 +1,316 @@ +#!/usr/bin/env python3 +"""Generator for routing-moves.svg. + +Bob's commute story: home wifi (R1) → cellular (R2, cell tower) → in-flight +(R3, Starlink ground station). Alice stays put; the connection follows Bob. +20s loop, all SMIL so every element shares one timeline (no CSS/SMIL drift). + +To change it, edit this script and run: python3 routing-moves.gen.py +It writes ../images/how-iroh-works/routing-moves.svg. +The MDX embeds this as ; the +intrinsic aspect ratio comes from the SVG viewBox (VB_W x VB_H). +""" +import os + +HERE = os.path.dirname(os.path.abspath(__file__)) +OUT_PATH = os.path.normpath(os.path.join( + HERE, "..", "images", "how-iroh-works", "routing-moves.svg")) + +MONO = "'Space Mono', monospace" +INDIGO, AMBER, GRAY = "#6366f1", "#d97706", "#888" +CYCLE = 20 # seconds + +VB_W, VB_H = 800, 380 + +# Animation thresholds (fractions of the cycle): +# 0.25 — Bob starts moving (and R2 fades in) +# 0.30 — single-bend path swaps to multi R1→R2 +# 0.55 — R2 + multi R1→R2 fade out +# 0.57 — R3 + multi R1→R3 fade in +# 0.75 — Bob reaches final position + + +def key_label(cx, y, text, size, color): + """Tiny drawn key glyph + monospace text, replacing the 🔑 emoji.""" + s = size + gold = "#eab308" + w = s * 1.3 + gap = s * 0.3 + xl = cx - (w + gap + s * 0.6 * len(text)) / 2 + cy = y - s * 0.30 + rb, rh = s * 0.34, s * 0.15 + bx = xl + rb + hs = s * 0.18 + xr = xl + w + top, bot = cy - hs / 2, cy + hs / 2 + tw = s * 0.13 + return ( + f'' + f'' + f'' + f'' + f'{text}' + ) + + +def iphone_outline(cx, top, bottom): + """iPhone outer body + screen path with notch in the top. Centered at cx, body height = bottom-top.""" + bx0 = cx - 45 # body 90 wide + sx0, sx1 = cx - 42, cx + 42 # screen 84 wide + sy0, sy1 = top + 3, bottom - 3 # screen y span + # Notch geometry (matches the original hand-tuned shape): + # outer corners at (cx-15, sy0) and (cx+15, sy0) — 30px wide + # inner notch bottom at (cx-9 .. cx+9) at y = sy0 + 9 + n_outer_l, n_outer_r = cx - 15, cx + 15 + n_inner_l, n_inner_r = cx - 9, cx + 9 + n_bottom = sy0 + 9 + body = f'' + path = ( + f'' + ) + return body + "\n " + path + + +def phone_iphone(cx, top, key, label, *, ip_text=None, animated_ips=None): + """Static iPhone — used for Alice.""" + lines = [f' '] + if ip_text is not None: + lines.append(f' {ip_text}') + if animated_ips: + for ip in animated_ips: + lines.append(ip) + lines += [ + f' {iphone_outline(cx, top, top + 150)}', + f' ', + f' iroh', + f' {key_label(cx, top + 95, key, 10, AMBER)}', + f' {label}', + f' ', + ] + return "\n".join(lines) + + +def phone_android(cx, top, key, label, *, animated_ips=None): + """Android phone — used for Bob (with movement and IP changes).""" + bx0 = cx - 45 + sx0 = cx - 42 + lines = [' '] + lines.append(f' ') + if animated_ips: + for ip in animated_ips: + lines.append(' ' + ip) + lines += [ + f' ', + f' ', + f' ', + f' ', + f' iroh', + f' {key_label(cx, top + 95, key, 10, AMBER)}', + f' {label}', + ' ', + ] + return "\n".join(lines) + + +def bob_ip(text, key_times, vals, *, initial_opacity=None): + extra = '' if initial_opacity is None else f' opacity="{initial_opacity}"' + return ( + f'' + f'{text}' + f'' + ) + + +def morph_path(d0, mid_d, k_mid_start, k_mid_end, stroke_attrs): + """Path with d-morph animation between two shapes.""" + return ( + f' \n' + f' \n' + ) + + +# ============================ Alice (static iPhone) ============================ +alice_block = phone_iphone(120, 190, "a4f7c0…", "Alice", ip_text="10.0.0.42") + +# ============================ Bob (Android, moves) ============================ +bob_ips = [ + bob_ip("10.0.0.7", "0;0.29;0.31;1", "1;1;0;0"), + bob_ip("192.168.1.7", "0;0.29;0.31;0.55;0.57;1", "0;0;1;1;0;0", initial_opacity="0"), + bob_ip("172.16.0.7", "0;0.55;0.57;1", "0;0;1;1", initial_opacity="0"), +] +bob_block = phone_android(240, 190, "8e2b1d…", "Bob", animated_ips=bob_ips) + +# ============================ Connection paths ============================ +single_d = "M 120 240 Q 120 161 180 161 Q 240 161 240 240" +single_d2 = "M 120 240 Q 120 161 180 161 Q 285 161 285 240" +single_path = ( + f' \n' + f' \n' + f' \n' + f' \n' + f' \n' +) + +multi_r1r2_d = "M 120 240 C 120 170 180 200 180 130 L 180 90 C 180 30 420 30 420 90 L 420 130 C 420 200 285 170 285 240" +multi_r1r2_d2 = "M 120 240 C 120 170 180 200 180 130 L 180 90 C 180 30 420 30 420 90 L 420 130 C 420 200 528 170 528 240" +multi_r1r2_path = ( + f' \n' + f' \n' + f' \n' + f' \n' + f' \n' +) + +multi_r1r3_d = "M 120 240 C 120 170 180 200 180 130 L 180 90 C 180 10 660 10 660 90 L 660 130 C 660 200 552 170 552 240" +multi_r1r3_d2 = "M 120 240 C 120 170 180 200 180 130 L 180 90 C 180 10 660 10 660 90 L 660 130 C 660 200 720 170 720 240" +multi_r1r3_path = ( + f' \n' + f' \n' + f' \n' + f' \n' + f' \n' +) + +# ============================ Routers ============================ +# R1 — home wifi (always visible). Public IP fades in once multi-routing kicks in. +r1 = f''' + + home router + + 203.0.113.1 + + + + + + + 10.0.0.1 + ''' + +# R2 — cell tower (appears 5s, fades out at 11s). 3 panels on a mast with crossbar. +r2 = f''' + + + + mobile network + + 198.51.100.1 + + + + + + 192.168.1.1 + ''' + +# R3 — Starlink ground station (fades in at 11s). Flat tilted parallelogram + kickstand. +r3 = f''' + + + + satellite internet + + 100.64.10.1 + + + 172.16.0.1 + ''' + +# ============================ Timer bar ============================ +timer = f''' + + + + + ''' + +# ============================ Assemble ============================ +svg = f''' + + + + + + + + +{alice_block} + + +{bob_block} + +{single_path} +{multi_r1r2_path} +{multi_r1r3_path} + +{r1} + +{r2} + +{r3} + + +{timer} + +''' + +open(OUT_PATH, "w").write(svg) +print(f"wrote {len(svg)} bytes -> {OUT_PATH}") From 8a32590b51f810e0e57b34a63d29673f05dca395 Mon Sep 17 00:00:00 2001 From: Ruediger Klaehn Date: Wed, 24 Jun 2026 10:19:16 +0300 Subject: [PATCH 2/4] Switch from css to smil fully so the animations survive minify --- images/how-iroh-works/connect-by-key.svg | 82 ++++-------- images/how-iroh-works/endpoint-startup.svg | 17 +-- scripts/connect-by-key.gen.py | 141 +++++++++++---------- scripts/endpoint-startup.gen.py | 17 +-- 4 files changed, 109 insertions(+), 148 deletions(-) diff --git a/images/how-iroh-works/connect-by-key.svg b/images/how-iroh-works/connect-by-key.svg index f686cee..5a5f474 100644 --- a/images/how-iroh-works/connect-by-key.svg +++ b/images/how-iroh-works/connect-by-key.svg @@ -1,49 +1,16 @@ + - - @@ -52,13 +19,13 @@ 10.0.0.42 - 192.168.1.7 - - 203.0.113.15 - - 172.20.4.88 - - 192.168.10.55 + 192.168.1.7 + + 203.0.113.15 + + 172.20.4.88 + + 192.168.10.55 @@ -82,7 +49,7 @@ fill="none" stroke="#6366f1" stroke-width="1.5"/> iroh - a4f7c0… + a4f7c0… @@ -90,23 +57,24 @@ iroh - 8e2b1d… + 8e2b1d… Alice Bob - + + connect - + - - - + + + diff --git a/images/how-iroh-works/endpoint-startup.svg b/images/how-iroh-works/endpoint-startup.svg index 2e8c23a..480f091 100644 --- a/images/how-iroh-works/endpoint-startup.svg +++ b/images/how-iroh-works/endpoint-startup.svg @@ -1,20 +1,11 @@ - - - - - + + @@ -120,7 +111,9 @@ - + + + us-east: 19 ms us-west: 71 ms eu-west: 102 ms diff --git a/scripts/connect-by-key.gen.py b/scripts/connect-by-key.gen.py index 558b770..ada7650 100644 --- a/scripts/connect-by-key.gen.py +++ b/scripts/connect-by-key.gen.py @@ -7,7 +7,12 @@ between the keys (unidirectional → bidirectional after the handshake) stays up. The point: iroh dials keys, not addresses. -14s loop. CSS-driven keyframes (single clock, no SMIL/CSS mixing). +14s loop. SMIL-driven (/), single clock. We avoid +CSS keyframes on purpose: Mintlify serves SVG assets with a +`Content-Security-Policy: default-src 'none'` header, which blocks the inline +`. + +```sh +python3 scripts/csp_preview.py # then open http://localhost:8000/ +python3 scripts/csp_preview.py 8080 # override the port if 8000 is busy +``` + +If a diagram animates there, it will animate in production. If it freezes there +but moves in `mint dev`, it's still relying on CSS/` - - - - - - - - - 10.0.0.42 - - 192.168.1.7 - - 203.0.113.15 - - 172.20.4.88 - - 192.168.10.55 - - - - - - iroh - a4f7c0… - - - - - - - iroh - 8e2b1d… - - - Alice - Bob - - - - - - connect - - - - - - - - - - diff --git a/images/how-iroh-works/embedding-phone.svg b/images/how-iroh-works/embedding-phone.svg deleted file mode 100644 index df4cb51..0000000 --- a/images/how-iroh-works/embedding-phone.svg +++ /dev/null @@ -1,121 +0,0 @@ - - - - - iOS app (Swift) -iroh via iroh-ffi - - - - swift app - - iroh - - - - - Android app (Kotlin) -iroh via iroh-ffi - - - - - kotlin app - - iroh - - - - - Native desktop (C++) -iroh via iroh-c-ffi - - - - c++ app - - iroh - - - - - - - Web app (JavaScript) -iroh compiled to WASM - - - - - - - - - js app - - iroh - - - - - Server daemon (Rust) -iroh crate directly - - - - - - - - - - - - rust daemon - - iroh - - - - - Embedded firmware (Rust) -iroh crate - - - - - - - - - - - - - - - - - - firmware - - iroh - - diff --git a/images/how-iroh-works/endpoint-startup.svg b/images/how-iroh-works/endpoint-startup.svg deleted file mode 100644 index 480f091..0000000 --- a/images/how-iroh-works/endpoint-startup.svg +++ /dev/null @@ -1,130 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - us-west - - - - - - - - - - - - 52.10.18.7 - - - - - us-east - - - - - - - - - - - - 44.208.61.5 - - - - - eu-west - - - - - - - - - - - - 18.196.142.9 - - - - - - - - - - - - - - - iroh - 8e2b… - - - - 73.118.42.9 - - - - - - - - - - - - us-east: 19 ms - us-west: 71 ms - eu-west: 102 ms - - - - - - - - - - diff --git a/images/how-iroh-works/hole-punching-lan.svg b/images/how-iroh-works/hole-punching-lan.svg deleted file mode 100644 index c62dfaa..0000000 --- a/images/how-iroh-works/hole-punching-lan.svg +++ /dev/null @@ -1,156 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - relay - - - - - - - - - - 192.168.0.1 - - - - - - - - iroh - 1a9c… - Alice - - - EndpointId: 1a9c… - Addr: 192.168.0.3:2104 - Addr: 4.9.8.2:2104 - relay: us-east - - - - - - - - - iroh - 8e2b… - Bob - - - EndpointId: 8e2b… - Addr: 192.168.0.5:4153 - Addr: 4.9.8.2:4153 - relay: us-east - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - direct (LAN) - - - - - - - ADD_ADDRESS 192.168.0.5:4153 - ADD_ADDRESS 4.9.8.2:4153 - - - - - - - - Bob - Candidate: 192.168.0.5:4153 - Candidate: 4.9.8.2:4153 - - - - - REACH_OUT192.168.0.3:2104 - REACH_OUT4.9.8.2:2104 - - - - - - - - Alice - Candidate: 192.168.0.3:2104 - Candidate: 4.9.8.2:2104 - - - - - PATH_CHALLENGE - 192.168.0.5:4153 - - - - - PATH_RESPONSE - PATH_CHALLENGE - 192.168.0.3:2104 - - - - - PATH_RESPONSE - - - - - - - diff --git a/images/how-iroh-works/hole-punching.svg b/images/how-iroh-works/hole-punching.svg deleted file mode 100644 index d4757f1..0000000 --- a/images/how-iroh-works/hole-punching.svg +++ /dev/null @@ -1,257 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - relay - - - - 8.3.1.9 - - - - - - - 10.0.0.1 - - - - 4.9.8.2 - - - - - - - 192.168.0.1 - - - - - - - - iroh - 1a9c… - Alice - - - EndpointId: 1a9c… - Addr: 10.0.0.3:2104 - Addr: 8.3.1.9:2104 - relay: us-west - - - - - - - - - iroh - 8e2b… - Bob - - - EndpointId: 8e2b… - Addr: 192.168.0.3:4153 - Addr: 4.9.8.2:4153 - relay: us-east - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - not routable - - - - - - not routable - - - - - - no mapping - - - - - - - - 4.9.8.2:4153 → - 10.0.0.3:2104 - - - - - - - - - - - - - - - - - - - 8.3.1.9:2104 → - 192.168.0.3:4153 - - - - - - - - - - - - - - - - - - ADD_ADDRESS 192.168.0.3:4153 - ADD_ADDRESS 4.9.8.2:4153 - - - - - - - - Bob - Candidate: 192.168.0.3:4153 - Candidate: 4.9.8.2:4153 - - - - - REACH_OUT10.0.0.3:2104 - REACH_OUT8.3.1.9:2104 - - - - - - - - Alice - Candidate: 10.0.0.3:2104 - Candidate: 8.3.1.9:2104 - I need to reach out - - - - - PATH_CHALLENGE - 192.168.0.3:4153 - - - - - PATH_CHALLENGE - 10.0.0.3:2104 - - - - - PATH_CHALLENGE - 4.9.8.2:4153 - - - - - PATH_CHALLENGE - 8.3.1.9:2104 - - - - - PATH_RESPONSE - PATH_CHALLENGE - - - - - PATH_RESPONSE - - - - - - - diff --git a/images/how-iroh-works/publish-relay-dht.svg b/images/how-iroh-works/publish-relay-dht.svg deleted file mode 100644 index cdf67db..0000000 --- a/images/how-iroh-works/publish-relay-dht.svg +++ /dev/null @@ -1,149 +0,0 @@ - - - - - - - - - - - - - Mainline DHT - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - iroh - 8e2b… - Bob - - - NodeId: 8e2b… - Home relay: us-east - - - - - - - - - iroh - 1a9c… - Alice - - 8e2b… is at relay us-east - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - DHT put - 8e2b… - Relay: us-east - - - - - - - DHT get - 8e2b… - - - - - - - ;; ANSWER SECTION: - TXT "relay=https://us-east" - - - - - - - diff --git a/images/how-iroh-works/publish-relay.svg b/images/how-iroh-works/publish-relay.svg deleted file mode 100644 index ca079a7..0000000 --- a/images/how-iroh-works/publish-relay.svg +++ /dev/null @@ -1,125 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - dns.iroh.link - - - - - - 1f3a… → eu-west - 7c0e… → us-west - b48d… → us-east - 2a91… → eu-west - 8e2b… → us-east - - - - - - - - - iroh - 8e2b… - Bob - - - NodeId: 8e2b… - Home relay: us-east - - - - - - - - - iroh - 1a9c… - Alice - - - 8e2b… is at relay us-east - - - - - - - - - - - - - - - - - HTTPS PUT - Relay: us-east - Signed by: 8e2b… - - - - - - - - - DNS LOOKUP - TXT _iroh.8e2b….dns.iroh.link - - - - ;; ANSWER SECTION: - TXT "relay=https://us-east" - - - - - - - - diff --git a/images/how-iroh-works/routing-moves.svg b/images/how-iroh-works/routing-moves.svg deleted file mode 100644 index 357ba06..0000000 --- a/images/how-iroh-works/routing-moves.svg +++ /dev/null @@ -1,148 +0,0 @@ - - - - - - - - - - - 10.0.0.42 - - - - iroh - a4f7c0… - Alice - - - - - - 10.0.0.7 - 192.168.1.7 - 172.16.0.7 - - - - - iroh - 8e2b1d… - Bob - - - - - - - - - - - - - - - - - - - - - - - - home router - - 203.0.113.1 - - - - - - - 10.0.0.1 - - - - - - - mobile network - - 198.51.100.1 - - - - - - 192.168.1.1 - - - - - - - satellite internet - - 100.64.10.1 - - - 172.16.0.1 - - - - - - - - - - diff --git a/scripts/README.md b/scripts/README.md deleted file mode 100644 index 41f8254..0000000 --- a/scripts/README.md +++ /dev/null @@ -1,58 +0,0 @@ -# Diagram generators - -Python generators for the animated SVGs on -[`/what-is-iroh`](../what-is-iroh.mdx). Each `*.gen.py` writes one SVG into -`../images/how-iroh-works/`. - -Regenerate one: - -```sh -python3 connect-by-key.gen.py -``` - -Regenerate all: - -```sh -for f in *.gen.py; do python3 "$f"; done -``` - -Scripts are self-contained (stdlib only). `endpoint-startup.gen.py` additionally -reads `endpoint-startup.land.txt` (world-map path data). - -## SMIL only — no CSS animations - -The SVGs are embedded via ``. In production, Mintlify serves image -assets with the HTTP header `Content-Security-Policy: default-src 'none'`. With -no `style-src 'unsafe-inline'`, the browser **blocks the SVG's inline ` - - - - - - - - - 10.0.0.42 - -{chr(10).join(ip_rows)} - - -{alice_iphone()} - - -{bob_android()} - - - Alice - Bob - - - - {UNI_ANIM} - - connect - - - - {BI_ANIM} - - - - - - -''' - -open(OUT_PATH, "w").write(svg) -print(f"wrote {len(svg)} bytes -> {OUT_PATH}") diff --git a/scripts/csp_preview.py b/scripts/csp_preview.py deleted file mode 100644 index b4530e8..0000000 --- a/scripts/csp_preview.py +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env python3 -"""Local repro of the *production* serving conditions for the diagrams. - -`mint dev` serves SVGs with no CSP, so CSS animation works in preview but dies -on the deployed site, which sends `Content-Security-Policy: default-src 'none'`. -This server replays that exact header and embeds every diagram via (the -way Mintlify does), so you can confirm the SMIL animations survive it. - -Run: python3 scripts/csp_preview.py -Open: http://localhost:8000/ - -If an animation moves here, it will move on docs.iroh.computer. If it freezes -here but works in `mint dev`, it's still relying on CSS/ - - - -{wires} - -{packets} - -{relay(uwx, uwy, "us-west", ip_uw)} - -{relay(uex, uey, "us-east", ip_ue)} - -{relay(ewx, ewy, "eu-west", ip_ew)} - -{bob} - -{phone_ip} - -{home} - -{readout} - - - - - - -''' - -open(OUT_PATH, "w").write(svg) -print(f"wrote {len(svg)} bytes -> {OUT_PATH}") diff --git a/scripts/endpoint-startup.land.txt b/scripts/endpoint-startup.land.txt deleted file mode 100644 index a746ad2..0000000 --- a/scripts/endpoint-startup.land.txt +++ /dev/null @@ -1 +0,0 @@ -M 329,417 L 324,417 L 326,416 L 326,413 L 329,412 L 329,417 Z M 307,363 L 306,364 L 299,364 L 299,362 L 300,361 L 304,361 L 307,363 Z M 253,365 L 252,366 L 249,365 L 246,363 L 247,361 L 253,361 L 256,365 L 253,365 Z M 274,351 L 278,353 L 278,351 L 282,351 L 286,353 L 287,355 L 290,355 L 290,357 L 292,357 L 294,360 L 292,363 L 290,361 L 286,361 L 284,363 L 283,361 L 281,362 L 279,367 L 278,364 L 275,363 L 270,363 L 267,364 L 265,362 L 265,360 L 273,361 L 275,360 L 273,357 L 273,354 L 270,353 L 271,351 L 274,351 Z M 240,332 L 242,334 L 246,334 L 255,342 L 259,344 L 259,346 L 263,346 L 266,349 L 266,350 L 249,352 L 252,348 L 250,346 L 247,346 L 244,340 L 242,340 L 236,337 L 230,336 L 228,334 L 230,333 L 225,332 L 222,336 L 220,336 L 219,338 L 215,338 L 220,332 L 228,329 L 235,330 L 240,332 Z M 250,325 L 249,326 L 246,320 L 247,315 L 248,316 L 250,325 Z M 249,306 L 244,307 L 243,305 L 249,304 L 249,306 Z M 253,306 L 252,311 L 249,303 L 253,306 Z M 785,245 L 781,248 L 782,250 L 777,252 L 775,251 L 774,249 L 785,245 Z M 733,245 L 735,247 L 743,247 L 743,248 L 745,247 L 745,249 L 738,250 L 738,249 L 732,248 L 733,245 Z M 694,228 L 692,239 L 679,232 L 680,228 L 685,229 L 694,228 Z M 664,208 L 667,212 L 666,221 L 664,221 L 662,223 L 660,221 L 659,209 L 661,210 L 664,208 Z M 665,201 L 664,206 L 662,205 L 661,201 L 661,198 L 665,195 L 665,201 Z M 316,172 L 324,172 L 320,176 L 314,173 L 313,170 L 315,168 L 316,172 Z M 325,154 L 317,152 L 312,149 L 320,150 L 325,153 L 325,154 Z M 31,158 L 29,159 L 21,156 L 14,150 L 9,148 L 7,145 L 8,143 L 20,146 L 24,152 L 29,155 L 31,158 Z M 352,144 L 349,150 L 352,147 L 355,149 L 354,151 L 358,153 L 360,151 L 365,153 L 363,158 L 367,157 L 369,165 L 367,171 L 361,170 L 363,164 L 361,163 L 356,169 L 353,169 L 356,166 L 352,164 L 337,164 L 336,162 L 339,160 L 337,158 L 341,154 L 346,143 L 349,140 L 353,137 L 356,138 L 352,144 Z M -13,121 L -8,121 L -10,128 L -6,134 L -8,134 L -15,125 L -15,120 L -13,121 Z M 587,133 L 579,137 L 572,136 L 576,129 L 574,122 L 584,114 L 588,113 L 593,118 L 590,122 L 591,127 L 587,133 Z M 680,111 L 677,116 L 672,112 L 672,109 L 679,107 L 680,111 Z M 605,90 L 600,97 L 610,97 L 609,102 L 605,108 L 610,108 L 614,117 L 618,118 L 622,129 L 628,130 L 627,134 L 625,136 L 627,140 L 622,143 L 616,143 L 608,145 L 606,144 L 603,147 L 598,146 L 595,149 L 592,147 L 599,140 L 603,139 L 596,138 L 595,135 L 600,133 L 597,129 L 598,125 L 605,125 L 606,121 L 602,117 L 597,116 L 596,114 L 597,111 L 596,109 L 593,113 L 593,106 L 590,103 L 592,96 L 596,90 L 605,90 Z M 242,66 L 240,70 L 238,69 L 237,67 L 239,65 L 242,66 Z M 229,63 L 224,66 L 220,66 L 219,64 L 223,61 L 229,61 L 229,63 Z M 214,43 L 215,46 L 217,45 L 231,51 L 231,54 L 234,54 L 238,56 L 234,58 L 226,56 L 224,53 L 212,60 L 210,56 L 204,57 L 208,54 L 210,42 L 214,43 Z M 551,37 L 549,42 L 555,46 L 549,51 L 531,57 L 511,54 L 516,51 L 506,48 L 514,47 L 514,45 L 504,43 L 507,39 L 514,38 L 522,42 L 529,39 L 535,40 L 543,37 L 551,37 Z M 258,33 L 253,33 L 252,30 L 254,26 L 258,25 L 262,27 L 261,31 L 258,33 Z M 164,20 L 161,22 L 154,20 L 150,21 L 144,18 L 152,13 L 159,16 L 164,20 Z M 188,17 L 188,24 L 194,18 L 200,23 L 199,28 L 203,32 L 208,27 L 212,22 L 212,14 L 226,16 L 232,19 L 233,22 L 229,26 L 232,30 L 232,33 L 222,38 L 216,39 L 211,37 L 203,49 L 198,53 L 191,54 L 187,57 L 187,61 L 181,62 L 176,67 L 170,75 L 169,80 L 168,88 L 175,89 L 180,101 L 186,99 L 195,102 L 203,108 L 214,113 L 228,114 L 227,119 L 228,126 L 232,134 L 239,140 L 242,138 L 245,131 L 243,120 L 239,117 L 247,114 L 252,109 L 255,104 L 254,100 L 251,94 L 245,89 L 251,82 L 247,65 L 251,64 L 264,66 L 268,64 L 278,71 L 279,73 L 288,74 L 289,88 L 294,89 L 297,93 L 304,89 L 312,79 L 327,101 L 325,106 L 336,113 L 343,115 L 346,117 L 348,123 L 352,124 L 354,126 L 354,134 L 347,139 L 340,141 L 334,147 L 326,148 L 315,146 L 303,147 L 299,152 L 293,155 L 281,170 L 285,169 L 292,160 L 302,154 L 310,153 L 314,157 L 309,161 L 312,174 L 319,177 L 326,176 L 331,169 L 332,173 L 335,176 L 329,180 L 313,187 L 308,192 L 305,191 L 304,186 L 313,180 L 300,181 L 301,183 L 285,191 L 282,196 L 282,200 L 284,204 L 286,204 L 285,201 L 287,203 L 286,205 L 272,208 L 268,209 L 275,208 L 277,209 L 270,212 L 267,212 L 267,211 L 266,213 L 267,213 L 266,218 L 263,223 L 260,219 L 262,227 L 258,235 L 259,230 L 256,227 L 256,221 L 255,224 L 256,229 L 253,228 L 256,230 L 256,236 L 258,237 L 259,246 L 256,251 L 251,253 L 248,257 L 245,257 L 243,260 L 242,262 L 237,266 L 232,273 L 232,283 L 238,304 L 237,315 L 235,316 L 233,316 L 232,313 L 230,311 L 225,297 L 226,293 L 224,289 L 219,283 L 214,286 L 208,280 L 195,281 L 193,282 L 194,288 L 190,289 L 187,289 L 183,285 L 179,286 L 175,285 L 172,285 L 168,287 L 164,292 L 159,295 L 156,301 L 157,311 L 154,322 L 153,334 L 156,346 L 161,355 L 163,358 L 168,360 L 170,363 L 181,359 L 187,355 L 189,344 L 198,341 L 205,340 L 206,342 L 206,345 L 202,353 L 203,354 L 201,362 L 199,361 L 200,362 L 199,374 L 196,378 L 200,380 L 201,378 L 205,379 L 210,378 L 215,378 L 222,382 L 223,384 L 223,389 L 222,394 L 222,402 L 220,411 L 228,425 L 230,425 L 232,426 L 239,423 L 240,421 L 246,422 L 251,427 L 253,427 L 257,422 L 259,422 L 260,414 L 263,411 L 266,411 L 266,409 L 270,410 L 278,402 L 281,404 L 280,408 L 277,408 L 278,411 L 278,415 L 276,419 L 278,424 L 280,424 L 281,419 L 280,417 L 279,411 L 285,409 L 285,406 L 286,403 L 288,408 L 291,408 L 294,412 L 295,414 L 304,414 L 307,417 L 310,417 L 313,415 L 313,414 L 325,413 L 321,415 L 322,418 L 326,419 L 330,422 L 331,428 L 333,427 L 338,432 L 341,436 L 341,439 L 343,440 L 347,445 L 353,447 L 354,445 L 357,445 L 363,447 L 368,449 L 373,455 L 373,457 L 375,457 L 379,473 L 382,474 L 382,478 L 378,484 L 380,486 L 388,487 L 388,494 L 392,489 L 406,496 L 408,500 L 407,504 L 413,502 L 422,505 L 429,505 L 436,510 L 442,518 L 446,520 L 450,520 L 452,522 L 454,535 L 452,546 L 443,560 L 437,573 L 435,573 L 434,578 L 434,591 L 433,606 L 431,609 L 430,618 L 425,626 L 425,633 L 421,636 L 420,640 L 414,640 L 407,643 L 404,646 L 398,648 L 393,653 L 389,660 L 389,669 L 387,679 L 383,682 L 378,694 L 371,703 L 368,709 L 363,717 L 358,721 L 354,720 L 352,720 L 347,718 L 344,718 L 341,714 L 341,718 L 347,723 L 346,728 L 349,731 L 349,734 L 344,743 L 337,746 L 328,748 L 323,747 L 323,760 L 321,762 L 316,763 L 311,761 L 309,762 L 310,769 L 313,771 L 316,769 L 317,772 L 313,774 L 309,779 L 307,789 L 303,789 L 299,793 L 298,798 L 302,803 L 307,804 L 305,810 L 300,814 L 296,822 L 292,824 L 290,827 L 292,834 L 295,838 L 289,838 L 282,842 L 281,848 L 274,846 L 262,838 L 261,834 L 262,830 L 260,825 L 259,814 L 261,807 L 266,802 L 259,800 L 264,794 L 265,783 L 271,785 L 273,771 L 270,769 L 268,778 L 265,777 L 269,755 L 271,750 L 269,736 L 271,736 L 279,704 L 278,694 L 280,688 L 279,680 L 282,672 L 286,630 L 285,619 L 284,609 L 279,605 L 279,603 L 270,596 L 257,584 L 255,579 L 256,577 L 240,534 L 232,527 L 234,524 L 232,517 L 233,513 L 239,503 L 238,500 L 237,504 L 234,501 L 235,499 L 234,493 L 236,492 L 238,483 L 238,480 L 244,476 L 243,474 L 245,474 L 246,468 L 248,467 L 252,459 L 250,458 L 251,454 L 250,440 L 247,435 L 246,431 L 247,429 L 243,425 L 240,425 L 236,431 L 238,435 L 234,437 L 233,433 L 233,434 L 231,433 L 230,431 L 225,430 L 225,431 L 222,428 L 221,424 L 216,421 L 215,417 L 214,421 L 211,418 L 211,413 L 210,412 L 211,411 L 202,398 L 202,397 L 203,398 L 203,396 L 201,395 L 201,397 L 198,397 L 188,392 L 185,392 L 180,387 L 175,380 L 168,376 L 159,380 L 139,370 L 134,365 L 126,362 L 124,359 L 119,355 L 117,351 L 116,348 L 117,347 L 118,341 L 114,332 L 103,316 L 99,313 L 98,311 L 99,307 L 93,302 L 92,298 L 90,297 L 85,290 L 80,275 L 73,271 L 72,274 L 73,282 L 88,306 L 92,322 L 95,322 L 98,328 L 96,332 L 91,324 L 85,319 L 84,310 L 79,305 L 78,306 L 74,302 L 71,299 L 74,298 L 75,296 L 76,293 L 69,286 L 61,263 L 57,259 L 55,258 L 55,256 L 45,252 L 44,248 L 40,242 L 36,231 L 30,223 L 29,217 L 27,214 L 28,202 L 26,197 L 28,191 L 29,179 L 28,169 L 25,161 L 26,159 L 33,162 L 35,168 L 37,166 L 34,155 L 21,146 L 12,143 L 10,137 L 10,133 L 4,130 L 3,124 L -2,119 L -3,116 L -9,111 L -11,105 L -17,100 L -19,94 L -32,93 L -37,91 L -47,84 L -60,80 L -67,81 L -82,75 L -87,76 L -86,81 L -104,87 L -104,83 L -102,76 L -97,74 L -98,72 L -104,76 L -108,80 L -115,85 L -111,89 L -116,93 L -125,98 L -127,101 L -134,105 L -136,108 L -141,111 L -144,110 L -158,117 L -166,119 L -167,118 L -152,109 L -146,108 L -132,97 L -131,92 L -129,88 L -134,90 L -136,89 L -139,92 L -142,88 L -143,91 L -145,87 L -149,90 L -152,90 L -152,83 L -155,81 L -161,82 L -169,77 L -169,74 L -172,71 L -165,60 L -161,59 L -158,60 L -154,57 L -150,58 L -147,56 L -148,52 L -150,51 L -147,49 L -155,50 L -156,52 L -160,50 L -167,51 L -174,49 L -176,47 L -182,43 L -164,37 L -160,37 L -161,40 L -151,40 L -155,35 L -161,33 L -169,27 L -175,25 L -173,21 L -164,21 L -158,18 L -157,14 L -152,11 L -138,7 L -134,8 L -127,4 L -120,6 L -116,9 L -114,7 L -106,8 L -106,9 L -99,11 L -94,10 L -71,14 L -65,12 L -31,21 L -21,16 L -14,17 L 1,12 L 4,15 L 8,13 L 9,10 L 12,11 L 20,17 L 27,12 L 27,18 L 33,16 L 35,14 L 41,15 L 59,20 L 70,21 L 77,24 L 70,28 L 79,29 L 96,27 L 101,31 L 106,28 L 101,25 L 104,23 L 114,22 L 122,27 L 128,26 L 136,29 L 150,28 L 150,24 L 154,23 L 162,25 L 161,32 L 165,26 L 168,27 L 171,20 L 160,13 L 160,5 L 166,1 L 172,2 L 177,5 L 183,12 L 179,16 L 188,17 Z M 75,-8 L 73,-4 L 84,-6 L 90,-3 L 96,-6 L 100,-4 L 104,2 L 106,-0 L 103,-7 L 107,-8 L 112,-7 L 117,-5 L 122,7 L 138,13 L 138,16 L 130,17 L 133,19 L 131,22 L 115,19 L 100,22 L 80,23 L 77,20 L 70,18 L 66,19 L 60,14 L 84,11 L 75,9 L 58,10 L 55,7 L 66,5 L 59,5 L 51,3 L 58,-5 L 71,-9 L 75,-8 Z M 122,-10 L 117,-5 L 110,-10 L 118,-11 L 122,-10 Z M 256,-7 L 256,-6 L 241,-5 L 234,-9 L 234,-11 L 248,-11 L 256,-7 Z M 207,-8 L 211,-4 L 215,-9 L 227,-12 L 236,-5 L 235,-0 L 244,-2 L 249,-5 L 266,2 L 267,5 L 275,3 L 280,7 L 292,10 L 296,13 L 300,19 L 292,22 L 303,27 L 311,28 L 317,34 L 325,35 L 323,39 L 315,47 L 309,44 L 302,38 L 296,39 L 295,43 L 308,51 L 311,58 L 310,63 L 292,56 L 304,66 L 304,68 L 291,65 L 281,61 L 275,58 L 277,56 L 263,49 L 263,51 L 249,52 L 245,50 L 248,45 L 267,44 L 266,42 L 273,32 L 272,29 L 270,27 L 263,23 L 253,21 L 256,19 L 251,15 L 247,15 L 243,12 L 241,14 L 232,15 L 197,11 L 193,8 L 198,5 L 191,5 L 190,-2 L 193,-8 L 198,-10 L 211,-12 L 207,-8 Z M 141,-12 L 147,-11 L 156,-12 L 157,-10 L 152,-7 L 160,-4 L 159,2 L 151,5 L 146,4 L 131,-3 L 131,-6 L 141,-5 L 136,-9 L 141,-12 Z M 175,-5 L 170,-0 L 165,-0 L 162,-6 L 162,-10 L 164,-13 L 169,-14 L 188,-13 L 181,-7 L 175,-5 Z M 45,4 L 33,7 L 30,4 L 19,1 L 29,-11 L 24,-15 L 40,-17 L 59,-15 L 69,-10 L 51,-4 L 45,1 L 45,4 Z M 173,-20 L 171,-17 L 158,-20 L 161,-23 L 168,-25 L 173,-20 Z M 150,-32 L 154,-29 L 154,-25 L 152,-20 L 144,-20 L 139,-21 L 139,-25 L 131,-24 L 131,-29 L 136,-29 L 143,-31 L 150,-31 L 150,-32 Z M 104,-28 L 106,-26 L 115,-27 L 116,-23 L 113,-20 L 97,-19 L 85,-16 L 78,-16 L 77,-18 L 87,-21 L 65,-21 L 59,-22 L 65,-28 L 70,-30 L 83,-28 L 91,-24 L 100,-23 L 93,-30 L 97,-32 L 102,-32 L 104,-28 Z M 168,-34 L 174,-32 L 183,-32 L 187,-30 L 186,-27 L 195,-24 L 208,-23 L 216,-25 L 233,-25 L 238,-22 L 239,-20 L 229,-16 L 223,-17 L 200,-16 L 179,-19 L 177,-26 L 172,-29 L 162,-30 L 157,-32 L 159,-35 L 168,-34 Z M 66,-38 L 65,-33 L 61,-31 L 41,-26 L 34,-28 L 52,-37 L 66,-38 Z M 1130,-34 L 1131,-30 L 1135,-32 L 1149,-32 L 1160,-28 L 1164,-26 L 1163,-22 L 1141,-15 L 1154,-12 L 1159,-13 L 1161,-9 L 1163,-11 L 1171,-12 L 1186,-11 L 1187,-8 L 1207,-7 L 1207,-12 L 1225,-11 L 1233,-7 L 1235,-3 L 1232,0 L 1238,5 L 1246,8 L 1250,1 L 1258,4 L 1266,2 L 1275,4 L 1279,3 L 1287,3 L 1283,-3 L 1289,-6 L 1333,-1 L 1337,3 L 1349,8 L 1368,7 L 1378,8 L 1382,10 L 1381,15 L 1387,17 L 1393,16 L 1411,17 L 1420,16 L 1428,22 L 1434,20 L 1430,16 L 1432,13 L 1448,15 L 1458,14 L 1471,18 L 1478,20 L 1478,47 L 1472,50 L 1466,50 L 1470,53 L 1475,61 L 1475,64 L 1474,65 L 1465,64 L 1448,70 L 1434,79 L 1432,82 L 1425,77 L 1413,82 L 1410,80 L 1406,83 L 1399,82 L 1398,86 L 1392,93 L 1392,95 L 1398,97 L 1397,107 L 1393,107 L 1391,113 L 1393,116 L 1384,119 L 1383,127 L 1376,128 L 1374,135 L 1367,142 L 1361,112 L 1363,103 L 1367,99 L 1367,96 L 1375,94 L 1391,79 L 1400,73 L 1404,64 L 1398,64 L 1395,70 L 1383,77 L 1379,69 L 1367,71 L 1355,83 L 1359,87 L 1341,89 L 1341,84 L 1334,83 L 1328,87 L 1313,85 L 1298,87 L 1264,116 L 1271,117 L 1274,122 L 1278,123 L 1282,120 L 1287,120 L 1294,127 L 1294,133 L 1290,140 L 1288,159 L 1280,169 L 1279,173 L 1263,193 L 1256,197 L 1253,197 L 1250,194 L 1244,199 L 1243,201 L 1241,200 L 1239,203 L 1238,205 L 1238,210 L 1228,217 L 1227,221 L 1232,225 L 1237,237 L 1237,245 L 1235,249 L 1227,253 L 1223,254 L 1223,245 L 1221,238 L 1225,237 L 1221,231 L 1219,230 L 1217,231 L 1214,229 L 1217,225 L 1217,219 L 1212,216 L 1206,218 L 1202,221 L 1197,223 L 1199,220 L 1198,217 L 1202,213 L 1200,209 L 1190,216 L 1187,221 L 1183,221 L 1180,224 L 1183,229 L 1187,230 L 1187,233 L 1190,235 L 1196,230 L 1200,233 L 1203,233 L 1204,236 L 1197,238 L 1195,242 L 1190,245 L 1188,250 L 1193,254 L 1195,260 L 1201,272 L 1201,277 L 1198,279 L 1199,282 L 1202,284 L 1200,295 L 1197,296 L 1185,320 L 1172,332 L 1167,333 L 1164,336 L 1162,333 L 1160,337 L 1153,340 L 1148,341 L 1146,348 L 1144,349 L 1142,344 L 1144,341 L 1137,339 L 1129,346 L 1125,352 L 1124,357 L 1132,373 L 1136,377 L 1139,382 L 1141,395 L 1140,407 L 1131,416 L 1121,427 L 1119,423 L 1121,419 L 1117,415 L 1113,414 L 1109,403 L 1105,400 L 1100,400 L 1101,395 L 1097,395 L 1097,402 L 1092,418 L 1093,423 L 1096,423 L 1099,435 L 1101,439 L 1104,440 L 1111,448 L 1113,453 L 1113,467 L 1115,468 L 1117,477 L 1113,477 L 1103,467 L 1097,450 L 1098,445 L 1097,442 L 1089,429 L 1089,433 L 1088,429 L 1091,408 L 1089,404 L 1089,397 L 1087,393 L 1085,377 L 1083,371 L 1074,379 L 1069,377 L 1070,369 L 1069,363 L 1066,355 L 1066,352 L 1064,352 L 1060,346 L 1056,332 L 1051,332 L 1052,334 L 1050,338 L 1048,337 L 1047,338 L 1044,337 L 1044,339 L 1034,341 L 1035,346 L 1032,350 L 1025,354 L 1020,362 L 1012,371 L 1012,374 L 1003,378 L 1001,383 L 1003,398 L 1001,404 L 1000,416 L 998,416 L 996,421 L 997,423 L 993,425 L 989,432 L 985,425 L 981,409 L 977,400 L 975,387 L 970,378 L 967,356 L 966,341 L 959,345 L 956,345 L 950,337 L 952,334 L 950,331 L 945,326 L 941,324 L 940,319 L 936,314 L 913,316 L 893,312 L 891,304 L 889,302 L 881,307 L 875,305 L 870,299 L 865,298 L 859,282 L 856,283 L 853,281 L 851,284 L 848,283 L 852,299 L 859,305 L 859,311 L 862,319 L 862,314 L 863,310 L 864,309 L 866,311 L 865,319 L 867,323 L 870,322 L 877,323 L 888,307 L 889,317 L 891,322 L 893,324 L 900,327 L 905,335 L 899,348 L 896,347 L 895,349 L 895,358 L 893,358 L 890,360 L 888,365 L 885,365 L 883,367 L 883,369 L 881,371 L 878,371 L 870,375 L 868,380 L 856,386 L 852,391 L 848,391 L 846,394 L 837,396 L 834,400 L 827,400 L 823,383 L 824,383 L 823,372 L 816,360 L 815,354 L 810,348 L 806,342 L 806,333 L 803,326 L 798,322 L 796,313 L 787,296 L 785,296 L 786,287 L 781,299 L 778,294 L 774,284 L 776,292 L 782,309 L 790,324 L 789,330 L 796,337 L 798,360 L 803,364 L 807,378 L 816,388 L 826,402 L 826,405 L 823,406 L 827,409 L 830,415 L 832,415 L 842,413 L 850,409 L 855,408 L 863,404 L 863,414 L 856,440 L 847,457 L 842,466 L 825,483 L 818,497 L 815,499 L 814,502 L 812,503 L 809,515 L 807,517 L 804,525 L 805,529 L 808,532 L 807,543 L 813,558 L 814,584 L 811,594 L 808,598 L 798,604 L 786,619 L 785,624 L 788,635 L 789,634 L 789,648 L 775,659 L 775,662 L 777,662 L 773,679 L 769,684 L 763,695 L 754,706 L 751,709 L 743,712 L 743,714 L 740,713 L 737,715 L 732,713 L 727,714 L 713,720 L 708,715 L 707,716 L 707,714 L 705,705 L 707,704 L 707,699 L 692,668 L 688,646 L 688,635 L 683,626 L 680,614 L 676,607 L 676,592 L 679,577 L 685,567 L 685,558 L 681,547 L 683,543 L 677,519 L 665,500 L 662,493 L 665,479 L 664,478 L 666,465 L 665,460 L 662,459 L 660,453 L 652,457 L 648,457 L 644,448 L 640,443 L 629,444 L 610,454 L 606,452 L 598,451 L 592,452 L 584,456 L 582,456 L 577,453 L 565,440 L 561,436 L 558,433 L 557,425 L 550,417 L 549,412 L 545,408 L 543,408 L 541,403 L 540,394 L 538,389 L 536,386 L 538,385 L 541,377 L 541,373 L 543,363 L 542,350 L 538,344 L 539,338 L 542,333 L 544,325 L 548,320 L 551,308 L 554,306 L 557,299 L 560,296 L 564,296 L 574,284 L 573,275 L 575,266 L 578,261 L 587,255 L 591,244 L 595,244 L 598,247 L 602,247 L 609,248 L 614,245 L 619,243 L 627,239 L 643,237 L 645,238 L 650,235 L 660,236 L 665,234 L 668,234 L 668,238 L 672,235 L 673,237 L 670,240 L 670,243 L 672,245 L 671,251 L 668,254 L 669,258 L 672,258 L 673,261 L 675,262 L 680,264 L 686,265 L 692,268 L 695,274 L 706,278 L 711,281 L 715,277 L 714,271 L 716,268 L 719,265 L 722,264 L 729,265 L 731,268 L 739,270 L 740,273 L 746,272 L 758,277 L 763,273 L 767,273 L 771,274 L 772,277 L 773,275 L 781,277 L 785,273 L 786,264 L 791,252 L 791,247 L 792,244 L 790,241 L 792,238 L 789,239 L 785,237 L 782,241 L 775,242 L 771,238 L 766,238 L 765,241 L 761,242 L 757,238 L 752,238 L 749,232 L 745,228 L 748,223 L 745,219 L 750,213 L 757,213 L 759,208 L 768,208 L 774,204 L 780,202 L 787,202 L 803,209 L 812,209 L 818,205 L 819,203 L 817,198 L 795,180 L 798,179 L 802,174 L 799,171 L 806,168 L 806,167 L 791,171 L 786,173 L 787,178 L 789,179 L 794,179 L 793,181 L 788,182 L 781,186 L 779,185 L 780,182 L 774,180 L 780,176 L 779,175 L 771,173 L 771,171 L 766,171 L 761,180 L 761,182 L 759,183 L 757,183 L 756,191 L 753,194 L 752,198 L 754,205 L 758,207 L 757,209 L 751,209 L 745,215 L 744,210 L 739,209 L 733,211 L 736,215 L 734,216 L 731,216 L 729,213 L 728,214 L 731,221 L 729,223 L 734,228 L 734,232 L 730,230 L 731,233 L 728,234 L 730,240 L 727,240 L 723,237 L 720,227 L 715,216 L 712,214 L 713,204 L 710,200 L 696,192 L 692,187 L 693,187 L 691,184 L 691,182 L 688,180 L 686,183 L 685,181 L 685,179 L 686,178 L 682,177 L 679,179 L 678,185 L 680,188 L 684,192 L 687,197 L 692,203 L 696,203 L 697,204 L 696,205 L 703,210 L 708,215 L 707,217 L 704,214 L 700,213 L 698,217 L 702,220 L 701,223 L 699,224 L 697,229 L 695,230 L 697,223 L 693,215 L 685,208 L 681,207 L 677,204 L 670,196 L 668,189 L 662,186 L 651,195 L 641,193 L 635,195 L 634,203 L 630,207 L 624,209 L 618,220 L 620,224 L 618,227 L 617,232 L 613,233 L 610,238 L 599,238 L 594,243 L 592,243 L 589,236 L 584,235 L 582,237 L 580,236 L 577,237 L 578,227 L 575,227 L 574,224 L 578,211 L 577,198 L 575,195 L 582,190 L 599,193 L 611,193 L 613,189 L 614,175 L 606,165 L 598,162 L 598,157 L 604,156 L 612,157 L 611,150 L 615,153 L 626,147 L 628,142 L 638,137 L 642,127 L 649,125 L 653,125 L 654,123 L 658,123 L 658,125 L 662,121 L 658,111 L 658,104 L 660,100 L 665,100 L 670,96 L 669,102 L 669,104 L 672,105 L 671,107 L 669,107 L 666,111 L 667,117 L 672,119 L 672,121 L 679,118 L 687,123 L 704,116 L 709,117 L 709,118 L 713,118 L 715,116 L 721,113 L 720,103 L 723,98 L 727,96 L 731,101 L 735,101 L 736,92 L 734,93 L 731,90 L 731,86 L 743,84 L 753,84 L 759,81 L 754,78 L 745,78 L 729,82 L 726,78 L 721,76 L 722,69 L 720,63 L 722,59 L 727,55 L 741,46 L 740,44 L 734,40 L 726,42 L 721,47 L 722,51 L 705,62 L 701,72 L 709,80 L 705,88 L 700,90 L 698,101 L 695,107 L 690,107 L 687,112 L 681,112 L 680,106 L 672,89 L 669,84 L 660,92 L 653,94 L 647,90 L 644,68 L 648,63 L 661,58 L 670,51 L 690,28 L 711,15 L 722,12 L 730,12 L 737,7 L 745,7 L 754,5 L 769,10 L 763,12 L 768,16 L 773,14 L 781,18 L 794,20 L 812,27 L 816,31 L 816,35 L 811,39 L 803,40 L 781,35 L 778,36 L 786,41 L 786,51 L 796,55 L 797,52 L 794,49 L 797,46 L 809,50 L 813,49 L 809,44 L 820,37 L 825,38 L 829,40 L 832,35 L 828,31 L 830,27 L 827,23 L 840,25 L 843,29 L 837,30 L 837,34 L 841,36 L 848,34 L 849,30 L 876,21 L 879,22 L 875,26 L 881,26 L 884,24 L 893,24 L 900,21 L 906,25 L 911,21 L 906,17 L 908,14 L 923,17 L 946,26 L 950,23 L 945,19 L 945,18 L 939,17 L 940,14 L 938,7 L 947,0 L 950,-6 L 953,-7 L 966,-5 L 967,-1 L 962,4 L 965,6 L 967,11 L 966,20 L 971,24 L 969,29 L 960,38 L 965,39 L 967,37 L 972,35 L 973,32 L 978,29 L 975,25 L 977,20 L 972,20 L 971,16 L 974,9 L 968,4 L 977,-1 L 976,-6 L 978,-6 L 981,-2 L 979,4 L 984,6 L 982,1 L 990,-2 L 1000,-2 L 1008,2 L 1004,-4 L 1004,-11 L 1012,-12 L 1034,-13 L 1030,-17 L 1035,-21 L 1041,-21 L 1050,-25 L 1063,-25 L 1064,-27 L 1077,-28 L 1081,-26 L 1091,-30 L 1100,-30 L 1101,-33 L 1106,-36 L 1117,-38 L 1125,-36 L 1119,-35 L 1130,-34 Z M 854,207 L 856,212 L 859,212 L 860,214 L 856,215 L 854,222 L 853,224 L 854,232 L 859,233 L 862,237 L 869,238 L 876,236 L 877,223 L 873,221 L 874,216 L 871,216 L 872,210 L 877,211 L 881,209 L 877,205 L 876,201 L 872,203 L 872,208 L 870,202 L 871,199 L 870,197 L 865,195 L 862,189 L 860,187 L 860,185 L 864,185 L 864,180 L 868,179 L 873,180 L 873,174 L 873,170 L 868,170 L 864,168 L 854,173 L 852,177 L 847,178 L 842,185 L 847,191 L 846,196 L 854,207 Z M 172,-37 L 161,-37 L 160,-39 L 170,-39 L 173,-38 L 172,-37 Z M 94,-38 L 86,-36 L 78,-39 L 82,-41 L 89,-41 L 96,-40 L 94,-38 Z M 738,-39 L 727,-37 L 719,-38 L 722,-40 L 719,-42 L 729,-44 L 731,-41 L 738,-39 Z M 163,-41 L 156,-39 L 152,-41 L 150,-44 L 150,-46 L 158,-46 L 164,-43 L 163,-41 Z M 143,-43 L 145,-40 L 129,-43 L 118,-43 L 123,-45 L 117,-47 L 117,-49 L 139,-46 L 143,-43 Z M 707,-52 L 722,-47 L 710,-44 L 708,-39 L 704,-38 L 701,-32 L 696,-32 L 685,-36 L 690,-39 L 683,-41 L 673,-46 L 670,-52 L 683,-54 L 685,-52 L 692,-52 L 694,-54 L 701,-54 L 707,-52 Z M 293,-75 L 316,-73 L 325,-72 L 325,-70 L 297,-64 L 308,-64 L 289,-58 L 280,-53 L 268,-50 L 253,-49 L 260,-49 L 256,-47 L 260,-44 L 240,-35 L 240,-34 L 248,-34 L 248,-32 L 236,-28 L 223,-30 L 209,-29 L 193,-30 L 193,-33 L 201,-35 L 199,-40 L 202,-40 L 215,-37 L 208,-42 L 200,-43 L 204,-46 L 213,-47 L 214,-50 L 207,-52 L 205,-56 L 222,-55 L 230,-57 L 202,-57 L 194,-60 L 184,-64 L 183,-67 L 205,-69 L 212,-72 L 218,-71 L 223,-70 L 227,-73 L 242,-75 L 293,-75 Z M 491,-78 L 520,-72 L 512,-70 L 468,-69 L 470,-68 L 487,-68 L 501,-66 L 511,-68 L 515,-66 L 509,-62 L 545,-67 L 559,-66 L 562,-63 L 539,-56 L 524,-55 L 535,-55 L 526,-46 L 526,-38 L 532,-34 L 524,-33 L 516,-31 L 525,-28 L 526,-22 L 521,-21 L 527,-15 L 517,-15 L 522,-12 L 521,-10 L 507,-9 L 513,-4 L 513,-1 L 504,-4 L 502,-2 L 508,-1 L 514,4 L 516,9 L 508,10 L 498,4 L 500,8 L 494,12 L 513,13 L 487,24 L 468,26 L 463,29 L 457,36 L 446,41 L 430,44 L 426,48 L 426,53 L 423,57 L 416,63 L 418,68 L 413,80 L 406,81 L 399,75 L 390,75 L 385,71 L 382,65 L 374,56 L 371,52 L 371,46 L 364,40 L 366,35 L 362,32 L 367,25 L 374,22 L 376,19 L 377,14 L 365,18 L 359,16 L 359,12 L 361,8 L 375,10 L 362,3 L 358,4 L 354,2 L 359,-4 L 346,-18 L 340,-21 L 340,-24 L 328,-28 L 293,-27 L 279,-34 L 301,-36 L 281,-38 L 270,-41 L 271,-43 L 306,-50 L 308,-52 L 295,-55 L 300,-57 L 316,-62 L 323,-63 L 321,-66 L 347,-69 L 362,-69 L 367,-67 L 380,-70 L 408,-65 L 396,-69 L 397,-72 L 413,-76 L 430,-75 L 436,-78 L 491,-78 Z \ No newline at end of file diff --git a/scripts/hole-punching-lan.gen.py b/scripts/hole-punching-lan.gen.py deleted file mode 100644 index fbd464c..0000000 --- a/scripts/hole-punching-lan.gen.py +++ /dev/null @@ -1,366 +0,0 @@ -#!/usr/bin/env python3 -"""Generator for hole-punching-lan.svg ("Direct connections on the same network"). - -The easy, very common case: Alice and Bob are two devices on the *same* home -network, behind the *same* router. They still discover each other over the relay -(ADD_ADDRESS / REACH_OUT), but then the *local* candidate addresses are directly -reachable across the LAN — so the very first PATH_CHALLENGE to the local address -succeeds. No NAT hole to punch, no sacrificial probe, no public-address probes. - -Layout is a "Y": the relay sits on top of the stem (the router's uplink), the -router is the junction, Alice and Bob hang off the two legs. Data first flows up -the stem to the relay and back; once the LAN path validates, the stem fades to -gray and the two legs (the direct LAN path) carry everything. - -Same look as the other how-iroh-works figures. - -Edit this script and run: python3 hole-punching-lan.gen.py -Writes ../images/how-iroh-works/hole-punching-lan.svg. -The MDX embeds this as ; the -intrinsic aspect ratio comes from the SVG viewBox (VB_W x VB_H). -""" - -import os - -HERE = os.path.dirname(os.path.abspath(__file__)) -OUT_PATH = os.path.normpath(os.path.join( - HERE, "..", "images", "how-iroh-works", "hole-punching-lan.svg")) - -MONO = "'Space Mono', monospace" -INDIGO, AMBER, GRAY, INK, BLUE, RED, GREEN = "#6366f1", "#d97706", "#888", "#111", "#2563eb", "#dc2626", "#15803d" -FADED = "#aab2bd" # inactive/secondary connection — clearly lighter than the structure gray -CYCLE = 27.0 -T_POPUP_FADE = 23.5 # popups fade a little after the LAN path is validated - - -def anim_opacity(points): - times = ";".join(f"{t/CYCLE:.4f}" for t, _ in points) - vals = ";".join(str(v) for _, v in points) - return (f'') - - -def anim_motion(wire_id, points): - times = ";".join(f"{t/CYCLE:.4f}" for t, _ in points) - kp = ";".join(str(k) for _, k in points) - return (f'') - - -def dot(color, opa_pts, mot_pts, wire): - return f''' - - {anim_opacity(opa_pts)} - {anim_motion(wire, mot_pts)} - ''' - - -def key_label(cx, y, text, size, color): - """A tiny drawn gold key (bow + shaft + teeth) + monospace label, centered on cx - (replaces the 🔑 emoji, which no pure-vector renderer can draw).""" - s = size - gold = "#eab308" - w = s * 1.3 # key glyph width - gap = s * 0.3 - xl = cx - (w + gap + s * 0.6 * len(text)) / 2 - cy = y - s * 0.30 - rb, rh = s * 0.34, s * 0.15 # bow outer / hole radius - bx = xl + rb # bow center x - hs = s * 0.18 # shaft thickness - xr = xl + w # right end - top, bot = cy - hs / 2, cy + hs / 2 - tw = s * 0.13 # tooth width - mono = "'Space Mono', monospace" - return ( - f'' - f'' - f'' - f'' - f'{text}' - ) - - -def iphone_screen(px0, py0, pw, ph, indigo): - """iPhone-style screen outline path (with notch) sized to fit inside an outer body of pw x ph.""" - sx0, sx1 = px0+3, px0+pw-3 - sy0, sy1 = py0+3, py0+ph-3 - cx = px0 + pw/2 - no = 7 # outer notch half-width - ni = 4 # inner notch half-width - nd = 5 # notch depth - ny = sy0 + nd - r = 6 # screen corner radius - return ( - f'' - ) - -def phone(px0, py0, keytext, iphone=False): - pw, ph = 50, 92 - cx = px0+pw/2 - if iphone: - screen = iphone_screen(px0, py0, pw, ph, INDIGO) - else: - screen = f'\n ' - return f''' - {screen} - - iroh - {key_label(cx, py0+60, keytext, 8, AMBER)}''' - - -def router_lan(cx, cy, lan_ip): - # single home router; the stem (uplink to the relay) leaves the top center, - # so the LAN gateway IP goes below and there is no label above. - bx, by, bw, bh = cx-30, cy-20, 60, 40 - return f''' - - - - - - - {lan_ip} - ''' - - -def relay(cx, cy, label): - bx, by, bw, bh = cx-32, cy-9, 64, 18 - s = [' ', - f' ', - f' ', - f' ', - f' '] - for i in range(6): - vx = bx+24+i*5 - s.append(f' ') - s.append(f' {label}') - s.append(' ') - return "\n".join(s) - - -def facts(x, lines, anchor="start"): - out = [f' '] - for y, label, val in lines: - out.append(f' {label} {val}') - out.append(' ') - return "\n".join(out) - - -# ---- positions (a "Y": relay on top, router at the junction, phones on the legs) ---- -RELAY = (440, 66) -ROUTER = (440, 200) -A_PH = (175, 312) # Alice phone top-left -B_PH = (655, 312) # Bob phone top-left -acx, bcx = 200, 680 -PH = 92 -JX, JY = 440, 220 # junction (router bottom center) where the legs meet the stem -RELAY_B = 78 # relay box bottom - -# The legs curve: they leave each phone vertically and arrive at the router vertically -# (from below). They enter the router box at two *separate* points tucked inside it, so -# the legs never cross; the vertical stem leaves the box top center up to the relay. -DV, DV2 = 64, 88 # control-point offsets: vertical out of the phone / into the router -OJ = 16 # legs enter the box this far either side of center -JYH = 213 # internal junction, hidden inside the router box -JXA, JXB = JX-OJ, JX+OJ -_legA = f"C {acx} {A_PH[1]-DV} {JXA} {JYH+DV2} {JXA} {JYH}" -_legB = f"C {bcx} {B_PH[1]-DV} {JXB} {JYH+DV2} {JXB} {JYH}" -_legA_r = f"C {JXA} {JYH+DV2} {acx} {A_PH[1]-DV} {acx} {A_PH[1]}" -_legB_r = f"C {JXB} {JYH+DV2} {bcx} {B_PH[1]-DV} {bcx} {B_PH[1]}" - -# stem (uplink to the relay); lan path (Alice -> router -> Bob) -stem_d = f"M {JX} {JYH} L {JX} {RELAY_B}" -lan_d = f"M {acx} {A_PH[1]} {_legA} L {JXB} {JYH} {_legB_r}" -# relay round-trips: up the stem to the relay and back down (the latency made visible) -relay_b2a = f"M {bcx} {B_PH[1]} {_legB} L {JX} {JYH} L {JX} {RELAY_B} L {JX} {JYH} L {JXA} {JYH} {_legA_r}" -relay_a2b = f"M {acx} {A_PH[1]} {_legA} L {JX} {JYH} L {JX} {RELAY_B} L {JX} {JYH} L {JXB} {JYH} {_legB_r}" - -alice_facts = facts(A_PH[0]+62, [ - (A_PH[1]+18, "EndpointId:", "1a9c…"), - (A_PH[1]+36, "Addr:", "192.168.0.3:2104"), - (A_PH[1]+54, "Addr:", "4.9.8.2:2104"), - (A_PH[1]+72, "relay:", "us-east"), -]) -bob_facts = facts(B_PH[0]+62, [ - (B_PH[1]+18, "EndpointId:", "8e2b…"), - (B_PH[1]+36, "Addr:", "192.168.0.5:4153"), - (B_PH[1]+54, "Addr:", "4.9.8.2:4153"), - (B_PH[1]+72, "relay:", "us-east"), -]) - -# ---- beat 1: ADD_ADDRESS (Bob advertises his candidates to Alice, over the relay) ---- -addaddr_pkt = dot(BLUE, - [(0, 0), (1.0, 0), (1.3, 1), (4.3, 1), (4.6, 0), (CYCLE, 0)], - [(0, 1), (1.0, 1), (4.5, 0), (CYCLE, 0)], "relay-b2a") -addaddr_co_pts = [(0, 0), (1.0, 0), (1.4, 1), (4.4, 1), (4.8, 0), (CYCLE, 0)] -addaddr_co = f''' - {anim_opacity(addaddr_co_pts)} - - ADD_ADDRESS 192.168.0.5:4153 - ADD_ADDRESS 4.9.8.2:4153 - ''' - -# Alice now knows Bob's candidates — ~2s popup beside Alice -ak_pts = [(0, 0), (4.4, 0), (4.7, 1), (6.6, 1), (6.9, 0), (CYCLE, 0)] -alice_knows = f''' - {anim_opacity(ak_pts)} - - - - - Bob - Candidate: 192.168.0.5:4153 - Candidate: 4.9.8.2:4153 - ''' - -# ---- beat 2: REACH_OUT (Alice -> Bob over the relay), carries Alice's candidates ---- -reachout_pkt = dot(BLUE, - [(0, 0), (7.5, 0), (7.8, 1), (10.8, 1), (11.1, 0), (CYCLE, 0)], - [(0, 0), (7.5, 0), (11.0, 1), (CYCLE, 1)], "relay-a2b") -reachout_co_pts = [(0, 0), (7.5, 0), (7.9, 1), (10.9, 1), (11.2, 0), (CYCLE, 0)] -reachout_co = f''' - {anim_opacity(reachout_co_pts)} - - REACH_OUT192.168.0.3:2104 - REACH_OUT4.9.8.2:2104 - ''' - -# Bob now knows Alice's candidates — ~2s popup beside Bob -bk_pts = [(0, 0), (11.0, 0), (11.3, 1), (13.2, 1), (13.5, 0), (CYCLE, 0)] -bob_knows = f''' - {anim_opacity(bk_pts)} - - - - - Alice - Candidate: 192.168.0.3:2104 - Candidate: 4.9.8.2:2104 - ''' - -# ============================ LAN hole punch (no NAT in the way) ============================ -# Same subnet, so the local candidate is directly reachable: the first PATH_CHALLENGE to the -# local address gets through. No sacrificial probe, no public probes, no drops. -# Alice is the client → she probes first. Each step gets a clear window. -T_C1 = 14.2 # Alice PATH_CHALLENGE -> Bob (local) — gets through -T_C2 = 16.8 # Bob PATH_CHALLENGE + PATH_RESPONSE -> Alice -T_R = 19.4 # Alice PATH_RESPONSE -> Bob — both directions validated -T_BLUE = T_R + 2.2 - - -def hp_pkt(t0, dur, kp0, kp1, color=BLUE): - arr = t0 + dur - return f''' - - {anim_opacity([(0, 0), (t0, 0), (t0+0.15, 1), (arr-0.12, 1), (arr+0.06, 0), (CYCLE, 0)])} - {anim_motion("lan", [(0, kp0), (t0, kp0), (arr, kp1), (CYCLE, kp1)])} - ''' - - -def callout(side, lines, t0, t1, w=170): - x = 232 if side == "L" else (648 - w) - h = 18 + len(lines)*19 - pts = [(0, 0), (t0-0.3, 0), (t0, 1), (t1, 1), (t1+0.3, 0), (CYCLE, 0)] - rows = "\n".join( - f' {txt}' - for i, (txt, bold) in enumerate(lines)) - return f''' - {anim_opacity(pts)} - -{rows} - ''' - - -# beat 3 — Alice's PATH_CHALLENGE to Bob's local address: directly reachable, gets through -c1_pkt = hp_pkt(T_C1, 2.0, 0, 1) -c1_co = callout("L", [("PATH_CHALLENGE", True), ("192.168.0.5:4153", False)], T_C1-0.3, T_C1+2.1) - -# beat 4 — Bob answers: PATH_RESPONSE (to Alice's challenge) + his own PATH_CHALLENGE -c2_pkt = hp_pkt(T_C2, 2.0, 1, 0) -c2_co = callout("R", [("PATH_RESPONSE", True), ("PATH_CHALLENGE", True), ("192.168.0.3:2104", False)], T_C2-0.3, T_C2+2.1) - -# beat 5 — Alice's PATH_RESPONSE to Bob — both directions validated -r_pkt = hp_pkt(T_R, 2.0, 0, 1) -r_co = callout("L", [("PATH_RESPONSE", True)], T_R-0.3, T_R+2.1, w=150) - -# the stem (relay uplink) is blue/active until the LAN path validates, then fades to gray -stem_stroke = (f'') - -# "direct" confirmation appears when the path is validated -direct_pts = [(0, 0), (T_BLUE, 0), (T_BLUE+0.4, 1), (CYCLE, 1)] -direct_badge = f''' - {anim_opacity(direct_pts)} - - direct (LAN) - ''' - -VB_X, VB_Y, VB_W, VB_H = 0, 30, 940, 430 -svg = f''' - - - {stem_stroke} - - - - - - -{relay(*RELAY, "relay")} - -{router_lan(*ROUTER, "192.168.0.1")} - - - -{phone(*A_PH, "1a9c…", iphone=True)} - Alice - -{alice_facts} - - - -{phone(*B_PH, "8e2b…")} - Bob - -{bob_facts} - - -{addaddr_pkt} -{reachout_pkt} -{c1_pkt} -{c2_pkt} -{r_pkt} -{direct_badge} - - -{addaddr_co} -{alice_knows} -{reachout_co} -{bob_knows} -{c1_co} -{c2_co} -{r_co} - - - - - - -''' - -open(OUT_PATH, "w").write(svg) -print(f"wrote {len(svg)} bytes -> {OUT_PATH}") diff --git a/scripts/hole-punching.gen.py b/scripts/hole-punching.gen.py deleted file mode 100644 index 6f88316..0000000 --- a/scripts/hole-punching.gen.py +++ /dev/null @@ -1,469 +0,0 @@ -#!/usr/bin/env python3 -"""Generator for hole-punching.svg ("Establishing direct connections"). - -Initial state of the hole-punching story: Alice and Bob, each behind a normal -home router, are connected *through the relay* (us-east) — all data flows up to -the relay and back down. Later steps (CallMeMaybe, address discovery, the ping, -and the direct path) build on this. - -Continues the discovery story: Bob = 8e2b…, home relay us-east; Alice = 1a9c…. -Same look as the other how-iroh-works figures; all connections blue. - -Edit this script and run: python3 hole-punching.gen.py -Writes ../images/how-iroh-works/hole-punching.svg. -The MDX embeds this as ; the -intrinsic aspect ratio comes from the SVG viewBox (VB_W x VB_H). -""" - -import os - -HERE = os.path.dirname(os.path.abspath(__file__)) -OUT_PATH = os.path.normpath(os.path.join( - HERE, "..", "images", "how-iroh-works", "hole-punching.svg")) - -MONO = "'Space Mono', monospace" -INDIGO, AMBER, GRAY, INK, BLUE, RED, GREEN = "#6366f1", "#d97706", "#888", "#111", "#2563eb", "#dc2626", "#15803d" -FADED = "#aab2bd" # inactive/secondary connection — clearly lighter than the structure gray -CYCLE = 39.0 # one loop; popups fade after both checks are green, then the direct path stands alone -T_POPUP_FADE = 29.5 # NAT bubbles fade out here (a few seconds after both mappings are validated) - - -def anim_opacity(points): - times = ";".join(f"{t/CYCLE:.4f}" for t, _ in points) - vals = ";".join(str(v) for _, v in points) - return (f'') - - -def anim_motion(wire_id, points): - times = ";".join(f"{t/CYCLE:.4f}" for t, _ in points) - kp = ";".join(str(k) for _, k in points) - return (f'') - - -def dot(color, opa_pts, mot_pts, wire="relay-path"): - return f''' - - {anim_opacity(opa_pts)} - {anim_motion(wire, mot_pts)} - ''' - - -def key_label(cx, y, text, size, color): - """A tiny drawn gold key (bow + shaft + teeth) + monospace label, centered on cx - (replaces the 🔑 emoji, which no pure-vector renderer can draw).""" - s = size - gold = "#eab308" - w = s * 1.3 # key glyph width - gap = s * 0.3 - xl = cx - (w + gap + s * 0.6 * len(text)) / 2 - cy = y - s * 0.30 - rb, rh = s * 0.34, s * 0.15 # bow outer / hole radius - bx = xl + rb # bow center x - hs = s * 0.18 # shaft thickness - xr = xl + w # right end - top, bot = cy - hs / 2, cy + hs / 2 - tw = s * 0.13 # tooth width - mono = "'Space Mono', monospace" - return ( - f'' - f'' - f'' - f'' - f'{text}' - ) - - -def iphone_screen(px0, py0, pw, ph, indigo): - """iPhone-style screen outline path (with notch) sized to fit inside an outer body of pw x ph.""" - sx0, sx1 = px0+3, px0+pw-3 - sy0, sy1 = py0+3, py0+ph-3 - cx = px0 + pw/2 - no = 7 # outer notch half-width - ni = 4 # inner notch half-width - nd = 5 # notch depth - ny = sy0 + nd - r = 6 # screen corner radius - return ( - f'' - ) - -def phone(px0, py0, keytext, iphone=False): - pw, ph = 50, 92 - cx = px0+pw/2 - if iphone: - screen = iphone_screen(px0, py0, pw, ph, INDIGO) - else: - screen = f'\n ' - return f''' - {screen} - - iroh - {key_label(cx, py0+60, keytext, 8, AMBER)}''' - - -def router(cx, cy, pub_ip, lan_ip): - # same design as the "Guaranteed connections" diagram: public IP above, - # L-shaped antennas, body, LAN IP below - bx, by, bw, bh = cx-30, cy-20, 60, 40 - return f''' - {pub_ip} - - - - - - - {lan_ip} - ''' - - -def relay(cx, cy, label): - bx, by, bw, bh = cx-32, cy-9, 64, 18 - s = [' ', - f' ', - f' ', - f' ', - f' '] - for i in range(6): - vx = bx+24+i*5 - s.append(f' ') - s.append(f' {label}') - s.append(' ') - return "\n".join(s) - - -def facts(x, lines, anchor="start"): - out = [f' '] - for y, label, val in lines: - out.append(f' {label} {val}') - out.append(' ') - return "\n".join(out) - - -# ---- positions ---- -RELAY = (440, 66) -R1 = (200, 250) # Alice's router -R2 = (680, 250) # Bob's router -A_PH = (175, 312) # Alice phone top-left -B_PH = (655, 312) # Bob phone top-left -acx, bcx = 200, 680 -PH = 92 - -# relay legs: Alice <-> relay (attach left of box) and relay <-> Bob (attach right of box), -# so the "relay" label below the box stays clear -# one continuous relay pipe: rises through each router, flattens and passes -# horizontally *through* the relay (in the left edge, out the right), then drops. -# The relay clearly sits on the pipe; the "relay" label stays clear below it. -rx, ry = RELAY -relay_d = (f"M {acx} {A_PH[1]} L {acx} {R1[1]-20} " - f"C {acx} 120 {rx-160} {ry} {rx-32} {ry} " - f"L {rx+32} {ry} " - f"C {rx+160} {ry} {bcx} 120 {bcx} {R2[1]-20} " - f"L {bcx} {B_PH[1]}") - -alice_facts = facts(A_PH[0]+62, [ - (A_PH[1]+18, "EndpointId:", "1a9c…"), - (A_PH[1]+36, "Addr:", "10.0.0.3:2104"), - (A_PH[1]+54, "Addr:", "8.3.1.9:2104"), - (A_PH[1]+72, "relay:", "us-west"), -]) -bob_facts = facts(B_PH[0]+62, [ - (B_PH[1]+18, "EndpointId:", "8e2b…"), - (B_PH[1]+36, "Addr:", "192.168.0.3:4153"), - (B_PH[1]+54, "Addr:", "4.9.8.2:4153"), - (B_PH[1]+72, "relay:", "us-east"), -]) - -# ---- beat 1: ADD_ADDRESS (Bob advertises his candidate addresses to Alice, over the relay) ---- -# one packet carrying both ADD_ADDRESS frames travels Bob -> relay -> Alice. Not time-critical, -# and it does nothing to the routers' forwarding tables. -addaddr_pkt = dot(BLUE, - [(0, 0), (1.0, 0), (1.3, 1), (3.9, 1), (4.2, 0), (CYCLE, 0)], - [(0, 1), (1.0, 1), (4.0, 0), (CYCLE, 0)]) -addaddr_co_pts = [(0, 0), (1.0, 0), (1.4, 1), (4.0, 1), (4.4, 0), (CYCLE, 0)] -addaddr_co = f''' - {anim_opacity(addaddr_co_pts)} - - ADD_ADDRESS 192.168.0.3:4153 - ADD_ADDRESS 4.9.8.2:4153 - ''' - -# when the ADD_ADDRESS packet lands, Alice now knows Bob's candidates — ~2s popup -# beside Alice (briefly covering her own info panel) -ak_pts = [(0, 0), (4.0, 0), (4.3, 1), (6.2, 1), (6.5, 0), (CYCLE, 0)] -alice_knows = f''' - {anim_opacity(ak_pts)} - - - - - Bob - Candidate: 192.168.0.3:4153 - Candidate: 4.9.8.2:4153 - ''' - -# ---- beat 2: REACH_OUT (Alice -> Bob over the relay) — carries Alice's candidate addresses -# and is the trigger that starts probing; shown here decoupled from the probes for clarity ---- -reachout_pkt = dot(BLUE, - [(0, 0), (6.8, 0), (7.1, 1), (9.2, 1), (9.4, 0), (CYCLE, 0)], - [(0, 0), (6.8, 0), (9.3, 1), (CYCLE, 1)]) -reachout_co_pts = [(0, 0), (6.8, 0), (7.1, 1), (9.3, 1), (9.6, 0), (CYCLE, 0)] -reachout_co = f''' - {anim_opacity(reachout_co_pts)} - - REACH_OUT10.0.0.3:2104 - REACH_OUT8.3.1.9:2104 - ''' - -# Bob now knows Alice's candidates — ~2s popup beside Bob (toward center) -bk_pts = [(0, 0), (9.3, 0), (9.6, 1), (11.6, 1), (11.9, 0), (CYCLE, 0)] -bob_knows = f''' - {anim_opacity(bk_pts)} - - - - - Alice - Candidate: 10.0.0.3:2104 - Candidate: 8.3.1.9:2104 - I need to reach out - ''' - -# ============================ hole punch (Alice is the client → she probes first) ============================ -# Locals fail (not routable). Then the public chain on ONE shared direct route: Alice's sacrificial -# probe opens her hole but is rejected at Bob's NAT; Bob's probe opens his hole and gets through; -# Alice replies PATH_RESPONSE+PATH_CHALLENGE; Bob replies PATH_RESPONSE → both paths validated. -# No timeouts. Each step gets a clear window before the next. -T_AL, T_BL = 12.4, 14.6 # local probes (fail) -T_AP = 16.9 # Alice public — sacrificial -T_BP = 19.9 # Bob public — succeeds -T_AR = 22.9 # Alice reply: PATH_RESPONSE + PATH_CHALLENGE -T_BR = 25.7 # Bob reply: PATH_RESPONSE → validated -T_BLUE = T_BR + 2.4 # direct path turns blue once the response validates it - -DROP_A, DROP_B = (acx, 165), (bcx, 165) # local drops — straight up above each router (no bend toward the other side, which read as if the packet was trying to reach the peer). Kept high enough that the "not routable" label clears the router's public IP below. -probe_local_a_d = f"M {acx} {A_PH[1]} L {acx} {DROP_A[1]}" -probe_local_b_d = f"M {bcx} {B_PH[1]} L {bcx} {DROP_B[1]}" - -# the one direct (internet) route, Alice(0) -> Bob(1). Every public packet rides it. -ARC = 158 -direct_d = f"M {acx} {A_PH[1]} L {acx} {R1[1]-20} C {acx} {ARC} {bcx} {ARC} {bcx} {R2[1]-20} L {bcx} {B_PH[1]}" - - -def cubic_len(p0, p1, p2, p3, n=120): - def b(t, a, bb, c, d): return (1-t)**3*a + 3*(1-t)**2*t*bb + 3*(1-t)*t*t*c + t**3*d - prev, L = p0, 0.0 - for i in range(1, n+1): - t = i/n - cur = (b(t, p0[0], p1[0], p2[0], p3[0]), b(t, p0[1], p1[1], p2[1], p3[1])) - L += ((cur[0]-prev[0])**2 + (cur[1]-prev[1])**2)**0.5 - prev = cur - return L - - -_seg = A_PH[1] - (R1[1]-20) -_arc = cubic_len((acx, R1[1]-20), (acx, ARC), (bcx, ARC), (bcx, R2[1]-20)) -F_BR = (_seg + _arc) / (_seg + _arc + _seg) # fraction at Bob's router — where the sacrificial bounces -REJECT = (bcx, R2[1]-20) # Bob's router WAN — sacrificial probe rejected here - - -def hp_pkt(wire, t0, dur, kp0=0, kp1=1, color=BLUE): - arr = t0 + dur - return f''' - - {anim_opacity([(0, 0), (t0, 0), (t0+0.15, 1), (arr-0.12, 1), (arr+0.06, 0), (CYCLE, 0)])} - {anim_motion(wire, [(0, kp0), (t0, kp0), (arr, kp1), (CYCLE, kp1)])} - ''' - - -def drop_x(p, t_arr, label, hold=1.9, dx=0): - pts = [(0, 0), (t_arr-0.05, 0), (t_arr+0.15, 1), (t_arr+hold, 1), (t_arr+hold+0.3, 0), (CYCLE, 0)] - if dx: # label to the side instead of below - lbl = f'{label}' - else: - lbl = f'{label}' - return f''' - {anim_opacity(pts)} - - - {lbl} - ''' - - -def callout(side, lines, t0, t1, w=164): - x = 240 if side == "L" else (656 - w) - h = 18 + len(lines)*19 - pts = [(0, 0), (t0-0.3, 0), (t0, 1), (t1, 1), (t1+0.3, 0), (CYCLE, 0)] - rows = "\n".join( - f' {txt}' - for i, (txt, bold) in enumerate(lines)) - return f''' - {anim_opacity(pts)} - -{rows} - ''' - - -def pc_co(side, addr, t0, t1): - return callout(side, [("PATH_CHALLENGE", True), (addr, False)], t0, t1) - - -def nat_bubble(side, l1, l2, t_on, t_used): - # thinking bubble on a router: the NAT mapping (remote public -> local private). A green - # check appears next to the mapping the moment a packet actually uses it. - out0, out1 = T_POPUP_FADE, T_POPUP_FADE+1.0 - pts = [(0, 0), (t_on, 0), (t_on+0.4, 1), (out0, 1), (out1, 0), (CYCLE, 0)] - hg_pts = [(0, 0), (t_on+0.4, 0), (t_on+0.7, 1), (t_used, 1), (t_used+0.3, 0), (CYCLE, 0)] # hourglass: created, temporary - chk_pts = [(0, 0), (t_used, 0), (t_used+0.3, 1), (CYCLE, 1)] # check: used / validated - if side == "a": # to the right of Alice's router, trail back to it - bx, trail = 240, [(236, 251, 3), (231, 250, 2.5), (227, 249, 2)] - else: # to the right of Bob's router - bx, trail = 720, [(716, 251, 3), (711, 250, 2.5), (707, 249, 2)] - dots = "\n".join(f' ' for c in trail) - cx = bx+138 - return f''' - {anim_opacity(pts)} -{dots} - - {l1} - {l2} - - - - - - {anim_opacity(hg_pts)} - - - - {anim_opacity(chk_pts)} - - ''' - - -# beat 3 — sequential locals (Alice first), not routable -loc_a_pkt = hp_pkt("probe-local-a", T_AL, 1.4) -loc_b_pkt = hp_pkt("probe-local-b", T_BL, 1.4) -loc_a_drop = drop_x(DROP_A, T_AL+1.4, "not routable", hold=0.5) -loc_b_drop = drop_x(DROP_B, T_BL+1.4, "not routable", hold=0.5) -loc_a_co = pc_co("L", "192.168.0.3:4153", T_AL-0.3, T_AL+1.5) -loc_b_co = pc_co("R", "10.0.0.3:2104", T_BL-0.3, T_BL+1.5) - -# beat 4 — Alice's sacrificial public probe: opens her hole, rejected at Bob's NAT (bounces) -pub_a_pkt = hp_pkt("direct", T_AP, 2.0, kp0=0, kp1=F_BR) -pub_a_drop = drop_x(REJECT, T_AP+2.0, "no mapping", hold=0.6, dx=16) -pub_a_co = pc_co("L", "4.9.8.2:4153", T_AP-0.3, T_AP+2.1) -nat_a = nat_bubble("a", "4.9.8.2:4153 →", "10.0.0.3:2104", T_AP+0.6, t_used=21.9) - -# beat 5 — Bob's public probe: opens his hole, through Alice's hole, reaches Alice (success) -pub_b_pkt = hp_pkt("direct", T_BP, 2.2, kp0=1, kp1=0) -pub_b_co = pc_co("R", "8.3.1.9:2104", T_BP-0.3, T_BP+2.3) -nat_b = nat_bubble("b", "8.3.1.9:2104 →", "192.168.0.3:4153", T_BP+0.6, t_used=24.9) - -# beat 6 — Alice's bundled reply (PATH_RESPONSE + PATH_CHALLENGE) → Bob -reply_pkt = hp_pkt("direct", T_AR, 2.2, kp0=0, kp1=1) -reply_co = callout("L", [("PATH_RESPONSE", True), ("PATH_CHALLENGE", True)], T_AR-0.3, T_AR+2.3, w=164) - -# beat 7 — Bob's PATH_RESPONSE → Alice; both directions validated -resp_pkt = hp_pkt("direct", T_BR, 2.2, kp0=1, kp1=0) -resp_co = callout("R", [("PATH_RESPONSE", True)], T_BR-0.3, T_BR+2.3, w=150) - -# the direct path itself: appears faded-gray as Bob's packet goes through, turns blue once validated -direct_opa = [(0, 0), (T_BP, 0), (T_BP+0.3, 1), (CYCLE-0.5, 1), (CYCLE-0.2, 0), (CYCLE, 0)] -direct_stroke = (f'') -direct_path = (f' ' - f'{anim_opacity(direct_opa)}{direct_stroke}') - -# the relay stays blue/active (user data flows over it during hole punching) and only fades to -# gray at the moment the direct path becomes blue — i.e. when the handover happens -relay_stroke = (f'') - -VB_X, VB_Y, VB_W, VB_H = 0, 30, 940, 430 -svg = f''' - - - {relay_stroke} - -{direct_path} - - - - - - - -{relay(*RELAY, "relay")} - -{router(*R1, "8.3.1.9", "10.0.0.1")} - -{router(*R2, "4.9.8.2", "192.168.0.1")} - - - -{phone(*A_PH, "1a9c…", iphone=True)} - Alice - -{alice_facts} - - - -{phone(*B_PH, "8e2b…")} - Bob - -{bob_facts} - - -{addaddr_pkt} -{reachout_pkt} -{loc_a_pkt} -{loc_b_pkt} -{pub_a_pkt} -{pub_b_pkt} -{reply_pkt} -{resp_pkt} -{loc_a_drop} -{loc_b_drop} -{pub_a_drop} -{nat_a} -{nat_b} - - -{addaddr_co} -{alice_knows} -{reachout_co} -{bob_knows} -{loc_a_co} -{loc_b_co} -{pub_a_co} -{pub_b_co} -{reply_co} -{resp_co} - - - - - - -''' - -open(OUT_PATH, "w").write(svg) -print(f"wrote {len(svg)} bytes -> {OUT_PATH}") diff --git a/scripts/publish-relay-dht.gen.py b/scripts/publish-relay-dht.gen.py deleted file mode 100644 index b37a098..0000000 --- a/scripts/publish-relay-dht.gen.py +++ /dev/null @@ -1,308 +0,0 @@ -#!/usr/bin/env python3 -"""Generator for publish-relay-dht.svg ("Mainline DHT based address lookup"). - -The fully peer-to-peer variant of the DNS discovery figure. Instead of one -server (dns.iroh.link) there is a cloud (the Mainline DHT) holding many nodes. -Bob publishes his signed record to several random DHT nodes (green); Alice -resolves it by querying several random nodes (blue). - -All dots travel at the same speed, so they reach their nodes (and return) at -different times depending on distance. No arrowheads — the moving dots show the -direction. The answer is revealed as soon as the FIRST query response returns. - -One master SMIL loop (CYCLE seconds): publish -> Alice resolves -> arrows fade -out -> final state holds ~10s -> loop. - -Edit this script and run: python3 publish-relay-dht.gen.py -Writes ../images/how-iroh-works/publish-relay-dht.svg. -The MDX embeds this as ; the -intrinsic aspect ratio comes from the SVG viewBox (VB_W x VB_H). -""" - -import math -import os - -HERE = os.path.dirname(os.path.abspath(__file__)) -OUT_PATH = os.path.normpath(os.path.join( - HERE, "..", "images", "how-iroh-works", "publish-relay-dht.svg")) - -MONO = "'Space Mono', monospace" -INDIGO, AMBER, GRAY, INK = "#6366f1", "#d97706", "#888", "#111" -GREEN, BLUE, RED = "#15803d", "#2563eb", "#dc2626" -SPEED = 150.0 # px/second — same for every dot - - -def arc(x1, y1, x2, y2, k=0.16): - mx, my = (x1+x2)/2, (y1+y2)/2 - dx, dy = x2-x1, y2-y1 - L = math.hypot(dx, dy) - px, py = -dy/L, dx/L - if py > 0: - px, py = -px, -py - cx, cy = mx+px*k*L, my+py*k*L - d = f"M {x1:.0f} {y1:.0f} Q {cx:.0f} {cy:.0f} {x2:.0f} {y2:.0f}" - def q(t, a, b, c): return (1-t)**2*a + 2*(1-t)*t*b + t**2*c - N, length, prev = 50, 0.0, (x1, y1) - for i in range(1, N+1): - t = i/N - cur = (q(t, x1, cx, x2), q(t, y1, cy, y2)) - length += math.hypot(cur[0]-prev[0], cur[1]-prev[1]) - prev = cur - return d, length - - -def key_label(cx, y, text, size, color): - """A tiny drawn gold key (bow + shaft + teeth) + monospace label, centered on cx - (replaces the 🔑 emoji, which no pure-vector renderer can draw).""" - s = size - gold = "#eab308" - w = s * 1.3 # key glyph width - gap = s * 0.3 - xl = cx - (w + gap + s * 0.6 * len(text)) / 2 - cy = y - s * 0.30 - rb, rh = s * 0.34, s * 0.15 # bow outer / hole radius - bx = xl + rb # bow center x - hs = s * 0.18 # shaft thickness - xr = xl + w # right end - top, bot = cy - hs / 2, cy + hs / 2 - tw = s * 0.13 # tooth width - mono = "'Space Mono', monospace" - return ( - f'' - f'' - f'' - f'' - f'{text}' - ) - - -def iphone_screen(px0, py0, pw, ph, indigo): - """iPhone-style screen outline path (with notch) sized to fit inside an outer body of pw x ph.""" - sx0, sx1 = px0+3, px0+pw-3 - sy0, sy1 = py0+3, py0+ph-3 - cx = px0 + pw/2 - no = 7 # outer notch half-width - ni = 4 # inner notch half-width - nd = 5 # notch depth - ny = sy0 + nd - r = 6 # screen corner radius - return ( - f'' - ) - -def phone(px0, py0, keytext, iphone=False): - pw, ph = 48, 88 - cx = px0+pw/2 - if iphone: - screen = iphone_screen(px0, py0, pw, ph, INDIGO) - else: - screen = f'\n ' - return f''' - {screen} - - iroh - {key_label(cx, py0+58, keytext, 8, AMBER)}''' - - -# ============================ geometry ============================ -# DHT cloud (soft, fill-only overlapping circles) -cloud_circles = [(320, 150, 56), (392, 124, 62), (462, 130, 58), (524, 156, 50), - (360, 172, 56), (442, 178, 56), (292, 174, 40), (548, 182, 38)] -nodes = [(320, 162), (376, 138), (432, 172), (484, 142), (528, 168), (402, 196)] -pub_nodes = nodes[0:5] # Bob publishes to these -look_nodes = nodes[1:6] # Alice queries these - -bx0, by0 = 96, 296 -bcx = bx0+24 -PH = 88 -bob_top = (bcx, by0) - -ax0, ay0 = 664, 296 -acx = ax0+24 -alice_top = (acx, ay0) - -# ============================ timeline (constant speed) ============================ -PUB_LAUNCH = 1.5 -pub = [] # (path_d, arrival_time) -for n in pub_nodes: - d, L = arc(bob_top[0], bob_top[1], n[0], n[1], k=0.16) - pub.append((d, PUB_LAUNCH + L/SPEED)) -PUB_DONE = max(t for _, t in pub) - -ALICE_IN = PUB_DONE + 0.3 -LOOK_LAUNCH = ALICE_IN + 1.2 -look = [] # (path_d, reach_node_time, back_at_alice_time, has_data) -for n in look_nodes: - d, L = arc(n[0], n[1], alice_top[0], alice_top[1], k=0.16) - half = L/SPEED - look.append((d, LOOK_LAUNCH + half, LOOK_LAUNCH + 2*half, n in pub_nodes)) -FIRST_VALID = min(b for _, _, b, ok in look if ok) # first node that actually has the data -LAST_RETURN = max(b for _, _, b, _ in look) - -HOLD = 5.0 -ARROWS_GONE = LAST_RETURN + 0.6 -CYCLE = ARROWS_GONE + HOLD + 0.6 -OUT0, OUT1 = CYCLE - 0.6, CYCLE - 0.2 # persistent elements fade out before loop - - -def anim_opacity(points): - times = ";".join(f"{t/CYCLE:.4f}" for t, _ in points) - vals = ";".join(str(v) for _, v in points) - return (f'') - - -def anim_motion(wire_id, points): - times = ";".join(f"{t/CYCLE:.4f}" for t, _ in points) - kp = ";".join(str(k) for _, k in points) - return (f'') - - -def dot(wire_id, color, opa_pts, mot_pts): - return f''' - - {anim_opacity(opa_pts)} - {anim_motion(wire_id, mot_pts)} - ''' - - -def answer_dot(wire_id, color_down, launch, reach, back): - # query goes up blue, answer comes back coloured (green=valid, red=no data) - up = [(0, 0), (launch, 0), (launch+0.15, 1), (reach-0.05, 1), (reach, 0), (CYCLE, 0)] - down = [(0, 0), (reach, 0), (reach+0.05, 1), (back-0.05, 1), (back+0.1, 0), (CYCLE, 0)] - mot = [(0, 1), (launch, 1), (reach, 0), (back, 1), (CYCLE, 1)] - return f''' - {anim_motion(wire_id, mot)} - {anim_opacity(up)} - {anim_opacity(down)} - ''' - - -# ============================ static backdrop ============================ -cloud = [' ', ' '] -for cx, cy, r in cloud_circles: - cloud.append(f' ') -cloud.append(f' Mainline DHT') -cloud.append(' ') -cloud = "\n".join(cloud) - -# nodes are black; a published node turns green when its message arrives (one is never reached) -nd = [' ', ' '] -for x, y in nodes: - nd.append(f' ') -for i, (x, y) in enumerate(pub_nodes): - arr = pub[i][1] - g_pts = [(0, 0), (arr, 0), (arr+0.3, 1), (OUT0, 1), (OUT1, 0), (CYCLE, 0)] - nd.append(f' {anim_opacity(g_pts)}') -nd.append(' ') -node_dots = "\n".join(nd) - -bob = f''' - -{phone(bx0, by0, "8e2b…")} - Bob - - - NodeId: 8e2b… - Home relay: us-east - ''' - -alice_pts = [(0, 0), (ALICE_IN, 0), (ALICE_IN+0.6, 1), (OUT0, 1), (OUT1, 0), (CYCLE, 0)] -result_pts = [(0, 0), (FIRST_VALID, 0), (FIRST_VALID+0.5, 1), (OUT0, 1), (OUT1, 0), (CYCLE, 0)] -alice = f''' - - {anim_opacity(alice_pts)} -{phone(ax0, ay0, "1a9c…", iphone=True)} - Alice - - 8e2b… is at relay us-east{anim_opacity(result_pts)}''' - -# ============================ wires + dots ============================ -pub_wire_pts = [(0, 0), (0.6, 0), (1.0, 1), (PUB_DONE, 1), (PUB_DONE+0.5, 0), (CYCLE, 0)] -look_wire_pts = [(0, 0), (LOOK_LAUNCH-0.6, 0), (LOOK_LAUNCH, 1), (LAST_RETURN, 1), (LAST_RETURN+0.6, 0), (CYCLE, 0)] -wires, packets = [], [] -for i, (d, arr) in enumerate(pub): - wires.append(f' {anim_opacity(pub_wire_pts)}') - opa = [(0, 0), (PUB_LAUNCH, 0), (PUB_LAUNCH+0.15, 1), (arr-0.05, 1), (arr+0.1, 0), (CYCLE, 0)] - mot = [(0, 0), (PUB_LAUNCH, 0), (arr, 1), (CYCLE, 1)] - packets.append(dot(f"pw{i}", BLUE, opa, mot)) -for i, (d, reach, back, ok) in enumerate(look): - wires.append(f' {anim_opacity(look_wire_pts)}') - packets.append(answer_dot(f"lw{i}", GREEN if ok else RED, LOOK_LAUNCH, reach, back)) -wires = "\n".join(wires) -packets = "\n".join(packets) - -# ============================ callouts ============================ -pub_co_pts = [(0, 0), (1.6, 0), (2.0, 1), (PUB_DONE, 1), (PUB_DONE+0.5, 0), (CYCLE, 0)] -pub_co = f''' - - {anim_opacity(pub_co_pts)} - - DHT put - 8e2b… - Relay: us-east - ''' - -# DHT get callout (read request) — same box as the answer, shown until the answer arrives -read_pts = [(0, 0), (LOOK_LAUNCH-0.3, 0), (LOOK_LAUNCH+0.1, 1), (FIRST_VALID-0.1, 1), (FIRST_VALID+0.3, 0), (CYCLE, 0)] -read_co = f''' - - {anim_opacity(read_pts)} - - DHT get - 8e2b… - ''' - -# answer popup is visible while the dots are still travelling, then vanishes -ans_pts = [(0, 0), (FIRST_VALID, 0), (FIRST_VALID+0.4, 1), (LAST_RETURN, 1), (LAST_RETURN+0.6, 0), (CYCLE, 0)] -ans_co = f''' - - {anim_opacity(ans_pts)} - - ;; ANSWER SECTION: - TXT "relay=https://us-east" - ''' - -VB_X, VB_Y, VB_W, VB_H = 0, 44, 820, 376 -svg = f''' - -{cloud} - -{wires} - -{node_dots} - -{bob} - -{alice} - -{packets} - -{pub_co} - -{read_co} - -{ans_co} - - - - - - -''' - -open(OUT_PATH, "w").write(svg) -print(f"wrote {len(svg)} bytes -> {OUT_PATH} (CYCLE={CYCLE:.1f}s, first valid answer at {FIRST_VALID:.1f}s)") diff --git a/scripts/publish-relay.gen.py b/scripts/publish-relay.gen.py deleted file mode 100644 index e4a3d3c..0000000 --- a/scripts/publish-relay.gen.py +++ /dev/null @@ -1,284 +0,0 @@ -#!/usr/bin/env python3 -"""Generator for publish-relay.svg (the "Publishing the home relay" figure). - -Adapted from rklaehn's "DNS discovery" slide, restyled to match the other -how-iroh-works figures. No map. Our device "Bob" (left) publishes its current -home relay to the vanilla DNS server dns.iroh.link with a signed DNS packet sent -as an HTTPS PUT (green). Alice (right) resolves it with a DNS lookup (blue). -Continues the endpoint-startup story (Bob's home relay = us-east, key 8e2b...). - -One master loop (CYCLE seconds), driven entirely by SMIL so every element stays -in sync: publish once -> record added -> Alice resolves once -> arrows fade out --> final state holds ~10s -> loop. - -To change it, edit this script and run: python3 publish-relay.gen.py -It writes ../images/how-iroh-works/publish-relay.svg. -The MDX embeds this as ; the -intrinsic aspect ratio comes from the SVG viewBox (VB_W x VB_H). -""" - -import math -import os - -HERE = os.path.dirname(os.path.abspath(__file__)) -OUT_PATH = os.path.normpath(os.path.join( - HERE, "..", "images", "how-iroh-works", "publish-relay.svg")) - -MONO = "'Space Mono', monospace" -INDIGO, AMBER, GRAY, INK = "#6366f1", "#d97706", "#888", "#111" -GREEN, BLUE = "#15803d", "#2563eb" - -CYCLE = 14.5 # seconds for one full loop (~5s static hold) - - -def anim_opacity(points): - times = ";".join(f"{t/CYCLE:.4f}" for t, _ in points) - vals = ";".join(str(v) for _, v in points) - return (f'') - - -def anim_motion(wire_id, points): - times = ";".join(f"{t/CYCLE:.4f}" for t, _ in points) - kp = ";".join(str(k) for _, k in points) - return (f'') - - -def arc(x1, y1, x2, y2, k=0.16): - mx, my = (x1+x2)/2, (y1+y2)/2 - dx, dy = x2-x1, y2-y1 - L = math.hypot(dx, dy) - px, py = -dy/L, dx/L - if py > 0: - px, py = -px, -py - cx, cy = mx+px*k*L, my+py*k*L - return f"M {x1:.0f} {y1:.0f} Q {cx:.0f} {cy:.0f} {x2:.0f} {y2:.0f}" - - -def key_label(cx, y, text, size, color): - """A tiny drawn gold key (bow + shaft + teeth) + monospace label, centered on cx - (replaces the 🔑 emoji, which no pure-vector renderer can draw).""" - s = size - gold = "#eab308" - w = s * 1.3 # key glyph width - gap = s * 0.3 - xl = cx - (w + gap + s * 0.6 * len(text)) / 2 - cy = y - s * 0.30 - rb, rh = s * 0.34, s * 0.15 # bow outer / hole radius - bx = xl + rb # bow center x - hs = s * 0.18 # shaft thickness - xr = xl + w # right end - top, bot = cy - hs / 2, cy + hs / 2 - tw = s * 0.13 # tooth width - mono = "'Space Mono', monospace" - return ( - f'' - f'' - f'' - f'' - f'{text}' - ) - - -def iphone_screen(px0, py0, pw, ph, indigo): - """iPhone-style screen outline path (with notch) sized to fit inside an outer body of pw x ph.""" - sx0, sx1 = px0+3, px0+pw-3 - sy0, sy1 = py0+3, py0+ph-3 - cx = px0 + pw/2 - no = 7 # outer notch half-width - ni = 4 # inner notch half-width - nd = 5 # notch depth - ny = sy0 + nd - r = 6 # screen corner radius - return ( - f'' - ) - -def phone(px0, py0, keytext, iphone=False): - pw, ph = 48, 88 - cx = px0+pw/2 - if iphone: - screen = iphone_screen(px0, py0, pw, ph, INDIGO) - else: - screen = f'\n ' - return f''' - {screen} - - iroh - {key_label(cx, py0+58, keytext, 8, AMBER)}''' - - -def doc_packet(wire_id, color, opa_pts, mot_pts): - # a simple dot riding a wire (the popup explains what the request is) - return f''' - - {anim_opacity(opa_pts)} - {anim_motion(wire_id, mot_pts)} - ''' - - -# ============================ static backdrop ============================ - -# dns.iroh.link server, top-center (vanilla 3U rack, gray LEDs) -sx, sy, sw, sh = 378, 56, 64, 66 -scx = sx+sw/2 -srv = [' ', ' ', - f' '] -for i in range(3): - ry = sy + 4 + i*21 - srv.append(f' ') - srv.append(f' ') - for j in range(6): - vx = sx+24+j*5 - srv.append(f' ') - if i < 2: - yy = sy+4+(i+1)*21-2 - srv.append(f' ') -srv.append(f' dns.iroh.link') -srv.append(' ') -server = "\n".join(srv) - -# Bob (left, publisher) — always present -bx0, by0 = 96, 296 -bcx = bx0+24 -PH = 88 -bob = f''' - -{phone(bx0, by0, "8e2b…")} - Bob - - - NodeId: 8e2b… - Home relay: us-east - ''' - -# ============================ records on the server ============================ -rec_x = sx+sw+30 -existing = [("1f3a…", "eu-west"), ("7c0e…", "us-west"), - ("b48d…", "us-east"), ("2a91…", "eu-west")] -rlh, ry0 = 16, 60 -rec_lines = [f' {k} → {r}' - for i, (k, r) in enumerate(existing)] -ynew = ry0 + len(existing)*rlh -rec_pts = [(0, 0), (3.5, 0), (4.0, 1), (13.8, 1), (14.2, 0), (CYCLE, 0)] -record = f''' - - -{chr(10).join(rec_lines)} - 8e2b… → us-east{anim_opacity(rec_pts)} - ''' - -# ============================ Alice (right, resolver) ============================ -ax0, ay0 = 664, 296 -acx = ax0+24 -alice_pts = [(0, 0), (3.8, 0), (4.4, 1), (13.8, 1), (14.2, 0), (CYCLE, 0)] -result_pts = [(0, 0), (8.0, 0), (8.5, 1), (13.8, 1), (14.2, 0), (CYCLE, 0)] -alice = f''' - - {anim_opacity(alice_pts)} -{phone(ax0, ay0, "1a9c…", iphone=True)} - Alice - - - 8e2b… is at relay us-east{anim_opacity(result_pts)}''' - -# ============================ wires ============================ -put_d = arc(bcx, by0, sx+18, sy+sh, k=0.18) # Bob -> server -lookup_d = arc(sx+sw-18, sy+sh, acx, ay0, k=0.18) # server -> Alice (answer direction) -put_wire_pts = [(0, 0), (0.6, 0), (1.0, 1), (4.0, 1), (4.5, 0), (CYCLE, 0)] -lookup_wire_pts = [(0, 0), (4.4, 0), (5.0, 1), (8.3, 1), (8.8, 0), (CYCLE, 0)] -wires = f''' {anim_opacity(put_wire_pts)} - {anim_opacity(lookup_wire_pts)}''' - -# ============================ packets ============================ -# PUT: Bob(0) -> server(1), fades out as it lands (~3.5s) -put_pkt = doc_packet("put-wire", BLUE, - [(0, 0), (1.5, 0), (1.7, 1), (3.4, 1), (3.6, 0), (CYCLE, 0)], - [(0, 0), (1.5, 0), (3.5, 1), (CYCLE, 1)]) -# LOOKUP: on lookup-wire 0=server,1=Alice. Query Alice(1)->server(0), answer server(0)->Alice(1). -lookup_pkt = doc_packet("lookup-wire", BLUE, - [(0, 0), (5.0, 0), (5.2, 1), (7.9, 1), (8.1, 0), (CYCLE, 0)], - [(0, 1), (5.0, 1), (6.5, 0), (8.0, 1), (CYCLE, 1)]) - -# ============================ callouts ============================ -put_co_pts = [(0, 0), (1.6, 0), (2.0, 1), (4.0, 1), (4.5, 0), (CYCLE, 0)] -put_co = f''' - - {anim_opacity(put_co_pts)} - - HTTPS PUT - Relay: us-east - Signed by: 8e2b… - ''' - -lookup_co_pts = [(0, 0), (4.6, 0), (5.0, 1), (8.3, 1), (8.8, 0), (CYCLE, 0)] -# the query content is shown on the way up, then replaced by the answer as the dot heads back (~6.5s) -query_pts = [(0, 0), (4.6, 0), (5.0, 1), (6.3, 1), (6.6, 0), (CYCLE, 0)] -answer_pts = [(0, 0), (6.4, 0), (6.7, 1), (8.3, 1), (8.7, 0), (CYCLE, 0)] -lookup_co = f''' - - {anim_opacity(lookup_co_pts)} - - - {anim_opacity(query_pts)} - DNS LOOKUP - TXT _iroh.8e2b….dns.iroh.link - - - {anim_opacity(answer_pts)} - ;; ANSWER SECTION: - TXT "relay=https://us-east" - - ''' - -VB_X, VB_Y, VB_W, VB_H = 0, 44, 820, 376 -svg = f''' - - - - - - - - - - -{wires} - -{server} - -{record} - -{bob} - -{alice} - -{put_pkt} -{lookup_pkt} - -{put_co} - -{lookup_co} - - - - - - -''' - -open(OUT_PATH, "w").write(svg) -print(f"wrote {len(svg)} bytes -> {OUT_PATH}") diff --git a/scripts/routing-moves.gen.py b/scripts/routing-moves.gen.py deleted file mode 100644 index 64cd38d..0000000 --- a/scripts/routing-moves.gen.py +++ /dev/null @@ -1,316 +0,0 @@ -#!/usr/bin/env python3 -"""Generator for routing-moves.svg. - -Bob's commute story: home wifi (R1) → cellular (R2, cell tower) → in-flight -(R3, Starlink ground station). Alice stays put; the connection follows Bob. -20s loop, all SMIL so every element shares one timeline (no CSS/SMIL drift). - -To change it, edit this script and run: python3 routing-moves.gen.py -It writes ../images/how-iroh-works/routing-moves.svg. -The MDX embeds this as ; the -intrinsic aspect ratio comes from the SVG viewBox (VB_W x VB_H). -""" -import os - -HERE = os.path.dirname(os.path.abspath(__file__)) -OUT_PATH = os.path.normpath(os.path.join( - HERE, "..", "images", "how-iroh-works", "routing-moves.svg")) - -MONO = "'Space Mono', monospace" -INDIGO, AMBER, GRAY = "#6366f1", "#d97706", "#888" -CYCLE = 20 # seconds - -VB_W, VB_H = 800, 380 - -# Animation thresholds (fractions of the cycle): -# 0.25 — Bob starts moving (and R2 fades in) -# 0.30 — single-bend path swaps to multi R1→R2 -# 0.55 — R2 + multi R1→R2 fade out -# 0.57 — R3 + multi R1→R3 fade in -# 0.75 — Bob reaches final position - - -def key_label(cx, y, text, size, color): - """Tiny drawn key glyph + monospace text, replacing the 🔑 emoji.""" - s = size - gold = "#eab308" - w = s * 1.3 - gap = s * 0.3 - xl = cx - (w + gap + s * 0.6 * len(text)) / 2 - cy = y - s * 0.30 - rb, rh = s * 0.34, s * 0.15 - bx = xl + rb - hs = s * 0.18 - xr = xl + w - top, bot = cy - hs / 2, cy + hs / 2 - tw = s * 0.13 - return ( - f'' - f'' - f'' - f'' - f'{text}' - ) - - -def iphone_outline(cx, top, bottom): - """iPhone outer body + screen path with notch in the top. Centered at cx, body height = bottom-top.""" - bx0 = cx - 45 # body 90 wide - sx0, sx1 = cx - 42, cx + 42 # screen 84 wide - sy0, sy1 = top + 3, bottom - 3 # screen y span - # Notch geometry (matches the original hand-tuned shape): - # outer corners at (cx-15, sy0) and (cx+15, sy0) — 30px wide - # inner notch bottom at (cx-9 .. cx+9) at y = sy0 + 9 - n_outer_l, n_outer_r = cx - 15, cx + 15 - n_inner_l, n_inner_r = cx - 9, cx + 9 - n_bottom = sy0 + 9 - body = f'' - path = ( - f'' - ) - return body + "\n " + path - - -def phone_iphone(cx, top, key, label, *, ip_text=None, animated_ips=None): - """Static iPhone — used for Alice.""" - lines = [f' '] - if ip_text is not None: - lines.append(f' {ip_text}') - if animated_ips: - for ip in animated_ips: - lines.append(ip) - lines += [ - f' {iphone_outline(cx, top, top + 150)}', - f' ', - f' iroh', - f' {key_label(cx, top + 95, key, 10, AMBER)}', - f' {label}', - f' ', - ] - return "\n".join(lines) - - -def phone_android(cx, top, key, label, *, animated_ips=None): - """Android phone — used for Bob (with movement and IP changes).""" - bx0 = cx - 45 - sx0 = cx - 42 - lines = [' '] - lines.append(f' ') - if animated_ips: - for ip in animated_ips: - lines.append(' ' + ip) - lines += [ - f' ', - f' ', - f' ', - f' ', - f' iroh', - f' {key_label(cx, top + 95, key, 10, AMBER)}', - f' {label}', - ' ', - ] - return "\n".join(lines) - - -def bob_ip(text, key_times, vals, *, initial_opacity=None): - extra = '' if initial_opacity is None else f' opacity="{initial_opacity}"' - return ( - f'' - f'{text}' - f'' - ) - - -def morph_path(d0, mid_d, k_mid_start, k_mid_end, stroke_attrs): - """Path with d-morph animation between two shapes.""" - return ( - f' \n' - f' \n' - ) - - -# ============================ Alice (static iPhone) ============================ -alice_block = phone_iphone(120, 190, "a4f7c0…", "Alice", ip_text="10.0.0.42") - -# ============================ Bob (Android, moves) ============================ -bob_ips = [ - bob_ip("10.0.0.7", "0;0.29;0.31;1", "1;1;0;0"), - bob_ip("192.168.1.7", "0;0.29;0.31;0.55;0.57;1", "0;0;1;1;0;0", initial_opacity="0"), - bob_ip("172.16.0.7", "0;0.55;0.57;1", "0;0;1;1", initial_opacity="0"), -] -bob_block = phone_android(240, 190, "8e2b1d…", "Bob", animated_ips=bob_ips) - -# ============================ Connection paths ============================ -single_d = "M 120 240 Q 120 161 180 161 Q 240 161 240 240" -single_d2 = "M 120 240 Q 120 161 180 161 Q 285 161 285 240" -single_path = ( - f' \n' - f' \n' - f' \n' - f' \n' - f' \n' -) - -multi_r1r2_d = "M 120 240 C 120 170 180 200 180 130 L 180 90 C 180 30 420 30 420 90 L 420 130 C 420 200 285 170 285 240" -multi_r1r2_d2 = "M 120 240 C 120 170 180 200 180 130 L 180 90 C 180 30 420 30 420 90 L 420 130 C 420 200 528 170 528 240" -multi_r1r2_path = ( - f' \n' - f' \n' - f' \n' - f' \n' - f' \n' -) - -multi_r1r3_d = "M 120 240 C 120 170 180 200 180 130 L 180 90 C 180 10 660 10 660 90 L 660 130 C 660 200 552 170 552 240" -multi_r1r3_d2 = "M 120 240 C 120 170 180 200 180 130 L 180 90 C 180 10 660 10 660 90 L 660 130 C 660 200 720 170 720 240" -multi_r1r3_path = ( - f' \n' - f' \n' - f' \n' - f' \n' - f' \n' -) - -# ============================ Routers ============================ -# R1 — home wifi (always visible). Public IP fades in once multi-routing kicks in. -r1 = f''' - - home router - - 203.0.113.1 - - - - - - - 10.0.0.1 - ''' - -# R2 — cell tower (appears 5s, fades out at 11s). 3 panels on a mast with crossbar. -r2 = f''' - - - - mobile network - - 198.51.100.1 - - - - - - 192.168.1.1 - ''' - -# R3 — Starlink ground station (fades in at 11s). Flat tilted parallelogram + kickstand. -r3 = f''' - - - - satellite internet - - 100.64.10.1 - - - 172.16.0.1 - ''' - -# ============================ Timer bar ============================ -timer = f''' - - - - - ''' - -# ============================ Assemble ============================ -svg = f''' - - - - - - - - -{alice_block} - - -{bob_block} - -{single_path} -{multi_r1r2_path} -{multi_r1r3_path} - -{r1} - -{r2} - -{r3} - - -{timer} - -''' - -open(OUT_PATH, "w").write(svg) -print(f"wrote {len(svg)} bytes -> {OUT_PATH}") diff --git a/what-is-iroh.mdx b/what-is-iroh.mdx index a36c91a..41ebbbf 100644 --- a/what-is-iroh.mdx +++ b/what-is-iroh.mdx @@ -11,7 +11,7 @@ application — in Rust, or in C, C++, Swift, Python, JavaScript, and Kotlin through [our bindings](/languages). - iroh embedded across six device contexts: iOS, Android, desktop, browser, server, embedded + iroh embedded across six device contexts: iOS, Android, desktop, browser, server, embedded ## Core features @@ -52,7 +52,7 @@ That key — not an IP address — is the address. It lets you connect to a peer matter where it is, even as its network location changes underneath it. - Alice connects to Bob by key — IPs change, the key stays stable + Alice connects to Bob by key — IPs change, the key stays stable ### Reliable connections @@ -62,7 +62,7 @@ possible. When network conditions change, iroh immediately reacts and switches to the new best path — transparently, without dropping the connection. - Alice and Bob connected via a shared router, with two more routers available for when Bob moves + Alice and Bob connected via a shared router, with two more routers available for when Bob moves Now that we've seen *what* iroh does, here's *how*. @@ -94,7 +94,7 @@ closest relay becomes the **home relay**, and the endpoint keeps a secure WebSocket connection open to it. - A map of the US and Europe with iroh relays in us-west (Seattle), us-east (Delaware), and eu-west (Frankfurt), and an endpoint in Florida + A map of the US and Europe with iroh relays in us-west (Seattle), us-east (Delaware), and eu-west (Frankfurt), and an endpoint in Florida ### Finding peers @@ -110,7 +110,7 @@ reach Bob resolves that record with a DNS or HTTPS query. See [DNS address lookup](/connecting/dns-address-lookup). - Bob publishes a signed DNS record with his home relay to dns.iroh.link via an HTTPS PUT; Alice resolves it with a DNS lookup + Bob publishes a signed DNS record with his home relay to dns.iroh.link via an HTTPS PUT; Alice resolves it with a DNS lookup #### Mainline DHT lookup @@ -122,7 +122,7 @@ exact same signed record using the [DHT address lookup](/connecting/dht-address-lookup). - Bob publishes his signed record to several random nodes of the Mainline DHT; Alice resolves it by querying several random nodes + Bob publishes his signed record to several random nodes of the Mainline DHT; Alice resolves it by querying several random nodes ### Direct connections @@ -141,7 +141,7 @@ but using its own transport parameter ID. See [NAT traversal](/concepts/nat-traversal). - Alice and Bob, each behind a home router, connected through the relay — all data flows up to the relay and back down + Alice and Bob, each behind a home router, connected through the relay — all data flows up to the relay and back down #### Local connections @@ -151,7 +151,7 @@ just needs to learn the other's local address. See [local address lookup](/connecting/local-address-lookup). - Alice and Bob on the same home network discover each other over the relay, then connect directly across the LAN — the relay uplink fades away once the local path is validated + Alice and Bob on the same home network discover each other over the relay, then connect directly across the LAN — the relay uplink fades away once the local path is validated ## How the pieces stack together