Defines a bounded rendezvous overlay that resolves known swarm descriptors to untrusted peer addresses while making bootstrap, replay, resource, persistence, and metadata-disclosure boundaries explicit.
A version-1 descriptor SHALL bind the swarm application name, magic bytes, and major/minor version pair used by the existing compatibility handshake. Its canonical bytes SHALL be, in order:
darkfi-swarm-id-v1 followed by one zero byte;Application names MUST contain 1 through 32 UTF-8 bytes. Other private-secret
lengths and flag values MUST be rejected. SwarmId SHALL be the 32-byte
BLAKE3 hash of the canonical bytes. Implementations SHALL rely on collision
resistance rather than claim collision is impossible. Patch, prerelease, and
build metadata SHALL NOT affect the ID.
For public darkirc, magic fb e5 c7 b5, and compatibility pair 0.5, the
canonical bytes SHALL be:
6461726b66692d737761726d2d69642d76310000076461726b697263fbe5c7b50000000000000000000000000000000500
and SwarmId SHALL be:
cad1d01807849a400541725bd75af14e53d259392082c14fa96ce6313551feb4.
A generated private secret MUST use a cryptographically secure random source. A caller-supplied secret SHALL be accepted only as exactly 32 bytes and SHALL be documented as requiring independent high entropy. It makes an identifier difficult to derive only before observation. It MUST NOT be represented as authentication, encryption, authorization, or continuing confidentiality.
Public enumeration MUST omit records from accepted ads marked non-public, but visibility is unauthenticated sender data rather than an intrinsic property of an ID. Persistent gossip peers necessarily observe IDs they store or relay, and an attacker that learns an ID can submit another ad marking it public.
Bootstrap addresses SHALL use ordinary non-seed sessions capable of carrying swarm requests and responses, not existing short-lived seed sessions. “Ordinary” SHALL describe session type, not a requirement that a transient keep the channel indefinitely. A transient with a cache SHALL first construct and start an overlay instance using cached peers only. If no ordinary compatible channel completes within the stage timeout, it SHALL fully stop and discard that instance before constructing a fresh overlay instance using configured ordinary bootstrap peers. It MUST NOT depend on runtime manual-session reload.
Each stage timeout MUST be finite and no greater than 300 seconds. The cache
MUST contain no more than 256 endpoint records, 262,144 encoded file bytes, or
1,024 encoded bytes per URL. A record SHALL contain only the exact canonical
connect URL and resolved endpoint actually used by a successfully completed
ordinary channel advertising ("swarm-ad-store", 1). Advertised external
addresses MUST NOT be cached from that feature handshake. The cache MUST NOT
contain features, swarm IDs, ads, queries, or source/query associations.
Before dialing a fresh advertised or configured untrusted target, the joiner SHALL resolve clearnet names once and validate the selected socket address. A cached target SHALL instead revalidate and reuse its stored successfully connected socket without DNS resolution. Unless explicit local-test mode is enabled, both paths MUST reject loopback, private, shared, link-local, multicast, unspecified, documentation, benchmarking, and otherwise reserved destinations for IPv4 and IPv6. Production lilith MUST NOT enable local-test mode.
Every validated target SHALL carry the original URL, one mandatory exact socket, and a direct or trusted-proxy route kind. For a direct route, the socket is the validated destination and connection SHALL use it without a second DNS lookup; the original hostname MAY be retained only for TLS identity.
For a trusted-proxy route, the mandatory socket SHALL be the exact endpoint from trusted local proxy configuration, not the advertised destination. That proxy socket MAY be loopback/private under local trust policy; an advertisement MUST NOT select or override it. The untrusted destination SHALL be either a globally routable IP literal or a canonical Tor/I2P hidden-service name matching the transport. A hidden-service name MUST NOT undergo local DNS and MAY be passed only inside proxy negotiation and for TLS identity. Arbitrary clearnet hostnames MUST NOT be sent for remote proxy DNS. Missing/malformed proxy configuration and transport/scheme mismatch SHALL reject the candidate. One join attempt SHALL dial one exact route at most once.
A clearnet DNS answer SHALL contain at most 16 socket addresses and every answer
SHALL consume the join's resolution budget and pass address classification.
Empty or oversized answers SHALL fail. A bounded nonempty answer SHALL be
iterated without unchecked indexing; its allowed sockets SHALL be shuffled with
OsRng before selecting at most one exact socket for that URL. A cached trusted-
proxy route SHALL revalidate that its exact socket still matches current trusted
local proxy configuration; mismatch fails without DNS or fallback.
An advertised candidate SHALL remain a typed pair of original URL and validated resolved socket through the swarm connection attempt. Before compatibility succeeds it MUST NOT be downgraded into a URL-only hostlist, persisted, or sent through a connector/refinery path that resolves it again. Failed candidates SHALL be dropped. After compatibility succeeds, ordinary host persistence MAY record the peer URL, but every future reconnect or refinement attempt MUST resolve, validate, budget, and dial one exact socket again.
The complete attacker-selected candidate pipeline SHALL be fallible: URL parse,
scheme allowlist, host/port extraction, DNS result handling, address
classification, validated-target construction, proxy selection and negotiation,
transport/TLS dialing, and compatibility. It MUST NOT use unwrap, expect,
explicit panic, unchecked slicing/indexing, or an unimplemented transport branch.
Unsupported or unaudited schemes MUST be rejected before dialer construction.
Empty or oversized DNS results, malformed targets, missing/malformed proxies,
timeouts, cancellation, and transport errors MUST return bounded candidate
errors. Bounded multiple-address results MUST be iterated and classified without
unchecked selection. Processing SHALL continue or terminate only according to
the join budget.
OsRng-shuffled allowed exact socket is selected for that URLAn overlay channel SHALL remain owned by the overlay P2p identified by fixed
swarm magic bytes, app name, version policy, host state, and protocol registry.
It MUST NOT carry swarm application messages, be transferred to a swarm
P2p, be re-handshaken in place under a swarm identity, or multiplex traffic
tagged by swarm ID. Joining SHALL create a separate swarm channel under that
swarm's own identity and state.
A persistent participant SHALL keep its overlay active while it performs
durable store or gossip duties. A participant serving any swarm SHALL keep the
overlay active while advertisement authoring is enabled. The default transient
policy SHALL retain the overlay for the application session and MUST NOT stop it
as an automatic reaction to lookup or join completion. The application session
ends only through explicit overlay stop or full SwarmPool shutdown. An explicit
reduced-privacy policy SHALL stop after every caller-visible lookup/join terminal
outcome—success, empty result, error, timeout, or cancellation—but MUST NOT stop
after an internal lookup phase within join. Configuration and documentation
MUST warn that responder/swarm observers can correlate the query, swarm
connection, and teardown timing. Stopping the overlay MUST NOT stop or transfer
the independent swarm P2p; a later lookup establishes a new overlay session
only when no active overlay remains.
Every lookup request SHALL carry a fresh cryptographically random 16-byte request ID. Its response or bounded error SHALL echo that ID. A channel SHALL have no more than 32 outstanding requests. Responses with unknown, duplicate, expired, or mismatched request IDs MUST be rejected. Pending request state MUST be removed on response, timeout, or disconnect. Request timeout SHALL default to 10 seconds, be configurable no higher than 60 seconds, and produce a local timeout error; a late response is unsolicited and receives no wire error.
Protocol hard limits SHALL be:
| Message | Maximum encoded bytes |
|---|---|
SwarmAd |
65,536 |
GetSwarmAddrs |
128 |
SwarmAddrs |
65,536 |
GetPublicSwarms |
128 |
PublicSwarms |
16,384 |
SwarmError |
128 |
Every encoded URL MUST be no more than 1,024 bytes. A page cursor SHALL be a
fixed 65-byte value encoded as version u8, last returned key [32], and
terminal key [32]. Size/count validation MUST occur before store, index,
response, or relay work.
Message command strings and canonical field order SHALL be:
| Command | Fields in encoded order |
|---|---|
ad |
SwarmId[32], visibility u8, ad ID [32], lifetime u32, URL vector |
getaddr |
request ID [16], SwarmId[32], optional cursor |
addrs |
request ID [16], SwarmId[32], URL vector, optional cursor |
getswarm |
request ID [16], optional cursor |
swarms |
request ID [16], SwarmId vector, optional cursor |
err |
request ID [16], error code u8 |
Fields SHALL use existing DarkFi canonical wire encoding. Visibility values
SHALL be 0 = public and 1 = non-public; other values are invalid. Error
values SHALL be 0 = malformed, 1 = invalid cursor, 2 = enumeration
disabled, and 3 = busy; other values are invalid. Lifetime is unsigned
32-bit and cursor version SHALL be one.
Every attacker-controlled swarm, version, and verack decoder SHALL return a
fallible error for malformed or truncated input without unwind. Such paths MUST
NOT use unwrap, expect, explicit panic, unchecked slicing/indexing, or
reserve/allocate from an unvalidated declared length/count. Bounds validation
MUST precede allocation and element decoding.
An ad SHALL contain exactly one swarm ID, public/non-public visibility, a fresh 32-byte per-ad ID, lifetime seconds, and 1 through 32 serving addresses. The ad ID MUST use a cryptographically secure random source and MUST NOT be reused for another emission or swarm. It is deduplication data, not identity.
Lifetime MUST be 1 through 86,400 seconds. Ads MUST NOT contain last_seen, a
stable node ID, signing key, author, relay provenance, hop count, or identifier
shared with another swarm. Every address MUST be valid, publicly shareable,
and within the URL bound. A receiver SHALL reject the entire ad if any field or
address is invalid.
A persistent participant SHALL use finite nonzero per-swarm, global-address, per-swarm protected-ID, and global protected-ID caps. Configured values MUST NOT exceed 1,024 addresses per swarm, 65,536 total addresses, 1,024 protected IDs per swarm in the general pool, or 262,144 protected IDs globally. Runtime expiry SHALL use a monotonic deadline from local receipt and sender clocks SHALL have no effect.
Those caps SHALL default respectively to 256 addresses per swarm, 16,384 total addresses, 256 general protected IDs per swarm, and 65,536 protected IDs globally.
The receiver SHALL clamp each accepted address lifetime to the lesser of the wire lifetime and a local receive cap. That cap SHALL default to 7,200 seconds, MUST be nonzero, and MUST NOT exceed 86,400 seconds. Local author lifetime SHALL also default to 7,200 seconds and MUST NOT exceed 86,400 seconds.
Receiving a retained ad ID MUST NOT extend expiry or repeat relay work. The dedup deadline SHALL be exactly 86,400 seconds after the associated locally clamped address expiry, making total protection no greater than 172,800 seconds from acceptance. A protected ID MUST NOT be evicted before that deadline. A fresh remote ad SHALL be rejected without address mutation or relay when its swarm's general-pool quota or the global general pool has no expired slot. Expired IDs may be evicted deterministically.
Local-author reserve-swarm partitions SHALL default to 32 and MUST NOT exceed
Startup SHALL validate the partitions independently: general protected IDs MUST fit the global general capacity and each general per-swarm quota; local IDs MUST fit 256 slots for each distinct reserved swarm and the configured partition count. Persisted local IDs already occupy their reserve and MUST NOT be counted again as general state. Locally authored IDs MAY NOT evict protected IDs. The reserve prevents remote admission from blocking allocated local cadence, but does not provide preferential validation or remote role privilege.
Fresh IDs may refresh addresses subject to caps. Address eviction SHALL choose expired entries first, then earliest expiry, then lexical key. Stores MUST NOT dial advertised addresses or record ad sources, queriers, query history, or source-peer/swarm associations. Replay IDs SHALL remain globally keyed; each record SHALL bind its advertised swarm only for quota/protection accounting, so reuse of one ad ID under another swarm is still a duplicate. A local-author reserve record necessarily marks an ID as generated by this process; that local fact and reserve occupancy/use/failure/timing MUST NOT enter wire messages, RPC, status, metrics, telemetry, or peer-linked state, even as aggregate counters. Fresh forged IDs can still poison within bounds; the store provides no authenticity.
Acceptance SHALL atomically commit the global seen-ID record, quota/reserve accounting, address records, and public-index mutation before any relay job is enqueued. Commit failure SHALL cause no mutation or relay. Rollback of the database to a snapshot before that commit can remove the seen ID and permit a later replay; this capability makes no non-rollbackable replay guarantee.
Store state SHALL be normalized per (SwarmId, canonical address). Accepting
a fresh ad updates visibility and expiry for every address present in that ad;
addresses absent from it retain their current record until independently
updated, expired, or evicted. An ID is publicly enumerable iff at least one
live normalized record is marked public. Public-to-non-public and reverse
updates of the same address take effect atomically. Direct lookup returns all
live records regardless of visibility.
Persistent replay state SHALL use unsigned 64-bit monotonic epoch ticks in
seconds. Each seen-ID record stores its checked deadline tick, and one atomic
metadata checkpoint stores elapsed tick every 300 seconds by default, no less
often than every 600 seconds, and on clean shutdown. On restart, a record with
deadline <= checkpoint SHALL be expired without subtraction. Otherwise the
implementation SHALL use checked subtraction, reject a delta greater than
173,400 seconds as incoherent, clamp valid remaining duration to 172,800
seconds, and use checked duration conversion and Instant deadline addition.
Overflow, underflow, missing/incoherent epoch metadata, or failed deadline
construction SHALL return a typed startup error.
If a durable checkpoint cannot complete before the 600-second maximum interval, the persistent store SHALL reject fresh ad acceptance and local authoring until a checkpoint succeeds or controlled shutdown completes; it MUST NOT continue creating deadline deltas outside the validated bound.
All surviving records and new epoch metadata SHALL replace the old epoch in one atomic batch; interruption leaves the old epoch loadable. Downtime and the uncheckpointed interval are not subtracted, so they MAY extend a record present in the loaded database, but restart MUST NOT reset every such ID to a fresh full horizon or shorten it. A database rollback before seen-ID commit can remove the record entirely and is explicitly outside that guarantee. Address records continue to use persisted wall expiry and MAY expire conservatively on clock anomalies. If separately validated general/reserve capacities cannot contain valid persisted protected IDs, startup SHALL fail without eviction.
Malformed or unverifiable persisted seen-ID, quota/reserve, or epoch state SHALL fail startup. Malformed address records MAY be quarantined and a public index MAY be rebuilt only when authoritative replay/accounting state remains intact.
Persistent participants SHALL relay each newly accepted ad without changing identifying contents. Relay fanout MUST be finite and no greater than 64. Queued relay work MUST be bounded and duplicates MUST NOT be requeued. Own ads SHALL be authored only on a fixed 1,800-second base cadence with independently sampled uniform jitter from -600 through +600 seconds. This cadence is not configurable in version one. Authored lifetime SHALL default to 7,200 seconds and MUST NOT exceed 86,400 seconds. Swarm start, listener start, and new overlay channels MUST NOT trigger authoring.
A transient MAY relay newly accepted ads from bounded memory but SHALL NOT author one. No author field means an immediate sender is not protocol-level proof of authorship; this MUST NOT be described as hiding authorship against timing, topology, first-seen, or global observation.
Direct lookup SHALL name one swarm and return addresses only for it. Each page
SHALL echo the request ID, contain at most 64 addresses and 65,536 encoded
bytes, and include at most one fixed cursor. On the first page, the responder
SHALL capture the current greatest live ordered key as the terminal key. A next
cursor SHALL contain the last returned key and that fixed terminal key. Later
pages SHALL return only live keys strictly greater than the last key and no
greater than the terminal key, advancing the last key monotonically. A cursor
whose version, length, or key ordering is invalid SHALL return a bounded invalid-
cursor error. Index mutation MUST NOT invalidate a well-formed cursor, create a
server snapshot, or force traversal restart; it MAY cause records added/removed
during traversal to be included or omitted. Page limits still bound completion.
For every page, the responder SHALL derive canonical keys, return each key at
most once in strictly ascending order, and keep every key in
(previous_last, terminal] when a previous cursor exists. A next cursor MUST be
absent on an empty page and otherwise its last key MUST equal the greatest
returned key with last < terminal. The requester SHALL independently derive
and validate those keys, reject duplicates within/across pages, reject an empty
page with a next cursor, and reject a response cursor that changes the first
page's terminal, fails to advance, or disagrees with returned keys. Requester
dedup state remains bounded by page/item limits.
Public enumeration SHALL be disabled by default and require explicit local enablement. If enabled, it SHALL return IDs having at least one live normalized address record marked public, at most 256 IDs and 16,384 bytes per page. It SHALL be available to every protocol-correct connected peer without role privilege and MAY be disabled globally without disabling direct lookup. Visibility is not authenticated: an attacker can cause an observed ID to appear by submitting a public-marked address record. No response SHALL include ad sources or querier data.
A persistent participant SHALL advertise exactly
("swarm-ad-store", 1), maintain durable bounded state, and relay ads. A node
without it SHALL be treated as transient. The feature MUST NOT grant privilege.
Local and remote version messages SHALL allow at most 10 external addresses and
10 features; node ID SHALL be at most 64 encoded bytes, app name at most 32,
semver prerelease and build strings at most 32 each, every URL at most 1,024,
and every feature name at most 32. Complete outgoing VersionMessage and
VerackMessage encoded-size validation MUST succeed before send. Inbound
decoding of both messages MUST check every declared variable length/count,
including semver strings, before reservation or allocation while preserving
existing field order and valid wire encoding byte-for-byte.
A transient SHALL accept no inbound overlay connections, author no ads, and persist no swarm-overlay ad/query/history state. It MAY keep bounded in-memory ads and the bounded successful-endpoint-only cache. Swarm lifecycle persistence and transport-managed state are separate scopes and MUST be documented separately. Both roles apply identical decoding, validation, authorization, and work bounds.
VERACK_MAX_BYTESEvery message type SHALL have hard per-channel metering. In a 10-second window, one channel SHALL accept at most 32 ads, 16 direct lookup requests, 16 direct responses, 4 public-list requests, 4 public-list responses, and 16 bounded errors before strict-policy delay/penalty. It SHALL also receive at most 32 store-write and 32 relay-enqueue work tokens per 10 seconds and at most 1,048,576 response bytes per 60 seconds.
Variable fields SHALL be validated before allocation or work. Configured local limits SHALL use these defaults and MUST NOT exceed these maxima:
| Resource | Default | Maximum |
|---|---|---|
| relay queue jobs | 1,024 | 4,096 |
| concurrent durable writes | 8 | 32 |
| concurrent query reads | 16 | 64 |
| concurrent relay workers | 8 | 32 |
| pages consumed per direct join lookup | 16 | 16 |
| pages consumed per public enumeration | 4 | 16 |
| candidate addresses per join attempt | 64 | 256 |
| previously compatible retry attempts | 16 | 64 |
| persisted compatible retry URLs per swarm | 64 | 256 |
| local-author reserve swarm partitions | 32 | 256 |
| active swarms | 32 | 256 |
| concurrent lifecycle attempts | 8 | 32 |
| shutdown deadline seconds | 120 | 600 |
| pending-request timeout seconds | 10 | 60 |
| configured ordinary overlay peers | 8 | 256 |
| overlay bind/listener addresses | 1 | 16 |
| serving bind addresses per swarm | 1 | 16 |
| serving external addresses per swarm | 1 | 32 |
| overlay inbound channels | 64 | 256 |
| overlay outbound channels | 8 | 64 |
| overlay manual channels | 8 | 256 |
| total established overlay channels | 80 | 512 |
| untrusted dial concurrency | 4 | 16 |
| untrusted dial starts per minute | 32 | 128 |
| DNS resolutions per join attempt | 64 | 256 |
| dials per resolved destination per attempt | 1 | 1 |
Overlay instances SHALL use strict ban policy. Queue/concurrency/retry settings outside these ranges SHALL be rejected at configuration time.
A small request MUST NOT induce an unbounded response, write, relay, allocation, or outbound connection. Budget accounting SHALL use ephemeral channel IDs, not peer addresses, and SHALL be removed on disconnect.
Lookup results SHALL remain ephemeral typed original-URL/resolved-socket targets for the requested swarm until compatibility succeeds; they MUST NOT enter a URL-only host/refinery set first. A joining swarm MUST apply ordinary magic, application-name, and major/minor checks before treating a peer as compatible. Failure MUST drop the candidate, remain fallible, and MUST NOT penalize the overlay relay. Passing compatibility does not authenticate an operator or authorize application access. Future ordinary reconnect/refinement attempts MUST independently resolve and validate a new exact target.
Before resolution or dialing, the joiner SHALL place previously compatibility- verified persisted ordinary peers in one tier and fresh overlay URLs in another. The swarm retry index itself SHALL contain at most the configured persisted- compatible cap; when full, a newly compatible URL remains usable for its current session but MUST NOT evict an existing retry URL merely to enter that index.
A direct join lookup SHALL continue through the first page's terminal until no
next cursor remains or its fixed 16-page cap is consumed. It SHALL use bounded
OsRng reservoir sampling across every valid URL returned in that traversal to
select at most the candidate-address cap, rather than truncating an ordered
prefix when candidate capacity is reached. The full bounded persisted index and fresh
reservoir SHALL then be independently shuffled with OsRng; wire, store, URL,
hash, DNS-answer, or lexical order MUST NOT choose the attempted prefix.
After both URL tiers are built, candidate preparation SHALL consume no more than half the then-remaining overall deadline; verified and fresh resolution/ validation SHALL each have half that subdeadline. Unused verified time MAY be donated to fresh preparation but not conversely. Every persisted peer MUST still undergo fresh resolution and egress validation.
Already validated targets SHALL be installed as a two-phase pre-start manual plan. At swarm start, the remaining candidate-dial duration SHALL be split at a monotonic midpoint. The verified phase MUST stop/cancel by that midpoint and consume no more than its configured retry limit or half the total candidate- attempt budget. Fresh targets activate for the second half and retain at least half the attempt capacity; if the verified phase is empty, fresh dialing MAY begin immediately. Every connector uses the exact validated target. No fresh candidate is persisted before compatibility.
OsRng
before resolution/dial selection, so lexical order does not choose the budgetWire messages MUST NOT intentionally bind different swarms to one stable node or signing identity. Durable state MUST NOT contain querier identity or query history. An answering peer nevertheless observes the requested ID; requests on one channel are linkable; timing, topology, public enumeration, and endpoint reuse are metadata surfaces. The capability MUST NOT claim PIR, guaranteed origin anonymity, absence of remote traces, or global-observer resistance.
Gossip and store peers necessarily observe and MAY retain every swarm-ID to advertised-endpoint mapping they receive. The forbidden provenance association is a mapping from overlay source peer to swarm/ad authorship; the rendezvous ID-to-endpoint mapping is intentional protocol output and is not confidential.
Every overlay and swarm P2p instance SHALL use an independently CSPRNG-
generated VersionMessage.node_id; it MUST NOT be persisted or reused across
instances, swarms, overlay/swarm roles, or process restart.
Serving documentation SHALL identify endpoint reuse as directly linkable and SHALL NOT claim automatic independent Tor/I2P provisioning.
P2p instances