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:
- Private networks:
"MY-COMPANY-INTERNAL" - Topic-specific spaces:
"ANIMALS-2024-12-21","RECIPES-2024-12-21" - Testing:
"TEST-SEED"for development without polluting the main namespace
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.
- No DNS registrar
- No smart contract
- No authority to ask permission from
- Just math
Collision-Free: 256-bit space = 2²&sup5;&sup6; possible addresses
- That's 115,792,089,237,316,195,423,570,985,008,687,907,853,269,984,665,640,564,039,457,584,007,913,129,639,936 addresses
- More addresses than atoms in the observable universe
- You'll never accidentally collide with someone else's bunion
Hierarchical: Parent hashes create a Merkle chain structure
"to be"is cryptographically linked to"to""to be or"is cryptographically linked to"to be"- You can verify any path by checking the chain back to ROOT
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:
- The bunion for "To be or not to be" existed before Shakespeare wrote it
- The bunion for "Call me Ishmael" existed before Melville wrote Moby Dick
- The bunion for your name, your email, your thoughts - all exist mathematically right now
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:
- DNS: You register a domain (creation)
- URLs: You set up a server (creation)
- Blockchain addresses: You generate a keypair (creation)
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:
- Sign a Root to attest to an entire document version
- Sign a DagNode to attest to a specific section
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:
- Anyone can calculate any bunion (no registration)
- Anyone can create Roots at any bunion (no gatekeeping)
- Multiple Roots can coexist at the same coordinate
Three simple primitives:
- Root (entry point at bunion)
- DagNode (content tree)
- Signature (attestation)
Higher layers decide:
- Which Root to use (if multiple exist)
- How to resolve conflicts
- What "latest" means
- Trust and authentication
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:
roots_at(bunion_hash)- Find Roots by coordinatesignatures_for(root_hash)- Find Signatures by what they sign
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:
- Bunion - Deterministic coordinates in 256-bit space
- Root - Entry points at bunions (like Git commits)
- DagNode - Content tree nodes (like Git trees/blobs)
- Signature - Attestations about Roots
- Store - Platform-agnostic persistence
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
- A long phrase requires 40+ hash operations
- Doing this 100 times = 4,000+ redundant hashes
- Solution: Cache intermediate results automatically
2. Convenience: Character-by-character hashing is too low-level
- Solution:
runtime.bunion(path)- does the loop for you AND caches
3. Content encoding: Structured formats need to become DAG trees
- Solution: Encoders that automatically convert formats to DAG structures
4. P2P practicality: In a P2P network, you might have partial data
- Solution: UnaData wrapper that tracks completeness
5. Publishing workflow: Multiple steps from content to stored
- Solution:
runtime.publish(path, html)does it all
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:
- Fetch just the <h1> - download that subtree only
- Share identical text - same text content = same hash
- Stream rendering - display elements as nodes arrive
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:
- Discovered the bunion coordinate for "blog/my-first-post"
- Used JsonEncoder to parse JSON into DagNode tree (bottom-up)
- Created Root:
{ bunion_hash, encoder: "json", children } - Stored all DagNodes + Root in Store
- 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:
- Laptop → Phone claim exists? ✓
- Phone → Laptop claim exists? ✓
- CONFIRMED! Both keys coalesced into one persona.
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:
- ✓ Rooted at public key hash (globally unique by cryptography)
- ✓ Multiple people can use same persona name (different roots)
- ✓ Discovery through social verification (handles, QR codes, web of trust)
Asynchronous Operation:
- ✓ Claims work offline (no coordination needed)
- ✓ Verification happens when devices come online
- ✓ Perfect for offline-first, intermittent connectivity
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:
- You only discover content your network knows about
- Want to find more? Connect to more peers
- Popular content spreads organically through announcements
- Rare content requires knowing the right peers
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:
- Serve static resources - Deliver the HTML/JS/WASM bundle containing the Unaisa runtime
- WebRTC signaling - Help browser peers establish P2P connections
That's it. The gateway does NOT:
- Calculate bunions (that's local runtime)
- Store DagNodes (that's local Store)
- Maintain a peer registry (that's gossip)
- Publish content (that's local runtime + P2P mesh)
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?
- Schema-less: Self-describing like JSON, matches Unaisa's flexible design
- Compact: Binary encoding, efficient for P2P bandwidth
- Evolvable: Can add fields without breaking compatibility
- Wide support: Rust (
ciborium), JavaScript (cbor-x), all major languages - IPLD compatible: DAG-CBOR is IPFS's standard format
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:
- DagNodes should be small (kilobytes, not gigabytes)
- P2P mesh is for discovery and coordination, not bulk transfer
- Specialized protocols (BitTorrent, IPFS) handle large files better
Design Philosophy
No External Dependencies
What This Means:
- ✓ Single Rust binary compiles to native code for any platform
- ✓ WASM module runs in any modern browser without dependencies
- ✓ Self-contained HTTP server serves static assets
- ✗ No Python, Node.js, or other external runtimes required
- ✗ No
npm install,pip install, or package manager dependencies
Platform Agnostic
Every peer is equal. The browser is just another platform.
Design principles:
- Use the best transport for each environment
- Same protocol, different transports
- No platform is privileged over another
- Peers interoperate regardless of platform
Permissionless
Anyone can publish anywhere:
- No registration required to create bunion coordinates
- No permission needed to publish content at any coordinate
- Multiple publishers can coexist at the same coordinate
- Discovery happens through social verification
Inspirations
- Bitcoin: Fully self-contained node software
- SQLite: Single file database, no server
- Go: Single binary deployment philosophy
- WASM: Universal compilation target
- IPFS: Content-addressed P2P networking
- Freenet: Censorship-resistant mesh networks
- Gemini Protocol: Simplicity over features
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, andchildren. Doesn't nest. - DagNode
- Content tree node (like Git tree/blob). Contains
hash,content(CBOR bytes), andchildren. Nests to form content structure. - Signature
- Attestation about a Root or DagNode. Contains
hash,signs,signer, andsig. 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:
- Bunions turn any phrase into a deterministic coordinate
- Three primitives (Root, DagNode, Signature) store content at those coordinates
- The Runtime provides caching, encoding, and convenience APIs
- The Identity Layer enables collision-free identities via cryptographic roots
- The P2P Layer connects peers without central infrastructure
- 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.