A Python port of the Invisible Internet Project (I2P)
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 · 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 · 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 · 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 · 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] ← 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 · [hidden]</span>
695 </div>
696 <div class="node-row">
697 <span class="node-dot hidden"></span>
698 <span class="node-text green">validator-002 · [hidden]</span>
699 </div>
700 <div class="node-row">
701 <span class="node-dot hidden"></span>
702 <span class="node-text green">validator-003 · [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 · 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... ← 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 < 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>