Felix Flores

← Back

Unaisa: Core Primitives Draft

Universal Node Addressing through Intrinsic Structure Alignment

A minimal explanation of the core protocol


The Core Idea

"to be or not to be"

Take this phrase. Break it into characters:

t → o →   → b → e →   → o → r →   → n → o → t →   → t → o →   → b → e

Now, build a Merkle chain where each step incorporates the next character:

Seed           → HASH₀
HASH₀ + 't'    → HASH₁
HASH₁ + 'o'    → HASH₂
HASH₂ + ' '    → HASH₃  (whitespace counts!)
HASH₃ + 'b'    → HASH₄
HASH₄ + 'e'    → HASH₅
...
HASH₁₆ + 'e'   → HASH₁₇  (final hash)

This final hash is the coordinate. Everyone who runs this calculation with the same seed gets the exact same result. It's deterministic.

In Unaisa, we call this coordinate a bunion:

hash("to be or not to be") → 43b23a7addd32e559945cc14b4427b506b44ec1da69780f349044e651a8eac3f

That's Unaisa: Merkle chains turn phrases into coordinates.

Everything else - the P2P mesh, the content DAGs, the multiple publishers - builds on this one simple idea.


What is a Bunion?

A bunion (Boundless Unified Network Ingress Origin Node) is the fundamental addressing primitive in Unaisa. It's a 256-bit coordinate derived from a Merkle chain.

Every bunion has four essential properties:

1. Hash - The 256-bit BLAKE3 hash that IS the coordinate

// Build the chain character by character
let mut bunion = bunion::root("UNAISA-2024-12-21");
for ch in "to be or not to be".chars() {
    bunion = bunion::next(&bunion, ch);
}

// Bunion {
//   hash: "43b23a7addd32e559945cc14b4427b506b44ec1da69780f349044e651a8eac3f",
//   path: "to be or not to be",
//   parent_hash: "136a2accea160302ed3d94196dc7d71ee6e7d47bc5a68bd8d5fe515945c9c9f1",
//   seed: "UNAISA-2024-12-21"
// }

2. Path - The human-readable string that generated this coordinate

path: "to be or not to be"

3. Parent Hash - Links to the previous bunion in the chain

parentHash: "136a2accea160302ed3d94196dc7d71ee6e7d47bc5a68bd8d5fe515945c9c9f1"
               ↑
               This is the hash for "to be or not to b"

4. Seed - The root of the entire address space

seed: "UNAISA-2024-12-21"

About Seeds: The default seed "UNAISA-2024-12-21" creates the canonical Unaisa address space. All peers using this seed will compute identical bunion hashes for the same phrases, enabling global interoperability.

Custom seeds create isolated "bunion universes" - useful for:

Peers must share the same seed to discover each other's content.

The Bunion Chain

Here's the actual chain for "to be or not to be":

ROOT                  → d51d9e1542a5104bf88ca59e0c05bfd3059ebaf31a401b3c35a9a28913ef5c6b
ROOT + 't'            → e78214761a6a1791c49b051d932cd09c0eaaaa27dda7c7bd068d413026d886a7
... + 'o'             → bff8226dfc12e68fdaadca6ad5266ae0b69ed421385a3997879e39ccc0313921
... + ' '             → 07093c5d68ed3ff32ab66c62238bf3977179eb0adb1f1f3be6d98efa5db1c802 (whitespace!)
... + 'b'             → f9147e1a760346e5f97d5e62fbfcb793dfb6c63b8765b9d27fc29e1c747d709a
... + 'e'             → 795f41f30fca6a499b46ae614c30e4654c422f2d0fda81e22b18a04b5dff5004
...
... + 'e' (final)     → 43b23a7addd32e559945cc14b4427b506b44ec1da69780f349044e651a8eac3f

Key Properties

Deterministic: Same path + same seed = same bunion hash. Always.

// Anyone can verify by building the chain:
let mut bunion = bunion::root("UNAISA-2024-12-21");
for ch in "to be or not to be".chars() {
    bunion = bunion::next(&bunion, ch);
}
// bunion.hash = "43b23a7addd32e559945cc14b4427b506b44ec1da69780f349044e651a8eac3f"

No Registration: You don't "claim" a bunion. You calculate it.

Collision-Free: 256-bit space = 2²&sup5;&sup6; possible addresses

Hierarchical: Parent hashes create a Merkle chain structure

You Don't Create Bunions - You Discover Them

Here's a profound insight: Every sequence of characters that has been written or will ever be written already exists as a coordinate.

Think about it:

Bunions are coordinates in the space of all possible strings. You don't create them. You discover them by computing their hash.

This is fundamentally different from:

With bunions: The coordinate already exists. You're just calculating it.


The Three Primitives

Now that we understand bunions - deterministic coordinates in 256-bit space - the natural question is: what do you store at these coordinates?

The answer: three content-addressed primitives inspired by Git's object model.

Root

Entry point at a bunion coordinate (like Git's commit):

Root {
    hash: String,           // BLAKE3(bunion_hash + encoder + children)
    bunion_hash: String,    // The coordinate this content is attached to
    encoder: String,        // "html5", "json", "markdown"
    children: Vec<String>,  // DagNode hashes (the content tree)
}

Roots don't nest. They're always the entry point. Multiple Roots can exist at the same bunion (like multiple commits on a branch).

DagNode

Content tree nodes (like Git's tree/blob):

DagNode {
    hash: String,           // BLAKE3(content + children_hashes)
    content: Vec<u8>,       // CBOR-encoded data (opaque bytes)
    children: Vec<String>,  // Child DagNode hashes
}

DagNodes nest. They form the content structure. Parents point to children (Git-style), so changes cascade up to the root.

Signature

Attestation about content:

Signature {
    hash: String,           // BLAKE3(signs + signer + sig)
    signs: String,          // Hash of object being signed (Root OR DagNode)
    signer: String,         // Public key hash
    sig: Vec<u8>,           // Ed25519 signature bytes
}

Signatures are separate from content. Adding a signature doesn't change the Root or its DagNodes.

Signatures can sign any content-addressed object:

Granular signing: Different parties can sign different parts. Legal team signs terms, user signs agreement, appendix remains unsigned.

How They Relate

Bunion (computed coordinate)
    │
    ├── Root A ─────────────└
    │   bunion_hash: X   │
    │   encoder: html5   │
    │   children: [P]    │
    │                    ▾
    │              ┌───────────└
    │              │ DagNode P │──▸ DagNode Q ──▸ DagNode R
    │              └───────────┐
    │
    └── Root B (different version)
        bunion_hash: X
        encoder: html5
        children: [S]

Signatures can sign Root OR DagNode:

Signature 1                    Signature 2
    signs: Root A's hash           signs: DagNode Q's hash
    signer: alice_key              signer: legal_team
    (whole document)               (specific section)

Key insight: DagNodes contain opaque bytes, not structured objects. They're format-agnostic primitives. The encoder (specified in Root) knows how to interpret them.

The Four Concepts

Concept Type Purpose
Bunion Computed Coordinate (WHERE)
Root Stored Entry point at bunion
DagNode Stored Content tree (WHAT)
Signature Stored Attestation (WHO)

Everything else in Unaisa builds on these primitives.


How Roots Attach to Bunions

Now we understand the primitives. But how do they connect? How does content get attached to a coordinate?

The Root Primitive

A Root explicitly stores its bunion coordinate:

Root {
    hash: String,           // BLAKE3(bunion_hash + encoder + children)
    bunion_hash: String,    // ← The coordinate this content is at
    encoder: String,        // How to decode the content
    children: Vec<String>,  // The content tree (DagNode hashes)
}

Example: Publishing content at "Call me Ishmael":

Step 1: Discover the bunion coordinate
  bunion("Call me Ishmael") = "3a720bf2..."

Step 2: Build content tree (DagNodes, bottom-up)
  ... create DagNodes for content ...
  content_root_hash = "abc123..."

Step 3: Create Root
  Root {
    bunion_hash: "3a720bf2...",  // The coordinate
    encoder: "html5",
    children: ["abc123..."],     // Points to content
    hash: BLAKE3(bunion_hash + encoder + children)
  }

Multiple Roots, Same Bunion

Because Unaisa is permissionless, multiple people can publish to the same coordinate.

The coordinate for "hello world" exists mathematically. Anyone can discover it. Anyone can create Roots there.

Bunion "hello world" (hash: 3b9ec76f...)
    │
    ├── Root A (Alice)
    │     bunion_hash: "3b9ec76f..."
    │     encoder: "html5"
    │     children: [content_A]
    │     hash: "ROOT_A"
    │
    └── Root B (Bob)
          bunion_hash: "3b9ec76f..."
          encoder: "html5"
          children: [content_B]
          hash: "ROOT_B"

This is not a protocol error. Multiple Roots at the same bunion is like multiple commits - the application layer decides which one to use.

Why This Design?

Permissionless:

Three simple primitives:

Higher layers decide:


Storage: The Store Trait

Now that we understand the three primitives, we need a way to persist and retrieve them. This is where the Store trait comes in.

The Store Trait

Store is an abstraction for primitive storage. It defines the interface; implementations are platform-specific.

trait Store {
    // Roots
    fn put_root(&self, root: Root);
    fn get_root(&self, hash: &str) -> Option<Root>;
    fn roots_at(&self, bunion_hash: &str) -> Vec<Root>;

    // DagNodes
    fn put_node(&self, node: DagNode);
    fn get_node(&self, hash: &str) -> Option<DagNode>;

    // Signatures
    fn put_signature(&self, sig: Signature);
    fn signatures_for(&self, hash: &str) -> Vec<Signature>;
}

Key Properties

Type-specific storage: Roots, DagNodes, and Signatures are stored separately with their own accessors.

Direct lookups: No content parsing needed:

Immutable primitives: Once created, Roots, DagNodes, and Signatures never change. All put methods are idempotent.

No deletion: There's no delete() method. Primitives are permanent by design.

Platform-Specific Implementations

// Testing - ephemeral in-memory storage
use unaisa::dag::MemoryStore;
let store = MemoryStore::new();

// Native/Desktop - persistent SQLite storage
use unaisa::dag::SqliteStore;
let store = SqliteStore::open("~/.unaisa/nodes.db")?;

// Browser/WASM - IndexedDB storage
use unaisa::dag::IndexedDbStore;
let store = IndexedDbStore::new("unaisa-nodes").await?;

The Runtime Layer

Summary: What We've Built So Far

We've now covered the Core Layer - the complete foundation of Unaisa:

These are powerful primitives. They give you full control. But they're intentionally low-level - like manually parsing HTTP with TcpStream instead of using Axum.

Why We Need a Runtime Layer

1. Performance: Calculating long bunion chains repeatedly is wasteful

2. Convenience: Character-by-character hashing is too low-level

3. Content encoding: Structured formats need to become DAG trees

4. P2P practicality: In a P2P network, you might have partial data

5. Publishing workflow: Multiple steps from content to stored

Layered Architecture

┌─────────────────────────────────────────└
│  Application Layer                      │
│  - Your code                            │
│  - Business logic                       │
│  - UI/UX                                │
└──────────────▾──────────────────────────┐
               │
┌──────────────▾──────────────────────────└
│  Runtime Layer                          │
│  - Unaisa struct (caching, convenience) │
│  - UnaData (completeness tracking)      │
│  - Encoders (HTML/XML/Markdown → DAG)   │
│  - Publishing workflow                  │
└──────────────▾──────────────────────────┐
               │
┌──────────────▾──────────────────────────└
│  P2P Layer                              │
│  - Peer (connection management)         │
│  - ConnectionInfo (P2P primitive)       │
│  - Gossip protocol                      │
│  - Bootstrap methods (QR, URL, gateway) │
└──────────────▾──────────────────────────┐
               │
┌──────────────▾──────────────────────────└
│  Core Layer                             │
│  - Bunion (coordinate)                  │
│  - Root, DagNode, Signature primitives  │
│  - Store trait                          │
│  - Pure functions, no state             │
└─────────────────────────────────────────┐

UnaData: The Runtime's Query Wrapper

The runtime wraps primitives in UnaData - a query result that provides convenient access:

/// Runtime query wrapper (NOT stored)
pub struct UnaData {
    pub bunion_hash: String,
    pub encoder: String,
    pub roots: Vec<String>,  // Root hashes at this bunion with this encoder
}

impl UnaData {
    /// Get fully decoded content (HTML string, JSON string, etc.)
    pub fn get(&self, root_hash: &str, store: &impl Store) -> Result<String, Error>;

    /// Find signatures for a root
    pub fn signatures(&self, root_hash: &str, store: &impl Store) -> Vec<Signature>;
}

Key insight: get() returns fully decoded content, not primitive DagNodes. You get the original HTML/JSON/Markdown string back.

Encoders: Structured Content as DAGs

Encoders parse structured formats and create trees of DagNodes - one node per element:

HTML Input:
  <article>
    <h1>The Cat Sat</h1>
    <p>On the mat.</p>
  </article>

DAG Output (built bottom-up):
  Root
    bunion_hash: "..."
    encoder: "html5"
    children: [article_hash]
           │
           ▾
  DagNode (article)
    content: { tag: "article" }
    children: [h1_hash, p_hash]
           │
    ┌──────◂──────└
    ▾             ▾
  DagNode       DagNode
  (h1)          (p)
  tag: "h1"     tag: "p"
  children:     children:
   [text1]       [text2]

Now you can:

The Encoder Trait

trait DagEncoder {
    /// Parse input into DagNode tree (bottom-up: leaves first)
    fn decode(&self, input: &str) -> Result<Vec<DagNode>, Error>;

    /// Reconstruct original format from Root (top-down traversal)
    fn encode(&self, root: &Root, store: &dyn Store) -> Result<String, Error>;

    /// Encoder name (e.g., "html5", "markdown")
    fn name(&self) -> &str;
}

Built-in Encoders

// JsonEncoder (default):
runtime.publish("path", r#"{"key": "value"}"#)?;

// HtmlEncoder:
runtime.publish_html("path", "<h1>Hello</h1>")?;

// MarkdownEncoder:
runtime.publish_markdown("path", "# Hello\n\nWorld")?;

Structural Sharing

DagNodes can be shared between Roots (like Git trees between commits). This is a natural consequence of content-addressed storage.

Root 1 (v1)                    Root 2 (v2)
  │                              │
  ▾                              ▾
body (B1)                      body (B2)       ← DIFFERENT
  children: [h1:X, p:P1]         children: [h1:X, p:P2]
        │      │                       │      │
        ▾      ▾                       ▾      ▾
    h1 (X)   p (P1)                h1 (X)   p (P2)
   "Hello"  "Original"            "Hello"  "Updated"
      ↑                              ↑
      └──────── SHARED ────────────┐

The h1 DagNode with "Hello" is stored once but referenced by both versions.

Node Selection and Signing

The at() method uses JSON Path as the universal baseline:

Syntax Meaning
$ Root node
$[0] First child
$[0][2][1] Nested positional navigation
$[*] All children
$..* All descendants (recursive)

Encoder-Specific Queries:

// HTML encoder: CSS selectors
doc.at("#terms")?              // By ID
doc.at(".clause")?             // By class
doc.at("section > p")?         // CSS selector

// JSON encoder: JSON Path with keys
doc.at("$.terms.clauses[0]")?  // Key-based path

// Markdown encoder: Heading paths
doc.at("## Terms")?            // By heading text

Signing Examples:

// Sign entire document (Root):
doc.at("$").sign(&key)?;

// Granular section signing:
doc.at("#terms").sign(&legal_key)?;   // Legal team signs terms
doc.at("#agreement").sign(&user_key)?; // User signs agreement
// Appendix remains unsigned

Publishing Workflow

Putting It All Together

The Simple Case: Publish JSON

use unaisa::Unaisa;

// Initialize runtime with seed
let mut runtime = Unaisa::new("UNAISA-2024-12-21");

// Publish JSON content
let json = r#"{
    "type": "blog-post",
    "title": "My First Post",
    "content": "Hello, Unaisa!",
    "author": "Alice"
}"#;

let root_hash = runtime.publish("blog/my-first-post", json)?;

println!("Published!");
println!("Root hash: {}", root_hash);

What just happened?

Behind the scenes, the runtime:

  1. Discovered the bunion coordinate for "blog/my-first-post"
  2. Used JsonEncoder to parse JSON into DagNode tree (bottom-up)
  3. Created Root: { bunion_hash, encoder: "json", children }
  4. Stored all DagNodes + Root in Store
  5. Returned the Root hash

The Full Lifecycle

use unaisa::Unaisa;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 1. Initialize runtime
    let mut runtime = Unaisa::new("UNAISA-2024-12-21");

    // 2. Publish content
    let html = "<h1>Hello World</h1><p>First post!</p>";
    let root_hash = runtime.publish_html("hello", html)?;
    println!("✓ Published: {}", root_hash);

    // 3. List content at coordinate
    let slots = runtime.list("hello")?;
    println!("✓ Found {} encoder type(s)", slots.len());

    // 4. Get decoded content
    let reconstructed = slots[0].get(&root_hash, &runtime.store())?;
    assert_eq!(reconstructed, html);
    println!("✓ Verified: content matches");

    Ok(())
}

The Identity Layer

The identity system provides collision-free decentralized identity rooted at cryptographic coordinates. Multiple people can use the same persona name (like "alice") without conflicts because identity is rooted at public key hashes, not human-readable names.

The DNS Problem: Namespace Collisions

✗ OLD (Namespace Collision):
personas.alice/  ← ONLY ONE "alice" can exist globally!
├── key.e3b0c442...
└── handle.twitter.alice

✓ NEW (Collision-Free):
keys.e3b0c442.../  ← Alice's laptop key (unique by cryptography)
├── DagNode(name: "alice")
├── DagNode(bio: "...")
└── DagNode(handle: twitter.alice)

keys.a7f9d3c1.../  ← Bob's key (also wants name "alice")
├── DagNode(name: "alice")  ← Fine! Different root = no collision
└── DagNode(handle: github.alice)

Bidirectional Claims: Offline-First Linking

No separate verification step. Links are confirmed when bidirectional claims exist:

Social Handle Verification

Social handles use the same bidirectional claims model, just asymmetric:

Bidirectional claim for social handles:

  p1 → p2: Your Unaisa key claims the handle (DagNode)
           keys.e3b0c442.../
           └── handle_claim { handle: "twitter.alice", verification_code: "unaisa-verify-7f3a9b2c" }

  p2 → p1: Your Twitter account "claims back" by posting the code
           Tweet: "Verifying my Unaisa identity: unaisa-verify-7f3a9b2c"

Runtime checks BOTH directions exist → VERIFIED

Key Properties

Collision-Free Namespace:

Asynchronous Operation:


The P2P Layer

Core Principle

Pure peer-to-peer connectivity with no central dependencies. The gateway is optional infrastructure for convenience, not a requirement.

The P2P Primitive: ConnectionInfo

pub struct ConnectionInfo {
    peer_id: PeerId,
    transport_data: Vec<u8>, // WebRTC SDP, IP:port, or whatever the transport needs
}

This is everything needed to establish a connection. Everything else builds on this.

Bootstrap Methods

1. QR Code Exchange (No Infrastructure)

// Peer A: Generate QR code
let qr_code = peer_a.connection_info().to_qr_code();
// Display QR code on screen

// Peer B: Scan QR code
let info = ConnectionInfo::from_qr_scan(camera);
let conn = peer_b.connect(info);

Use case: Two people in the same room, phone-to-phone, laptop-to-phone.

2. URL/Link Sharing

// Peer A: Generate shareable URL
let url = format!("unaisa://connect?{}",
    peer_a.connection_info().to_base64());
// Share via email, chat, SMS, etc.

// Peer B: Click/paste URL
let info = ConnectionInfo::from_url(url);
let conn = peer_b.connect(info);

3. Gateway-Assisted (Optional Convenience)

For browser peers that can't do direct TCP/UDP. The gateway is a dumb relay - it just helps with WebRTC signaling.

Network Growth Through Gossip

Initial state:
A knows: [B]
B knows: [C, D]
C knows: [E]

Step 1: A connects to B
  A → B: "I know about: [B]"
  B → A: "I know about: [C, D]"

Result:
  A knows: [B, C, D]  ← learned about C and D from B
  B knows: [A, C, D]  ← learned about A

Network continues growing as peers share what they know!

Content Discovery & Sync

Content discovery in Unaisa is serendipitous by design - you discover what your peers have, like torrents.

The Serendipity Principle:

This is intentional - Unaisa is a social network of content, not a global search engine.


The Gateway (Bootstrap Infrastructure)

What Is a Gateway?

A Gateway is optional bootstrap infrastructure with exactly two responsibilities:

  1. Serve static resources - Deliver the HTML/JS/WASM bundle containing the Unaisa runtime
  2. WebRTC signaling - Help browser peers establish P2P connections

That's it. The gateway does NOT:

You Don't Even Need a Gateway

The gateway is just a convenience, not a requirement:

# Option 1: QR Code Bootstrap
# Two peers scan each other's QR codes - no gateway needed

# Option 2: CLI with Local Files
cargo install unaisa
unaisa bunion "hello world"

# Option 3: Offline HTML Bundle
# Bundle the runtime into a standalone HTML file
# Hand it to someone, they open it, they have Unaisa

Ideal Gateway Devices

Gateways are minimal - they can run on extremely cheap hardware:

Device Price Notes
Raspberry Pi Pico 2 W ~$6 Runs on USB power, WiFi-enabled
ESP32 ~$5 Ultra-cheap, runs Rust via esp-rs
Raspberry Pi Zero 2 W ~$15 Full Linux, extremely low power
Old smartphone Free Repurposed device, always-on WiFi

Gateways as "Public Libraries": Think of gateways like public library WiFi access points - free to use, operated by community members, provide initial entry to the network. Once you're in, you don't need them anymore. Anyone can run one.


Wire Protocol

All P2P messages use CBOR (Concise Binary Object Representation, RFC 8949) with length-prefixed framing.

Message Framing

┌──────────────◂─────────────◂─────────────────────────└
│ Length       │ Version     │ CBOR Payload            │
│ (4 bytes BE) │ (1 byte)    │ (variable length)       │
└──────────────▾─────────────▾─────────────────────────┐

Why CBOR?


API Reference

Core Layer API

Bunion Module

/// Create the ROOT bunion for a seed
pub fn root(seed: &str) -> Bunion

/// Calculate the next bunion by appending one character
pub fn next(current: &Bunion, next_char: char) -> Bunion

/// Calculate bunion at a path (convenience)
pub fn at(seed: &str, path: &str) -> Bunion

The Three Primitives

/// Entry point at a bunion (like Git commit)
pub struct Root {
    pub hash: String,
    pub bunion_hash: String,
    pub encoder: String,
    pub children: Vec<String>,
}

/// Content tree node (like Git tree/blob)
pub struct DagNode {
    pub hash: String,
    pub content: Vec<u8>,
    pub children: Vec<String>,
}

/// Attestation about content (Root or DagNode)
pub struct Signature {
    pub hash: String,
    pub signs: String,
    pub signer: String,
    pub sig: Vec<u8>,
}

Store Trait

pub trait Store {
    // Roots
    fn put_root(&self, root: Root);
    fn get_root(&self, hash: &str) -> Option<Root>;
    fn roots_at(&self, bunion_hash: &str) -> Vec<Root>;

    // DagNodes
    fn put_node(&self, node: DagNode);
    fn get_node(&self, hash: &str) -> Option<DagNode>;

    // Signatures
    fn put_signature(&self, sig: Signature);
    fn signatures_for(&self, hash: &str) -> Vec<Signature>;
}

Runtime Layer API

Unaisa Struct

impl Unaisa {
    // === Construction ===
    pub fn new(seed: &str) -> Self
    pub fn default() -> Self  // Uses "UNAISA-2024-12-21"

    // === Bunion Operations ===
    pub fn bunion(&mut self, path: &str) -> &Bunion
    pub fn chain(&mut self, path: &str) -> Vec<&Bunion>

    // === Content Operations ===
    pub fn publish(&mut self, path: &str, content: &str) -> Result<String, Error>
    pub fn publish_html(&mut self, path: &str, html: &str) -> Result<String, Error>
    pub fn publish_markdown(&mut self, path: &str, md: &str) -> Result<String, Error>

    // === Content Discovery ===
    pub fn list(&self, path: &str) -> Result<Vec<UnaData>, Error>

    // === Identity Operations ===
    pub fn create_key(&mut self, device_name: Option<&str>) -> Result<String, Error>
    pub fn claim_handle(&mut self, handle: &str) -> Result<String, Error>
    pub fn resolve_persona(&self, key_hash: &str) -> Result<Persona, Error>
}

P2P Layer API

pub struct ConnectionInfo {
    pub peer_id: PeerId,
    pub transport_data: Vec<u8>,
}

impl ConnectionInfo {
    pub fn to_qr_code(&self) -> QrCode
    pub fn from_qr_scan(data: &[u8]) -> Result<Self, Error>
    pub fn to_base64(&self) -> String
    pub fn to_url(&self) -> String
}

pub struct Peer {
    pub id: PeerId,
    pub known_peers: Vec<ConnectionInfo>,
}

impl Peer {
    pub fn connection_info(&self) -> ConnectionInfo
    pub fn connect(&self, info: ConnectionInfo) -> Result<Connection, Error>
    pub fn gossip(&self, conn: &Connection)
}

Implementation Notes

Platform Considerations

Platform Runtime Transport Storage
Browser WASM WebRTC IndexedDB
Desktop/Server Native binary QUIC, TCP, WebRTC SQLite
Mobile Native + FFI QUIC, WebRTC SQLite
Embedded Native (no_std) TCP, WiFi Flash

Large Files

Recommendation: Store references to large files, not the files themselves.

{
  "type": "video-reference",
  "ipfs_cid": "QmX4e8f7d...",
  "torrent_magnet": "magnet:?xt=urn:btih:...",
  "http_url": "https://cdn.example.com/video.mp4",
  "size_bytes": 1073741824,
  "blake3_hash": "abc123..."
}

Why:


Design Philosophy

No External Dependencies

What This Means:

Platform Agnostic

Every peer is equal. The browser is just another platform.

Design principles:

Permissionless

Anyone can publish anywhere:

Inspirations


Appendices

Appendix A: IPFS Integration

Bunions (deterministic coordinates) can integrate with IPFS (content-addressed storage) via a registry that maps bunion hashes to CIDs.

Bunion (coordinate) ──→ Registry ──→ CID(s) ──→ IPFS Network
   a3f8bc...              maps to      QmX4e8f...    (actual content)

Appendix B: Glossary

Bunion
A 256-bit coordinate derived from a phrase via character-by-character Merkle chain hashing. Deterministic, hierarchical, collision-free.
Root
Entry point at a bunion coordinate (like Git commit). Contains hash, bunion_hash, encoder, and children. Doesn't nest.
DagNode
Content tree node (like Git tree/blob). Contains hash, content (CBOR bytes), and children. Nests to form content structure.
Signature
Attestation about a Root or DagNode. Contains hash, signs, signer, and sig. Separate primitive.
Store
Storage abstraction for the three primitives.
UnaData
Runtime query wrapper (NOT stored). Groups Roots at a bunion by encoder. get() returns fully decoded content.
Encoder
Converts structured content (HTML, Markdown, JSON) to/from DagNode trees.
ConnectionInfo
P2P primitive containing peer ID and transport-specific data.
Gateway
Optional bootstrap infrastructure. Serves static files and provides WebRTC signaling. NOT required.
Gossip
Protocol for peers to share known peer information.
Persona
Runtime view of an identity. Coalesced from multiple linked keys.
Seed
Initial value for bunion chain calculation. Default: "UNAISA-2024-12-21".

Appendix C: Ubiquitous Language

Use This Not This Why
"bunion" "address", "URL" Bunions are coordinates, not addresses
"discover" "create" Bunions exist mathematically; you discover them
"coordinate" "location" More precise for a 256-bit hash
"children" "parent_hash" Parents point to children (Git-style)
"encoder" "parser" Bidirectional (decode + encode)
"serendipitous discovery" "search" You find what your network knows

Conclusion

Unaisa provides a complete system for permissionless, decentralized content addressing:

  1. Bunions turn any phrase into a deterministic coordinate
  2. Three primitives (Root, DagNode, Signature) store content at those coordinates
  3. The Runtime provides caching, encoding, and convenience APIs
  4. The Identity Layer enables collision-free identities via cryptographic roots
  5. The P2P Layer connects peers without central infrastructure
  6. The Gateway optionally bootstraps browsers into the mesh

The result is a system where anyone can publish content to any coordinate, discover content through social mechanisms, and participate in a resilient mesh network - all without servers, registration, or permission.

The network is the computer. The browser is the runtime. The mesh is the infrastructure.

No servers. No dependencies. Just peers.