A Python port of the Invisible Internet Project (I2P)
at main 993 lines 32 kB view raw
1<!DOCTYPE html> 2<html lang="en"> 3<head> 4<meta charset="UTF-8"> 5<meta name="viewport" content="width=device-width, initial-scale=1.0"> 6<title>You Are Visible — Ethereum Validator Privacy</title> 7<meta name="description" content="Your Ethereum validator is broadcasting its home IP address. i2p-python is the native Python I2P implementation that fixes this."> 8<link rel="preconnect" href="https://fonts.googleapis.com"> 9<link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Space+Mono:wght@400;700&family=Syne:wght@400;600;700;800&display=swap" rel="stylesheet"> 10<style> 11 *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } 12 13 :root { 14 --navy: #06091a; 15 --navy2: #0c1230; 16 --navy3: #111a3e; 17 --red: #ff2b4a; 18 --red-dim: #8b0e20; 19 --mint: #00e5a0; 20 --cyan: #00c8ff; 21 --gray: #8a95b0; 22 --light: #c8d0e8; 23 --white: #eef1fb; 24 --mono: 'Space Mono', monospace; 25 --display: 'Bebas Neue', sans-serif; 26 --body: 'Syne', sans-serif; 27 } 28 29 html { scroll-behavior: smooth; } 30 31 body { 32 background: var(--navy); 33 color: var(--white); 34 font-family: var(--body); 35 font-size: 16px; 36 line-height: 1.6; 37 overflow-x: hidden; 38 } 39 40 /* NAV */ 41 nav { 42 position: fixed; top: 0; left: 0; right: 0; z-index: 100; 43 display: flex; align-items: center; justify-content: space-between; 44 padding: 1.25rem 2.5rem; 45 background: linear-gradient(to bottom, rgba(6,9,26,0.95), transparent); 46 backdrop-filter: blur(2px); 47 } 48 .nav-logo { 49 font-family: var(--mono); font-size: 13px; 50 color: var(--mint); letter-spacing: 0.08em; 51 text-decoration: none; 52 } 53 .nav-links { display: flex; gap: 2rem; list-style: none; } 54 .nav-links a { 55 font-family: var(--mono); font-size: 12px; color: var(--gray); 56 text-decoration: none; letter-spacing: 0.05em; 57 transition: color 0.2s; 58 } 59 .nav-links a:hover { color: var(--white); } 60 .nav-cta { 61 font-family: var(--mono); font-size: 12px; 62 color: var(--navy); background: var(--mint); 63 padding: 0.5rem 1.25rem; text-decoration: none; 64 letter-spacing: 0.05em; transition: opacity 0.2s; 65 } 66 .nav-cta:hover { opacity: 0.85; } 67 68 /* HERO */ 69 #hero { 70 position: relative; height: 100vh; min-height: 640px; 71 display: flex; flex-direction: column; 72 justify-content: center; align-items: flex-start; 73 padding: 0 2.5rem; overflow: hidden; 74 } 75 #hero-canvas { 76 position: absolute; inset: 0; 77 opacity: 0.55; 78 } 79 .hero-eyebrow { 80 font-family: var(--mono); font-size: 11px; 81 color: var(--red); letter-spacing: 0.2em; text-transform: uppercase; 82 margin-bottom: 1.25rem; 83 display: flex; align-items: center; gap: 0.6rem; 84 animation: fadeUp 0.8s ease 0.2s both; 85 } 86 .pulse-dot { 87 width: 7px; height: 7px; border-radius: 50%; 88 background: var(--red); 89 animation: pulse 1.6s ease-in-out infinite; 90 } 91 @keyframes pulse { 92 0%, 100% { opacity: 1; transform: scale(1); } 93 50% { opacity: 0.4; transform: scale(0.7); } 94 } 95 .hero-headline { 96 font-family: var(--display); 97 font-size: clamp(72px, 12vw, 160px); 98 line-height: 0.9; 99 letter-spacing: 0.02em; 100 color: var(--white); 101 animation: fadeUp 0.8s ease 0.35s both; 102 } 103 .hero-headline .threat { color: var(--red); } 104 .hero-sub { 105 font-size: clamp(15px, 2vw, 18px); 106 color: var(--light); max-width: 520px; 107 margin-top: 1.5rem; margin-bottom: 2.5rem; 108 font-weight: 400; line-height: 1.65; 109 animation: fadeUp 0.8s ease 0.5s both; 110 } 111 .hero-stat-row { 112 display: flex; gap: 3rem; align-items: baseline; 113 animation: fadeUp 0.8s ease 0.65s both; 114 } 115 .hero-stat .num { 116 font-family: var(--display); font-size: 56px; 117 line-height: 1; color: var(--red); 118 } 119 .hero-stat .num.mint { color: var(--mint); } 120 .hero-stat .label { 121 font-family: var(--mono); font-size: 11px; 122 color: var(--gray); letter-spacing: 0.1em; 123 margin-top: 0.25rem; 124 } 125 .scroll-hint { 126 position: absolute; bottom: 2rem; left: 50%; 127 transform: translateX(-50%); 128 font-family: var(--mono); font-size: 11px; 129 color: var(--gray); letter-spacing: 0.15em; 130 display: flex; flex-direction: column; align-items: center; gap: 0.5rem; 131 animation: fadeIn 1s ease 1.2s both; 132 } 133 .scroll-arrow { 134 width: 1px; height: 32px; 135 background: linear-gradient(to bottom, var(--gray), transparent); 136 animation: scrollPulse 2s ease-in-out infinite; 137 } 138 @keyframes scrollPulse { 139 0%, 100% { opacity: 0.4; transform: scaleY(1); } 140 50% { opacity: 1; transform: scaleY(1.15); } 141 } 142 143 /* SECTIONS */ 144 section { 145 padding: 7rem 2.5rem; 146 max-width: 1100px; margin: 0 auto; 147 } 148 .section-tag { 149 font-family: var(--mono); font-size: 11px; 150 color: var(--gray); letter-spacing: 0.2em; 151 text-transform: uppercase; margin-bottom: 1rem; 152 display: flex; align-items: center; gap: 0.75rem; 153 } 154 .section-tag::before { 155 content: ''; display: block; 156 width: 24px; height: 1px; background: var(--gray); 157 } 158 h2 { 159 font-family: var(--display); 160 font-size: clamp(48px, 7vw, 96px); 161 line-height: 0.95; letter-spacing: 0.02em; 162 margin-bottom: 1.5rem; 163 } 164 h2 .accent { color: var(--red); } 165 h2 .mint { color: var(--mint); } 166 h2 .cyan { color: var(--cyan); } 167 168 /* ATTACK EXPLAINER */ 169 #attack { 170 max-width: 100%; 171 padding: 7rem 2.5rem; 172 background: linear-gradient(180deg, var(--navy) 0%, var(--navy2) 50%, var(--navy) 100%); 173 } 174 .attack-inner { 175 max-width: 1100px; margin: 0 auto; 176 } 177 .steps-grid { 178 display: grid; 179 grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); 180 gap: 1.5px; 181 margin-top: 4rem; 182 border: 1px solid rgba(255,43,74,0.2); 183 } 184 .step { 185 padding: 2.5rem; 186 background: var(--navy2); 187 border: 1px solid rgba(255,43,74,0.1); 188 transition: background 0.3s, border-color 0.3s; 189 cursor: default; 190 } 191 .step:hover { 192 background: rgba(255,43,74,0.06); 193 border-color: rgba(255,43,74,0.35); 194 } 195 .step-num { 196 font-family: var(--mono); font-size: 11px; 197 color: var(--red); letter-spacing: 0.15em; 198 margin-bottom: 1.25rem; 199 } 200 .step-title { 201 font-family: var(--body); font-size: 20px; 202 font-weight: 700; color: var(--white); 203 margin-bottom: 0.75rem; line-height: 1.25; 204 } 205 .step-body { 206 font-size: 14px; color: var(--gray); 207 line-height: 1.7; font-weight: 400; 208 } 209 .step-body code { 210 font-family: var(--mono); font-size: 12px; 211 color: var(--cyan); background: rgba(0,200,255,0.08); 212 padding: 0.15em 0.4em; 213 } 214 215 /* BIG NUMBERS */ 216 #numbers { 217 padding: 7rem 2.5rem; 218 background: var(--navy2); 219 } 220 .numbers-inner { 221 max-width: 1100px; margin: 0 auto; 222 } 223 .numbers-grid { 224 display: grid; 225 grid-template-columns: repeat(3, 1fr); 226 gap: 2px; margin-top: 4rem; 227 } 228 .number-card { 229 padding: 3rem 2.5rem; 230 background: var(--navy); 231 border: 1px solid rgba(255,255,255,0.06); 232 position: relative; overflow: hidden; 233 } 234 .number-card::before { 235 content: ''; 236 position: absolute; top: 0; left: 0; right: 0; height: 2px; 237 background: var(--red); 238 transform: scaleX(0); transform-origin: left; 239 transition: transform 0.6s ease; 240 } 241 .number-card:hover::before { transform: scaleX(1); } 242 .number-card .big { 243 font-family: var(--display); 244 font-size: clamp(64px, 8vw, 120px); 245 line-height: 1; color: var(--red); 246 display: block; 247 } 248 .number-card .description { 249 font-family: var(--mono); font-size: 12px; 250 color: var(--gray); letter-spacing: 0.08em; 251 margin-top: 0.75rem; line-height: 1.6; 252 display: block; 253 } 254 .number-card .source { 255 font-family: var(--mono); font-size: 10px; 256 color: rgba(138,149,176,0.5); margin-top: 1.25rem; 257 display: block; 258 } 259 260 /* WHAT IS I2P */ 261 #what-is { 262 padding: 7rem 2.5rem; 263 } 264 .split { 265 display: grid; 266 grid-template-columns: 1fr 1fr; 267 gap: 5rem; align-items: center; 268 max-width: 1100px; margin: 4rem auto 0; 269 } 270 .split-text p { 271 font-size: 15px; color: var(--light); 272 line-height: 1.75; margin-bottom: 1.25rem; 273 font-weight: 400; 274 } 275 .split-text p strong { color: var(--white); font-weight: 600; } 276 .viz-box { 277 background: var(--navy2); 278 border: 1px solid rgba(0,200,255,0.15); 279 padding: 2rem; position: relative; 280 } 281 .viz-box::before { 282 content: '// network topology'; 283 font-family: var(--mono); font-size: 10px; 284 color: var(--gray); letter-spacing: 0.1em; 285 position: absolute; top: -12px; left: 1rem; 286 background: var(--navy); padding: 0 0.5rem; 287 } 288 .viz-comparison { 289 display: grid; grid-template-columns: 1fr 1fr; 290 gap: 1px; background: rgba(255,255,255,0.06); 291 } 292 .viz-side { 293 background: var(--navy); padding: 1.5rem 1.25rem; 294 } 295 .viz-side-label { 296 font-family: var(--mono); font-size: 10px; 297 letter-spacing: 0.15em; margin-bottom: 1rem; 298 display: block; 299 } 300 .viz-side-label.bad { color: var(--red); } 301 .viz-side-label.good { color: var(--mint); } 302 .node-row { 303 display: flex; align-items: center; gap: 0.6rem; 304 margin-bottom: 0.6rem; 305 } 306 .node-dot { 307 width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; 308 } 309 .node-dot.exposed { background: var(--red); } 310 .node-dot.hidden { background: var(--mint); } 311 .node-dot.attacker { background: var(--cyan); } 312 .node-text { 313 font-family: var(--mono); font-size: 10px; color: var(--gray); 314 } 315 .node-text.red { color: rgba(255,43,74,0.8); } 316 .node-text.green { color: rgba(0,229,160,0.8); } 317 318 /* COMPARISON TABLE */ 319 #compare { 320 padding: 7rem 2.5rem; 321 background: var(--navy2); 322 } 323 .compare-inner { 324 max-width: 1100px; margin: 0 auto; 325 } 326 .compare-table { 327 width: 100%; border-collapse: collapse; margin-top: 3rem; 328 font-size: 14px; 329 } 330 .compare-table th { 331 font-family: var(--mono); font-size: 11px; 332 color: var(--gray); letter-spacing: 0.12em; 333 text-align: left; padding: 1rem 1.25rem; 334 border-bottom: 1px solid rgba(255,255,255,0.08); 335 font-weight: 400; 336 } 337 .compare-table th.highlight { 338 color: var(--mint); background: rgba(0,229,160,0.05); 339 } 340 .compare-table td { 341 padding: 1rem 1.25rem; 342 border-bottom: 1px solid rgba(255,255,255,0.04); 343 color: var(--gray); vertical-align: middle; 344 } 345 .compare-table td:first-child { color: var(--light); font-weight: 600; } 346 .compare-table td.highlight { 347 background: rgba(0,229,160,0.05); 348 color: var(--mint); 349 font-family: var(--mono); font-size: 12px; 350 } 351 .compare-table tr:hover td { background: rgba(255,255,255,0.02); } 352 .check { color: var(--mint); } 353 .cross { color: var(--red); } 354 .muted { color: rgba(138,149,176,0.4); } 355 356 /* THE PORT section */ 357 #port { 358 padding: 7rem 2.5rem; 359 } 360 .port-inner { 361 max-width: 1100px; margin: 0 auto; 362 } 363 .port-grid { 364 display: grid; 365 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 366 gap: 2px; margin-top: 4rem; 367 } 368 .port-stat { 369 padding: 2rem 1.5rem; 370 background: var(--navy2); 371 border: 1px solid rgba(0,229,160,0.1); 372 } 373 .port-stat .big { 374 font-family: var(--display); 375 font-size: clamp(40px, 5vw, 64px); 376 line-height: 1; color: var(--mint); 377 display: block; 378 } 379 .port-stat .description { 380 font-family: var(--mono); font-size: 11px; 381 color: var(--gray); letter-spacing: 0.08em; 382 margin-top: 0.5rem; display: block; 383 } 384 385 /* CTA */ 386 #cta { 387 padding: 8rem 2.5rem; 388 text-align: center; position: relative; overflow: hidden; 389 } 390 #cta::before { 391 content: ''; 392 position: absolute; inset: 0; 393 background: radial-gradient(ellipse 60% 60% at 50% 50%, rgba(0,229,160,0.06) 0%, transparent 70%); 394 } 395 .cta-inner { position: relative; max-width: 700px; margin: 0 auto; } 396 .cta-inner h2 { 397 font-size: clamp(48px, 8vw, 100px); 398 color: var(--white); margin-bottom: 1.5rem; 399 } 400 .cta-inner p { 401 font-size: 16px; color: var(--gray); 402 margin-bottom: 3rem; line-height: 1.7; 403 } 404 .cta-buttons { 405 display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap; 406 } 407 .btn-primary { 408 font-family: var(--mono); font-size: 13px; 409 color: var(--navy); background: var(--mint); 410 padding: 1rem 2.5rem; text-decoration: none; 411 letter-spacing: 0.08em; transition: opacity 0.2s; 412 display: inline-block; 413 } 414 .btn-primary:hover { opacity: 0.85; } 415 .btn-secondary { 416 font-family: var(--mono); font-size: 13px; 417 color: var(--mint); background: transparent; 418 padding: 1rem 2.5rem; text-decoration: none; 419 letter-spacing: 0.08em; 420 border: 1px solid rgba(0,229,160,0.35); 421 transition: border-color 0.2s, background 0.2s; 422 display: inline-block; 423 } 424 .btn-secondary:hover { 425 border-color: var(--mint); 426 background: rgba(0,229,160,0.06); 427 } 428 .btn-ghost { 429 font-family: var(--mono); font-size: 13px; 430 color: var(--gray); background: transparent; 431 padding: 1rem 2rem; text-decoration: none; 432 letter-spacing: 0.08em; 433 border: 1px solid rgba(255,255,255,0.12); 434 transition: border-color 0.2s, color 0.2s; 435 display: inline-block; 436 } 437 .btn-ghost:hover { 438 border-color: rgba(255,255,255,0.3); 439 color: var(--white); 440 } 441 442 /* FOOTER */ 443 footer { 444 padding: 2.5rem; 445 border-top: 1px solid rgba(255,255,255,0.06); 446 display: flex; align-items: center; justify-content: space-between; 447 } 448 footer .footer-left { 449 font-family: var(--mono); font-size: 11px; 450 color: var(--gray); letter-spacing: 0.08em; 451 } 452 footer .footer-right { 453 display: flex; gap: 1.5rem; 454 } 455 footer a { 456 font-family: var(--mono); font-size: 11px; 457 color: var(--gray); text-decoration: none; 458 letter-spacing: 0.08em; transition: color 0.2s; 459 } 460 footer a:hover { color: var(--white); } 461 462 /* ANIMATIONS */ 463 @keyframes fadeUp { 464 from { opacity: 0; transform: translateY(20px); } 465 to { opacity: 1; transform: translateY(0); } 466 } 467 @keyframes fadeIn { 468 from { opacity: 0; } 469 to { opacity: 1; } 470 } 471 .reveal { 472 opacity: 0; transform: translateY(24px); 473 transition: opacity 0.7s ease, transform 0.7s ease; 474 } 475 .reveal.visible { 476 opacity: 1; transform: translateY(0); 477 } 478 .reveal-delay-1 { transition-delay: 0.1s; } 479 .reveal-delay-2 { transition-delay: 0.2s; } 480 .reveal-delay-3 { transition-delay: 0.3s; } 481 482 /* RESPONSIVE */ 483 @media (max-width: 768px) { 484 nav { padding: 1rem 1.25rem; } 485 .nav-links { display: none; } 486 section, #attack, #numbers, #compare, #cta, #port { padding: 4rem 1.25rem; } 487 .split { grid-template-columns: 1fr; gap: 2.5rem; } 488 .numbers-grid { grid-template-columns: 1fr; } 489 .steps-grid { grid-template-columns: 1fr; } 490 .port-grid { grid-template-columns: 1fr 1fr; } 491 footer { flex-direction: column; gap: 1rem; text-align: center; } 492 .footer-right { justify-content: center; } 493 } 494</style> 495</head> 496<body> 497 498<!-- NAV --> 499<nav> 500 <a href="#" class="nav-logo">i2p // python</a> 501 <ul class="nav-links"> 502 <li><a href="#attack">The Attack</a></li> 503 <li><a href="#compare">Why I2P</a></li> 504 <li><a href="blog.html">The Port</a></li> 505 <li><a href="the-fix.html">The Fix</a></li> 506 </ul> 507 <a href="https://github.com/Bimo-Studio/i2p-python" class="nav-cta">VIEW ON GITHUB</a> 508</nav> 509 510<!-- HERO --> 511<section id="hero"> 512 <canvas id="hero-canvas"></canvas> 513 514 <div class="hero-eyebrow"> 515 <span class="pulse-dot"></span> 516 LIVE THREAT — ETHEREUM MAINNET 517 </div> 518 519 <h1 class="hero-headline"> 520 YOU<br> 521 ARE<br> 522 <span class="threat">VISIBLE.</span> 523 </h1> 524 525 <p class="hero-sub"> 526 Researchers from ETH Zurich identified the home IP addresses of 15% of all 527 Ethereum validators — using four nodes and 72 hours of passive observation. 528 No exploit. No hacking. Just watching. 529 </p> 530 531 <div class="hero-stat-row"> 532 <div class="hero-stat"> 533 <div class="num" id="counter-validators">0</div> 534 <div class="label">VALIDATORS EXPOSED</div> 535 </div> 536 <div class="hero-stat"> 537 <div class="num">72h</div> 538 <div class="label">TIME TO DEANONYMIZE</div> 539 </div> 540 <div class="hero-stat"> 541 <div class="num">4</div> 542 <div class="label">NODES REQUIRED</div> 543 </div> 544 </div> 545 546 <div class="scroll-hint"> 547 <span>SCROLL</span> 548 <span class="scroll-arrow"></span> 549 </div> 550</section> 551 552<!-- ATTACK STEPS --> 553<div id="attack"> 554 <div class="attack-inner"> 555 <p class="section-tag">THE ATTACK</p> 556 <h2>How They <span class="accent">Find</span><br>Your Node</h2> 557 558 <div class="steps-grid"> 559 <div class="step reveal"> 560 <div class="step-num">STEP 01 ///</div> 561 <div class="step-title">Your node joins attestation subnets</div> 562 <div class="step-body"> 563 Every Ethereum validator subscribes to specific gossip subnets based on its 564 committee assignments. These subscriptions are announced publicly over 565 the <code>discv5</code> discovery protocol. Every peer you connect to can see which 566 subnets you're subscribed to. 567 </div> 568 </div> 569 <div class="step reveal reveal-delay-1"> 570 <div class="step-num">STEP 02 ///</div> 571 <div class="step-title">Subnet subscriptions are a fingerprint</div> 572 <div class="step-body"> 573 The combination of subnets a node is subscribed to at any given time is 574 highly unique. Like a fingerprint. And unlike a fingerprint, this fingerprint 575 is broadcast to every node in the network — including the four nodes an 576 attacker controls. 577 </div> 578 </div> 579 <div class="step reveal reveal-delay-2"> 580 <div class="step-num">STEP 03 ///</div> 581 <div class="step-title">The fingerprint maps to your IP</div> 582 <div class="step-body"> 583 Ethereum Node Records — the identity cards nodes exchange — include your 584 cleartext IP address alongside your fingerprint. Four observer nodes, 585 72 hours of data, and a correlation table. Heimbach et al., USENIX 2025: 586 over <code>15%</code> of all validators linked to real-world IP addresses. 587 </div> 588 </div> 589 </div> 590 </div> 591</div> 592 593<!-- BIG NUMBERS --> 594<div id="numbers"> 595 <div class="numbers-inner"> 596 <p class="section-tag">BY THE NUMBERS</p> 597 <h2>The Scale of<br><span class="accent">Exposure</span></h2> 598 599 <div class="numbers-grid"> 600 <div class="number-card reveal"> 601 <span class="big">15%</span> 602 <span class="description"> 603 OF ALL ACTIVE VALIDATORS<br> 604 SUCCESSFULLY DEANONYMIZED 605 </span> 606 <span class="source">Heimbach et al., USENIX Security 2025</span> 607 </div> 608 <div class="number-card reveal reveal-delay-1"> 609 <span class="big">900k+</span> 610 <span class="description"> 611 VALIDATORS ON ETHEREUM<br> 612 MAINNET TODAY 613 </span> 614 <span class="source">~135,000 validator IP addresses exposed in this attack</span> 615 </div> 616 <div class="number-card reveal reveal-delay-2"> 617 <span class="big">$0</span> 618 <span class="description"> 619 COST TO RUN THE ATTACK<br> 620 FOUR STANDARD NODES 621 </span> 622 <span class="source">No zero-days. No specialized hardware. Just observation.</span> 623 </div> 624 </div> 625 </div> 626</div> 627 628<!-- WHAT IS I2P / THE FIX TEASER --> 629<section id="what-is"> 630 <p class="section-tag">THE SOLUTION</p> 631 <h2>35 <span class="mint">Bytes.</span></h2> 632 633 <div class="split"> 634 <div class="split-text"> 635 <p> 636 Bitcoin solved this in 2021. BIP-155 added native I2P address support 637 to Bitcoin's peer discovery protocol. Monero followed. Both projects made 638 the same observation: if you want a node to be private, give it a private address. 639 </p> 640 <p> 641 Ethereum has <strong>Ethereum Node Records</strong> — a flexible key-value format 642 designed for exactly this kind of extension. Adding a new key requires no 643 consensus from the core team. The spec says so explicitly. 644 </p> 645 <p> 646 We're adding one. <strong><code style="color: var(--mint);">"i2p"</code></strong>: 647 a 32-byte hash of an I2P destination address. Any node that understands it 648 can route its peer connections through the I2P anonymization network. 649 Any node that doesn't — silently ignores it. 650 </p> 651 <p> 652 <strong>i2p-python</strong> is the native Python implementation of the I2P protocol 653 that makes this possible. A complete, from-scratch port of the Java I2P router — 654 3,240+ tests, 14 crypto primitives, NTCP2 and SSU2 transports, full SAM bridge. 655 Not a wrapper. Not a binding. A real implementation. 656 </p> 657 <p> 658 Zero breaking changes. Full backwards compatibility. Optional. Additive. 659 And it eliminates the attack vector entirely. 660 </p> 661 </div> 662 663 <div class="viz-box reveal"> 664 <div class="viz-comparison"> 665 <div class="viz-side"> 666 <span class="viz-side-label bad">// WITHOUT I2P</span> 667 <div class="node-row"> 668 <span class="node-dot exposed"></span> 669 <span class="node-text red">validator-001 &middot; 82.144.21.9</span> 670 </div> 671 <div class="node-row"> 672 <span class="node-dot exposed"></span> 673 <span class="node-text red">validator-002 &middot; 51.91.78.202</span> 674 </div> 675 <div class="node-row"> 676 <span class="node-dot exposed"></span> 677 <span class="node-text red">validator-003 &middot; 95.216.4.17</span> 678 </div> 679 <div class="node-row"> 680 <span class="node-dot attacker"></span> 681 <span class="node-text" style="color:var(--cyan)">observer &middot; 104.21.0.1</span> 682 </div> 683 <div style="margin-top: 1rem; font-family: var(--mono); font-size: 10px; color: rgba(255,43,74,0.6); line-height: 1.6;"> 684 ENR record includes:<br> 685 ip=82.144.21.9<br> 686 secp256k1=[pubkey]<br> 687 attnets=[bitmask] &larr; fingerprint 688 </div> 689 </div> 690 <div class="viz-side"> 691 <span class="viz-side-label good">// WITH I2P</span> 692 <div class="node-row"> 693 <span class="node-dot hidden"></span> 694 <span class="node-text green">validator-001 &middot; [hidden]</span> 695 </div> 696 <div class="node-row"> 697 <span class="node-dot hidden"></span> 698 <span class="node-text green">validator-002 &middot; [hidden]</span> 699 </div> 700 <div class="node-row"> 701 <span class="node-dot hidden"></span> 702 <span class="node-text green">validator-003 &middot; [hidden]</span> 703 </div> 704 <div class="node-row"> 705 <span class="node-dot attacker"></span> 706 <span class="node-text" style="color:var(--cyan)">observer &middot; 104.21.0.1</span> 707 </div> 708 <div style="margin-top: 1rem; font-family: var(--mono); font-size: 10px; color: rgba(0,229,160,0.6); line-height: 1.6;"> 709 ENR record includes:<br> 710 i2p=3a9f1c2d8e... &larr; 32 bytes<br> 711 secp256k1=[pubkey]<br> 712 attnets=[bitmask] 713 </div> 714 </div> 715 </div> 716 </div> 717 </div> 718</section> 719 720<!-- COMPARISON TABLE --> 721<div id="compare"> 722 <div class="compare-inner"> 723 <p class="section-tag">COMPETITIVE ANALYSIS</p> 724 <h2>Why Not<br><span class="accent">Tor?</span></h2> 725 726 <p style="font-size: 15px; color: var(--gray); max-width: 600px; margin-top: 1.5rem; line-height: 1.7;"> 727 Every existing privacy network was evaluated. Only one passes all the requirements 728 Ethereum's peer-to-peer stack actually demands. 729 </p> 730 731 <table class="compare-table reveal"> 732 <thead> 733 <tr> 734 <th>Requirement</th> 735 <th>Tor</th> 736 <th>Nym</th> 737 <th>HOPR</th> 738 <th>Lokinet</th> 739 <th class="highlight">I2P</th> 740 </tr> 741 </thead> 742 <tbody> 743 <tr> 744 <td>UDP support (discv5)</td> 745 <td class="cross">TCP only</td> 746 <td class="cross">No</td> 747 <td class="cross">No</td> 748 <td class="check">Yes</td> 749 <td class="highlight check">Yes</td> 750 </tr> 751 <tr> 752 <td>Hidden services (both sides)</td> 753 <td class="check">Yes</td> 754 <td class="cross">No</td> 755 <td class="cross">No</td> 756 <td class="check">Yes</td> 757 <td class="highlight check">Yes</td> 758 </tr> 759 <tr> 760 <td>No token required</td> 761 <td class="check">Yes</td> 762 <td class="cross">NYM token</td> 763 <td class="cross">HOPR token</td> 764 <td class="cross">OXEN stake</td> 765 <td class="highlight check">Yes</td> 766 </tr> 767 <tr> 768 <td>Latency &lt; 200ms</td> 769 <td class="muted">~150ms</td> 770 <td class="cross">500ms+</td> 771 <td class="cross">300ms+</td> 772 <td class="muted">~100ms</td> 773 <td class="highlight check">~80ms</td> 774 </tr> 775 <tr> 776 <td>Production network</td> 777 <td class="check">Yes</td> 778 <td class="cross">Testnet</td> 779 <td class="cross">Limited</td> 780 <td class="cross">Small</td> 781 <td class="highlight check">50k+ nodes</td> 782 </tr> 783 <tr> 784 <td>Bitcoin precedent (BIP-155)</td> 785 <td class="check">Yes</td> 786 <td class="cross">No</td> 787 <td class="cross">No</td> 788 <td class="cross">No</td> 789 <td class="highlight check">Yes</td> 790 </tr> 791 </tbody> 792 </table> 793 </div> 794</div> 795 796<!-- THE PORT --> 797<div id="port"> 798 <div class="port-inner"> 799 <p class="section-tag">THE IMPLEMENTATION</p> 800 <h2>i2p-python:<br><span class="mint">From Scratch</span></h2> 801 802 <p style="font-size: 15px; color: var(--gray); max-width: 600px; margin-top: 1.5rem; line-height: 1.7;"> 803 Not a wrapper. Not a binding. A complete reimplementation of the Java I2P router 804 in Python — every protocol, every crypto primitive, every transport layer. 805 </p> 806 807 <div class="port-grid"> 808 <div class="port-stat reveal"> 809 <span class="big">3,240+</span> 810 <span class="description">TESTS PASSING</span> 811 </div> 812 <div class="port-stat reveal reveal-delay-1"> 813 <span class="big">15</span> 814 <span class="description">SOURCE PACKAGES</span> 815 </div> 816 <div class="port-stat reveal reveal-delay-2"> 817 <span class="big">14</span> 818 <span class="description">CRYPTO PRIMITIVES</span> 819 </div> 820 <div class="port-stat reveal reveal-delay-3"> 821 <span class="big">34</span> 822 <span class="description">INDEPENDENT REPOS</span> 823 </div> 824 </div> 825 </div> 826</div> 827 828<!-- CTA --> 829<section id="cta"> 830 <div class="cta-inner"> 831 <p class="section-tag" style="justify-content: center;"> 832 GET INVOLVED 833 </p> 834 <h2>You Can Just<br><span class="mint">Build Things.</span></h2> 835 <p> 836 The implementation is here. The EIP is drafted. A test network is being stood up. 837 We're not waiting for permission — the ENR spec was designed to be extended without it. 838 </p> 839 <div class="cta-buttons"> 840 <a href="the-fix.html" class="btn-primary">SEE THE FIX</a> 841 <a href="blog.html" class="btn-secondary">READ THE BUILD LOG</a> 842 <a href="https://github.com/Bimo-Studio/i2p-python" class="btn-ghost">GITHUB</a> 843 </div> 844 </div> 845</section> 846 847<!-- FOOTER --> 848<footer> 849 <div class="footer-left"> 850 i2p // python — MIT LICENSE — A <a href="https://bimo.studio" style="color: var(--mint);">bimo.studio</a> PROJECT 851 </div> 852 <div class="footer-right"> 853 <a href="https://arxiv.org/abs/2409.04366">RESEARCH</a> 854 <a href="https://github.com/Bimo-Studio/i2p-python">GITHUB</a> 855 </div> 856</footer> 857 858<script> 859// HERO CANVAS 860const canvas = document.getElementById('hero-canvas'); 861const ctx = canvas.getContext('2d'); 862 863function resize() { 864 canvas.width = canvas.offsetWidth; 865 canvas.height = canvas.offsetHeight; 866} 867resize(); 868window.addEventListener('resize', resize); 869 870const nodes = []; 871const NODE_COUNT = 55; 872 873for (let i = 0; i < NODE_COUNT; i++) { 874 nodes.push({ 875 x: Math.random() * canvas.width, 876 y: Math.random() * canvas.height, 877 vx: (Math.random() - 0.5) * 0.35, 878 vy: (Math.random() - 0.5) * 0.35, 879 r: Math.random() * 2 + 1, 880 state: 'safe', 881 revealTime: 3000 + Math.random() * 12000, 882 revealed: false, 883 }); 884} 885 886const observers = [3, 17, 31, 42]; 887observers.forEach(i => { if (nodes[i]) nodes[i].state = 'observer'; }); 888 889let startTime = null; 890 891function draw(ts) { 892 if (!startTime) startTime = ts; 893 const elapsed = ts - startTime; 894 895 const W = canvas.width; 896 const H = canvas.height; 897 ctx.clearRect(0, 0, W, H); 898 899 nodes.forEach(n => { 900 n.x += n.vx; 901 n.y += n.vy; 902 if (n.x < 0 || n.x > W) n.vx *= -1; 903 if (n.y < 0 || n.y > H) n.vy *= -1; 904 if (!n.revealed && elapsed > n.revealTime && n.state === 'safe') { 905 n.revealed = true; 906 n.state = 'exposed'; 907 } 908 }); 909 910 for (let i = 0; i < nodes.length; i++) { 911 for (let j = i + 1; j < nodes.length; j++) { 912 const a = nodes[i], b = nodes[j]; 913 const dx = a.x - b.x, dy = a.y - b.y; 914 const dist = Math.sqrt(dx*dx + dy*dy); 915 if (dist < 130) { 916 const alpha = (1 - dist / 130) * 0.18; 917 let color = `rgba(138,149,176,${alpha})`; 918 if ((a.state === 'observer' || b.state === 'observer') && 919 (a.state === 'exposed' || b.state === 'exposed')) { 920 color = `rgba(255,43,74,${alpha * 2})`; 921 } else if (a.state === 'observer' || b.state === 'observer') { 922 color = `rgba(0,200,255,${alpha * 1.4})`; 923 } 924 ctx.beginPath(); 925 ctx.strokeStyle = color; 926 ctx.lineWidth = 0.5; 927 ctx.moveTo(a.x, a.y); 928 ctx.lineTo(b.x, b.y); 929 ctx.stroke(); 930 } 931 } 932 } 933 934 nodes.forEach(n => { 935 let fillColor; 936 if (n.state === 'observer') fillColor = 'rgba(0,200,255,0.9)'; 937 else if (n.state === 'exposed') fillColor = 'rgba(255,43,74,0.9)'; 938 else fillColor = 'rgba(138,149,176,0.5)'; 939 940 if (n.state === 'exposed') { 941 ctx.beginPath(); 942 ctx.arc(n.x, n.y, n.r * 5, 0, Math.PI * 2); 943 ctx.fillStyle = 'rgba(255,43,74,0.08)'; 944 ctx.fill(); 945 } 946 if (n.state === 'observer') { 947 ctx.beginPath(); 948 ctx.arc(n.x, n.y, n.r * 4, 0, Math.PI * 2); 949 ctx.fillStyle = 'rgba(0,200,255,0.1)'; 950 ctx.fill(); 951 } 952 953 ctx.beginPath(); 954 ctx.arc(n.x, n.y, n.r, 0, Math.PI * 2); 955 ctx.fillStyle = fillColor; 956 ctx.fill(); 957 }); 958 959 requestAnimationFrame(draw); 960} 961requestAnimationFrame(draw); 962 963// COUNTER 964function animateCounter(el, target, duration, suffix) { 965 let start = null; 966 function step(ts) { 967 if (!start) start = ts; 968 const p = Math.min((ts - start) / duration, 1); 969 const ease = 1 - Math.pow(1 - p, 3); 970 el.textContent = Math.round(ease * target).toLocaleString() + (suffix || ''); 971 if (p < 1) requestAnimationFrame(step); 972 } 973 requestAnimationFrame(step); 974} 975 976setTimeout(() => { 977 animateCounter(document.getElementById('counter-validators'), 135000, 2200, '+'); 978}, 1200); 979 980// SCROLL REVEAL 981const observer = new IntersectionObserver((entries) => { 982 entries.forEach(e => { 983 if (e.isIntersecting) { 984 e.target.classList.add('visible'); 985 observer.unobserve(e.target); 986 } 987 }); 988}, { threshold: 0.12 }); 989 990document.querySelectorAll('.reveal').forEach(el => observer.observe(el)); 991</script> 992</body> 993</html>