← Back to all projects

LEARN DECENTRALIZED PROTOCOLS NOSTR C

Learn Decentralized Protocols: From Zero to Nostr and Beyond

Goal: Deeply understand decentralized communication protocols—from cryptographic primitives to building a complete Nostr implementation and designing your own protocol, all in C.


Why This Matters

Every decentralized protocol solves the same fundamental problem: How do people communicate without trusting a central authority?

Whether it’s Nostr, ActivityPub, Matrix, or Bitcoin—they all rely on the same building blocks:

  • Cryptography: Proving identity without passwords
  • Networking: Moving messages between computers
  • Distributed Systems: Agreement without a central server
  • Protocol Design: Rules everyone follows

By building these from scratch in C, you’ll understand not just what these protocols do, but why they work the way they do. You’ll see the tradeoffs, understand the attack vectors, and be able to design your own.


The Decentralized Protocol Landscape

Why So Many Protocols?

Each protocol makes different tradeoffs:

                    CENTRALIZED ←──────────────────→ DECENTRALIZED
                         │                                  │
    Twitter/X ───────────┤                                  │
                         │                                  │
    Bluesky (AT Protocol)┼──────┐                           │
                         │      │ More portable             │
    Mastodon (ActivityPub)──────┼──────┐                    │
                         │      │      │ Federated          │
    Matrix ──────────────┼──────┼──────┼──────┐             │
                         │      │      │      │             │
    Nostr ───────────────┼──────┼──────┼──────┼─────────────┤ Simple, keypair identity
                         │      │      │      │             │
    Scuttlebutt ─────────┼──────┼──────┼──────┼─────────────┤ True P2P, offline-first
                         │      │      │      │             │
    IPFS + libp2p ───────┼──────┼──────┼──────┼─────────────┤ Content-addressed

Protocol Comparison

Protocol Identity Architecture Complexity Key Innovation
Nostr Keypair (secp256k1) Client-Relay Simple Dumb relays, smart clients
ActivityPub @user@server Federated servers Medium W3C standard, widespread
Matrix @user:homeserver Federated homeservers Complex E2E encryption, bridges
Scuttlebutt Keypair (ed25519) Pure P2P Medium Offline-first, append-only
AT Protocol DID + handle Federated with indexers Complex Portable identity
IPFS Keypair P2P + DHT Complex Content addressing

The Nostr Philosophy

Nostr stands for “Notes and Other Stuff Transmitted by Relays”

Its genius is simplicity:

  1. Users have keypairs (public key = identity)
  2. Events are signed JSON (tamper-proof)
  3. Relays store and forward events (dumb pipes)
  4. Clients do the hard work (filtering, display)

This is the opposite of traditional platforms where the server is smart and clients are dumb.


Core Concept Analysis

The Cryptographic Foundation

┌─────────────────────────────────────────────────────────────────┐
│                    CRYPTOGRAPHIC PRIMITIVES                     │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  HASHING (SHA-256)          SIGNING (Schnorr/secp256k1)         │
│  ┌───────────┐              ┌─────────────────────────────┐     │
│  │ message   │──hash──→     │  private key + message      │     │
│  └───────────┘              │           ↓                 │     │
│       ↓                     │      signature              │     │
│  ┌───────────┐              │           ↓                 │     │
│  │ 32 bytes  │              │  public key + sig + msg     │     │
│  │ (fixed)   │              │           ↓                 │     │
│  └───────────┘              │    true/false (verify)      │     │
│                             └─────────────────────────────┘     │
│                                                                 │
│  ENCRYPTION (NIP-44)        KEY DERIVATION                      │
│  ┌─────────────────────┐    ┌─────────────────────────────┐     │
│  │ my_priv + your_pub  │    │ seed phrase (BIP-39)        │     │
│  │         ↓           │    │           ↓                 │     │
│  │  shared secret      │    │ master key (BIP-32)         │     │
│  │         ↓           │    │           ↓                 │     │
│  │  encrypt(message)   │    │ derived keys (HD wallet)    │     │
│  └─────────────────────┘    └─────────────────────────────┘     │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

The Nostr Event Structure

Every piece of data in Nostr is an “event”:

{
  "id": "32-byte hex SHA-256 hash of serialized event",
  "pubkey": "32-byte hex public key of event creator",
  "created_at": 1234567890,
  "kind": 1,
  "tags": [
    ["e", "event_id_being_replied_to"],
    ["p", "pubkey_being_mentioned"]
  ],
  "content": "Hello, Nostr!",
  "sig": "64-byte hex Schnorr signature"
}

The id is computed by hashing:

[0, "pubkey", created_at, kind, tags, "content"]

The Client-Relay Model

┌──────────┐     WebSocket      ┌──────────┐     WebSocket      ┌──────────┐
│  Client  │◄──────────────────►│  Relay   │◄──────────────────►│  Client  │
│  (Alice) │                    │          │                    │  (Bob)   │
└──────────┘                    └──────────┘                    └──────────┘
     │                               │                               │
     │  ["EVENT", {...}]             │                               │
     │──────────────────────────────►│                               │
     │                               │  ["EVENT", {...}]             │
     │                               │──────────────────────────────►│
     │                               │                               │
     │  ["REQ", "sub1", {filters}]   │                               │
     │──────────────────────────────►│                               │
     │                               │                               │
     │  ["EVENT", "sub1", {...}]     │                               │
     │◄──────────────────────────────│                               │

Project 1: SHA-256 Implementation from Scratch

  • File: LEARN_DECENTRALIZED_PROTOCOLS_NOSTR_C.md
  • Main Programming Language: C
  • Alternative Programming Languages: Rust, Assembly, Go
  • Coolness Level: Level 4: Hardcore Tech Flex
  • Business Potential: 1. The “Resume Gold”
  • Difficulty: Level 2: Intermediate
  • Knowledge Area: Cryptography / Hashing
  • Software or Tool: None (pure C)
  • Main Book: “Serious Cryptography, 2nd Edition” by Jean-Philippe Aumasson

What you’ll build: A complete SHA-256 implementation that produces the exact same output as OpenSSL, following the NIST FIPS 180-4 specification.

Why it teaches decentralized protocols: Every decentralized protocol uses SHA-256. Nostr event IDs are SHA-256 hashes. Bitcoin blocks are double-SHA-256. Understanding hashing at the bit level makes everything else click.

Core challenges you’ll face:

  • Understanding message padding → maps to why input length matters
  • Implementing the compression function → maps to bit manipulation mastery
  • Working with 32-bit words and rotations → maps to low-level C operations
  • Matching official test vectors → maps to cryptographic correctness

Key Concepts:

  • Hash function properties: “Serious Cryptography”, Chapter 6 - Jean-Philippe Aumasson
  • SHA-256 specification: NIST FIPS 180-4 (free PDF)
  • Bit manipulation in C: “The C Programming Language”, Chapter 2 - K&R
  • Endianness and byte order: “Computer Systems: A Programmer’s Perspective”, Chapter 2 - Bryant & O’Hallaron

Difficulty: Intermediate Time estimate: 1 week Prerequisites: Solid C knowledge, understanding of binary/hex, bit operations

Real world outcome:

$ ./sha256 "Hello, Nostr!"
185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969

$ echo -n "Hello, Nostr!" | openssl dgst -sha256
SHA256(stdin)= 185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969

Your implementation produces the exact same output as OpenSSL!

Implementation Hints:

SHA-256 works in three phases:

1. Preprocessing (Padding):

  • Append a 1 bit to the message
  • Append 0 bits until length ≡ 448 (mod 512)
  • Append original length as 64-bit big-endian integer
  • Total is now a multiple of 512 bits (64 bytes)

2. Initialize hash values:

H[0] = 0x6a09e667  (first 32 bits of fractional part of sqrt(2))
H[1] = 0xbb67ae85  (sqrt(3))
H[2] = 0x3c6ef372  (sqrt(5))
... etc for primes 2,3,5,7,11,13,17,19

3. Process each 512-bit chunk:

  • Break chunk into sixteen 32-bit words W[0..15]
  • Extend to 64 words: W[16..63] using formula
  • Initialize working variables a,b,c,d,e,f,g,h from H
  • 64 rounds of compression using the round constants K[0..63]
  • Add compressed values back to H

The key operations:

  • ROTR(x, n): Rotate right by n bits
  • Ch(x,y,z): Choice - if x then y else z
  • Maj(x,y,z): Majority - at least 2 of 3 are 1
  • Σ0, Σ1, σ0, σ1: Specific rotation/shift combinations

Start with the test vectors from NIST to verify correctness.

Learning milestones:

  1. Padding works correctly → You understand message structure
  2. Single block hashes correctly → You understand the compression function
  3. Multi-block messages work → You understand chaining
  4. All test vectors pass → You’ve implemented real cryptography!

Project 2: Elliptic Curve Basics (secp256k1)

  • File: LEARN_DECENTRALIZED_PROTOCOLS_NOSTR_C.md
  • Main Programming Language: C
  • Alternative Programming Languages: Rust, Python (for learning), Go
  • Coolness Level: Level 5: Pure Magic (Super Cool)
  • Business Potential: 1. The “Resume Gold”
  • Difficulty: Level 3: Advanced
  • Knowledge Area: Cryptography / Elliptic Curves
  • Software or Tool: libsecp256k1 (Bitcoin’s library)
  • Main Book: “Serious Cryptography, 2nd Edition” by Jean-Philippe Aumasson

What you’ll build: A program that generates keypairs, signs messages using Schnorr signatures (BIP-340), and verifies signatures—the exact cryptographic operations Nostr uses.

Why it teaches decentralized protocols: Your identity in Nostr IS your public key. Understanding how keypairs work, why signatures are unforgeable, and how verification works is essential to understanding trustless systems.

Core challenges you’ll face:

  • Understanding elliptic curve math → maps to why this cryptography works
  • Using libsecp256k1 correctly → maps to using crypto libraries safely
  • Generating secure random numbers → maps to entropy and security
  • Implementing BIP-340 Schnorr signatures → maps to Nostr’s exact signature scheme

Key Concepts:

  • Elliptic curve cryptography: “Serious Cryptography”, Chapter 11 - Jean-Philippe Aumasson
  • BIP-340 specification: bips.dev/340
  • libsecp256k1 usage: bitcoin-core/secp256k1 examples
  • Secure random generation: Reading from /dev/urandom or using getrandom()

Difficulty: Advanced Time estimate: 1-2 weeks Prerequisites: Project 1 (SHA-256), basic understanding of modular arithmetic

Real world outcome:

$ ./keygen
Private key (keep secret!): 3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aadf7f5c39
Public key (share this): 97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322

$ ./sign 3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aadf7f5c39 "Hello, Nostr!"
Signature: 908a15e46fb4d8...

$ ./verify 97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322 "Hello, Nostr!" 908a15e46fb4d8...
Signature is VALID

Implementation Hints:

Setting up libsecp256k1:

git clone https://github.com/bitcoin-core/secp256k1.git
cd secp256k1
./autogen.sh
./configure --enable-module-schnorrsig --enable-module-extrakeys
make
sudo make install

Key concepts to understand:

  1. Elliptic curve: y² = x³ + 7 (over a prime field)
  2. Generator point G: A known point on the curve
  3. Private key: A random 256-bit integer k
  4. Public key: The point P = k × G (scalar multiplication)
  5. Schnorr signature: (R, s) where R is a point and s is a scalar

Why it’s secure: Given P, finding k is computationally infeasible (discrete log problem).

Using libsecp256k1:

// Pseudo-code structure
#include <secp256k1.h>
#include <secp256k1_schnorrsig.h>
#include <secp256k1_extrakeys.h>

// 1. Create context
secp256k1_context *ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN | SECP256K1_CONTEXT_VERIFY);

// 2. Generate 32 random bytes for private key
unsigned char seckey[32];
// Fill from /dev/urandom

// 3. Verify the key is valid (in range)
if (!secp256k1_ec_seckey_verify(ctx, seckey)) { /* regenerate */ }

// 4. Create keypair
secp256k1_keypair keypair;
secp256k1_keypair_create(ctx, &keypair, seckey);

// 5. Extract public key (x-only for BIP-340)
secp256k1_xonly_pubkey pubkey;
secp256k1_keypair_xonly_pub(ctx, &pubkey, NULL, &keypair);

// 6. Sign a 32-byte message hash
unsigned char sig[64];
unsigned char msg_hash[32]; // SHA-256 of message
unsigned char aux_rand[32]; // 32 bytes of randomness
secp256k1_schnorrsig_sign32(ctx, sig, msg_hash, &keypair, aux_rand);

// 7. Verify
int valid = secp256k1_schnorrsig_verify(ctx, sig, msg_hash, 32, &pubkey);

Important: Never use predictable randomness. Always read from a secure source.

Learning milestones:

  1. Generate valid keypairs → You understand key generation
  2. Sign and verify messages → You understand digital signatures
  3. Understand why it’s secure → You grasp the math
  4. Match Nostr test vectors → You’re ready for the protocol!

Project 3: JSON Parser in C

  • File: LEARN_DECENTRALIZED_PROTOCOLS_NOSTR_C.md
  • Main Programming Language: C
  • Alternative Programming Languages: Rust, Zig
  • Coolness Level: Level 3: Genuinely Clever
  • Business Potential: 2. The “Micro-SaaS / Pro Tool”
  • Difficulty: Level 2: Intermediate
  • Knowledge Area: Parsing / Data Formats
  • Software or Tool: None (pure C) or cJSON
  • Main Book: “Compilers: Principles and Practice” by Parag H. Dave

What you’ll build: A minimal JSON parser that can parse Nostr events, extract fields, and serialize events back to canonical JSON (required for signature verification).

Why it teaches decentralized protocols: Nostr uses JSON for all messages. But the signature is computed over canonical JSON—exact formatting matters. Understanding parsing deeply ensures you can correctly verify and create events.

Core challenges you’ll face:

  • Lexical analysis (tokenizing) → maps to breaking JSON into tokens
  • Recursive descent parsing → maps to handling nested structures
  • Memory management → maps to who owns the parsed data?
  • Canonical serialization → maps to exact formatting for signatures

Key Concepts:

  • JSON specification: RFC 8259 (ECMA-404)
  • Recursive descent parsing: “Compilers: Principles and Practice”, Chapter 4 - Dave & Dave
  • Memory management patterns in C: “21st Century C”, Chapter 6 - Ben Klemens
  • String handling in C: “C Interfaces and Implementations”, Chapter 3 - David Hanson

Difficulty: Intermediate Time estimate: 1-2 weeks Prerequisites: Strong C skills, understanding of recursion

Real world outcome:

$ cat event.json
{"kind":1,"content":"Hello!","pubkey":"97c70a44...","created_at":1234567890}

$ ./json_parse event.json
Parsed event:
  kind: 1
  content: "Hello!"
  pubkey: "97c70a44..."
  created_at: 1234567890

$ ./json_canonical event.json
[0,"97c70a44...",1234567890,1,[],"Hello!"]

Implementation Hints:

JSON has six value types:

  • object: {key: value, ...}
  • array: [value, ...]
  • string: "..."
  • number: integers and floats
  • boolean: true / false
  • null: null

Lexer (tokenizer):

typedef enum {
    TOKEN_LBRACE,    // {
    TOKEN_RBRACE,    // }
    TOKEN_LBRACKET,  // [
    TOKEN_RBRACKET,  // ]
    TOKEN_COLON,     // :
    TOKEN_COMMA,     // ,
    TOKEN_STRING,    // "..."
    TOKEN_NUMBER,    // 123, 1.5
    TOKEN_TRUE,
    TOKEN_FALSE,
    TOKEN_NULL,
    TOKEN_EOF
} TokenType;

Parser (recursive descent):

JsonValue* parse_value(Lexer *lex) {
    Token tok = peek(lex);
    switch (tok.type) {
        case TOKEN_LBRACE:   return parse_object(lex);
        case TOKEN_LBRACKET: return parse_array(lex);
        case TOKEN_STRING:   return parse_string(lex);
        case TOKEN_NUMBER:   return parse_number(lex);
        case TOKEN_TRUE:
        case TOKEN_FALSE:    return parse_bool(lex);
        case TOKEN_NULL:     return parse_null(lex);
        default:             return NULL; // error
    }
}

JsonValue* parse_array(Lexer *lex) {
    expect(lex, TOKEN_LBRACKET);
    JsonArray *arr = new_array();
    while (peek(lex).type != TOKEN_RBRACKET) {
        array_push(arr, parse_value(lex));
        if (peek(lex).type == TOKEN_COMMA) advance(lex);
    }
    expect(lex, TOKEN_RBRACKET);
    return wrap_array(arr);
}

Nostr canonical form (for signing): The event is serialized as a JSON array:

[0, <pubkey>, <created_at>, <kind>, <tags>, <content>]
  • No spaces
  • No trailing commas
  • UTF-8 strings (no escaping beyond required)
  • Numbers as integers (no decimals)

This exact serialization is SHA-256 hashed to create the event ID.

Alternative: Use cJSON - a lightweight, single-file JSON parser. But building your own teaches you more!

Learning milestones:

  1. Parse simple values → You understand tokenizing
  2. Parse nested objects/arrays → You understand recursive parsing
  3. Serialize to canonical form → You understand Nostr’s requirements
  4. Round-trip (parse → serialize → parse) → You have a working parser!

Project 4: TCP Echo Server

  • File: LEARN_DECENTRALIZED_PROTOCOLS_NOSTR_C.md
  • Main Programming Language: C
  • Alternative Programming Languages: Rust, Go
  • Coolness Level: Level 2: Practical but Forgettable
  • Business Potential: 1. The “Resume Gold”
  • Difficulty: Level 2: Intermediate
  • Knowledge Area: Networking / Sockets
  • Software or Tool: POSIX sockets
  • Main Book: “The Linux Programming Interface” by Michael Kerrisk

What you’ll build: A TCP server that accepts connections and echoes back whatever clients send—the foundation for all network protocols.

Why it teaches decentralized protocols: Every protocol runs over networks. Understanding sockets, connections, and the client-server model is fundamental. This is the simplest possible server that actually works.

Core challenges you’ll face:

  • Socket creation and binding → maps to how servers listen
  • Accept and connection handling → maps to client connections
  • Reading and writing data → maps to message exchange
  • Handling multiple clients → maps to concurrent connections

Key Concepts:

  • BSD sockets API: “The Linux Programming Interface”, Chapters 56-61 - Michael Kerrisk
  • TCP/IP basics: “TCP/IP Illustrated, Volume 1”, Chapters 1-4 - W. Richard Stevens
  • Concurrency models: “Advanced Programming in the UNIX Environment”, Chapter 16 - Stevens & Rago
  • Select/poll/epoll: “The Linux Programming Interface”, Chapter 63 - Michael Kerrisk

Difficulty: Intermediate Time estimate: Weekend Prerequisites: Basic C, understanding of file descriptors

Real world outcome:

# Terminal 1: Start server
$ ./echo_server 8080
Server listening on port 8080...
Client connected from 127.0.0.1:54321
Received: Hello, server!
Client disconnected

# Terminal 2: Connect client
$ nc localhost 8080
Hello, server!
Hello, server!
^C

Implementation Hints:

The socket dance:

// 1. Create socket
int server_fd = socket(AF_INET, SOCK_STREAM, 0);

// 2. Set socket options (reuse address)
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

// 3. Bind to address
struct sockaddr_in addr = {
    .sin_family = AF_INET,
    .sin_port = htons(8080),
    .sin_addr.s_addr = INADDR_ANY
};
bind(server_fd, (struct sockaddr*)&addr, sizeof(addr));

// 4. Listen for connections
listen(server_fd, 10);  // backlog of 10

// 5. Accept connections (blocking)
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len);

// 6. Read and write
char buffer[1024];
ssize_t n = read(client_fd, buffer, sizeof(buffer));
write(client_fd, buffer, n);  // echo back

// 7. Close
close(client_fd);
close(server_fd);

Handling multiple clients:

Option 1: Fork per client (simple but heavy)

while (1) {
    int client_fd = accept(server_fd, ...);
    if (fork() == 0) {
        // Child process handles client
        close(server_fd);
        handle_client(client_fd);
        exit(0);
    }
    close(client_fd);  // Parent closes client fd
}

Option 2: Select/poll (event-driven)

fd_set read_fds;
FD_ZERO(&read_fds);
FD_SET(server_fd, &read_fds);
// Add all client fds...

select(max_fd + 1, &read_fds, NULL, NULL, NULL);

if (FD_ISSET(server_fd, &read_fds)) {
    // New connection
}
for each client_fd:
    if (FD_ISSET(client_fd, &read_fds)) {
        // Data from client
    }

Option 3: Epoll (Linux, most scalable) - for later projects

Learning milestones:

  1. Single client connects and echoes → You understand basic sockets
  2. Multiple sequential clients work → You understand accept loop
  3. Concurrent clients work → You understand multiplexing
  4. Server handles disconnections gracefully → You understand error handling

Project 5: WebSocket Server from Scratch

  • File: LEARN_DECENTRALIZED_PROTOCOLS_NOSTR_C.md
  • Main Programming Language: C
  • Alternative Programming Languages: Rust, Go, Node.js
  • Coolness Level: Level 4: Hardcore Tech Flex
  • Business Potential: 2. The “Micro-SaaS / Pro Tool”
  • Difficulty: Level 3: Advanced
  • Knowledge Area: Networking / WebSocket Protocol
  • Software or Tool: OpenSSL (for SHA-1), or libwebsockets
  • Main Book: “TCP/IP Sockets in C, 2nd Edition” by Donahoo & Calvert

What you’ll build: A WebSocket server that handles the HTTP upgrade handshake, frames messages according to RFC 6455, and maintains persistent connections.

Why it teaches decentralized protocols: Nostr relays communicate with clients over WebSocket. Understanding the framing, masking, and ping/pong keep-alive is essential for building a relay.

Core challenges you’ll face:

  • HTTP upgrade handshake → maps to protocol negotiation
  • WebSocket frame parsing → maps to binary protocol handling
  • Masking/unmasking data → maps to security requirements
  • Handling fragmentation → maps to message reassembly

Key Concepts:

  • WebSocket specification: RFC 6455
  • HTTP/1.1 basics: RFC 2616 (understanding headers)
  • Binary parsing: Working with variable-length fields
  • Base64 encoding: For the handshake key

Resources for WebSocket implementation:

Difficulty: Advanced Time estimate: 2 weeks Prerequisites: Project 4 (TCP Echo Server), SHA-1 (can use OpenSSL)

Real world outcome:

# Server running
$ ./ws_server 8080
WebSocket server listening on port 8080...
New connection from 127.0.0.1
Received: {"type": "hello"}
Sent: {"type": "welcome"}

# Browser console
let ws = new WebSocket("ws://localhost:8080");
ws.onmessage = (e) => console.log(e.data);
ws.send(JSON.stringify({type: "hello"}));
// Output: {"type": "welcome"}

Implementation Hints:

The WebSocket handshake:

Client sends:

GET /chat HTTP/1.1
Host: localhost:8080
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

Server responds:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

The Sec-WebSocket-Accept is computed:

char *magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
char combined[256];
sprintf(combined, "%s%s", client_key, magic);
unsigned char sha1_hash[20];
SHA1(combined, strlen(combined), sha1_hash);
char *accept = base64_encode(sha1_hash, 20);

WebSocket frame format:

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|     Extended payload length continued, if payload len == 127  |
+ - - - - - - - - - - - - - - - +-------------------------------+
|                               |Masking-key, if MASK set to 1  |
+-------------------------------+-------------------------------+
| Masking-key (continued)       |          Payload Data         |
+-------------------------------- - - - - - - - - - - - - - - - +
:                     Payload Data continued ...                :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|                     Payload Data (continued)                  |
+---------------------------------------------------------------+

Key fields:

  • FIN: Is this the final fragment?
  • Opcode: 0x1 = text, 0x2 = binary, 0x8 = close, 0x9 = ping, 0xA = pong
  • MASK: Client-to-server messages MUST be masked
  • Payload length: 7 bits, or 7 bits = 126 + 16 bits, or 7 bits = 127 + 64 bits

Unmasking:

for (int i = 0; i < payload_len; i++) {
    payload[i] ^= mask[i % 4];
}

Learning milestones:

  1. Handshake succeeds → You understand HTTP upgrade
  2. Receive unmasked text frames → You understand frame parsing
  3. Send frames back → You understand frame construction
  4. Handle ping/pong → You understand keep-alive
  5. Browser connects successfully → You have a working WebSocket server!

Project 6: Minimal Nostr Client

  • File: LEARN_DECENTRALIZED_PROTOCOLS_NOSTR_C.md
  • Main Programming Language: C
  • Alternative Programming Languages: Rust, Go, Python
  • Coolness Level: Level 4: Hardcore Tech Flex
  • Business Potential: 2. The “Micro-SaaS / Pro Tool”
  • Difficulty: Level 3: Advanced
  • Knowledge Area: Protocol Implementation / Nostr
  • Software or Tool: libsecp256k1, libwebsockets (or your own)
  • Main Book: “TCP/IP Illustrated, Volume 1” by W. Richard Stevens

What you’ll build: A command-line Nostr client that can generate keypairs, connect to relays, publish notes, and subscribe to events.

Why it teaches decentralized protocols: This is where everything comes together. You’re building a real Nostr client that interoperates with the existing network. Your notes will appear on other Nostr clients!

Core challenges you’ll face:

  • Event creation and signing → maps to NIP-01 compliance
  • Relay connection management → maps to WebSocket client
  • Subscription filtering → maps to REQ message construction
  • Event verification → maps to signature validation

Key Concepts:

  • NIP-01: Basic protocol
  • NIP-19: Bech32 encoding (npub, nsec, note)
  • Client-relay messages: EVENT, REQ, CLOSE, EOSE
  • Event kinds: 0 = metadata, 1 = text note, 3 = contacts, etc.

Resources:

Difficulty: Advanced Time estimate: 2-3 weeks Prerequisites: Projects 1-5 (SHA-256, crypto, JSON, WebSocket)

Real world outcome:

# Generate identity
$ ./nostr-cli keygen
Secret key (nsec): nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laq46le3s
Public key (npub): npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63qhgvfd8

# Post a note
$ ./nostr-cli post --sec nsec1... "Hello from my C client!"
Event ID: 1234abcd...
Published to 3 relays

# Subscribe to notes from a pubkey
$ ./nostr-cli sub --author npub1sg6plzptd64u...
[2024-01-15 10:30:45] Hello from my C client!
[2024-01-15 10:28:12] Testing, testing...

Implementation Hints:

Event structure (NIP-01):

typedef struct {
    char id[65];        // 32 bytes hex
    char pubkey[65];    // 32 bytes hex
    uint64_t created_at;
    int kind;
    char **tags;        // Array of string arrays
    int tag_count;
    char *content;
    char sig[129];      // 64 bytes hex
} NostrEvent;

Creating an event:

NostrEvent* create_event(const char *privkey_hex, int kind, const char *content) {
    NostrEvent *event = malloc(sizeof(NostrEvent));

    // Get public key from private key
    get_pubkey_from_privkey(privkey_hex, event->pubkey);

    event->created_at = time(NULL);
    event->kind = kind;
    event->content = strdup(content);
    event->tags = NULL;
    event->tag_count = 0;

    // Compute ID (hash of canonical JSON)
    char *canonical = serialize_for_id(event);
    sha256(canonical, event->id);

    // Sign the ID
    schnorr_sign(privkey_hex, event->id, event->sig);

    return event;
}

Canonical serialization for ID:

[0, "<pubkey>", <created_at>, <kind>, <tags>, "<content>"]

Client-to-relay messages:

// Publish event
["EVENT", <event JSON>]

// Subscribe
["REQ", "<subscription_id>", {"kinds": [1], "limit": 10}]

// Unsubscribe
["CLOSE", "<subscription_id>"]

Relay-to-client messages:

// Event matching subscription
["EVENT", "<subscription_id>", <event JSON>]

// End of stored events
["EOSE", "<subscription_id>"]

// Notice (informational)
["NOTICE", "<message>"]

// OK (event accepted/rejected)
["OK", "<event_id>", true/false, "<message>"]

Popular public relays to test with:

  • wss://relay.damus.io
  • wss://nos.lol
  • wss://relay.nostr.band

Learning milestones:

  1. Generate valid keypairs → You understand Nostr identity
  2. Connect to relay via WebSocket → You understand transport
  3. Publish event that other clients see → You’re on Nostr!
  4. Subscribe and receive events → You understand the full flow

Project 7: Nostr Relay from Scratch

  • File: LEARN_DECENTRALIZED_PROTOCOLS_NOSTR_C.md
  • Main Programming Language: C
  • Alternative Programming Languages: Rust, Go, TypeScript
  • Coolness Level: Level 5: Pure Magic (Super Cool)
  • Business Potential: 3. The “Service & Support” Model
  • Difficulty: Level 4: Expert
  • Knowledge Area: Protocol Implementation / Server Architecture
  • Software or Tool: SQLite, libwebsockets, libsecp256k1
  • Main Book: “The Linux Programming Interface” by Michael Kerrisk

What you’ll build: A complete Nostr relay that stores events, handles subscriptions, validates signatures, and serves multiple clients concurrently.

Why it teaches decentralized protocols: The relay is the “server” in Nostr’s architecture—but it’s a dumb pipe, not an authority. Building one teaches you about event storage, query optimization, and scalable server design.

Core challenges you’ll face:

  • Event storage and indexing → maps to database design
  • Subscription management → maps to real-time filtering
  • Concurrent client handling → maps to async I/O
  • Signature verification → maps to trustless validation

Key Concepts:

  • NIP-01: Basic relay behavior
  • NIP-11: Relay information document
  • SQLite in C: sqlite.org/cintro.html
  • Event-driven architecture: epoll/kqueue patterns

Difficulty: Expert Time estimate: 1 month Prerequisites: All previous projects

Real world outcome:

$ ./nostr-relay --port 7777 --db ./events.db
Nostr relay starting...
Loaded 1,234 events from database
Listening on ws://localhost:7777
NIP-11 info at http://localhost:7777

# In another terminal
$ ./nostr-cli post --relay ws://localhost:7777 "Hello, my relay!"
Event published!

$ ./nostr-cli sub --relay ws://localhost:7777 --kinds 1
[Your event appears]

Implementation Hints:

Database schema (SQLite):

CREATE TABLE events (
    id TEXT PRIMARY KEY,      -- 32-byte hex event ID
    pubkey TEXT NOT NULL,     -- 32-byte hex public key
    created_at INTEGER NOT NULL,
    kind INTEGER NOT NULL,
    tags TEXT,                -- JSON array
    content TEXT,
    sig TEXT NOT NULL,        -- 64-byte hex signature
    raw TEXT NOT NULL         -- Original JSON for serving
);

CREATE INDEX idx_pubkey ON events(pubkey);
CREATE INDEX idx_kind ON events(kind);
CREATE INDEX idx_created_at ON events(created_at);
CREATE INDEX idx_pubkey_kind ON events(pubkey, kind);

Subscription filter matching:

typedef struct {
    char **ids;         // Event IDs to match
    char **authors;     // Pubkeys to match
    int *kinds;         // Event kinds to match
    char ***tags;       // Tag filters (#e, #p, etc.)
    uint64_t since;     // Events after this time
    uint64_t until;     // Events before this time
    int limit;          // Max events to return
} NostrFilter;

bool event_matches_filter(NostrEvent *event, NostrFilter *filter) {
    if (filter->ids && !in_list(event->id, filter->ids)) return false;
    if (filter->authors && !in_list(event->pubkey, filter->authors)) return false;
    if (filter->kinds && !int_in_list(event->kind, filter->kinds)) return false;
    if (filter->since && event->created_at < filter->since) return false;
    if (filter->until && event->created_at > filter->until) return false;
    // Tag matching...
    return true;
}

Connection handling (pseudo-code):

typedef struct {
    int fd;
    ws_connection *ws;
    Subscription *subs;     // Linked list of subscriptions
    int sub_count;
} ClientConnection;

void handle_message(ClientConnection *client, char *msg) {
    JsonValue *json = json_parse(msg);
    char *type = json_array_get_string(json, 0);

    if (strcmp(type, "EVENT") == 0) {
        NostrEvent *event = parse_event(json_array_get(json, 1));
        if (verify_event(event)) {
            store_event(event);
            broadcast_to_subscribers(event);
            send_ok(client, event->id, true, "");
        } else {
            send_ok(client, event->id, false, "invalid: bad signature");
        }
    }
    else if (strcmp(type, "REQ") == 0) {
        char *sub_id = json_array_get_string(json, 1);
        NostrFilter *filter = parse_filter(json_array_get(json, 2));
        add_subscription(client, sub_id, filter);

        // Send stored events matching filter
        query_and_send_events(client, sub_id, filter);
        send_eose(client, sub_id);
    }
    else if (strcmp(type, "CLOSE") == 0) {
        char *sub_id = json_array_get_string(json, 1);
        remove_subscription(client, sub_id);
    }
}

NIP-11 relay information:

{
  "name": "My C Relay",
  "description": "A Nostr relay written in C from scratch",
  "pubkey": "...",
  "contact": "admin@example.com",
  "supported_nips": [1, 11, 20],
  "software": "c-relay",
  "version": "0.1.0"
}

Learning milestones:

  1. Accept connections → Basic WebSocket server works
  2. Store and retrieve events → Database integration works
  3. Handle subscriptions → Real-time filtering works
  4. Verify signatures → Trustless operation achieved
  5. Your client connects to your relay → Full stack Nostr!

Project 8: Bech32 Encoding (NIP-19)

  • File: LEARN_DECENTRALIZED_PROTOCOLS_NOSTR_C.md
  • Main Programming Language: C
  • Alternative Programming Languages: Rust, Go
  • Coolness Level: Level 3: Genuinely Clever
  • Business Potential: 1. The “Resume Gold”
  • Difficulty: Level 2: Intermediate
  • Knowledge Area: Encoding / Error Detection
  • Software or Tool: None (pure C)
  • Main Book: “Serious Cryptography, 2nd Edition” by Jean-Philippe Aumasson

What you’ll build: A Bech32/Bech32m encoder and decoder for Nostr’s human-readable key formats (npub, nsec, note, nprofile, nevent, etc.).

Why it teaches decentralized protocols: Human-readable formats prevent errors when sharing keys. Bech32’s error detection catches typos. NIP-19 uses this for all user-facing identifiers.

Core challenges you’ll face:

  • Understanding Bech32 encoding → maps to base conversion with checksums
  • Implementing the checksum → maps to polynomial arithmetic
  • Handling different NIP-19 types → maps to TLV encoding
  • Error detection → maps to why Bech32 was designed this way

Key Concepts:

  • BIP-173: Original Bech32 specification
  • NIP-19: Bech32-encoded entities
  • GF(32) arithmetic: The math behind Bech32 checksums
  • TLV encoding: Type-Length-Value for complex types

Difficulty: Intermediate Time estimate: 1 week Prerequisites: Understanding of binary/hex, basic C

Real world outcome:

$ ./bech32 encode npub 97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322
npub1jlrsty3k6n9f4c29kveelju4gd0lwz67vndnpr3qvvwpddnr3c3qkx7zxe

$ ./bech32 decode npub1jlrsty3k6n9f4c29kveelju4gd0lwz67vndnpr3qvvwpddnr3c3qkx7zxe
Type: npub
Hex: 97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322

$ ./bech32 decode npub1jlrsty3k6n9f4c29kveelju4gd0lwz67vndnpr3qvvwpddnr3c3qkx7zxf
ERROR: Invalid checksum (typo in last character)

Implementation Hints:

Bech32 alphabet (no 1, b, i, o to avoid confusion):

const char *BECH32_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";

Encoding process:

  1. Convert data bytes to 5-bit groups
  2. Prepend human-readable part (HRP) like “npub”
  3. Compute 6-character checksum
  4. Concatenate: HRP + “1” + data + checksum

NIP-19 types: | Prefix | Content | |——–|———| | npub | 32-byte public key | | nsec | 32-byte private key | | note | 32-byte event ID | | nprofile | TLV: pubkey + relays | | nevent | TLV: event ID + relays + author | | naddr | TLV: identifier + pubkey + kind + relays |

5-bit conversion:

// Convert 8-bit bytes to 5-bit groups
int convert_bits(uint8_t *out, size_t *outlen,
                 const uint8_t *in, size_t inlen,
                 int frombits, int tobits, int pad) {
    uint32_t acc = 0;
    int bits = 0;
    const uint32_t maxv = (1 << tobits) - 1;
    *outlen = 0;

    for (size_t i = 0; i < inlen; i++) {
        acc = (acc << frombits) | in[i];
        bits += frombits;
        while (bits >= tobits) {
            bits -= tobits;
            out[(*outlen)++] = (acc >> bits) & maxv;
        }
    }
    if (pad && bits > 0) {
        out[(*outlen)++] = (acc << (tobits - bits)) & maxv;
    }
    return 1;
}

Checksum (polynomial over GF(32)):

uint32_t bech32_polymod(uint8_t *values, size_t len) {
    uint32_t c = 1;
    for (size_t i = 0; i < len; i++) {
        uint8_t c0 = c >> 25;
        c = ((c & 0x1ffffff) << 5) ^ values[i];
        if (c0 & 1) c ^= 0x3b6a57b2;
        if (c0 & 2) c ^= 0x26508e6d;
        if (c0 & 4) c ^= 0x1ea119fa;
        if (c0 & 8) c ^= 0x3d4233dd;
        if (c0 & 16) c ^= 0x2a1462b3;
    }
    return c;
}

Learning milestones:

  1. Encode npub correctly → You understand the basic encoding
  2. Decode back to hex → Round-trip works
  3. Checksum catches errors → Error detection works
  4. Handle nprofile with TLV → You understand complex types

Project 9: Encrypted Direct Messages (NIP-44)

  • File: LEARN_DECENTRALIZED_PROTOCOLS_NOSTR_C.md
  • Main Programming Language: C
  • Alternative Programming Languages: Rust, Go
  • Coolness Level: Level 4: Hardcore Tech Flex
  • Business Potential: 2. The “Micro-SaaS / Pro Tool”
  • Difficulty: Level 3: Advanced
  • Knowledge Area: Cryptography / Encryption
  • Software or Tool: libsecp256k1, libsodium (for ChaCha20)
  • Main Book: “Serious Cryptography, 2nd Edition” by Jean-Philippe Aumasson

What you’ll build: Encrypted direct messaging for Nostr using NIP-44’s XChaCha20-Poly1305 encryption with ECDH key agreement.

Why it teaches decentralized protocols: Privacy in decentralized systems requires end-to-end encryption. NIP-44 shows how two parties with only public keys can establish a shared secret and communicate privately.

Core challenges you’ll face:

  • ECDH shared secret → maps to Diffie-Hellman key exchange
  • Key derivation (HKDF) → maps to turning shared secrets into keys
  • Authenticated encryption → maps to ChaCha20-Poly1305
  • Nonce handling → maps to security requirements

Key Concepts:

  • NIP-44: Encrypted Payloads
  • ECDH: Elliptic Curve Diffie-Hellman
  • HKDF: HMAC-based Key Derivation Function
  • XChaCha20-Poly1305: Authenticated encryption

Difficulty: Advanced Time estimate: 2 weeks Prerequisites: Projects 2 (ECC basics), strong crypto understanding

Real world outcome:

# Alice encrypts a message to Bob
$ ./nip44 encrypt \
    --my-sec nsec1alice... \
    --their-pub npub1bob... \
    --message "Secret message for Bob"
Ciphertext: AfKxPy3jN2... (base64)

# Bob decrypts
$ ./nip44 decrypt \
    --my-sec nsec1bob... \
    --their-pub npub1alice... \
    --ciphertext "AfKxPy3jN2..."
Plaintext: Secret message for Bob

Implementation Hints:

NIP-44 encryption flow:

Alice (sender)                              Bob (recipient)
---------------                             ---------------
alice_priv, alice_pub                       bob_priv, bob_pub

1. ECDH:
   shared_point = alice_priv × bob_pub      shared_point = bob_priv × alice_pub
   (These are the same point!)

2. Conversation key:
   conversation_key = HKDF(shared_point.x, "nip44-v2")

3. Encryption (Alice):
   nonce = random 24 bytes
   ciphertext = XChaCha20-Poly1305(conversation_key, nonce, plaintext)
   message = version || nonce || ciphertext || mac

4. Decryption (Bob):
   parse message → nonce, ciphertext
   plaintext = XChaCha20-Poly1305_decrypt(conversation_key, nonce, ciphertext)

Using libsecp256k1 for ECDH:

secp256k1_pubkey their_pubkey;
secp256k1_ec_pubkey_parse(ctx, &their_pubkey, their_pubkey_bytes, 33);

unsigned char shared_secret[32];
secp256k1_ecdh(ctx, shared_secret, &their_pubkey, my_private_key, NULL, NULL);

HKDF (key derivation):

// Simplified - use a proper crypto library
void hkdf_sha256(const uint8_t *ikm, size_t ikm_len,
                 const uint8_t *salt, size_t salt_len,
                 const uint8_t *info, size_t info_len,
                 uint8_t *okm, size_t okm_len);

XChaCha20-Poly1305 (use libsodium):

#include <sodium.h>

// Encrypt
crypto_aead_xchacha20poly1305_ietf_encrypt(
    ciphertext, &ciphertext_len,
    plaintext, plaintext_len,
    NULL, 0,  // Additional data
    NULL,     // nsec (not used)
    nonce,
    key
);

// Decrypt
int result = crypto_aead_xchacha20poly1305_ietf_decrypt(
    plaintext, &plaintext_len,
    NULL,
    ciphertext, ciphertext_len,
    NULL, 0,
    nonce,
    key
);
if (result != 0) { /* tampering detected! */ }

Message format (NIP-44 v2):

[1 byte version = 0x02]
[24 bytes nonce]
[2 bytes padded length (big-endian)]
[padded ciphertext]
[16 bytes Poly1305 MAC]

Learning milestones:

  1. ECDH produces same secret for both parties → You understand key agreement
  2. Encryption/decryption round-trips → Symmetric crypto works
  3. Tampered messages fail verification → You understand authentication
  4. Interoperates with other NIP-44 implementations → You’re compatible!

Project 10: Kademlia DHT Implementation

  • File: LEARN_DECENTRALIZED_PROTOCOLS_NOSTR_C.md
  • Main Programming Language: C
  • Alternative Programming Languages: Rust, Go, Python
  • Coolness Level: Level 5: Pure Magic (Super Cool)
  • Business Potential: 4. The “Open Core” Infrastructure
  • Difficulty: Level 4: Expert
  • Knowledge Area: Distributed Systems / P2P Networking
  • Software or Tool: None (pure C with sockets)
  • Main Book: “Designing Data-Intensive Applications” by Martin Kleppmann

What you’ll build: A distributed hash table using the Kademlia algorithm—the same technology that powers BitTorrent, IPFS, and many P2P networks.

Why it teaches decentralized protocols: DHTs solve the discovery problem: how do you find data (or peers) without a central server? Understanding Kademlia unlocks understanding of all P2P networks.

Core challenges you’ll face:

  • XOR distance metric → maps to how Kademlia organizes nodes
  • K-buckets and routing tables → maps to efficient peer discovery
  • Node lookup algorithm → maps to iterative querying
  • Storing and retrieving values → maps to distributed storage

Key Concepts:

  • Kademlia paper: “Kademlia: A Peer-to-peer Information System Based on the XOR Metric” - Maymounkov & Mazières
  • BEP-5: BitTorrent DHT specification
  • jech/dht: C implementation reference
  • Distributed systems: “Designing Data-Intensive Applications”, Chapter 5 - Kleppmann

Difficulty: Expert Time estimate: 1 month Prerequisites: Strong networking knowledge, understanding of distributed systems

Real world outcome:

# Start first node (bootstrap)
$ ./kademlia-node --port 6881 --bootstrap
Node ID: a3f8c2b1...
Listening on port 6881
Routing table: 0 nodes

# Start second node, joining network
$ ./kademlia-node --port 6882 --join 127.0.0.1:6881
Node ID: 7d2e9f4a...
Joined network via 127.0.0.1:6881
Routing table: 1 node

# Store a value
$ ./kademlia-cli put mykey "Hello, DHT!"
Stored at 3 nodes

# Retrieve from any node
$ ./kademlia-cli get mykey
Value: "Hello, DHT!"

Implementation Hints:

XOR distance: In Kademlia, “distance” between two node IDs is their XOR:

void xor_distance(const uint8_t *a, const uint8_t *b, uint8_t *result, size_t len) {
    for (size_t i = 0; i < len; i++) {
        result[i] = a[i] ^ b[i];
    }
}

// Compare distances (which is closer to target?)
int compare_distance(const uint8_t *a, const uint8_t *b,
                     const uint8_t *target, size_t len) {
    uint8_t dist_a[len], dist_b[len];
    xor_distance(a, target, dist_a, len);
    xor_distance(b, target, dist_b, len);
    return memcmp(dist_a, dist_b, len);
}

K-buckets:

  • Each node maintains k (usually 20) buckets
  • Bucket i stores nodes whose ID differs from ours in bit i
  • Closer buckets have more entries
#define K 20            // Nodes per bucket
#define ID_BITS 160     // SHA-1 sized IDs

typedef struct {
    uint8_t id[20];
    struct sockaddr_in addr;
    time_t last_seen;
} KademliaNode;

typedef struct {
    KademliaNode nodes[K];
    int count;
} KBucket;

typedef struct {
    KBucket buckets[ID_BITS];
} RoutingTable;

int bucket_index(const uint8_t *our_id, const uint8_t *their_id) {
    // Find first differing bit
    for (int i = 0; i < 20; i++) {
        uint8_t diff = our_id[i] ^ their_id[i];
        if (diff != 0) {
            // Count leading zeros
            for (int j = 7; j >= 0; j--) {
                if (diff & (1 << j)) {
                    return i * 8 + (7 - j);
                }
            }
        }
    }
    return ID_BITS - 1;  // Same ID
}

RPC messages (BEP-5):

  • ping: Check if node is alive
  • find_node: Find k nodes closest to a target ID
  • get_peers: Find peers for an infohash (BitTorrent)
  • announce_peer: Announce that we have data

Node lookup algorithm:

function find_node(target):
    shortlist = our k closest nodes to target
    queried = {}

    while True:
        # Pick α unqueried nodes from shortlist
        to_query = pick_unqueried(shortlist, α)
        if to_query is empty:
            break

        # Query them in parallel
        for node in to_query:
            response = send_find_node(node, target)
            queried.add(node)
            for new_node in response:
                if new_node not in shortlist:
                    shortlist.add(new_node)

        # Keep only k closest
        shortlist = k_closest(shortlist, target)

    return shortlist

Learning milestones:

  1. Two nodes discover each other → Basic networking works
  2. Routing table updates correctly → K-bucket logic works
  3. find_node returns closest nodes → Lookup algorithm works
  4. Store and retrieve values → You have a working DHT!

Project 11: Gossip Protocol Implementation

  • File: LEARN_DECENTRALIZED_PROTOCOLS_NOSTR_C.md
  • Main Programming Language: C
  • Alternative Programming Languages: Rust, Go
  • Coolness Level: Level 4: Hardcore Tech Flex
  • Business Potential: 3. The “Service & Support” Model
  • Difficulty: Level 3: Advanced
  • Knowledge Area: Distributed Systems / Consensus
  • Software or Tool: None (pure C)
  • Main Book: “Designing Data-Intensive Applications” by Martin Kleppmann

What you’ll build: A gossip (epidemic) protocol for propagating information across a network of nodes, where each node randomly shares with neighbors.

Why it teaches decentralized protocols: Gossip protocols are how information spreads in decentralized networks without central coordination. They’re robust to failures and scale well.

Core challenges you’ll face:

  • Random peer selection → maps to probabilistic propagation
  • Message deduplication → maps to preventing infinite loops
  • Convergence analysis → maps to how fast does everyone know?
  • Failure handling → maps to robustness

Key Concepts:

  • Gossip protocols: “Designing Data-Intensive Applications”, Chapter 5 - Kleppmann
  • SWIM protocol: Scalable Weakly-consistent Infection-style Membership
  • Vector clocks: Tracking causality
  • Crytographic hashes for dedup: Seen message IDs

Difficulty: Advanced Time estimate: 2 weeks Prerequisites: Networking basics, understanding of probability

Real world outcome:

# Start a network of 10 nodes
$ ./gossip-network --nodes 10

# Inject a message at node 0
$ ./gossip-cli inject --node 0 --message "Hello, gossip!"

# Watch propagation
Node 0: received message "Hello, gossip!"
Node 3: received message "Hello, gossip!" (from node 0)
Node 7: received message "Hello, gossip!" (from node 3)
Node 1: received message "Hello, gossip!" (from node 0)
...
All 10 nodes received message in 4 rounds

Implementation Hints:

Basic gossip algorithm:

typedef struct {
    char id[32];            // Message hash
    char content[1024];
    uint64_t timestamp;
} GossipMessage;

typedef struct {
    int node_id;
    int *peers;             // Known peer IDs
    int peer_count;
    HashSet *seen_messages; // Deduplication
} GossipNode;

void gossip_round(GossipNode *node, GossipMessage *new_messages, int count) {
    // 1. Select random peers (fanout, usually 3)
    int fanout = 3;
    int selected[fanout];
    for (int i = 0; i < fanout; i++) {
        selected[i] = node->peers[rand() % node->peer_count];
    }

    // 2. Send all new messages to selected peers
    for (int i = 0; i < fanout; i++) {
        for (int j = 0; j < count; j++) {
            send_to_peer(selected[i], &new_messages[j]);
        }
    }
}

void on_receive(GossipNode *node, GossipMessage *msg) {
    // Deduplicate
    if (hashset_contains(node->seen_messages, msg->id)) {
        return;  // Already seen
    }
    hashset_add(node->seen_messages, msg->id);

    // Process message
    handle_message(msg);

    // Will be gossiped in next round
    add_to_outbox(msg);
}

Convergence math: With fanout f and n nodes:

  • Round 1: 1 node knows
  • Round 2: ~f nodes know
  • Round 3: ~f² nodes know
  • Round log_f(n): all nodes know

Anti-entropy (full state sync): Periodically, nodes do full state comparison with a random peer to catch any missed messages.

Learning milestones:

  1. Messages propagate to all nodes → Basic gossip works
  2. Deduplication prevents loops → No message explosion
  3. Network handles node failures → Robustness works
  4. Convergence time is logarithmic → Efficiency achieved

Project 12: ActivityPub Mini-Implementation

  • File: LEARN_DECENTRALIZED_PROTOCOLS_NOSTR_C.md
  • Main Programming Language: C
  • Alternative Programming Languages: Rust, Go, TypeScript
  • Coolness Level: Level 4: Hardcore Tech Flex
  • Business Potential: 3. The “Service & Support” Model
  • Difficulty: Level 4: Expert
  • Knowledge Area: Federated Systems / HTTP Signatures
  • Software or Tool: OpenSSL, HTTP library
  • Main Book: “Building Microservices, 2nd Edition” by Sam Newman

What you’ll build: A minimal ActivityPub server that can send and receive activities, implementing the core federation protocol used by Mastodon.

Why it teaches decentralized protocols: ActivityPub is the W3C standard for federated social networking. It uses HTTP, JSON-LD, and cryptographic signatures. Comparing it to Nostr reveals different design philosophies.

Core challenges you’ll face:

  • HTTP Signatures → maps to authenticating server-to-server requests
  • JSON-LD and ActivityStreams → maps to semantic data formats
  • Inbox/Outbox model → maps to activity delivery
  • WebFinger discovery → maps to finding users across servers

Key Concepts:

  • ActivityPub spec: W3C Recommendation
  • HTTP Signatures: RFC draft for signing HTTP requests
  • JSON-LD: Linked Data in JSON
  • WebFinger: RFC 7033 for user discovery

Difficulty: Expert Time estimate: 1 month Prerequisites: HTTP, JSON, cryptography projects

Real world outcome:

$ ./activitypub-server --domain example.com --port 443
ActivityPub server running at https://example.com

# User lookup
$ curl "https://example.com/.well-known/webfinger?resource=acct:alice@example.com"
{
  "subject": "acct:alice@example.com",
  "links": [
    {"rel": "self", "type": "application/activity+json",
     "href": "https://example.com/users/alice"}
  ]
}

# Mastodon can now follow @alice@example.com!

Implementation Hints:

Actor object:

{
  "@context": ["https://www.w3.org/ns/activitystreams"],
  "type": "Person",
  "id": "https://example.com/users/alice",
  "inbox": "https://example.com/users/alice/inbox",
  "outbox": "https://example.com/users/alice/outbox",
  "preferredUsername": "alice",
  "publicKey": {
    "id": "https://example.com/users/alice#main-key",
    "owner": "https://example.com/users/alice",
    "publicKeyPem": "-----BEGIN PUBLIC KEY-----\n..."
  }
}

HTTP Signature (for server-to-server):

POST /users/bob/inbox HTTP/1.1
Host: other-server.com
Date: Sun, 06 Nov 2024 08:49:37 GMT
Digest: SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=
Signature: keyId="https://example.com/users/alice#main-key",
           algorithm="rsa-sha256",
           headers="(request-target) host date digest",
           signature="base64encodedSignature..."

Creating the signature:

// 1. Create signing string
char signing_string[4096];
snprintf(signing_string, sizeof(signing_string),
    "(request-target): post /users/bob/inbox\n"
    "host: other-server.com\n"
    "date: %s\n"
    "digest: SHA-256=%s",
    date_header, digest_base64);

// 2. Sign with RSA-SHA256
unsigned char signature[256];
RSA_sign(NID_sha256, hash, hash_len, signature, &sig_len, rsa_private_key);

// 3. Base64 encode
char *sig_b64 = base64_encode(signature, sig_len);

Core activities:

  • Create: New content (note, article)
  • Follow: Request to follow someone
  • Accept: Accept a follow request
  • Announce: Boost/share content
  • Like: Like content

Learning milestones:

  1. WebFinger resolves users → Discovery works
  2. Actor endpoint returns valid JSON-LD → ActivityStreams works
  3. Can receive Follow from Mastodon → Inbox works
  4. Can send Accept back → HTTP Signatures work
  5. Posts appear on Mastodon → Full federation!

Project 13: Build Your Own Protocol

  • File: LEARN_DECENTRALIZED_PROTOCOLS_NOSTR_C.md
  • Main Programming Language: C
  • Alternative Programming Languages: Rust, Go
  • Coolness Level: Level 5: Pure Magic (Super Cool)
  • Business Potential: 5. The “Industry Disruptor”
  • Difficulty: Level 5: Master
  • Knowledge Area: Protocol Design / Systems Architecture
  • Software or Tool: All previous tools
  • Main Book: “Designing Data-Intensive Applications” by Martin Kleppmann

What you’ll build: Your own decentralized communication protocol, designed from first principles based on everything you’ve learned.

Why it teaches decentralized protocols: Design is the ultimate test of understanding. When you must make the tradeoffs yourself—between simplicity and features, privacy and discoverability, consistency and availability—you truly understand the problem space.

Core challenges you’ll face:

  • Choosing an identity model → maps to fundamental protocol philosophy
  • Designing the message format → maps to extensibility vs simplicity
  • Defining the network topology → maps to centralization tradeoffs
  • Handling edge cases → maps to real-world robustness

Key Concepts:

  • CAP theorem: Consistency, Availability, Partition tolerance
  • Protocol evolution: How to add features without breaking compatibility
  • Security analysis: What can go wrong?
  • Incentive design: Why would people run nodes?

Difficulty: Master Time estimate: 2-3 months Prerequisites: All previous projects

Real world outcome:

  • A complete protocol specification document
  • Reference implementations of client and server
  • Working prototype that people can actually use
  • Understanding that rivals protocol designers at major companies

Implementation Hints:

Questions to answer:

  1. Identity:
    • Keypairs (like Nostr/Scuttlebutt)?
    • Server-based (like ActivityPub/Matrix)?
    • DIDs (like AT Protocol)?
    • What’s your reasoning?
  2. Message format:
    • JSON (human-readable, larger)?
    • Binary (compact, faster)?
    • Hybrid (JSON + binary payloads)?
    • Schema evolution strategy?
  3. Transport:
    • WebSocket (good for web clients)?
    • Raw TCP (simpler)?
    • UDP (for P2P)?
    • Multiple transport support?
  4. Network topology:
    • Client-relay (like Nostr)?
    • Full P2P (like Scuttlebutt)?
    • Federated (like ActivityPub)?
    • Hybrid?
  5. Data model:
    • Events/messages (Nostr)?
    • Activities (ActivityPub)?
    • Append-only log (Scuttlebutt)?
    • Graph (AT Protocol)?
  6. Privacy:
    • Public by default?
    • Encrypted by default?
    • Metadata protection?
  7. Extensibility:
    • How do you add new message types?
    • How do old clients handle new features?
    • Versioning strategy?

Protocol design template:

# MyProtocol Specification v0.1

## 1. Overview
[What problem does this solve?]

## 2. Identity
[How are users identified?]

## 3. Message Format
[What do messages look like?]

## 4. Transport
[How are messages sent?]

## 5. Operations
[What operations does the protocol support?]

## 6. Security Considerations
[What attacks are possible? How are they mitigated?]

## 7. Extensibility
[How can the protocol evolve?]

Learning milestones:

  1. Spec document complete → You can articulate your design
  2. Basic prototype works → Your design is implementable
  3. Two implementations interoperate → Your spec is clear enough
  4. You can explain every tradeoff → You truly understand protocols

Project Comparison Table

Project Difficulty Time Depth of Understanding Fun Factor
1. SHA-256 ⭐⭐ 1 week Cryptographic foundations ⭐⭐⭐
2. Elliptic Curves ⭐⭐⭐ 1-2 weeks Identity & signatures ⭐⭐⭐⭐
3. JSON Parser ⭐⭐ 1-2 weeks Data serialization ⭐⭐⭐
4. TCP Echo Server ⭐⭐ Weekend Network basics ⭐⭐⭐
5. WebSocket Server ⭐⭐⭐ 2 weeks Protocol upgrades ⭐⭐⭐⭐
6. Nostr Client ⭐⭐⭐ 2-3 weeks Full Nostr protocol ⭐⭐⭐⭐⭐
7. Nostr Relay ⭐⭐⭐⭐ 1 month Server architecture ⭐⭐⭐⭐⭐
8. Bech32 Encoding ⭐⭐ 1 week User-facing formats ⭐⭐⭐
9. Encrypted DMs ⭐⭐⭐ 2 weeks End-to-end encryption ⭐⭐⭐⭐
10. Kademlia DHT ⭐⭐⭐⭐ 1 month P2P discovery ⭐⭐⭐⭐⭐
11. Gossip Protocol ⭐⭐⭐ 2 weeks Information propagation ⭐⭐⭐⭐
12. ActivityPub ⭐⭐⭐⭐ 1 month Federated systems ⭐⭐⭐⭐
13. Your Protocol ⭐⭐⭐⭐⭐ 2-3 months Protocol design ⭐⭐⭐⭐⭐

Path A: Nostr-Focused (Fastest to Working Code)

  1. Project 2 (Elliptic Curves) - Use libsecp256k1
  2. Project 3 (JSON) - Use cJSON library
  3. Project 5 (WebSocket) - Use libwebsockets
  4. Project 6 (Nostr Client) - Tie it together
  5. Project 8 (Bech32) - Polish with user-friendly keys
  6. Project 7 (Nostr Relay) - Build the server side

Path B: Deep Understanding (Maximum Learning)

  1. Project 1 (SHA-256) - Understand hashing
  2. Project 2 (Elliptic Curves) - Understand signatures
  3. Project 3 (JSON Parser) - Build from scratch
  4. Project 4 (TCP Server) - Understand sockets
  5. Project 5 (WebSocket) - Build from scratch
  6. Project 6 (Nostr Client) - Apply everything
  7. Project 10 (DHT) - Understand P2P
  8. Project 13 (Your Protocol) - Design your own

Path C: Compare Protocols

  1. Projects 1-6 (Nostr foundation)
  2. Project 12 (ActivityPub) - Compare federated approach
  3. Project 10 (DHT) - Understand pure P2P
  4. Project 11 (Gossip) - Understand propagation
  5. Project 13 (Your Protocol) - Synthesize insights

Final Project: Contribute to the Nostr Ecosystem

  • File: LEARN_DECENTRALIZED_PROTOCOLS_NOSTR_C.md
  • Main Programming Language: C
  • Alternative Programming Languages: Any
  • Coolness Level: Level 5: Pure Magic (Super Cool)
  • Business Potential: 4. The “Open Core” Infrastructure
  • Difficulty: Level 4: Expert
  • Knowledge Area: Open Source / Protocol Evolution
  • Software or Tool: Git, GitHub
  • Main Book: N/A - Real-world contribution

What you’ll do: Contribute to the Nostr ecosystem—whether by proposing a NIP, improving an existing implementation, building a unique client, or creating tooling.

Why this is the ultimate test: Building in isolation is one thing. Contributing to a living protocol used by thousands of people, coordinating with other developers, and making real impact—that’s engineering.

Ideas for contribution:

  1. Propose a NIP: Have an idea for improving Nostr? Write it up!
  2. Build a unique client: What’s a use case nobody’s addressed?
  3. Improve relay software: Performance, features, reliability
  4. Create developer tooling: Libraries, testing tools, documentation
  5. Bridge to other protocols: ActivityPub, Matrix, email

Resources:


Resources Summary

Protocol Specifications

Libraries

Books

  • “Serious Cryptography, 2nd Edition” by Jean-Philippe Aumasson - Cryptography
  • “The Linux Programming Interface” by Michael Kerrisk - Systems programming
  • “TCP/IP Illustrated, Volume 1” by W. Richard Stevens - Networking
  • “Designing Data-Intensive Applications” by Martin Kleppmann - Distributed systems

Learning Resources

Protocol Comparisons


Summary

# Project Main Language
1 SHA-256 Implementation C
2 Elliptic Curve Basics (secp256k1) C
3 JSON Parser C
4 TCP Echo Server C
5 WebSocket Server C
6 Minimal Nostr Client C
7 Nostr Relay C
8 Bech32 Encoding (NIP-19) C
9 Encrypted Direct Messages (NIP-44) C
10 Kademlia DHT C
11 Gossip Protocol C
12 ActivityPub Mini-Implementation C
13 Build Your Own Protocol C
Final Contribute to Nostr Ecosystem C/Any

By the time you complete these projects, you won’t just understand Nostr—you’ll understand how ALL decentralized communication works at its core. The skills transfer to blockchain, P2P networks, secure messaging, and any future protocol that hasn’t been invented yet.

Happy building! The decentralized future needs more people who understand it from the ground up. 🔐