itsgoin/website/tech.html
Scott Reimers 800388cda4 ItsGoin v0.3.2 — Decentralized social media network
No central server, user-owned data, reverse-chronological feed.
Rust core + Tauri desktop + Android app + plain HTML/CSS/JS frontend.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 20:23:09 -04:00

202 lines
13 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>How It Works — ItsGoin</title>
<meta name="description" content="Technical overview of ItsGoin: mesh networking, encryption, sync protocol, content addressing, and peer discovery.">
<link rel="stylesheet" href="style.css">
</head>
<body>
<nav>
<a href="index.html" class="logo">ItsGoin</a>
<div class="links">
<a href="index.html">About</a>
<a href="tech.html" class="active">How It Works</a>
<a href="design.html">Design</a>
<a href="download.html">Download</a>
</div>
</nav>
<div class="container wide">
<section>
<h1 style="font-size: 2rem; font-weight: 800; letter-spacing: -0.03em; margin-bottom: 0.5rem;">How ItsGoin works</h1>
<p>A technical overview of the networking, encryption, and sync mechanisms. For the full design rationale, see the <a href="design.html">design document</a>.</p>
</section>
<!-- Networking -->
<section>
<h2>Networking: QUIC mesh over iroh</h2>
<p>ItsGoin uses <a href="https://iroh.computer">iroh</a> (v0.96) for peer-to-peer networking. iroh provides QUIC transport with built-in NAT traversal, hole punching, and mDNS LAN discovery. Every node has a persistent ed25519 identity key.</p>
<div class="card">
<h3>Connection types</h3>
<table>
<tr><th>Type</th><th>Slots</th><th>Lifetime</th><th>Purpose</th></tr>
<tr><td><strong>Mesh</strong></td><td>101 (desktop), 15 (mobile)</td><td>Long-lived</td><td>Structural backbone for routing and propagation</td></tr>
<tr><td><strong>Session</strong></td><td>20 (desktop), 5 (mobile)</td><td>Minutes</td><td>Active interactions: DMs, delivery, peer discovery</td></tr>
<tr><td><strong>Ephemeral</strong></td><td>Unlimited</td><td>Single request</td><td>One-off sync or blob fetch</td></tr>
</table>
</div>
<div class="card">
<h3>Mesh architecture (101 slots)</h3>
<p>Desktop nodes maintain 101 mesh connections divided into:</p>
<ul style="padding-left: 1.25rem; margin: 0.5rem 0; color: var(--text-muted);">
<li><strong>10 Preferred</strong> &mdash; bilateral agreements with close peers, protected from eviction</li>
<li><strong>71 Local</strong> &mdash; discovered through N2 diversity scoring</li>
<li><strong>20 Wide</strong> &mdash; random connections for network-wide reach</li>
</ul>
<p>New mesh connections require bilateral agreement: the connector sends an <code>InitialExchange</code> message, and the acceptor either grants a slot or sends <code>RefuseRedirect</code> with an alternative peer suggestion.</p>
</div>
<div class="card">
<h3>Growth loop</h3>
<p>Nodes reactively grow their mesh using diversity scoring. When a slot opens, the node evaluates N2 candidates (peers reported by mesh connections) and selects the one that maximizes reach into new parts of the network. The scoring formula:</p>
<pre><code>score = 1/reporter_count + 0.3 if not_already_in_N3</code></pre>
<p>Low reporter count means fewer of your existing peers know this candidate &mdash; connecting to them brings the most new reach. The growth loop backs off after 3 consecutive failures to avoid wasting resources on unreachable candidates.</p>
</div>
</section>
<!-- Knowledge layers -->
<section>
<h2>Knowledge layers: N1 / N2 / N3</h2>
<p>Each node has three layers of network awareness, built passively from mesh peer exchanges:</p>
<div class="card">
<table>
<tr><th>Layer</th><th>Contains</th><th>Source</th><th>Shared?</th></tr>
<tr><td><strong>N1 (Mesh)</strong></td><td>Live connections + social contacts</td><td>Direct</td><td>Yes (merged, no addresses)</td></tr>
<tr><td><strong>N2 (Reach)</strong></td><td>Peers' N1 shares</td><td>1 hop</td><td>Yes (NodeIds only)</td></tr>
<tr><td><strong>N3 (Search)</strong></td><td>Peers' N2 shares</td><td>2 hops</td><td>Never</td></tr>
</table>
</div>
<p><strong>Privacy guarantee</strong>: N1 shares merge mesh peers with social contacts into one list, making it impossible for outsiders to distinguish infrastructure connections from social relationships. Addresses are never shared &mdash; they're resolved on-demand through the chain.</p>
<p><strong>Discovery cascade</strong> when connecting to a specific peer:</p>
<ol class="steps">
<li><strong>Social route cache</strong> &mdash; cached addresses for follows/audience</li>
<li><strong>Peers table</strong> &mdash; stored addresses from previous connections</li>
<li><strong>N2: ask reporter</strong> &mdash; ask the mesh peer who reported the target</li>
<li><strong>N3: chain resolve</strong> &mdash; 2-hop chain through reporters</li>
<li><strong>Worm search</strong> &mdash; fan-out to all mesh peers, bloom to wide referrals</li>
<li><strong>Relay introduction</strong> &mdash; hole punch via intermediary</li>
<li><strong>Own-device relay</strong> &mdash; route through your own devices (e.g. home computer to phone) when direct connection fails</li>
</ol>
</section>
<!-- Encryption -->
<section>
<h2>Encryption: envelope model</h2>
<p>ItsGoin uses <strong>1-layer envelope encryption</strong> for all private content. The design preserves content addressing (PostId = BLAKE3 of ciphertext) while allowing visibility updates without changing the PostId.</p>
<div class="card">
<h3>Per-post encryption</h3>
<p>Each private post gets a random Content Encryption Key (CEK). The post body is encrypted with <strong>ChaCha20-Poly1305</strong> using this CEK. The CEK is then wrapped separately for each intended recipient using <strong>X25519 Diffie-Hellman</strong> derived from ed25519 identity keys.</p>
<pre><code>PostId = BLAKE3(ciphertext)
CEK = random 256-bit key
ciphertext = ChaCha20-Poly1305(post_body, CEK)
wrapped_key[i] = X25519_DH(author_ed25519, recipient_ed25519[i]) XOR CEK</code></pre>
<p>Visibility is separate metadata &mdash; re-wrapping the CEK for new recipients doesn't change the PostId or require re-encrypting the content.</p>
</div>
<div class="card">
<h3>Group keys for circles</h3>
<p>Circles (private groups) use per-circle ed25519 keypairs instead of per-recipient wrapping. A single DH operation wraps the CEK for the entire group, keeping overhead constant regardless of group size (~100 bytes vs. ~500 bytes per recipient).</p>
<p><strong>Epoch rotation</strong>: when a member is removed, the circle generates a new keypair (new epoch). The new seed is distributed to remaining members. Old posts remain readable with old keys; new posts use the new epoch. Forward secrecy without re-encrypting history.</p>
</div>
<div class="card">
<h3>Visibility levels</h3>
<table>
<tr><th>Level</th><th>Encryption</th><th>Audience</th></tr>
<tr><td><strong>Public</strong></td><td>None</td><td>Anyone</td></tr>
<tr><td><strong>Friends</strong></td><td>Per-recipient CEK wrapping</td><td>Mutual follows</td></tr>
<tr><td><strong>Circle</strong></td><td>Group key CEK wrapping</td><td>Circle members</td></tr>
<tr><td><strong>Direct</strong></td><td>Per-recipient CEK wrapping</td><td>Specified recipients</td></tr>
</table>
</div>
</section>
<!-- Sync -->
<section>
<h2>Sync protocol (v3)</h2>
<p>The protocol uses a single ALPN (<code>itsgoin/3</code>) with 37+ message types multiplexed over QUIC bi-streams and uni-streams.</p>
<div class="card">
<h3>Pull-based sync</h3>
<p>Every 5 minutes, nodes pull posts from connected peers. The <strong>sender</strong> filters posts before sending &mdash; only posts the requester is authorized to see (based on follows, encryption recipients, and audience membership). This means the requester never learns about posts they can't decrypt.</p>
</div>
<div class="card">
<h3>Push for immediacy</h3>
<p>Real-time push via uni-streams for instant delivery:</p>
<ul style="padding-left: 1.25rem; margin: 0.5rem 0; color: var(--text-muted);">
<li><strong>PostNotification</strong> (0x42) &mdash; lightweight "new post" alert to mesh peers</li>
<li><strong>PostPush</strong> (0x43) &mdash; direct encrypted delivery to recipients</li>
<li><strong>ProfileUpdate</strong> (0x50) &mdash; instant profile propagation</li>
<li><strong>DeleteRecord</strong> (0x51) &mdash; signed deletion propagation</li>
<li><strong>VisibilityUpdate</strong> (0x52) &mdash; re-wrap CEK without changing PostId</li>
</ul>
</div>
</section>
<!-- Files -->
<section>
<h2>Files &amp; content distribution</h2>
<div class="card">
<h3>Content-addressed blobs</h3>
<p>Files are stored as content-addressed blobs: <code>BlobId = BLAKE3(file_bytes)</code>. Stored on the filesystem in 256 shards (<code>blobs/{hex[0..2]}/{hex}</code>) with metadata in SQLite. Max 4 attachments per post, 10MB each.</p>
</div>
<div class="card">
<h3>CDN hosting tree</h3>
<p>Each blob has a hosting tree: 1 upstream source + up to 100 downstream nodes. <strong>AuthorManifest</strong> (ed25519-signed) carries the author's 10 most recent posts as a neighborhood, traveling with blob responses. <strong>ManifestPush</strong> propagates updates down the tree.</p>
<p>When a node evicts a blob, it sends <strong>BlobDeleteNotice</strong> so the tree can heal &mdash; downstream nodes find a new upstream.</p>
</div>
<div class="card">
<h3>Eviction priority</h3>
<p>When storage fills up, blobs are evicted using a social-aware scoring formula:</p>
<pre><code>priority = pin_boost + relationship * recency * freshness / (peer_copies + 1)</code></pre>
<p>Your own blobs are auto-pinned and never evicted. Blobs from people you follow score higher. Blobs with many copies elsewhere score lower (safe to drop).</p>
</div>
</section>
<!-- Anchors -->
<section>
<h2>Anchors: always-on peers</h2>
<p>Anchors are standard ItsGoin nodes running on stable servers. They're regular peers that happen to be always on, which makes them useful for two things:</p>
<ul style="padding-left: 1.25rem; margin: 0.5rem 0; color: var(--text-muted);">
<li><strong>Bootstrap</strong> &mdash; new nodes connect here to discover the network and get referrals to other peers</li>
<li><strong>Matchmaker</strong> &mdash; introduce peers to each other so they can establish direct connections</li>
</ul>
<p>Anchors maintain a referral list of recently-seen peers with tiered usage caps (3/2/1 uses depending on list size). When an anchor's mesh is full, it keeps session connections for matchmaking so it remains useful as a bootstrap point.</p>
<p>Anchors run the same code as every other node. No special protocol, no special trust, no special services. They're just peers that are always on.</p>
</section>
<!-- Stack -->
<section>
<h2>Technology stack</h2>
<div class="card">
<table>
<tr><th>Component</th><th>Technology</th></tr>
<tr><td>Core library</td><td>Rust</td></tr>
<tr><td>P2P networking</td><td>iroh 0.96 (QUIC + mDNS)</td></tr>
<tr><td>Local storage</td><td>SQLite (rusqlite 0.32)</td></tr>
<tr><td>Content addressing</td><td>BLAKE3</td></tr>
<tr><td>Encryption</td><td>ChaCha20-Poly1305 + X25519 (from ed25519)</td></tr>
<tr><td>Desktop/mobile shell</td><td>Tauri v2</td></tr>
<tr><td>Frontend</td><td>Plain HTML/CSS/JS (no build step)</td></tr>
<tr><td>Platforms</td><td>Android, Linux (AppImage)</td></tr>
</table>
</div>
</section>
</div>
<footer>
<p>ItsGoin &mdash; Apache 2.0 License &mdash; <a href="https://itsgoin.com">itsgoin.com</a></p>
</footer>
</body>
</html>