Introduction
,____ _____ _____ ____ _____ _____ ___ ,______
| _ \| ____| ____| _ \ / _ \ \/ /_ _| _ \| ____|
| |_) | _| | _| | |_) | | | \ / | || | | | _|
| __/| |___| |___| _ <| |_| / \ | || |_| | |___
|_| |_____|_____|_| \_\\___/_/\_\___|____/|_____|
ENCRYPTED BY DEFAULT. PSEUDONYMOUS BY DESIGN.
NO SERVERS. NO ACCOUNTS. NO GATEKEEPERS.
TRUST NO ONE. TALK TO ANYONE.
peeroxide-cli is a command-line toolkit for interacting with the peeroxide P2P networking stack. It provides a set of tools for peer discovery, connectivity diagnostics, and decentralized data transfer, all while maintaining full wire-compatibility with the existing Hyperswarm and HyperDHT networks.
The binary is named peeroxide.
Core Tools
The toolkit consists of eight primary commands:
- init: Generate configuration files and install man pages.
- lookup: Query the DHT to find peers announcing a specific topic.
- announce: Announce your presence on a topic so others can discover you.
- ping: Diagnose reachability through bootstrap checks, NAT classification, or targeted peer pings.
- cp: Transfer files directly between peers over an encrypted swarm connection.
- dd (Dead Drop): Perform anonymous store-and-forward messaging via the DHT. The
ddcommand supports both v1 and v2 protocols, with v2 auto-selected for new put operations. - chat: Join topic-based interactive chat rooms.
- node: Run a long-running DHT bootstrap / coordination node.
Quick Start
It’s recommended to run peeroxide init first to generate a default configuration and install system manual pages.
Key Concepts
To use peeroxide effectively, it helps to understand the underlying architecture:
- DHT and Routing: How the Kademlia-based Distributed Hash Table handles peer discovery and routing.
- Keys and Identity: How Ed25519 keypairs define peer identity and secure connections.
- Topics and Discovery: How peers group together using 32-byte topic keys.
init
The peeroxide init command handles environment setup by generating configuration files or installing man pages. It provides a non-interactive way to bootstrap your local environment before running other peeroxide subcommands.
Command Modes
The init command operates in two mutually exclusive modes.
Config Mode (Default)
In its default mode, init writes a fresh config.toml file to your configuration directory. It includes a [network] table and commented examples of available fields.
- First run: Creates parent directories and writes the file.
- Rerun without flags: Prints a message stating the config already exists and exits with code 0.
- Rerun with
--force: Overwrites the existing file entirely. - Rerun with
--update: Merges newnetwork.publicornetwork.bootstrapvalues into the existing file while preserving comments and formatting.
Man-page Mode
When invoked with --man-pages, the command skips configuration and instead generates and installs system man pages.
CLI Flags
| Flag | Type | Default | Description |
|---|---|---|---|
--force | bool | false | Overwrites an existing config file. Conflicts with --update. |
--update | bool | false | Updates specific fields in an existing config. Requires --public or --bootstrap. Conflicts with --force. |
--public | bool | false | Sets network.public = true. Adds default public HyperDHT bootstrap nodes. |
--bootstrap <ADDR> | Vec<String> | [] | Sets network.bootstrap. Repeatable. In update mode, this replaces the entire bootstrap list. |
--man-pages [PATH] | PathBuf | /usr/local/share/man/ | Installs generated man pages. Writes to PATH/man1/. |
Flag Conflicts
--man-pagescannot be used with--force,--update,--public, or--bootstrap.--forceand--updateare mutually exclusive.--updaterequires at least one field to change (--publicor--bootstrap).
Global CLI Flags
The peeroxide binary defines several global flags that apply to most subcommands. peeroxide init itself only consumes two of them:
--config <FILE>— used as the target write path (and as the source path for--update).-v/--verbose— controls tracing verbosity.
The remaining global flags listed below are accepted by the parser but do not affect init (which has its own local --public and --bootstrap flags applied to the generated/updated config). They take effect on subcommands that do DHT work (lookup, announce, ping, cp, dd, chat, node).
| Flag | Type | Description |
|---|---|---|
-v, --verbose | u8 count | Increases logging level. -v for info, -vv for debug. RUST_LOG overrides this. (Used by init.) |
--config <FILE> | String | Specifies a custom path for the config file. For init, this is the write target. |
--no-default-config | bool | Skips loading the default configuration file. (Not consumed by init.) |
--public | bool | Includes default public HyperDHT bootstrap nodes. (Not consumed by init; init has its own local --public for the generated config.) |
--no-public | bool | Excludes default public HyperDHT bootstrap nodes. Conflicts with --public. (Not consumed by init.) |
--bootstrap <ADDR> | Vec<String> | Adds a bootstrap node address (host:port). Repeatable. (Not consumed by init; init has its own local --bootstrap for the generated config.) |
Config File Locations
Target Path Precedence (init)
When init determines where to write the config file, it follows this order:
- Path provided via
--config <FILE> - Environment variable
$PEEROXIDE_CONFIG $XDG_CONFIG_HOME/peeroxide/config.toml~/.config/peeroxide/config.toml- Default fallback
.config/peeroxide/config.toml
Runtime Load Precedence
When running commands, peeroxide loads configuration in this order:
- Path provided via
--config <FILE> - Environment variable
$PEEROXIDE_CONFIG $XDG_CONFIG_HOME/peeroxide/config.toml- Platform-specific config directory (e.g.,
Library/Application Supporton macOS) ~/.config/peeroxide/config.toml
Config Schema
The config file uses the TOML format.
[network]
| Field | Type | Default | Description |
|---|---|---|---|
public | bool | None | If true, adds public bootstrap nodes. If false, removes them. |
bootstrap | Vec<String> | None | List of host:port or ip:port bootstrap addresses. |
[node]
| Field | Type | Default | Description |
|---|---|---|---|
port | u16 | 49737 | The local port to bind for DHT operations. |
host | String | "0.0.0.0" | The local address to bind. |
stats_interval | u64 | 60 | Interval in seconds for logging node statistics. |
max_records | usize | 65536 | Maximum number of DHT records to store. |
max_lru_size | usize | 65536 | Maximum size of the LRU cache for routing. |
max_per_key | usize | 20 | Maximum records allowed per key. |
max_record_age | u64 | 1200 | Maximum age in seconds for DHT records. |
max_lru_age | u64 | 1200 | Maximum age in seconds for LRU entries. |
[announce] and [cp]
These tables are currently empty and reserved for future use.
Bootstrap Resolution
Peeroxide resolves the bootstrap-node list in two stages: a base-list selection from CLI/config (CLI overrides), then a public-default adjustment.
Stage 1 — pick the base list (in peeroxide-cli/src/config.rs):
- If
--bootstrap <ADDR>was supplied (one or more times) on the command line, use only those CLI bootstraps for the base list. The config file’snetwork.bootstrapis ignored in this case. - Otherwise, use the
network.bootstraplist from the config file (if any). - If neither source supplied bootstraps, the base list starts empty.
Stage 2 — apply the public-default adjustment (in peeroxide-cli/src/cmd/mod.rs::resolve_bootstrap):
- If
public=true(via flag or config), add the default public HyperDHT bootstrap nodes to the base list. - If the list is still empty after step 1, automatically add the default public HyperDHT bootstrap nodes (so a fresh install with no config and no flags still connects).
- If
public=false(via--no-publicor config), remove all default public bootstrap nodes from the list.
This ensures the node is never isolated unless specifically requested by combining --no-public with an empty bootstrap list. The --no-public flag replaces the legacy --firewalled flag behavior.
Note: this resolution happens at runtime in subcommands that do DHT work (lookup, announce, ping, cp, dd, chat, node). peeroxide init uses its own local --public and --bootstrap flags to populate the generated/updated config file; the base-list selection and public-default adjustment do not run during init.
Man-page Installation
When running peeroxide init --man-pages, the tool:
- Identifies the target directory (default
/usr/local/share/man/). - Ensures the
man1/subdirectory exists. - Cleans up any existing
peeroxide*.1files in that directory. - Writes fresh man pages for the main command and all subcommands.
Exit Codes
0: Success.1: Runtime error, file system error, or TOML parsing error.2: Usage error or invalid arguments provided to the CLI.
DHT and Routing
Peeroxide uses a Kademlia-based Distributed Hash Table (DHT) for peer discovery and coordination. This DHT is wire-compatible with the HyperDHT network used by Hyperswarm.
Kademlia Basics
The DHT operates on several core Kademlia principles:
- XOR Distance: The “distance” between two nodes or a node and a key is calculated using the bitwise XOR of their 32-byte IDs. This metric defines the topology of the network.
- Routing Table & k-buckets: Each node maintains a routing table organized into buckets (k-buckets). Each bucket covers a specific range of distances from the node’s own ID.
- Iterative Lookup: Finding a value or node involves querying the closest known nodes to the target key, which then return even closer nodes, eventually converging on the target.
Peeroxide relies on the pkarr and mainline crates for much of its underlying DHT logic.
Bootstrap Nodes
A DHT is a decentralized network, but new nodes need an entry point to join. These entry points are called bootstrap nodes.
- Public Network: By default,
peeroxideuses a set of stable public bootstrap nodes to connect to the global HyperDHT network. If neither the config file’snetwork.bootstrapnor the command-line--bootstrapflag supplies any nodes, the runtime auto-fills the public bootstrap set so a fresh install still connects. - Configuration: You can supply custom bootstrap nodes via the
--bootstrapflag or thenetwork.bootstrapsetting in your config file. Note: CLI--bootstrapoverrides the config file’snetwork.bootstraprather than combining with it. - Public Default Adjustments:
--publicexplicitly adds the default public bootstrap nodes (useful when you have custom bootstraps but also want public connectivity).--no-publicexplicitly removes them from the resolved list. - Isolated Mode: Combining
--no-publicwith no custom bootstraps (and nonetwork.bootstrapin the config) yields an empty bootstrap list. In that state, the node has no entry point and can only be reached by peers who already know its address.
Connectivity
The DHT doesn’t just store peer records; it also facilitates connectivity:
- Holepunching: The DHT helps two firewalled peers coordinate a direct UDP connection.
- Relaying: If a direct connection is impossible, the DHT can help set up a relayed connection through other nodes.
For more details on how these primitives are used in practice, see the lookup and announce command documentation.
DHT Primitives
This page is a reference for the four core operations that peeroxide-dht exposes and that every higher-level subsystem (announce, lookup, cp, dd, chat) is built on top of. Once you understand DHT and Routing at the conceptual level, this is the next layer down: the actual operations you can perform against the network.
immutable_put / immutable_get — Content-Addressed Storage
Stores arbitrary bytes on DHT nodes, addressed by the BLAKE2b-256 hash of the value itself. Content-addressed: you can only retrieve a value if you already know its hash.
immutable_put(value: &[u8])— computestarget = hash(value), queries the K closest nodes to that target, commits the raw bytes. Returns the 32-byte hash.immutable_get(target: [u8; 32])— queries nodes closest totarget; any node that has the value returns it. The client verifieshash(returned_value) == target.
| Property | Detail |
|---|---|
| Data stored | Raw Vec<u8> — arbitrary bytes, no signing, no keys, no seq |
| Addressing | hash(value) — immutable; changing the value yields a different address |
| Max payload | ~900–1000 bytes (UDP framing; no explicit code constant) |
| Wire commands | IMMUTABLE_PUT = 8, IMMUTABLE_GET = 9 |
| Discoverability | The reader must already know the hash (given out-of-band or via a mutable pointer) |
mutable_put / mutable_get — Signed, Updateable Storage
Stores arbitrary bytes signed by an Ed25519 keypair, addressed by hash(public_key). The owner can update the value by incrementing a sequence number.
mutable_put(key_pair, value: &[u8], seq: u64)— computestarget = hash(public_key), signs(seq, value)with the secret key, and sendsMutablePutRequest { public_key, seq, value, signature }to the closest nodes.mutable_get(public_key: &[u8; 32], seq: u64)— queries withtarget = hash(public_key)and a requested minimumseq. Nodes return the stored value only ifstored.seq >= requested_seq. The client verifies the signature.
| Property | Detail |
|---|---|
| Data stored | { public_key: [u8;32], seq: u64, value: Vec<u8>, signature: [u8;64] } |
| Addressing | hash(public_key) — one mutable slot per keypair |
| Max payload (value) | ~1002 bytes (token present, seq ≤ 252; derived in Size Budget for mutable_put below) |
| Seq semantics | Strictly monotonic. SEQ_REUSED (16) error if equal; SEQ_TOO_LOW (17) if lower |
| Salt support | Not implemented — there is no salt field; one record per keypair |
| Wire commands | MUTABLE_PUT = 6, MUTABLE_GET = 7 |
announce / lookup — Peer Discovery and Rendezvous
Originally designed as peer-discovery primitives — store structured peer records (public key + relay addresses) under a topic hash. In this workspace they are also used as a general-purpose rendezvous mechanism: the announcer’s public key acts as a pointer to a further mutable_put slot containing the actual record. See The Rendezvous Pattern below.
announce(target: [u8;32], key_pair, relay_addresses)— queries the closest nodes for the topic and sends a signedAnnounceMessagecontainingHyperPeer { public_key, relay_addresses }. Multiple peers can announce under the same topic simultaneously.lookup(target: [u8;32])— queries the closest nodes; they returnLookupRawReply { peers: Vec<HyperPeer>, bump }— all peers that have announced on that topic (up to 20 per node).
| Property | Detail |
|---|---|
| Data stored | HyperPeer { public_key: [u8;32], relay_addresses: Vec<Ipv4Peer> } |
| Multi-writer | Yes — up to 20 announcers per topic per node |
| IP in stored record | No — the source IP is not stored in HyperPeer; only the pubkey + relay addresses |
| Announce with no addresses | Allowed — relay_addresses = [] is valid |
MAX_RECORDS_PER_LOOKUP | 20 per node (per-node cap; the total across all queried nodes can exceed 20) |
MAX_RELAY_ADDRESSES | 3 (truncated on store) |
| Wire commands | LOOKUP = 3, ANNOUNCE = 4, FIND_PEER = 2 |
Key differences from put/get:
lookup/announceis multi-writer — many peers announce under one topic.put/getis single-writer — one value per address.announcestores a small structured record;putstores opaque bytes (up to ~1000 B per record).
The Rendezvous Pattern
An announce “topic” is just a 32-byte address. There’s no constraint that it correspond to a real peer-discoverable resource — it can be:
- a hash of an application-meaningful key,
- a deterministic derivation from any shared input (a string, a public key, a secret, a counter, …) via BLAKE2b-256 or any other 32-byte hash,
- or even a random value, if both writer and reader can agree on it out-of-band.
This makes announce / lookup a generic rendezvous primitive: anyone who can independently arrive at the same 32-byte topic can find every other announcer at it. And because the only structured field the protocol actually requires the announcer to publish is a 32-byte public_key, that pubkey can be treated as a pointer — typically to a mutable_put slot owned by that same ephemeral keypair — rather than as a literal peer identity.
The generic three-step pattern is:
- Derive a topic — any agreed-upon function
f(...) -> [u8;32]. Writer and reader must arrive at the same value. - Writer — generate an ephemeral keypair
k, publish the actual record atmutable_put(k, value, seq), andannouncek.public_keyon the topic. Therelay_addressesfield carries no meaning and is typically left empty. - Reader —
lookupthe topic, thenmutable_geteach returned pubkey to retrieve the actual records in parallel.
This sidesteps mutable_put’s one-record-per-pubkey limitation: a single topic can host many simultaneous writers, each with its own independent mutable_put slot. The TTLs on the rendezvous record and on the payload record are also independent, so a writer can refresh them on different cadences.
Increasing Footprint with Epochs and Buckets
The per-node cap is 20 announcers per topic per node. Two complementary techniques extend that footprint by embedding deterministic salt into the topic derivation:
- Epochs — incorporate a quantized timestamp (for example,
floor(unix_secs / 60)). The topic rotates over time automatically; writer and reader both use the current epoch when deriving the topic, and readers typically scan a small backward window so announcers near a boundary aren’t missed. Epoch rotation also bounds how long an observer can correlate a single topic. - Buckets — incorporate a small integer
0..N. Writers hash to one of N possible topics (deterministically or at random); readers scan all N in parallel to find every announcer.
Combining the two yields epoch_window × bucket_count distinct topics for the same logical rendezvous — e.g. a 2-epoch window with 4 buckets gives an effective capacity of 8 × 20 = 160 announcers per node, all discoverable in 8 parallel lookups.
Concrete uses in this workspace
The chat and dd v2 subsystems each lean on this pattern. Their exact topic-derivation rules, epoch/bucket counts, and write/read flows are documented alongside the rest of their wire formats and protocols:
- chat — see Wire Format and Protocol.
- dead drop v2 — see Architecture and Wire Format.
TTL (Time-To-Live)
All stored values are ephemeral — they expire from node storage.
| Storage type | TTL (default) |
|---|---|
Announcement records (RecordCache) | 20 minutes (max_record_age) |
| Mutable / immutable LRU cache | 20 minutes (max_lru_age) |
| Router forward entries | 20 minutes (DEFAULT_FORWARD_TTL) |
Clients must periodically re-announce / re-put to keep data alive. The 20-minute default matches the Node.js reference implementation. Both cp and dd issue periodic refreshes during long-running operations for exactly this reason.
Size Budget for mutable_put
The most common protocol-design question is “how many bytes can I put inside one mutable_put value?” Starting from libudx’s MAX_PAYLOAD = 1180 and subtracting the wire overhead for a mutable_put request with the routing token present and seq ≤ 252:
1180 libudx MAX_PAYLOAD
- 75 outer RPC Request fixed fields (type, flags, tid, to, token, command, target)
- 3 outer compact-encoding length prefix for put_bytes
- 32 public_key field
- 1 seq compact-encoding (1 byte for seq ≤ 252)
- 3 inner compact-encoding length prefix for value
- 64 signature
─────
1002 bytes available for the message value payload
In practice the higher-level subsystems reserve a small margin and call this MAX_RECORD_SIZE = 1000 (see chat::wire and deaddrop::v2::wire). Subtract per-record framing — author pubkey, timestamp, content type, signature, length-prefix bytes — to derive the payload budget for your own protocol. The chat subsystem’s Reference and dead drop’s Wire Format chapters carry the exact per-record overhead and the resulting content budgets.
Keys and Identity
Identity in peeroxide is anchored in cryptography. Every peer is identified by an Ed25519 keypair.
Peer Identity
A peer’s Public Key is its stable identity on the network. This key is 32 bytes long and is typically represented as a 64-character hexadecimal string, often prefixed with an @ symbol in the CLI (e.g., @ab12cd34...).
Keypair Types
When running peeroxide commands, you can use different types of keypairs:
- Ephemeral Keypairs: Generated randomly for a single session. These are used by default when no seed is provided. Once the process exits, the identity is lost.
- Seeded Keypairs: Derived deterministically from a secret seed string using the
--seedflag. The seed is hashed to produce a 32-byte secret, which is then used to generate the Ed25519 keypair. This allows a peer to maintain a stable identity across multiple runs.
Secure Connections
Peeroxide uses the Noise XX handshake protocol to establish authenticated, end-to-end encrypted connections between peers.
- Authentication: During the handshake, peers exchange and verify their public keys. This ensures that you are communicating with exactly the peer you intended to, and that no man-in-the-middle can impersonate them.
- Encryption: Once the handshake is complete, all data is sent over a
SecretStream, which provides confidentiality and integrity.
For more information on the cryptographic protocols, see the documentation for the ed25519-dalek and noise-protocol crates.
Topics and Discovery
In peeroxide, discovery is organized around topics. A topic is a 32-byte key that serves as a rendezvous point for peers with shared interests.
What is a Topic?
Technically, a topic is always a 32-byte value. However, the CLI allows you to specify topics in two ways:
- Raw Hex: A 64-character hexadecimal string is interpreted directly as the 32-byte topic key.
- Plaintext Name: Any other string is hashed using BLAKE2b-256 to derive a 32-byte topic key. For example, the topic name
"my-application"becomes the hash of those bytes.
This dual approach allows for both human-readable “namespaced” discovery and opaque, randomly generated “private” rendezvous points.
How Discovery Works
The discovery process involves two main actions:
Announcing
When you announce on a topic, you are telling the DHT that your peer identity (public key) is available for connections related to that topic. The swarm automatically handles re-announcing at regular intervals to ensure your record stays fresh on the DHT.
Looking Up
When you lookup a topic, you query the DHT for the public keys and addresses of all peers currently announcing on that topic.
Usage in Tools
Topic-based discovery is the foundation for several peeroxide commands:
- lookup: Find peers on a topic.
- announce: Join a topic.
- cp: Uses a topic as a one-time rendezvous point for a file transfer.
- ping: Can resolve a topic name to find and ping the associated peers.
By using the same topic name or hex key across different tools and peers, you can easily build decentralized workflows without needing a central coordinator.
Lookup Overview
The lookup command queries the Distributed Hash Table (DHT) to discover peers announcing a specific topic. It provides a way to find connection points (relay addresses and public keys) for any given service or dataset.
Topic Resolution
Topics can be provided as either a plaintext string or a raw 64-character hexadecimal key.
- Raw Key: If the input is exactly 64 hex characters, it is treated as a raw 32-byte key.
- Plaintext: Otherwise, the input string is hashed using BLAKE2b-256 via the
discovery_keyfunction to derive the target topic key.
Usage
peeroxide lookup <TOPIC> [FLAGS]
Flags
| Flag | Description |
|---|---|
--with-data | Fetch metadata stored on the DHT for each discovered peer. |
--json | Output results as Newline Delimited JSON (NDJSON) to stdout. |
Peer Discovery and Deduplication
The lookup command identifies unique peers by their 32-byte public key. If multiple DHT records are found for the same public key, their relay addresses are merged into a single union set, ensuring no duplicate addresses are displayed for a single peer.
Peers are displayed in the order they were first seen during the lookup process.
Metadata Retrieval
When the --with-data flag is used, peeroxide performs a mutable_get for each discovered peer’s public key (using seq=0). This process runs concurrently with a concurrency limit of 16 to ensure high performance even when many peers are found.
The status of the data retrieval is reported for each peer, indicating whether data was found, missing, or if an error occurred during retrieval.
See Also
- Output Formats for details on human-readable and JSON output.
Lookup Output Formats
The lookup command supports two output modes: human-readable (default) and NDJSON.
Human-Readable Output
By default, lookup writes diagnostic information and peer records to stderr. The stdout stream remains empty.
Header
The output begins with the resolved topic:
LOOKUP <hex>(if raw hex was provided)LOOKUP blake2b("topic")(if plaintext was provided)found <N> peers
Peer Record
For each peer found:
@<hex_public_key>relays: host:port, ...(or(direct only)if no relays are registered)
Metadata (with --with-data)
If data is successfully retrieved:
data: "string"(escaped UTF-8) or0x<hex>(if binary)seq: <u64>
Statuses for missing or failed data:
data: (not stored)data: (error: <message>)
JSON Output (NDJSON)
When the --json flag is used, lookup emits Newline Delimited JSON to stdout. Diagnostic logs may still appear on stderr.
Peer Schema
Each discovered peer is emitted as a separate JSON object:
{
"type": "peer",
"public_key": "<hex>",
"relay_addresses": ["host:port", ...]
}
Peer Schema (with --with-data)
If metadata is requested, the peer object includes status fields:
-
Success:
{ "type": "peer", "public_key": "...", "relay_addresses": [...], "data_status": "ok", "data": "<string>", "data_encoding": "utf8"|"hex", "seq": <u64> }Note: The
datafield contains raw hexadecimal characters (without a0xprefix) when the encoding is"hex". -
Missing:
{ "type": "peer", "data_status": "none", "data": null, "seq": null, ... } -
Error:
{ "type": "peer", "data_status": "error", "data": null, "seq": null, "error": "<message>", ... }
Summary Schema
A final summary object is emitted after all peers have been processed:
{
"type": "summary",
"topic": "<hex>",
"peers_found": <int>
}
Exit Codes
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Fatal error (e.g., DHT failure, invalid arguments) |
| 130 | Terminated by SIGINT (Ctrl+C) |
Announce Overview
The announce command makes your node discoverable on the DHT for a specific topic. It allows other peers using lookup to find your public key and connection details.
Identity and Keypairs
By default, announce generates a random ephemeral keypair for each session.
- Ephemeral: A new identity is created on startup and lost when the process exits.
- Seeded: Using the
--seed <string>flag, you can derive a deterministic keypair. The seed is hashed using thediscovery_keyfunction to produce the 32-byte secret seed.
Usage
peeroxide announce <TOPIC> [FLAGS]
Flags
| Flag | Description |
|---|---|
--seed <string> | Use a seed to maintain a stable identity across restarts. |
--data <string> | Store metadata (max 1000 bytes) on the DHT. |
--duration <sec> | Exit automatically after the specified number of seconds. |
--ping | Enable the Echo Protocol to accept and respond to connectivity probes. |
Metadata
The --data flag allows you to attach a small payload (up to 1000 UTF-8 bytes) to your DHT record. This is useful for sharing service versioning, protocol capabilities, or small state updates.
- Sequence Numbers: Metadata is stored with a sequence number (
seq) based on the current Unix epoch in seconds. - Refresh: To ensure your record does not expire,
announceautomatically refreshes the metadata every 600 seconds (10 minutes) while the process is running.
Output
All output from announce is written to stderr. The stdout stream is always empty.
Startup
ANNOUNCE blake2b("topic") as @<pk_hex>announced to closest nodesmetadata: "..." (<N> bytes, seq=<u64>)(if applicable)
Shutdown
UNANNOUNCE blake2b("topic")(orUNANNOUNCE <hex>if raw hex was provided)done
Exit Codes
| Code | Meaning |
|---|---|
| 0 | Success (including exit via SIGINT or SIGTERM) |
| 1 | Fatal error (e.g., DHT failure, invalid data size) |
See Also
- Architecture for internal implementation details.
- Echo Protocol for details on the
--pingmode.
Announce Architecture
The announce command manages a long-running swarm session, coordinating DHT presence and optionally handling incoming connections.
Initialization Flow
- Identity Generation: A
KeyPairis either generated randomly or derived from a seed. - Swarm Setup: A
SwarmConfigis constructed with the identity and DHT configuration. The--public/network.publicsetting drives bootstrap node selection (see init/overview.md → Global CLI Flags); it does not change firewall semantics. - Joining Topic: The node joins the topic using
JoinOpts { client: false }. This instructs the DHT to act as a server for this topic, making the node discoverable to lookup queries. - Flushing: The node waits for the join operation to flush, ensuring at least one announcement has reached the DHT.
Metadata Persistence
If the --data flag is provided, the node performs an initial mutable_put to the DHT.
- Storage: The data is signed by the node’s private key and stored at the node’s public key address on the DHT.
- Sequence: The
seqfield is set to the current Unix epoch in seconds. - Lifecycle: A background task triggers every 600 seconds to re-put the data, preventing expiration and keeping the DHT record fresh.
Connection Management
When a peer discovers this node via lookup, they may attempt to open a direct UDX connection.
- Accepting: The
announceloop listens on theconn_rxchannel for incoming connections. - Modes:
- Default: Incoming connections are immediately dropped to minimize resource usage.
- Echo Mode (
--ping): Connections are accepted and passed to the Echo Protocol handler.
Shutdown Sequence
The command remains active until it receives a termination signal (SIGINT or SIGTERM) or the --duration timer expires.
- Unannounce: The node sends a
leaverequest to the DHT for the active topic. - Cleanup: The background refresh task is aborted, and the swarm handle is destroyed.
- Exit: The process exits with code 0.
Echo Protocol
The Echo Protocol is a simple diagnostic service enabled by the --ping flag in the announce command. It allows remote peers to measure latency and verify end-to-end connectivity over the peeroxide swarm.
Protocol Constants
- PING_MAGIC:
0x50 0x49 0x4E 0x47(b"PING") - PONG_MAGIC:
0x50 0x4F 0x4E 0x47(b"PONG") - MAX_ECHO_SESSIONS: 64 (concurrency limit)
- HANDSHAKE_TIMEOUT: 5 seconds
- IDLE_TIMEOUT: 30 seconds
- ECHO_MSG_LEN: 16 bytes
Echo Probe Frame
Each probe message is exactly 16 bytes long and follows this layout:
| Offset | Size | Field | Description |
|---|---|---|---|
| 0 | 8 | seq | u64, little-endian sequence number |
| 8 | 8 | timestamp_nanos | u64, little-endian Unix timestamp in nanoseconds |
Session Lifecycle
- Accept: The server accepts an incoming UDX connection.
- Semaphore Acquisition: The server attempts to acquire a permit from a 64-slot semaphore. If all slots are full, the connection is dropped.
- Handshake:
- The server waits up to 5 seconds for the client to send the
PING_MAGIC(4 bytes). - If the magic matches, the server replies with
PONG_MAGIC(4 bytes).
- The server waits up to 5 seconds for the client to send the
- Echo Loop:
- The server enters a loop, waiting for messages from the client with a 30-second idle timeout.
- Each message must be exactly 16 bytes (
ECHO_MSG_LEN). - The server echoes the exact 16-byte message back to the client.
- Disconnect: The session ends if the timeout is reached, a message of incorrect length is received, or the stream is closed.
- Release: The semaphore permit is released, allowing a new session to begin.
Sequence Diagram
sequenceDiagram
participant Client
participant Server
Client->>Server: Connect (UDX)
Note over Server: Acquire Semaphore (max 64)
Client->>Server: "PING" (4 bytes)
Server->>Client: "PONG" (4 bytes)
loop Echo Loop
Client->>Server: 16-byte Probe
Server->>Client: 16-byte Probe (Echo)
end
Note over Client,Server: Idle Timeout (30s) or Close
Note over Server: Release Semaphore
Logging
The announce command logs session events to stderr:
[connected] @<pk> (echo mode)[disconnected] @<pk> (<N> probes echoed)
Ping Overview
The ping tool is a multi-purpose diagnostic utility for the Peeroxide network. It allows you to verify connectivity to bootstrap nodes, classify your local NAT type, and perform targeted probes of peers discovered on the DHT.
Usage
peeroxide ping [target] [flags]
Targets
The ping tool behaves differently depending on the provided target:
- No target: Performs a bootstrap check. It probes all configured bootstrap nodes to populate the local routing table, collect reflexive addresses, and classify your NAT type.
host:port: Performs a direct UDP probe to a specific network address.@<64-char-hex-pubkey>: Resolves the peer’s relay addresses viafind_peerand probes them.<topic>: Resolves up to 20 peers announcing on the given topic (specified as a plain string or 64-char hex) and probes their relay addresses.
Operational Modes
Bootstrap Check
When run without a target, ping sends CMD_FIND_NODE requests to bootstrap nodes. This process:
- Verifies reachability to the core network.
- Discovers your reflexive (public) IP and port as seen by multiple nodes.
- Classifies your NAT Type based on the consistency of these reflexive addresses.
- Populates your local DHT routing table with closer nodes.
UDP Probing (Direct, PubKey, Topic)
Standard probes use the DHT CMD_PING RPC. This is a lightweight UDP-based check that verifies the target node is online and responding at the network level.
Connection Probing (--connect)
When the --connect flag is used with a PubKey or Topic target, the tool performs a full Noise XX handshake and establishes a SecretStream. After the secure connection is established, it executes the Echo Protocol to measure end-to-end encrypted latency.
Flags
--count <N>: Number of probes to send (default: 1). Set to0for infinite probing.--interval <seconds>: Delay between probes (default: 1.0s).--connect: Attempt a full Noise handshake and Echo protocol test.--json: Output results as newline-delimited JSON (NDJSON) for machine consumption.--public: Use the public bootstrap network (shorthand for adding default bootstrap nodes).
Exit Codes
0: All probes succeeded.1: Partial or total failure (e.g., timeouts, resolution errors).130: Interrupted by SIGINT (Ctrl+C).
Ping Architecture
The ping tool operates at two distinct layers of the Peeroxide stack: the DHT RPC layer (UDP) and the Secure Stream layer (encrypted TCP-like transport).
Control Flow
The primary logic resides in peeroxide-cli/src/cmd/ping.rs. Depending on the target type, it orchestrates a resolution phase followed by a probing phase.
Targeted Probing with --connect
When using @pubkey or <topic> with the --connect flag, the tool follows this sequence:
- Resolution: Queries the DHT (
find_peerorlookup) to discover relay addresses for the target peer(s). - Handshake: Initiates a Noise XX handshake via the discovered relay addresses.
- SecretStream: Establishes a multiplexed, encrypted stream.
- Echo Protocol: Once the stream is ready, it initiates the Echo Protocol.
sequenceDiagram
participant CLI as ping --connect
participant DHT as HyperDHT
participant Peer as Remote Peer
CLI->>DHT: find_peer(remote_pk)
DHT-->>CLI: relay_addresses[]
rect rgb(240, 240, 240)
Note over CLI, Peer: Noise XX + SecretStream
CLI->>Peer: Noise Handshake (connect_with_nodes)
Peer-->>CLI: Handshake Complete
end
rect rgb(220, 240, 220)
Note over CLI, Peer: Echo Protocol
CLI->>Peer: PING (4 bytes)
Peer-->>CLI: PONG (4 bytes)
loop Every interval
CLI->>Peer: Probe [seq, timestamp] (16 bytes)
Peer-->>CLI: Echo [seq, timestamp] (16 bytes)
Note right of CLI: Latency = send_time.elapsed()
end
end
NAT Classification Logic
During a bootstrap check (no target), the tool collects reflexive addresses (Ipv4Peer) returned by multiple bootstrap nodes in their response to field.
The classification is performed by the NatType enum:
- Open: The reflexive IP is a local interface address (verified by attempting to
bindthe IP) AND the reflexive port matches the local bound port. - Consistent: All reflexive samples report the same
host:port, but the IP is external. This indicates a hole-punchable NAT. - Random: The reflexive host is the same across samples, but the ports vary. This typically requires relaying.
- MultiHomed: Different bootstrap nodes report different reflexive hosts.
- Unknown: No reflexive address samples were collected.
DHT Interaction
- Bootstrap mode: Uses
dht.ping(host, port)which translates to aCMD_FIND_NODErequest on the wire. This is used because bootstrap nodes are expected to return closer nodes to help populate the routing table. - Direct/Targeted mode: Uses the same
dht.pingmechanism but focuses on the RTT and reachability of the specific target.
Ping Output Formats
The ping tool provides human-readable output on stderr by default and machine-parseable NDJSON on stdout when the --json flag is used.
Human-Readable Output (stderr)
Bootstrap Check
BOOTSTRAP CHECK (3 nodes)
bootstrap1.example.com:49737 OK 12ms (20 nodes) node_id=ab12...
bootstrap2.example.com:49737 TIMEOUT
--- bootstrap summary ---
3 nodes, 2 reachable, 1 unreachable
50 unique peers discovered via routing tables
public address: 1.2.3.4:5000 (consistent across 2 nodes)
NAT type: consistent (hole-punchable)
Targeted Ping
PING 1.2.3.4:49737 (direct)
[1] OK 12ms node_id=ab12...
--- 1.2.3.4:49737 ping statistics ---
1 probes, 1 responded, 0 timed out (0% probe loss)
rtt min/avg/max = 12.0/12.0/12.0 ms
JSON Output (stdout)
JSON output is emitted as newline-delimited objects.
Resolve Events
Emitted before probing starts for PubKey and Topic targets.
{"type":"resolve","method":"find_peer","public_key":"<64-hex>"}
{"type":"resolve","status":"found","addresses":2}
Probe Events
Emitted for each individual probe. UDP probes use rtt_ms, while encrypted echo probes use latency_ms.
// UDP probe (direct/bootstrap)
{"type":"probe","seq":1,"status":"ok","rtt_ms":12.3,"node_id":"<hex>"}
// Echo probe (connect mode)
{"type":"probe","seq":1,"status":"ok","latency_ms":48.3}
Summary Events
Emitted at the end of a session if count > 1 or count == 0.
// Bootstrap summary
{
"type": "bootstrap_summary",
"nodes": 3,
"reachable": 2,
"unreachable": 1,
"nat_type": "consistent",
"closer_nodes_total": 50,
"public_host": "1.2.3.4",
"public_port": 5000,
"port_consistent": true
}
// Targeted summary
{
"type": "summary",
"target": "1.2.3.4:49737",
"probes_sent": 5,
"probes_responded": 4,
"probes_timed_out": 1,
"rtt_min_ms": 11.0,
"rtt_avg_ms": 20.5,
"rtt_max_ms": 45.0
}
Overview
The cp command provides secure, point-to-point file transfers between peers over the Hyperswarm network. It allows you to send and receive files using a shared topic, which serves as a rendezvous point on the DHT.
Key Features
- Direct Transfers: Files are streamed directly between peers using UDX, ensuring high performance and reliable delivery.
- Topic-Based Discovery: Use human-readable strings or 64-character hex keys to coordinate transfers without needing to know the other peer’s IP address.
- Piping Support: Full support for
stdin(-) as a source andstdout(-) as a destination, making it easy to integrate with other command-line tools. - Progress Tracking: Real-time progress bars and transfer statistics provided via
stderr. - End-to-End Encryption: All transfers are secured using Noise handshakes via SecretStream.
Basic Usage
Sending a File
To send a file, specify the file path and a topic. The command will output the topic (useful if you let it generate a random one) and wait for a receiver.
peeroxide cp send my-file.txt "shared-topic"
Receiving a File
To receive a file, use the same topic. You can specify a destination path or a directory.
peeroxide cp recv "shared-topic" ./downloads/
Streaming with Pipes
You can pipe data directly through cp:
# Sender
cat data.tar.gz | peeroxide cp send - "backup-topic"
# Receiver
peeroxide cp recv "backup-topic" - > restored.tar.gz
Command Options
send Options
file: Path to the file to send, or-forstdin.topic: Optional topic name or 64-char hex key.--name: Override the filename sent in the metadata.--keep-alive: Keep the sender running for multiple sequential transfers.--progress: Show a transfer progress bar.
recv Options
topic: The shared topic from the sender.dest: Optional destination path or directory, or-forstdout.--yes: Skip the confirmation prompt.--force: Overwrite existing files without asking.--timeout: Seconds to wait for a sender (default: 60).--progress: Show a transfer progress bar.
cp — Protocol
The cp tool is built on top of the peeroxide swarm, leveraging the DHT for peer discovery and UDX for efficient data transport.
Data Flow
The transfer process involves two main stages: discovery and streaming.
1. Discovery
- Sender: Joins the topic on the DHT as a server (
JoinOpts { client: false, .. }). It announces its presence and waits for incoming connections. - Receiver: Joins the topic on the DHT as a client (
JoinOpts { server: false, .. }). It looks for peers announcing the topic and attempts to connect to them.
2. Protocol Handshake
Once a connection is established, the peers perform a simple JSON-based handshake:
- Metadata: The sender transmits a JSON object containing file information:
filename: The name of the file being sent.size: The total size in bytes (if known;nullwhen streaming from stdin).version: The protocol version (currently1).
- Acceptance: The receiver validates the metadata and (unless
--yesis used) prompts the user for confirmation.
3. Streaming
Data is streamed in chunks (CHUNK_SIZE = 65536 bytes) over the established SecretStream.
sequenceDiagram
participant S as Sender
participant DHT as DHT / Bootstrap
participant R as Receiver
S->>DHT: Announce Topic
R->>DHT: Lookup Topic
DHT-->>R: Peer Address (Sender)
R->>S: Establish Connection (UDX + Noise XX)
S->>R: Send Metadata (JSON)
Note over R: Prompt User (unless --yes)
loop Data Streaming
S->>R: Data Chunk (64 KB)
end
S->>R: Shutdown Stream
R-->>S: Connection Closed
Implementation Details
Chunking
While the underlying UDX protocol handles packetization, cp reads and writes data in 64 KB blocks. This balances memory usage with throughput efficiency.
File Handling
- Temporary Files: During a receive operation, data is written to a hidden temporary file in the destination directory.
- Atomic Rename: Once the transfer is complete and the received size matches the expected size, the temporary file is renamed to the final destination. This prevents partial or corrupted files if a transfer is interrupted.
- Sanitization: Filenames provided by the sender are sanitized to prevent path traversal attacks (e.g., stripping
..components and leading slashes).
Network Configuration
The cp command uses the same runtime bootstrap-resolution as every other DHT-using subcommand (build_dht_config(cfg) in peeroxide-cli/src/cmd/mod.rs). Bootstrap node selection is therefore driven by the shared rules documented in init/overview.md → Global CLI Flags: CLI --bootstrap overrides the config file’s network.bootstrap; --public adds default public bootstrap nodes; an empty list auto-fills with the defaults; --no-public removes them. The --public flag does not change the node’s firewall state; NAT traversal for cp always relies on hole-punching via the DHT.
cp — Output Formats
The cp command separates data, status, and progress across output streams.
Standard Output (stdout)
send subcommand
Prints the transfer topic to stdout so it can be captured or piped to the receiver:
a3f2b1... (64-char hex topic key)
recv subcommand (streaming mode)
When destination is -, file data is written directly to stdout:
peeroxide cp recv <topic> - > received_file.bin
Standard Error (stderr)
All status messages, progress, and connection info go to stderr, keeping stdout clean for data.
Sender output:
CP SEND example.bin (1.2 MB)
topic: a3f2b1...
connected from @deadbeef...
done: 1.2 MB in 2.3s (540 KB/s)
Receiver output:
CP RECV topic: a3f2b1...
looking up sender...
connected to @deadbeef...
Incoming file: example.bin (1.2 MB)
Save to: ./example.bin
done: 1.2 MB in 2.3s (540 KB/s)
Progress Display
When transfer size is known, cp displays a progress bar on stderr showing percentage, bytes transferred, speed, and ETA. When size is unknown (stdin source), a spinner with running byte count is shown instead.
Reliability
cp does not implement retransmission or resumption at the application layer. Reliability is provided by the UDX transport layer (BBR congestion control, retransmission, ordering). If a transfer fails mid-stream:
- The temporary file is left in place (not renamed to final destination).
- Re-running
cp recvwith the same topic will restart the transfer from the beginning. - The sender must still be running and on the same topic.
For large files over unreliable networks, consider compressing the payload before transfer to reduce exposure to interruptions.
Dead Drop Overview
The dd command provides an anonymous, asynchronous store-and-forward mechanism using the DHT. It allows a sender to “put” data on the network that a receiver can later “get” using a unique key, without requiring both parties to be online at the same time.
Unlike the cp command, which establishes a direct peer-to-peer connection, dd uses DHT records to store data. This makes it ideal for scenarios where the sender and receiver have intermittent connectivity or want to avoid direct IP discovery.
Key Features
- Asynchronous Delivery: Data is stored on DHT nodes. The receiver picks it up whenever they’re ready.
- Protocol Versions: Supports both the original v1 linked-list protocol and the high-performance v2 tree-indexed protocol.
- Passphrase Support: Pickup keys can be derived from human-readable passphrases.
- Anonymity: No direct connection is established between the sender and receiver.
- Acknowledgements: Optional pickup notifications (acks) let the sender know when data was retrieved.
- Progress Control: Use
--no-progressfor silent operation or--jsonfor machine-readable event streams.
Protocol Selection
The dd command supports two protocol versions:
| Version | Characteristics | Selection |
|---|---|---|
| V1 | Simple linked-list of mutable records. Limited to 64MB. Sequential fetches. | Explicit via --v1 on put. Auto-detected on get. |
| V2 | Merkle-tree indexed. Massive capacity. Parallel fetching with need-lists and AIMD congestion control. | Default on put. Auto-detected on get. |
Dispatch Rules
- Putting:
dd putdefaults to v2. Use the--v1flag to force the legacy protocol. - Getting:
dd getautomatically dispatches based on the first byte of the fetched root record (0x01for v1,0x02for v2).
Quick Start
Putting Data
Put a message using a passphrase (v2 by default):
echo "Hello from the void" | peeroxide dd put - --passphrase "my secret drop"
Put a file using a raw key (generated randomly):
peeroxide dd put my-file.dat
Force v1 for compatibility with older clients:
peeroxide dd put my-file.dat --v1
Getting Data
Retrieve data using a passphrase:
peeroxide dd get --passphrase "my secret drop"
Retrieve data using a 64-character hex pickup key:
peeroxide dd get 7215c9...82a3
Write to a file while suppressing progress bars:
peeroxide dd get 7215c9...82a3 --output saved-file.dat --no-progress
How it Differs from cp
| Feature | cp | dd |
|---|---|---|
| Connection | Direct P2P (UDX) | Mediated via DHT storage |
| Online Requirement | Both must be online | Asynchronous |
| Discovery | Topic-based | Key-based (Public Key) |
| Speed | High (Direct) | Moderate (DHT round-trips) |
| Metadata | Filename, size | Sequential or Tree chunks |
Dead Drop Architecture
The dd command implements two distinct protocol architectures for storing and retrieving data on the DHT. Both protocols are built on the DHT primitives documented in DHT Primitives (mutable_put / mutable_get / immutable_put / immutable_get / announce).
Protocol V1: Linear Chain
The V1 protocol is a simple linked list of mutable DHT records. Each record contains a portion of the file and the public key of the next chunk in the chain.
V1 Flow
sequenceDiagram
participant S as Sender
participant DHT as DHT Nodes
participant R as Receiver
Note over S: Chunking + Key Derivation
S->>DHT: mutable_put(root_pk)
S->>DHT: mutable_put(chunk_1_pk)
S->>DHT: ...
S->>DHT: mutable_put(chunk_N_pk)
Note over R: Get root_pk
R->>DHT: mutable_get(root_pk)
DHT-->>R: root record
loop Sequential Fetch
R->>DHT: mutable_get(next_pk)
DHT-->>R: chunk record
end
V1 features sequential fetching with exponential retry logic (1s to 30s) per chunk, bounded by the global timeout.
Protocol V2: Merkle Tree
V2 uses a hierarchical tree structure to enable massive file support and parallel fetching.
V2 Flow
sequenceDiagram
participant S as Sender
participant DHT as DHT Nodes
participant R as Receiver
Note over S: Canonical Tree Build
S->>DHT: immutable_put(data_chunks)
S->>DHT: mutable_put(index_chunks)
S->>DHT: mutable_put(root_pk)
Note over R: BFS Parallel Fetch
R->>DHT: mutable_get(root_pk)
DHT-->>R: root (metadata + top slots)
rect rgb(240, 240, 240)
Note over R: Parallel BFS Loop
R->>DHT: mutable_get(index_pk)
R->>DHT: immutable_get(data_hash)
end
Note over R: Need-list Cycle
R->>DHT: announce(need_topic)
R->>DHT: mutable_put(need_topic, ranges)
DHT-->>S: watch(need_topic)
S->>R: Republish missing chunks
AIMD Congestion Control
V2 employs an Additive Increase / Multiplicative Decrease (AIMD) controller to manage concurrency:
- EWMA-based: Smoothes sample noise with an alpha of 0.1.
- Decision interval: 20 samples.
- Fast-trip: Shrinks immediately if 10 degraded samples occur within a window.
- Shrink: 0.75x current (minimum 1).
- Grow: +2 permits.
Robustness Mechanisms
- Stall Watchdog: Checks every 5s. If no put resolves for 30s, it forces AIMD to a recovery floor.
- Sliding-window Timeout:
getoperations abort only if no chunk decodes for--timeoutseconds. - Graceful Shutdown: First Ctrl-C triggers a sticky cancel signal that enqueues cleanups (like empty need-list sentinels). A second double-press force-exits.
- Need-list Lifecycle: Receivers publish the encoded missing-range need-list via
mutable_putevery 20s and announce keepalive on the need topic every 60s. Senders poll the need topic every 5s and prioritize enqueuing the full path (index + data) for any newly-listed chunks.
DHT Wire Monitoring
The dd command monitors raw network overhead by reading atomic counters from the underlying DHT handle.
| Method | Return |
|---|---|
wire_stats() | (u64, u64) (sent, received) |
wire_counters() | WireCounters (shared atomic handles) |
These counters allow the progress UI to calculate “wire amplification” — the ratio of total bytes sent/received versus actual payload bytes delivered.
Dead Drop Wire Format
The dd command supports two versioned wire formats for DHT records. All multi-byte integers are encoded in little-endian (LE) byte order. The underlying DHT operations (mutable_put / mutable_get / immutable_put / immutable_get) are documented in DHT Primitives.
Version 1 Wire Format
V1 records are limited to 1000 bytes total and form a linear linked list of mutable records.
V1 Constants
MAX_CHUNKS: 65,535MAX_PAYLOAD: 1,000 (total record limit)ROOT_HEADER_SIZE: 39NON_ROOT_HEADER_SIZE: 33ROOT_PAYLOAD_MAX: 961NON_ROOT_PAYLOAD_MAX: 967VERSION:0x01
V1 Layouts
Root Chunk (V1)
[ver: 1][total_chunks: 2 LE][crc32c: 4 LE][next_pk: 32][payload: up to 961]
Non-root Chunk (V1)
[ver: 1][next_pk: 32][payload: up to 967]
Version 2 Wire Format
V2 records use a tree structure. Data chunks are stored in immutable records, while index and root chunks are stored in mutable records.
V2 Constants
VERSION:0x02MAX_CHUNK_SIZE: 1,000DATA_HEADER_SIZE: 2DATA_PAYLOAD_MAX: 998NON_ROOT_INDEX_HEADER_SIZE: 1NON_ROOT_INDEX_SLOT_CAP: 31ROOT_INDEX_HEADER_SIZE: 13ROOT_INDEX_SLOT_CAP: 30NEED_LIST_HEADER_SIZE: 3NEED_ENTRY_SIZE: 8NEED_LIST_ENTRY_CAP: 124
V2 Tree Structure
The tree is constructed bottom-up. Leaf layers pack 31 data hashes per index chunk. Higher layers pack 31 index pubkeys per chunk. The root holds the top-layer keys directly.
| Depth | Max Data Chunks | Capacity (approx) |
|---|---|---|
| 0 | 30 | 29 KB |
| 1 | 930 | 928 KB |
| 2 | 28,830 | 28 MB |
| 3 | 893,730 | 891 MB |
| 4 | 27,705,630 | 27 GB |
Note: The implementation enforces a SOFT_DEPTH_CAP of 4.
V2 Layouts
Data Chunk (V2)
Stored via immutable_put. The salt is reserved for randomization but currently fixed at 0x00.
[0x02][salt: 0x00][payload: up to 998]
Non-root Index Chunk (V2)
Stored via mutable_put. Contains 32-byte slots (either data hashes or child index pubkeys).
[0x02][slots: 31 x 32]
Root Index Chunk (V2)
The entry point. Contains file metadata and top-level slots.
[0x02][file_size: 8 LE][crc32c: 4 LE][slots: 30 x 32]
Need-list Record (V2)
Published by the receiver on the need topic to request missing data.
[0x02][count: 2 LE][entries: count x {start: 4 LE, end: 4 LE}]
Each entry is a half-open range [start, end) of data-chunk indices in the canonical DFS file order (chunk 0 is the first chunk of the file, chunk 1 is the next, etc.). The sender consults the need-list and republishes every data chunk in any listed range, plus the full index-tree path required to make those data chunks reachable.
When the receiver has no missing chunks, it publishes a “receiver done” sentinel: a raw empty byte string at the need topic. The decoder treats a zero-byte value as the sentinel (it is not the same as the encoded need-list with count = 0).
Salt Situation
While the V2 format reserves a byte for a per-deaddrop salt to randomize data chunk addresses, the current implementation enforces salt(...) -> 0x00. All V2 data chunk headers are currently prefixed with [0x02][0x00].
Dead Drop Operations
The dd command supports both human-readable terminal output and machine-readable JSON output for integration with other tools.
Command Line Flags
In addition to the dd-specific flags shown below, both dd put and dd get accept the inherited top-level global flags: --config <FILE>, --no-default-config, --public, --no-public, --bootstrap <ADDR> (repeatable), and -v / --verbose. These control config file loading, DHT bootstrap node selection, and tracing verbosity; see init/overview.md → Global CLI Flags for the bootstrap-resolution algorithm.
dd put Flags
| Flag | Default | Description |
|---|---|---|
<file> | required | Input file path. Use - for stdin. |
--max-speed <S> | none | Limit transfer speed. Parses k/m suffixes (base-10, case-insensitive). |
--refresh-interval <secs> | 600 | Seconds between refresh cycles (must be > 0). |
--ttl <secs> | none | Stop refreshing after N seconds (must be > 0). |
--max-pickups <N> | none | Exit after N unique pickup acks (must be > 0). |
--passphrase <S> | none | Deterministic root seed from discovery_key(passphrase). |
--interactive-passphrase | none | TTY prompt for passphrase with hidden input. |
--no-progress | false | Suppress progress UI. |
--json | false | Emit JSON-Lines progress on stdout. |
--v1 | false | Force legacy v1 protocol. |
dd get Flags
| Flag | Default | Description |
|---|---|---|
<key> | required* | 64-character hex pickup key or passphrase text. |
--passphrase <S> | none | Derive pickup key from passphrase. |
--interactive-passphrase | none | TTY prompt for passphrase with hidden input. |
--no-progress | false | Suppress progress UI. |
--output <PATH> | stdout | Write payload to file instead of stdout. |
--json | false | Emit JSON-Lines progress. Requires --output. |
--timeout <secs> | 1200 | Sliding no-progress timeout in seconds (must be > 0). |
--no-ack | false | Suppress pickup acknowledgement announce. |
*Key is required unless a passphrase flag is provided.
Key Derivation and Passphrases
- Passphrase Derivation: When a passphrase is used, the root seed is derived via
discovery_key(passphrase). - Interactive Fallback: The
--interactive-passphraseflag attempts to open/dev/ttyfor hidden input, falling back to stdin if unavailable. - Key vs Passphrase: If a positional argument is exactly 64 characters of valid hex, it is treated as a raw 32-byte pickup key. Otherwise, it is treated as passphrase text and hashed via
discovery_key.
Progress UX
The mode is selected automatically:
--json-> JSON Lines on stdout.--no-progress-> Progress disabled.- stderr is TTY -> Interactive bars.
- else -> Periodic log line (every 2s).
Bar Layouts
- V1 Put:
↑ filename D(bytes/total) [bar] pct rate ETA - V2 Put:
↑ filename I[idx/total] D(bytes/total) [bar] pct rate ETA - V2 Get (4-bar multi):
- index:
I[idx/total] rate - data:
D(bytes/total) [bar] pct rate ETA - wire:
W ↑ rate ↓ rate (x amplification) - overall:
filename bytes/total pct
- index:
Wire amplification (wire_total / bytes_done) is omitted until the first payload byte is received.
Machine-Readable Output (--json)
The --json flag enables a stream of JSON Lines on stdout. Events use type as a discriminator and RFC3339 timestamps.
Event Schema
| Type | Description |
|---|---|
start | Operation initiated. Includes version, filename, bytes_total, indexes_total, data_total. |
progress | Periodic update. Includes bytes_done, rate_bytes_per_sec, eta_seconds, elapsed_seconds. |
result | Objective achieved. put returns pickup_key and chunks. get returns crc and output. |
ack | Sender-only. Emitted when a recipient acknowledges receipt. Includes peer and pickup_number. |
done | Operation completed. Includes final counters and elapsed_seconds. |
V1 Convention: indexes_total and indexes_done are always 0 in V1 events.
Acknowledgement (Ack) Mechanism
When a get operation completes (unless --no-ack is set), the receiver announces on the ack topic:
ack_topic = discovery_key(root_pk || b"ack")
The sender polls this topic every 30s and counts unique announcer public keys.
Future Direction
Both dd v1 and v2 protocols are shipped and fully supported. There is no speculative dd roadmap documented at this time.
Chat Subsystem Overview
Peeroxide chat provides a serverless, end-to-end encrypted messaging environment built on the HyperDHT. It enables real-time communication without centralized accounts, phone numbers, or servers. Every identity is a public key, and every message is a cryptographically signed and encrypted record stored briefly in the distributed hash table.
Why Chat?
Traditional messaging apps rely on central servers to store your messages, manage your identity, and route your traffic. Peeroxide chat removes these intermediaries. It treats the network as a shared space where peers discover each other through topics and exchange data directly.
This design ensures:
- Censorship Resistance: There is no central point to shut down.
- Privacy by Default: All messages are encrypted. Metadata is minimized through epoch-based topic rotation.
- Self-Sovereign Identity: You own your cryptographic keys. Your identity is not tied to a service provider.
Identity Model
Your identity in Peeroxide is an Ed25519 keypair. This keypair is stored in a local profile. When you send a message, it is signed with your private key, allowing anyone with your public key to verify that it came from you.
Profiles allow you to manage multiple identities on one machine. Each profile includes:
- A permanent secret seed.
- An optional screen name.
- An optional biography.
- A friends list.
Separately, a shared known-users name cache lives at ~/.config/peeroxide/chat/known_users. It is process-wide (not per profile) and acts as a soft directory mapping public keys to the most-recently-seen screen name for each peer you have encountered.
Channels
Peeroxide uses a topic-based discovery system. A “channel” is simply a name that maps to a DHT topic.
Public Channels
Public channels use a well-known derivation for their discovery topic. Anyone who knows the channel name (e.g., general or rust-dev) can join, read history, and post messages.
Private Channels
Private channels add a secret “group salt” to the topic derivation. Only peers who possess the salt can discover the channel topic or decrypt the messages within it. This enables private group conversations on the public DHT without revealing the participants or the content to outsiders.
Direct Messaging (DMs)
Direct messaging allows private, one-to-one communication between two specific public keys.
When you start a DM with another user, Peeroxide derives a unique dm_channel_key using your public key and theirs. Because the derivation is order-independent, both parties arrive at the same key. The communication is further secured using an ephemeral shared secret derived via X25519 Elliptic Curve Diffie-Hellman (ECDH).
The Inbox Concept
Because there is no server to hold messages while you are offline, Peeroxide uses an “Inbox” mechanism to facilitate discovery.
Your inbox is a set of rotating DHT topics derived from your public key. When someone wants to start a DM or invite you to a private channel, they generate a one-shot invite-feed keypair, publish an encrypted InviteRecord at that feed via mutable_put, and then announce that feed on your current inbox topic.
Your client periodically monitors these topics. When a new invite appears, it notifies you and provides the necessary keys to join the conversation. This “nudge” mechanism allows peers to find each other even if they aren’t currently in the same channel.
Profiles and the Nexus
The “Nexus” is your personal landing page on the DHT. It contains your screen name and biography. When you are active, your client publishes your Nexus record directly under your identity public key (via mutable_put on that key, with no extra topic derivation).
Your friends fetch your Nexus by mutable_get-ing your identity public key, picking up name and bio updates. This keeps your identity consistent across different channels and sessions.
For more details on the technical implementation, see Wire Format and Protocol.
Chat User Guide
The Peeroxide chat subsystem provides a set of CLI tools for managing identities, communicating in channels, and sending direct messages.
Global Flags
These flags apply to all peeroxide chat subcommands.
| Flag | Description |
|---|---|
--debug | Enable stderr debug event logs. |
--probe | Enable internal trace probes (stdin, post, fetch_batch, etc) to stderr. |
--line-mode | Force line-based I/O even when running on a TTY. |
In addition, every chat subcommand inherits the top-level peeroxide global flags documented in init: --config <FILE>, --no-default-config, --public, --no-public, --bootstrap <ADDR> (repeatable), and -v / --verbose. These control config file loading, DHT bootstrap node selection, and tracing verbosity.
Subcommand: join
Join a public or private channel for real-time conversation.
peeroxide chat join <channel> [flags]
Flags
| Flag | Default | Description |
|---|---|---|
--profile <name> | default | Use a specific identity profile. |
--group <salt> | Set a private channel salt. Conflicts with --keyfile. | |
--keyfile <path> | Read private channel salt from a file. Conflicts with --group. | |
--no-nexus | Skip personal nexus (profile page) refresh and publication. | |
--no-friends | Skip background friend nexus refresh. | |
--read-only | Listen only; do not post messages or announce feeds. | |
--stealth | Shorthand for --no-nexus --read-only --no-friends. | |
--feed-lifetime <min> | 60 | Rotation lifetime for your feed keypair. |
--batch-size <n> | 16 | Maximum messages per publish batch. Values below 1 are clamped to 1. |
--batch-wait-ms <ms> | 50 | Maximum time to wait for a batch to fill before publishing. |
--stay-after-eof | Enter read-only mode on stdin EOF instead of exiting. | |
--no-inbox | Disable background inbox monitoring. | |
--inbox-poll-interval <s> | 15 | How often to poll the inbox for new invites. Values below 1 are clamped to 1. |
Examples
Join a public channel:
peeroxide chat join general
Join a private channel with a secret group name:
peeroxide chat join development --group "super-secret-salt-2026"
Subcommand: dm
Start an encrypted direct message session with another user.
peeroxide chat dm <recipient> [flags]
The recipient can be resolved using several formats (see Recipient Resolution below).
Flags
chat dm supports most of the session flags from join (--profile, --no-nexus, --no-friends, --read-only, --stealth, --feed-lifetime, --batch-size, --batch-wait-ms, --stay-after-eof, --no-inbox, --inbox-poll-interval), plus a DM-only flag:
| Flag | Description |
|---|---|
--message <text> | Initial lure text sent with the inbox invite. Ignored in stealth/read-only mode. |
chat dm does not accept --group / --keyfile; the channel key for a DM is derived deterministically from the two participants’ identity public keys via dm_channel_key.
Recipient Resolution
The recipient argument is resolved in the following order:
- 64-character hex public key.
@shortkey(e.g.,@a1b2c3d4).name@shortkey(e.g.,alice@a1b2c3d4).- 8-character shortkey (e.g.,
a1b2c3d4). - Friend alias (defined in your friends list).
- Screen name from the
known_userscache.
Subcommand: inbox
Monitor your inbox for new invites without entering an interactive UI.
peeroxide chat inbox [flags]
Flags
| Flag | Default | Description |
|---|---|---|
--profile <name> | default | Use a specific profile. |
--poll-interval <secs> | 15 | Interval between inbox scans. Values below 1 are clamped to 1. |
--no-nexus | Accepted for flag-surface parity with chat join / chat dm, but has no effect on chat inbox (which does not run a nexus publisher). | |
--no-friends | Accepted for flag-surface parity with chat join / chat dm, but has no effect on chat inbox (which does not run a friend refresh task). |
Profile Management: whoami and profiles
whoami
Prints information about your current profile, including your public key, screen name, and nexus topic.
peeroxide chat whoami [--profile <name>]
| Flag | Default | Description |
|---|---|---|
--profile <name> | default | Profile to inspect. |
profiles
Manage multiple identities. Subcommands:
peeroxide chat profiles list
peeroxide chat profiles create <name> [--screen-name <name>]
peeroxide chat profiles delete <name>
| Subcommand | Args / flags | Description |
|---|---|---|
list | — | List all available profiles. |
create <name> | --screen-name <name> (optional) | Create a new profile. If --screen-name is omitted, a deterministic vendor name is generated and stored. |
delete <name> | — | Delete a profile. The default profile cannot be deleted. |
Friend Management: friends
Manage your list of trusted peers.
peeroxide chat friends [subcommand] [flags]
If no subcommand is given, friends list runs.
Subcommands and flags
| Subcommand | Flags | Description |
|---|---|---|
list | --profile <name> (default default) | Show all friends in the profile. |
add <key> | --alias <name> (optional), --profile <name> (default default) | Add a new friend. Key resolution follows the same rules as DM recipients. If --alias is omitted, the alias auto-fills from the known-users cache or a vendor name. |
remove <key> | --profile <name> (default default) | Remove a friend from the profile’s list. |
refresh | — | One-shot DHT update for friends’ profile information. Does not accept a --profile flag — operates on the default profile only. |
Personal Page: nexus
Manage your public profile information (Nexus) published on the DHT.
peeroxide chat nexus [flags]
If --lookup is supplied, the command short-circuits to lookup mode. Otherwise, --set-name and --set-bio are written to the profile first (both are applied in one run). After the setters, behavior is:
--publish: perform a one-shot Nexus publish and exit.--daemon: enter the background loop (publish every 480 s, refresh all friends every 600 s).- No
--publish/--daemon, but at least one setter was supplied: exit without publishing. - No flags at all (or only
--profile): perform a one-shot Nexus publish and exit.
Flags
| Flag | Default | Description |
|---|---|---|
--profile <name> | default | Profile to publish from / inspect. |
--set-name <name> | Update your screen name (writes the profile’s name file before publishing). | |
--set-bio <text> | Update your biography (writes the profile’s bio file before publishing). | |
--publish | Publish your Nexus record to the DHT once. | |
--daemon | Enter a background loop: publish your Nexus every 480s and refresh all friends every 600s. | |
--lookup <pubkey> | Lookup and print the Nexus information for a specific public key. Short-circuits the rest. |
Screen Name and Bio Files
A profile’s screen name and bio live as plain UTF-8 text files inside the profile directory:
~/.config/peeroxide/chat/profiles/<profile>/name
~/.config/peeroxide/chat/profiles/<profile>/bio
Both files are optional. If name is missing, a deterministic vendor name is generated from the profile’s identity public key whenever a screen name is needed. If bio is missing or empty, the published Nexus record carries an empty bio.
You can populate them two ways:
peeroxide chat nexus --set-name <text>/--set-bio <text>— writes the file with the supplied text (after trimming leading and trailing whitespace), then optionally publishes if--publish/--daemonis also given. Both setters can be supplied in one command.- Edit the file directly with any editor. Multi-line bios are supported; the entire file content (after UTF-8 decoding) becomes the bio. The first line is treated specially by friends’ clients — the friends file caches only the first line of each friend’s bio for the
friends listdisplay, but the full bio is shown when a friend explicitly looks the identity up viachat nexus --lookup.
Size Limit
The screen name and bio are serialized together into a single NexusRecord published to the DHT as a mutable_put value. The full record (3 framing bytes + name UTF-8 bytes + bio UTF-8 bytes) must fit within 1000 bytes, which is the MAX_RECORD_SIZE constant for chat records.
In practice: with a typical 10–40 byte screen name, the bio budget is roughly 950–990 UTF-8 bytes (note: bytes, not characters — many non-ASCII characters take 2–4 bytes each).
If the combined size is too large, the publish step fails with:
warning: nexus serialize failed: record too large: N bytes exceeds 1000 byte limit
The bio file is still saved on disk in this case — only the DHT publish is skipped. Shorten the bio (or screen name) and re-run with --publish to recover.
Stealth Mode
The --stealth flag is supported by both chat join and chat dm. It is a shorthand for --no-nexus --read-only --no-friends, but the behavioral and threat-model implications are easier to reason about as a single concept.
What --stealth suppresses
Passing --stealth is equivalent to enabling all three of:
--read-only— your publisher is disabled entirely. No feed keypair is created, no message records are written viaimmutable_put, noFeedRecordis published viamutable_put, and noannounceis sent on the channel or DM rendezvous topics. You become a pure observer of the channel.--no-nexus— your profile’s Nexus record (screen name + bio) is not published. Other peers cannot resolve your identity public key to your screen name via the DHT, and you do not consume amutable_putslot at your identity public key.--no-friends— the background friend-Nexus refresh task does not run. Your DHT does not issue periodicmutable_gets on each friend’s identity public key, which would otherwise be observable to DHT nodes near those keys.
What --stealth does NOT suppress
--stealth stops the publishing side of the protocol. Several other observable activities continue:
- Channel discovery is still active. Reading any channel requires
lookups on its discovery topics, followed bymutable_gets on each announcer’s feed public key. Both operations remain visible to the DHT nodes serving them. - Inbox monitoring is independent of
--stealth. A stealth session still polls your profile’s inbox topics every--inbox-poll-intervalseconds (8 lookups per cycle by default — current + previous epoch, 4 buckets each). The wire-level lookup carries only the derived inbox topic, not your public key, so a passive DHT participant who does not already know your identity cannot recover it from these queries alone. However, an observer who already knows your public key can independently derive the same inbox topics and recognize the polling pattern, which lets them correlate the polling source IP with your identity. If that matches your threat model, also pass--no-inbox. - DM under stealth is receive-only. The DM channel key is symmetric between the two parties, so you can decrypt incoming messages. But you never
announceyour DM feed, never publish a message, and never send the per-epoch nudge. Your DM peer has no way to know you are listening. - Network-level metadata is unchanged. Every DHT operation goes out over UDP to peers who see your IP address. The Hyperswarm DHT has no traffic-mixing or onion-routing layer. If IP-level identifiability matters in your threat model — and especially if your public key is already known to an adversary — route peeroxide’s traffic through a transport you trust to provide that property: typically a VPN that gives you a different egress IP, mixes your traffic with other clients, and does not retain per-flow logs.
When --stealth is enough
It is sufficient when your only goal is to read a channel without contributing to its announce set or signaling your presence to other channel participants — for example, when you are using a fresh profile whose public key no observer has associated with you, and you want to listen first before deciding whether to post.
When --stealth is not enough
It is not sufficient when your public key is already known to an adversary and IP-level correlation matters. In that case the chain your public key → derived inbox / Nexus / announce topics → DHT lookups from your IP is exploitable by a sufficiently positioned observer. Combine --stealth --no-inbox with a trustworthy anonymizing transport in front of the binary.
Recipes
-
Lurk on a channel without joining its announce set:
peeroxide chat join general --stealth -
Same, plus suppress inbox polling:
peeroxide chat join general --stealth --no-inbox -
Lurk under a burner profile so the activity is not tied to your main identity:
peeroxide chat profiles create burner peeroxide chat join general --stealth --profile burner
Interactive Usage
When running in a TTY, join and dm enter an interactive mode with a status bar and slash commands. See Interactive TUI for details.
In line mode (or when stdin is redirected), Peeroxide prints messages to stdout and notices to stderr. This is useful for piping chat into other tools.
Message Display
Messages are formatted as:
[TIMESTAMP] [DISPLAY_NAME]: CONTENT
If a message arrives significantly after its timestamp, it is prefixed with [late].
Display names are resolved with the following precedence:
- Friend alias (e.g.,
(Bob)). - Friend’s vendor name + screen name (e.g.,
(Vendor) <Alice@a1b2c3d4>). - Non-friend with a wire
screen_nameon the message (e.g.,<Alice@a1b2c3d4>). - Non-friend without a wire
screen_namebut present in the sharedknown_userscache (e.g.,<Cached-Name@a1b2c3d4>). - Vendor fallback (e.g.,
<Fancy-Tiger@a1b2c3d4>).
A ! suffix on a name indicates the user is currently in a 300-second cooldown period after a name change.
Interactive TUI
Peeroxide chat features a terminal-based interactive interface (TUI) designed for real-time communication.
Mode Selection
The TUI is automatically enabled if:
stdoutis a TTY.stdinis a TTY.- The
--line-modeflag is not set. - The
PEEROXIDE_LINE_MODEenvironment variable is unset, empty, or"0"(any other non-empty value forces line mode).
If any of these conditions are not met, the client falls back to line mode. If TUI initialization fails on a TTY, a warning is printed and the client reverts to line mode.
Status Bar Layout
The status bar sits at the bottom of the terminal and provides real-time feedback on network activity and session state.
● Sending 3 Receiving 12 inbox Feeds 2 DHT 32 general
Components
- Activity Indicator (●): Lights up when any DHT operation (put, get, announce, lookup) is in flight.
- Left Segments:
Sending N: Number of messages currently in the publish batching pipeline.Receiving N: Number of messages currently being fetched or ordered.Ready: Indicates the publisher queue is empty and the client is idle.- Note: These slots use “sticky width”—once they grow to accommodate a larger number, they remain that size until the terminal is resized.
- Center Segment:
- Shows
inbox(ori) when there are no unread invites. - Shows
INBOX(orI) in yellow-on-black when new invites have arrived. - The segment is centered. It automatically shrinks or disappears if the terminal width is too narrow to avoid overlapping left or right segments.
- Shows
- Right Segments:
Feeds N: Total number of active feeds being tracked in the session.DHT N: Current number of connected peers in the DHT routing table.<channel>: The name of the current channel or the recipient’s name.
Keyboard Controls
| Key | Behavior |
|---|---|
Enter | Send the current input buffer. |
Ctrl-C | If buffer is non-empty: Clear the buffer. If buffer is empty: Arms a 2-second force-quit window. |
Ctrl-D | If buffer is empty: Initiate graceful exit. If non-empty: Forward-delete character. |
Ctrl-L | Full screen repaint and history replay. |
Up/Down | Move the cursor up or down within the multi-line input area. |
Ctrl-C Force Quit
When the buffer is empty, pressing Ctrl-C once will display a yellow-on-black notice:
*** press Ctrl-C again within 2 seconds to force quit — press Ctrl-D for graceful exit
Pressing Ctrl-C a second time within the 2-second window will terminate the process immediately. This remains responsive even if the network publisher is blocked. Any other key disarms the window.
Slash Commands
Commands can be entered directly into the input buffer starting with a /.
| Command | Action |
|---|---|
/help, /? | Display available commands. |
/quit, /exit | Initiate a graceful shutdown. |
/ignore [name] | List ignored users, or add a user to the ignore list. |
/unignore <name> | Remove a user from the ignore list. |
/friend [name] | List friends, or add a user to your friends list. |
/unfriend <name> | Remove a user from your friends list. |
/inbox | Display and drain the list of accumulated invites. Resets the INBOX status segment. |
Input Features
Multi-line Input
The input area above the status bar supports multi-line text. Use Alt-Enter (or your terminal’s equivalent) to insert a newline without sending.
Bracketed Paste
The TUI supports bracketed paste mode. When you paste large blocks of text, the client treats it as a single input operation, preventing the terminal from interpreting pasted newlines as “Send” commands.
History Replay
The client maintains a bounded in-memory scrollback buffer of the last 500 messages (HISTORY_CAP). When the terminal is resized or repainted (Ctrl-L), the client replays the last min(history_len, terminal_height) entries to restore context.
Terminal Lifecycle
The TerminalGuard ensures the terminal state is correctly managed:
- Enter: Scrolls existing terminal content into the scrollback, enables raw mode, enables bracketed paste, hides the cursor, and installs a panic hook.
- Exit/Panic: Resets the scroll region, restores the cursor, disables bracketed paste, restores original colors, and disables raw mode.
EOF and Shutdown
When stdin reaches EOF (e.g., via Ctrl-D or piped input completion):
- Default: The client begins a graceful drain. It displays
*** flushing publish queue (Ctrl-C to abort)…and waits for all pending messages to be published to the DHT. There is no fixed timeout, thoughCtrl-Ccan be used to skip the wait. - –stay-after-eof: Instead of exiting, the client enters read-only listener mode, allowing you to continue seeing incoming messages without being able to reply.
Wire Format
Peeroxide chat uses a structured wire format for all data exchanged over the DHT. All records are encrypted within a common frame. The underlying DHT operations (mutable_put / mutable_get / immutable_put / immutable_get / announce / lookup) and their per-record size budget are documented in DHT Primitives.
Encryption Frame
Every record (Message, Feed, Summary, Nexus, Invite) is encapsulated in an XSalsa20-Poly1305 encryption frame.
[nonce: 24 bytes] [tag: 16 bytes] [ciphertext: variable]
- Cipher: XSalsa20-Poly1305.
- Nonce: 24-byte random nonce generated per message.
- Tag: 16-byte authentication tag.
- No AAD: No additional authenticated data is used in the frame.
Record Types
MessageEnvelope
The MessageEnvelope represents a single chat message.
| Field | Size | Description |
|---|---|---|
id_pubkey | 32 | Ed25519 public key of the author. |
prev_msg_hash | 32 | Blake2b hash of the previous message in this feed’s chain. |
timestamp | 8 | Unix timestamp in seconds (u64 Little Endian). |
content_type | 1 | 0x01 for text. |
screen_name_len | 1 | Length of the screen name string. |
screen_name | var | UTF-8 encoded screen name. |
content_len | 2 | Length of the content (u16 Little Endian). |
content | var | UTF-8 encoded message content. |
signature | 64 | Ed25519 signature over the message body. |
Signature Scheme:
The signature covers the following bytes:
b"peeroxide-chat:msg:v1:" || prev_msg_hash || timestamp || content_type || screen_name_len || screen_name || content
FeedRecord
The FeedRecord is a mutable record stored at a feed’s public key. It acts as an index of recent messages.
| Field | Size | Description |
|---|---|---|
id_pubkey | 32 | Author’s permanent public key. |
ownership_proof | 64 | Proof that id_pubkey owns this feed. |
next_feed_pubkey | 32 | Pointer to the next feed after rotation (all zeros if none). |
summary_hash | 32 | Hash of the latest SummaryBlock for this feed. |
msg_count | 1 | Number of message hashes in this record (max 26). |
msg_hashes | 32 * N | Array of message hashes, newest-first. |
Ownership Proof:
An Ed25519 signature by the id_pubkey over:
b"peeroxide-chat:ownership:v1:" || feed_pubkey || channel_key
SummaryBlock
The SummaryBlock is an immutable record used to store history that has been evicted from the FeedRecord.
| Field | Size | Description |
|---|---|---|
id_pubkey | 32 | Author’s public key. |
prev_summary_hash | 32 | Hash of the previous SummaryBlock (all zeros if none). |
msg_count | 1 | Number of hashes in this block. |
msg_hashes | 32 * N | Array of message hashes, oldest-first. |
signature | 64 | Ed25519 signature. |
Signature Scheme:
Covers: b"peeroxide-chat:summary:v1:" || prev_summary_hash || msg_hashes...
NexusRecord
The NexusRecord contains profile information published to the author’s personal topic.
| Field | Size | Description |
|---|---|---|
name_len | 1 | Length of the screen name. |
name | var | UTF-8 encoded screen name. |
bio_len | 2 | Length of the biography (u16 Little Endian). |
bio | var | UTF-8 encoded biography. |
InviteRecord
Used for DMs and private channel invites in the Inbox.
| Field | Size | Description |
|---|---|---|
id_pubkey | 32 | Author’s public key. |
ownership_proof | 64 | Ownership proof (same as FeedRecord). |
next_feed_pubkey | 32 | Next feed pointer. |
invite_type | 1 | 0x01 = DM, 0x02 = Private Channel. |
payload_len | 2 | Length of the payload (u16 Little Endian). |
payload | var | Encrypted payload (see below). |
DM Payload: Opaque lure text.
Private Invite Payload: [name_len: u8][name][salt_len: u16 LE][salt].
Key Derivation
All derivation functions use keyed BLAKE2b-256.
| Key | Derivation Formula |
|---|---|
channel_key (Public) | hash([b"peeroxide-chat:channel:v1:", len4(name), name]) |
channel_key (Private) | hash([b"peeroxide-chat:channel:v1:", len4(name), name, b":salt:", len4(salt), salt]) |
dm_channel_key | hash([b"peeroxide-chat:dm:v1:", min(pk_a, pk_b), max(pk_a, pk_b)]) |
msg_key | keyed_blake2b(channel_key, b"peeroxide-chat:msgkey:v1") |
dm_msg_key | `keyed_blake2b(ecdh_secret, b“peeroxide-chat:dm-msgkey:v1:“ |
invite_key | `keyed_blake2b(ecdh_secret, b“peeroxide-chat:invite-key:v1:“ |
announce_topic | `keyed_blake2b(channel_key, b“peeroxide-chat:announce:v1:“ |
inbox_topic | `keyed_blake2b(hash(pk), b“peeroxide-chat:inbox:v1:“ |
DM ECDH
For direct messages, Ed25519 keys are converted to X25519:
- Public Key: Edwards-to-Montgomery conversion.
- Secret Key:
SHA-512(seed)[0..32]with standard clamping. - Shared Secret: standard
x25519scalar multiplication.
Epoch and Bucket Math
- Epoch:
unix_time_secs / 60(60-second intervals). - Buckets: 4 buckets per epoch (0, 1, 2, 3).
- Discovery: A client scans
(current_epoch, previous_epoch) × 4 buckets, resulting in 8 lookups per cycle. - Randomization: Each session uses a random permutation of the 4 buckets to distribute load.
Operational Protocol
The Peeroxide chat protocol defines how peers discover each other, synchronize message feeds, and maintain a consistent conversation state without a central server.
Feed Lifecycle
A “feed” is a sequence of messages published by a single identity under a temporary Ed25519 keypair.
Rotation
To enhance privacy and limit the impact of key compromise, feed keypairs are rotated periodically.
- At session start, a random feed keypair and a lifetime wobble (between 0.5x and 1.5x of
--feed-lifetime) are chosen. - A rotation watcher checks the feed age every 30 seconds.
- When the lifetime is reached, the publisher generates a new feed keypair.
- The publisher first announces the new feed.
- It then updates the old
FeedRecordto include thenext_feed_pubkeypointer. - The old feed remains active briefly to ensure peers follow the transition before it is abandoned.
Message Publishing Pipeline
The publisher uses a bounded queue to batch and write messages to the DHT.
- Batching: Messages are accumulated in a queue. A batch is processed when it reaches
--batch-sizeor after--batch-wait-ms. - Immutable Put: Each message in the batch is stored as an immutable record on the DHT.
- Mutable Put: The
FeedRecordfor the current feed is updated to include the hashes of the new messages. This operation is retried up to 3 times (at 200ms, 500ms, and 1000ms intervals) to handle DHT congestion. - Announce: The publisher announces the feed’s availability on the channel’s
announce_topic.
Reader Discovery Loop
The reader task starts with a one-shot cold-start scan, then settles into a steady-state discovery loop.
Cold-Start Historical Scan
On startup, the reader fires concurrent lookups across the last 20 epochs × 4 buckets = 80 discovery topics (i.e. a 20-minute backwards window, since each epoch is 60 s). This surfaces feeds that announced before the session started so the client has visible history immediately, instead of waiting up to a full epoch rotation for the steady-state loop to reach them.
Steady-State Loop
After the cold-start completes, the continuous loop runs:
- Discovery: Every 8 seconds, the reader performs lookups on the 8 discovery topics (current and previous epoch across 4 buckets).
- Polling: For every discovered peer, the reader fetches and decrypts their
FeedRecord. - Fetching: New message hashes found in the
FeedRecordare fetched as immutable records. - Ordering: Messages are passed to the
ChainGatefor causal ordering.
Ordering and Deduplication
DedupRing
The DedupRing is a FIFO cache with a capacity of 1000 hashes. It ensures that the client never processes or displays the same message twice, even if it is rediscovered through different feeds or topics.
ChainGate
The ChainGate enforces strict ordering based on the prev_msg_hash field in each MessageEnvelope.
- If a message arrives and its
prev_msg_hashmatches the last seen message from that sender, it is released to the UI. - If it doesn’t match, it is buffered, and the reader triggers a refetch of the missing hash with an exponential backoff.
- If a gap remains for more than 5 seconds (
GAP_TIMEOUT), theChainGateforce-releases the buffered messages, marking them aslate.
History and Eviction
The FeedRecord has a limited capacity (max 26 hashes). When the message count reaches SUMMARY_EVICT_TRIGGER (20), the publisher performs an eviction.
- The 15 oldest messages (
SUMMARY_EVICT_COUNT) are moved into a newSummaryBlock. - The
SummaryBlockis stored as an immutable record. - The
FeedRecordis updated to point to the newSummaryBlockhash and contains only the remaining 5 newest messages. - On a cold start, a reader can walk back through these
SummaryBlockpointers up to aMAX_SUMMARY_DEPTHof 100 blocks.
Inbox and Invites
The inbox monitor handles parallel scanning for new invites.
- Snapshot: The monitor takes a snapshot of currently known feed sequences.
- Parallel Scan: It fires 8 concurrent DHT lookups for the 8 inbox topics.
- Resolution: Peer pubkeys found in the topics are fanned out into parallel
mutable_getcalls to retrieveInviteRecords. - Verification: Invites are decrypted using the
invite_key(derived via ECDH) and verified. - Nudge: In DM sessions, a “nudge” is sent at most once per epoch to signal the sender’s presence. A nudge is an encrypted
InviteRecordpublished viamutable_puton the sender’s invite-feed keypair (with the lure payload truncated to 800 bytes), followed by anannounceon the recipient’s current inbox topic. This matches the regular inbox-invite write path.
Graceful Shutdown
Upon exit, the client attempts a clean teardown:
- Publisher Drain: It waits for the publish queue to empty.
- Invite Retraction: For DM sessions, it attempts to retract the inbox invite by publishing an empty payload to the invite feed with a 1-second timeout.
- Terminal Reset: The TUI is disabled and terminal settings are restored.
Chat Reference
Technical reference tables for constants, flags, and filesystem layouts in the Peeroxide chat subsystem.
Constants
| Constant | Value | Description |
|---|---|---|
MAX_RECORD_SIZE | 1000 bytes | Maximum size of any single DHT record. |
MSG_FIXED_OVERHEAD | 180 bytes | Combined size of envelope fields (excluding name/content). |
MAX_SCREEN_NAME_CONTENT | 820 bytes | Max sum of screen name + content lengths. |
NONCE_SIZE | 24 bytes | XSalsa20 nonce size. |
TAG_SIZE | 16 bytes | Poly1305 tag size. |
CONTENT_TYPE_TEXT | 0x01 | Record content type for text messages. |
INVITE_TYPE_DM | 0x01 | Inbox invite type for direct messages. |
INVITE_TYPE_PRIVATE | 0x02 | Inbox invite type for private channels. |
SUMMARY_EVICT_TRIGGER | 20 | Messages in FeedRecord before summary eviction. |
SUMMARY_EVICT_COUNT | 15 | Number of messages moved to summary on eviction. |
MUTABLE_PUT_RETRY_MS | [200, 500, 1000] | Retry intervals for mutable DHT updates. |
ROTATION_CHECK_INTERVAL | 30s | How often the publisher checks for feed rotation. |
MAX_SUMMARY_DEPTH | 100 | Maximum number of summary blocks to walk back. |
FEED_EXPIRY_SECS | 1200 | Time (20 min) after which a feed is considered stale. |
DISCOVERY_INTERVAL_SECS | 8 | Frequency of reader discovery lookups. |
HISTORY_CAP | 500 | TUI scrollback history limit (in memory). |
CTRL_C_ARM_WINDOW | 2s | Double-press window for force-exit. |
DEDUP_RING_CAPACITY | 1000 | Max hashes stored in the deduplication set. |
GAP_TIMEOUT | 5s | Time before ChainGate force-releases out-of-order msgs. |
REFETCH_SCHEDULE_MS | [0, 500, 1500, 3000] | Backoff intervals for missing hash refetching. |
CLI Flags
Global Flags
--debug: Enable stderr debug logs.--probe: Enable stderr trace probes.--line-mode: Force line-based I/O.
Subcommand: join
--profile <name>: Profile to use (default:default).--group <salt>: Private channel salt (conflicts with--keyfile).--keyfile <path>: Private salt from file (conflicts with--group).--no-nexus: Skip nexus refresh/publish.--no-friends: Skip friend refresh.--read-only: Listen only mode.--stealth: Shorthand for--no-nexus --read-only --no-friends. Note this does not suppress inbox polling; see Stealth Mode in the user guide for the full threat-model breakdown.--feed-lifetime <min>: Feed rotation interval (default:60).--batch-size <n>: Max messages per batch (default:16). Values below1are clamped to1.--batch-wait-ms <ms>: Batch window (default:50).--stay-after-eof: Enter listener mode on EOF.--no-inbox: Disable inbox monitor.--inbox-poll-interval <s>: Inbox scan frequency (default:15). Values below1are clamped to1.
Subcommand: dm
Same session-flag surface as join, except --group and --keyfile are not accepted (the DM channel key is derived deterministically from the two participants’ identity public keys). DM also adds:
--message <text>: Initial inbox-invite lure text. Ignored in stealth/read-only mode.
Subcommand: inbox
--profile <name>: Profile to use (default:default).--poll-interval <secs>: Polling interval (default:15). Values below1are clamped to1.--no-nexus,--no-friends: Accepted for flag-surface parity withchat join/chat dmbut are no-ops here (the inbox CLI does not run nexus / friend background tasks).
Subcommand: whoami
--profile <name>: Profile to inspect (default:default).
Subcommand: profiles
profiles list: no flags.profiles create <name> [--screen-name <name>]: optional initial screen name; otherwise a deterministic vendor name is generated.profiles delete <name>: rejectsdefault.
Subcommand: friends
friends list [--profile <name>]: also the implicit default if no subcommand is given.friends add <key> [--alias <name>] [--profile <name>]: alias auto-fills from the known-users cache (or vendor name) when omitted.friends remove <key> [--profile <name>].friends refresh: one-shot DHT refresh; does not accept--profileand operates on thedefaultprofile only.
Subcommand: nexus
--profile <name>,--set-name <name>,--set-bio <text>,--publish,--lookup <pubkey-hex>,--daemon(publish every 480 s, refresh all friends every 600 s).--lookupshort-circuits to lookup mode.- When at least one of
--set-name/--set-biois supplied and neither--publishnor--daemonis set, the setters are written to the profile and the command exits without publishing. - When no flags (or only
--profile) are supplied, the command still performs a single Nexus publish.
Profile Directory Layout
Profiles are stored under ~/.config/peeroxide/chat/profiles/ (the chat subsystem uses the XDG-style ~/.config/peeroxide/chat/ root regardless of the platform-specific config dir used by peeroxide’s top-level config file).
~/.config/peeroxide/chat/profiles/<profile_name>/
├── seed # 32-byte raw Ed25519 secret seed
├── name # Optional UTF-8 screen name
├── bio # Optional UTF-8 biography
└── friends # Friend list (TSV)
Friends File Schema
The friends file is a Tab-Separated Values (TSV) file:
<64-hex-pubkey>\t<alias>\t<cached_name>\t<cached_bio_line>
Shared Known Users
Located at ~/.config/peeroxide/chat/known_users.
- Format: TSV
<64-hex-pubkey>\t<screen_name> - Capacity: 1000 entries (FIFO).
- Reloading: 5s mtime-debounced reload.
Name Resolution Precedence
NameResolver (peeroxide-cli/src/cmd/chat/name_resolver.rs) resolves a peer’s identity public key in the following order:
- Friend Alias: the friend’s locally assigned alias, if non-empty.
- Known Screen Name: the latest screen name for that pubkey in the shared
~/.config/peeroxide/chat/known_userscache, if non-empty. - Vendor Fallback: a deterministic auto-generated name derived from the pubkey seed.
Note: the friends file’s per-friend cached_name and cached_bio_line columns are populated by the nexus refresh task for display in the friends-list and friend nexus prints, but NameResolver itself does not consult them — it goes straight from friend alias to the shared known_users cache.
The two output formats:
bar_label()— compact label used in the status bar:- friend alias source → bare alias (e.g.
bob). - any other source →
name@shortkey(e.g.alice@a1b2c3d4), whereshortkeyis the first 8 hex characters of the pubkey.
- friend alias source → bare alias (e.g.
formal()— uniform fully-qualified label:name (shortkey)(e.g.alice (a1b2c3d4)), regardless of source.
Security Model
This appendix describes the threat model and security properties of the peeroxide-cli tools.
Transport Security
All peer-to-peer connections established by peeroxide-cli use the Noise XX handshake protocol followed by a SecretStream encrypted channel. This provides:
- Mutual authentication: Both peers authenticate with their Ed25519 public keys during handshake.
- Forward secrecy: Session keys are ephemeral and derived per-connection.
- Confidentiality and integrity: All application data is encrypted and authenticated.
The Noise XX pattern is the same used by the Node.js Hyperswarm stack, ensuring interoperability.
Peer Identity
A peer’s identity is its Ed25519 public key (32 bytes). There is no central authority — identity is self-sovereign. Anyone who controls a private key controls that identity.
- Ephemeral identities (default for
announce) are generated fresh on each run and leave no persistent trace. - Seeded identities (
--seedflag) are deterministic:KeyPair::from_seed(discovery_key(seed.as_bytes())). The seed string is a secret — anyone who knows it can derive the same keypair.
Warning: Treat
--seedvalues like passwords. They are not hashed with a KDF — raw BLAKE2b is fast, making brute-force of short seeds feasible.
DHT Trust Model
The DHT is untrusted infrastructure. Any node can relay packets, and routing table entries are not authenticated. Mitigations:
- Mutable DHT values are Ed25519-signed by the originating keypair. Verifiers (including
lookup --with-data) confirm the signature before using the data. Forging data requires the private key. - Immutable DHT values (used by
cp) are addressed by the SHA-256 hash of their content. Content is verified on retrieval. - Topic keys are not secret — anyone who knows the topic can look up its peer list. Do not treat topic confidentiality as a security property.
dd (Dead Drop) Threat Model
dd uses mutable DHT storage addressed by (public_key, topic). Security properties:
- Only the holder of the private key can write to a slot (signatures enforced by the DHT).
- Anyone who knows
(public_key, topic)can read the slot — there is no access control on reads. - Data is signed but not encrypted at the DHT layer. For sensitive payloads, encrypt the application data before using
dd. ddis designed for asynchronous communication where sender and receiver share a topic out-of-band.
cp Threat Model
cp data is immutable and content-addressed. Anyone who knows the cp:// key can retrieve the data. There is no access control. Do not use cp for sensitive data without prior encryption.
Echo Protocol Security
The echo protocol (see Echo Protocol) is intentionally minimal — it echoes arbitrary 16-byte payloads after a PING/PONG handshake. Session concurrency is capped at MAX_ECHO_SESSIONS = 64 to limit resource exhaustion. The handshake uses a 5-second timeout to prevent slowloris-style attacks.
Because announce connections go through the Noise XX handshake, all echo traffic is authenticated and encrypted. An unauthenticated party cannot reach the echo server.
Denial of Service Considerations
- Bootstrap nodes are configurable. An attacker controlling all bootstrap nodes can eclipse a node from the DHT.
- The DHT is susceptible to Sybil attacks in the general case — this is a known limitation of permissionless DHTs.
announcedata refresh runs every 600 seconds. If a node is offline, its announcement expires naturally according to DHT TTL policies.
Limits and Performance
This appendix documents hard limits, configurable bounds, and observed performance characteristics of the peeroxide-cli tools.
Hard Limits
| Constant | Value | Context |
|---|---|---|
MAX_ECHO_SESSIONS | 64 | Concurrent echo sessions per announce process |
HANDSHAKE_TIMEOUT | 5 s | Echo protocol handshake timeout |
IDLE_TIMEOUT | 30 s | Echo session idle timeout |
ECHO_MSG_LEN | 16 bytes | Echo probe frame size (fixed) |
ECHO_TIMEOUT (ping) | 5 s | Per-probe timeout in ping --connect mode |
MAX_CHUNKS | 65 535 | Maximum chunks in a single dd message |
MAX_PAYLOAD | 1 000 bytes | Maximum payload per dd chunk |
ROOT_HEADER_SIZE | 39 bytes | dd root chunk header size |
NON_ROOT_HEADER_SIZE | 33 bytes | dd non-root chunk header size |
CHUNK_SIZE (cp) | 65 536 bytes | cp file chunk size |
--data max (announce) | 1 000 bytes | Maximum --data payload for announce |
lookup --with-data concurrency | 16 | buffer_unordered(16) for mutable DHT gets |
| ping topic mode peer cap | 20 | Maximum peers probed per topic lookup |
Derived Limits
Maximum dd message size:
MAX_CHUNKS × MAX_PAYLOAD = 65 535 × 1 000 = ~65.5 MB
Practical limit is lower due to DHT value size constraints and network latency.
Maximum cp file size:
Limited by available DHT storage and client memory. Each 65 536-byte chunk is stored as a separate immutable DHT value. There is no hard-coded upper bound in the CLI, but very large files will require many round-trips.
Timing
| Parameter | Value | Notes |
|---|---|---|
announce refresh interval | 600 s | Background mutable put to keep slot alive |
announce seq | Unix epoch seconds | Two refreshes in the same second produce identical seq — see ISSUES.md |
ping --interval default | 1.0 s | Configurable |
ping --count default | 1 | 0 = infinite |
Concurrency
lookup --with-datafetches peer data in parallel with a concurrency window of 16 (buffer_unordered(16)).announcehandles incoming connections concurrently; echo sessions are bounded byMAX_ECHO_SESSIONS = 64.cpuploads/downloads chunks sequentially per file (parallelism may be added in future releases).
Exit Codes
| Code | Meaning | Tools |
|---|---|---|
| 0 | Success / clean shutdown | all tools |
| 1 | Fatal error | all tools |
| 130 | SIGINT received | lookup, ping |
| 0 | SIGINT/SIGTERM received | announce (intentional — clean shutdown is success) |
Note: announce returns 0 on SIGINT/SIGTERM because interactive shutdown is the normal workflow. lookup and ping return 130 to allow callers to distinguish interruption from success.
Chat
| Parameter | Value | Description |
|---|---|---|
| Max record size | 1000 bytes | Maximum size for a single DHT record |
| Message overhead | 180 bytes | Fixed overhead (screen name + content combined ≤ 820 bytes) |
| Encryption | XSalsa20-Poly1305 | Security parameters: nonce 24 bytes, tag 16 bytes |
| Epoch length | 60 s | Time window for message bucketing |
| Buckets per epoch | 4 | Sub-divisions within an epoch for message distribution |
| DHT lookups per cycle | 8 | Checks current and previous epoch across 4 buckets |
| Discovery interval | 8 s | Cadence for looking up new peers |
| Feed expiry | 1200 s | Time before a peer feed is considered stale |
| Summary eviction trigger | 20 messages | Number of messages before clearing old history |
| Summary eviction count | 15 messages | Number of messages removed during eviction |
| Mutable put retries | 3 | Retries at 200 ms, 500 ms, and 1000 ms intervals |
| Rotation check interval | 30 s | Frequency of checking for epoch/bucket rotation |
| Dedup ring capacity | 1000 hashes | Number of message hashes stored to prevent duplicates |
| Gap timeout | 5 s | Maximum wait time for out-of-order messages |
| TUI history cap | 500 lines | Scrollback buffer limit in the interactive interface |
Chat Performance
The inbox polling mechanism uses parallel lookups and mutable gets. A full inbox cycle typically completes in 2-4 seconds of wall-clock time. This is a significant improvement over earlier nested-serial designs which required 10-20 seconds for the same operation.
Dead Drop (v2)
| Parameter | Value | Description |
|---|---|---|
| Max chunk size | 1000 bytes | Total size including headers |
| Data payload | 998 bytes | Actual data bytes per non-root chunk |
| Root index slots | 30 | Pointers to child chunks in the root node |
| Non-root index slots | 31 | Pointers to child chunks in intermediate nodes |
| Need-list entries | 124 | 8-byte entries published in each DHT record |
| Parallel fetch cap | 64 | Maximum concurrent DHT requests |
| Soft depth cap | 4 | Maximum tree depth (~27 GB capacity) |
| Per-put timeout | 30 s | Maximum duration for a single chunk upload |
| Stall watchdog check | 5 s | Frequency of progress monitoring |
| Stall watchdog trigger | 30 s | Time with no progress before triggering a restart |
| Need-list publish | 20 s | Frequency of publishing the local need-list |
| Need-list announce | 60 s | Keepalive interval for the need-list topic |
| Refresh interval | 600 s | Default cadence for re-announcing data availability |
| Initial concurrency | 128 | Starting sender concurrency for AIMD |
| Fetch backoff | 500 ms to 15 s | Progressive delay for failed mutable or immutable gets |
Tree Capacity by Depth
The implementation enforces SOFT_DEPTH_CAP = 4. Depths beyond that are theoretical only and are rejected at PUT time.
| Depth | Max Data Chunks | Approx. Capacity |
|---|---|---|
| 0 | 30 | 29 KB |
| 1 | 930 | 928 KB |
| 2 | 28,830 | 28 MB |
| 3 | 893,730 | 891 MB |
| 4 | 27,705,630 | 27 GB |
AIMD Algorithms
v2 (Current):
- Uses Exponentially Weighted Moving Average (EWMA) with alpha 0.1.
- Decision interval of 20 samples.
- Fast-trip threshold of 10.
- Shrink factor: 0.75×.
- Growth factor: +2.
v1 (Legacy):
- Uses a tumbling window of 10 samples.
- Halves concurrency if degradation exceeds 30%.
- Increases concurrency by 1 if 0% degradation is detected.