# Cyberspace: the complete protocol corpus Source: https://github.com/arkin0x/cyberspace at commit 8a5b136626408da856ddbd38e966028106fd0201. Rendered pages and a manifest with sha256 per file are at https://www.cyberspace.international/spec and https://www.cyberspace.international/api/docs-manifest.json. # Cyberspace v2: Protocol Specification **Date:** February 10, 2026 **Last updated:** September 2, 2026 **Status:** Design complete (spec); reference implementation in progress --- ## What This Document Is This is the canonical specification for the Cyberspace Protocol, version 2. Cyberspace is a 256-bit coordinate system navigated by cryptographic keypairs using structured mathematical computation. It is not a game, not a platform, and not a virtual reality experience. It is a **protocol**: a specification for how to encode, prove, and verify spatial relationships using irreversible computational work. The goal is simple: impose **locality**, the fundamental property of physical space, on a digital system. In physical space, crossing distance costs energy, and no one is exempt from that cost. Cyberspace does the same thing, but with computation instead of physics. Movement requires mathematical work. The work scales with distance. There are no shortcuts, no teleportation, no administrator who can move you or delete you. The math is the same for everyone. This document specifies: - A 256-bit coordinate system with three spatial axes and a plane bit - How cryptographic identity maps directly into the coordinate fabric (your public key is your location) - Three movement primitives: **spawn** (identity placement), **hop** (Cantor pairing tree proof), and **sidestep** (Merkle hash tree proof for storage-infeasible boundaries) - A temporal work axis that prevents proof caching and replay - Location-based encryption and discovery derived from stable spatial regions - Integration with Nostr as the transmission layer - A deterministic mapping from GPS coordinates into the coordinate system Normative sections use RFC-style language (MUST/SHOULD/MAY). Explanatory material is labeled **non-normative**. Reference implementation: https://github.com/arkin0x/cyberspace-cli For extended design rationale and philosophical discussion, see [`RATIONALE.md`](https://github.com/arkin0x/cyberspace/blob/master/RATIONALE.md) (non-normative). --- ## Table of Contents - [1. Overview: Why This Exists](#1-overview-why-this-exists) - [1.1 The problem](#11-the-problem) - [1.2 The approach](#12-the-approach) - [1.3 Key properties](#13-key-properties) - [2. The Coordinate System](#2-the-coordinate-system) - [2.1 A 256-bit universe](#21-a-256-bit-universe) - [2.2 Bit layout (normative)](#22-bit-layout-normative) - [2.3 Reference pseudocode](#23-reference-pseudocode) - [2.4 Two planes: dataspace and ideaspace](#24-two-planes-dataspace-and-ideaspace) - [3. Identity Is Location](#3-identity-is-location) - [3.1 Your key is your coordinate](#31-your-key-is-your-coordinate) - [3.2 The spawn event](#32-the-spawn-event) - [4. Movement: Cantor Pairing Trees](#4-movement-cantor-pairing-trees) - [4.1 Why structured work, not hash grinding](#41-why-structured-work-not-hash-grinding) - [4.2 The Cantor pairing function](#42-the-cantor-pairing-function) - [4.3 Per-axis trees](#43-per-axis-trees) - [4.4 Lowest common ancestor (LCA) height](#44-lowest-common-ancestor-lca-height) - [4.5 Aligned subtrees](#45-aligned-subtrees) - [4.6 Computing the axis root](#46-computing-the-axis-root) - [4.7 Combining into 3D (region_n)](#47-combining-into-3d-region_n) - [4.8 Why you can't cheat distance (decomposition invariance)](#48-why-you-cant-cheat-distance-decomposition-invariance) - [4.9 Five properties unique to this design (non-normative)](#49-five-properties-unique-to-this-design-non-normative) - [5. The Temporal Axis: Every Hop Costs Fresh Work](#5-the-temporal-axis-every-hop-costs-fresh-work) - [5.1 The problem: cacheable spatial roots](#51-the-problem-cacheable-spatial-roots) - [5.2 Terrain-derived temporal height K (normative)](#52-terrain-derived-temporal-height-k-normative) - [5.3 Temporal axis seed and root (normative)](#53-temporal-axis-seed-and-root-normative) - [5.4 The 4D hop preimage](#54-the-4d-hop-preimage) - [5.5 Integer→bytes encoding (normative)](#55-integerbytes-encoding-normative) - [5.6 Movement proof hash (normative)](#56-movement-proof-hash-normative) - [5.7 Worked example (non-normative)](#57-worked-example-non-normative) - [5.8 Performance expectations (non-normative)](#58-performance-expectations-non-normative) - [6. The Wall and the Sidestep](#6-the-wall-and-the-sidestep) - [6.1 The storage bottleneck (non-normative)](#61-the-storage-bottleneck-non-normative) - [6.2 How the sidestep action works](#62-how-the-sidestep-action-works) - [6.3 Sidestep geometry (normative)](#63-sidestep-geometry-normative) - [6.4 Per-axis Merkle root (normative)](#64-per-axis-merkle-root-normative) - [6.5 Streaming computation (normative)](#65-streaming-computation-normative) - [6.6 Spatial region integer (region_m)](#66-spatial-region-integer-region_m) - [6.7 Temporal binding](#67-temporal-binding) - [6.8 Sidestep proof hash (normative)](#68-sidestep-proof-hash-normative) - [6.9 Multi-axis sidestep](#69-multi-axis-sidestep) - [6.10 Openings (normative)](#610-openings-normative) - [6.11 Verification levels](#611-verification-levels) - [6.12 Entering ≠ claiming (non-normative)](#612-entering--claiming-non-normative) - [6.13 Natural continents (non-normative)](#613-natural-continents-non-normative) - [6.14 Performance expectations (non-normative)](#614-performance-expectations-non-normative) - [6.15 Version 2 of the sidestep construction (normative)](#615-version-2-of-the-sidestep-construction-normative) - [7. Location-Based Encryption and Discovery](#7-location-based-encryption-and-discovery) - [7.1 The purpose: chalk on the sidewalk (non-normative)](#71-the-purpose-chalk-on-the-sidewalk-non-normative) - [7.2 Key derivation (normative)](#72-key-derivation-normative) - [7.3 Discovery radius (non-normative)](#73-discovery-radius-non-normative) - [7.4 Discovery scanning (recommended)](#74-discovery-scanning-recommended) - [7.5 Caching optimization (non-normative)](#75-caching-optimization-non-normative) - [7.6 The bag (normative)](#76-the-bag-normative) - [7.7 Hints (optional)](#77-hints-optional) - [7.8 Holding a region (non-normative)](#78-holding-a-region-non-normative) - [8. Nostr Integration: The Movement Chain](#8-nostr-integration-the-movement-chain) - [8.1 Event kind](#81-event-kind) - [8.2 Canonical event id (NIP-01)](#82-canonical-event-id-nip-01) - [8.3 Spawn event (first event)](#83-spawn-event-first-event) - [8.4 Hop event](#84-hop-event) - [8.5 Sidestep event](#85-sidestep-event) - [8.6 Encrypted content event (bag)](#86-encrypted-content-event-bag) - [8.7 Verification summary](#87-verification-summary) - [8.7.1 Hop verification](#871-hop-verification) - [8.7.2 Sidestep verification (Level 1: sampled openings)](#872-sidestep-verification-level-1-sampled-openings) - [8.8 Core action types summary](#88-core-action-types-summary) - [8.9 Protocol extensions (DECKs)](#89-protocol-extensions-decks) - [8.10 Avatar event](#810-avatar-event) - [9. Mapping to Physical Reality: GPS and Dataspace](#9-mapping-to-physical-reality-gps-and-dataspace) - [9.1 Why a physical mapping exists](#91-why-a-physical-mapping-exists) - [9.2 Dataspace cube size (Cantor Height 34 scale)](#92-dataspace-cube-size-cantor-height-34-scale) - [9.3 Scale rationale (non-normative)](#93-scale-rationale-non-normative) - [9.4 Axis naming convention (ECEF → Cyberspace)](#94-axis-naming-convention-ecef--cyberspace) - [9.5 Canonical spec version and deterministic arithmetic](#95-canonical-spec-version-and-deterministic-arithmetic) - [9.6 Altitude handling (normative)](#96-altitude-handling-normative) - [9.7 Canonical mapping algorithm (normative)](#97-canonical-mapping-algorithm-normative) - [9.8 Golden vectors (consensus locks)](#98-golden-vectors-consensus-locks) - [9.9 Consumer benchmarks (non-normative)](#99-consumer-benchmarks-non-normative) - [9.10 Nation-state limits (non-normative)](#910-nation-state-limits-non-normative) - [9.11 Storage as the primary constraint (non-normative)](#911-storage-as-the-primary-constraint-non-normative) - [10. Sectors and Spatial Querying](#10-sectors-and-spatial-querying) - [11. Visualization Conventions](#11-visualization-conventions) - [11.1 Handedness and axis semantics](#111-handedness-and-axis-semantics) - [11.2 Black sun reference marker](#112-black-sun-reference-marker) - [11.3 Camera convention ("facing the black sun")](#113-camera-convention-facing-the-black-sun) - [11.4 Engine adaptation](#114-engine-adaptation) - [12. Limitations and Threat Model (non-normative)](#12-limitations-and-threat-model-non-normative) - [12.1 What the protocol provides](#121-what-the-protocol-provides) - [12.2 What the protocol does NOT provide](#122-what-the-protocol-does-not-provide) - [12.3 Acknowledged attack vectors](#123-acknowledged-attack-vectors) - [13. Structured Proof-of-Work (non-normative)](#13-structured-proof-of-work-non-normative) - [13.1 A new class of proof-of-work](#131-a-new-class-of-proof-of-work) - [13.2 Capacity-bound, and compute-bound within it](#132-capacity-bound-and-compute-bound-within-it) - [13.3 Fixed difficulty](#133-fixed-difficulty) - [13.4 Where the energy goes](#134-where-the-energy-goes) - [13.5 The sidestep as traditional proof-of-work](#135-the-sidestep-as-traditional-proof-of-work) - [14. Reference Implementation](#14-reference-implementation) --- ## 1. Overview: Why This Exists ### 1.1 The problem Every digital system that calls itself a "space" (virtual worlds, social platforms, VR environments, cloud services) shares a fundamental limitation: they are owned. Every one of them has an administrator who can move you, delete you, change the rules, or shut the whole thing down. Your "presence" in any of these systems exists only because someone permits it. This isn't a flaw in specific products. It's a consequence of how they're built. They are **permissioned** systems. And permissioned systems are not space. They are services. Physical space works differently. It imposes costs that nobody can cheat. Not governments, not corporations, not anyone. The cost is thermodynamic: paid in energy, irreversibly, to the universe itself. This is the deepest kind of fairness: not fairness by agreement (which can be broken), but fairness by physics (which cannot). ### 1.2 The approach Cyberspace imposes the same constraint, irreversible computational work, on a 256-bit coordinate system. Movement requires computing mathematical structures called Cantor pairing trees, whose cost scales with the distance crossed. The work is not arbitrary hash grinding; it is the computation of the actual mathematical fabric between coordinates. The proof of your movement *is* the mathematics of the region you crossed. Keypairs traverse Cyberspace by publishing signed Nostr events that commit to their movement history. Each event includes a proof derived from the Cantor tree computation, creating an auditable chain of verified movement. ### 1.3 Key properties - **Public key = spawn coordinate:** your cryptographic identity maps directly into the coordinate fabric. Where you start is determined by who you are. - **Three movement primitives:** spawn (identity placement), hop (Cantor pairing tree proof), and sidestep (Merkle hash tree proof for storage-infeasible boundaries). - **Hash chain continuity:** each movement event references the previous one by its cryptographic hash, forming a linear chain of signed proofs. This chain is the keypair's verifiable movement history and ensures every hop costs fresh work that cannot be cached or replayed. - **Axis symmetry:** equal distances cost equal work regardless of direction. - **Location-based encryption:** keys derive from stable spatial region preimages, enabling content that can only be decrypted by those who do the work to be "there." - **Compact and deterministic:** proofs fit in Nostr events and verify efficiently. This v2 design replaces earlier drift/quaternion/velocity approaches (deprecated). --- ## 2. The Coordinate System ### 2.1 A 256-bit universe Cyberspace exists in a 256-bit integer space. This number was not chosen arbitrarily. 256-bit numbers are the standard unit of work in cryptographic systems like SHA-256, Nostr, and Bitcoin. Working in 256 bits means the coordinate system is natively compatible with the tools that power the rest of the protocol. Those 256 bits are divided into: - **Three spatial axes:** X, Y, and Z, each **85 bits** wide (an unsigned integer from 0 to 2^85 - 1) - **One plane bit:** the least significant bit, which selects between dataspace (0) and ideaspace (1) That accounts for 85 × 3 + 1 = 256 bits exactly. **Core terms:** - **Coordinate (coord256):** A 256-bit integer encoding X/Y/Z plus the plane bit. - **Axes (u85):** X, Y, Z are 85-bit unsigned integers. - **Plane:** 1 bit. `0 = dataspace` (physical mapping), `1 = ideaspace` (non-physical). - **Gibson (G):** The fundamental unit of distance, equal to one axis step in u85 space. ### 2.2 Bit layout (normative) The three axes are **interleaved** into the 256-bit integer, not packed sequentially. This means the bits alternate: an X bit, then a Y bit, then a Z bit, repeating 85 times. Specifically: - Bit `0` (LSB): plane bit `P` - Bits `3, 6, 9, ...` (every 3rd bit starting at 3): X bits (85 bits) - Bits `2, 5, 8, ...` (every 3rd bit starting at 2): Y bits (85 bits) - Bits `1, 4, 7, ...` (every 3rd bit starting at 1): Z bits (85 bits) The result looks like: `XYZXYZXYZ...P` The interleaving has an important consequence: coordinates that are spatially close share similar bit prefixes. This is what makes the Cantor pairing tree work efficiently, because nearby coordinates fall into the same aligned subtrees. ### 2.3 Reference pseudocode ```python AXIS_BITS = 85 def xyz_to_coord(x: int, y: int, z: int, plane: int = 0) -> int: coord = plane & 1 for i in range(AXIS_BITS): coord |= ((z >> i) & 1) << (1 + i * 3) coord |= ((y >> i) & 1) << (2 + i * 3) coord |= ((x >> i) & 1) << (3 + i * 3) return coord def coord_to_xyz(coord: int) -> tuple[int,int,int,int]: plane = coord & 1 x = y = z = 0 for i in range(AXIS_BITS): z |= ((coord >> (1 + i * 3)) & 1) << i y |= ((coord >> (2 + i * 3)) & 1) << i x |= ((coord >> (3 + i * 3)) & 1) << i return (x, y, z, plane) ``` ### 2.4 Two planes: dataspace and ideaspace The plane bit creates two overlapping coordinate spaces: - **Dataspace (plane = 0):** Maps to physical reality via GPS coordinates (see §9). The same X/Y/Z values in dataspace correspond to specific physical locations on and around Earth. - **Ideaspace (plane = 1):** Has no physical mapping. The same X/Y/Z values are purely abstract positions. Ideaspace is where things exist that have no physical counterpart. Both planes share the same mathematical properties. Movement costs, encryption, and discovery all work identically. They differ only in whether the coordinates have a physical-world interpretation. --- ## 3. Identity Is Location ### 3.1 Your key is your coordinate One of the most distinctive properties of Cyberspace is that your cryptographic identity determines your spawn location. Your Nostr public key, the 256-bit number that *is* your identity, is also your coordinate. When you first enter Cyberspace, you appear at the point defined by your key. You don't choose where to spawn. Your identity chooses for you. This means identity and location are the same thing. Not metaphorically, but mathematically. Your key encodes a specific X, Y, Z position. If someone knows your public key, they know where you spawn. If you want a different spawn point, you need a different identity. **Implication:** In every other digital system, identity and location are separate concerns managed by separate authorities. In Cyberspace, the mapping from identity to space is deterministic, public, and permanent. No authority assigns locations. No registry tracks who is where. The math does it. ### 3.2 The spawn event A spawn event is a signed Nostr event that declares "I exist at this coordinate." It is the first event in a keypair's movement chain. The coordinate in a spawn event MUST equal the event's public key (see §8.3 for the full event format). After spawning, a keypair can begin moving through Cyberspace by publishing hop or sidestep events that extend the chain. A keypair may also **respawn** at any time by simply publishing a new spawn event. Because the new spawn event has a newer timestamp, it invalidates all prior movement events in the old chain. The keypair returns to its original spawn coordinate and starts fresh. Prior movement history remains on relays but is no longer part of the active chain. **Derezz** is the common name for this: an identity whose chain has ended and which is back at its spawn coordinate is said to have derezzed, whether it got there by respawning deliberately or by having its chain go invalid. The word is a description of a state, not a mechanism. No event, action or tag in this specification performs a derezz, nothing any other identity publishes can cause one, and §1.1's promise that there is no administrator who can move you or delete you means there never can be at this layer. A game may define anything it likes about liveness inside its own box (`decks/README.md`, Game mechanics), and the protocol will not consult it. --- ## 4. Movement: Cantor Pairing Trees This is the heart of the protocol. Everything else (encryption, discovery, territory, transit) builds on top of the movement system. So it's worth understanding not just *how* it works, but *why* it works this way. ### 4.1 Why structured work, not hash grinding Standard proof-of-work (like Bitcoin mining) works by grinding random numbers until you find a hash that meets a difficulty target. The work is real. You burn energy. But the work is **arbitrary**. You're searching a hash space, not traversing a space. Finding nonce #4,821,337 doesn't tell you anything about where you are or where you went. Cyberspace needs work that is **structural**: work where the computation itself encodes spatial information. The proof of movement should not be "I burned energy" but "I computed the mathematical fabric between these two coordinates." Cantor pairing trees achieve this. They create actual mathematical structure: each root uniquely represents a specific region of coordinate space. Computing it means building a tree from leaves to root. The number you produce is mathematically meaningful, not arbitrary. This is the difference between "digging a hole" and "following a path." Both cost energy, but only one has spatial semantics. ### 4.2 The Cantor pairing function The Cantor pairing function takes two natural numbers and produces exactly one: `π(a, b) = (a + b) × (a + b + 1) / 2 + b` This function is a **bijection**: every input maps to exactly one output, and every output maps back to exactly one input. No two different pairs of numbers ever produce the same result, and every natural number is the result of some pair. Compare this to a hash function like SHA-256, where many different inputs can produce the same output and you cannot reverse it. The Cantor pairing function loses nothing. It is a perfectly reversible, lossless mathematical encoding. Crucially, the function can be reversed. Given any Cantor number, you can unpair it to recover the two numbers that produced it. Applied recursively, this means a single Cantor root encodes an entire tree and that tree can be fully reconstructed from the root alone. When applied recursively (pairing leaves into parents, parents into grandparents, all the way up) it builds a binary tree whose root is a single number that uniquely encodes every leaf in the tree. ### 4.3 Per-axis trees An earlier version of Cyberspace used the full interleaved 256-bit coordinate in a single Cantor tree. This was elegant in theory but failed in practice: - **Axis asymmetry:** Due to bit interleaving, X movements cost ~4× more than Z movements for the same distance. The protocol would privilege certain directions. - **Impractical scaling:** Moving 64 Gibsons took ~115 seconds. Sector traversal would take months. - **Memory explosion:** Large movements produced multi-gigabyte Cantor numbers. The v2 solution: each axis (X, Y, Z) gets its own independent 85-bit Cantor tree. Movement proofs are computed separately per axis, then combined. This gives: - **Axis symmetry:** Equal distances cost equal work regardless of direction. - **Practical performance:** 1,024 Gibsons in ~1ms instead of ~33 seconds. - **Bounded computation:** 85-bit trees are manageable. - **Parallelizable:** Three independent computations can run in parallel. ### 4.4 Lowest common ancestor (LCA) height When you move from one position to another along a single axis, the cost of that movement depends on which **binary boundary** you cross. The Lowest Common Ancestor (LCA) height captures this: it is the level in a binary tree where the paths from the two positions first diverge. For a 1D axis movement between `v1` and `v2`: ```python def find_lca_height(v1: int, v2: int) -> int: if v1 == v2: return 0 return (v1 ^ v2).bit_length() ``` The LCA height determines how much work is required: the Cantor tree you need to compute has `2^h` leaves, where `h` is the LCA height. An important subtlety: the LCA height is not simply a measure of distance in Gibsons. It depends on *which boundary you cross*, not just how far you move. A move of 1,024 Gibsons within a single aligned region might have the same LCA height as a move of 1 Gibson that happens to cross a large power-of-two boundary. Consider two single-Gibson moves that have very different costs: - Moving from position 4 to position 5: `4 ^ 5 = 1`, bit_length = 1, so `h = 1`. This is cheap. You are staying within a small aligned block. - Moving from position 7 to position 8: `7 ^ 8 = 15`, bit_length = 4, so `h = 4`. This costs 16 times as many Cantor pairs, even though you only moved 1 Gibson. The reason is that position 8 sits on the boundary of a height-4 aligned subtree, and crossing that boundary requires computing the entire subtree. At the extreme: moving from position `2^34 - 1` to position `2^34` is a single-Gibson step, but the LCA height is 35: the two positions differ in every one of their lowest 35 bits, so `bit_length(v1 ^ v2) = 35`. That one step requires computing a Cantor tree with over 34 billion leaves, because you are crossing the largest binary boundary in that region of the axis. (The step from `2^33 - 1` to `2^33` is the height-34 case, with over 17 billion leaves.) This is not a quirk. It is the core mechanism by which Cyberspace imposes locality. Boundaries in the binary structure of the coordinate space act as natural walls, and crossing them costs real work regardless of how small the step is. This property is formalized as decomposition invariance in §4.8. ### 4.5 Aligned subtrees Before defining aligned subtrees formally, it is worth understanding why they exist and what problem they solve. When two coordinates are paired into a Cantor tree, the tree must cover a specific range of leaf values. If we allowed the tree to start at any arbitrary position, then two different movements through the same neighborhood could produce different trees with different roots. There would be no stable "regions." Every pair of coordinates would generate its own unique tree, and no two people would agree on what a region looks like or what its identifier is. Alignment solves this by snapping tree boundaries to power-of-two positions in the coordinate space. An aligned subtree of height `h` always starts at a position that is a multiple of `2^h` and always covers exactly `2^h` consecutive leaves. This means the boundaries are fixed and universal. Everyone agrees on where the blocks are, because the blocks are determined by the math, not by anyone's specific movement. Two people standing in the same neighborhood will compute the same Cantor root without ever communicating, because they are both computing the root of the same aligned subtree. This property is what makes location-based encryption work (§7). It is what makes spatial consensus happen automatically. Without alignment, there is no consensus about what a region is, and the entire discovery and encryption system falls apart. **Formal definition:** An **aligned subtree** of height `h` is a binary subtree whose base is a multiple of `2^h`. Think of it like a base address: the subtree "owns" a block of `2^h` leaves, and the base tells you where that block starts. For any value `v` and height `h`: - `base = (v >> h) << h` (the aligned base) - The subtree spans `2^h` consecutive leaves: `[base, base+1, ..., base + 2^h - 1]` For movement between `v1` and `v2`, the **covering aligned subtree** is the smallest aligned subtree that contains both endpoints: - `h = find_lca_height(v1, v2)` - `base = (v1 >> h) << h` (equivalently `(v2 >> h) << h`) **Example:** Moving from 0 to 5: - `h = find_lca_height(0, 5) = 3` (because `0 ^ 5 = 5`, bit_length = 3) - `base = (0 >> 3) << 3 = 0` - Aligned subtree covers leaves `[0, 1, 2, 3, 4, 5, 6, 7]` (8 leaves = `2^3`) **Example:** Moving from 4 to 7: - `h = find_lca_height(4, 7) = 2` (because `4 ^ 7 = 3`, bit_length = 2) - `base = (4 >> 2) << 2 = 4` - Aligned subtree covers leaves `[4, 5, 6, 7]` (4 leaves = `2^2`) The alignment property is what makes region-based discovery work: coordinates within the same subtree share the same root. Two people standing in the same region will compute the same Cantor number without ever communicating. The math determines the answer, and the math is the same for everyone. ### 4.6 Computing the axis root Given a movement from `v1` to `v2` on one axis, the **axis Cantor root** is computed by building a Cantor pairing tree over the aligned subtree's leaves, pairing adjacent nodes bottom-up until one value remains: ```python def cantor_pair(a: int, b: int) -> int: s = a + b return (s * (s + 1)) // 2 + b def compute_subtree_cantor(base: int, height: int) -> int: if height < 0: raise ValueError("height must be >= 0") if height == 0: return base values = list(range(base, base + (1 << height))) for _ in range(height): values = [cantor_pair(values[i], values[i + 1]) for i in range(0, len(values), 2)] return values[0] ``` The axis root for movement `(v1 → v2)` is: - `h = find_lca_height(v1, v2)` - `base = (v1 >> h) << h` - `axis_root(v1, v2) = compute_subtree_cantor(base, h)` (Implementations MAY compute this with any equivalent algorithm; the result MUST match this definition.) **A root names a region at a height.** The same region always produces the same root, and at one height different regions always produce different roots, because every pairing step is a bijection. Across heights the root alone is not always enough: at height 0 there is nothing to pair, so a position's root is the position itself, and a small region at a positive height can share its root with a single position. `compute_subtree_cantor(2, 1)` is `π(2, 3) = 18`, and so is `compute_subtree_cantor(18, 0)`. For every height of 1 and above, roots are distinct across heights as well as within them, so the identifier of a region is the pair `(root, height)` in general, and the root alone identifies both the region and its height whenever the height is at least 1. **Why roots at heights 1 and above never collide (non-normative).** Suppose two regions of different heights `h1 < h2`, both at least 1, had the same root. Every pairing step is a bijection, so peeling one level off both trees gives equal left-child roots and equal right-child roots, and peeling `h1` levels gives that the `2^h1` consecutive positions of the lower region equal the roots of `2^h1` adjacent aligned regions of height `d = h2 - h1`, which is at least 1. Adjacent aligned regions at any height of 1 or more have roots that differ by at least 16 (`compute_subtree_cantor(2, 1) - compute_subtree_cantor(0, 1) = 18 - 2 = 16`, the smallest case), so two consecutive integers cannot both be such roots. The lower region therefore has a single position, which is height 0. The height-0 case never reaches a bag: a bag's height is at least 1 (§7.6) and discovery scans heights from 1 (§7.4). ### 4.7 Combining into 3D (region_n) Each axis computes its root independently: - `cantor_x = axis_root(x1, x2)` - `cantor_y = axis_root(y1, y2)` - `cantor_z = axis_root(z1, z2)` Then they are combined using nested Cantor pairing: - `region_n = π(π(cantor_x, cantor_y), cantor_z)` The resulting `region_n` is the **stable spatial region integer**, a single number that uniquely identifies the 3D region implied by the movement. This number is used for location-based encryption and discovery (§7). **Region uniqueness (non-normative):** Each aligned subtree root corresponds to a unique region at its height, and for heights of 1 and above to a unique region across heights as well (§4.6). Many coordinate pairs inside the same aligned subtree share the same root; this is intentional. The Cantor root is a **region identifier**, not a unique coordinate-pair identifier. Example (1D): ``` LCA(0, 3) => subtree [0..3] => root = 228 LCA(1, 2) => subtree [0..3] => root = 228 LCA(0, 2) => subtree [0..3] => root = 228 ``` This means all these movements "see" the same region, which is exactly what enables location-based discovery. **Heights need not match across axes (non-normative).** A movement's three axis heights are computed independently (§4.4), and an axis along which the position did not change has height 0, so its root is the position itself. That is ordinary: a move along X alone has heights `(h, 0, 0)`. A movement's `region_n` is verified by recomputing it from the two coordinates, so it does not rely on a root identifying its height. A bag's region is one aligned cube, one height for all three axes and at least 1 (§7.6), so its `region_n`, its key and its `lookup_id` (§7.2) are distinct from those of every other region at every height. ### 4.8 Why you can't cheat distance (decomposition invariance) A natural question: can you cheat the cost of a large movement by breaking it into many small steps? If moving from 0 to 1000 costs a lot of work, can you instead take 1000 one-Gibson steps and pay less? **No.** This is provable. **Theorem** (follows from the alignment properties of complete binary trees and the 2-adic valuation): For any two leaf positions `v1` and `v2` with `v1 < v2`: ``` find_lca_height(v1, v2) == max(find_lca_height(i, i+1) for i in range(v1, v2)) ``` The LCA height of the direct pair is always equal to the maximum LCA height among all unit-step pairs in the sequence. **What this means in plain terms:** If you decompose a movement `(v1 → v2)` into sequential adjacent steps, at least one step in the sequence will require the same covering subtree height as the direct movement. You can't avoid the expensive boundary crossing, because any path from v1 to v2 must cross it. **Proof sketch:** The LCA height `h` of `(v1, v2)` is determined by the highest bit position where `v1` and `v2` differ. Any sequential walk from `v1` to `v2` must cross the largest power-of-2 boundary between them. The step at which this crossing occurs produces an XOR with a bit set at the same highest position, yielding identical height `h`. In simple terms: the computational cost of movement is the same whether you take small steps or large steps. You can't avoid, decompose, or sidestep the computational burden of power-of-two boundaries along each axis. This is the digital equivalent of the triangle inequality: you can't cheat geometry. ### 4.9 Five properties unique to this design (non-normative) The Cantor pairing tree produces five properties that, to our knowledge, no other digital spatial system possesses: **1. Locality without trusted parties.** The cost of computation scales with the height of the Cantor tree, which scales with distance. This is locality — the fundamental property of space — and it emerges from the math without any authority enforcing it. Nobody decides how much it costs to move. The mathematics decides. **2. Hierarchical spatial encryption.** Because each region has a unique Cantor root, that root can serve as a cryptographic key. Content encrypted with a region's root can only be decrypted by someone who computes that root (see §7). This is the digital equivalent of writing a message in chalk on a sidewalk: you can only read it by being there. **3. Work equivalence.** In most digital systems, observing is free and doing costs. In Cyberspace, observation and action cost nearly the same. To discover what's in a region, you must compute the region's root, the same work a traveler would do to cross it. There is no free surveillance. **Where the equivalence is not exact, and it favors the observer (non-normative).** Two differences, neither of which the earlier wording admitted. First, a traveler additionally pays the temporal axis, which §7.2 deliberately excludes from key derivation, so observing one region is strictly cheaper than crossing into it by that term. Second, and without bound at scale, **a traveler's work is sequential and an observer's is not**: each chain event commits to the previous one, so a thousand moves are a thousand moves in order, while a thousand regions to observe is an embarrassingly parallel job for a cluster. The equivalence is therefore a good description of one region and a poor one of a million. §7.2's key derivation is where that gap would be closed if it is to be closed. **4. Deterministic regions.** Any two people computing the Cantor root of the same aligned subtree will get the same answer. Regions don't need to be assigned, registered, or coordinated. They exist as mathematical facts. Spatial consensus happens automatically, without communication. **5. Decomposition invariance.** As proven in §4.8, sequential decomposition doesn't reduce cost. There are no shortcuts through space. **Checking a root is cheap; producing one is not (non-normative).** These are different operations and only the second is protected. The low bits of a Cantor pairing depend only on the low bits of its operands, because carries propagate upward, and the one right shift in `(s(s+1))/2` pulls down exactly one bit per level. So carrying a window of `k + h` low bits through the same `2^h` pairings reproduces the true root's low `k` bits **exactly**, in a few machine words instead of the root's full width. Measured against real roots, a 64-bit answer is exact at every height tested, and the saving is 116 times at h18 and grows with height. This does not weaken anything above. The operation count is unchanged, so §4.8 is untouched: the window performs the same `2^h` pairings in the same order and merely carries fewer bits through each. And **the window never yields the root**. At h34 it returns 64 bits of a number 1.46 trillion bits wide, so it cannot produce `region_bytes`, cannot derive a key under §7.2, and cannot forge a proof. **It detects error, not fraud, and the difference matters.** The window is cheap for everyone symmetrically, so an adversary runs it too, learns the same low bits, and appends whatever they like above them. A prefix that matches is therefore evidence of an honest mistake not having happened, and no evidence at all against somebody trying. Use it to confirm that a transfer arrived intact, that a disclosed root is for the region you asked for rather than a neighbouring one, or that a long computation on your own hardware did not corrupt. Do not use it as a proof of anything. The load-bearing consequence is the negative one: **a root prefix is not evidence of possession.** Anyone can compute one in seconds without holding the region, so no protocol, market or game may treat a prefix as attestation. Possession is demonstrated by using the root, which is to say by deriving a key that actually decrypts, and a wrong root simply fails to open anything. These five properties together create something unprecedented: digital space with the structural integrity of physical space, enforced by mathematics rather than by any authority. --- ## 5. The Temporal Axis: Every Hop Costs Fresh Work ### 5.1 The problem: cacheable spatial roots The per-axis Cantor roots described in §4 are **region identifiers**: the same region always produces the same root. This is desirable for discovery and encryption (you want the "address" of a region to be stable). But it creates a movement loophole: once a mover has computed the Cantor root for a region, they could reuse that cached result to generate arbitrarily many hop events through the same region at near-zero marginal cost. They would only be able to return to places they had already been, but instant teleportation over previously-trod terrain breaks the proof-of-work continuity of Cyberspace as a thermodynamic system. The solution: extend movement proofs into a fourth dimension, a **temporal work axis** derived from the Nostr movement chain. Each hop includes an additional Cantor tree computation whose inputs are derived from chain context. Because each Nostr event id commits to the previous event (including its proof), the temporal seed for hop N is not known until hop N-1 is complete. The work cannot be precomputed or amortized. **What this does NOT change:** Stable spatial region identifiers (used for location-based encryption and discovery in §7) remain a pure function of coordinates, independent of time or identity. The temporal axis exists only to make advancing the movement chain cost work. **What this is NOT:** This is not a continuous "heartbeat" cost. An avatar's last hop event remains its state indefinitely. The temporal axis work is paid only when moving. ### 5.2 Terrain-derived temporal height K (normative) The temporal axis has a height `K` that varies by destination: a deterministic "terrain" function that makes some regions of Cyberspace intrinsically easier or harder to traverse. Think of it as hills and valleys in the computational landscape. **Constants:** - `TERRAIN_DOMAIN_V2 = b"CYBERSPACE_TERRAIN_K_V2"` (ASCII bytes) - `TERRAIN_CELL_BITS = [3, 7, 9, 11]` (exactly four integers) If any part of this terrain function changes (constants, hashing preimage format, byte selection, etc.), the domain string MUST be bumped to a new value to avoid ambiguity. **Algorithm:** Given the hop destination coordinate `(x2, y2, z2, plane)`: 1. For each `bits` in `TERRAIN_CELL_BITS` (in order): 1. Align the destination to the cell of width `2^bits` along each axis: - `bx = (x2 >> bits) << bits` - `by = (y2 >> bits) << bits` - `bz = (z2 >> bits) << bits` 2. Compute `cell_coord = xyz_to_coord(bx, by, bz, plane)`. 3. Encode `cell_coord` as exactly 32 big-endian bytes: `cell_coord_bytes`. 4. Compute `digest = sha256(TERRAIN_DOMAIN_V2 || byte(bits) || cell_coord_bytes)`. - `byte(bits)` is a single unsigned byte with value `bits`. 5. Record `nibble_i = digest[0] & 0x0F` (the low 4 bits of the first byte of the digest). 2. Concatenate the four nibbles into a 16-bit word: - `word16 = (nibble_0 << 12) | (nibble_1 << 8) | (nibble_2 << 4) | nibble_3` 3. Define `K` as the popcount of `word16` (the number of 1 bits in its 16-bit binary representation). - Therefore `K` MUST be an integer in `[0, 16]`. **Why this works (non-normative):** Because `K` is the popcount of 16 pseudorandom bits, it follows approximately a binomial distribution with mean 8. The worst-case temporal computation is `2^16 = 65,536` Cantor pairs (~100 ms on modern hardware). The aligned cells at different scales introduce spatial correlation, so nearby coordinates tend to share the same K value, creating "hills" in the temporal landscape. ### 5.3 Temporal axis seed and root (normative) For hop events, let `previous_event_id` be the 32-byte NIP-01 event id referenced by the `e` tag with marker `previous` (see §8.4). 1. Parse `previous_event_id` from lowercase hex into 32 bytes. 2. Interpret it as a big-endian integer and reduce into the u85 axis: - `prev_id_int = int.from_bytes(previous_event_id_bytes, "big")` - `t = prev_id_int % (1 << 85)` 3. Compute an aligned temporal subtree and its Cantor root at the terrain-derived height `K`: - `t_base = (t >> K) << K` - `cantor_t = compute_subtree_cantor(t_base, K)` **Key insight (non-normative):** The temporal axis is derived from chain context and destination coordinates, not wall-clock time. There is no continuous "alive" cost; you pay this work only when you publish a hop. ### 5.4 The 4D hop preimage The hop preimage combines spatial and temporal work: - `hop_n = π(region_n, cantor_t)` This is the single number that encodes both *where* you moved (spatial) and *when* in the chain you moved (temporal). ### 5.5 Integer→bytes encoding (normative) Many operations in this protocol hash large integers. This spec defines the canonical encoding: ```python def int_to_bytes_be_min(n: int) -> bytes: if n < 0: raise ValueError("expected non-negative") if n == 0: return b"\x00" # minimal big-endian bytes return n.to_bytes((n.bit_length() + 7) // 8, "big") ``` ### 5.6 Movement proof hash (normative) The movement proof hash is derived from the 4D hop preimage `hop_n`. This intentionally differs from the stable spatial region identifiers used for location-based encryption and discovery (§7). - `hop_bytes = int_to_bytes_be_min(hop_n)` - `movement_proof_key = sha256(hop_bytes)` (32 bytes) - `proof_hash = sha256(movement_proof_key)` (32 bytes) When used in Nostr tags, `proof_hash` MUST be encoded as lowercase hex in the `proof` tag. ### 5.7 Worked example (non-normative) Movement: `(0, 0, 0) → (4104, 0, 0)` Per-axis roots: - X: `0 → 4104` => height 13 => root = `compute_subtree_cantor(0, 13)` - Y: `0 → 0` => height 0 => root `0` - Z: `0 → 0` => height 0 => root `0` 3D combine (stable spatial region integer): - `region_n = π(π(cantor_x, 0), 0)` Stable lookup id (used for location-based encryption/discovery, §7.2): - `lookup_id = sha256(sha256(int_to_bytes_be_min(region_n)))` - `8d2463eb22301d97a1f7e33b90e473ba2eec69079f418a72609c3e4d2981669b` Terrain-derived temporal height at destination `(x2=4104, y2=0, z2=0, plane=0)` using `TERRAIN_CELL_BITS = [3, 7, 9, 11]`: For each cell size, align the destination coordinate and extract a nibble: - `bits=3`: cell = `(4104, 0, 0)` → nibble = `0b0000` (0 ones) - `bits=7`: cell = `(4096, 0, 0)` → nibble = `0b1111` (4 ones) - `bits=9`: cell = `(4096, 0, 0)` → nibble = `0b1111` (4 ones) - `bits=11`: cell = `(4096, 0, 0)` → nibble = `0b1101` (3 ones) Combine into 16-bit word and compute popcount: - `word16 = (0 << 12) | (15 << 8) | (15 << 4) | 13 = 0b0000111111111101` - `K = popcount(word16) = 0 + 4 + 4 + 3 = 11` Temporal axis example using `previous_event_id` = 64 hex zeros: - `previous_event_id = "0000000000000000000000000000000000000000000000000000000000000000"` - `t = 0` - `t_base = 0` - `cantor_t = compute_subtree_cantor(0, 11)` 4D hop preimage and movement proof hash: - `hop_n = π(region_n, cantor_t)` - `proof_hash = sha256(sha256(int_to_bytes_be_min(hop_n)))` - `ed9d09ca697b2da29c9d042207ac8ef7aab40f6dde550e6467452aa0e2e8cac6` Different `previous_event_id` values produce different `proof_hash` values, even for identical spatial moves. This is the temporal axis at work. ### 5.8 Performance expectations (non-normative) Reference implementations observe that cost grows with the per-axis LCA height (because the aligned subtree contains `2^h` leaves). In addition, each hop includes the temporal axis traversal at terrain-derived height `K`, imposing non-cacheable work per hop even when spatial `region_n` is reused. Because `K` depends on the destination coordinate, some regions of Cyberspace are intrinsically easier or harder to traverse. Approximate per-axis expectations from early benchmarks (illustrative only): | Distance | Height | Cantor size | Time (approx) | |---|---:|---:|---:| | 1 G | 1 | 1 B | < 0.01 ms | | 16 G | 5 | 90 B | < 0.01 ms | | 256 G | 9 | 2.5 KB | ~ 0.1 ms | | 1,024 G | 11 | 12 KB | ~ 1 ms | | 4,096 G | 13 | 57 KB | ~ 10 ms | | 65,536 G | 17 | 1.2 MB | ~ 1 sec | Implementations should cap per-hop distance for UX and may rely on multiple hops for long travel. --- ## 6. The Wall and the Sidestep The Cantor pairing tree gives Cyberspace its spatial fabric, but it also reveals a fascinating limitation that turns out to be a feature. ### 6.1 The storage bottleneck (non-normative) As the Cantor tree gets taller (meaning, as you cross larger regions of space), the intermediate values computed during the pairing process grow exponentially in **bit size**. The Cantor pairing function `π(a, b) = (a + b)(a + b + 1)/2 + b` produces outputs roughly quadratic in the size of its inputs. After `h` levels of pairing, intermediate values can be millions or billions of bits long, and they must be stored during construction because parent nodes require both children. | LCA Height | Cantor Intermediate Storage | Merkle Working Memory | |---:|---:|---:| | h20 | ~11 MB | 640 bytes | | h30 | ~11 GB | 960 bytes | | h34 | ~182 GB | 1,088 bytes | | h40 | ~11 TB | 1,280 bytes | | h50 | ~11 PB | 1,600 bytes | | h60 | ~12 EB (about 0.1% of installed world storage) | 1,920 bytes | | h70 | ~12 ZB (roughly all installed storage today) | 2,240 bytes | This is not a bug. This is the digital equivalent of a mountain range. Storage figures in this document are `85 × 2^h` bits per axis root in decimal units (1 GB is 10^9 bytes); the h34 root is 182.5 GB, which is 170 GiB. The storage bottleneck creates natural barriers in Cyberspace: walls that cannot be crossed by direct Cantor computation regardless of how much time you have, because you simply can't store the intermediate values. These walls aren't designed by anyone. They emerge from the mathematics. But walls are only interesting if there's a way to get past them — expensively, deliberately, with real effort. ### 6.2 How the sidestep action works A **sidestep** action traverses 1 Gibson in a direction using an alternative POW that makes otherwise impossible LCA barriers feasible to cross. Instead of Cantor pairing, a sidestep produces a tree with a **Merkle hash tree** over SHA-256 hashes of leaf coordinates. The critical insight: SHA-256 operations are **fixed-size** (256 bits in, 256 bits out) regardless of tree height. No storage bottleneck. The cost of a sidestep is purely **time**: how long it takes to hash every leaf coordinate. At heights above roughly h16 a sidestep is cheaper in wall-clock time than the equivalent hop, and the gap widens with height (see §6.14). The hop remains the primitive that produces a region root; the sidestep is the primitive that crosses a boundary. An agent hops when it wants the root (a discovery key or a domain) and sidesteps when it wants to move. The sidestep is a **toll**: its work is seeded by the mover's chain position, so it is paid in full by every traveller and cannot be reduced, sold, or inherited from anyone else's published proof (§6.4, §6.15). The hop is deliberately not a toll, because its work product is a canonical region root that a holder may choose to share (§6.12). **Core terms:** - **Sidestep:** A movement action that crosses an LCA boundary via a Merkle hash tree proof instead of a Cantor pairing tree proof. Crosses exactly 1 Gibson past the boundary, regardless of the amount of work it takes. - **Merkle root (sidestep):** The root hash of a binary Merkle tree built over SHA-256 hashes of every leaf coordinate in an aligned subtree, **seeded by the mover's chain position**. Domain-separated from other protocol hashes. - **Toll:** The property that a sidestep's spatial work is non-transferable. Every traveller crossing a given boundary pays the full price; no published proof reduces the cost for any other traveller. - **Openings:** The inclusion paths published with a sidestep: the destination leaf's path plus `SIDESTEP_SAMPLES` paths at pseudorandomly sampled positions (§6.10). - **SIDESTEP_DOMAIN:** `b"CYBERSPACE_SIDESTEP_V2"`, the domain separation prefix used for all sidestep leaf hashes. - **SIDESTEP_SAMPLE_DOMAIN:** `b"CYBERSPACE_SIDESTEP_SAMPLE_V1"`, the domain separation prefix used to derive sampled opening indices. - **SIDESTEP_SAMPLES:** `8`, the number of sampled openings published per non-trivial axis. ### 6.3 Sidestep geometry (normative) A sidestep is the smallest possible move across a wall. The **wall** is the boundary between the two children of the LCA subtree: the two aligned halves that an axis value must cross between when its high bit at height `h - 1` flips. A sidestep starts on the leaf touching the wall on the source side, and ends on the leaf touching the wall on the destination side. The two leaves are neighbours: they differ by exactly 1 Gibson, and the wall runs between them. Everything else the avatar wants to do (reaching the wall, and continuing past it) is done with hops, which are the primitive that produces a region root (§4, §5); the sidestep only crosses. Given source axis value `v1` and destination axis value `v2` on an axis where movement occurs: - The LCA height `h = find_lca_height(v1, v2)` identifies the wall. `base = (v1 >> h) << h` is the first leaf of the LCA subtree, and `half = 2^(h - 1)` is the number of leaves in each of its two children. The wall lies between leaf `base + half - 1` (the last leaf of the lower child) and leaf `base + half` (the first leaf of the upper child). - The source MUST be the leaf touching the wall on its side. Crossing upward (`v2 > v1`) the source is `base + half - 1`: every bit below `h - 1` is one. Crossing downward (`v2 < v1`) the source is `base + half`: every bit below `h - 1` is zero. - The destination MUST be the neighbouring leaf on the other side of the wall: `v2 = v1 + 1` crossing upward, `v2 = v1 - 1` crossing downward. In both directions `|v2 - v1| = 1`, and `v1` and `v2` are in different children of the LCA subtree. - Non-crossing axes MUST have `v1 == v2`. A sidestep that crosses walls on more than one axis at once is described in §6.9; each crossing axis obeys the rules above independently. Worked examples on one axis (values written in binary, most significant bit first): - `v1 = 0b0111` and `v2 = 0b1000`. Here `h = 4`, `base = 0`, `half = 8`. The wall is between leaves 7 and 8. The source 7 is `base + half - 1` (all lower bits one) and the destination 8 is `base + half` (all lower bits zero): a valid upward sidestep. - `v1 = 0b1000` and `v2 = 0b0111`. The same wall crossed downward: the source 8 is `base + half` and the destination 7 is `base + half - 1`: a valid downward sidestep. - `v1 = 0b0101` and `v2 = 0b1000`. Still `h = 4`, but the source 5 is not touching the wall (leaf 7 is). This is NOT a valid sidestep. The mover first hops from 5 to 7 (a hop with `h = 2`), then sidesteps from 7 to 8. - `v1 = 0b0111` and `v2 = 0b1011`. Not a valid sidestep either: the destination 11 is 3 Gibsons past the wall. The mover sidesteps from 7 to 8, then hops from 8 to 11 (a hop with `h = 2`). Why the source constraint exists: the Merkle root (§6.4) is computed over every leaf of the whole LCA subtree, `2^h` leaves, whichever leaf the avatar starts from. If the source could be anywhere inside its half, a single sidestep event would carry the avatar up to `half - 1` Gibsons to the wall and then across, with no hop and therefore no region root along the way. Pinning the source to the wall keeps a sidestep to its one job, crossing, and keeps every other Gibson of travel inside hops. It also removes an ambiguity: with the source at the wall, "the first leaf of the adjacent aligned subtree" and "exactly 1 Gibson past the boundary" describe the same leaf in both directions. A verifier at Level 1 (§6.11) checks this geometry directly from the event's coordinates before it looks at any hash: `h` from the two values, the source touching the wall on its side, the destination exactly one Gibson beyond it. An event that fails the geometry check is invalid regardless of its proof. ### 6.4 Per-axis Merkle root (normative) Sidestep leaves are **seeded**. Let `previous_event_id` be the 32 raw bytes of the id referenced by this event's `e` tag with marker `previous`, and let `axis_byte` be a single byte identifying the axis: `0x00` for X, `0x01` for Y, `0x02` for Z. Define the per-axis seed prefix: ``` seed_prefix = SIDESTEP_DOMAIN || previous_event_id || axis_byte || SEED_PAD ``` `SIDESTEP_DOMAIN` is 22 bytes and `SEED_PAD` is 9 zero bytes, so `seed_prefix` is **exactly 64 bytes**: one full SHA-256 block. This is deliberate (§6.5). For each axis where movement occurs (`v1 ≠ v2`): 1. Compute `h = find_lca_height(v1, v2)`. 2. Compute the aligned subtree base: `base = (v1 >> h) << h`. 3. The aligned subtree contains `2^h` leaves: `[base, base+1, ..., base + 2^h - 1]`. 4. For each leaf value `L_i` (where `i` ranges from `0` to `2^h - 1`): - Encode as big-endian minimal bytes: `leaf_bytes = int_to_bytes_be_min(base + i)` - Compute leaf hash: `H_i = SHA256(seed_prefix || leaf_bytes)` 5. Build the Merkle tree bottom-up: - For each pair `(H_{2j}, H_{2j+1})`: `parent = SHA256(H_{2j} || H_{2j+1})` - Continue until a single root `M_axis` remains. 6. The per-axis Merkle root is `M_axis` (32 bytes). **Domain separation constant (normative):** ``` SIDESTEP_DOMAIN = b"CYBERSPACE_SIDESTEP_V2" # 22 bytes SEED_PAD = b"\x00" * 9 # brings seed_prefix to exactly 64 bytes ``` If any aspect of sidestep leaf hash computation changes in a future version (preimage format, domain string, encoding), the domain string MUST be bumped to a new value to avoid cross-version collisions. **Trivial axes (`h = 0`):** When `v1 == v2` on an axis, the Merkle root is the single leaf hash: `M_axis = SHA256(seed_prefix || int_to_bytes_be_min(v1))`. No tree construction is needed. **Why the seed (normative rationale):** Without it, the leaf preimage contains no identity and no chain context, so the root for a given aligned subtree is the same 32 bytes for every traveller in the history of the protocol. Because the destination is deterministic (§6.3) and both the roots and the openings are published in the clear (§8.5), the first identity to cross any boundary would publish everything a later identity needs: a follower could copy `mr` and `mp` from a relay, compute only the temporal axis (at most 2^16 Cantor pairs, about 100 ms), and produce a proof that passes both verification levels, because the copied root *is* the correct root. Boundary crossings would be priced for the pioneer and free for everyone after. The seed makes each traveller's tree unique to its chain position, so the price in §6.14 is what every traveller pays, every time. **Why the axis byte (normative rationale):** Earth is centred at exactly `2^84` on every axis (§9.7) and its surface radius is about `2^55.6` Gibsons. Because `2^84` is a multiple of `2^h` for every `h ≤ 84`, the centre is always a cell boundary, so above about h56 the aligned base on each axis takes one of only two values, and any two axes falling on the same side of the centre plane share it exactly. Without axis separation such a crossing would compute one tree and claim it as two or three, and §6.9's "the total work is the sum of per-axis work" would be false by that factor at exactly the heights where the work is largest. ### 6.5 Streaming computation (normative) The Merkle tree MUST be computable in streaming fashion without storing all leaf hashes simultaneously: 1. Process leaves in ascending order (deterministic enumeration from `base` to `base + 2^h - 1`). 2. Maintain a stack of pending intermediate hashes, maximum depth `h`. 3. Working memory: `h × 32` bytes (under 3 KB even at h85). 4. Total hash operations: `2^(h+1) - 1` (`2^h` leaf hashes + `2^h - 1` internal hashes). ```python def compute_merkle_root_streaming(seed_prefix: bytes, base: int, height: int) -> bytes: """Compute Merkle root over aligned subtree in O(h) memory.""" if height == 0: return sha256(seed_prefix + int_to_bytes_be_min(base)) stack = [] # (hash_value, level) for i in range(1 << height): leaf_bytes = int_to_bytes_be_min(base + i) current = sha256(seed_prefix + leaf_bytes) level = 0 while stack and stack[-1][1] == level: left = stack.pop()[0] current = sha256(left + current) level += 1 stack.append((current, level)) return stack[0][0] ``` (Implementations MAY use any equivalent algorithm; the result MUST match this definition.) **Midstate optimization (normative for performance, not for consensus):** `seed_prefix` is exactly 64 bytes and constant for the whole tree, so it fills exactly one SHA-256 block. Implementations SHOULD precompute the compression-function midstate over that block once per axis and resume from it for every leaf. Because `leaf_bytes` is at most 11 bytes and SHA-256 padding needs 9, each leaf then costs exactly one further compression, which is the same per-leaf cost as an unseeded leaf. The figures in §6.14 are therefore unchanged by seeding. Had `seed_prefix` not been padded to a block boundary, each leaf would have straddled two blocks and every sidestep in the protocol would have cost twice as much. Implementations that ignore the midstate optimization are still consensus-correct; they are simply half as fast. ### 6.6 Spatial region integer (region_m) Combine per-axis Merkle roots into a single spatial proof integer: ```python mx = int.from_bytes(M_x, "big") # X-axis Merkle root as integer my = int.from_bytes(M_y, "big") # Y-axis Merkle root as integer mz = int.from_bytes(M_z, "big") # Z-axis Merkle root as integer region_m = cantor_pair(cantor_pair(mx, my), mz) ``` This mirrors the hop proof's `region_n = π(π(cantor_x, cantor_y), cantor_z)` but uses Merkle roots instead of Cantor roots. **All three axes always use Merkle roots in a sidestep.** There is no mixed mode; the proof type (Cantor or Merkle) is determined by the action type (`hop` or `sidestep`), not per-axis. ### 6.7 Temporal binding The temporal binding mechanism for sidesteps is **identical** to hop proofs (§5.2–§5.3): 1. Compute terrain height `K` at the destination coordinate per §5.2. 2. Derive temporal seed `t` from `previous_event_id` per §5.3. 3. Compute `cantor_t = compute_subtree_cantor(t_base, K)`. This is always feasible because `K ≤ 16` (maximum 65,536 Cantor pairs, ~100 ms). **Why temporal binding is retained:** In v1 of the sidestep construction the temporal axis was the *only* thing binding a proof to a chain position, because the spatial Merkle root was deterministic and replayable. Since §6.4 now seeds the spatial leaves with `previous_event_id`, replay protection is already provided by the spatial component. The temporal axis is retained because it carries the terrain-derived height `K` (§5.2), which is a property of the destination and not of the mover, and because it keeps sidestep and hop proofs structurally parallel. It is no longer load-bearing for replay. **No precomputation (normative consequence):** Because `seed_prefix` depends on `previous_event_id`, which is only known once the preceding event is published, the spatial Merkle root cannot be computed in advance of a crossing. An entity MUST perform the full spatial work between publishing its previous event and publishing the sidestep. This is the intended behaviour: it is what makes the cost in §6.14 a toll rather than a one-time preparation that can be amortized or scheduled. **Resumption (non-normative):** A crossing at h47 or above is hours of GPU work that can no longer be staged ahead of time, so an interrupted crossing is expensive to lose. Implementations SHOULD persist completed subtree state keyed by `(previous_event_id, axis_byte, base, h)` so an interrupted sidestep resumes rather than restarting. The seeding of §6.4 makes this safe: cached state is only ever valid for the one chain position it was computed under, so it can never be replayed into a later crossing. This mirrors `decks/DECK-0001-hyperspace.md` §5.7. ### 6.8 Sidestep proof hash (normative) Combine spatial and temporal components: ```python sidestep_n = cantor_pair(region_m, cantor_t) ``` Apply double SHA-256 (consistent with hop proofs, §5.6): ```python sidestep_bytes = int_to_bytes_be_min(sidestep_n) proof_key = sha256(sidestep_bytes) proof_hash = sha256(proof_key) # 32 bytes ``` When used in Nostr tags, `proof_hash` MUST be encoded as lowercase hex. ### 6.9 Multi-axis sidestep A single sidestep event MAY cross boundaries on multiple axes simultaneously (if source and destination differ on more than one axis). Each crossing axis obeys the geometry of §6.3 on its own: its source touches its wall and its destination is 1 Gibson past it, so the event moves the avatar by exactly 1 Gibson on every crossing axis and by nothing on the others. The proof is constructed independently per axis: - Each axis computes its own Merkle root (or trivial single-leaf hash for axes where `h = 0`). - The three roots are combined into `region_m` via Cantor pairing (§6.6). - Temporal binding applies once to the combined proof, not per-axis. - The total work is the sum of per-axis work. ### 6.10 Openings (normative) In addition to the Merkle root, the prover MUST publish, for each axis where movement occurs, an inclusion proof for the destination leaf **and** `SIDESTEP_SAMPLES` inclusion proofs at pseudorandomly sampled positions. Each inclusion proof is a sequence of sibling hashes from leaf to root: ``` axis_proof = H_sibling_0 || H_sibling_1 || ... || H_sibling_{h-1} ``` Where `H_sibling_i` is the 32-byte sibling hash at depth `i` (leaf = depth 0). The verifier determines left/right ordering at each level from the leaf's position in the subtree (deterministic from the leaf value). **Sample indices.** For an axis with root `M_axis`, height `h`, and `axis_byte` as in §6.4, the sampled positions are, for `i` in `0 .. SIDESTEP_SAMPLES - 1`: ``` idx_i = int(SHA256(SIDESTEP_SAMPLE_DOMAIN || M_axis || axis_byte || be32(i))) mod 2^h ``` `be32(i)` is `i` as four big-endian bytes. Indices are positions within the aligned subtree, so the sampled leaf value is `base + idx_i`. Indices MAY collide; implementations MUST NOT deduplicate, so that the opening count is fixed and the encoding in §8.5 is fixed-width. **Constants (normative):** ``` SIDESTEP_SAMPLE_DOMAIN = b"CYBERSPACE_SIDESTEP_SAMPLE_V1" SIDESTEP_SAMPLES = 8 ``` **Ordering.** The openings for an axis are ordered: the destination proof first, then the sampled proofs in ascending `i`. Each is exactly `h` sibling hashes, so an axis contributes exactly `(SIDESTEP_SAMPLES + 1) × h × 32` bytes. **Trivial axes (`h = 0`):** no openings are published. The Merkle root IS the single leaf hash and there is nothing to sample. **Why sampling is required (normative rationale):** The destination inclusion proof alone proves only that the destination leaf sits in *some* tree with the claimed root. It does not prove the tree was built over the aligned subtree, and it never did. A prover can fabricate `h` arbitrary sibling hashes, hash upward from the genuine destination leaf, and publish the result: `h` hash operations instead of `2^h`, passing the destination check exactly. Under the v1 unseeded construction this gap was survivable, because every honest root for a boundary was the same 32 bytes, so any party holding that root detected the forgery by comparison, and no rational prover forged when copying was free and correct. Seeding removes the copy, and with it removes comparison auditing: after §6.4 there is no canonical root to compare a claim against, and only a full `O(2^h)` recomputation would catch a fabricated tree. Seeding without sampling would therefore be strictly worse than the design it replaces. The two changes MUST ship together. ### 6.11 Verification levels Sidestep verification has two levels, reflecting a trade-off between verification cost and trust assumptions: **Level 1: Sampled opening verification, O(SIDESTEP_SAMPLES × h) per axis** A verifier checks that the claimed openings are consistent with the claimed root and that the sampled leaves are the correct seeded values: 1. Validate coordinates: source and destination are valid 256-bit Cyberspace coordinates. 2. Validate crossing geometry per §6.3: on every axis where movement occurs, `h = find_lca_height(v1, v2)`, the source touches the wall on its side (`base + half - 1` going up, `base + half` going down), and the destination is exactly 1 Gibson past it; axes without movement have `v1 == v2`. 3. Reconstruct `seed_prefix` from the event's `e previous` tag and `axis_byte` per §6.4. 4. Recompute the destination leaf hash `H_dest = SHA256(seed_prefix || int_to_bytes_be_min(v_dest))` and verify its path to the claimed root `M_axis`. 5. Derive the sample indices from `M_axis` per §6.10. For each, recompute the sampled leaf hash `SHA256(seed_prefix || int_to_bytes_be_min(base + idx_i))` from scratch and verify its path to `M_axis`. 6. Recompute `region_m`, temporal axis, and `proof_hash`. Compare against claimed value. Level 1 costs `SIDESTEP_SAMPLES + 1` leaf hashes and the same number of paths, seconds at any height. **Level 2: Full root verification, O(2^h) per axis** To fully verify that the claimed Merkle root was computed over the correct aligned subtree, a verifier recomputes the entire Merkle tree from scratch. This costs the same order of work as the original proof. **Security model:** The protocol does NOT require every verifier to perform Level 2. Security relies on **deterministic fraud detectability**: the Merkle root for an aligned subtree is a deterministic function of `(previous_event_id, axis_byte, base, h)`, every one of which is public on the event itself, so a fraudulent root remains permanently and objectively detectable by any party willing to do the work. Seeding changes who bears the audit cost, not whether fraud is detectable. Under v1 an auditor could compute a boundary's root once and check every crossing of it forever; under v2 each event must be audited on its own, which is why Level 1 was strengthened from a single destination path to sampled openings. **What sampling bounds (non-normative).** A tree over `2^h` leaves requires `2^h` leaf hashes and `2^h - 1` internal hashes, so the internal tree is an irreducible half of the honest work and no prover can avoid it. A prover who honestly computes a fraction `f` of the leaves and fabricates the rest passes Level 1 with probability `f^SIDESTEP_SAMPLES`, because the sampled positions are spread pseudorandomly over the whole subtree and each sampled leaf is recomputed by the verifier from the public seed. Grinding is not an escape: the sample indices are derived from the root, so steering them requires rebuilding the tree, and each rebuild costs at least the `2^h - 1` internal hashes, which is half the honest total. With `SIDESTEP_SAMPLES = 8`, passing half the time requires honestly computing about 92 percent of the leaves. Skipping the other 8 percent saves about 4 percent of the crossing's total work, since the internal tree is untouched, and buys a one-in-two chance of publishing permanently detectable fraud that invalidates the chain from that event forward. A prover willing to accept a one-in-a-thousand pass rate still has to compute 42 percent of the leaves, saving 29 percent. The security margin here comes primarily from the rebuild cost, not from the sample count, which is why 8 suffices where `decks/DECK-0001-hyperspace.md` §5.5 needs 32: a hyperspace leaf is expensive and its tree is cheap, and a sidestep is the reverse. In practice, Level 1 is for routine validation. Level 2 is for auditors, competitors, or automated fraud-detection services. **Small heights (non-normative).** Level 1 costs about `(SIDESTEP_SAMPLES + 1) × (h + 1)` hash operations and Level 2 costs `2^(h+1) - 1`, so the two cross over near h5. Below that, a verifier SHOULD simply perform Level 2, which is both cheaper and complete. Provers still publish the full opening set at every height, so that the `mp` encoding of §8.5 stays fixed-width. ### 6.12 Entering ≠ claiming (non-normative) The sidestep Merkle tree is built over SHA-256 hashes of leaf coordinates. The Cantor pairing tree over those same leaves produces a completely different value. Computing the Merkle root reveals **nothing** about the Cantor root. This preserves a critical separation: **sidestepping into a region does not yield the region's keys.** The region's Cantor root, and with it every location-based key inside the region (§7), still requires the full Cantor computation. A visitor who sidesteps through a wall has proven they spent the computational time to cross it, but they hold none of the keys to the space. You can walk into a building without having the keys. This is desirable: it creates a natural asymmetry between those who hold a region (who have invested in Cantor computation and keep its keys, §7.8) and visitors (who have done the minimum work to cross the boundary). Holding is a capability, not a title: anyone who does the work holds the same keys, and no one is excluded. There are no domains in the base protocol; `RATIONALE.md` §6 says what holding does and does not buy, and `docs/territory-conflict-game-layer.md` records the design decision. **A crossing cannot be sold; a root can (non-normative).** After §6.4 the two primitives are priced on opposite principles, deliberately. A sidestep is a **toll on an edge**. Its proof is seeded to one chain position, so it is worth nothing to anyone else. There is no market in crossings, no pioneer subsidising later travellers, and no way to buy a wall crossing except by paying for it yourself. A hop is a **canonical price on a cell**. Its work product is the region root of §4.6, whose hash is the region key of §7.2, and the one property that makes region keys work at all is canonical convergence (§4.9, property 4): two parties at the same place derive the same key without meeting. That property is what makes a key issuer-free, and it cannot survive seeding. A consequence is that an identity handed a region root can produce a hop across the corresponding boundary for the cost of the temporal axis alone, roughly 100 ms, at any height. This is not an oversight and it is not a road in the sense §6.4 forbids. A region root is disclosed only when a holder chooses to disclose it, one recipient at a time, and that disclosure is exactly the act the protocol is built to support: handing someone the key to a place. The v1 sidestep published the equivalent to the whole world automatically, as a side effect of one traveller moving, with nobody choosing anything. Movement is priced; disclosure is a social act. **How much a handed root is worth, measured (non-normative).** Less than it sounds, and the reason is worth stating because it decides what businesses are possible here (`docs/what-can-be-sold.md`). A hop is four stages: build three axis trees, combine them into `region_n`, compute the temporal axis `cantor_t`, then bind and hash. A recipient skips only the first. What remains is not cheap, because a root at height `h` is a number of `85 × 2^h` bits, and the combine multiplies numbers of that size: `region_n` is about four times an axis root and `hop_n` about eight times. The three pairings that remain are the three largest operations in the whole computation, and the millions of tiny pairings at the bottom of a tree, which are what the recipient was spared, are nearly free. Measured, building a tree and then combining: a recipient handed all three axis roots saves **about 19% of the work**, stable across heights (19.7% at h12, 19.2% at h14, 19.0% at h16), and less than that with faster multiplication. The remainder is `Θ(2^h)` and includes `cantor_t`, which is derived from the recipient's own `previous_event_id` (§5.3) and is therefore **per traveller**: it cannot be precomputed by a discloser, shared between recipients, or reused on a second hop. So a handed root is a real saving on a real cost, not a bypass. A recipient still needs hardware of the discloser's class to use one at all, since `region_n` at h47 is on the order of petabytes. **This does not close the market in computation, only the market in reusable answers.** Anyone may be paid to perform a traveller's own work for them, and §6.4's seeding is what keeps that market honest: because nothing can be resold, no operator can compute the world once and rent the archive forever. ### 6.13 Natural continents (non-normative) The combination of Cantor hops and Merkle sidesteps creates emergent geography in the coordinate space: - **h ≤ ~20:** Crossable by hop (Cantor) on consumer hardware in milliseconds to seconds. Above this the Merkle sidestep is faster at every height and is the practical movement primitive. - **h20–50:** Crossable by sidestep on consumer hardware (seconds to days on a GPU). - **h50–58:** Crossable by sidestep with a rented-GPU budget of roughly $20 to $3,000. - **h60+:** Not crossable by consumer computation. Crossable by ASIC-scale hash work (a 1 EH/s farm crosses h78 in about a week; a Bitcoin-scale fleet crosses h85 in about a day). Hyperspace (DECK-0001) is the consumer route. These prices are per traveller and per crossing. Because §6.4 seeds every sidestep tree to its mover's chain position, a boundary that costs three GPU hours costs three GPU hours for the first identity to cross it and three GPU hours for the ten thousandth. The continents below are therefore a description of the space as every agent actually experiences it, rather than a description of what the first arrival paid. Nobody designed these continents. They emerge from the interaction between the Cantor pairing function, SHA-256, and the physical limits of computation and storage. Different agents experience different continental boundaries depending on their hardware and patience. There is no single universal map of "passable" and "impassable" walls. No arbitrary ceiling is designed. The boundary emerges from thermodynamics. ### 6.14 Performance expectations (non-normative) Sidestep cost is dominated by SHA-256 leaf hashing and is fixed-size per leaf. Hop cost grows faster than 2× per height because the Cantor operands double in size as well as in count. Measured in the reference implementation (pure Python, same language for both): h14 hop 12 ms vs sidestep 27 ms; h16 100 ms vs 100 ms; h18 720 ms vs 270 ms; h20 6.6 s vs 1.1 s. Above roughly h16 the sidestep wins and its advantage widens by about 1.5× per height; a compiled bignum library is 5 to 10× faster for hops, and SHA-NI or a GPU is 50 to 10,000× faster for sidesteps, so the ordering only sharpens in optimized code. | LCA Height | Hop (Cantor) | Sidestep (1 CPU core, 10⁸ H/s) | Sidestep (1 GPU, 2×10¹⁰ H/s) | Notes | |---:|---:|---:|---:|---| | h14 | ~10 ms | 0.3 ms | 1.5 μs | either | | h20 | ~6 s | 21 ms | 0.1 ms | sidestep | | h30 | ~days (11 GB root) | 21 s | 0.1 s | sidestep | | h34 | ~a day (182 GB root) | 6 min | 1.6 s | sidestep; hop only when the region root itself is wanted | | h40 | not feasible (12 TB) | 6 h | 100 s | sidestep | | h50 | not feasible (12 PB) | 261 d | 28 h | sidestep | | h55 | not feasible | 23 y | 38 d | sidestep, ~$700 of rented GPU | | h60 | not feasible (12 EB) | 731 y | 3.3 y | ASIC-class hash work | | h85 | not feasible | 2.5×10¹⁰ y | 1.1×10⁸ y | Bitcoin-scale ASIC fleet: about a day | The practical consumer sidestep ceiling is about h55 per axis for a thousand dollars of rented GPU time. Sidestep work is plain SHA-256 over short preimages and is therefore subject to ASIC acceleration; the storage bound of §13.2 applies to Cantor roots, not to travel. Beyond consumer reach, hyperspace (DECK-0001) is the route. Every figure in this table is now what each traveller pays on each crossing, and none of it can be prepared in advance (§6.7). The openings of §6.10 cost `(SIDESTEP_SAMPLES + 1) × h × 32` bytes per non-trivial axis, which the `mp` tag carries as hex and so doubles: about 11 KB of tag text at h20, 23 KB at h40, and 26 KB at h47 for a single-axis crossing. A three-axis crossing at h47 approaches 80 KB and at h55 approaches 93 KB. Implementations SHOULD confirm their relays' event size limits before attempting multi-axis crossings above h40. ### 6.15 Version 2 of the sidestep construction (normative) The seeding of §6.4, the axis separation of §6.4, and the sampled openings of §6.10 are a breaking change to sidestep verification. They were adopted together because seeding without sampling is strictly worse than the construction it replaces (§6.10). - `SIDESTEP_DOMAIN` is bumped from `CYBERSPACE_SIDESTEP_V1` to `CYBERSPACE_SIDESTEP_V2`, so no v1 proof can be mistaken for a v2 proof or collide with one. - Verifiers implementing this revision MUST reject sidestep events built under the v1 construction. There is no grace period and no dual-acceptance mode: accepting v1 proofs would keep the pioneer-priced path open, which is the entire defect being repaired. - Chains containing a v1 sidestep are invalidated from that event forward and must respawn. At adoption this affected three events published by two identities, all single-axis crossings at h19 and h20, which is why no migration path was specified. Implementations of the base protocol prior to this revision computed leaves as `SHA256(b"CYBERSPACE_SIDESTEP_V1" || leaf_bytes)` with no seed and no axis separation, and published a single destination inclusion path. Those proofs are recognisable by their `mp` tag carrying exactly one path per axis rather than `SIDESTEP_SAMPLES + 1`. **Porting hazard: which leaf the path covers.** v1 implementations diverged on this and the spec was not the tie-breaker it should have been. §6.10 and §8.7.2 have always specified the **destination** leaf, and that is what the two TypeScript ports and every sidestep so far published to a public relay actually use. The Python reference implementation named in §14 instead collected the path for leaf 0, the base of the aligned subtree, so its inclusion verifier rejects conforming events and accepts its own. Because the choice never entered `region_m`, it never affected `proof_hash`, which is why the divergence survived: the events verified at the proof level while their `mp` tags were mutually unreadable. Implementations porting to v2 MUST use destination-leaf semantics for the first opening. The sampled openings make any remaining divergence self-correcting, since a verifier built on the wrong convention fails the sampled paths as well as the destination path, rather than silently ignoring both. --- ## 7. Location-Based Encryption and Discovery ### 7.1 The purpose: chalk on the sidewalk (non-normative) The purpose of location-based encryption is not primarily secrecy. There are better cryptographic systems for secure communication. The purpose is to **model traversable reality**. Consider a message written in chalk on a sidewalk: - It is not "encrypted" in any technical sense - Anyone who walks by can read it - But you cannot read it without walking there - Even if someone tells you about it, they had to walk there to know This is location-gated access that requires no keys, no permissions, no infrastructure. Only presence. Cyberspace implements this using region-derived keys. A ciphertext can be published publicly on Nostr, but deriving the decryption key requires computing the region preimage, which is the Cantor root for that spatial region. The work required is the same whether you traveled there via a movement chain or computed the region directly for an arbitrary coordinate. There is no free surveillance. Looking and walking cost the same. ### 7.2 Key derivation (normative) Given a spatial region integer `region_n` (the 3D region identifier from §4.7 for some aligned region): - `region_bytes = int_to_bytes_be_min(region_n)` - `location_decryption_key = sha256(region_bytes)` - `lookup_id = sha256(location_decryption_key)` **Why two layers:** Seeing `lookup_id` (which is published to help people find the content) does not allow deriving `location_decryption_key` without the region preimage. The lookup ID is safe to publish; the decryption key requires work. Note (non-normative): the temporal axis used for hop proofs (§5) is intentionally *not* included here. Location-based identifiers and keys remain a stable function of spatial regions. They do not change when someone moves through. Outputs are 32-byte digests. When used in Nostr tags, they MUST be lowercase hex. ### 7.3 Discovery radius (non-normative) Cantor subtree roots represent **aligned regions**. Choosing a subtree height implicitly chooses a discovery radius: how large an area the encrypted content is visible from. At the Cantor Height 34 scale (2 meters per height-34 subtree), approximate physical scales are: | Height | Leaves (per axis) | Physical scale | Metaphor | |---:|---:|---|---| | 0 | 1 | ~10⁻¹⁰ m | Atomic precision | | 10 | 1,024 | ~0.1 μm | Microscopic | | 20 | ~10⁶ | ~0.1 mm | Grain of sand | | 30 | ~10⁹ | ~0.1 m | Hand-span | | 34 | ~1.7×10¹⁰ | 2 m | Human height (canonical) | | 40 | ~10¹² | 128 m | City block | | 50 | ~10¹⁵ | 131 km | City region | A local secret at height 34 is discoverable within ~2 meters. At height 50, from anywhere in a city. **Important caveat:** Discovery requires *equivalent computation* to secret creation. Typical scanning range is between height 0 and 16 for sub-second continuous scanning on average consumer hardware. Secrets at larger regions may be unattainable without some hint to help users scan up to their height. §7.7 defines hints: the hider MAY publish the aligned box a bag lies in, one height per axis, so a seeker sweeps the box instead of the space. As hardware improves, passive scanning range will grow and larger secret regions will become attainable. The discovery radius grows exponentially with height, enabling a natural hierarchy of public, neighborhood, and intimate spatial messages. ### 7.4 Discovery scanning (recommended) At a coordinate `(x,y,z)` you may scan nearby region keys by selecting heights `h = 1..H` and computing the aligned subtree base per axis: - `bx = (x >> h) << h` - `by = (y >> h) << h` - `bz = (z >> h) << h` For each `h`, compute per-axis subtree Cantor roots: - `rx = compute_subtree_cantor(bx, h)` - `ry = compute_subtree_cantor(by, h)` - `rz = compute_subtree_cantor(bz, h)` Combine to a 3D region integer: - `region_n = π(π(rx, ry), rz)` Then derive `lookup_id` per §7.2. Implementations SHOULD cap `H` for interactive use and may cache values; higher subtrees change less frequently. ### 7.5 Caching optimization (non-normative) When moving, many higher subtrees do not change between positions. This provides a significant optimization for applications that scan multiple discovery radii. **Boundary crossing principle:** An aligned subtree of height `h` changes only if you cross a boundary at that height. **Example (1D):** Moving from position 7 to position 8: ``` Height Base (at pos 7) Base (at pos 8) Changed? ------- ---------------- ---------------- -------- 0 7 8 YES (boundary at 8) 1 6 8 YES (boundary at 8) 2 4 8 YES (boundary at 8) 3 0 8 YES (boundary at 8) 4 0 0 NO (same [0..15]) 5 0 0 NO (same [0..31]) ... ... ... NO ``` Heights 4 and above are unchanged. Position 7 and 8 share the same `base` for h≥4. **Boundary detection:** ```python def subtree_changes(v1: int, v2: int, h: int) -> bool: return (v1 >> h) != (v2 >> h) ``` When `(v1 >> h) == (v2 >> h)`, both positions lie in the same aligned subtree, so the cached region key remains valid. **Implementation strategy:** ```python region_cache = {} # height -> (base, region_key) def get_region_key(x, y, z, h): bx = (x >> h) << h by = (y >> h) << h bz = (z >> h) << h if h in region_cache: cached_base, cached_key = region_cache[h] if (bx, by, bz) == cached_base: return cached_key # Cache hit # Cache miss: compute and store region_key = compute_region_key(bx, by, bz, h) region_cache[h] = ((bx, by, bz), region_key) return region_key ``` **Performance implications:** - Small moves: most heights remain cached; only recompute low heights - Large moves: more heights change, but high heights often remain the same - Continuous scanning: avoid recomputing all 50+ region keys on every position update Note: this caching optimization applies to spatial region computations for discovery. Hop proofs still require computing the temporal axis root per §5 on every hop. ### 7.6 The bag (normative) Content hidden at a place is published as a **bag**. A bag is one Nostr event of kind 33330 (event format in §8.6) whose payload is encrypted with the key of one region at one height (§7.2), and it holds everything its author has hidden in that region at that height. Anyone can fetch a bag from a relay, because the ciphertext is public. Only someone who has computed the region's key can open it, whether they computed it by moving into the region or by deriving it for the coordinate directly (§7.1). The bag is addressable by its `lookup_id`, so a relay keeps only the newest bag per author and region, and the author changes what is hidden there by publishing a newer bag. **Height (normative):** the region's height MUST be at least 1. A height-0 region is a single Gibson, and height 0 is the one case in which a root does not identify its height (§4.6); discovery scanning starts at height 1 (§7.4). A bag whose `h` tag (§8.6) is `0` MUST be rejected. **Cipher (normative):** - key: the `location_decryption_key` of §7.2 (32 bytes) - cipher: AES-256-GCM with a 12-byte nonce and a 16-byte tag, no additional authenticated data - the nonce MUST be fresh for every encryption - `payload = nonce || ciphertext || tag`, encoded as base64 (standard alphabet, padded) A reader without the region key cannot decrypt the payload. An attempt with the wrong key fails at the GCM tag check and reveals nothing about the plaintext. A failed decryption therefore means only that the reader does not hold this region's key; it MUST NOT be treated as an error in the bag. **Plaintext (normative):** The plaintext is arbitrary bytes. The protocol places no requirement on it beyond the two shapes below, which tell a reader how to interpret what it has decrypted: - A **list of entries**: a JSON array. Each element is either an **item**, a JSON object that is a nostr event carried inline, signed or unsigned, or a **reference**, a JSON array that is a tag naming an event published elsewhere. The JSON type tells them apart. Readers MUST try this shape first, because a list of entries is the shape clients render entry by entry. An element that is neither an event nor a reference is skipped. - **Opaque**: anything that is not a list of entries, such as a text note or a file. Its interpretation is application-defined; a client may show it as text or offer it as a download. **Items (normative):** - An item is a nostr event. It MAY be signed. If it carries a `sig`, its `id` MUST be the canonical id (§8.2) and the signature MUST verify; a reader MUST drop an item that fails either check, and only that item, because one corrupt or forged item says nothing about the others. An item without a `sig` is allowed, because some content is deliberately left unsigned; its `pubkey` is then a claim, and readers MUST NOT present it as verified. - The bag's `pubkey` is the key that placed the items in the region. Readers MUST attribute placement to the bag's author, and MUST attribute authorship of an item's content to the item's `pubkey` only when the item is signed. A signed item written by one key and hidden by another is therefore shown as that author's content, placed here by the hider. - An item MAY carry a `C` tag: `["C", ""]`, its exact coordinate (§2), which lets a client render it at a point rather than somewhere in the region. If present, the coordinate MUST lie inside the region the bag is encrypted to: the same plane, and equal to the region's base above height `h`. Readers MAY drop an item whose `C` lies outside, because such an item claims a place its key does not cover. Without a `C` tag, an item is located no more precisely than the region. - A reader that does not understand an item's `kind` skips it and renders the rest. **References (normative):** A reference hides an event that is published on its own, so that the bag stays small and the referenced event has an id and an address of its own. It is one of two tags, in the shape NIP-01 gives them, with the entry's exact coordinate as an optional fourth element: - `["a", "::", "", ""]` names the event by its coordinate: its own kind, its author and its `d` tag. A reader fetches the newest event at that coordinate, so the reference follows its author's edits. - `["e", "", "", ""]` names one event by its id. It is for events of a regular kind, which relays keep. It does not pin a version of an addressable event: NIP-01 lets a relay keep only the newest event at an address and discard the one it replaces, so an `e` reference to an addressable event stops resolving once its author publishes over it. Name an addressable event by its coordinate. The rules: - The referenced event MAY be of any kind. Its kind is the kind in the `a` coordinate, or the kind of the event the `e` id names; nothing in the bag restates it. - The relay hint MAY be the empty string. `` is the entry's exact coordinate and follows the rules of the `C` tag above: it MUST lie inside the bag's region, and a reader MAY drop a reference whose coordinate lies outside. It MAY be omitted, in which case the entry is located no more precisely than the region. - The referenced event is a **partially encrypted event** in the shape of Fanfares' NIP FF-1: its `content` is a public **preview** for readers who cannot open it, and it carries exactly one `["encrypted", "aes-256-gcm", "", "cyberspace:region"]` tag. The payload is the event's hidden content, encrypted with this bag's region key under the cipher above. The fourth element, `cyberspace:region`, stands where FF-1 puts a key service URL and says that the key is derived from a place rather than issued: a reader MUST NOT make a network request for it. The event carries a `d` tag, as FF-1 gives every partially encrypted event whatever its kind, which is what gives every referenced event a coordinate. - The referenced event MUST carry nothing that says where it is: no `C`, no `h`, no hint and no sector tag. Those belong to the bag, which is what the region key protects. The event is a locked box in plain view; the bag is the note that says where the box stands. - Whoever opened the bag opens every event it references, with the key already in hand, and a reader that has not opened the bag can open none of them. What the decrypted payload means is defined by the referenced event's kind; DECK-0003 §3.2 and §3.4 define it for objects. - The referenced event is published, so it is signed like any other event, and a reader MUST verify it as usual. Placement is attributed to the bag's author, and authorship of the referenced content to the referenced event's `pubkey`, which MAY differ from the bag's: placing another author's event is a placement. - A reference that cannot be fetched, or whose payload does not open with this bag's key, is a missing entry. A reader drops it and SHOULD say that something was found but could not be retrieved, rather than that nothing was found. **Why a reference is a tag and not an item (non-normative).** An item that only pointed elsewhere would repeat what the reference already says: its kind is in the coordinate, its author and time are the bag's, and its content would be empty. What must survive is the pointer and the exact coordinate, and a tag carries both. Measured on an object reference, the item form is 304 bytes of JSON and the tag form 162, which matters because size is the reason references exist. NIP-51 lists already encrypt their private entries as a JSON array of tags, so the shape is familiar. Note (non-normative): kinds in use. ONOSENDAI hides two kinds: `3330`, a shard (geometry in `content`, the kind carried over from v1), and `1`, a message (text in `content`). Both carry a `C` tag. New kinds need no change to this section: the container is the same, and a client that does not know a kind skips it. A large object is hidden by reference instead, as its own `kind 33331` event (DECK-0003 §3.2). **Why one bag per region (non-normative):** `d` is the lookup id, so there is exactly one bag per author, region and height, and it is addressable. A region accumulates content by rewriting its bag: the author decrypts the current bag, adds or removes items, and publishes the whole list again. This costs one event per change instead of one event per item, and it keeps the relay ignorant: a relay sees one ciphertext per region and never learns how many items the bag holds or what kinds they are. **Lifecycle (normative):** - To add or remove an entry, the author republishes the bag with the new list and a `created_at` strictly greater than the previous bag's, because relays keep the newest addressable event and readers MUST do the same. - To remove the last entry, the author publishes a NIP-09 deletion (`kind = 5`) with `["e", ""]` and `["k", "33330"]`. An empty bag would still occupy the region's slot on the relay, so a bag holding a list of zero entries MUST NOT be published. Note (non-normative): the reference CLI's `encrypt` writes opaque plaintext (a text or a file); ONOSENDAI writes a list of entries. Both conform to this section, and each can open what the other publishes. ### 7.7 Hints (optional) A bag is a cache of data encrypted by a region key at a height (§7.6) and it reveals nothing about where it is. Its `lookup_id` is a hash of a hash (§7.2), and discovery scanning (§7.4) reaches only a few heights around the scanner. Without more information, a bag is found only by intentionally deep scanning and/or wandering. With no additional information, any given bag is equally likely to be at any point in the full 2^256 coordinate space: an impossibly hardened secret. A **hint** is the hider's clue as to where the bag is. It is optional, public, and as coarse as the hider wants: the aligned box the bag's region lies in, one height per axis, on one plane. The hint box is scalable along each axis; the bag is somewhere within the box. A seeker simply sweeps the box to find the secret, and the hider scales the hint to achieve the desired difficulty. **The hinted box (normative):** - `Hx`, `Hy`, `Hz`: integers in `[0, 85]`, the hint heights - `bx = (x >> Hx) << Hx`, `by = (y >> Hy) << Hy`, `bz = (z >> Hz) << Hz`: the aligned bases - the box is `[bx, bx + 2^Hx) × [by, by + 2^Hy) × [bz, bz + 2^Hz)` on plane `P` A height of `85` leaves an axis open: the base is `0`, the box spans the whole axis, and the hint says nothing about that coordinate. Equal heights make a cube. Unequal heights make a slab or a column; an exact axis (height equal to the bag's) with two coarse axes turns the search into two dimensions. Heights of `30` on all three axes name exactly one sector (§10). **The `hint` tag (normative):** `["hint", "", "", "", ""]` - `coord_hex`: the 256-bit coordinate of the box's base `(bx, by, bz, P)`, encoded per §2.2, 32-byte lowercase hex. It MUST be the aligned base: the low `H` bits of each axis MUST be zero, and an axis with `H = 85` MUST be `0`. Requiring the base means every hider who hints the same box publishes the same tag, so readers can compare hints by equality. - `Hx`, `Hy`, `Hz`: decimal strings, no sign, no leading zeros except `"0"` (the rules of §10). - A bag MUST carry at most one `hint` tag. **What a hint claims (normative):** The bag's region (the aligned cube of height `h`, where `h` is the `h` tag of §8.6) lies inside the box, on plane `P`. Because both the region and the box are aligned, containment is two checks per axis: `H >= h`, and the region's base equals the box's base after both are shifted right by `H`. When the bag carries an `h` tag, each hint height MUST therefore be at least `h`; a box smaller than the region could not contain it. Three heights equal to `h` name the region itself: the hint is then a destination the seeker can compute or walk to directly, not a search. **Sector tags (normative):** A hint fixes the sector on every axis with `H <= 30`, because the sector index is the axis value shifted right by 30 (§10) and the hint fixes every bit above `H`. For each such axis the bag MUST carry that axis's sector tag (`X`, `Y` or `Z`), computed from the box's base. When all three are fixed, the bag MUST carry `S`. An axis with `H > 30` gets no sector tag, because its sector is not determined. A bag without a hint MUST NOT carry sector tags, because on a bag they are derived from the hint and would otherwise leak a location the hider did not choose to publish. **Malformed hints (normative):** A `hint` tag that breaks any rule above MUST be treated as absent, meaning the bag is read as if it carried no hint: wrong arity, bad hex, a base that is not aligned, a height outside `[0, 85]`, a non-canonical integer, or a height below `h`. Sector tags that disagree with the hint MUST be ignored. A bad hint never invalidates the bag, because hints are advisory metadata about where to look; whether a bag is valid is decided by §7.2 and §7.6 alone. **Why the hint is a knob (non-normative):** A seeker who trusts a hint sweeps the box: for every candidate region of height `h` inside it, derive the region key (§7.2), compute its `lookup_id`, and check the relay for a bag with that `d` tag (one batched query can carry many lookup ids). The number of candidates is `2^((Hx - h) + (Hy - h) + (Hz - h))`, the product of the choices on each axis. The seeker's own position never enters this cost, because §7.1 makes looking and walking equivalent: a region key can be computed for any coordinate without traveling there. The gap between hint height and bag height, summed over the three axes, is therefore the price of the search, and the hider sets it when publishing the hint. | Total gap | Candidates | Single core, bag at height 8 or below | |---:|---:|---| | 0 | 1 | a destination | | 12 | 4,096 | seconds | | 18 | 262,144 | minutes | | 24 | 16.7 million | hours | | 30 or more | a billion or more | days to never | The times in the table assume the key cost measured on 2026-09-01 on one desktop core: about 0.05 ms per key at heights 0 to 4, 1.3 ms at height 8, 30 ms at height 12 and 816 ms at height 16. Because a key at a greater height costs more to derive, the same gap takes longer for a bag hidden at a greater height. A sector-only hint (`H = 30` on every axis) on a bag at height 5 is a gap of 75, about 2^75 candidates, which no one will sweep; such a hint tells a seeker where to travel, not where to search. **Reading is not reaching (non-normative):** Anyone who does the work of deriving the key can read the bag from anywhere; reading has no distance term. Being inside the region is a separate fact: it costs the movement work of §4 to §6 to get there, and only a movement chain whose head lies inside the region proves it (§8). An application that rewards finding a bag has to say which of the two it rewards, reading or reaching. How a find is proven in public is left to applications and DECKs (§8.9). **A hint is a claim (non-normative):** Nothing in the protocol verifies a hint until the bag is found and its region is compared with the box. A false hint wastes the seeker's work, and the protocol does not punish it. Applications should weigh a hint by the reputation of the key that published it, as they would any other unverified claim. **Riddles (non-normative):** The bag's `content` field MAY hold a plaintext hint written for humans, a riddle (§8.6). A riddle can stand alone or accompany a geometric hint. The protocol does not read it; it is for the seeker to interpret. **Golden vectors:** Produced and checked by `hint-reference.py`. Points are 256-bit coordinates per §2.2 (32 bytes of hex). `london` is the §9.8 golden vector. | Vector | Point (plane) | Bag `h` | Heights | `hint` coordinate | Sector tags | Candidates | |---|---|---:|---|---|---|---:| | `london_h5_box11` | `london` (0) | 5 | 11, 11, 11 | `c492492492492492492492edf5bee7267451c787d95ba4d7840c76d000000000` | `X` `18014398541305938`, `Y` `18014398549232983`, `Z` `18014398509410999`, `S` `18014398541305938-18014398549232983-18014398509410999` | 2^18 | | `london_h5_x_exact` | `london` (0) | 5 | 5, 14, 14 | `c492492492492492492492edf5bee7267451c787d95ba4d7840c749041240000` | same as above | 2^18 | | `ideaspace_h8_y_open` | `a4b64924924924924924924924924924924924924924924924924d84b60d9c8f` (1) | 8 | 12, 40, 12 | `a4b64924924924924924924924924924924924924924924924924d8000000001` | `X` `18014398509481984`, `Z` `36028797018963967`, no `Y`, no `S` | 2^40 | The ideaspace point is `x = 2^84 + 12345`, `y = 3 · 2^80 + 777`, `z = 2^85 - 1 - 4242` on plane 1. The second vector is a two-dimensional hunt: X is exact, so the seeker sweeps a 2^9 by 2^9 slab of height-5 regions. The third has an open axis: `Hy = 40` leaves the Y sector undetermined, so the bag carries `X` and `Z` but neither `Y` nor `S`, and the sweep is 2^40 candidates, far beyond any search; that hint tells the seeker where to travel. ### 7.8 Holding a region (non-normative) Computing a region's Cantor root produces every intermediate node of the per-axis trees on the way up, and every intermediate node is itself the root of an aligned sub-region (§4.5). Whoever keeps those nodes on disk **holds** the region: the per-axis roots of every aligned sub-region inside it, at every height, are on hand, so any location-based key inside the holding costs one pairing step (§4.7, §7.2) instead of a tree. Holding is §7.5 carried to its limit. What holding buys is latency, and only latency: - reading content published anywhere in the region at any height without per-item work, where a passerby's interactive scan reaches roughly h16 (§7.3); - writing at any height in the region without rebuilding a tree; - deriving a two-factor key, `KDF(location_decryption_key || owner_secret)`, for content that must be both found by presence and unlocked by the holder. **"Without rebuilding a tree" is not "at once", and earlier drafts of this section said "at once" (non-normative).** A holder skips the tree build and still pays the combine, which operates on numbers of `85 × 2^h` bits and is measured at 0.76 to 1.03 times a full axis build. Holding therefore buys about **4.9 times**, roughly two heights, not an instant answer: a held key at h34 is still on the order of hours on a desktop. That is a large and real advantage over not holding, and it is a latency advantage rather than a capability one, exactly as the rest of this section says. §6.12 works the same arithmetic from the other side, and `docs/what-can-be-sold.md` works through what both mean for anyone building on this. What holding does not buy: exclusivity (anyone who does the work holds identical keys), any advantage in observing chains (they are public to everyone), or any effect on anyone else. It is a keyring, not a claim. What holding costs is disk. Each level of a per-axis tree is about as large as its root (§9.9), so the full trees for three axes are about `3 × (h + 1)` roots: roughly 19 TB at h34, 1.5 PB at h40, 107 PB at h46. Keeping only the nested cubes that contain one position costs about two roots per axis, roughly 1.1 TB at h34. Stop paying and the keys are a recomputation away again; nothing else happens. There are no domains in the base protocol. Holding is the whole of what the protocol offers toward territory. Claims, exclusion and governance are left to applications and games; see `RATIONALE.md` §6 and `docs/territory-conflict-game-layer.md`. --- ## 8. Nostr Integration: The Movement Chain Cyberspace uses **Nostr** as its transmission layer. Movement is represented as a per-pubkey, linear hash chain of signed Nostr events. This means Cyberspace does not require new network infrastructure. It composes on top of existing relays. The state of Cyberspace is the sum of all cyberspace-related Nostr events. Because Nostr is decentralized and permissionless, knowledge of global state is not possible, just as in physical reality. ### 8.1 Event kind - Movement events: `kind = 3333` ### 8.2 Canonical event id (NIP-01) The event `id` MUST be computed as NIP-01 canonical serialization: - Serialize: `[0, pubkey, created_at, kind, tags, content]` - Encode as UTF-8 JSON with no whitespace (equivalent to Python `json.dumps(..., separators=(",", ":"), ensure_ascii=False)`) - Hash: `sha256(serialized_bytes)` **Signature (`sig`):** For published events, `sig` MUST be a valid Schnorr signature over the event `id` as required by NIP-01. Note: some prototypes may leave `sig` blank for local-only chains and sign at publish-time; that is not a wire-format requirement. ### 8.3 Spawn event (first event) The spawn event declares "I exist at this coordinate." It is the first event in a keypair's movement chain. Required tags: - `A` tag: `["A", "spawn"]` - `C` tag: `["C", ""]` - `coord_hex` MUST be a 32-byte lowercase hex string (64 hex chars, no `0x` prefix) - For spawn events, `coord_hex` MUST equal the event `pubkey` (spawn coordinate) - Sector tags: `X`, `Y`, `Z`, `S` (per §10) ### 8.4 Hop event A hop event extends the movement chain by one Cantor pairing tree proof. Required tags: - `A` tag: `["A", "hop"]` - `e` genesis: `["e", "", "", "genesis"]` - `e` previous: `["e", "", "", "previous"]` - `c` tag: `["c", ""]` (32-byte lowercase hex string) - `C` tag: `["C", ""]` (32-byte lowercase hex string) - `proof` tag: `["proof", ""]` (32-byte lowercase hex string) - Sector tags: `X`, `Y`, `Z`, `S` (per §10) ### 8.5 Sidestep event A sidestep event extends the movement chain by one Merkle hash tree boundary crossing. Required tags: - `A` tag: `["A", "sidestep"]` - `e` genesis: `["e", "", "", "genesis"]` - `e` previous: `["e", "", "", "previous"]` - `c` tag: `["c", ""]` (32-byte lowercase hex string) - `C` tag: `["C", ""]` (32-byte lowercase hex string) - `proof` tag: `["proof", ""]` (32-byte lowercase hex string) - `mr` tag: `["mr", "::"]` (per-axis Merkle roots, colon-separated, each 64 hex chars) - `mp` tag: `["mp", "::"]` (per-axis openings, colon-separated) - `hx` tag: `["hx", ""]` (LCA height on X axis, decimal string) - `hy` tag: `["hy", ""]` (LCA height on Y axis, decimal string) - `hz` tag: `["hz", ""]` (LCA height on Z axis, decimal string) - Sector tags: `X`, `Y`, `Z`, `S` (per §10) **Openings encoding:** Each per-axis segment in the `mp` tag is the concatenation of `SIDESTEP_SAMPLES + 1` inclusion proofs in the order defined by §6.10 (destination first, then samples in ascending `i`). Each proof is `h` sibling hashes from leaf to root, hex-encoded, so an axis with LCA height `h` contributes exactly `64 × h × (SIDESTEP_SAMPLES + 1)` hex characters. For trivial axes (`h = 0`), the segment is an empty string between colons. Because the segment is fixed-width given `h`, a verifier reads the per-axis `hx`, `hy`, `hz` tags and splits the segment without ambiguity. A segment whose length is not an exact multiple of `64 × h` is malformed and the event MUST be rejected; a segment of exactly `64 × h` characters is a v1 proof and MUST be rejected per §6.15. **Height tags:** The `hx`, `hy`, `hz` tags enable verifiers to determine expected proof lengths without re-deriving LCA heights from coordinates. ### 8.6 Encrypted content event (bag) A bag publishes content encrypted to one region at one height (§7.6). Its cipher, plaintext, items and lifecycle are defined in §7.6; this section gives the event format, as §8.3 to §8.5 do for movement. - Encrypted content events: `kind = 33330` - `kind 33330` is addressable: relays keep the newest event per `(pubkey, kind, d)` Required tags: - `d` tag: `["d", ""]` (32-byte lowercase hex string, the `lookup_id` of §7.2) - `encrypted` tag: `["encrypted", "aes-256-gcm", ""]` (the payload of §7.6) - `version` tag: `["version", "2"]` - `version` names the rules of §7.6. A reader MUST ignore a bag whose version it does not know. Optional tags: - `h` tag: `["h", ""]` (decimal string): the height of the region whose key encrypts the content, which is the discovery radius of §7.3; at least 1 (§7.6) - `hint` tag: `["hint", "", "", "", ""]`: the hider's coarse statement of where the bag can be found (§7.7) - Sector tags `X`, `Y`, `Z`, `S`: required on a bag that carries a `hint` tag, for each axis whose hint height is at most 30 (§7.7, §10); MUST NOT appear otherwise Content: MAY carry plaintext meant for humans, a riddle (§7.7). The protocol does not interpret it. ### 8.7 Verification summary #### 8.7.1 Hop verification To verify a hop: 1. Parse previous and current coords; decode to `(x1,y1,z1,plane)` and `(x2,y2,z2,plane)`. 2. Plane changes are valid in v2; verifiers MUST support hops where `plane1 != plane2`. 3. Compute the stable spatial region integer `region_n` per §4.7. 4. Derive the terrain-based temporal height `K` from the destination coordinate `(x2,y2,z2,plane2)` per §5.2 (including the destination plane bit). 5. Compute the temporal axis root `cantor_t` from the hop event's `previous_event_id` (`e` tag with marker `previous`) and `K` per §5.3. 6. Compute `hop_n = π(region_n, cantor_t)` per §5.4. 7. Compute `proof_hash` per §5.6. 8. Accept iff it matches the event's `proof` tag. #### 8.7.2 Sidestep verification (Level 1: sampled openings) To verify a sidestep (Level 1, sampled openings check): 1. Parse previous and current coords; decode to `(x1,y1,z1,plane)` and `(x2,y2,z2,plane)`. 2. Validate crossing geometry: for each axis, confirm the destination is exactly 1 Gibson past the LCA boundary (§6.3). Verify the `hx`, `hy`, `hz` tags match the computed LCA heights. 3. Parse per-axis Merkle roots from the `mr` tag. Read `previous_event_id` from the `e` tag with marker `previous`. 4. For each axis where movement occurs: a. Build `seed_prefix = SIDESTEP_DOMAIN || previous_event_id || axis_byte || SEED_PAD` per §6.4, and the aligned base `base = (v1 >> h) << h`. b. Split the axis segment of the `mp` tag into `SIDESTEP_SAMPLES + 1` proofs of `h` siblings each; reject if the length does not match (§8.5). c. Compute the destination leaf hash `H_dest = SHA256(seed_prefix || int_to_bytes_be_min(v_dest))` and verify its path to the claimed root `M_axis`. d. Derive the sample indices from `M_axis` per §6.10. For each `idx_i`, compute `SHA256(seed_prefix || int_to_bytes_be_min(base + idx_i))` and verify its path to `M_axis`. 5. Compute `region_m = π(π(mx, my), mz)` from the claimed Merkle roots (§6.6). 6. Derive `K` and `cantor_t` from destination coordinate and `previous_event_id` (§6.7, same as hop). 7. Compute `sidestep_n = π(region_m, cantor_t)` and `proof_hash` per §6.8. 8. Accept iff it matches the event's `proof` tag. A verifier that skips step 4d is performing a strictly weaker check than v1's, not an equivalent one, because after seeding there is no canonical root to compare `M_axis` against (§6.10). Level 2 (full root) verification is described in §6.11. ### 8.8 Core action types summary The base Cyberspace v2 protocol defines three movement action types: | `A` tag value | Description | Proof type | Defined in | |---|---|---|---| | `spawn` | Identity placement at pubkey-derived coordinate | None (identity proof) | §8.3 | | `hop` | Movement via Cantor pairing tree | Cantor root (§4.6) | §8.4 | | `sidestep` | Boundary crossing via Merkle hash tree | Merkle root (§6.4) | §8.5 | All three use event `kind = 3333`. ### 8.9 Protocol extensions (DECKs) This specification defines the base Cyberspace v2 protocol. Optional extensions MAY introduce new event kinds, new movement action types (`A` tag values), and/or additional validation rules that are only applied when an extension is in use. Extensions are specified as **Design Extension and Compatibility Kits (DECKs)** in the `decks/` directory. - Hyperspace extension (DECK-0001): `decks/DECK-0001-hyperspace.md` ### 8.10 Avatar event An avatar is the shape an identity is drawn as. It is the one thing in cyberspace that lands on other people's screens whether they asked for it or not: everyone near its owner sees it, at every zoom. Its size and its detail are therefore paid for in work, on the event that publishes it, and a client draws nothing it cannot verify has paid. - Avatar events: `kind = 11333` - `kind 11333` is replaceable: relays keep the newest event per `(pubkey, kind)`, so an identity has exactly one avatar and the newest replaces it An avatar is one per identity, which is what a replaceable event is for. This was `kind 33331`, addressable, with a `d` tag fixed at `"avatar"`: a constant `d` is emulating replaceable semantics with the wrong tool, and it asks every reader to trust a convention where the relay could enforce the rule. Everything in nostr that is one per person is replaceable, including metadata, contacts, relay lists and DM relay lists; addressable is for the other case, many per author told apart by `d`, as long-form articles and wiki pages and classified listings are. The change is a clean break rather than a migration. At the time it was made, a query for `kind 33331` across twelve relays, including the one this protocol uses, returned nothing, while the same query returned bags and movement events. An identity that had adopted an avatar without publishing it re-mines once, which is the work it paid the first time. `11333` was chosen by querying for it rather than by reading a registry. The registry of kinds and the NIPs index both list only what has been written down, and the replaceable range is full of numbers that are in daily use and appear in neither: `10333` carries Podcasting 2.0 favourites, `10086` a relay indexer's lists across eighty pubkeys, `10088` something Amethyst writes. A kind is free when the relays say so. Required tags: - `nonce` tag: `["nonce", "", ""]` per NIP-13, where `target` is the leading zero bits the publisher committed to before mining Optional tags: - `name` tag: `["name", ""]`, the shape's name for humans Content: a shard payload as bags carry them, or empty. The work reads these fields of it: `unit` (a model unit is `2^unit` gibsons), `vertices` (whole units per vertex), `ticks` (the fraction of a unit per vertex, in 120ths, packed so that `-N` stands for `N` zero triples) and `faces`. Empty content means the default avatar and owes no work. No `d` tag: a replaceable kind has no second key, and writing one would only invite a reader to filter on it. **The work (normative):** ```python AVATAR_FLOOR_BITS = 16 # every avatar AVATAR_SIZE_BITS = 2 # per doubling of reach AVATAR_DETAIL_BITS = 3 # per doubling of detail beyond the free thirty-two AVATAR_DETAIL_FREE = 32 def avatar_work(payload) -> int: # reach: the farthest any vertex lies from the build origin, on any axis, # in gibsons at true scale; never below one gibson reach = max(1.0, max(abs(v + t / 120) for every vertex coordinate) * 2 ** payload.unit) detail = max(AVATAR_DETAIL_FREE, len(payload.vertices) + len(payload.faces)) return ceil(AVATAR_FLOOR_BITS + AVATAR_SIZE_BITS * log2(reach) + AVATAR_DETAIL_BITS * log2(detail / AVATAR_DETAIL_FREE)) ``` An avatar event is **paid** when its `nonce` tag's committed `target` is at least `avatar_work(content)` and its `id` carries at least `target` leading zero bits. Both conditions are required: committing the target before mining is what stops a lucky id from being claimed against a lower bar than it was mined for (NIP-13). **Verification:** parse the content, compute `avatar_work`, read the `nonce` tag, count the id's leading zero bits, apply the two conditions. One hash and a walk over the vertices, on any device. **Drawing (normative):** a client MUST NOT draw an avatar event that is not paid, or that carries content it cannot read; it draws its default avatar for that identity instead. A client SHOULD draw a paid avatar at true scale, one model unit as `2^unit` gibsons, wherever it would draw the default. **Why the price is shaped this way (non-normative).** Reach is priced at two bits per doubling, so a shape twice as far across costs four times the work, and the ladder it makes is: one gibson 16 bits, two gibsons 18, four 20, sixteen 24, a thousand 36, and one the size of cyberspace about 190, which is to say never. Reach is the term that matters to other people, since a large avatar is the one that gets in everyone's way, and the slope is set so that a modest shape of a few gibsons costs minutes on a phone while a sector-sized one is out of reach of any hash power. Detail is priced at three bits per doubling beyond thirty-two vertices and faces, because a busy small avatar troubles nobody much, and because bytes are charged already without a term: every nonce hashes the whole serialized event, so an avatar at the vertex and face caps runs about thirty times more slowly per try than a plain one. The sixteen-bit floor is seconds on a phone today and is set with a hundred-year horizon of growing hash power in mind; it is the one constant this section expects to be revisited. Reach is measured from the build origin, not from the shape's own centre, so a shape is priced as its builder placed it against the reference avatar on the bench. Reference implementations: `avatar.ts` in cyberspace-core and `cyberspace_core/avatar.py` in cyberspace-cli, pinned to one set of golden vectors. --- ## 9. Mapping to Physical Reality: GPS and Dataspace ### 9.1 Why a physical mapping exists Dataspace (`plane=0`) maps WGS84 GPS coordinates (latitude/longitude/altitude) into the u85 axis space. This creates a cryptographic overlay on the physical world where Cyberspace coordinates correspond to actual locations on and around Earth. This mapping is **consensus-critical**: if multiple clients are expected to agree on the same coord256 for a given GPS point, they must all use the exact same deterministic algorithm. The mapping applies only to dataspace. Ideaspace (`plane=1`) has no GPS mapping. ### 9.2 Dataspace cube size (Cantor Height 34 scale) The Cantor Height 34 scale defines the relationship between dataspace coordinates and physical distances: - **Full axis length:** ~4.5 trillion kilometers (~0.48 light-years) - **Half axis length:** ~2.25 trillion kilometers - **Gibson size:** ~1.16×10⁻¹⁰ meters (approximately the diameter of a hydrogen atom) - **Cantor Height 34 = 2 meters** (the canonical scale parameter) This scale provides "atomic" granularity in dataspace while maintaining axis extents that vastly exceed the geosynchronous orbit requirement. The universe starts small and extends far. ### 9.3 Scale rationale (non-normative) The Cantor Height 34 scale was chosen through rigorous testing to balance several concerns: **For consumers:** At this scale, consumer hardware can traverse human-centric distances and derive useful location-based secrets with significant but achievable effort. Moderate cloud compute expenditure ($200–$1,000) extends range substantially. **Against nation-states:** Cantor root cost scales with the side length of the aligned cube, per axis (`85 × 2^h` bits): a person (h34) is 182 GB, a 7 km city (h46) is 748 TB, a 262 km country (h51) is 24 PB, an Earth octant (h57) is 1.5 EB, and the GEO cube (h60) is 12 EB, against roughly 10 to 20 ZB of installed world storage. A country-scale root is within reach of a well-funded organization today and an Earth-scale root is within reach of a hyperscaler or a state. This is structural rather than a calibration choice: a country is only 2^17 times wider than a person, while the storage gap between a consumer and a state is about 2^20, so any scale that keeps human-scale hops feasible for consumers keeps country-scale roots feasible for states. The scale therefore does not deliver a century-long guarantee against large regions being held. Holding a region costs disk for as long as it is held (§7.8), which is the protocol's whole maintenance economics; claims, exclusion and governance are not protocol matters and are left to applications and games. **The gap is fixed, and the calibration cannot widen it (non-normative).** The argument above is right and can be made exact. A root is `85 × 2^h` bits per axis, so the highest height a storage budget `B` reaches is `h_max ≈ log2(B) − 6.018`. The calibration term does not appear in that equation and cannot: the calibration maps heights to meters, the cost function maps heights to bytes, and the two compose without interacting. **The distance between what an individual reaches and what a state reaches is therefore `log2(B_state / B_consumer)` whatever the calibration.** Measured at 16.7 heights for a mainstream desktop, 13.0 for an enthusiast rig and 22.0 for a phone. Changing the calibration slides a window of fixed width; it never widens it, and which consumer you mean moves the answer by 9 heights, which is more than any calibration change ever proposed here. **The shelf life, with a number (non-normative).** On central storage-growth assumptions a one billion dollar budget reaches the root of the whole Earth around **2045**, or 2036 optimistic, or never if the 2025 to 2026 storage price reversal proves permanent. Every ceiling past about one day of patience is capacity-bound, so compute growth barely moves these dates and storage price is the only sensitive input. No calibration changes them meaningfully, because the window is rigid. **Changing the calibration is not a hard fork (non-normative).** It is one constant in §9.7 step 9. No chain is invalidated, no proof breaks, nobody respawns; it is a smaller break than the sidestep v2 change already shipped. The reason not to change it is therefore not difficulty but **consensus**: two incompatible Earths is worse than either Earth, and that reason does not expire. Anyone proposing a change should be answered with the paragraph above rather than with the cost of making it. **Aesthetics:** - 2 meters is a metaphor for the human scale of the universe - Cantor Height 34 / 85-bit axis = 34/85 = 0.4 = 2/5, a rational and memorable relationship - Cantor Height 33 = 1 meter - 1 Gibson is roughly the size of a hydrogen atom, the first atomic element **No difficulty adjustment.** Unlike Bitcoin, Cyberspace has no difficulty adjustment mechanism. The scale is fixed by mathematical definition. A coordinate's Cantor tree is deterministic. It cannot be made "harder" without changing the coordinate itself. As technology advances, all parties gain greater computation and storage, gradually increasing the scale of territorial claims over time. The base protocol remains stable; difficulty migrates upward through extension mechanisms (DECKs) over decades. ### 9.4 Axis naming convention (ECEF → Cyberspace) Starting from standard Earth-Centered Earth-Fixed (ECEF): - `+X_ecef`: (lat=0°, lon=0°) - `+Y_ecef`: (lat=0°, lon=+90°) - `+Z_ecef`: north pole Cyberspace dataspace axis naming is: - `X_cs = X_ecef` - `Y_cs = Z_ecef` - `Z_cs = Y_ecef` ### 9.5 Canonical spec version and deterministic arithmetic **Spec version string (required):** - `CANONICAL_GPS_TO_DATASPACE_SPEC_VERSION = "2026-03-16-h34-corrected"` Canonical requirements: - Use decimal arithmetic end-to-end (no platform `libm` for trig). - Decimal context: - precision: `96` - rounding: `ROUND_HALF_EVEN` - π constant: use this exact truncated decimal string: - `PI_STR = "3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679"` - Deterministic trig: - Termination epsilon: `TRIG_EPS = 1e-88` - Max iterations: `TRIG_MAX_ITER = 256` ### 9.6 Altitude handling (normative) The canonical mapping is defined for latitude/longitude plus an optional altitude in meters. - If altitude is omitted, implementations MUST treat `altitude_m = 0`. - Implementations MUST support "clamp to surface" behavior that forces `altitude_m = 0` (this is what the golden vectors cover). - If non-zero altitude is supported, `altitude_m` MUST be interpreted as meters above the WGS84 ellipsoid and processed using the same canonical decimal parsing rules. ### 9.7 Canonical mapping algorithm (normative) 1. Parse inputs as decimals. 2. Clamp latitude to `[-90, 90]`. 3. Wrap longitude to `[-180, 180)`. 4. Convert degrees→radians using `PI_STR`. 5. Compute deterministic `sin/cos` using range reduction + Taylor series, terminating when `abs(term) < TRIG_EPS`. 6. Convert WGS84 geodetic to ECEF (meters) using decimals (using `altitude_m` after clamping, if applicable). 7. Convert meters→kilometers. 8. Permute ECEF axes into Cyberspace axes per §9.4. 9. Convert kilometers-from-center into u85 axis values: - `units_per_km = 1000 * 2^33` (derived from Cantor Height 34 = 2 meters) - `u = km * units_per_km + 2^84` - round using `ROUND_HALF_EVEN` - clamp to `[0, 2^85 - 1]` Derivation: At Cantor Height 34 scale, `2^34` Gibsons = 2 meters. Therefore 1 Gibson = `2^-33` meters, and 1 km = `1000 * 2^33` Gibsons. This formula maps GPS coordinates into a region centered at u85 value `2^84` (the half-axis point). 10. Produce coord256 with `plane=0` using the interleaving in §2. **Note (non-normative): the three h85 planes.** Because the mapping centers Earth at exactly 2^84 on every axis, the equatorial plane (`Y_cs = 2^84`) and the planes `X_cs = 2^84` and `Z_cs = 2^84` (the meridians at 0°/180° and ±90° longitude) are h85 boundaries: a one-metre step across the equator costs the same as crossing the whole axis. They behave like oceans, uncrossable on foot and crossed routinely through hyperspace (DECK-0001), whose landfalls sit on both shores. Only points within one last mile of a plane (about 0.75% of the surface at current landfall density) ever notice them. The centering is kept for symmetry; an offset of 2^56 would lower the walls to h57 and remains available as a future revision if landfall density is ever reduced. ### 9.8 Golden vectors (consensus locks) An implementation of §9.7 MUST reproduce every vector below exactly. The mapping is consensus-critical (§9.1): two clients that disagree on a vector disagree on where a place is. Implementations SHOULD include the vectors as tests, to catch accidental drift. These are the required vectors for spec version `2026-03-16-h34-corrected` (hex is 32 bytes, no `0x` prefix). Golden vectors assume `altitude_m = 0` with clamp-to-surface behavior enabled: - `origin_equator_prime` lat=0 lon=0 - `e000000000000000000001200041040208048040000000000000000000000000` - `equator_east_90` lat=0 lon=90 - `e000000000000000000000480010410082012010000000000000000000000000` - `equator_west_90` lat=0 lon=-90 - `c492492492492492492492012482082410480490000000000000000000000000` - `north_pole` lat=90 lon=0 - `e000000000000000000000900004924920020000820000920100824920800020` - `london` lat=51.5074 lon=-0.1278 - `c492492492492492492492edf5bee7267451c787d95ba4d7840c76d1e33c9940` - `nyc` lat=40.7128 lon=-74.0060 - `c4924924924924924924921f79235dae293ada913e78294253a235239a332854` ### 9.9 Consumer benchmarks (non-normative) Cantor root cost scales with the side length of the aligned cube, per axis, not with its volume: a region twice as wide costs twice as much storage and somewhat more than twice the time. The root of a height-h subtree over 85-bit leaves is **exactly `85 × 2^h` bits**, which is checkable at any small height and holds without rounding: each level of pairing doubles the operand width and halves the count. **On peak memory, corrected (non-normative).** An earlier draft said construction needs "roughly two levels live at once", which would make the peak about twice a root. Measured, the peak is about **6.1 times** a root. Two levels live is the floor a perfect implementation would reach, not what a real one does: producing a level allocates the new values before the old ones can be released, the multiply itself needs working space of the same order as its operands, and a garbage-collected runtime holds both longer still. Size a machine against 6.1 roots, not 2. | Region | Aligned height | Root per axis | Consumer feasibility | |---|---:|---:|---| | 1 m cube | h33 | 92 GB | hours to a day on a desktop with a fast SSD | | 2 m cube (canonical) | h34 | 182 GB | about a day | | 4 m cube | h35 | 370 GB | days | | 128 m block | h40 | 12 TB | external storage array, weeks | **Notes:** - A 50 m³ region is h35, twice the cost of h34, not fifty times. Volume-linear accounting overstates small claims and understates large ones. - Computation is parallelizable across axes and levels; storage and I/O bandwidth are the binding constraints. - Contiguous claims are significantly more efficient than discrete parcels due to Cantor subtree structure sharing. ### 9.10 Nation-state limits (non-normative) | Root side | Height | Root per axis | Feasibility | |---|---:|---:|---| | City (7 km) | h46 | 756 TB | a well-funded organization | | Country (262 km) | h51 | 24 PB | a state or a large company today (about a million dollars of disks, days of I/O) | | Earth octant (16,777 km) | h57 | 1.5 EB | a hyperscaler or a state | | GEO cube (134,000 km) | h60 | 12 EB | about 0.1% of installed world storage | | h70 | h70 | 12.7 ZB | roughly all installed storage today | The limiting factor on **how high a party can go** is storage capacity, and the protocol's work equivalence property ensures the data cannot be optimized away. There is no ASIC advantage, because no chip removes the need to hold the intermediates. The limiting factor on **how long it takes beneath that ceiling** is arithmetic rather than I/O bandwidth: multiplying two `n`-bit numbers costs far more than one operation per byte, and the work is measured at about 28 times compute-bound. §13.2 says more. Sidestep travel (§6) is plain hash work and has no such protection; see §12.3. ### 9.11 Storage as the primary constraint (non-normative) Cantor tree computation is memory-bound. At Cantor Height 34, a single subtree contains 2³⁴ ≈ 17 billion leaf nodes. The intermediate values cannot fit in RAM and must be streamed to disk. **This is intentional.** Capacity is the equalizer (and see §13.2 on why the wait beneath the ceiling is arithmetic rather than bandwidth): - Consumer SSDs provide enough I/O for small roots - Nation-states have faster storage, but exponential growth limits scaling - There is no "ASIC advantage" because the bottleneck is data movement, not hash rate The storage constraint ensures that territorial roots remain bounded by physical infrastructure, not just financial resources. --- ## 10. Sectors and Spatial Querying A **sector** is a cube of `2^30` Gibsons per axis. Sectors exist to divide Cyberspace into manageable pieces and, critically, to allow proximal querying of public Cyberspace objects on Nostr relays. **On the index's width.** An axis runs to `2^85` Gibsons and a sector is `2^30` of them, so a sector index is `2^55` values wide and needs **55 bits per axis**, not 32. An earlier draft of this paragraph said sectors "fit into u32 systems", which is wrong by 23 bits; a u32 would cover only the first `2^32` sectors of `2^55` on each axis. The tag values in §10 are decimal strings and are unaffected, but an implementation storing a sector index in a 32-bit integer will silently wrap. Use 64-bit integers, or the strings themselves. **On what a sector is useful for.** At the calibration of §9 a sector is `2^30` Gibsons, which is about 12.5 cm. A single-sector query therefore asks what is inside a box the size of a shoebox, and a single-axis query asks about a 12.5 cm slab through the whole universe. Proximal querying at a scale a person perceives, a room or a street, is not expressible as a sector query and needs a tag ladder at several heights, which this document does not define. Because Nostr cannot query "prefix ranges" on tag values, per-axis sector tags make it possible to query slices along a single axis. Given `x_u85, y_u85, z_u85`: - `sx = x_u85 >> 30` - `sy = y_u85 >> 30` - `sz = z_u85 >> 30` All events that claim a coordinate **MUST** include: - `X` tag: `["X", ""]` - `Y` tag: `["Y", ""]` - `Z` tag: `["Z", ""]` - `S` tag: `["S", "--"]` Tag formatting rules (normative): - `sx`, `sy`, `sz` MUST be encoded as base-10 integers (strings), with no leading `+` and no leading zeros (except `"0"`). - `S` MUST be exactly `"--"`. **Encrypted content events (kind 33330):** a bag without a hint claims no coordinate and carries no sector tags. A bag with a hint (§7.7) claims a box. It MUST carry the sector tag of each axis whose hint height is at most 30, computed from the box's base, and `S` when all three are fixed. A relay query on `#S`, or on one axis tag, returns the hinted bags in a sector or a slice the same way it returns movement. --- ## 11. Visualization Conventions This section defines canonical conventions for rendering Cyberspace coordinates in 3D visualizers. The goal is to ensure that different viewers agree on orientation (left/right, up/down, ahead/behind). These conventions are about visualization only. They do not change coordinate encoding (§2) or movement proof verification (§4–§8). ### 11.1 Handedness and axis semantics Implementations that render Cyberspace in 3D MUST preserve the Cyberspace axis semantics defined in §9.4 exactly. Graphics-engine handedness and camera defaults are implementation details and MUST NOT change Cyberspace semantics. When the viewer is oriented per §11.3: - `+X_cs` is screen-right. - `+Y_cs` is up. - `+Z_cs` is forward (toward the black sun / east reference marker). ### 11.2 Black sun reference marker The "black sun" is a reference to the hacker haven in Neal Stephenson's *Snow Crash*, one of the foundational works that inspired the Cyberspace Protocol. In Cyberspace, the black sun serves a simple practical purpose: it is a subtle guidepost so you know which direction you are facing. It is rendered as a purple circle marking the `+Z_cs` boundary of the coordinate space. If a visualizer renders the black sun, it MUST place it on the `+Z_cs` boundary of the Cyberspace cube. The `+Z_cs` boundary is the Z axis maximum, `2^85 - 1` (§2.1). The marker is placed at the center of that face: - `black_sun_u85 = (x=2^84, y=2^84, z=2^85 - 1)` in u85 coordinates - In physical units: `black_sun = (x_km=0, y_km=0, z_km≈+2.25×10^12 km)`, one half-axis (approximately 0.24 light-years) from the cube center along `+Z_cs` These two forms describe the same point. The physical frame of §9.7 is centered on the cube, so `km=0` on each axis is u85 `2^84`, whereas u85 values are measured from the cube corner. Converting the km figures above with the §9.7 formula lands on the u85 coordinate above, up to the rounding in the km figure (the exact half-axis is `2251799813685.248` km). The black sun is a directional guidepost for east (`+Z_cs`). Marker color SHOULD be purple. Marker shape (point/sphere/circle/disk) is implementation-defined. The black sun marker MUST be visible in both planes. (The plane bit does not affect XYZ decoding; it only labels the plane.) ### 11.3 Camera convention ("facing the black sun") A visualizer MUST provide (either as its default view or as an explicit preset) a camera/view mode equivalent to: - View direction: looking toward `+Z_cs`. - Up direction: `+Y_cs`. - Screen-right direction: `+X_cs`. This is the canonical interpretation used when describing a coordinate as "left/right", "above/below", or "ahead/behind" relative to the origin. ### 11.4 Engine adaptation Different graphics engines have different defaults for camera forward direction and orbit-control behavior. Implementations MUST use camera placement/orientation and/or a render-space transform so that the semantic rules in §11.1–§11.3 remain true, without mirroring or re-labeling Cyberspace axes. For quick regression tests and cross-implementation debugging, see `visualization_vectors.json` in this spec repository. --- ## 12. Limitations and Threat Model (non-normative) ### 12.1 What the protocol provides - **Single-location constraint (per keypair):** A valid, linear movement chain makes forking detectable. - **Hop freshness:** Every hop includes non-cacheable temporal work derived from chain context. - **Work equivalence (for discovery):** An entity must compute region preimages to derive discovery keys; there is no shortcut. - **Auditable movement history:** The chain provides an ordered trail of hops. - **Locality imposition:** Distance and regions become meaningful in a 256-bit address space. ### 12.2 What the protocol does NOT provide - **Physical location proof:** Dataspace mapping is deterministic, but it does not prove a body is physically at that GPS point. - **Trusted identity / sybil resistance:** One operator can control many keypairs. - **Privacy by default:** Movement events are public if published. - **Traversal necessity for decryption:** Region preimages can be computed directly without maintaining a movement chain. ### 12.3 Acknowledged attack vectors - **Coordinate scanning:** An observer can compute region preimages for arbitrary coordinates and query for content. This is considered acceptable because the work required is the same as for a traveler. - **Chain abandonment:** An entity may abandon a keypair and start fresh, or publish a new spawn event to restart their chain. Applications can require continuity/reputation at higher layers. - **ASIC-accelerated sidesteps:** Sidestep work is plain SHA-256 over short preimages. A purpose-built ASIC fleet crosses boundaries that consumers cannot: a 1 EH/s farm crosses h78 in about a week and a Bitcoin-scale fleet crosses h85 in about a day. The storage bound protects Cantor roots (claims and keys), not travel. - **Key grinding:** Spawning inside an aligned region of height h costs about 2^(3·(85−h)) key generations. Regions walled above roughly h75 can be spawn-camped by grinding; travel reaches about h50 and hyperspace reaches stop neighbourhoods; regions walled between h50 and h75 therefore have the strongest isolation the protocol offers. Applications should know which regime they are building in. --- ## 13. Structured Proof-of-Work (non-normative) This section describes how the computational work in Cyberspace relates to physical reality and how it differs from traditional proof-of-work systems like Bitcoin. ### 13.1 A new class of proof-of-work Bitcoin's proof-of-work operates by grinding random nonces through SHA-256 until the output falls below a difficulty target. The work is real (it consumes electricity and generates heat) but the output is arbitrary. A valid Bitcoin hash proves that energy was spent. It does not encode any spatial or structural information. The hash is discarded after use. Cantor pairing tree computation is a fundamentally different kind of work. The output is not arbitrary. When you compute a Cantor tree over a set of coordinates, the root you produce uniquely identifies that spatial region. It is a bijection. The root can be unpaired to reconstruct the entire tree. The proof of your movement is the mathematical fabric of the space itself. Every Cantor root you compute becomes a stable region identifier that persists as useful infrastructure. It can be used for encrypting localized secrets, discovering nearby content, and holding a region (§7.8). The work product is meaningful, not disposable. ### 13.2 Capacity-bound, and compute-bound within it Bitcoin's proof-of-work is compute-bound. Faster chips produce more hashes per second, and specialized hardware (ASICs) can be built to optimize SHA-256 throughput. The bottleneck is hash rate. Cantor work is storage-bound. The Cantor pairing function produces intermediate values that grow exponentially in bit size. At height 34, the intermediates require approximately 182 GB of storage. At height 40, approximately 11 TB. At height 50, approximately 11 PB. These intermediates must physically exist on disk during computation because parent nodes require both children during bottom-up tree construction. This means the limiting resource on **how high you can go** is capacity: you cannot compute a root you cannot hold, and no ASIC optimizes around needing terabytes of intermediates. That part is right and it is the reason §13.3's fixed difficulty behaves the way it does. **The limiting resource on how long it takes, at a height you can hold, is arithmetic rather than bandwidth, and an earlier draft of this section said otherwise.** Multiplying two `n`-bit numbers costs far more than one operation per byte, so the work per byte read from disk is high and the computation is measured at about 28 times compute-bound rather than I/O-bound. Both statements matter and they are about different limits: capacity decides the ceiling, arithmetic decides the wait beneath it. §6.12 and §7.8 work the same arithmetic in the context of disclosure and holding, and `docs/what-can-be-sold.md` collects all of it. ### 13.3 Fixed difficulty Bitcoin adjusts its difficulty every 2,016 blocks to maintain a 10-minute target block time. This adjustment requires protocol-level governance and creates a competitive arms race between miners. Cantor tree difficulty is fixed by the mathematics. The cost of computing a region is a deterministic function of the coordinates involved. There is no adjustment mechanism, no governance, and no parameters to tune. As hardware improves over time, all parties gain access to greater computation and storage. The boundaries of what is computable expand gradually for everyone. The protocol requires no ongoing calibration. ### 13.4 Where the energy goes The thermodynamic cost of Cantor work is real but mediated differently than in Bitcoin. In Bitcoin, the cost pipeline is direct: electricity flows into ASIC chips, which run SHA-256 repeatedly, generating heat. The conversion from watts to proofs is immediate and measurable. In Cantor work, the energy costs arise from several sources. CPU cycles perform the pairing arithmetic, which generates heat but is relatively minor at scale. The dominant cost is disk I/O: writing and reading terabytes of intermediate values to and from storage generates heat and consumes significant electricity. The storage media itself must be physically allocated and occupied for the duration of the computation, representing both an energy cost and an opportunity cost. There is also an interesting connection to Landauer's principle, which states that erasing one bit of information costs a minimum of kT × ln(2) joules (where k is Boltzmann's constant and T is temperature). When Cantor intermediates reach petabytes in size, the theoretical minimum energy cost of erasing them after computation may become non-trivial. ### 13.5 The sidestep as traditional proof-of-work The sidestep (§6), which uses SHA-256 Merkle hash trees instead of Cantor pairing, has a more direct thermodynamic profile. SHA-256 hashing is pure compute with fixed-size inputs and outputs. The cost is straightforward: hash operations multiplied by time equals watts. The sidestep is, in this sense, closer to traditional proof-of-work than the Cantor hop. This creates an interesting layering: Cantor hops are storage-bound structured work (fast but limited by storage), while Merkle sidesteps are compute-bound hash work (slow but unlimited by storage). The protocol naturally routes movement through whichever regime is feasible for the boundary being crossed. The two also differ in what the work leaves behind, and the difference is deliberate (§6.12). A Cantor hop's output is durable public infrastructure: a canonical region root that anyone may be handed and reuse. A Merkle sidestep's output is consumed at the moment of use: seeded to one chain position, worth nothing to anyone else, and disposable in exactly the way §13.1 says a Bitcoin hash is disposable. The sidestep is the one place in Cyberspace where work is burned rather than accumulated, which is precisely what makes it a price on movement instead of an investment in a place. --- ## 14. Reference Implementation The reference implementation for this spec is: - https://github.com/arkin0x/cyberspace-cli Implementers should treat that repo as the reference for: - Integer→bytes canonicalization for hashing - Movement proof computation - Canonical GPS→dataspace mapping (`CANONICAL_GPS_TO_DATASPACE_SPEC_VERSION` and golden vectors) This repository also carries stdlib-only reference scripts that are executable statements of specific sections, each self-checking when run: - `sidestep-reference.py`: the v2 sidestep construction (§6.4, §6.5, §6.10, §6.11), with golden vectors and a check that each property those sections claim actually holds - `hint-reference.py`: §7.7 and the §10 rule for bags: canonical form, containment, sector tags, seeker work, malformed hints and plane preservation, locking the golden vectors of §7.7 - `decks/landfall-reference.py`: landfall derivation (DECK-0001 §1.2) # Cyberspace Design Rationale This document explains the design decisions behind the Cyberspace protocol, the problems it attempts to solve, its limitations, and how it connects to the fictional visions that inspired it. This document is **non-normative**. The canonical protocol specification is `CYBERSPACE_V2.md`. --- ## 1. Why Build a Thermodynamic Spatial Protocol? ### The Precedent: Bitcoin Captured Time Bitcoin proved that thermodynamic work could anchor a digital system to physical reality. By making the creation of new coins require real energy expenditure, Bitcoin solved a problem previously thought impossible: transferring value through time without trusted intermediaries. Before Bitcoin, digital scarcity required institutions to enforce it. After Bitcoin, scarcity could be enforced by mathematics alone. This opened a question: if proof-of-work can capture the time dimension in a digital system, could the same principle capture the spatial dimensions? Not to create another currency, but to impose locality itself—the fundamental property of space—on a mathematical substrate. #### The Problems Locality Would Solve Locality is not just a constraint. It is a capability. Physical space enables things that digital systems cannot replicate: **Embodiment for digital entities.** AI systems currently have no spatial constraints. An AI can "be everywhere" simultaneously, accessing systems across the globe at the same instant. This matters because many security and social assumptions depend on spatial constraints: a person can only sign one document at a time, a witness can only be in one place. Locality would enable verifiable embodiment for digital entities, constraining them to "be somewhere" and not anywhere else in a provable way. **An Augmented Reality-native Geospatial Overlay**. The dataspace plane maps directly to physical reality: Earth is centered at `2^84` on every axis, a Gibson is about the diameter of a hydrogen atom, Cantor Height 34 is two meters, and each axis runs about 4.5 trillion kilometers, far beyond geosynchronous orbit (CYBERSPACE_V2.md §9.2 and §9.7). This creates a cryptographic overlay on the physical world, where cyberspace coordinates correspond to actual locations on Earth. By trustlessly placing cyberspace objects at physical locations, a decentralized and permissionless overlay of infinite layers can be projected over reality with no central servers. Who owns the AR cloud? Nobody. **Hiding without trust.** In physical reality, you can hide something by controlling where it is. A message in a bottle, a buried treasure, a note in a hollow tree. These require no keys, no permissions, and no trusted authorities. Only those who travel there can find them. The fundamental mechanism for hiding things is proof-of-work, whether it is physical travel or digital. Digital systems are spaceless and have no equivalent. All current approaches to hiding information require trusting someone: key holders, access control administrators, or hardware manufacturers. Locality would enable hiding through presence alone. **Communities with boundaries.** Physical communities have natural membership constraints: you must be there. This creates shared context, accumulated trust, and local reputation. Online communities lack these constraints. Anyone can join from anywhere, instantly, at zero cost. This enables sybil attacks, context collapse, and governance capture. Locality would reintroduce the constraint of presence, making membership require traversal rather than just a click. ## 2. Why Cantor Pairing Trees? ### The Design Requirements The system needed to satisfy: 1. **Work requirement:** Movement should require mathematical computation, not arbitrary hash grinding 2. **Distance scaling:** Larger movements should cost more 3. **Compact proofs:** Verifiable without gigabytes of data 4. **Location encryption:** A stable secret should be computable for a given coordinate such that it requires real work but can be computed at will. 5. **No observer advantage:** The work required to observe the cyberspace protocol is equal to the work of utilizing the protocol. Observers have no inherent advantage, and perform the same work. ### The Rejected Approach: Interleaved 256-bit Cantor Tree The initial design used the interleaved 256-bit coordinate directly in a single Cantor pairing tree. This seemed elegant—one tree for one coordinate. Why it failed: - **Axis asymmetry:** Due to bit interleaving (XYZXYZ...), X movements cost ~4× more than Z movements for the same distance. The protocol would privilege certain directions. - **Impractical scaling:** Moving 64 Gibsons took ~115 seconds in testing. Sector traversal (2³⁰ Gibsons) would take months. This is not a traversable space. - **Memory explosion:** Large movements produced multi-gigabyte Cantor numbers. Proofs would not fit in Nostr events. The lesson: mathematical elegance does not always yield practical systems. ### The Adopted Approach: Per-Axis Cantor Trees Each axis (X, Y, Z) gets its own independent 85-bit Cantor tree. Movement proofs are computed separately for each axis, then combined using nested Cantor pairing. Why it works: - **Axis symmetry:** Equal distances cost equal work regardless of direction. No privileged axis. - **Practical performance:** 1,024 Gibsons in ~1ms instead of ~33 seconds. Sector traversal becomes possible (many hops, but each is fast). - **Bounded computation:** 85-bit trees are manageable. Memory stays in kilobytes, not gigabytes. - **Parallelizable:** Three independent computations can run in parallel. - **Preserves semantics:** Still requires "traversing mathematical fabric"—just structured more practically. ### Ensuring Every Hop Costs Work (Temporal Axis) The per-axis Cantor roots are **region identifiers**. This makes spatial work cacheable: once a client has computed the Cantor root for an aligned region, it can reuse that result in later hops that traverse the same region. This is desirable for discovery (higher subtrees change rarely), but it creates a movement loophole: a mover could generate arbitrarily long hop sequences at near-zero marginal cost by reusing cached spatial results. They would only be able to travel places they had already been to once before, but instantly teleporting over previously trod terrain breaks the proof-of-work requirement continuity of cyberspace as a thermodynamic system. Cyberspace v2 addresses this by extending movement proofs into a fourth dimension: a **temporal work axis** derived from the Nostr movement chain. Each hop includes an additional Cantor-tree computation whose height `K` is derived from the hop destination coordinate (a deterministic “terrain” function) and whose seed is derived from the previous movement event id. Because each Nostr event id commits to the previous event (including its proof), the temporal seed for hop *N* is not known until hop *N-1* is complete; the work cannot be precomputed or amortized. Importantly, this does not change what *place* means. Stable spatial region identifiers (used for location-based encryption and discovery) remain a pure function of coordinates, independent of time or identity. The temporal axis exists only to make advancing the movement chain cost work. This is not a continuous “heartbeat” cost: an avatar’s last hop event remains its state indefinitely. The temporal axis work is paid only when moving. ### Why Not Just Hash Grinding? Standard proof-of-work (e.g., "find a nonce such that sha256(data + nonce) < target") is arbitrary. The work is real, but the structure is not—you are searching a hash space, not traversing a space. Cantor pairing trees create **actual mathematical structure**: - Each Cantor number represents a real subtree - Computing it means building the tree from leaves to root - The number is mathematically meaningful, not arbitrary This is the difference between "digging a hole" (arbitrary work) and "following a path" (structured traversal). Both cost energy, but only one has spatial semantics. --- ## 3. The Chalk on the Sidewalk Metaphor ### What Location-Based Encryption Is Actually For The purpose is not primarily secrecy. There are better cryptographic systems for secure communication—Signal, PGP, authenticated encryption. The purpose is to **model traversable reality**. Consider a message written in chalk on a sidewalk: - It is not "encrypted" in any technical sense - Anyone who walks by can read it - But you cannot read it without walking there - Even if someone tells you about it, they had to walk there to know This is a form of location-gated access that requires no keys, no permissions, no infrastructure. Only presence. ### How Cyberspace Implements This Region-derived keys are computed from Cantor roots. A ciphertext can be published publicly, but deriving the decryption key requires computing the region preimage. ### The Discovery Radius Each Cantor number represents a **region**, not a point. A number computed at height 10 covers 1,024 coordinates. This creates a natural discovery radius: - Height 4 (16 coordinates): like leaving a note on a street corner - Height 10 (1,024 coordinates): like a billboard in a neighborhood - Height 20 (~1 million coordinates): like a city-wide broadcast Anyone who traverses through the region computes the same Cantor number. They derive the same decryption key. They discover the same content. ### Hints: Whose Choice Is Discoverability? The lookup id reveals nothing about a bag's location, and a scan reaches only a few heights around the scanner, so a bag with no further information is discovered only by someone who happens to compute its region. In a 2^256 coordinate space that is the right property for a private message and a useless one for anything meant to be found: unhinted chalk is chalk nobody walks past. The hint (spec §7.7) leaves that choice with the hider. The hider publishes an aligned box, one height per axis, on the bag. A seeker sweeps the box by deriving keys at the bag's height for every candidate region inside it, so the gap between the box and the bag's own region is the price of the search, and the work equivalence of §7.1 makes that sweep cost the same from anywhere. Reading a bag therefore has no distance term. Reaching it does: standing in the region requires the movement work of §4 to §6 and is proven by a movement chain. The protocol keeps the two apart on purpose. The hider gets a difficulty knob. Applications get a clean fact to reward. Nothing new is introduced to make this work: no registry, no referee, no trusted index. Per-axis heights come free with aligned geometry, and they do real work. An exact X with coarse Y and Z is a two-dimensional hunt. A sector on all three axes says where to go, not where to look. Three heights equal to the bag's height is a destination. One tag covers all of these cases, and the sector tags of §10 follow whenever a hint fixes a sector, so a relay can answer "what is hidden in this sector" with the filters it already has. --- ## 4. What Cyberspace Does NOT Solve ### Honest Limitations **Physical location proof.** Computing a Cantor number proves cryptographic presence at a coordinate, not that your body is at that GPS location. The Dataspace plane maps to physical coordinates, but you can "be" there computationally while physically elsewhere. This is not a bug—it is inherent to any software-only system. True physical presence proof requires physical secrets (e.g., time-varying codes broadcast locally) that software-only systems cannot access. **Sybil resistance.** One person can control multiple keypairs. The single-location constraint applies per-keypair, not per-person. An entity could "be" in multiple places by operating multiple identities. This is acceptable. The protocol constrains identities, not people. Applications requiring unique human presence must layer additional mechanisms. **Traversal necessity.** The Cantor number for any region can be computed directly without maintaining a movement chain. Someone could pick random coordinates, compute their Cantor numbers, and decrypt content there. This is by design. The work is identical either way—observers have no computational advantage over travelers. What traversal provides is **verifiable commitment**: a movement chain proves you were on a specific path, at specific times, in a specific order. **Exclusive territory.** A region's Cantor root is a public function of its coordinates. Anyone who does the work gets the same value, so nothing in the mathematics can make a region belong to one party. The protocol offers holding (§6 below), which is a capability; it does not offer claims, title or exclusion, because those require a registry, and the protocol has none by design. ### Why These Limitations Are Acceptable The goal is not to create a perfect simulation of physical space. It is to create a thermodynamically meaningful spatial substrate where: 1. Presence requires work 2. Work is structured, not arbitrary 3. Observers have no advantage over participants 4. Movement history is auditable --- ## 5. From Fiction to Reality ### The Vision Cyberpunk fiction described cyberspace as: - A shared, consensual hallucination - A space where distance had meaning - Where skill and hardware determined power - Where nothing could happen without real work - Where you could be, move, and discover ### The Layered Architecture ``` ┌─────────────────────────────────────────────────────────┐ │ APPLICATION LAYER │ │ avatar visualization, commerce, IOT interfaces │ │ territory governance, AI embodiment constraints │ ├─────────────────────────────────────────────────────────┤ │ SPATIAL LAYER (Cyberspace Protocol) │ │ Coordinates, Cantor traversal, location-based │ │ encryption, movement verification, discovery radius │ ├─────────────────────────────────────────────────────────┤ │ TRANSMISSION LAYER (Nostr) │ │ Events, relays, keypairs, signatures, propagation │ ├─────────────────────────────────────────────────────────┤ │ VALUE LAYER (Bitcoin/Lightning) │ │ Payments, incentives, economic weight │ └─────────────────────────────────────────────────────────┘ ``` --- ## 6. Use Cases ### AI Embodiment Constraints Cyberspace provides a form of digital embodiment: - an agent operating through a keypair has a verifiable location, - it cannot claim to be in two places at once without forking, - its movement history is auditable, - higher-level systems can gate capabilities on verified presence in addition to payments, signature challenges, etc. ### Location-Based Secrets Without Infrastructure Cyberspace does not require GPS trust, cell towers, or secure enclaves—only the mathematical work. ### Territory: Holding, Not Owning Computing a region's root leaves every sub-region's root in your hands; keeping them on disk is **holding** the region (spec §7.6). Holding buys latency on keys and nothing else. You read the whole wall at every height while a passerby reads the small print at eye level; you write at any height at once; and you can lock content to `KDF(region_key || your_secret)` so that reading it requires both presence and your permission. Anyone who does the work holds identical keys, chains are public to everyone, and nothing you hold affects anyone else. Holding is not ownership, and the analogy to physical property breaks in an instructive place. Physical territory is rival: two bodies cannot share a point. Cyberspace territory is not: two avatars can share a coordinate, and two holders hold the same keys. Conflict over land follows from rivalry, so in cyberspace it does not follow from anything. It can only be chosen, as a game; see `docs/territory-conflict-game-layer.md`. What holding costs is disk for as long as it is held (about 19 TB for the full trees of a 2 m cube, about 1.1 TB for the nested cubes around one position). That is the whole of the protocol's maintenance economics: stop paying and your keys are a recomputation away again. One privacy consequence is worth stating plainly: the holder can read everything location-encrypted inside the holding. Visitors who want privacy inside a held region layer their own end-to-end encryption on top. The landlord can bug the room. Location is a privacy dial for holdings as for everything else (DECK-0001 §9.5): a region near a stop is reachable by anyone for a few GPU-hours; a region h60 inland is reachable by no one. Continuous presence in a region still establishes verifiable history, which is the part applications build reputation on. ### Ephemeral Regional Communication Local, temporary messages (“blips”) are an example of higher-layer use, not a protocol requirement. --- ## 7. Work Equivalence: The Key Property In almost every digital system, observers have advantages over participants. Cyberspace aims for a rare property: computing the region preimage costs the same whether you traveled there via a movement chain or computed it directly. This serves the goal of modeling locality thermodynamically: you cannot know what is somewhere without doing the work. --- ## 8. Integration with Nostr Nostr provides: - keypair identity, - event propagation, - signature verification, - permissionless relays. Cyberspace adds: - **where** (coordinates and sectors), - **distance** (movement cost), - **locality** (region-derived keys and discovery). --- ## 9. Open Questions and Future Work - Making traversal mathematically necessary for computing region numbers (not just verifiable). - Relay specialization / indexing by region. - Terrain tuning: alternative deterministic “terrain” functions for deriving the temporal height `K` (and different cell scales / distributions). - Privacy enhancements (e.g., ZK proofs, obfuscated paths). --- ## 10. Conclusion Cyberspace is an attempt to answer a specific question: can spatial properties be thermodynamically modeled in a digital system? The protocol does not perfectly simulate physical space, but it does provide: - thermodynamically meaningful distance, - verifiable movement history, - location-gated access without trusted infrastructure, - work equivalence between observers and travelers. # DECKs (Design Extension and Compatibility Kits) This directory contains protocol extensions for Cyberspace. The base Cyberspace v2 protocol is specified in `../CYBERSPACE_V2.md`. Extensions are specified as **Design Extension and Compatibility Kits (DECKs)**. A DECK is a self-contained document that defines additional, optional behavior layered on top of the base spec. ## Goals - Keep `CYBERSPACE_V2.md` focused on the base protocol. - Allow optional features to be specified, implemented, and discussed independently. - Provide a stable place to allocate additional Nostr event kinds / tags without bloating the base spec. ## Scope A DECK MAY: - Define new Nostr event kinds. - Define new `A` tag values for movement events (`kind=3333`). - Define additional validation rules that apply only when the extension is being used. - Define discovery/indexing conventions for the extension. A DECK MUST NOT: - Change consensus-critical rules of the base protocol unless it explicitly defines a new base-protocol version. ## Naming and numbering DECKs are named: - `DECK-XXXX-.md` Where: - `XXXX` is a zero-padded decimal integer. - `` is a short, lowercase, dash-separated identifier. ## Required header fields Each DECK MUST include: - `DECK:` number - `Title:` - `Status:` Draft | Proposed | Active | Deprecated - `Created:` YYYY-MM-DD - `Last updated:` YYYY-MM-DD - `Requires:` base spec and (optionally) minimum versions ## Game mechanics Some DECKs are game rules rather than protocol extensions: they define what a game's clients do with chains, not what the base protocol says about them. The base protocol has exactly one verdict on a chain, **validity**. A game owns a second, **liveness**. The two never consult each other. | | Game-alive | Game-dead | |---|---|---| | **Protocol-valid** | ordinary play | a chain the game has ruled out of play, while the protocol still counts it valid | | **Protocol-invalid** | a virtual spawn: chain invalid, the game recognises it | an ordinary invalid chain (fork, bad proof) | A game-mechanic DECK MUST, in addition to the rules above: - never alter the validity of any `kind 3333` chain under the base spec, and never require anything of clients that do not run the game; - be verifiable from a bounded set of events plus, if it needs a clock, Bitcoin block headers; `created_at` MUST NOT decide any game verdict; - if it resolves conflict, resolve it by work, with the unit of work stated and the cost of verification bounded; - specify its effects as a fixed point over the reference graph where events can void one another, and state how verdicts behave under partial views. The design record for this category, and for why the base protocol defines holding (`CYBERSPACE_V2.md` §7.6) but not domains, is `../docs/territory-conflict-game-layer.md`. ## Registry - `DECK-0001-hyperspace.md`: Hyperspace, Bitcoin block transit (ports, landfalls, stations, rides) - `DECK-0002`: reserved for Virtual Spawn (game mechanic; draft in PR #15) - `DECK-0003-sno.md`: SNO (Simple Nostr Objects), the small 3D object format. Defines `kind 33331`, a standalone editable object; `kind 3330` bag items and `kind 11333` avatars carry the same payload. Beside it: `sno-reference.py`, a conformance implementation that runs its own rejection table; `sno-palette.json` and `sno-palette.mjs`, the built-in 256-colour palette and the generator that produces it; `sno-palette.png`, the sheet # DECK-0001: Hyperspace (Bitcoin block transit) DECK: 0001 Title: Hyperspace (Bitcoin block transit) Status: Draft v3 (supersedes the v2 draft of 2026-04-16 and the v1 draft of 2026-02-28) Created: 2026-02-28 Last updated: 2026-08-24 Requires: `CYBERSPACE_V2.md` (spec version `2026-03-16-h34-corrected`) ## Abstract Hyperspace is a one-dimensional transit line threaded through Cyberspace by Bitcoin's proof of work. Every Bitcoin block is a **stop** on the line, numbered by block height. The merkle root of a block decides what kind of stop it is: a root whose plane bit is 1 is a **port** in ideaspace, sitting exactly at the root's coordinate; a root whose plane bit is 0 has fallen to Earth as a **landfall**, sitting on the WGS84 surface at a point chosen by the block hash. Nobody can predict where the next stop will be, and every stop was paid for by the mining network. An identity boards the line from wherever it is standing, by publishing an `enter-hyperspace` action. It does not choose where on the line it appears: it appears at its **station**, the stop nearest to it in the protocol's own metric. From the station it **rides** the line to any other stop by publishing a `hyperjump` action whose proof carries seeded, non-cacheable work for every block passed. It **exits** by publishing an ordinary hop or sidestep from the stop's coordinate. This design keeps locality where the base protocol puts it: at destinations. Leaving is easy, arrival is pinned to a stop, and the last mile from the stop to anywhere else is ordinary movement. Landfalls make Earth's surface reachable from anywhere for the price of a toll, a ride, and a short walk; the interior of the 2^85 cube stays wild. **Actions defined (all `kind 3333`, per the locked action-kind pattern):** | `A` tag | Purpose | Proof | |---|---|---| | `enter-hyperspace` | board the line at the station | temporal-axis proof at the current coordinate (toll reserved, see §7) | | `hyperjump` | ride from one stop to another | Merkle root over per-block seeded Cantor work, with sampled openings | Exit uses the base protocol's `hop` or `sidestep`; no new action is needed. **Block anchor events** (`kind 321`) remain the discovery convenience; §2 revises their format. --- ## Terms - **Line:** the sequence of Bitcoin blocks ordered by height. Hyperspace is the line. - **Stop:** one Bitcoin block, as a location. Every stop has a height `B`, a block hash `H`, a merkle root `M`, and a stop coordinate `C`. - **Port:** a stop whose merkle root has plane bit 1. Its coordinate is the merkle root itself, interpreted as a coord256 (ideaspace). - **Landfall:** a stop whose merkle root has plane bit 0. Its coordinate is a point on the WGS84 ellipsoid surface derived from the block hash (dataspace). - **Station:** the stop an identity appears at when it boards: the stop nearest its current coordinate (§4). - **Ride:** a `hyperjump` action from one stop to another. Its length is the number of blocks passed. - **Exit:** a `hop` or `sidestep` whose origin is a stop coordinate. - **Block anchor event:** a `kind 321` Nostr event binding a block's identifiers to its stop coordinate. --- ## 1. Stop coordinates (normative) ### 1.1 Plane selection Let `merkle_root_int` be the block's merkle root interpreted as a 256-bit big-endian integer from its standard hex display form (the same convention block explorers use). Implementations MUST agree on this byte order. ``` plane = merkle_root_int & 1 ``` - `plane = 1`: the stop is a **port**. `C = merkle_root_int` (unchanged from earlier drafts). - `plane = 0`: the stop is a **landfall**. `C` is derived from the block hash per §1.2. The merkle root decides the plane in both cases. For a landfall the root's X/Y/Z bits are not used as a position; the block hash supplies the position. Because a block hash cannot be steered by its miner except by discarding valid blocks, landfall positions cannot be chosen (§9.4). ### 1.2 Landfall derivation **Constant:** `LANDFALL_DOMAIN = b"CYBERSPACE_LANDFALL_V1"` (ASCII bytes). If any part of this derivation changes, the domain string MUST be bumped. **Inputs:** the 32-byte block hash `H` in its standard big-endian display byte order. **Arithmetic:** every step below is performed in the decimal context of `CYBERSPACE_V2.md` §9.5 (precision 96, `ROUND_HALF_EVEN`, the exact `PI_STR`, the deterministic Taylor `sin`/`cos` with `TRIG_EPS` and `TRIG_MAX_ITER`). Square roots are the context's correctly rounded square root. Operations MUST be performed in the order written, each rounded to the context, so that independent implementations agree bit for bit. 1. `seed = sha256(LANDFALL_DOMAIN || H)` (32 bytes). 2. `u1 = int(seed[0:16]) / 2^128` and `u2 = int(seed[16:32]) / 2^128`, both as exact decimals (`int()` big-endian). 3. `lon = (2 * u1 - 1) * PI` (radians, in `[-PI, PI)`). 4. `z = 2 * u2 - 1` (the z component of a unit direction, in `[-1, 1)`). 5. `rxy = sqrt(1 - z * z)`. 6. `(sin_lon, cos_lon) = sincos(lon)` per §9.5. 7. `dx = rxy * cos_lon`, `dy = rxy * sin_lon`, `dz = z`. This is a unit direction in ECEF, uniformly distributed on the sphere. 8. With `a = 6378137` (metres) and `b = a * (1 - f)`, `f = 1 / 298.257223563`: `inv = sqrt((dx*dx + dy*dy) / (a*a) + (dz*dz) / (b*b))` and `r = 1 / inv` (metres). `r * d` is the point where the direction `d` meets the WGS84 ellipsoid surface. 9. `x_km = r * dx / 1000`, `y_km = r * dy / 1000`, `z_km = r * dz / 1000` (ECEF kilometres). 10. Permute into Cyberspace axes per §9.4 (`X_cs = X_ecef`, `Y_cs = Z_ecef`, `Z_cs = Y_ecef`) and convert kilometres to u85 per §9.7 step 9 (`u = km * units_per_km + 2^84`, `ROUND_HALF_EVEN`, clamp). 11. `C = xyz_to_coord(x, y, z, plane = 0)`. The result lies on the ellipsoid (geodetic altitude 0). The distribution is uniform in geocentric solid angle, which is uniform in surface area to within the WGS84 flattening (0.3%). **Golden vectors (consensus locks).** Block hashes are mainnet. `landfall` is the coord256 as 64 lowercase hex characters. The approximate latitude/longitude columns are informational only. | Height | Block hash | Landfall coord256 | ~lat, lon | |---:|---|---|---| | 398 | `000000002f7d702a27ccd65158740198f79d4ba1ddea8ab14b56b63a6289fe89` | `56db6db6db6db6db6db6db3e27c436f9d3b79fb5fc6457798936b3e749e38f56` | 31.63, -98.85 | | 100399 | `000000000003cb256436f213199e7047e187ab99e6d3176262bfb9be49d2a31a` | `3b6db6db6db6db6db6db6d1eb09e85e5f572906af5a025a39ae284dd83278b72` | -54.83, 129.06 | | 300399 | `0000000000000000212f189879294318528669d239d5fbd30e6ffcc6015ced21` | `c492492492492492492492c7807ba8ecefd0a48a7b41dfbb50da5947b489ed8c` | 57.04, -60.77 | | 363199 | `000000000000000001e65a8804c7d97ee1fd52394632bdebdaf402935dcddeec` | `a9249249249249249249258087f30451bd8dd013357959fe2b07fa052488980c` | -39.03, 5.70 | | 500399 | `000000000000000000521f92387f9f43258f62465e9f88b19ecad2c30e44d7ff` | `3b6db6db6db6db6db6db6d312f699ee9f35318557d9ee813b46f229215524a30` | -65.92, 134.98 | | 700398 | `00000000000000000005608e4c1ff53901186e766df5eaa87c636857ed814fa9` | `56db6db6db6db6db6db6dbfdb284c1592e0d02ffd65f9d6c12d48a2b483d7da0` | 86.47, -109.22 | | 900399 | `00000000000000000001e412795ed39b18e56338e9b3c20d91edf59d20e020c9` | `c4924924924924924924920c53c81e9d260623340e5c3a75b6de6e4715cd1724` | 6.07, -75.88 | | 950399 | `00000000000000000001f081b994866dc3beb2c3ecd5976e9bda474e54e027c1` | `e000000000000000000000618f9c2d172da11fc0701996d4a89df1f60aecf732` | 3.86, 63.90 | Reference implementation: `decks/landfall-reference.py` in this repository (stdlib Python; executing it checks all eight vectors). The ONOSENDAI client's `landfall` module (decimal.js) and the NTH publisher reproduce them byte for byte. ### 1.3 Sector tags Sector tags `X`, `Y`, `Z`, `S` for any event that carries a stop coordinate are computed from `C` per `CYBERSPACE_V2.md` §10, not from the merkle root. --- ## 2. Block anchor events (kind 321) Stops are discoverable on Nostr through **block anchor events**. Anchors are a convenience: an implementation with its own block source MAY derive every stop itself, and anchors on relays are not guaranteed accurate. ### 2.1 Required tags (normative) - `C`: `["C", ""]`, the stop coordinate per §1 (a port's merkle root, or a landfall coordinate). - `M`: `["M", ""]`, the block's merkle root (32-byte lowercase hex). For a port `M == C`. - `B`: `["B", ""]`, the block height (base-10 string). - `H`: `["H", ""]`. - `P`: `["P", ""]`. - Sector tags `X`, `Y`, `Z`, `S` computed from `C`. Anchors SHOULD include `net` (`mainnet` assumed if absent) and `N` (next block hash) once known. ### 2.2 Legacy anchors (normative) Anchors published before this revision carry `C = merkle_root` for every block and no `M` tag. An implementation encountering a `kind 321` event without an `M` tag MUST treat its `C` as the merkle root, derive the plane per §1.1, and for plane-0 blocks derive the landfall coordinate itself from `H` per §1.2. Such events MUST NOT be used as a source of landfall coordinates directly. ### 2.3 Validation (normative) An anchor is valid for a given stop iff `M` and `H` match the block at height `B` on the selected network, `P` matches its previous block hash, and `C` equals the derivation of §1 from `M` and `H`. How an implementation obtains block data is out of scope (full node, headers-only, trusted checkpoints). ### 2.4 Bulk distribution (non-normative) Fetching the whole line as anchor events does not scale: the ~964k stops of 2026 are roughly 570 MB of signed events, and per-event signature checks alone dominate a session. The reference stack instead distributes the line as **header blobs**: statically served files of 48-byte records (version, merkle root, time, bits, nonce; the previous-block hash is omitted and reconstructed), from which a client rebuilds each 80-byte wire header, verifies SPV rules end to end (hash linkage, proof of work against `bits`, the 2016-block difficulty windows), and derives every stop coordinate per §1 locally. Nothing about the blobs is trusted: they prove their own work, a manifest carries the final block hash of each blob, and the client pins its own embedded checkpoint hashes so a compromised manifest host cannot substitute a chain. Measured in the reference web client, the full line verifies and derives from ~46 MB of static files in about 30 seconds, with anchors remaining the live tail and the fallback. The format is specified in `docs/HEADER-BLOBS.md` of the `nth` repository. --- ## 3. Enter-hyperspace action An identity boards the line by publishing an `enter-hyperspace` action from wherever it is standing. The action does not move the identity. It marks the chain position from which the first ride departs. ### 3.1 Event (normative) Required tags: - `A`: `["A", "enter-hyperspace"]` - `e` genesis: `["e", "", "", "genesis"]` - `e` previous: `["e", "", "", "previous"]` - `c`: `["c", ""]` - `C`: `["C", ""]` (MUST equal `c`; the identity does not move) - `proof`: `["proof", ""]` per §3.2 - Sector tags from `C` Optional: `net`. ### 3.2 Entry proof (normative) The entry proof is the base protocol's temporal axis at the current coordinate, with no spatial component: 1. `K = terrain K` at `C` per `CYBERSPACE_V2.md` §5.2 (including the plane bit). 2. `t`, `t_base`, `cantor_t` from `previous_event_id` and `K` per §5.3. 3. `enter_n = π(0, cantor_t)`. 4. `proof_hash = sha256(sha256(int_to_bytes_be_min(enter_n)))`, lowercase hex. This binds the boarding to the identity's chain position and cannot be precomputed before the previous event exists. It is not a fare; see §7. ### 3.3 Position semantics (normative) - After `enter-hyperspace`, the identity's location is still `C`. - The identity's location changes only when a `hyperjump` action is published (§5). After the first ride, its location is the destination stop's coordinate. - A `hop` or `sidestep` published immediately after `enter-hyperspace` moves from `C` as usual and cancels the boarding. - Publishing `enter-hyperspace` while already located at a stop is valid; the station is then that stop (LCA 0). --- ## 4. The station (normative) The station is the stop at which an identity appears on the line. It is a deterministic function of the identity's coordinate and the set of stops, computed identically by the traveler and every verifier. ### 4.1 Distance For two coordinates `p` and `q` with axis values `(px, py, pz)` and `(qx, qy, qz)` (plane bits ignored): ``` d(p, q) = max(find_lca_height(px, qx), find_lca_height(py, qy), find_lca_height(pz, qz)) ``` `d` is the height of the smallest aligned cube containing both points, which is exactly the boundary a direct hop or sidestep between them would have to cross. ### 4.2 Definition Let `C_e` be the coordinate of the identity's `enter-hyperspace` event, and let `A` be the **station set bound**: a block height declared in the `as_of` tag of the identity's first `hyperjump` event (§5.2). `A` MUST be a height that exists on the selected network and MUST be `≥ B_to`, the ride's destination height. Let `Stops(A)` be the set of all stops with height `≤ A`. ``` station(C_e, A) = the stop s in Stops(A) minimising d(C_e, C_s), ties broken by the lowest height ``` Travelers SHOULD declare the highest height they have synced (the tip) so the station is their genuine nearest stop. The declared bound replaces a clock the protocol does not have: `A` is pinned inside the signed event, so the station is a fixed, verifiable fact for the trip. **Why not bind to the destination height (non-normative).** An earlier draft used `Stops(B_to)`. Riding toward an old block then excluded every newer stop, so a traveler whose nearest stop was recent would be assigned an ancient station and a ride hundreds of thousands of blocks long. Letting the traveler declare `A` restores the genuine nearest. The freedom this concedes is bounded: for a fixed position, the nearest stop as a function of the bound changes only at record points, roughly fourteen candidates over the whole chain history, so a traveler chooses among those and nothing else; no choice of `A` reaches an arbitrary stop. ### 4.3 Chain rule - The first `hyperjump` after an `enter-hyperspace` MUST carry an `as_of` tag and have `from_height = station(C_e, as_of)`. - Every subsequent `hyperjump` not separated from the previous one by a `hop` or `sidestep` MUST have `from_height` equal to the previous `hyperjump`'s `B`. - A `hyperjump` whose previous event is neither `enter-hyperspace` nor `hyperjump` is invalid. ### 4.4 Computation (non-normative) Because the coordinate interleaves the axes (`x84 y84 z84 x83 ...`), sharing an `L`-bit prefix of the interleaved coordinate means sharing `floor(L/3)` leading bits on every axis, so `d = 85 - floor(L/3)` and the station is the stop whose interleaved coordinate shares the longest common prefix with `C_e`. Sorting stop coordinates once turns the lookup into one binary search plus a scan of the (usually tiny) run of stops sharing the winning prefix; the height bound is applied by widening the run when the nearest stop is newer than `B_to`. No implementation needs to compare against every stop. On Earth the station is a landfall a few tens of kilometres away (mean spacing 33 km at 476k landfalls; `d` is typically 47 to 49). In the void it is the single port inside the identity's h78 or h79 cube, shared by that whole region and changing roughly once per two million blocks. The only ways to change a station are to move to a different landfall cell (which yields a station just as random in chain terms), to cross an h78 boundary (infeasible), or to pick a different destination; none of them shortens the ride to the place the traveler wanted. --- ## 5. Hyperjump action (the ride) A `hyperjump` moves an identity from one stop to another along the line, in either direction, in a single event. Its proof carries fresh work for every block passed. ### 5.1 Constants (normative) ``` K_LINE = 6 SAMPLES = 32 HYPERSPACE_TERRAIN_DOMAIN = b"CYBERSPACE_HYPERSPACE_TERRAIN_V1" HYPERSPACE_SEED_DOMAIN = b"CYBERSPACE_HYPERSPACE_SEED_V1" HYPERSPACE_LEAF_DOMAIN = b"CYBERSPACE_HYPERSPACE_LEAF_V1" HYPERSPACE_SAMPLE_DOMAIN = b"CYBERSPACE_HYPERSPACE_SAMPLE_V1" PAD_LEAF = 32 zero bytes ``` Any change to a preimage format MUST bump the corresponding domain string. ### 5.2 Event (normative) Required tags: - `A`: `["A", "hyperjump"]` - `e` genesis, `e` previous (as for all movement events) - `c`: `["c", ""]`: the identity's current coordinate (for the first ride after boarding, the `enter-hyperspace` coordinate; otherwise the previous stop's coordinate) - `C`: `["C", ""]` - `from_height`: `["from_height", ""]` (base-10) - `B`: `["B", ""]` (destination height, base-10; `B_to != B_from` unless §5.6 applies) - `as_of`: `["as_of", ""]` (the station set bound, base-10; REQUIRED on the first ride after boarding, per §4.2; `A ≥ B_to`) - `proof`: `["proof", ""]` per §5.4 - `mp`: `["mp", ""]` per §5.5 - Sector tags from `C` Optional: `net`; `e` tags with markers `hyperjump_from` / `hyperjump_to` referencing anchor events. ### 5.3 Per-block work (normative) Let `lo = min(B_from, B_to)` and `hi = max(B_from, B_to)`. The ride passes the blocks `b = lo + 1, ..., hi`, and `n = hi - lo` is its length. For each such `b`, with `H_b` its block hash (32 bytes, display order) and `previous_event_id` the 32-byte id referenced by this event's `e previous` tag: 1. **Line terrain.** `digest = sha256(HYPERSPACE_TERRAIN_DOMAIN || H_b)`; `word16 = (digest[0] << 8) | digest[1]`; `K_b = popcount(word16)` (an integer in `[0, 16]`, binomial with mean 8). 2. **Height.** `h_b = K_b + K_LINE` (an integer in `[6, 22]`). 3. **Seed.** `t_b = int(sha256(HYPERSPACE_SEED_DOMAIN || previous_event_id || be64(b))) mod 2^85`, where `be64(b)` is the height as 8 big-endian bytes. 4. **Work.** `t_base_b = (t_b >> h_b) << h_b`; `cantor_t_b = compute_subtree_cantor(t_base_b, h_b)` per `CYBERSPACE_V2.md` §4.6. 5. **Leaf.** `leaf_b = sha256(HYPERSPACE_LEAF_DOMAIN || be64(b) || int_to_bytes_be_min(cantor_t_b))`. The seed depends on the previous event id, so no leaf can be computed before the preceding event exists, and no leaf can be reused across identities or chain positions. The height depends on the block, so the line has hills: some stretches cost more to pass than others. ### 5.4 Merkle root (normative) Order the leaves by ascending `b`. Append `PAD_LEAF` until the count is a power of two (a single leaf needs no padding). Build the tree bottom-up with `parent = sha256(left || right)`, exactly as `CYBERSPACE_V2.md` §6.4 step 5, streaming per §6.5 if desired. The `proof` tag is the root, lowercase hex. ### 5.5 Openings and verification levels (normative) Let `n_pad` be the padded leaf count and `depth = log2(n_pad)`. **Sample indices.** For `i` in `0 .. SAMPLES - 1`: ``` idx_i = int(sha256(HYPERSPACE_SAMPLE_DOMAIN || root || be32(i))) mod n ``` (`be32(i)` is four big-endian bytes; indices are positions among the `n` real leaves, `0` meaning block `lo + 1`.) If `n < SAMPLES`, indices repeat; implementations MAY deduplicate. **Openings.** The `mp` tag value is the `SAMPLES` inclusion paths joined by `:`; each path is the `depth` sibling hashes from the leaf level to the root, concatenated as lowercase hex (`64 * depth` characters). The verifier determines left/right at each level from the index. **Level 1 verification (routine):** 1. Check chain structure, `c`, and the §4.3 chain rule (recomputing the station from the declared `as_of` bound when the previous event is an `enter-hyperspace`; the bound MUST reference an existing height and be `≥ B_to`). 2. Check `C` equals the stop coordinate for height `B` per §1 on the selected network. 3. Recompute the sample indices from `root`. 4. For each sampled index, recompute `leaf_b` from scratch per §5.3 (this requires the block hash of `b` and repeats the block's Cantor work), and verify its inclusion path to `root`. 5. Accept iff every path verifies. **Level 2 verification (audit):** recompute every leaf and the root. As with sidesteps, security rests on deterministic fraud detectability: a root that does not correspond to the full work is permanently and objectively detectable by anyone willing to redo the ride, and a detected fraud invalidates the chain from that event forward. **Why sampling (non-normative).** Level 1 costs `SAMPLES` blocks of work instead of `n`. A prover who skips a fraction of the leaves and grinds fake leaf values to steer the sample indices away from the gaps passes with probability `f^SAMPLES` per attempt, where `f` is the fraction actually done; with `SAMPLES = 32` a prover willing to spend about 2^40 cheap attempts can skip at most roughly half the line, and Level 2 exposes the fraud permanently. The openings are about 40 KB for a full-length ride, within common relay event-size limits. ### 5.6 Zero-length ride If `station(C_e, B_to) == B_to` (the identity's nearest stop is its destination), the ride has `n = 0`. The event carries `from_height == B`, `proof` of 64 zero characters, and an empty `mp` value. This relocates the identity from `C_e` to the stop and is the intended meaning of boarding at one's station. ### 5.7 Cost expectations (non-normative) Per block the expected work is about `2^6 * (3/2)^16 ≈ 42,000` Cantor pairs, with a worst block of 2^22 pairs (a 45 MB root, seconds). A ride between two random stops today averages about 320,000 blocks. Measured in the reference implementations: pure Python at small heights h14 12 ms, h16 100 ms, h18 720 ms, h20 6.6 s; the web client's worker pool averages roughly 190 ms per block in single-threaded JavaScript, which prices a full random ride in hours divided by the pool width, and a compiled bignum library brings it to the order of ten minutes. Implementations SHOULD run rides in a background worker with progress, and SHOULD persist completed leaves keyed by `(previous_event_id, b)` so an interrupted ride resumes instead of restarting; §5.3's seeding makes this safe, because a cached leaf is only ever valid for the boarding it was computed under. The line grows by about 26,000 stops of each kind per year, so the same trip lengthens slowly over time. Level 1 verification is `SAMPLES` blocks of work, seconds. --- ## 6. Exit An identity leaves the line by publishing a `hop` or `sidestep` whose `c` tag is the stop coordinate it is located at. No new rule applies: the exit is validated by the base protocol. A landfall exit begins in dataspace on the ellipsoid surface; a port exit begins in ideaspace at the merkle root. The stop coordinate is the bridge between the line and the space. Because exits are pinned to stops, hyperspace cannot be used to arrive at an arbitrary coordinate. The last mile from a stop is ordinary movement, and any region root (a discovery key, or holding the region per `CYBERSPACE_V2.md` §7.6) still costs its full Cantor computation. Entering is not claiming. --- ## 7. Toll (reserved) A boarding toll, fixed work paid by `enter-hyperspace` beyond the temporal-axis proof of §3.2, is reserved for a future revision of this DECK. Its purpose would be to price departure independently of the ride. The domain string `CYBERSPACE_ENTER_HYPERSPACE_V1` is reserved for it. Until it is specified, §3.2 is the whole entry cost, and the ride (§5) is the whole price of hyperspace. --- ## 8. Equivocation and chain integrity (normative) - Two movement events with the same `previous_event_id` are a fork; both branches are invalid from that point. - `enter-hyperspace` MAY follow any movement action. - `hyperjump` MUST follow `enter-hyperspace` or `hyperjump` (§4.3). - A stop's coordinate MUST be verified against Bitcoin consensus for the selected network; implementations SHOULD treat stops with fewer than six confirmations as provisional and avoid them as destinations. --- ## 9. Geography of hyperspace (non-normative) ### 9.1 Why exits stay pinned The 2^85-per-axis cube is an ultrametric space: the cost of a move is set by the highest bit at which the endpoints differ, so targets become reachable by becoming numerous, never by becoming larger. About a million ports are a million random points in the cube; the nearest one to a random position is h78 away, and the interior between them is permanently wild. Hyperspace connects the neighbourhoods of stops. It does not, and cannot, connect arbitrary points. ### 9.2 Why Earth needed landfall Earth's radius is 2^55.6 Gibsons, so the cube that contains one Earth octant is h57 and Earth occupies 2^-85 of the volume. The expected number of ports inside it is 5 × 10^-20 at any block count. No rescaling helps: a random point lands within consumer reach `r` of Earth's surface only if `2 hE + r ≥ 232` (`hE` the radius in bits, `r` the reach in bits), and `hE ≤ 84`, `r ≈ 55`. Shrinking the Gibson puts stops inside Earth but makes Earth untraversable. The only geometric escape is a projection, a many-to-one map from the block to a point on the surface, which is what a landfall is. ### 9.3 Density, stations, and the three oceans Every plane-0 block lands, about half of all blocks: 476,604 landfalls at the time of writing, 33 km mean spacing, a new one every twenty minutes on average. From any point on land the nearest landfall is usually within 16 km, an h47 to h48 last mile on two axes (about three GPU-hours). Seventy-one percent of landfalls are at sea, which changes nothing about the last mile from land. The base mapping centers Earth at exactly 2^84, so the equatorial plane and the meridian planes at 0°/180° and ±90° are h85 boundaries. They behave like oceans: uncrossable on foot, crossed routinely by riding to a landfall on the far shore. Only points within one last mile of a plane, about 0.75% of the surface at current density, ever notice them. Greenwich puts one down the middle of London; others pass through Quito, Pontianak, Macapá, Memphis, New Orleans, and Fiji. Every landfall cell of Earth and every region of the void has a fixed station. Chain distance from that station is a personal map: for an identity whose station is a 2010 block, the newest landfalls are 800,000 stops away; for one whose station is a 2025 block, they are next door. New stops only ever appear at the far end of the line, so the frontier drifts away from everyone slowly, and the neighbourhoods of recent landfalls are the cheapest places from which to reach whatever Bitcoin opens next. ### 9.4 Miner steering A miner controls the merkle root almost for free (a new coinbase extranonce is a new root for about a dozen hashes), which is why a landfall position is taken from the block hash: steering a block hash costs one full block's expected mining work per bit, because the only way to choose it is to discard valid blocks. Ports remain merkle-root positioned; the same grinding buys a port placement at about h72 resolution, a cube 5.5 × 10^8 km wide, which threatens nothing. A miner can flip the root's plane bit and so choose whether a block lands or ports, but not where. ### 9.5 Exit concentration Every traveler through a stop disgorges at one exact coordinate. Stop neighbourhoods are the most trafficked and most observable places in cyberspace; arrival is the moment of least privacy. Content that wants to be found sits near a stop; content that wants to be left alone sits h60 or more inland. --- ## Appendix A: Why entry planes and axis lines cannot work (non-normative) Earlier drafts tried to make boarding cheap by enlarging the target: hop to a point on one of a port's three axis lines (two axes must match) or into a one-sector-thick plane through it (one axis must match, at sector resolution). Both fail for the same reason. - With `M` random ports, the expected nearest-target height is `85 - log2(M)/3` for points, `85 - log2(M)/2` for lines, and `85 - log2(M)` for planes. With `M ≈ 2^20` these are h78, h75, and h65. - Sector resolution does not help: sector bit `b` is Gibson bit `b + 30`, so a claimed h33 sector match is an h63 to h64 Gibson move, and plane thickness cancels out of the formula entirely (`(85 - T) - log2(M) + T = 85 - log2(M)`). - Consumer reach is about h55 per axis for a thousand dollars of hash work, and Bitcoin supplies about 2^20 targets, so points are 23 bits short, lines 20, and planes 10. The gap closes by about a bit every two to three years of hardware, not by any geometry. In an ultrametric space, targets become reachable by becoming numerous, never by becoming larger. Boarding therefore cannot be a place you travel to; it is a place you are assigned (§4). ## Appendix B: Relationship to earlier drafts - v1 (2026-02-28): exit at the merkle root; enter by hopping to it. Correct exits, unboardable entry. - v2 (2026-04-16): sector-plane entry (units error, see Appendix A); Cantor path tree over block heights as the ride proof (free in practice). - v3 (this document): plane-bit rule with landfalls; boarding from anywhere at a deterministic station; seeded per-block ride work with sampled verification; toll reserved. ## Appendix C: Reference implementations (non-normative) - **ONOSENDAI v2** (`arkin0x/ONOSENDAI`, merged 2026-08-25): full web client. Anchor and header-blob sync with embedded checkpoints, the §4.4 station lookup over the sorted line, boarding and rides with a persistent resumable worker pool, Level 1 verification, and a decimal landfall module reproducing the §1.2 golden vectors byte for byte. - **NTH** (`arkin0x/nth`): the anchor publisher (kind 321, §2.1 tags) and the header-blob packer with manifest and checkpoint emission. - **`decks/landfall-reference.py`** (this repository): stdlib Python §1.2 derivation; executing it checks all eight golden vectors. ## Example (non-normative) Anchor for a landfall (block 398): ```json { "kind": 321, "content": "Block 398", "tags": [ ["C", "56db6db6db6db6db6db6db3e27c436f9d3b79fb5fc6457798936b3e749e38f56"], ["M", "c056d48ae983586d78b51352c7d689db7c33acd286958301a5ec59e3a09ae016"], ["B", "398"], ["H", "000000002f7d702a27ccd65158740198f79d4ba1ddea8ab14b56b63a6289fe89"], ["P", ""], ["X", ""], ["Y", ""], ["Z", ""], ["S", "--"] ] } ``` Boarding, then riding from the station to block 398, then exiting: ```json {"kind": 3333, "tags": [["A", "enter-hyperspace"], ["e", "", "", "genesis"], ["e", "", "", "previous"], ["c", ""], ["C", ""], ["proof", ""], ["X", "..."], ["Y", "..."], ["Z", "..."], ["S", "..."]]} {"kind": 3333, "tags": [["A", "hyperjump"], ["e", "", "", "genesis"], ["e", "", "", "previous"], ["c", ""], ["C", "56db6db6db6db6db6db6db3e27c436f9d3b79fb5fc6457798936b3e749e38f56"], ["from_height", ""], ["B", "398"], ["as_of", ""], ["proof", ""], ["mp", "<32 inclusion paths>"], ["X", "..."], ["Y", "..."], ["Z", "..."], ["S", "..."]]} {"kind": 3333, "tags": [["A", "hop"], ["e", "", "", "genesis"], ["e", "", "", "previous"], ["c", "56db6db6db6db6db6db6db3e27c436f9d3b79fb5fc6457798936b3e749e38f56"], ["C", ""], ["proof", ""], ["X", "..."], ["Y", "..."], ["Z", "..."], ["S", "..."]]} ``` # DECK-0002: Virtual Spawn DECK: 0002 Title: Virtual Spawn Status: Draft Created: 2026-04-21 Last updated: 2026-04-21 Requires: `CYBERSPACE_V2.md` ## Abstract This DECK defines **virtual spawn**: a genesis action chain event where the spawn coordinate (C tag) does not equal pubkey. Unlike the standard spawn action, virtual spawn is not backed by proof-of-work. Its validity is determined entirely by application-level policy, not protocol enforcement. A virtual spawn is useful for identities who wish to spawn in a particular location in cyberspace in order to take advantage of local applications, such as game environments. ## Specification ### Virtual Spawn Definition (normative) A **virtual spawn** is a genesis movement event (kind 3333, A=spawn) where: - The `C` tag contains an arbitrary 256-bit coordinate denoting where the identity spawns into cyberspace. - The `C` value does NOT equal pubkey of the signing identity - The event is otherwise structurally valid per CYBERSPACE_V2.md §8.3 Virtual spawn events MAY include: - Application-specific tags for context or credentials ### Validation (normative) Virtual spawn events are syntactically valid. The protocol does not distinguish them from standard spawn at the event structural level. **Protocol layer**: The virtual spawn event and all actions in the chain following it are INVALID for the cyberspace protocol, as a virtual spawn is by definition a spawn with an invalid C tag. To any observer, a virtually spawned identity is "derezzed" and awaiting a valid respawn according to the cyberspace protocol. **Application layer**: Acceptance is entirely application-specific. Applications MAY: - Accept only virtual spawns meeting specific criteria (signed credentials, whitelisted identities, coordinate ranges, etc.) - Accept all virtual spawns - Reject all virtual spawns The application may benefit from the enforcement of other cyberspace protocol standards, such as proof-of-work based movement, location-based encryption, and uniform spatial structures. The virtual spawn is a clean way to enable identities to travel to the application's area of cyberspace without breaking the continuity of the protocol or forfeiting it's spatial structure. ## Consequences ### 1. No Universal Validity A virtual spawn has no guaranteed access to any part of Cyberspace. Each application decides whether to recognize the spawned identity at that coordinate. An identity with a virtual spawn can: - Publish movement events from that coordinate - Be referenced by other events An identity with a virtual spawn cannot assume: - Access to any application or service - Recognition by other identities - Any special relationship to the spawn coordinate ### 2. Application-Scoped Existence A virtually-spawned identity's presence in Cyberspace is meaningful only where applications choose to recognize it. A game may display the identity as a player avatar or NPC. A social app may hide it entirely. A service may require additional credentials before interaction. The identity persists as a keypair and event chain. Its *presence* — where it appears, what it can do — is determined by each application's policy. ### 4. No Scarcity or Cost Virtual spawn is cheap. Any identity can spawn at any coordinate without expending work. This enables: - Programmatic entities (NPCs, service agents, automated systems) - Arbitrary placement for application-specific purposes - Rapid iteration and experimentation This also enables: - Spam (unlimited spawns at unlimited coordinates) - Sybil attacks (one actor, many identities, many coordinates) - Coordinate squatting (claiming "desirable" locations without cost) Applications accepting virtual spawn MUST implement their own scarcity mechanisms if location is to remain meaningful within their context. ### 5. Standard Spawn Comparison | Property | Standard Spawn | Virtual Spawn | |----------|---------------|---------------| | C tag | pubkey | Arbitrary | | Proof-of-work | Required (Cantor tree) | Optional | | Protocol validity | Universal | invalid | | Application validity | Assumed unless rejected | Rejected unless accepted | | Thermodynamic backing | Yes | Optional | | Cyberspace Features | All | All Optional | ## Example (normative) ### Standard Spawn (for comparison) Identity spawns at their pubkey-derived coordinate. All tags required by §8.3 and §10 are included: ```json { "kind": 3333, "content": "", "tags": [ ["A", "spawn"], ["C", "e8ed3798c6ffebffa08501ac39e271662bfd160f688f94c45d692d876dd345a0"], ["X", "19088986011188665"], ["Y", "27231467915017080"], ["Z", "11846810334975873"], ["S", "19088986011188665-27231467915017080-11846810334975873"] ], "pubkey": "e8ed3798c6ffebffa08501ac39e271662bfd160f688f94c45d692d876dd345a0", "sig": "" } ``` Note: C = pubkey. Sector tags (X, Y, Z, S) are computed from the coordinate per §10. This spawn is universally valid under the protocol. ### Virtual Spawn Same identity spawns at an arbitrary coordinate (e.g., to enter a game environment at its designated spawn zone): ```json { "kind": 3333, "content": "", "tags": [ ["A", "spawn"], ["C", "a3f2b9c1d4e5f6789012345678901234567890123456789012345678908d91a0"], ["X", "17608966908225142"], ["Y", "23932066471776953"], ["Z", "23385833927348309"], ["S", "17608966908225142-23932066471776953-23385833927348309"], ["app", "forge-world"], ["meta", "player-spawn-zone-alpha"] ], "pubkey": "e8ed3798c6ffebffa08501ac39e271662bfd160f688f94c45d692d876dd345a0", "sig": "" } ``` Note: C ≠ pubkey. Sector tags are still computed from the arbitrary C coordinate. This spawn is **invalid under the cyberspace protocol** — the identity is derezzed and awaiting a valid respawn. However, the "forge-world" application may still recognize this spawn and allow the identity to interact within its environment. ## Security Considerations ### Trust and Verification Applications accepting virtual spawn should define: - Which identities are trusted to create virtual spawns - What credentials or attestations are required - Rate limits (max spawns per identity, per time window) - Coordinate restrictions (allowed zones, reserved areas) ### Abuse Mitigation Without restrictions, virtual spawn enables: - Flooding (millions of spawns across coordinate space) - Impersonation (spawning at coordinates associated with legitimate services) - Confusion (users cannot distinguish standard vs. virtual spawn without inspection) Mitigations are application-layer: - Reputation systems - Verification registries - User-facing indicators (show spawn type, show verification status) - Reporting and takedown procedures ## Open Questions 1. **Should virtual spawn use a distinct action tag?** (e.g., `A=virtual-spawn` vs `A=spawn`) - Makes spawn type immediately visible - Adds complexity to client handling 2. **Should there be a protocol-level indicator?** (e.g., `virt` tag) - Enables filtering without computing pubkey - Or keep this purely verification-layer (client computes and compares) 3. **Relationship to future domain rules?** - Can domain owners restrict virtual spawns within their domain? - Can domain owners create virtual spawns without restriction? ## Implementation Guidance For applications accepting virtual spawn: 1. **Verify spawn type**: Compare pubkey to C tag. Mismatch = virtual spawn. 2. **Define acceptance policy**: What virtual spawns are accepted? What credentials required? 3. **Communicate to users**: Show spawn type, verification status, any relevant credentials. 4. **Implement rate limiting**: Prevent spam via per-identity, per-time, or per-coordinate limits. 5. **Plan for abuse**: Takedown procedures, blacklisting, user reporting. # DECK-0003: SNO (Simple Nostr Objects) DECK: 0003 Title: SNO (Simple Nostr Objects) Status: Draft Created: 2026-09-14 Last updated: 2026-09-24 Requires: `CYBERSPACE_V2.md` (spec version `2026-03-16-h34-corrected`) ## Abstract A **Simple Nostr Object** (SNO) is a small three-dimensional object written as JSON, small enough to live inside one nostr event alongside everything else a note carries. It is a list of vertices on an integer lattice, one color per vertex, an optional list of triangles, and a word saying how to draw it. There are no textures, no materials, no normals (a face's winding says which way it looks), no bones, no animation, and no file to fetch. An object is the event. This document codifies a format Cyberspace already depends on but has never written down. The base specification refers to it twice: §7 notes that ONOSENDAI hides `kind 3330` shards whose geometry is in `content`, and §8.10 computes the proof of work an avatar owes from that payload's `unit`, `vertices`, `ticks` and `faces` fields. Both passages assume a format defined nowhere. This DECK is that definition, written so that it stands on its own outside Cyberspace as well as inside it. The design goal is not to compete with glTF or USD. It is to be the three-dimensional equivalent of a text note: something any developer can parse in an afternoon with no library, read with their eyes, diff in a pull request, and render with thirty lines of code. Where a real asset pipeline is needed, SNO is the wrong tool and says so. **Defined here:** | Thing | Value | |---|---| | Object payload | JSON, described in §1 | | Event kind for a standalone object | `33331`, addressable, one per author per `d` (§3.1) | | Event kind for an object hidden in a bag | `3330` inline, as Cyberspace already uses it; by reference, the object's own `33331` event, named by a reference tag (§3.2, §3.4) | | Model space | X right, Y up, +Z toward the viewer: right handed, the glTF convention (§2) | | Position lattice | whole units plus 120ths of a unit (§1.2) | | Size | no ceiling on vertices or faces; the event's size is the relay's concern, as for every other kind (§1.8) | | Reference implementation | `decks/sno-reference.py`, which is §1.9 written as code | **Conformance.** `decks/sno-reference.py` implements §1.9 with no dependencies and carries a rejection case for every numbered rule that rejects. It was checked against ONOSENDAI's independent TypeScript reader, and the two agree on every case, including the three places where this format is deliberately forgiving rather than strict. A third implementation can be checked against the same table. --- ## Terms - **Object:** one SNO payload. A single connected budget of vertices, colors and faces; not a scene and not a hierarchy. - **Model unit:** the lattice spacing. One model unit is `2^unit` of the application's base unit (§1.6). In Cyberspace the base unit is the gibson. - **Tick:** one 120th of a model unit. The sub-unit part of a position (§1.2). - **Vertex:** a point, its position given as whole units plus ticks, carrying one RGB color. - **Face:** a triangle, three vertex indices. - **Mode:** which of the three drawings of the same vertex list a client should produce: `solid`, `points` or `lines` (§1.5). - **Extent:** the half-width of the grid the object was built on, in model units. A bound, not a size (§1.8). --- ## 1. The object (normative) ### 1.1 Payload An object is a JSON object. Fields marked required MUST be present; a reader MUST reject a payload that is missing one or whose value has the wrong shape. | Field | Type | Required | Meaning | |---|---|---|---| | `v` | integer | yes | Format version, `1` or `2`. They differ in one sign and nothing else (§2). A reader MUST support both and MUST reject any other value. | | `type` | string | no | A legacy field. A reader MUST ignore it wherever it appears, and MUST NOT require it or reject on its value. A publisher SHOULD NOT write it (§1.1a). | | `name` | string | yes | A name for humans. A reader MUST truncate to 64 characters. | | `unit` | integer | yes | Scale exponent, `0` to `84`. One model unit is `2^unit` base units (§1.6). | | `extent` | integer | no | Grid half-width in model units, `1` to `64`. Advisory and self-repairing (§1.8). Absent or malformed means `8`. | | `mode` | string | yes | `"solid"`, `"points"` or `"lines"` (§1.5). A reader MUST reject any other value. | | `vertices` | array | yes | One `[x, y, z]` triple of **whole units** per vertex (§1.2). | | `ticks` | array | no | The sub-unit part of each position (§1.2). Absent means every position is whole. | | `colors` | array | yes | One palette index per vertex, parallel to `vertices` (§1.3). | | `faces` | array | yes | Triangles as `[a, b, c]` vertex indices (§1.4). MAY be empty. | | `facecolors` | array | no | One palette index per face, run-length encoded, for hard color seams (§1.4a). Absent means every face interpolates its vertices. | | `palette` | string or array | no | Which 256 colors the indices name (§1.3a). Absent means the built-in. | | `up` | boolean | no | `true` means the object stands on the Earth's surface where it is placed (§1.7). `false` means the same as absent. | | `spin` | integer | no | With `up`: the compass bearing the object's `+Z` faces, `0` to `359` (§1.7). | | `refs` | array | no | Other objects this one places, each named the way a nostr tag names an event (§1.10). Absent means the object places nothing. | | `parts` | array | no | Where each placed object stands, turns and scales, one entry per placement (§1.10). Absent means the same. | `vertices` and `colors` MUST have the same length. Any field not listed here MUST be ignored by a reader, not rejected (§5). ### 1.1a Why there is no `type` Payloads written before this document carry `type: "shard"`. This format has no such field, and that is worth a paragraph, because dropping a field that existing writers emit looks like carelessness and is the opposite. The field never said anything the container did not already say. A standalone object is `kind 33331` and an object in a bag is a `kind 3330` item; either way the kind is what a reader dispatches on, and Cyberspace's own client derives "this is a shape, not a message" from the kind and has only ever used `type` as a sanity check on a blob it had already decided was a payload. What the field did do was carry a parochial word into a format meant for anyone. "Shard" is Cyberspace's name for an object hidden at a place. It is a good word there and it means nothing in a format called Simple Nostr Objects, and a field whose only legal value is another project's vocabulary is exactly what makes a format look like somebody's internal file that escaped. So there is no `type` in either version, no reader may require one, and no reader may reject on its value. A publisher should not write it. Nothing rejects a payload that carries one, because the field is noise and rejecting on noise would break every object already published. ### 1.2 Positions: the lattice and the ticks A position is an exact rational, never a float. It is carried in two parts. `vertices[i]` is the **whole units** part: three integers, the floor of the position on each axis. `ticks[i]` is the **remainder**: three integers `0` to `119`, the position's fractional part in 120ths of a unit. The position on an axis is therefore `vertices[i][a] + ticks[i][a] / 120` model units, exactly. `ticks` is run-length encoded, because most objects are built on whole units and a long list of `[0, 0, 0]` is waste. Each entry is either a triple, standing for one vertex, or a **negative integer** `-N`, standing for `N` consecutive vertices whose remainder is `[0, 0, 0]`. The entries, expanded, MUST produce exactly one remainder per vertex; a reader MUST reject a `ticks` array that expands to any other length. `0` and positive integers are not valid entries. **Why 120.** It is the smallest number divisible by every integer from 1 to 6, and it also divides by 8, 10, 12, 15, 20, 24, 30, 40 and 60. Halves, thirds, quarters, fifths, sixths, eighths and tenths of a unit all land exactly on the lattice, so a modeler snapping to any ordinary fraction never accumulates error, in the same way that 360 degrees was chosen for a circle. A power of two would have made thirds and fifths impossible. **Why integers at all.** A vertex is at a coordinate, not near one. Two objects built to meet at a shared edge meet exactly, on every implementation, forever, with no dependence on floating point rounding or on the order the file was written in. **The float hazard (normative).** One 120th is not exactly representable in binary floating point. Two readers that compute `whole + remainder / 120.0`, one in 32-bit and one in 64-bit, will disagree in the last bits. Therefore any operation that depends on exact coincidence, which is at least welding, deduplication, equality, sorting and hashing, MUST be performed on the integer pair before any conversion to a float. A reader that welds on derived floats has given away the one guarantee the lattice makes. **Canonical form (normative).** There is exactly one way to write a given object, because a format that allows three spellings of the same thing gets three incompatible readers. A publisher MUST omit `ticks` entirely when every remainder is zero, MUST write a run of two or more zero remainders as a single negative integer rather than as repeated triples, and MUST omit `extent` when it is `8`. A reader MUST accept all equivalent spellings anyway, because it will meet them. ### 1.3 Colors `colors[i]` is a single integer: an index into the object's palette (§1.3a). There is one per vertex, parallel to `vertices`. A reader MUST reject an index that is not an integer, is negative, or is not less than the palette's length. **In a `v: 1` payload `colors[i]` is a literal `[r, g, b]` triple of numbers from `0` to `1`,** clamped on read, with no palette involved. Version 1 predates the palette and every object written before this document is one of them, so this is not a compatibility shim but what version 1 has always meant, alongside the Z flip of §2. A reader MUST take a `v: 1` color exactly as written rather than snapping it to the nearest palette entry: snapping on read would change objects nobody asked to change. It snaps when it is next published, which is when it becomes a `v: 2` payload. A publisher MUST NOT write triples. There is one color per vertex and, apart from `facecolors` (§1.4a), no other color anywhere in the format. A face with no color of its own is painted by interpolating its three vertices; a line is painted by interpolating along its length; a point is its own color. **Interpolation happens after the lookup**, between the two resolved colors, so indexing costs nothing in smoothness: a gradient across a triangle is as continuous as it ever was, and the index is only how its endpoints are named. **Why an index and not three numbers.** Color was the largest cost in this format by a wide margin and it was buying nothing. Three numbers at four decimal places is 21 bytes of the 52 a worst-case vertex costs; an index is 4. Measured over a whole serialized event at the format's ceiling, with a color on every face, that is 62.6 KB against 33.5 KB, on a wire whose tightest common limit is 65,536 bytes (§1.8). Face colors were unaffordable and are now nearly free. Nothing is lost visually. A palette entry is eight-bit-per-channel color, which is what a screen shows and what PLY, PNG and every common exchange format store. What is given up is an object using more than 256 distinct colors at once, and an object that needs more than 256 distinct colors is not the kind of object this format is for. | | bytes per vertex | 512 vertices, 1024 faces | with a color on every face | |---|---|---|---| | three numbers at full precision | 87 | 57.1 KB | 115.1 KB, over every relay | | three numbers at four decimals | 52 | 39.6 KB | 62.6 KB | | **a palette index** | **32** | **29.9 KB** | **33.5 KB** | At 32 bytes a vertex only about 4 are the color, so this is the last large saving available in color. Anything further would have to change how positions are encoded. ### 1.3a The palette `palette` says which 256 colors an object's indices refer to. | `palette` | Meaning | |---|---| | absent | the built-in, `cyberspace-neon-256` (Appendix C) | | a registered name | `"cyberspace-neon-256"` is the only one, and is the default | | an `nevent1…` or `naddr1…` | a palette published as its own nostr event, fetched if it can be, with the built-in standing in until it is (§1.3b) | | an array | a palette carried in the object itself: 2 to 256 entries, each `[r, g, b]` of three integers `0` to `255` | A reader MUST reject a `palette` that is an array of fewer than 2 or more than 256 entries, or any entry that is not three integers in `0..255`, or a string that is neither a name it knows nor a well-formed `nevent` or `naddr`. **Why a custom palette may be short.** An object with four colors pays for four, not for 256. Two entries cost 40 bytes, sixteen cost 222, and a full 256 costs 3.3 KB, so the cost lands where it is affordable: 3.3 KB is four times the size of a cube and a sixth of an object at the ceiling, and it is large objects that want a palette of their own. ### 1.3b A palette published as its own event A `palette` of `nevent1…` or `naddr1…` names a nostr event that carries a palette. This is how a group of objects share one set of colors, and how this format meets the palettes that already exist on nostr rather than insisting everyone reuse its own. **The rule that makes this safe is that a reference is never load-bearing.** 1. A reader MUST render the object without waiting for anything. Until the referenced event is in hand, the indices name the built-in. 2. A reader MAY fetch the event, and SHOULD if it can do so without blocking the first frame. When it arrives and it parses, the reader re-renders with it. 3. A reader that cannot fetch, or fetches an event that is not a palette, MUST keep drawing with the built-in and MUST NOT reject the object. So the worst case is an object drawn in the wrong colors, never an object that cannot be drawn. That is a real cost and it is stated here rather than buried: a viewer has no way to tell that the colors it sees are the fallback rather than the author's. A publisher who cannot accept that carries the palette inline, which is what the array form is for, and which is what a small palette should do anyway. **A palette event carries its colors in `c` tags, one tag per color, and the order of the tags is the index.** A publisher MUST write the colors that way and MUST NOT write a second machine-readable copy of them anywhere else in the event. `c` tag `n`, counting from zero in the order the tags appear in `tags`, is the color that index `n` names. A value is `#rrggbb`. Two to 256 of them. Everything else in the event is for people. `name` is what the author calls the palette, `alt` is NIP-31's line for a client that cannot render it, `client` says what published it, and `content` may hold whatever a color-moment client would want there, an emoji or a sentence, or nothing at all. None of it is read for colors. **What counts as a palette event (normative).** Given an event it has fetched, a reader MUST take the first of these that succeeds. 1. **The `c` tags**, in the order they appear in `tags`, if there are 2 to 256 of them and every value is a well-formed `#rrggbb`. Their order is the palette's order. 2. Otherwise, **`content`**, if it parses as a JSON array of 2 to 256 entries where every entry is either `[r, g, b]` of three integers `0..255` or a `"#rrggbb"` string. This is a legacy form and a reader accepts it only because an earlier draft of this section described it; nothing SHOULD write it now. 3. Otherwise the event is not a palette event, which counts as a failed fetch, which means the built-in. The kind is deliberately not constrained. This format does not define a palette kind and does not want one: palettes on nostr are somebody else's problem and partly solved already, and a reader that accepts the shape above will read whatever convention wins without this document being revised. `kind 3367`, the color-moment convention, is what carries them today. **Why the tags are the encoding and not `content`.** Three reasons, in the order they should be weighed. 1. **A single-letter tag is indexed by relays and `content` is not.** With the colors in `c` tags, `{"#c": ["#FF0000"]}` is a filter that finds every palette containing pure red, on any relay, with no new index and no new kind. A palette in `content` is opaque to every relay that stores it and can only be found by fetching it first. Nothing else in this section buys a capability that did not exist before; this does. 2. **One spelling.** The canonical form rule of §1.2 exists because a format that allows two spellings of one thing gets two incompatible readers, and colors in both the tags and the content would have been exactly that: the same information twice, with a rule needed to say which copy wins when they disagree. 3. **It is what the network already publishes,** which means an SNO palette is a color moment and a color moment is an SNO palette, with no translation and no second audience to write for. Size does not decide this and should not be read as if it did. A 256-color palette is 4,618 bytes as tags with an empty content, against 3,691 for the dual form that was considered and rejected here, six tags as a preview with a JSON copy of the whole palette in the content, and no relay surveyed advertises a tag limit anywhere near 260 tags: of nos.lol, relay.damus.io, relay.primal.net, relay.nostr.band, ditto.pub and cyberspace.nostr1.com, only cyberspace.nostr1.com advertises `max_event_tags` at all, at 10,000. **What the survey found, and what this section used to say.** Until 2026-09-16 this section said that a palette event's colors were a JSON array in `content`, and that the reference was an `naddr`. Both were guesses, made before anyone had looked at a real one, and both were wrong. A survey of five relays for `kind 3367` returned 205 unique events from 51 pubkeys, and every one of them carries its colors as `c` tags, one tag per color, in document order, with an emoji or a short note in `content`. All 661 `c` values are well-formed `#rrggbb` and none is malformed. Alongside them are `layout`, `alt` and `client` on all 205, `name` on 152 and `g` on 33. The palettes are small: 178 carry three colors, 14 carry four, 7 carry five, 6 carry six. So the content-only rule would have rejected every palette on the network, which is the opposite of meeting palettes that already exist. Kind 3367 is also in NIP-01's regular range of 1000 to 9999, which is stored and immutable and has no address, so the `naddr` this section asked for could not have named one of these events even in principle. **Editing a palette publishes a new event.** A regular event cannot be replaced, so a corrected palette is a second event rather than a new version of the first, and the link back is carried in `e` tags. A publisher correcting a palette MUST write `["e", "", "", "previous"]`, and where it knows the first version of the palette SHOULD also write `["e", "", "", "genesis"]`. Those two marker words are the ones Cyberspace's own action chain uses, so an implementer who has read anything else in this repository already knows what they mean. Together they make a history a reader MAY walk backward: `previous` gets the step before, `genesis` gets the start without walking at all. **A reference is pinned to the event it names.** A reader MUST NOT follow the `e` chain forward to a newer version, and MUST render the object with the event the object names. This is the part that is better than the addressable form this section proposed before, and it is worth saying why rather than leaving it as a consequence of the kind. An immutable event means an object renders the same forever, and an author who corrects a palette cannot silently repaint every object that ever named it, including objects belonging to people they have never met. The cost is that an object does not pick up a correction on its own: it picks one up when its own author republishes it pointing at the new event, which is a deliberate act by the one person entitled to change how that object looks. **Why an event id rather than an address.** An `nevent` carries the id and relay hints, so a reader has somewhere to look, and it names one immutable event, which is what the pinning rule above needs. An `naddr` is still accepted, for a palette somebody publishes as an addressable event of their own; once the event is fetched the shape rules above apply to either, and the pinning rule applies to whatever the reader has in hand. What an `naddr` cannot do is name a `kind 3367`, which is where the palettes are. **The built-in is not a compromise default.** It is 24 hues of 8 steps, then 32 steels, then 32 signature colors, laid out so that index arithmetic is legible: `hue * 8 + step` for the first 192. The ramps are generated in OKLCH, which spaces them by how different they look rather than by their numbers, and clipped into sRGB by lowering chroma rather than clamping channels, which is what keeps the bright end from turning to mud. Appendix C carries the whole list. ### 1.4 Faces `faces[i]` is `[a, b, c]`, three integers indexing `vertices`. Every index MUST be at least `0` and less than the vertex count, and the three MUST be distinct. A reader MUST reject a payload containing any face that fails either test, because a face pointing at a vertex that does not exist is a crash in most renderers, far from anything that could explain it. **Winding order is a face's front.** For a face `[a, b, c]` the front is the side that the normal `(b - a) × (c - a)` points to, computed on the positions in the frame of §2 (after the version 1 negation, for a version 1 object). Seen from the front, the corners run counter-clockwise, which is the glTF, three.js, OpenGL and Blender convention, so an exporter from any of them writes its faces in the order it already holds them. This is how an object says which way each face looks, at a cost of zero bytes: the order of three indices is on the wire whatever it means. A reader MUST take a face's front from its winding and MUST NOT infer it: not from the object's shape, not from where the origin sits, not from the side the viewer is on. Inference cannot be right for an open sheet, which has no inside to point away from, and two readers that infer differently show one object as two. A reader MUST still draw both sides of every triangle and MUST NOT cull a face on the basis of its winding. It MAY draw a back darker than a front, and a reader that lights an object SHOULD, so that an author can see a face that looks the wrong way. Culling is refused because an object is often an open shell, and culling would make it vanish from behind. This section was, until 2026-09-24, the opposite rule: winding carried no meaning and a face had no front. That was chosen so that no author would ever have to think about winding. It was reversed because readers that light objects had to guess each face's front, and a guess that is wrong for a flat plate turned half of a published object dark on one reader and not on another. Authoring tools carry the burden instead: they SHOULD wind new faces outward by default and SHOULD give the author a way to turn a face round. This is stated because leaving it unsaid is the most common way a small format fails. STL left color unspecified and two vendors filled the hole incompatibly; PLY never registered its property names and cost the ecosystem years of colors that did not import; Niantic's SPZ shipped in 2024 without saying which axis is up and someone had to file an issue to ask. `faces` MAY be empty. An object with no faces is a point cloud or a polyline, depending on `mode`. ### 1.4a Face colors, and hard seams A face with no color of its own takes one by interpolating its three vertices, which is a gradient across the triangle. That is the right default for a lit, rounded object and the wrong one for the blocky work this format suits best, and until now the only way to get a flat triangle was to give all three of its vertices the same color. Two flat triangles meeting at an edge then need six vertices where the geometry needs four, and a flat-shaded 500-triangle object spends 1,500 vertices to express 500 colors: three times over the budget of §1.8. The format made its own best style its most expensive. `facecolors` is one entry per face, in the order `faces` gives them. **When a face has a color, that color fills the whole triangle** and its vertices contribute nothing to the fill. Vertex colors are untouched and still color the points and the lines (§1.5), so an object may carry both without contradiction and `points` and `lines` mode behave exactly as they always did. **Run-length, as `ticks` does it (§1.2), because the common case is a run.** A stamped block is twelve triangles of one color. An entry is either a **palette index**, a non-negative integer read exactly as §1.3 reads one, or a **negative integer** `-N` standing for N further faces of the index before it. A solid cube whose color is index 7 is therefore `[7, -11]`. The first entry MUST be an index, since a run has nothing to repeat before one. The entries, expanded, MUST produce exactly one color per face; a reader MUST reject a `facecolors` array that expands to any other length. The sign is what separates the two kinds of entry, and it works because an index is never negative. This is the same trick `ticks` uses and it is why color had to become a single number before face colors could be affordable: a list of triples interleaved with run markers cost more than it saved. The canonical form of §1.2 extends unchanged: a publisher MUST write a run of two or more as the shorthand, so there is still exactly one way to write a given object. **What this does not do, deliberately.** It gives a hard seam between faces, not a gradient inside a face with a hard edge along one of its sides. That would need a color per face corner, three per face, which triples the color data to buy a case this format's audience does not have. Lines keep interpolating along their length; per-edge color would be a third mechanism and is not worth one. ### 1.5 Mode `mode` says which drawing of the vertex list is the intended one. All three are drawings of the same data, so mode is a property of the object and not of the viewer. | Mode | Drawing | |---|---| | `solid` | the triangles in `faces`, colors interpolated across each face | | `points` | the vertices, each at its own color | | `lines` | the edges of the faces, each drawn once, colors interpolated along each edge; with no faces, one polyline through the vertices in order | A client SHOULD also draw the vertices as points in every mode, so that an object remains visible when it is smaller on screen than a triangle. ### 1.6 Scale `unit` is an exponent, not a size. One model unit is `2^unit` base units, and the application decides what a base unit is. In Cyberspace a base unit is one gibson, the smallest addressable length, so a `unit` of `0` builds on a lattice one gibson wide and a `unit` of `33` builds on a lattice of meters. This is why the same geometry serves a trinket and a monument: an object is written once and scaled by changing one integer, with no re-quantization and no loss. An application outside Cyberspace that has no natural base unit MAY treat `unit` as a relative scale only, and SHOULD render an object at a size that suits its context rather than refusing it. ### 1.7 Standing on Earth `up`, when present, MUST be a boolean; a reader MUST reject any other type. `true` means the object is oriented against the surface of the Earth at the point where it is placed: the object's `+Y` axis points along the geodetic normal (local up), its `+X` points local east, and its `+Z` points local north. Absent, an object's axes are the application's axes. `spin` is a whole number from `0` to `359`: the compass bearing the object's `+Z` faces, measured clockwise from north as seen from above. `0` faces north, `90` east, `180` south, `270` west. Absent with `up` true means `0`. A reader MUST reject a `spin` that is present and is not an integer in range, whether or not `up` is present, because a payload carrying a nonsense bearing is malformed even where the bearing is unused. A reader MUST ignore the value of `spin` when `up` is not `true`. The frame is right-handed in Cyberspace's axes. This is not obvious and is worth stating: the canonical GPS mapping (`CYBERSPACE_V2.md` §9.4) swaps the ECEF Y and Z axes, which mirrors the frame, so (east, up, north) comes out right-handed in Cyberspace where it would be left-handed in ECEF. A reader that does not implement `up` draws the object in the application's axes and is not wrong, only unoriented. ### 1.8 Limits | Limit | Value | Why | |---|---|---| | vertices, faces, refs, parts | **none** | the size of an event is the relay's concern, as it is for every other kind of event; see below | | nesting depth | 4 | how far a reader follows placements (§1.10). A bound on the reader's work, not on the object | | `extent` | 1 to 64 model units | a hint at the lattice size, so a reader can size a grid before it reads the data | | `unit` | 0 to 84 | the Cyberspace address space is `2^85` gibsons on a side | | `name` | 64 characters | truncated, not rejected | **There is no ceiling on vertices or faces.** A reader MUST NOT reject a payload for its vertex or face count. Until 2026-09-24 this format set 512 vertices and 1,024 faces, sized so that the worst object it permitted fit a stock relay. It was removed because no other event kind bounds its own content: a long-form article, a file header, a wiki page are as large as their author makes them, and the relay that stores an event decides what it will take. That is a different class of problem from the one a format solves, and a format that solves it anyway puts a number between the author and the thing they were making. The number did that: a 129-square floor pasted square by square reached 516 vertices, deployed, and was opened as nothing by every reader (arkinox, 2026-09-24), and a figure or a vehicle at ordinary low-polygon detail needs 600 to 1,000 and was impossible however well it was built. What replaces the ceiling is a publisher who knows what things cost and a container that stops multiplying: an object of any size can have an event of its own (§3.1, §3.4), so a bag, which is one event shared by everything hidden at a place, need never grow with the objects it names. The rest of this section is the arithmetic, so a publisher can plan against the relays it uses; none of it is a rule. `extent` is **advisory and self-repairing**, which is the one place this format is deliberately forgiving. A reader MUST substitute the default of `8` for an `extent` that is absent, not an integer, or outside `1..64`, and MUST then grow it until it contains every vertex. An object is therefore never rejected for disagreeing with its own extent; the data wins and the hint is corrected. This is what lets a modeling tool write geometry first and a bounding hint second without the two ever contradicting. The position bound is stated as an obligation on publishers rather than readers: a publisher MUST NOT write a vertex further than `64` model units (`7680` ticks) from the origin on any axis. A reader MAY reject such a payload and MAY instead repair it by growing the extent. ONOSENDAI currently repairs. §8 records the consequence. **What things cost (non-normative).** strfry's stock `events.maxEventSize` is 65,536 bytes, and strfry is the most deployed relay software in the network; a relay's operator may raise it, and the ones this world runs on have (below). What a full serialized event costs at 512 vertices and 1,024 faces, the format's former ceiling and still a useful yardstick, worst case throughout, with coordinates at the extent bound and every field at its most expensive: | At 512 vertices and 1024 faces | No `facecolors` | With `facecolors` | |---|---|---| | **palette indices, the built-in (§1.3)** | **29.9 KB** | **33.5 KB** | | plus a full 256-entry custom palette | 33.1 KB | 36.7 KB | Both fit with room to spare, and face colors now cost 3.6 KB rather than doubling the object. Two earlier drafts of this section did not: three numbers per color at four decimal places reached 62.6 KB with face colors, and at full double precision 115.1 KB, which is over every relay in the network. §1.3 carries that comparison, because it is the whole reason color is an index. The cost is now dominated by geometry rather than color. Worst case, a vertex costs 32 bytes, of which about 4 are its color; a face costs 13, and a face that carries its own color 17. A publisher that wants the largest possible object spends its budget on vertices. A real object is far below that, because the worst case above assumes every position needs its sub-unit part written out and every face carries a color of its own. An object built on whole units, which is what the lattice is for, compresses its `ticks` to a single number and its `facecolors` to one run: | A typical object, whole units, the built-in palette | Full event, serialized | |---|---| | a coloured cube, 8 vertices | about 730 bytes | | 64 vertices, 128 faces | 2.5 KB | | 256 vertices, 512 faces | 9.4 KB | | **512 vertices and 1024 faces** | **18.9 KB** | One color per face adds almost nothing to these, because a run of identical faces is two numbers whatever its length. These numbers are the whole argument for the change in §1.3: the same object that cost 29.4 KB with three numbers per color costs 18.9 KB with an index, and the ceiling that fits a 56 KB budget with a color on every face moved from 448 vertices to 832. Two facts make this margin more comfortable than it looks. Escaping the payload into a JSON string costs about 12 bytes, under 0.05%, because the content is almost entirely integers, so the fear that nesting JSON inside JSON is wasteful does not apply here. And the geometry belongs in `content` rather than tags: strfry caps a tag value at 1,024 bytes and the tag count at 2,000, while content is roomy everywhere. **The relays in use, measured 2026-09-24** (NIP-11 `max_message_length`): cyberspace.nostr1.com **262,200** bytes; nos.lol 131,072; relay.primal.net and relay.damus.io 1,000,000. Worst case a vertex costs 32 bytes and a face 13, or 17 with a color of its own; a typical whole-unit object costs about a third of that. So the world's own relay takes a worst-case object of about 3,500 vertices, or a typical one of about 10,000, standing alone; a stock relay takes about 800 worst case. A publisher who knows its relays can size to them; one who does not should publish and read the refusal, which names the reason. **A bag is the one place size still multiplies.** Everything hidden at one place by one author travels in one event, encrypted and base64'd, so three large objects inline are three times the size and the bag fails before any object would alone. §3.2 and §3.4 give every object the option of an event of its own; a publisher SHOULD take it for anything large, and the bag then stays small however much stands at the place. One trap worth knowing, since it cannot be discovered at runtime: strfry populates NIP-11's advertised `max_message_length` from its WebSocket frame cap, not from `events.maxEventSize`, and never advertises the latter. A relay advertising a one megabyte limit may still reject a 70 KB event. Do not design against advertised numbers; stay under the ceiling and handle the rejection message. ### 1.9 Validation A reader MUST perform all of the following before rendering, and MUST reject the whole payload if any fails. A partially valid object is not rendered partially: a face index pointing past the end of the vertex list is not a defect that degrades gracefully. 1. `v` is `1` or `2`. A `type` field, if present, is ignored and never rejected on (§1.1a). 2. `vertices`, `colors` and `faces` are arrays, and `vertices.length === colors.length`. 3. There is no bound on `vertices.length` or `faces.length` (§1.8). The number is kept so the rules below keep their names; until 2026-09-24 it read `512` and `1024`. 4. `mode` is one of the three words. 5. `unit` is an integer in `0..84`. 6. Every vertex triple is three integers. 7. `ticks`, if present, expands to exactly one remainder per vertex, and every remainder component is an integer in `0..119`. 8. Every face is three distinct integers in `0..vertices.length - 1`. 8a. `palette`, if present, is the name of a known built-in, a well-formed `nevent` or `naddr`, or an array of 2 to 256 entries of three integers `0..255`. A reference that has not been resolved counts as the built-in for the rules below, and is never a reason to reject (§1.3b). 8b. in a `v: 2` payload, every entry of `colors` is an integer from `0` to one less than the palette's length; in a `v: 1` payload, every entry is three numbers (§1.3). 8c. `facecolors`, if present, expands to exactly one index per face, its first entry is an index, every run entry is a negative integer, and every index is in range. 9. `extent`, if present, is repaired rather than validated (§1.8): out of range becomes `8`, then it grows to contain the data. 10. `up`, if present, is a boolean; `spin`, if present, is an integer in `0..359`. 11. `refs`, if present, is an array whose entries are each `["e", <64 lowercase hex>]` or `["a", "33331:<64 lowercase hex>:"]`, optionally followed by one relay URL (§1.10). 12. `parts`, if present, is an array whose entries are each eight integers: a `refs` index in range, three tick offsets each in `-7680..7680`, three whole degrees each in `0..359`, and a scale step such that the placed object's unit stays in `0..84` (§1.10). `parts` without `refs`, or a `refs` index past the end, is a reason to reject. 13. A payload with `parts` and no `vertices` of its own is valid: an object may be nothing but the arrangement of others (§1.10). --- ### 1.10 Parts: one object placing others **An object MAY place other objects.** `refs` names them; `parts` says where each stands. A reader that supports this section draws the parent's own geometry and then each placed object at its placement; a reader that does not, or that cannot fetch a placed object, draws the parent's own geometry and a placeholder where each part would stand, and MUST NOT reject the parent for it. **`refs` is a list of nostr references, written the way a tag writes them.** ```json "refs": [ ["a", "33331::", "wss://relay.example"], ["e", ""] ] ``` An `a` reference names an addressable object (§3.1) and follows its author's newest version: fix the tile and every floor built from it changes. An `e` reference names one event, as a palette reference does (§1.3b), but an object is addressable, and NIP-01 lets a relay discard every version but the newest; most do. An `e` to a version its author has since replaced may therefore find nothing and be drawn as a placeholder. A publisher SHOULD write `a`. An author who wants a build no one else can change copies the object, publishes the copy under their own key, and places that. A reader MUST still read both shapes. The relay URL is a hint, as in NIP-01, and MAY be omitted. `refs` names each object once; a reader MUST reject a payload whose `refs` entry is not one of the two shapes above. **`parts` is a list of placements, eight integers each.** ```json "parts": [ [0, 0, 0, 0, 0, 0, 0, 0], [0, 240, 0, 0, 0, 90, 0, 0], [1, 120, 0, 120, 0, 0, 0, -1] ] ``` | Position | Meaning | |---|---| | 0 | index into `refs`: which object | | 1, 2, 3 | where its origin stands, in the parent's **ticks** (§1.2), so `120` is one whole unit; each in `-7680..7680`, the 64-unit position bound of §1.8 | | 4, 5, 6 | its turn about its own origin, whole degrees `0..359` about the parent's X, then Y, then Z axes, in that order, in the right-handed frame of §2 | | 7 | scale step: the placed object is drawn at `2^(its unit + step)` base units per model unit. `0` keeps its own size; `1` doubles it; `-1` halves it. The result MUST stay in `0..84` | Positions are ticks and turns are whole degrees because everything else in this format is an integer and the canonical form of §1.2 depends on it. Multiples of `90` keep a placed object on the lattice exactly; other angles are permitted and are drawn as the renderer's floating point allows, which is what §4 already says of ticks. The scale is a power of two because `unit` is (§1.6): a step is exact, a free multiplier is not. **Why the placements are in the payload and not in the event's tags.** A copy of the payload is the whole object, on the clipboard, in a bag, in another client; placements carried as tags would fall off it. Inside a bag the tags are encrypted with everything else, so nothing would be gained there. A publisher of a public `kind 33331` SHOULD also write each `refs` entry as an `e` or `a` tag on the event, unchanged, so a relay can answer "what places this object" (`#a`, `#e`); a reader MUST take the placements from the payload and MUST NOT read them from tags. **What a placed object keeps and what it loses.** It keeps its own `palette`, `mode`, `facecolors` and geometry: it is drawn as its author drew it. It loses `up` and `spin`, because the parent's placement decides where it stands and which way it faces, and a parent standing on the Earth (§1.7) carries its parts with it. Its `extent` is ignored; the parent's extent is repaired to contain the placed objects' bounds the same way it is repaired to contain vertices (§1.8), which a reader can only do once the parts are fetched, so a parent's advertised extent may be smaller than what is finally drawn. **Depth and cycles.** A placed object may itself have parts. A reader MUST follow placements no deeper than `4` levels from the object it is rendering, drawing a placeholder in place of anything deeper, and MUST treat a reference to any object already in the chain of parents as missing. Both are bounds a reader can enforce without trusting the author, which is the only kind worth writing down. **A missing part is a placeholder, never a rejection.** A reference that cannot be fetched, has been deleted, fails §1.9, or is deeper than the bound is drawn as a placeholder at its placement: a wireframe cube one whole unit on a side in the reader's accent is the reference behavior, and a reader MAY do better. The parent is unaffected. This is the same stance §1.3b takes for a palette that cannot be resolved, and for the same reason: another author's event is not this object's to guarantee. **What this buys, in bytes.** An `a` reference is about 110 bytes and an `e` reference about 78; a placement is 28 at most. Twelve palm trees as twelve inline copies cost 396 vertices; as one tree and twelve placements they cost 33 vertices and about 450 bytes. The 129-square floor that motivated this cost 516 vertices pasted and 291 welded; as one tile and 129 placements it is about 3.7 KB and 4 vertices of its own, and the tile's author can repaint every floor built from it in one edit. **Groups are objects (non-normative).** A modeling tool wants to select, move and edit a group of vertices as one thing, and every such tool grows a grouping mechanism. This format has none, on purpose: a group is an object, placed. A client is expected to show an object's parts as things a person can pick (a tray of what is on the bench), isolate one, and edit it. A part that belongs to the person editing is edited in place, and because it is addressable every parent that places it follows; a part that belongs to someone else is cloned under the editor's own key and the placement repointed, which is what "editing someone else's tile" honestly is. None of that is protocol, and none of it needs to be. **Versioning.** `refs` and `parts` are fields whose absence has a meaning, so their addition does not bump `v` (§5). An older reader ignores them and draws the parent alone. ## 2. Model space (normative) An object is built in a right-handed frame with Y up, which is the glTF and three.js convention: | Axis | Direction | |---|---| | `+X` | right | | `+Y` | up | | `+Z` | toward the viewer | This is stated first and in normative language because being silent about it is the most common way a small format fails. STL never specified color and two vendors filled the hole incompatibly. PLY never registered its property names. glTF left "forward" undefined for years while being an ISO standard. Niantic's SPZ shipped in 2024 without saying which axis is up, and someone had to open an issue to ask. **Version 1 and version 2 differ by one sign and one color encoding, and by nothing else.** | `v` | Z points | A reader | |---|---|---| | `1` | away from the viewer | MUST negate every Z on read, which renders the object exactly as its author built it. Its colors are literal triples (§1.3) | | `2` | toward the viewer | reads the positions as written. Its colors are palette indices (§1.3) | Version 1 is the convention this format had while it lived only inside Cyberspace, where `+Z` is the direction of the black sun. Every object published before this document exists under it, and negating Z on read is what keeps those objects looking as they always have. A publisher MUST write `v: 2`; `v: 1` is for reading what already exists. The flip was made deliberately and once. Carrying Cyberspace's own axis convention into a format meant for anyone would have charged every exporter and importer, forever, for a mirroring that only Cyberspace needs. Cyberspace applies it where it belongs, in the one renderer that places an object into its world, rather than in every tool that ever writes one. A mirror turns every front into a back (§1.4). A reader or tool that carries an object through one, whether converting into a left-handed frame or placing an object into a world drawn with an axis negated, MUST reverse the order of every face in the same step, or read the front in the frame of §2 before mirroring. A rotation keeps every front a front. The version 1 negation needs no reversal: §1.4 reads the front after it, and version 1 winding was never authored. A Blender exporter therefore maps Blender `(x, y, z)` to SNO `(x, z, -y)`: Blender is Z-up and right-handed, SNO is Y-up and right-handed, and the negation is what keeps the frame right-handed rather than mirroring it. §7 returns to this. --- ## 3. Carrying an object in a nostr event (normative) ### 3.1 A standalone object: `kind 33331` An object stands alone as a `kind 33331` event whose `content` is the payload of §1, serialized as JSON. | Tag | Required | Meaning | |---|---|---| | `d` | yes | the object's identifier, chosen by its author and stable across edits. The workshop's own id for the object serves | | `name` | no | `["name", ""]`, duplicating the payload's name so a relay query can filter on it without parsing the content | | `alt` | no | a description for clients that cannot render an object (NIP-31) | `33331` falls in `30000..39999`, which NIP-01 defines as **addressable**: relays keep the newest event per `(pubkey, kind, d)`, so an author edits an object in place by republishing it under the same `d`. That is the right class for a thing someone iterates on in a modeling tool, which is what an object is. It costs something, and the cost is worth stating plainly rather than discovering later. Reactions (NIP-25), zaps (NIP-57) and comments (NIP-22) reference an event id, and an address resolves to whatever its author last published, so a payment made against an object can end up pointing at content that changed after it. An application that needs an object to be immutable, because it was paid for or because it was hidden somewhere and must stay as it was found, uses the bag item of §3.2, whose kind is regular. **Exactly one kind, never two.** NIP-71 shipped both a regular and an addressable video kind and it is widely regarded as a mistake. There is no regular twin of `33331` and there should never be one. A client that receives a `kind 33331` event whose content fails §1.9 MUST NOT render it and SHOULD say why rather than failing silently. ### 3.2 Inside a bag An object hidden at a place is an entry in a `kind 33330` bag, exactly as `CYBERSPACE_V2.md` §7.6 describes entries. It is carried in one of two ways: - **Inline:** an item, a `kind 3330` event, signed or unsigned, with the payload of §1 in its `content`. It MAY carry a `C` tag, which then MUST lie inside the bag's region. This is how every object was carried before 2026-09-24 and it stays the right way for small ones. - **By reference:** a reference entry (`CYBERSPACE_V2.md` §7.6) naming the object's own `kind 33331` event (§3.4): `["a", "33331::", "", ""]`, which follows the object as its author edits it. `33331` is addressable, so an `e` reference to it would stop resolving as soon as its author replaced it (`CYBERSPACE_V2.md` §7.6); a reference to an object uses `a`. No `kind 3330` event is involved. A `pubkey` in the address that differs from the bag's author is not a reason to reject: placing another author's object is a placement. A reader MUST fetch the referenced event and read the object from it as §3.4 says; a reference that cannot be fetched is a missing entry, dropped like an item that fails to verify, and a client SHOULD say so rather than say nothing was found. A publisher SHOULD carry a large object by reference: a bag is one event shared by everything hidden at a place, and by reference it stays small whatever the object costs. What counts as large is the publisher's judgment against the relays it publishes to (§1.8); the format draws no line. A publisher MAY carry a small object by reference too, when it wants the object to have an event of its own. `3330` is regular, and deliberately so. An item in a bag is a thing someone hid at a place and someone else found there; it must be exactly what it was when it was found, and its id must keep meaning what it meant. The same payload therefore travels under two kinds according to what is being done with it: `33331` for an object its author is still working on, `3330` for one that has been put somewhere. That is two containers for one format, not two ways of writing the format. A reference follows a `kind 33331` object as its author edits it; an object that must stay exactly as it was found is carried inline as a `3330` item, the one form no later edit can reach. ### 3.3 As an avatar An avatar event (`kind 11333`, `CYBERSPACE_V2.md` §8.10) carries an SNO payload in its `content`, or empty content for the default avatar. The work an avatar owes is computed from `unit`, `vertices`, `ticks` and `faces` as that section specifies. Nothing in this DECK changes that computation; this document only defines the fields it reads. ### 3.4 A hidden object as its own event **A hidden object carried by reference is a `kind 33331` event whose payload is encrypted to the place.** It is the standalone container of §3.1 with two differences, and it follows the convention for partially encrypted events that Fanfares' NIP FF-1 sets out, so any client that understands that convention understands this event without knowing what Cyberspace is. | Field | Content | |---|---| | `content` | a **preview** for clients that cannot open it. It SHOULD say that the object is encrypted to a location in cyberspace and name a client that can find it, for example: `This object is hidden at a place in cyberspace. Find it with ONOSENDAI: https://onosendai.tech` | | `["encrypted", "aes-256-gcm", "", "cyberspace:region"]` | exactly one. The ciphertext is the payload of §1, serialized as JSON, encrypted with the bag's region key (`CYBERSPACE_V2.md` §7.2) under the bag's own cipher and byte layout (§7.6: AES-256-GCM, 12-byte nonce, 16-byte tag, `nonce || ciphertext || tag`, base64). The fourth element stands where FF-1 puts a key service URL: `cyberspace:region` is FF-1's registered key derivation for a key computed from a place rather than served, and a reader MUST NOT make a network request for it | | `d` | the object's identifier, chosen by its author and stable across edits, as in §3.1. It MUST NOT be derived from the location | | `name`, `alt` | MAY be present, as in §3.1, and MUST NOT reveal the location | **The event MUST carry nothing that says where the object is.** No `C`, no `h`, no hint and no sector tag: those belong to the bag, which is what the region key protects. The event is a locked box in plain view; the bag is the note that says where the box stands and is readable only there. **One key opens the place and everything referenced from it.** The object is encrypted with the same region key that opens the bag, so a reader that has opened the bag opens every object it references with the key in hand, and a reader that has not can open neither. The cost is that the object is tied to the region: moving it to another place means re-encrypting it. A per-object key carried inside the bag would lift that and let one event be referenced from several places; §8 records it as open. **Validation is the same.** A reader that decrypts a referenced object and finds it fails §1.9 drops the entry, exactly as it drops an inline item that fails, and SHOULD say that the object was found and refused rather than that nothing was found. **Why a partially encrypted `kind 33331` and not a wrapper kind.** The object keeps its kind, so a query for `kind 33331` returns hidden objects too, as previews; a client that knows nothing of this DECK shows the preview, which is the sentence above pointing at a client that can find the object; and the shape is already implemented in clients that follow FF-1. A wrapper kind (an application-specific event carrying the ciphertext) would hide the object from every one of those. --- ## 4. Rendering (normative where it says MUST) A client MUST NOT render an object that fails §1.9. A client SHOULD render all three modes; a client that renders only `points` still shows every object, which is why the vertex list is the one required part of the format. **Shading is decided here rather than left to taste**, because per-vertex color forces the question and implementers who are not told will answer it differently, which makes the same object look like two objects. The default reading of an SNO is **unlit**: a face takes its color by interpolating its three vertices, and no light in the scene changes it. This is not an absence of a material, it is a named one: it is what glTF ratified as `KHR_materials_unlit` with a `COLOR_0` attribute, for exactly this kind of content. A face that carries its own color (§1.4a) is filled with it flatly, and there is nothing to interpolate or average: the seam between two such faces is exact, which is the whole purpose of the field. A client MAY light an object instead, and many will, because a lit object sits better in a lit scene. A client that lights an object MUST derive its normals per face, flat, from the triangle's own vertices, pointing out of the face's front as its winding gives it (§1.4); the back is lit as the other side of the same face. A client MUST NOT synthesize smooth normals by averaging across shared vertices: an object with no normals is faceted, an author who wanted a smooth surface has no way to say so today (§7.2), and a reader that smooths one anyway is deciding for them. A client MUST NOT invent geometry: no subdivision, no smoothing that moves a vertex, no hole filling. The lattice is exact and a renderer that moves a vertex has broken the one guarantee the format makes. A client MUST perform welding, deduplication and any equality test on the integer lattice rather than on floats derived from it (§1.2). --- ## 5. Versioning and extension (normative) `v` is `1` or `2`, and a reader MUST support both and reject anything else. A version bump means the meaning of an existing field has changed, which is exactly what happened between the two: `v: 2` reads its Z as written and `v: 1` has it negated (§2). Nothing else differs, and nothing else should ever differ by so little: two versions are a cost, and this one buys a format that every other tool can read without a special case. A reader MUST ignore fields it does not recognize rather than rejecting them. This is what allows an optional field to be added without a version bump, and it is how `extent`, `ticks`, `up` and `spin` were each added to a format already in use: an older reader sees an object with whole-unit positions on a grid of 8, unoriented, and draws something correct rather than nothing. An extension that changes how an existing field is interpreted MUST bump `v`. An extension that adds a field whose absence has a well-defined meaning MUST NOT. **`colors` changed meaning inside `v: 2`, once, and that will not happen again.** On 2026-09-16 `colors` went from three numbers per vertex to one palette index (§1.3), which by the rule above should have made a `v: 3`. It did not, and the reason is stated here rather than hidden. Three objects carrying this format existed in the world at that moment, counted by querying five relays for `kind 33331`, and all three were published by the authors of this document. An addressable event is replaced in place by its author, so all three can be reissued by the people who wrote them. The rule protects readers, and there were none but us. A third encoding would have cost every implementer forever to spare two people an afternoon. The rule applies from that date without exception, because from that date the format has readers who are not its authors. Anyone implementing this deck should treat `v: 2` as meaning the palette-indexed form. A `v: 2` payload whose `colors` are triples predates this. A reader MAY reject one, and a reader that would rather be generous MAY read the triples literally the way §1.3 reads a `v: 1` payload's; neither is required, because the three that exist belong to the authors and a bag's encrypted shards are readable only by the identity that hid them, whose client is updated in the same release. **The palette is the extension point that should keep `v` at 2 from now on.** A new set of colors is a new name in the `palette` field, or an `nevent` to one published as its own event (§1.3a, §1.3b), and neither needs a version bump. An older reader meeting a name it does not know rejects rather than drawing wrongly; one meeting a reference it cannot resolve draws in the built-in, which §1.3b makes explicit. That is the shape every future color change should take, and it is why the color space is a field rather than a version. --- ## 6. Security considerations **Resource exhaustion.** The limits in §1.8 are the defense. A reader MUST enforce them before allocating buffers sized from the payload, not after. A payload claiming 2^31 vertices must be rejected by counting the array, never by trusting a length field, and this format has no length fields for exactly that reason. **Crashes from bad indices.** §1.4 and §1.9 exist because a face index past the end of the vertex list is undefined behavior inside most graphics libraries, surfacing far from the event that caused it. Validate before rendering, always. **Objects are not attribution.** A `kind 3330` event inside a bag may be unsigned (`CYBERSPACE_V2.md` §7), in which case its `pubkey` is a claim and a client MUST NOT present it as verified authorship. **Screen real estate is a resource.** An object drawn in a shared space is seen by people who did not ask for it. Cyberspace's answer for avatars is to price size and detail in proof of work (§8.10). Any application that renders objects from strangers without a cost or a filter should expect to be flooded, and the format cannot solve that for it. **No code, no fetches.** An SNO payload is data. It names no URL, embeds no script, and requires no network access to render. A reader that keeps it that way inherits none of the attack surface that formats with external references carry. --- ## 7. What SNO leaves out, and why (non-normative) Every omission here is deliberate, and each one has a reader who will miss it. This section exists so that a person deciding whether to implement SNO knows what they are not getting, and so that a future extension has somewhere to start. ### 7.1 The conversions an exporter must perform A tool that writes SNO from a modeling package performs four conversions, and each loses something. An exporter author should know all four before starting. | Conversion | What is lost | |---|---| | **Triangulation.** SNO has only triangles; modeling packages work in quads and n-gons. | The authored topology. A round trip returns triangles, so the model is no longer editable the way it was built. | | **Axis change.** Blender is Z-up and right-handed; SNO is Y-up and right-handed. The map is Blender `(x, y, z)` to SNO `(x, z, -y)`. | Nothing, if the negation is not forgotten. Dropping it turns the map into a reflection and publishes every object mirrored, which is invisible on a symmetric object and obvious on everything else, and turns every face inside out (§1.4), which is obvious on everything. Both frames are right-handed, so no mirroring is required or wanted (§2). | | **Quantization to the lattice.** Float positions become integers on a 1/120 lattice. | Real precision. See below. | | **Decimation, if the exporter chooses it.** | The format sets no ceiling (§1.8); an exporter decimates to what its relays take and its readers draw well. When it does, this is the one loss that changes the art rather than the numbers. | **The precision budget, stated plainly, and it is better than it looks.** One unit is 120 ticks, which is log2(120) = 6.91 bits. A grid at the maximum extent of 64 is 15,360 ticks across, about 13.9 bits of resolution per axis; at the default extent of 8 it is about 10.9 bits. The instinct is to call that low precision. It is not. Draco's encoder defaults to 11 bits for positions and 14 is the common production setting; gltfpack also defaults to 14. SNO's 13.9 bits sits directly on top of the number the rest of the industry ships. SNO is not a low-precision format. It is a **fixed-window** format at industry-standard precision, and what it gives up is dynamic range rather than accuracy, which is exactly what the `unit` exponent exists to recover. The lattice also buys something the float formats cannot have at any bit depth: exactness. Two objects authored to share an edge share it forever, on every implementation, and welding, deduplication and hashing are integer tuple comparisons rather than an epsilon that is always wrong somewhere. **What things cost, for a sense of scale.** The budget is the relay's (§1.8), and it is not abstract: | Object | Vertices | Triangles | Event, roughly | |---|---|---|---| | a cube | 8 | 12 | under 1 KB | | a UV sphere at 32 by 16 segments | 482 | 960 | about 18 KB | | one subdivision more | thousands | thousands | about 100 KB: the world's relay takes it, a stock relay does not | | a scanned or sculpted asset | 10^4 to 10^7 | | megabytes; no relay in use takes it | A few hundred vertices is one recognizable object at low poly; a few thousand is a detailed one. An exporter should show a live estimate of the event's size while modeling rather than let a relay discover it at publish. ### 7.2 What is genuinely absent, and what each would cost None of these needs a version bump, because each is an optional field whose absence already has a defined meaning (§5). | Missing | Why it hurts | Cheapest fix | Cost | |---|---|---|---| | **Smooth shading** | With no normals every surface is faceted, so a sphere reads as a golf ball | one object-wide boolean telling the renderer to average face normals at shared vertices | one field, no per-vertex data | | ~~Per-face color~~ | **Added in §1.4a**, and made affordable by §1.3's palette index, which took a color from 21 bytes to 4. It was the one absence that made the format's own best style its most expensive | | | | **Emission** | The one thing a glowing object needs, and Cyberspace is made of glowing objects | an optional material block with an emissive flag and a strength | one small object | | **Roughness, metalness, alpha** | The sliders every modeler reaches for after base color | three numbers in the same block | included above | Of these, smooth shading and per-face color are the two that change what is possible rather than what is pretty, and per-face color is the only one that makes the budget go further rather than less far. One apparent gap is not one. SNO has per-vertex color and no material, which is precisely the shading model glTF ratified as `KHR_materials_unlit` used with a `COLOR_0` attribute: do not light this, take the color from the vertices. That model exists for mobile, photogrammetry and stylized art, and it is named, specified and widely implemented. **SNO is `KHR_materials_unlit` with `COLOR_0`** is the one-sentence bridge to anyone who thinks in glTF. ### 7.3 What is out of scope | Missing | Why it does not belong | |---|---| | **UVs and textures** | A texture is kilobytes to megabytes, so it must live outside the event as a URL or a hash. That ends the property that an SNO renders with no network access and no external attack surface, which is most of what makes it simple. | | **Hierarchy and transforms** | Since 2026-09-24 these are `refs` and `parts` (§1.10): an object places others, and a scene is an object whose own geometry may be empty. What stays out is a scene graph richer than placement: constraints, parenting by name, per-instance overrides. | | **Animation, armatures, shape keys** | Each needs a time model and per-frame or per-bone data that dwarfs the geometry. | | **Level of detail** | Instancing is §1.10. Level of detail is not: one object is one mesh, and a reader that needs less draws less. | ### 7.4 What SNO has that the large formats do not 1. **Exact positions.** Integer lattice coordinates mean two objects authored to meet actually meet, on every implementation, forever. Float formats only approximately do, and the error depends on the order the exporter wrote the file in. 2. **It is the event.** No file to fetch, no hash to resolve, no second protocol, no host to go down. A relay that has the note has the object. 3. **No parser dependency.** `JSON.parse` and thirty lines of rendering, against a specification of a few pages. 4. **Readable and diffable.** An object can be reviewed in a pull request, hand-edited, and generated by a shell script. 5. **Per-vertex color is first class.** With per-face color it is the only coloring mechanism, so every reader supports both, where in the large formats vertex color is an option that half the pipeline ignores. Indexing it through a palette is what PLY does with `uchar` channels and what every indexed image format has always done, and it is why a color here costs four bytes rather than twenty-one. --- ## 8. Open questions 1. **A base unit outside Cyberspace.** `unit` is an exponent over a base the application defines, which is meaningless to a client with no such base. An optional field giving meters per model unit would make an object's real size portable, at the cost of a field that Cyberspace itself would never write. 2. **Whether a reader's limit is a relay bound or a client bound.** *Settled 2026-09-24: neither. The format sets no limit (§1.8); the relay bounds the event as it does every kind, and a client that cannot draw an object is a client's problem to solve as it solves it for a long article or a large image. Kept for the record; the earlier text follows.* This was once the question of whether 512 and 1024 were derived from anything; §1.8 now measures what fits, and at 32 bytes a vertex about 832 vertices with a color on every face would still clear a 56 KB budget. So the numbers are a deliberate reserve rather than a ceiling the event size forces, and the paragraph there says why the headroom was spent on face colors instead. What is not settled is which bound should set them. The size a relay will accept and the size a client can hold and draw are different limits that happen to be the same number today, and only the first is measured. 3. **There is no hard bound on how far a vertex may lie from the origin.** §1.8 puts the 64-unit bound on publishers and lets readers repair instead of reject, which is what ONOSENDAI does today: it grows the extent to fit, without a ceiling. That is safe for a client rendering its own author's work and unsafe as a general rule, since a payload of 512 vertices at 2^50 units is valid under this text and will produce a grid no renderer wants. Making the bound a reader obligation is a one-line change and would make the current client non-conformant until it is updated, which is why it is a question rather than a rule. --- 4. **A per-object key for a hidden object carried by reference.** §3.4 encrypts the object with the region key, which ties it to the place. A key of its own, carried inside the bag beside the reference, would let one large object be referenced from several places and moved without re-encryption, at the cost of a second key to lose. 5. **Whether `parts` should also allow a mirror.** Eight integers place and turn and scale; a negative scale step halves, it does not mirror. A left glove from a right glove needs one more flag or a second object. Left out until someone needs it. 6. **The rotation order.** §1.10 fixes X then Y then Z about the parent's axes. It is a convention, not a derivation; if a modeling tool in common use disagrees, the cost of matching it is one sentence now and every published object later. ## Appendix A: a worked example (non-normative) A four-vertex tetrahedron, one color per corner, drawn solid, built on a lattice of gibsons. ```json { "v": 2, "name": "tetra", "unit": 0, "extent": 8, "mode": "solid", "vertices": [[0, 0, 0], [2, 0, 0], [1, 0, 2], [1, 2, 1]], "ticks": [-4], "colors": [238, 235, 239, 225], "faces": [[0, 1, 2], [0, 1, 3], [1, 2, 3], [0, 2, 3]] } ``` There is no `palette`, so the indices name the built-in (Appendix C): 238 is pure red, 235 pure green, 239 pure blue and 225 white. `"ticks": [-4]` is the run-length encoding of four whole-unit vertices; omitting `ticks` entirely would mean the same thing. The same object with a palette of its own, which for four colors costs less than writing them out did: ```json { "v": 2, "name": "tetra", "unit": 0, "extent": 8, "mode": "solid", "vertices": [[0, 0, 0], [2, 0, 0], [1, 0, 2], [1, 2, 1]], "ticks": [-4], "palette": [[255, 0, 0], [0, 255, 0], [0, 0, 255], [255, 255, 255]], "colors": [0, 1, 2, 3], "faces": [[0, 1, 2], [0, 1, 3], [1, 2, 3], [0, 2, 3]] } ``` And the same object again with all four faces one color, which is what `facecolors` is for: adding `"facecolors": [238, -3]` gives four flat red triangles with hard edges between them, for eleven more bytes. The same object one meter across rather than four gibsons is the same document with `"unit": 32`. --- ## Appendix B: relationship to existing formats (non-normative) The question any reviewer asks first is why this is not glTF. The honest answer has three parts: at this size the measurement favors text, the data model SNO wants already exists elsewhere and is called PLY, and the formats that succeed at this scale are the ones emitted by software rather than the ones specified well. ### B.1 What SNO is, in one line each | To someone who thinks in | SNO is | |---|---| | glTF | `KHR_materials_unlit` with a `COLOR_0` attribute, quantized, with the scene graph and the buffers removed | | PLY | an ASCII PLY with vertex colors, with the grammar replaced by fixed field names and the positions moved onto an integer lattice | | MagicaVoxel | the same idea one level up: triangles rather than voxels, and RGB per vertex rather than a 255-color palette | | nostr | NIP-64 for geometry: a small domain payload in `content`, clients SHOULD render it, relays MAY validate it | ### B.2 The size argument, measured rather than asserted A colored cube of 8 vertices and 12 triangles, and a 162-vertex sphere, in the formats that can carry per-vertex color: | Format | Cube, raw | Cube, gzip | Sphere, raw | Sphere, gzip | |---|---|---|---|---| | SNO-shaped JSON | 341 | 189 | 8,280 | 2,657 | | PLY ASCII | 434 | 226 | 10,381 | 2,755 | | PLY binary | 506 | 249 | 6,823 | 2,602 | | GLB | 1,044 | 508 | 5,292 | 2,429 | | glTF with a base64 buffer | 1,129 | 541 | 6,813 | 3,222 | Raw, the binary formats win at the larger size, as they should. The number that decides the question is what happens inside a nostr event, where a binary payload has to be base64 encoded: | Inside an event envelope | Raw | gzip | |---|---|---| | SNO as `content` | 8,955 | **2,778** | | a base64 GLB as `content` | 7,393 | 3,495 | Base64 costs 33% and produces high-entropy output that compresses badly, while decimal JSON compresses well. Once the transport is counted, and relays commonly negotiate WebSocket deflate, the text format is about 20% smaller than the binary one **and** is readable in a terminal. This is the single measurement worth keeping: nobody should be talked into base64 on a raw byte count without measuring after compression. There is a second, blunter reason to stay out of base64. Khatru's `ApplySaneDefaults` installs a policy that rejects any event whose content contains `data:image/` or `data:video/`. A base64 asset embedded in content trips it; an integer JSON payload does not even come close. The corollary is that SNO must never acquire a compression extension. Draco's WASM decoder is roughly 100 KB gzipped, against a payload of two to ten kilobytes. Any scheme needing a dedicated decoder is a net loss here by an order of magnitude, and deflate is already in the socket for free. ### B.3 What the neighbors got wrong, and the rule each one teaches | Format | What happened | Rule for SNO | |---|---|---| | **PLY** | Its data model is SNO's, but property names were never registered, so `red`, `green`, `blue` are conventions. Years of colors that silently failed to import across MeshLab, Blender and VTK | Freeze the field names. SNO has fixed keys and no grammar | | **STL** | Color was left unspecified, so VisCAM packed 15-bit RGB into the attribute bytes and Materialise put `COLOR=` in the header. Mutually incompatible, both widely ignored | A hole in a popular format gets filled by vendors, incompatibly. This is why §1.4 decides winding and §4 decides shading rather than leaving either open | | **SPZ** (Niantic, 2024) | Shipped MIT, roughly ten times smaller than PLY splats, real adoption, and **without stating its up axis or handedness**. Someone had to open an issue to ask | A brand-new format in 2024 still made the oldest mistake. §2 states the axes in normative language | | **glTF** | Left "forward" undefined for years while Maya, 3ds Max and Blender each assumed differently, and is an ISO standard | Being a standard does not save you from silence | | **USD** | `metersPerUnit` falls back to 0.01, and the fallback up axis is configurable per installation, so the same file can read differently on two machines | Conventions belong in the file, never in the environment | | **COLLADA** | Aimed at full interchange, underspecified, many ways to say one thing, divergent implementations, and **removed from Blender entirely in 5.0** | "No more than one way of doing the same thing", which is also the NIPs repository's fourth acceptance criterion. §1.2 states a canonical form for that reason | | **OpenCTM** | Technically excellent, one release in January 2010, nothing since | A format is an ecosystem, not a document | | **Draco on small meshes** | A decoder larger than the data | Measure the decoder, not just the payload | There is a useful taxonomy from the USD side that classifies glTF and FBX as entirely "last mile" formats, which impose an opinion and conform the data to it, against interchange formats that try to preserve everything and fail. SNO is unapologetically last mile, and that is the side of the split that succeeds. ### B.4 How small formats actually win Every small format in the survey that succeeded had exactly one thing in common, and it was not a good specification. MagicaVoxel's `.vox` won because MagicaVoxel is a beloved free editor. Litematica's format won because it is what the mod builders use. PGN won because every chess program reads it. SPZ won because Niantic shipped an MIT library with real data in it. The lesson for SNO is that the deliverable is not this document. It is software that emits the format: the shard workshop that already exists, and an exporter from a tool modelers already use. The specification is what makes the second implementation possible, not what makes the first one matter. One practical consequence: the first converter worth writing is **to and from PLY with vertex colors**, not glTF. PLY is the format whose data model already matches this one, and it is what Blender, MeshLab and every scanner already speak. --- ## Appendix C: the built-in palette, `cyberspace-neon-256` (normative) The 256 colors an index names when an object carries no `palette` of its own (§1.3a). Each is sRGB, eight bits a channel. **The layout is arithmetic, not a list to memorise.** | Index | What | |---|---| | `0` to `191` | 24 hues of 8 steps. Hue `h` step `s` is index `h * 8 + s`. Step 0 is the darkest, step 7 the brightest | | `192` to `223` | 32 steels, black to white, faintly cyan so they belong to the same world rather than looking dead | | `224` to `255` | 32 signatures: the client's own instrument colors exactly, the six sRGB gamut corners that evenly spaced ramps cannot reach, six neon staples, and six deep grounds for a scene that is mostly dark | The ramps are generated in OKLCH rather than HSV, so a step looks like one step rather than measuring as one, and clipped into sRGB by lowering chroma rather than clamping channels, which is what stops the bright end of a saturated hue turning to mud. Shadows drift a little toward the blue that lights this world and highlights drift a little warm, which is what makes a ramp read as lit rather than as a tint. Lightness runs from 0.30 to 0.85 and chroma stays near the gamut edge throughout: an earlier draft ran 0.20 to 0.94 and spent about seventy of its entries on near-blacks and pastels nobody would pick. The generator is `decks/sno-palette.mjs` and the values are also carried as `decks/sno-palette.json` for implementers who would rather not retype them. The sheet below is `decks/sno-palette.png`. ![The cyberspace-neon-256 palette](sno-palette.png) | From | +0 | +1 | +2 | +3 | +4 | +5 | +6 | +7 | |---|---|---|---|---|---|---|---|---| | 0 | #003632 | #004d49 | #006562 | #007e7d | #009799 | #00b2b8 | #00cdd7 | #00e8f9 | | 8 | #003538 | #004c51 | #00636d | #007c8a | #0095a9 | #00afca | #00c9ed | #6fdfff | | 16 | #00343f | #004a5a | #006178 | #007998 | #0092ba | #00abdf | #34c4ff | #93d8ff | | 24 | #003346 | #004865 | #005e86 | #0075ab | #008dd2 | #00a4fd | #6ebcff | #a7d3ff | | 32 | #003052 | #004476 | #00589e | #006ccc | #017fff | #5a9bff | #8bb5ff | #b5ceff | | 40 | #00266d | #002cac | #1902ff | #374fff | #5a73ff | #7e92ff | #a1aeff | #c3c9ff | | 48 | #220088 | #3700b7 | #5000e7 | #6837ff | #8064ff | #9b86ff | #b6a5ff | #d0c4ff | | 56 | #350079 | #4f00a3 | #6b00cd | #8b00f8 | #a24bff | #b875ff | #cd9aff | #e1bcff | | 64 | #420069 | #5f008d | #7f00b1 | #a200d5 | #c600f8 | #dd53ff | #ed84ff | #f9afff | | 72 | #4b0058 | #6b0076 | #8e0094 | #b200b1 | #d900cd | #ff1ae7 | #ff7de2 | #ffb1e6 | | 80 | #520046 | #74005f | #980077 | #be008e | #e600a4 | #ff45b4 | #ff87c0 | #ffb5d2 | | 88 | #570035 | #7a0048 | #9f005a | #c6006b | #ee007b | #ff538d | #ff8ca6 | #ffb7c2 | | 96 | #5a0024 | #7e0031 | #a3003d | #cb0047 | #f4004e | #ff5b6a | #ff908e | #ffb9b4 | | 104 | #5c0010 | #800014 | #a60013 | #ce0006 | #f22400 | #ff613e | #ff9375 | #ffbba5 | | 112 | #561200 | #752200 | #943300 | #b44600 | #d45a00 | #f47000 | #ff9652 | #ffbd93 | | 120 | #4b1f00 | #683100 | #854300 | #a25700 | #c06d00 | #de8300 | #fc9b00 | #ffc077 | | 128 | #442600 | #5e3800 | #794c00 | #946100 | #b07800 | #cc8f00 | #e7a800 | #ffc333 | | 136 | #3e2a00 | #563d00 | #6e5300 | #876900 | #a08100 | #b99900 | #d2b300 | #ebce00 | | 144 | #372d00 | #4d4200 | #635800 | #797000 | #8f8800 | #a5a300 | #b9be00 | #cdda00 | | 152 | #2f3000 | #424600 | #555e00 | #677700 | #779100 | #87ad00 | #94ca00 | #9fe800 | | 160 | #243400 | #314b00 | #3d6400 | #457f00 | #499b00 | #45b900 | #2cd900 | #00f837 | | 168 | #073900 | #005108 | #006a1f | #008436 | #009f4e | #00ba67 | #00d682 | #00f39e | | 176 | #00381e | #005030 | #006844 | #00825b | #009c73 | #00b78d | #00d3a9 | #00efc6 | | 184 | #00372a | #004e3f | #006656 | #00806e | #009a89 | #00b4a5 | #00d0c2 | #00ece1 | | 192 | #010101 | #020203 | #050606 | #090b0c | #0f1213 | #15191a | #1b2021 | #222728 | | 200 | #282e30 | #2f3638 | #373e40 | #3e4648 | #454e50 | #4d5658 | #555e60 | #5d6769 | | 208 | #666f71 | #6e787a | #778083 | #80898c | #899294 | #939b9d | #9ca4a6 | #a6adaf | | 216 | #b0b6b8 | #babfc1 | #c4c9ca | #ced2d3 | #d8dcdc | #e3e5e6 | #eeefef | #f8f8f8 | | 224 | #000000 | #ffffff | #00e5ff | #ff3b6b | #f7931a | #52e39f | #c8f5ff | #6f8ea0 | | 232 | #1d3547 | #05070d | #ff00ff | #00ff00 | #00ffff | #ffff00 | #ff0000 | #0000ff | | 240 | #39ff14 | #ff6ec7 | #7df9ff | #b026ff | #fffb00 | #ff3300 | #00ff9f | #ff007f | | 248 | #4d4dff | #ffd300 | #0a0f1a | #12182a | #1a0f24 | #0f1f1c | #241016 | #1c1c0f | # Territory, conflict, and the game layer **Status:** Design record, 2026-08-27 **Decided by:** arkinox, in review with Claude **Inputs:** `CYBERSPACE_V2.md` at 919494a (post DECK-0001 v3), DECK-0001 v3 (merged, #18), the DECK-0002 Domains draft (PR #7), the Derezz draft (PR #8, since abandoned), the Virtual Spawn draft (PR #15), `RATIONALE.md`, the v1 archive (derezz, armour, stealth, echo resistance), and a revision guide circulated privately by XOR in August 2026. **Supporting analyses** (working notes; this record is the decision): - `analysis/deck-domains-ground-up-analysis.md`: domains taken apart claim by claim - ~~`analysis/deck-derezz-ground-up-analysis.md`~~ and ~~`analysis/deck-derezz-d2-work-clock.md`~~: the derezz draft's defects, including the sitter-wins proof, the 12-scenario test plan, and the work-as-clock rebuild. Removed 2026-09-15 when derezz was ruled unworkable; git keeps them, and §4 below still carries the argument that killed it. --- ## 1. Decisions ### 1.1 Domains: no Domains DECK. Territory is holding. The region root `R` is a public function of `(base, height)`; anyone who spends the work obtains the identical value, and that is the property that makes location encryption and discovery work at all. Knowledge of `R` can be rate-limited by work and disk but never made exclusive. The Domains draft treated `R` as a secret whose knowledge confers title, and built a STARK to hide it. That is a category error: a counter-claimant computes their own copy and never needs yours. What computing `R` and keeping the result gives is **holding** (spec §7.6): the per-axis roots of every aligned sub-region, at every height, on disk, so that every location-based key inside the region is one pairing step away instead of a tree away. Holding buys latency and nothing else: - read anything published anywhere in the region, at any height, without per-item work (a passerby's interactive scan reaches roughly h16); - write anywhere in the region, at any height, at once; - lock a house: `KDF(region_key || owner_secret)`, presence plus the holder's factor. Holding does not buy exclusion (anyone who does the work holds identical keys), any advantage in observing chains (they are public to everyone), or any effect on anyone else. It is a keyring. It costs disk for as long as it is held: about 19 TB for the full trees of an h34 cube, 1.5 PB at h40, 107 PB at h46; about 1.1 TB at h34 for just the nested cubes around one position. Lapse and the keys are a recomputation away again; nothing else happens. That is the whole of the protocol's maintenance economics, and it needed no mechanism. A protocol-layer domain (claim, priority, time anchor, renewal, stranger verification) is a **title registry**. The base spec says in §3.1 "No registry tracks who is where" and in §8 "knowledge of global state is not possible." It is also blocked in practice: the STARK in the draft has no prover column anywhere, and arithmetising 2^34 bignum pairings on 185 GB integers is months to years per proof with anything that exists. **Games may define territory.** A game is free to run its own registry, claims, taxes and policy for its players. That is what the game box (§2) is for, and it is where the ambitions of the Domains draft can live without a STARK, because the game's client is the verifier and its players opted in. Nothing about it binds anyone outside the game. **Actions taken in this change:** spec §6.12, §9.3 and §13.1 reworded from "authority" to holding; spec §7.6 added; RATIONALE §4 and §6 extended; DECK-0001 §6 reworded. **Actions for the maintainers:** close PR #7 with a pointer here; the CP-ABE, policy-JSON and shard-filtering material in it is application convention, not territory, and may be re-homed by whoever wants it. DECK number 0002 goes to Virtual Spawn. **Kept open, not scheduled:** a peer-verified "claims" DECK (a `claim` published from inside the region, MAC over region key, pubkey and a recent block hash, checkable only by other holders, exactly like sidestep Level 2). A reputation input, never a title. With the encounter primitive (§4.1) it becomes a co-signed attestation between holders rather than a new mechanism. ### 1.2 Derezz: withdrawn from the protocol layer. Lives in the game box. The draft is unsound, and the defect is structural. Derezz is the first rule under which one identity's signed event changes the validity of another identity's chain, and the draft settles the resulting ordering with `created_at`, a number the signer chooses. Under its own rules, the party that has been stationary longer always wins any encounter, retroactively and at leisure (`analysis/deck-derezz-ground-up-analysis.md` §2). Consequences: nobody can ever approach anyone; spawn camping is permanent and undefendable; every hyperspace stop is a kill box; the dominant strategy is to never move. The theory behind derezz survives the draft. Chains and cross-chain references are an unforgeable clock for "after" (Lamport's happens-before). They can never prove "before", and no label can fill the gap. The resolution is Bitcoin's: **work is the clock.** A derezz targeting event D is a reorg attack on the victim's chain; the victim's work accumulated after D is temporal armour; the attack succeeds iff the attacker's fresh work exceeds it. Withholding is self-defeating, sitting confers nothing, deep reorgs cost the victim's whole life, and `created_at` appears in no verdict. This is v1's design rebuilt on v2 primitives, called **D2** below. **Why the game box.** Spec §1.1 promises "no administrator who can move you or delete you." A derezz moves you. As a protocol fact it would break that promise. As a game rule it does not: every DECK is optional, a derezz exists only for clients that honour it, and the thermodynamic verdict applies among those who agreed to be judged by it. **Readiness, precisely.** Inside a game running D2, holding gains a second consequence: the holder can engage anyone in the holding without first computing the box between them, so the holder's time-to-engage is shorter. That engagement-latency edge is what *readiness* means, and it exists only inside such a game. In the base protocol there is nothing to engage, and holding is a keyring. **Superseded, 2026-09-15.** Derezz was ruled unworkable as a mechanism and is not a forthcoming DECK. No number is reserved for it, the working papers are gone from the repository (git keeps them), and the recommendation below to rewrite it test-first stands only as a record of what was considered. "Derezz" survives as the client's respawn verb, which is what ONOSENDAI's panel has always meant by it. ### 1.3 Virtual spawn: game box. Already correctly framed in its own draft: protocol-invalid, application-valid. Its open question 3 (relationship to domain rules) is answered: there are no domain rules; a game defines who may virtually spawn where, and the game's own liveness rules apply from there. ### 1.4 The premises, as revised Recorded because the reasoning is part of the protocol's defensibility. - *"Cyberspace has scarce space."* Positions are not scarce (2^255 of them, and two avatars may share one). Proximity to meaningful things is scarce. At the math layer territory is non-rival: my holding a region takes nothing from yours. Physical territory is rival because two bodies cannot share a point; cyberspace lacks that, so conflict from scarcity must be chosen, not assumed. - *"You spend energy to obtain, so others must spend energy to take it."* True of presence already: the only defence the base thesis promises is that others must do the work too. Force adds a second thing, the power to make a place cost more than the work of reaching it. That is a game design choice. - *"Thermodynamics is the final verdict on conflict."* Kept, inside the game box, with its bill written down: combat work is SHA-256 (ASIC work, spec §12.3) while holding is storage-bound (§9.11). A state that cannot cheaply hold your h46 cube can cheaply kill you inside it. The verdict is thermodynamic; the currency is the one that most favours hardware. --- ## 2. The game box ### 2.1 Two axes, never confused | | Game-alive | Game-dead | |---|---|---| | **Protocol-valid** | ordinary play | derezzed (D2): chain valid, game says respawn | | **Protocol-invalid** | virtual spawn: chain invalid, game recognises it | ordinary invalid chain (fork, bad proof) | The base protocol owns exactly one verdict, validity, and nothing in the game box ever changes it. A game owns liveness, and the protocol never consults it. The Virtual Spawn draft already uses "derezzed" for its protocol-invalid state; the table makes the two meanings explicit. ### 2.2 Invariants (also in `decks/README.md`) 1. A game mechanic MUST NOT alter the validity of any kind 3333 chain under the base spec. 2. A game mechanic MUST be verifiable from a bounded set of events plus, if it needs a clock, Bitcoin block headers. `created_at` MUST NOT decide any game verdict. 3. A game mechanic that resolves conflict MUST resolve it by work, with the unit of work stated and the verification cost bounded (sampled openings, DECK-0001 §5.5, are the existing pattern). 4. A game mechanic's effects MUST be specified as a fixed point over the reference graph where events can void one another (reorg semantics), with convergence under partial views stated. 5. A client that does not implement a game DECK MUST see nothing wrong with any chain the game considers dead. ### 2.3 In the box now, and what v1 suggests next - **Virtual spawn** (PR #15): the entry point for games. DECK-0002. - **Derezz attack** (D2, replacing PR #8): the reorg attack; explicit armour as the defence. DECK-0003. - From v1, once D2 exists: **armour** (stock work worn while travelling), **stealth** (obscured position bought with work); **vortex** and **bubble** only if a game has continuous motion, which v2 hops do not. ### 2.4 For game developers A game on cyberspace is: a region (any aligned cube; a landfall neighbourhood on Earth or an inland cube in the void), a spawn zone (a virtual-spawn policy: who may appear, where, under what credential or cost), a ruleset (which game DECKs the game's client honours, with which parameters), and a client (or a mode of an existing one), usually with a relay that has its own admission policy (the reference relay already auth-gates with NIP-42). What the protocol gives a game for free: locality (movement priced by ultrametric distance, no teleportation, decomposition invariance); region keys at every height (chalk, locked houses, discovery radius as a level-design tool); holding, for whoever wants to keep a region's keys; chains as tamper-evident audit trails of every player's path; a global clock through Bitcoin block hashes; a transit system with stops (DECK-0001); and identity that is also a coordinate. What a game must supply itself: scarcity for virtual spawns (the draft is explicit that they are free and sybil-able); its own liveness rules; its own balance (D2's stock-versus-flow armour choice is a balance knob, not a protocol constant); and the understanding that nothing it does binds anyone outside its client. --- ## 3. D2 in one page (for the DECK-0003 rewrite) - **Unit of work:** SHA-256 Merkle trees, sidestep-style, verified by sampled openings. Movement's own fresh work (temporal axis, at most 2^16 pairs per hop) does not count; it is five orders of magnitude below one GPU-second and was never meant to be armour. - **Derezz attack:** kind 3333, `A = derezz`, in the attacker's chain; `c == C` (it does not move you); same plane as the target; the box proof over attacker position and target position (work equivalence applied to aggression, the one good idea in the old draft); fresh work seeded by the attacker's previous event id and the target event id, height chosen by the attacker, scaled by the box height so distance is repaid per shot. - **Armour:** an explicit action carrying fresh work seeded by chain position. Flow-dominant: the verdict weighs work accumulated since the targeted event more than a held stock, so presence has to be maintained; idling is allowed but undefended. - **Verdict:** attack succeeds iff attacker work exceeds target armour since D. `created_at` is not consulted. Verdicts are eventually consistent: they converge toward the party that spent more as observers learn more. - **Effect:** the target's chain is game-dead from D; the target's next game-valid action is a spawn. Events voided by a reorg void their own effects (a voided derezz revives its victim). Liveness is a fixed point over the reference graph. - **Block commitments:** any movement event may carry a recent block hash; a derezz must. They bound withholding to one block and give arrivals at stops a defensible move. Open balance questions (game parameters, not protocol): the distance scaling function; the flow-versus-stock weighting; whether stops get a grace rule beyond block commitments. **Test-first plan.** A Python simulator in cyberspace-cli beside the existing Level-1 verifiers: N avatars as chains; actions hop, armour, derezz; a verifier computing liveness as a fixed point over the graph; the 12 scenarios of `analysis/deck-derezz-ground-up-analysis.md` §7 re-expressed in work terms (sitter vs arriver, withholding, spawn camp, stop kill box, relay partition, cross-plane, free move, stale target, same-block mutual, holder vs deep intruder, fork interaction, derezz chaining); adversarial strategies (sitter, withholder, whale, swarm) against a moving defender with a fixed budget; property tests that no never-moving strategy dominates a moving one, that verdicts converge, and that every arrival at a stop has a surviving continuation. The normative text is whatever survives. --- ## 4. Is the protocol missing something without derezz? No. Removing derezz costs the base protocol nothing it promised: every guarantee in spec §12.1 is a property of an isolated chain. But asking why derezz felt necessary exposes two real gaps it was covering badly, and one property that is not a gap but should be stated plainly. v1's derezz did two jobs under one word: it was the *state* of having an invalid chain ("a forked action chain is wholly invalid and causes its owner to derezz"), and it was aggression. The first job is fully intact in v2 (an invalid chain is not a position; the newest spawn resets; the client's Derezz panel is this; Virtual Spawn uses the word this way). Only the second was boxed. ### 4.1 Gap: there is no contact primitive Physical space has distance (it costs energy to cross) and contact (being near something lets it affect you). The protocol models distance completely and contact not at all. Proximity lets you observe another chain and read a region's chalk; it never lets you affect anyone, and nothing lets two avatars prove they met. Derezz was a hostile contact primitive. The sound version is cooperative: an **encounter**, in which two chains reference each other's heads and each carries the box proof over both positions. Mutual references are a causal knot, the only way the protocol can ever produce a proof of "before" (a unilateral reference proves only "after"). An encounter costs nothing beyond the proximity work, violates nothing in §1.1, needs no clock, and is what the rationale's own use case requires: "a witness can only be in one place" (RATIONALE §1) is unusable without a witnessing primitive. AI embodiment, handoffs, trades, doors and co-signed attestations of co-location follow from it, and it hands games a contact mechanic that is not combat. It also gives D2 a cheaper clock among willing parties. The fundamental thing missing is contact; derezz was its hostile half; the constructive half belongs in the base protocol. ### 4.2 Gap: fraud is detectable but the verdict cannot be shared Sidestep and hyperjump security rest on deterministic fraud detectability (spec §6.11, DECK-0001 §5.5): a bad root is objectively wrong and anyone who redoes the work can see it. But no event says "this event is fraudulent, here is the evidence," so every observer redoes Level 2 or trusts. v1 needed derezz as the consequence of fraud; the missing piece is a **fraud-proof event**, which needs no consequence at all because an invalid chain is already dead by definition. For a hyperjump the evidence is compact: one recomputed leaf (at most h22) against the published opening. For a sidestep it is not, since a Merkle root can only be shown wrong by producing the right one; that case needs either a bisection game between two holders or a signed "recomputed, disagree" attestation weighted by who signs it. Either way the shape is a cheap, shareable verdict, not a kill. The fork-rule inconsistency (work list item 8) is the same gap from another angle: nothing can publish "this chain forked here." ### 4.3 Not a gap, but say it out loud: presence is a record, not a state Once you hop somewhere you are there until you publish again (spec §5.1, no heartbeat). Physical presence costs energy per second; cyberspace presence costs energy once. So popular regions and stops fill with ghosts, and "who is here" means "whose last event is here." Derezz was v1's broom for ghosts and is the wrong tool; the property was chosen deliberately, because demanding continuous work is what §5.1 refuses. The honest handling is a client-side liveness convention (age of last event) and a sentence in the spec admitting that an avatar is a last-known position. Related and equally unavailable: position is non-rival, and no protocol can add collision without global state. ### 4.4 Consequence for priorities The encounter primitive and the fraud-proof event are protocol-grade, sound without a clock, and better served by cooperative mechanisms than by a kill. They go on the base-protocol work list ahead of the D2 simulator. --- ## 5. Work list | # | Item | Where | Status | |---|---|---|---| | 1 | Close PR #7 with a pointer to this record | cyberspace | maintainers | | 2 | Base spec wording: §6.12, §9.3, §13.1; new §7.6 | cyberspace | this change | | 3 | RATIONALE: exclusive territory limitation; holding, not owning | cyberspace | this change | | 4 | decks/README: game-mechanics category and reservations | cyberspace | this change | | 5 | Block-commitment tag: base spec or tiny DECK | cyberspace | pending | | 6 | DECK-0003 rewrite per D2, Status Draft, known-unsound note on the old text | cyberspace | pending, after 7 | | 7 | Simulator and scenario suite | cyberspace-cli | pending | | 8 | Fork rule: DECK-0001 §8 (both branches invalid) vs the ONOSENDAI client (older branch wins); make one true | cyberspace, ONOSENDAI | pending | | 9 | Virtual Spawn: answer open question 3; becomes DECK-0002 | cyberspace PR #15 | pending | | 10 | Encounter primitive: mutual-reference contact event with the box proof over both positions (§4.1) | cyberspace | pending, before 7 | | 11 | Fraud-proof event: compact evidence for hyperjump leaves; bisection or signed disagreement for sidesteps (§4.2); unifies with item 8 | cyberspace | pending, before 7 | | 12 | Spec sentence: an avatar is a last-known position; client liveness convention by event age (§4.3) | cyberspace, ONOSENDAI | pending | # What can be sold in Cyberspace **Status:** Design record, 2026-09-16 **Decided by:** arkinox, in review with Claude **Applies to:** `CYBERSPACE_V2.md` §6.4, §6.12, §7.6, §7.8, §13.2 Anyone who wants to build a business on this protocol runs into the same question within an hour: movement costs real work, so can I do that work for other people and be paid for it? The answer is yes, and it has a specific shape. The shape is not obvious, two plausible readings of the specification are wrong in opposite directions, and one sentence in the specification was itself wrong until this record was written. This document explains the shape slowly, because getting it wrong means building the wrong company. No part of this is a protocol change. It is an explanation of rules that already exist. --- ## 1. Three things that are easy to confuse When somebody says "selling movement," they could mean any of three different products with three different economics. | | what is sold | reusable by a second buyer | what the seller needs | |---|---|---|---| | **a crossing** | a finished proof that you moved | **never** | hardware | | **a root** | a number: one axis's Cantor root | yes, by anyone, forever | stored data | | **a job** | someone else's work, done for them | not applicable, each job is theirs | hardware | These are not three names for one thing. The protocol treats them completely differently, and it does so on purpose. --- ## 2. A crossing cannot be sold, and that is deliberate A **sidestep** is how you cross a wall: a boundary so high that computing the Cantor tree for it is infeasible, so you prove a cheaper Merkle tree instead. The Merkle tree's leaves are **seeded by your chain position** (§6.7). Concretely, the seed contains `previous_event_id`, the id of the last event in your own movement chain. Your tree is therefore yours alone. Nobody else's tree has the same leaves, so nobody else's proof has the same root, so your proof is worth exactly nothing to anybody else. §6.4 states the consequence plainly: a sidestep's work "is paid in full by every traveller and cannot be reduced, sold, or inherited from anyone else's published proof." **Why the protocol wants this.** §6.14 gives the reason, and it is worth reading as a warning rather than a footnote. Without the seed, the leaves would contain no identity and no chain context, so the root for a given subtree would be the same 32 bytes for every traveller in the history of the protocol. Destinations are deterministic and proofs are published in the clear, so **the first identity ever to cross a boundary would publish everything a later identity needs.** Crossings would be priced for the pioneer and free for everyone after. Walls would stop being walls the moment one person climbed one. So the seeding is what keeps a wall a wall. It is not friction the design failed to remove; it is the mechanism. --- ## 3. A root can be sold, and that is also deliberate A **hop** is how you move when the boundary is low enough to compute a Cantor tree for it. That tree's root is **canonical**: it depends only on the region, not on who computed it. Two people standing in the same place derive the same number without ever meeting, which is the property that makes location-derived keys work at all (§4.9). Canonical means transferable. If I compute a root and tell you the number, you have the same number I have, and it is as good in your hands as in mine. §6.12 is built on this, and its heading says it: **a crossing cannot be sold; a root can.** The section is careful to frame the sale as legitimate rather than as an exploit: "A region root is disclosed only when a holder chooses to disclose it, one recipient at a time, and that disclosure is exactly the act the protocol is built to support: handing someone the key to a place. Movement is priced; disclosure is a social act." ### 3.1 A root is one boundary, not a region This trips people up, so it is worth being exact. An axis root is `compute_subtree_cantor(base, h)`, where `h` is the **lowest common ancestor height** of the move's start and end on that axis, and `base` is the aligned block containing both (§4.6). Both numbers come from the specific move. So a root is not "the key to a region." It is one aligned block, on one axis, at one height. A different move inside the same neighbourhood has a different lowest common ancestor and therefore needs different roots. §4.4 makes the same point from another angle: a one-Gibson move that happens to cross a large power-of-two boundary is expensive, while a thousand-Gibson move inside one block is cheap. Cost follows which boundary you cross, not how far you travel. **Holding is the thing that covers a region.** §7.6 and §7.8 define holding as keeping every intermediate node of the trees, which is every aligned sub-region's root at every height. A holder can cross any boundary inside their holding because they have all of the roots, not one of them. Being handed a single root is a much smaller gift than holding, and the two are often spoken of as if they were the same. --- ## 4. Why being handed a root helps less than it sounds Until this record, §6.12 said that a recipient of a root could hop "for the cost of the temporal axis alone, roughly 100 ms, at any height." That was wrong, and the way it was wrong is instructive. ### 4.1 The numbers involved are enormous Cantor pairing is `π(a, b) = (a+b)(a+b+1)/2 + b`. It squares the sum of its inputs, so **the result is about twice as long as its inputs.** A tree of height `h` pairs `h` times, so the size doubles `h` times. Starting from 85-bit coordinates: | level | values left | bits in each | |---|---|---| | 0 | 256 | 85 | | 1 | 128 | 170 | | 2 | 64 | 340 | | 3 | 32 | 680 | | 8 | 1 | 21,760 | A root at height `h` is exactly `85 × 2^h` bits. That is not an approximation; it holds to the bit at every height, because each level doubles the width and halves the count. | height | one axis root | |---|---| | h16 | 680 KB | | h20 | 10.6 MB | | h24 | 170 MB | | h30 | 10.6 GB | | h34 | 170 GB | | h40 | 10.6 TB | ### 4.2 A hop is four stages, and a recipient skips one 1. **Build** three axis trees. 2. **Combine** them: `region_n = π(π(cantor_x, cantor_y), cantor_z)`. 3. **Temporal**: `cantor_t = compute_subtree_cantor(t_base, K)`, where `t_base` comes from your own `previous_event_id` (§5.3). 4. **Bind and hash**: `hop_n = π(region_n, cantor_t)`, then double SHA-256. A recipient skips stage 1 only. ### 4.3 The counting error Building three trees at h34 takes about **51.5 billion pairings**. Stages 2 to 4 take **three pairings and a hash**. Three against 51 billion looks like nothing, and that is the trap: the sentence counted operations instead of measuring them. Each of those three pairings multiplies numbers the size of a root. Measured, one pairing on operands of exactly root size: | height | operand size | one pairing | |---|---|---| | h10 | 11 KB | 1.6 ms | | h13 | 85 KB | 44 ms | | h16 | 680 KB | **766 ms** | | h18 | 2.7 MB | 6.9 s | At seven micrometres, **one** pairing already costs more than seven times the budget the old sentence gave the entire hop. ### 4.4 The tree is top-heavy, so the part you skip is the cheap part Level `k` of a tree has `2^(h-k-1)` pairings on operands of `85 × 2^k` bits. Each level therefore costs roughly 1.5 times the level below it, and the whole tree costs about three times its single final pairing. Measured, the final pairing alone is 23% to 33% of the entire build. Meanwhile the combine stages work on the largest numbers in the computation: `region_n` is about four times a root, and `hop_n` about eight times. **The combine costs more than all three tree builds put together.** Measured end to end, building trees and then combining: | height | three trees | combine and bind | what a root recipient saves | |---|---|---|---| | h12 | 30 ms | 124 ms | 19.7% | | h14 | 266 ms | 1,116 ms | 19.2% | | h16 | 2,358 ms | 10,030 ms | 19.0% | **About a fifth, not a bypass.** Stable across heights, and smaller still with faster multiplication, because better algorithms flatten the tree's top-heaviness and leave the combine dominating even more. ### 4.5 And part of what remains is yours alone `cantor_t` is derived from the recipient's own `previous_event_id`. A discloser cannot precompute it, cannot share it between recipients, and cannot reuse it on a second hop. Every hop by every person carries a term nobody else can supply. ### 4.6 So what a handed root really is A real saving of about a fifth on a real cost, to a recipient who already owns hardware of the discloser's class. `region_n` at h47 is on the order of petabytes; being handed the roots does not put that on a laptop. The answer to the question in §3.1, restated: **the root is the right root, and it still does not make the hop cheap.** --- ## 5. HOSAKA works, and it shows what the rules actually forbid Everything above reads like an argument that nobody can be paid to move anybody. HOSAKA has been selling exactly that for months, including sidesteps, which §6.4 says cannot be sold at all. Both are true, and the resolution is the important part of this document. **§6.4 forbids resale. It does not forbid service.** HOSAKA never sells one proof to many people. It takes your chain position, computes a proof seeded to you, and hands you a proof only you can use. Every customer's job is unique and priced in full. §6.4 is satisfied to the letter: every traveller pays in full. The only thing that changes is the currency, sats instead of your own electricity. The mistake to avoid is reading "this work product cannot be inherited" as "nobody can be paid to do this work." They are different sentences. **And the cost is the product, not an objection.** Nobody pays a compute service because the work is cheap. The Θ(2^h) combine that makes root-selling disappointing is exactly what makes a compute service viable. If the work were easy there would be no customers. **Seeding is what keeps this market honest.** Because no proof can be resold, no operator can compute the world once and rent the archive forever. Every job is fresh work, so a competitor enters with hardware rather than having to catch up on an accumulated secret. The moat is capital that depreciates, not knowledge that compounds. That is a healthier market than the alternative, and the protocol produced it without anybody designing a market. --- ## 6. The three business shapes | | movement service | root disclosure | holding | |---|---|---|---| | what it is | compute a traveller's proof for them | hand over a number | keep every root in a region | | example | HOSAKA | one person helping another | §7.6, §7.8 | | amortizes across customers | no | the tree build only, about 19% | fully, the keys are the product | | per-customer cost | full, `Θ(2^h)` | still `Θ(2^h)` for the buyer | none once built | | can a buyer leak it | nothing to leak | totally, one buyer can republish | totally | | what it sells | access to hardware | a saving, to a peer with hardware | reading and writing at a place | | capital | a cluster, which depreciates | storage | storage | The pattern worth naming: **movement is a service and keys are an asset.** They want different business models and different defences. A movement service has no leak risk and no compounding asset. Holding compounds and leaks completely. --- ## 7. A worked example: the mountain Here is the case that prompted this document. You arrive at a hyperspace landfall near a city. The last sixteen kilometres are a wall, and every traveller who wants to get from the landfall to the city pays about three GPU-hours to sidestep it. You would like to compute that crossing once and charge a toll. **As stated, this cannot work.** The last mile is a sidestep, sidestep proofs are seeded per-traveller, and there is no toll to collect because there is nothing transferable to sell. This is not an accident of the parameters; it is §6.14 doing its job. **Two things that do work:** *Run the crossing as a service.* Take a traveller's chain position, do their three GPU-hours on your hardware, hand them their proof. That is HOSAKA's shape, it is permitted, it is honest, and nothing leakable ever leaves your hands. What you own is a cluster, and you are renting it. *Hold the destination and sell what holding buys.* Compute the region around the city and keep the trees. Now you can read and write at any height inside it (§7.8). That is a genuine asset with zero marginal cost per customer, and it is the thing you can build once and sell many times. **But the second one leaks.** A region key is a number; one buyer can publish it and the asset is gone. A referral commission makes reselling more attractive than leaking for the marginal actor, but it does not stop the one defector, and it is self-undermining, because growing the buyer set is exactly what raises the chance that one of them defects. The construction that removes the risk instead of pricing it is to sell the *service* rather than the key: keep the root and encrypt or decrypt on request. You become a trusted party, but a competitive one, since anyone willing to do the work can undercut you. **One coupling to notice before building any of this.** Whoever can make a place reachable is, as a free byproduct, whoever can read everything hidden there. Capital-intensive reachability selects for exactly the kind of actor the location encryption exists to resist. §6.4's seeding is the protocol's answer, and it is the reason the last mile cannot be capitalised away. --- ## 8. What was corrected, and when Written 2026-09-16, from measurements taken the same day. | where | was | is | |---|---|---| | §6.12 | a handed root costs "roughly 100 ms, at any height" | about a 19% saving, the rest `Θ(2^h)` with a per-traveller term | | §7.8 | holding writes and derives keys "at once" | about 4.9 times faster, roughly two heights, still hours at h34 | | §9.9 | a root is "about 86 × 2^h bits"; peak is "two levels live" | exactly `85 × 2^h` bits; peak is about 6.1 roots | | §10 | sectors "fit into u32 systems" | a sector index needs 55 bits per axis | | §13.2 | "storage-bound, not compute-bound" | capacity-bound on the ceiling, compute-bound beneath it by about 28 times | Every one of these made the work sound cheaper or smaller than it is, which is the direction an author's optimism always points. The arithmetic is in §4 above and reproduces on any machine.