Repository navigation
rnfs 0.2.0
The feature-complete release: the local API and the library for companion apps; providing at scale (sweep
providing, provider records on disk within a byte budget, storage tiers against floods); finding content when
identities are generated next to its key (the density check, disjoint lookup paths, the announce first-seen
log); LoRa islands (fast nodes sparing them, two-level records, partition mode); and new features (per-app API
tokens, encrypted folders, NomadNet add, gateway sites, name sync, name key backups, remote pin status, network
status, files inside folders provided by their own CIDs). LXMF messaging moved out of rnfs into an app of its own
(Removed, below). The wire format gained one optional request, /range (Added, below), which these nodes send
only to nodes that answer it; nothing else on the wire changed, and CIDs are unchanged: these nodes, 0.1.6, 0.1.5,
0.1.4 and 0.1.3 nodes work together (tests/interop.py against 0.1.6 passes). The local API stays compatible but
for LXMF: every route, field and status of 0.1.6 is unchanged but LXMF's, which are gone, and error bodies gain a
field. The upgrade of an <rnfs dir> is automatic and one-way.
Upgrade notes:
- State: the state database gains tables (
pin_times,pin_labels,name_exports,apps,block_sizes,
routing,peer_scores,announce_first_seen,provider_standing,remote_pins_asked,remote_pins_served,
name_syncs,name_sync_files,published_heal,name_announces,remote_pin_retries,range_support,
publish_items,publish_stores); an older rnfs can't open it afterwards. The publisher'spublishedand
published_earlierrows move intopublish_items(a row per provided item), leaving those tables empty. Pins
made before the upgrade have noaddedtime, name keys made before it count as never exported (rnfs status
lists them untilrnfs name exportis run), and remote pins asked before it have no status entry. - LXMF: rnfs no longer sends or receives LXMF (Removed, below). Pins of links auto-fetched over LXMF change
source fromlxmf:<address>toapp:lxmf:<address>(pin lsshows the new one), an app calledlxmf's, so
they stay pinned for an LXMF app to take over; nothing is released by the change, and an olderpins.json
imported at the upgrade gets the same rewrite.storage/lxmf_identity,lxmf/and the inbox and outbox in
rnfs.dbare kept, untouched.lxmf_*settings inrnfs.tomlare ignored with a warning. If you rely on
rnfs sendandrnfs inbox, don't upgrade before an LXMF app is there: the upgrade can't be undone. - Provider records: the upgrade gives each publisher of the records a node stores a standing
(provider_standing, and indexes onprovider_records): Earned if it was first seen at least 3 days before its
latest store and refreshed a record outside the newcomer pool a day or more after storing it, else Newcomer. The
newcomer pool's records are no longer pooled, and the remembered departed and evicted publishers are dropped. max_recordsis retired: amax_recordsleft inrnfs.toml(or given tornfs.Node) is ignored with a
warning instead of stopping the daemon;record_budget(bytes) replaces it.rnfs config set max_recordsis
refused as an unknown setting;rnfs config unset max_recordsremoves it.- Blocks: they move from
blocks/<cid>into 1,024 subdirectories,blocks/<xy>/<cid>(the CID's third- and
second-to-last characters), at the first start, which takes a while on a large store (a rename per block; the API
answers once it is done: 6 minutes for 883k blocks on a desktop SSD, and 6 for 857k on a 2-vCPU cloud VM, of which
110 s the move; the log says how far the listing and the move got about every 30 seconds, and, if they take over 10
seconds, the listing of every block that follows and the first working out of what the node provides). Each start
moves whatever is left, so an interrupted upgrade loses nothing, and a block Windows won't let rnfs move stays
readable until a later start moves it. An older rnfs doesn't find the moved blocks. A subdirectory ofblocks/that
is a link, not a directory or another user's stops the start. The state database gains ablock_sizestable, every
stored block's size, which the first start fills by listing the blocks. <rnfs dir>/daemon.json: the daemon writes it while it runs. Clients from 0.2.0 on read the API port from
it first, so one that can't read a newer daemon'srnfs.tomlstill finds it.- Settings:
rnfs.tomlmay now be rewritten by the daemon (rnfs config set,POST /api/config/set). Only the
assignments changed are replaced; comments and other lines are kept. Settings whoseapi_portequals a
non-zerogateway_portare now refused at start (such a daemon could never listen on both). - Routing state: the state database gains tables for the routing table (
routing), which is now kept across
restarts, for local scores (peer_scores) and for the announce first-seen log (announce_first_seen). - Files and folders inside pins are provided (
provide_entries, below, on by default): after the upgrade a node
that isn't slow also publishes the CID of every file and folder inside its own complete pins (not remote pins or
followed names), up toprovide_entries_max(100,000), so its first rounds publish more keys. Set
provide_entries = falseto keep 0.1.6's behaviour.
Security:
- A provider can't keep a fetch alive for ever by alternating unreachable and unestablished links (REL-1): a "no
path" result (retried once, a minute later) reset the count of lost requests, and a link that didn't establish
(lost) re-armed the retry, so a provider whose results alternated the two, as a node ignoring link requests and
answering every other path request gets, never failed, and its fetch, thread and searches lived as long as it
liked. Only an answer, or an outright failure, resets the lost count now. - A request for nothing served can't cancel a download (REL-2, specification section 3): a
/blocksor/range
request took a response slot, displacing a response in progress when the pool was full, before the node asked
whether it serves anything requested. One ~100-byte request for a made-up CID from a fresh link cancelled an honest
slow-link download, and repeating it broke every one. Such a request is now answered at once, with the same answer
as before, without a slot. - A request of the wrong shape is dropped before RNS indexes it (REL-3): RNS reads a request as
[time, path hash, data]unchecked, so one whose plaintext was an int, a short list or had a list as its path
raised on RNS's request thread, a traceback in the log for every such packet, before rnfs's per-link rate limit
saw it. The guards now drop it with a throttled verbose line; no handler could have served it. - A tar member of negative size is refused by rnfs itself (REL-4): Python's tarfile parses a negative size from
a base-256 field or a paxsize=record, and only interpreters with its own offset check (3.12.3 and later fixes)
refused such a stream; rnfs supports 3.11. Sealed folders and tar adds now refuse it on every interpreter. - A file in the rnfs dir can't be added through another name for it (REL-8, completing REV1): the check that
add
refuses the rnfs dir compared paths, and a file added as the add's root isn't walked (the walks skip the rnfs dir by
inode), so on a case-insensitive disk (macOS:~/.RNFS/api_token) or through a bind mount an app could add the
CLI's token, the node identity or a name key and read it back. The file's directories are now compared by device and
inode too, up to the rnfs dir'sfiles/. - Gateway documents are revalidated, not cached as immutable (REL-9, specification section 11): every
/rnfs/
response wasimmutablefor a year, though its Content-Security-Policy followsgateway_scripts, so a page cached
while scripts were allowed kept running them in the browser after they were turned off. HTML, SVG, other XML, PDF
and listings now carryCache-Control: public, no-cache(the browser keeps them and asks again, a304from the
node), and a304carries noCache-Control. Other files stay immutable. - Withdrawing and restoring the same records spends no write budget (REL-13, completing REV2): a withdrawal
spent a row of the provider write budget and a restore before the next save spent nothing but gave nothing back,
though neither row is then written. One identity looping over its own 27 records spent the budget other publishers'
new records need, while writing almost nothing, and every other Newcomer's new record was refused (an Earned one's
too, from an Earned attacker). The row a withdrawal spent is now given back when it is undone. - A keeper can't make this node keep and serve a name below the sequence it knows (REL-14): the keeper store's
rollback check, and the sequence a keeper query checks answers against, left out the name floor (the highest
sequence of a name resolved before, kept after its record left the table). A lying keeper's old, validly signed
record of such a name, once kept, was served onward by this node. The floor counts now. - A flood of fresh identities can't set the clock-offset estimate (REL-15, specification section 9): it was the
median of the 128 senders heard most recently, so 128 announces of fresh identities emitted 2 h ahead pushed every
honest sender out, and the node warned that its correct clock was 2 h behind. Once 5 senior servers were heard the
estimate is theirs alone, and the table forgets other senders first. - What fuzzing the gateway, the API, settings and tar streams found (REL-17): a web page could set any
Content-Type, control characters included, with the gateway's?filename=data:<type>,(the type is now taken from
the name's extension alone, and a failed response's buffered status line is dropped rather than sent before the
error's); a control character in a request's query reached a redirect'sLocation(percent-encoded now); a request
line of four words endingHTTP/0.9was answered without a status line or headers (refused as HTTP/0.9 is); a
deeply nested JSON body answered 500 (400 now); a whole number past 64 bits in a setting raisedOverflowError, and
upload_budget = 10**400passed validation, then stopped the node starting (settings hold 64-bit numbers, as
rnfs.toml does); and a tar member refused by name but declaring 2**88 bytes held a tar add's reader for good, as
tarfile skips it by reading its size and reads nothing for ever at the end (refused as an invalid tar now). Nothing
a remote node sends reached any of these. - A
/rangeprovider that disowns a block it was sending is scored as one that sent a bad block (REL-20): it could
send ranges of a block, checked only once whole, then answer that it doesn't hold it, so the airtime was spent and it
failed only for that fetch, free to do the same in the next. Its score now falls by what a block not matching its CID
costs; it isn't banned, as content unpinned and collected while it was sent does the same. - NomadNet links are bounded per link (specification sections 3 and 11), with no process-wide change: the answer
may be no larger thanmax_download_sizeand the free space above the disk reserve; each Resource segment may
decompress to one segment, and must follow the sequence an honest sender keeps (same original hash and total,
the next index, the segment count the total gives), else the transfer is cancelled and the link closed; what is
assembled may not outgrow the advertised total; a file's metadata must be a small flat map, checked before RNS
decodes it; a page must be one flat byte string, checked from its msgpack headers before RNS decodes it. The
daemon's process-wide decompression limit stays 0, andrnfs.dhtlinks keep every rule they have. - A NomadNet page answer may be at most 16 MiB (specification section 11): RNS reads an answer without metadata
(a page) whole into memory and decodes it, so a "page" as large as the free disk could exhaust a small node's
memory. One advertised larger is refused at its first segment, before any of it is transferred (the add fails
with too large); files, written to disk, keep the bound above. - Gateway sites don't render uploads as pages (specification section 11, Sites). A site's pages run script in
its own origin, so a file someone else got into a site's DAG could be made to run script there: on site hosts
?filename=now only names a download (Content-Disposition: attachment; the type comes from the path's own
name), and a file inside the site's DAG whose name gives no type is sniffed only to an image, audio, video, font,
wasm or plain text, else served asapplication/octet-stream, never as HTML, SVG or PDF. A site'sindex.html,
the files its DAG names.htmlor.svg, and a site whose root is itself a file render as before; the path
gateway is unchanged. - App names stay private (specification section 11): a request naming an app that doesn't exist in
X-Rnfs-Appgets anX-Rnfs-Prooftoo, keyed by an HMAC of a secret made at each start and the name, which no
client can check, instead of none; so every answer, the openGET /api/statusand the 403s included, looks the
same whether or not the app exists, and anyone who can reach the API port can no longer list the installed apps. - What an app's token can do, said plainly (specification section 11, README): an app can do anything the CLI
can, managing apps included in effect. The/api/appsroutes refuse its token, but it reads and writes files as
the daemon's user, which reaches the CLI's token;rnfs app addnow says so. Give a token only to software you
would trust with the CLI. - Unpacking a sealed folder counts what its entries take on disk (REV10; specification section 5,
Unpacking): the tar's headers are read first, before anything is stored, and every file and directory counts
at least one filesystem allocation unit (4 KiB or the filesystem's own, if larger) in the block store and
again as written, where the plaintext's bytes were counted before. A sender could seal a folder of many tiny
files that passed the check and then took 4 to 8 times as much, twice, filling the recipient's disk past the
reserve (1,000 one-byte files: 10.9 MB written where 2 MB was checked). Such a get is now refused (413, naming
--no-extract). - An unpack the process died in no longer leaves the plaintext stored (REV10): its decrypted blocks used to
stay in the block store, unpinned, until a gc or an eviction. An unpack now marks the state database while it
runs, and a start that finds the mark deletes every block nothing reaches before serving anything (the
unrecorded blocks of a fetch the same crash interrupted too: a retry fetches them again). - Documented: a sealed file is only as safe as the recipient's node identity (REV12), which is online while
the node runs and stored unencrypted in the rnfs dir (storage/identity): sealing protects against the
network, not against someone who can read the recipient's disk (README, Known limitations; specification
section 5). addrefuses the rnfs dir (specification section 5):rnfs add ~pinned and provided the node identity, the
name keys,api_tokenandrnfs.db, each findable by its own CID withprovide_entries, and an app token could
read the CLI's token throughadd.add(the CLI, the API, add jobs, sealed folders) now refuses the rnfs dir, a
directory holding it, anything insidestorage/,names/,state/,blocks/orlxmf/, and anything else in
the rnfs dir but outsidefiles/. Every walk (add, the shared folder,name sync, sealed folders) also skips,
with a warning, a directory that is the rnfs dir or one of those subdirectories by device and inode, which catches
a bind mount of them; one given as the root is refused.- Provider records have a write budget (REV2; specification section 6, "Provider records"): with no
per-publisher cap left, one identity storing and withdrawing records in a loop made the store write about
100,000 rows a second in-process, and 9.5 MB/s at 100 requests a second. New records and withdrawals now spend a
budget of 60 rows every 30 s open to all, plus 30 each kept for Earned and Known publishers, carried over for up
to 6 hours; past it new records are refused (False), withdrawals never are, and refreshes cost nothing. A
withdrawal followed by a store of the same key before the next save is a refresh, and a refresh is written once
it moves a record's last-stored time by 6 hours rather than 1. A Newcomer flood now makes the store write at most
about 150 kB/s (fresh identities, inserted and withdrawn, on a 1M-record database), a hundredth of what it did. - A full key can't be taken over (REV3; specification section 6): Earned publishers never gave way to each other
at a full key, so about 1000 identities that each refreshed one key on 3 UTC days (from about 48 hours) held all
its 1000 publishers, and every new provider of it but Known ones was refused. Now Earned records give way to
Earned and Known storers (a random one outside theK/2oldest), and Newcomers keep a tenth of the key: below it
a Newcomer evicts a random Earned record, past it Newcomers give way first, so a flood of fresh identities holds
at most that share. - Provider standings without records are capped (REV11; specification section 6): a publisher left without
records keeps its refresh days a week, and nothing else bounded those rows, so identities that each refreshed one
record a day later and withdrew it grew the state database (and took room from the record budget, which measures
it). At most 50,000 are kept (about 5 MB); past that the hourly pass forgets those with the fewest refresh days
first, so Earned publishers that left outlast such a flood. - Partition mode can't be forced from outside (spec section 6, "LoRa islands"; REV4): a server beyond the island
could answer a slow node's/findwith 3 made-up IDs, whose failures entered partition mode at once. Only failures
of servers the node knows (its routing table or first-seen log, or an identity or path RNS knows) count, and 3 in a
row ask for a probe of an off-island server that answered or announced itself recently, as silence does: partition
mode starts only if that probe fails (or nothing beyond the island was heard to probe). An ID that only ever failed
is never a probe target. - The announce first-seen log keeps its senior servers (spec section 6, "Announce first-seen log"; REV7): it
forgot the least recently heard server first, so about 3 announces a second of fresh identities pushed every honest
server out between its 6-hourly announces, and none ever became senior (provider_seniority), which publishing and
searching's senior rule needs. When full it now forgets first a server silent for 2 ×announce_interval(counted
from the start at the earliest), then the server not yet senior that it first heard most recently (in a flood, the
newcomer itself), and a senior server heard lately only when nothing else can go. At a tenth of the scale (6,553
rows, 200 honest servers, a fresh identity every 3.3 s for 2 days) every honest server became senior, against none. - Made-up IDs can't inflate the network size estimate (spec section 6, "Identities placed next to a key"; REV8):
a lookup took its sample, the distance to the 10th-closest server, over every ID replies named, so a server naming
made-up IDs just beyond the 3 closest (which a lookup completes without asking) inflated the estimate about 3
times, weakening the density check. A sample now counts only servers that answered or whose identity is known,
from the closest up to the first that isn't (its distance scaled to the 10th), so a node that knows few identities,
on a LoRa island, doesn't underestimate the network instead. Over 300 servers with such a server in every lookup:
288 -> 743 before, 288 -> 318 now. - RNS's identity blackhole is honoured (specification section 10, "Blackholed identities"): RNS drops a
blackholed identity's announces and paths, but rnfs kept using what it held, so the node stayed in the routing
table, was asked in lookups, and its provider records were handed out and fetched from. The node now reads the
list (rnpath -B, blackhole sources; through rnsd too) every 5 minutes and treats a listed identity's node ID as
banned while it is listed (nothing of it is saved: it ends when the identity leaves the list), and answersFalse
to every request on a link identified as a listed identity.
Removed:
- LXMF messaging left rnfs for an app of its own, built on the local API and the library (roadmap "LXMF moves
out of core"): rnfs is the core (content, the DHT, names, pinning, the local API, the library), and messaging is
a companion app's. Gone: the commandsrnfs send,inbox(inbox get,inbox rm),outbox,lxmf addressand
lxmf sync; the routesGET /api/lxmf/address,POST /api/lxmf/send,GET /api/lxmf/inbox,
POST /api/lxmf/inbox/get,/api/lxmf/inbox/rm,GET /api/lxmf/outboxandPOST /api/lxmf/sync(now 404
no_such_endpoint);lxmfamongGET /api/capabilities' features; thelxmf_address,lxmf_inboxand
untrusted_droppedfields ofGET /api/statusandrnfs status; thelxmfdependency, frompyproject.toml,
the lock and every package (the.deb, the Flatpak, the Windows exe, the install script). This is a deliberate
break of the local API for LXMF only: every other route, field and status of 0.1.6 is unchanged. The
library'slxmfmethods, their objects and theinboxevents, added during 0.2.0's development, never shipped,
nor did the deprecation path first planned for this release (warnings, aDeprecationheader). - What stays: nothing on the network changes, and
rnfs:URIs and the LXMF link map (the field an LXMF message
carries rnfs links in) stay part of the specification (section 13) as the contract an LXMF app and other clients
use;rnfs.Nodeandrnfs.Clientstill take anrnfs:URI as a target (rnfs/uris.py). Contacts of kind
lxmfstay in the address book, for an app to read which senders are trusted. On disk, nothing is deleted:
storage/lxmf_identityandlxmf/are left as they are (rnfs no longer creates or checkslxmf/), so an LXMF
app can adopt them and keep the node's LXMF address; the inbox and outbox stay inrnfs.db'slxmf_messages
table (0.1.4'slxmf_inbox.jsonandlxmf_outbox.jsonare still imported into it), which nothing reads,
changes or deletes, for the app to import. - Pins of links fetched automatically over LXMF stay pinned, as
app:lxmf:<address>(upgrade notes, above):
an app calledlxmfholds them, and nothing else releases them. An LXMF app registered under that name
(rnfs app add lxmf) takes them over; to release them without one, runrnfs app add lxmfthen
rnfs app rm lxmf. - Retired settings:
lxmf_enabled,lxmf_display_name,lxmf_propagation_node,lxmf_send_from_hint,
lxmf_sync_interval,lxmf_accept_from,lxmf_auto_fetch_fromandlxmf_auto_fetch_quota. One left in
rnfs.toml(or given tornfs.Node) is ignored with a warning, asmax_recordsis, instead of stopping the
daemon;rnfs config setrefuses one as unknown, andrnfs config unsetremoves it. - If you used
rnfs sendorrnfs inbox: use an LXMF app built on rnfs once one is released. Meanwhile an
rnfs:link can go through any LXMF client (Sideband, NomadNet, MeshChat) as text, and the recipient fetches
rnfs:<cid>/<path>?from=<node>withrnfs get <cid>/<path> --from <node>. If you rely on rnfs's own inbox
and auto-fetch, stay on 0.1.6 until then: an<rnfs dir>0.2.0 has upgraded can't go back to it.
Added, for apps:
- App tokens: an app can have a token of its own, kept across restarts (
rnfs app add, below), sent as
X-Rnfs-ApiwithX-Rnfs-App: <name>; the proof a client checks before sending it is keyed by the token's
verifier, which is all the daemon keeps of it (rnfs.db's newappstable).rnfs.Client(app=..., token=... or token_file=...)andApiConnection(app=, token=). Theapi_tokenstays the CLI's, unchanged, and a
request withoutX-Rnfs-Appis checked exactly as before. - Managing apps:
rnfs app add <name> [-o file]issues an app a token, printed once (alone on stdout, for
scripts) or written to a new file only you can read, and says on stderr that the app can do anything the CLI
can (export name keys, change settings, stop the daemon), managing apps included in effect;rnfs app ls [--json];
rnfs app rm <name>revokes the token and releases every pin the app made. RoutesGET /api/apps,
POST /api/apps/add,POST /api/apps/rm, for the CLI's token only (an app's gets 403 with the new error code
forbidden); capabilityapps. An app calledlxmfholds theapp:lxmf:pins rnfs's own LXMF left
(Removed, below). - An app's pins: what an app adds, pins, imports or makes with
dir set/dir rm(and its pin, add and import
jobs) is pinned with the sourceapp:<name>, orapp:<name>:<key>withX-Rnfs-Pin-Key: <key>
(client.pin_key(key)), shown bypin ls. Itspin rmreleases only its own source; the CLI's still
releases onlyuser's, and a pin key with the CLI's token is refused (400). Removing the app releases them all
and cancels its jobs. - Discovery:
<rnfs dir>/daemon.json, written atomically (0600) once the API listens and removed at stop: the
daemon's PID, API and gateway ports, version, start time, rnfs dir, Reticulum config directory, whether it
attached to a shared instance, and node ID.rnfs.apiclient.read_daemon_info()reads it, refusing a symlink,
a file others could have written, or one whose process has ended. - Stopping:
POST /api/shutdownstops the daemon as SIGTERM does, after answering;rnfs stop [--wait S]
uses it and waits until the API stops answering. Windows has no other clean way to stop a background daemon. - Error codes: every API error is
{"error", "code"}, the code machine-readable (rnfs.errors.CODES): the
node's errors by status (bad_request,not_found,too_large,denied,invalid_dag,busy,
storage_full,internal) and the API's own refusals (no_such_endpoint,method_not_allowed,
unauthorized,bad_host,timeout,length_required,body_too_large,too_many_connections,
unsupported). So a 404 for an endpoint a daemon lacks (no_such_endpoint) is told from one for missing
content (not_found).ApiError.code, andNoSuchEndpointandUnsupported(bothNotFounds) in the
library, which also recognises an older daemon's unknown-endpoint 404 by its message. - No API stability until 1.0.0: the library (
rnfs.Node,rnfs.Client) and the local API stay provisional
(rnfs.API_STATUS), and may change in 0.2.x releases;GET /api/capabilitiessays what a daemon offers. - An OpenAPI reference for the local API:
docs/openapi.json(OpenAPI 3.1) describes every route, its
parameters and bodies (streamed ones too), its answers, the error codes, the token headers and the event
kinds, for apps in any language; tests check it against the code (routes, codes, kinds,
capabilities, and the fields that live answers carry). It is in the sdist too. - Capabilities:
GET /api/capabilitieslists the daemon's routes, features, event kinds, error codes and
capabilities (events,pin_details,cache,config,jobs_add,add_tar, ...). - Events:
GET /api/events?after=&kinds=&epoch=&wait=, a long poll on the node's events (rnfs/events.py),
replacing an app's polling of each list:job(every state change, and a running job's progress at most once
a second),pin(added, removed, labelled),cache(added, removed),name(a newer record of a name the node holds
or watches) andpeer(a peer first heard, coming online, going offline). Events are numbered within a run;
the last 1024 are kept, and a client that falls behind, or asks of an earlier run (epoch), is toldlost
and reloads. At most 8 polls wait at once. - Pins:
pin ls --jsonandGET /api/pin/lsgive each pin'slabel,origin(where it was added from,
kept apart from the label),size,entries(a directory's entry count, as a number) andadded(when first
pinned).rnfs pin label <cid> [label]/POST /api/pin/labelgive a pin a label, which becomes its listed
name.nameis otherwise unchanged. - The cache:
rnfs cache ls [--json]/GET /api/cache/lslist the cached fetches (downloads not pinned),
most recently used first, with size, completeness and whether the same content is pinned or provided;
rnfs cache rm <cid>…/POST /api/cache/rmevict those leading to a CID now. Apps no longer need to look in
blocks/. - Settings:
GET /api/configgives every setting with its schema (type, choices, default, unit, range,
description, group, whether it applies without a restart), the running value, the value inrnfs.toml, and
whether the command line overrides it or a restart is pending;POST /api/config/setchangesrnfs.toml
(validated as a start would, nothing written otherwise;nullremoves a setting), keeping its comments and
other lines, and applies at once what can be.rnfs config [list|get|set|unset].rnfs.config.settings_schema()
needs no daemon. Settings that now apply without a restart:max_storage(enforced at once),
max_download_size,max_output_entries,max_upload_rate,remote_pin_quota,follow_quota,max_jobs
(queued jobs start if the new limit allows),max_finished_jobs,serve_interfaces,no_serve_interfaces,
quiet_hours,keep_max_names,keeper_queryandscan_interval. - Add and import as jobs:
POST /api/jobstakeskindadd(pathandPOST /api/add's options) and
import(path), with progress (total_bytesto read,done_bytesread,rate,eta), cancellation at
the next read, and the resultPOST /api/add//api/importwould give; persisted, and run again from the
start if interrupted.rnfs add --background,rnfs import --background. - Streaming, so a daemon that can't see the app's files (Flatpak) works fully:
POST /api/add/stream?format=tar
(a tar stream becomes a directory; links, devices, absolute or..paths and invalid names are skipped with
warnings; bounded bymax_output_entriesand the DAG depth limit; extended headers over 64 KiB, global headers
over 4 KiB in all, data after the tar's end, and compressed or malformed tars refused),
GET /api/get/stream?format=tar(a target as a tar, fetched first, every limit checked before the response
starts, with an exact length; files as stored; entries whose names hold\,\or/, or start as a Windows
drive does,C:, are skipped and counted),
GET /api/export/stream(an archive, byte-identical tornfs export) andPOST /api/import/stream. - Peers:
GET /api/peersandrnfs peersgainfirst_heard,rttandrtt_at(the round trip RNS measured
on the last link to the peer,pingincluded) andonline, worked out by the daemon: an rnfs link is open, or
within 15 minutes of its last sign of life — an answer, a request served, a link, or an announce emitted within
that time (a path answer, or an announce relayed late, doesn't count). Decided on the monotonic clock, so a
wall-clock step changes nothing.
rnfs peersgains RTT and ONLINE columns. - rnfs.Client and rnfs.Node: typed methods for what was only reachable through
Client.api:peers(),
stats(),contacts.*,names.keep/unkeep/keptandnames.follow/unfollow/following, with objects (Peer,
Stats,Contact,Kept,Followed);jobs.addandjobs.import_archive; and the additions above (events,
capabilities, pin labels, the cache, settings, streams). A Node raises what a Client gets back. - rnfs links in the library:
rnfs.Link,rnfs.parse_uri,rnfs.format_uri, and the LXMF link map's
rnfs.encode_fieldandrnfs.decode_field(spec section 13), for an LXMF app and other clients that carry rnfs
links, so they need no internal module (rnfs/uris.py). They need no node and load no RNS. - Starting: while a daemon opens its block store, before its API listens,
rnfs statusand the other commands
say it is starting, naming its process, rather than that no daemon is running; the library raises
DaemonStarting(.pid), aDaemonNotRunning, so code catching that is unchanged. Told by the rnfs dir's
lock, which the daemon takes first (apiclient.dir_holder). - Per-call timeouts:
with client.timeout(seconds):bounds the calling thread's requests only, long polls
fitting inside it (a Node accepts it and does nothing);ApiConnection.request/call/get/post(timeout=). - Gateway sites, an origin per root:
http://<k-cid>.rnfs.localhost:8789/<path>serves<cid>/<path>and
http://<name>.name.localhost:8789/<path>the name's target, so a page canfetch()files of its own DAG.
<k-cid>is the CID ask+ base36 (56 or 60 characters: an rnfs-dag CID's usual form is too long for a
DNS label), accepted in these hosts only (rnfs.content.cid.to_base36); ETags, listings and logs still show
the usual base32 CID. Site responses keep the page's origin (sandbox allow-same-origin …), can be framed
only by their own origin, and carryOrigin-Agent-Cluster: ?1; service worker scripts (403) and?format=
(400) are refused there, and paths are DAG paths only. Browsers resolve*.localhostthemselves; other tools
should keep using the path gateway, which is unchanged. All*.rnfs.localhostsites share cookies (as do all
*.name.localhostsites): neither the gateway nor the API uses them.rnfs url <cid>|<name>[/path]prints a
target's site URL.
Added:
- Name key backups: losing a name's key loses the name for good.
rnfs name createandrnfs name import
say so and print thernfs name exportcommand (the API's results carry it aswarning); the node remembers
which keys were exported byname exportor imported, andrnfs status(GET /api/status,
Status.unexported_names) lists the names whose key never was.name listentries andrnfs.Namegain
exported. - Clock problems are noticed: RNS drops an announce emitted (by its sender's clock) no later than the one it took
a path from, so a Raspberry Pi without a clock that came back on an older time lost its name updates silently, for
up to a week. The node now remembers each name's last announce (name_announces) and holds the name's announces
while its clock is behind it: a publish is committed as usual, its result says why the announce is held
(announce_held, printed byrnfs name publish, aRuntimeWarningfromName.publish), and the announce goes
out once the clock passes the last one, or after a week held in all.GET /api/status(Status) gains
announces_heldandclock_offset, the clock's offset from the network's estimated from the emission times of
the announces heard (the median over senders, so a few wrong clocks don't move it); a warning is logged, and
printed byrnfs status, while the clock is over 5 minutes behind. Only behind is reported: RNS delivers many
announces late (it holds those arriving on an interface that just came up), which looks like a clock ahead, so
clock_offsetreads 0 then. The estimate never changes the clock or what the node does. Name syncs publish
through the same path. - Pin by NomadNet address:
rnfs add <destination>:/page/<path>(a page as the node renders it) or
<destination>:/file/<path>(a file it serves) fetches it from that NomadNet node over a link of rnfs's own and adds
it like a file, pinneduser(with an app's token, that app's source) with the link as its origin inpin ls; the
usualaddoptions apply (--no-pin,--wrap,--name,--to,--chunker,--only-hash,--background), and it
runs as anaddjob with progress. Each run takes a new snapshot. The link is anonymous unless--identify, which
identifies with an identity of its own (storage/nomadnet_identity, made on first use), never the node's;rnfs status(GET /api/status,Status) shows its hash,nomadnet_identity_hash, for the NomadNet node's operator to
list, making it if there is none yet. A page the node answers with its "Request Not Allowed" page fails rather than
being pinned, naming that hash when the add identified. A local file named like such a link
is added as./<name>.POST /api/addtakesnomadnet(withnameandidentify) instead ofpath, as do add
jobs (POST /api/jobs); capabilityadd_nomadnet;rnfs.Node/rnfs.Clientadd_nomadnet()and
jobs.add_nomadnet(). Request data (a page's fields orvar_values) isn't supported. - Encrypted folders:
rnfs add --to <recipient>… <folder>(and--to-self,--background,POST /api/addwith
to) seals a folder as one sealed file: the folder is packed as a tar, everything under one top directory
<folder>/, and sealed under the name<folder>.tar, so its file names and structure are hidden along with its
content (not its approximate total size). The sealed-file format is unchanged: a recipient on 0.1.x gets a tar it can
unpack itself. The tar starts with a marker, a pax global header holding only the POSIXcommentrnfs sealed folder, which extractors ignore. The folder is walked asaddwalks one (symlinks, devices and invalid names skipped
with warnings; names holding\,\or/, or starting as a Windows drive does, skipped too); more than
max_output_entriesentries or a tree deeper thanMAX_DEPTHis refused, and so are a folder name over 251 bytes and
--no-sealed-namewith a folder (the name is what makes a recipient'sgetunpack it). - Unpacking sealed folders:
rnfs getof a sealed folder (a sealed file whose sealed name ends in.tarand whose
content is a valid tar starting with the marker) unpacks it, by a recipient. A tar sealed as a file (by 0.1.x, or
rnfs add --to bob backup.tarnow) has no marker and is still written as that file, byte for byte;GET /api/info
gives a recipientsealed_folder. The decrypted tar is read withPOST /api/add/stream?format=tar's rules (links,
devices, absolute or..paths skipped;max_output_entriesandMAX_DEPTH) and written withget's (temporary
name, unsafe names skipped). Without-othe output is named after the tar's top directory. The free-space check
counts the content twice (its blocks, then the files), and the decrypted blocks are deleted afterwards, never pinned
or provided. A marked.tarwhose content isn't a valid tar is written as the file, with a warning; one past a limit
is refused.rnfs get --no-extractwrites the.tarinstead;POST /api/get,POST /api/jobs(kept with the job),
Node/Client.getandjobs.gettakeno_extract, andWrittengainsextracted.POST /api/getwithout
no_extractunpacks a sealed folder; content sealed before 0.2.0 is written as it always was.GET /api/capabilities
listssealed_folders.catstill gives the decrypted tar, and the gateway and--rawthe sealed bytes.POST /api/add/stream?format=tarstill can't seal (400). - Remote pin status (local only;
/pinand the wire are unchanged): a node records the remote pins it asks
for (node, CID, pin or release, when, whether accepted) and works out whether each accepted one is done by
looking the node up among the CID's providers: paced (in the background at most 60 lookups an hour, 4 at
once, each pin hourly while waiting and daily after; on slow nodes 4 an hour, one at a time), waiting for up
to 24 hours after asking, then missing if not found. So "done" lags the node's publishing and is only as good as
the DHT. A node serving remote pins records what became of each (waiting, fetching, pinned, dropped with why:
quota, not found, denied, ...; refused because denied or its queue was full; released), which was only logged.
At most 10 000 asked entries and 1 000 served outcomes are kept.remote_pinevents report both.
rnfs pin remote ls [NODE_ID] [CID] [--check] [--json]lists the pins asked for (--checklooks them up now:
one lookup each, at most 32 a call, 4 on a slow node);rnfs pin remote ls --served [--json]lists those served
and each identity's use ofremote_pin_quota.GET /api/pin/remote?node=&cid=&check=1and
GET /api/pin/remote/served;ApiConnection.pin_remote_ls/pin_remote_served;Node/Client.remote_pins()
andserved_pins()(RemotePin,ServedPins,ServedPin,RemotePinUsage); capability
remote_pin_status. - Publishing a folder to a name (local behaviour built on
name publish; nothing on the wire changes):
rnfs name sync <alias> <folder>keeps a name pointing at a folder's current content. Everyscan_intervalthe
folder is imported as the shared folder is (unchanged files recognised by their metadata, not read again; links,
special files and files with other hard links left out; a folder in the rnfs directory refused), with FastCDC,
so an edited file shares most of its blocks with the version before. When its root changes the name is
published to it, asname publishdoes (the previous version'sname:pin released), once the folder has
stayed unchanged a minute and at most once an hour per name (RNS's transport nodes stop rebroadcasting a
destination announcing faster than that). A folder that is missing, unreadable, empty or whose import fails
pauses the sync, logged once, and nothing is published.rnfs name unsync <alias>stops it, the name keeping
its target;rnfs name rmstops it too;rnfs name syncs [--json]lists the syncs and how they stand. Syncs
are kept in the state database and resume after a restart. In private modename syncwarns, asname publish
does, and the sync logs it once.POST /api/name/sync {"alias", "path"},POST /api/name/unsync {"alias"},
GET /api/name/syncs;ApiConnection.name_sync,name_unsync,name_syncs;Node/Client.names.sync,
unsyncandsyncs()(FolderSync); capabilityname_sync. An app's token may start and stop a sync, as it
may publish; the name's pin stays the name's. - Files and folders inside pins are provided (new settings
provide_entriesandprovide_entries_max; an
entry is an ordinary provider record of its CID): every file and directory a directory entry names inside a
complete pin of the user's own content (the user's, the shared folder's, names' and apps' pins: not those other
nodes asked for or followed names'), at every depth, can be found by its own CID rather than only as
<root>/<path>, and the same file in two folders from either. Not chunks or other intermediate nodes, and never
inside cached downloads, which provide only their root; serving is unchanged (an entry's blocks are its pin's).
Unset (the default), it is on up toprovide_entries_maxentry keys (100,000): pins oldest first, each pin's
entries all or none, a pin that would pass the cap left out (logged). On a slow node
it is on only while republishing every key is estimated to take at most 2% of the channel (from the slowest
interface's bitrate, the keys, and the network size the publish rounds' regions show: about 1,300 keys at 100
servers and 40 from 1,000 at 1 kbit/s), and none are provided past that; once on, they stay on up to 2.5%.
trueorfalseoverrides the default; both settings apply at once when changed through the API. Unpinning a folder
withdraws its entries, except those still inside another pin.rnfs status(andGET /api/status,
Status) gainsprovided_entries,provide_entries_off(why none are provided) andprovide_entries_capped;
providedcounts entries too. Providing entries tells anyone looking up a file's CID that the node has it. - Network status (spec section 11, "Network status"):
rnfs status(GET /api/status, the library's
Status.networkandStatus.record_store) reports what the node sees of the network, with nothing sent to learn
it:networkgives the number of DHT servers the density check estimates (size_estimate, null until 8 lookups
completed;known_nodesis the routing table's, to compare) and how many searches and published keys the check ran
on and fired on in the last hour and day;record_storegives what counts againstrecord_budget(the state
database's size), the records the store would hold when full, whether it is full, and for each storage tier
(Known, Earned, Newcomer) its records and share against its floor and the new records refused and records evicted
in the last hour and day.rnfs statussums these up in
a few lines after the other fields (network_size,density_check,partition_forwhile in partition mode,
record_store,record_tiers,records_refused,records_evicted); every existing field and line is unchanged.
The counts are kept in memory (per minute over the last day; a restart starts them afresh), and the status reads
nothing more from the state database, so apps can call it as often as before.
The DHT and provider records (local behaviour: nothing on the wire changes):
-
The routing table is saved and loaded at start, so a restart doesn't make every server new again: servers keep
their seniority (which ranks providers and keepers), bans last their time, and/findreplies still leave out
servers silent for over 2 ×announce_interval, the time the node was down included. -
Local scores: each node keeps a private score per peer, earned by work it checks itself (blocks served that
verified,/findanswers that led to providers who served or to closer servers that answered) and lost by
failures, bad blocks and silence (scores halve each week). Bootstrap nodes start with a small seed they keep
only by earning it. Nothing is sent; at most 4096 peers are scored. -
A join (at start, and again while too few servers are live) asks up to 6 servers of the saved routing table
first, best scored first, and bootstrap nodes only if fewer than two of those answer: a node that has joined
once is its own bootstrap list, so a restart doesn't flood path requests for bootstrap nodes on radio. After
such a join, a node that isn't slow still asks the default bootstrap nodes (until one answers, at most 2), so
one that saved only an island of servers reaches the rest of the network; a slow node no longer does. -
Choosing providers: within each seniority group (seniority stays first), providers with a better local score
are tried first, before fewer hops: a provider whose blocks verified before is preferred to an unknown one. -
The announce first-seen log: each node records when it first heard each server's announce (at most 65,536
servers, about 12 MB when full: servers silent for 2 ×announce_intervalforgotten first, then the newest not
yet senior, Security above), kept across restarts, and can give
the closest servers to a key among those heard for at leastprovider_seniority, which identities generated
next to a key can't be. Publishing and searching use it (below). -
Sweep providing: republishing many keys takes far fewer lookups. One closest-nodes lookup now serves every
due key of its region (the keys sharing as many leading bits as the key looked up shares with its 10th-closest
server), with about 1.6 more/finda region to fill in its far half; each key still gets its own 10 closest
servers (100% of keys on their true 10 closest over a simulated 2000-server network, against about 97% with a
lookup per key). A round needs about servers ÷ 12.5 lookups however many keys are due: 1,000 keys on a
1,000-server network take about 80 lookups instead of 1,000. Slow nodes' 10 lookups a round now count regions,
not keys, so a round publishes every due key of up to 10 regions. -
Sweep providing on LoRa islands and at a first start: a node that knows few identities (on an island, or just
started) found a key's 10 closest among them, padded with servers far from the key, so its regions held many keys
its lookup knew little about. Regions are now never shorter than the network size estimate implies, a key a
lookup until there is an estimate, and a region covers only the keys it can store on all but 2 of their 10 closest
servers it heard of (it heard of others whose identity it doesn't know: the others get lookups of their own). Over
test_topology's world of backbone and islands (1,000 / 3,000 servers), an island node's records reach 7.0 / 4.7 of
their true 10 closest instead of 3.8 / 2.3 (a lookup per key: 7.8 / 5.0), and a node's first round after its first
start 6.5 / 5.0 instead of 0.3 / 0.1, its second 7.8 / 6.9 instead of 1.9 / 0.7. It costs island nodes keys per
round (1.5 / 1.2 keys a region lookup instead of 6.6 / 4.9, so a slow island node's round of 10 lookups publishes
fewer keys); a backbone node, which knows every identity it hears of, runs as many lookups as before
(tests/bench_publisher.py, 2,000 servers: 159 for 100,000 keys and 176 for 1M, against 158 and 176 without the
size estimate). -
Stores to long-known servers too: each key is also stored on the 10 servers closest to it among those the
node first heard at leastprovider_seniority(1 day) ago and has heard in the last 2 ×announce_interval.
Without an attack they are mostly the key's 10 closest already, so this adds few stores; identities generated
next to a key can't push the record off them. Withdrawals reach them too. -
Content is far harder to hide by placing identities next to its key (spec section 6, "Identities placed next
to a key"). Ten identities generated closer to a key than every honest server (about 4 s of one core each among
10,000 servers) hid it almost completely from 0.1.x nodes (found 0–4% of the time); with an upgraded publisher
and searcher it is found 99–100% of the time over the fake network oftests/test_eclipse.py(98.9% on fast
nodes and 90.8% on slow ones across every attack of the eclipse simulation). Four local mechanisms, nothing on
the wire changed:- a network size estimate: the median distance to the 10th-closest server over the node's last 16 complete
lookups for other keys, counting only servers the lookup verified (Security above), kept in the state
database'smetatable so a restart has it at once (the density check waits for 8); - the density check: when the servers found near a key are packed more than twice as densely as the estimate
allows (from the 10th on), a search waits until 5 servers of each of two rings beyond them have answered (those
ranked after the dense core, and those beyond where 10 honest servers would be expected), and a publisher gives
the key a widened lookup of its own and stores it on 10 servers of each ring too; - disjoint lookup paths: a publisher's region lookup runs as 3 lookups that share no node (2 on slow nodes) and
stores each key on each path's 10 closest too; a search that finds no provider at all falls back to them, in
what is left of the samelookup_timeoutand of its 80/find(a search still sends at most 80 in all); - searches also wait for the 3 closest of the 10 servers nearest the key that the announce first-seen log heard
at leastprovider_seniorityago (none extra without an attack: they are the closest anyway).
The cost: a little more traffic per publish (over the fake network of 2,000 servers, +17
/findand +3 stores
to publish one key, once per republish; with sweep providing about 0.65/findand 13 stores a key instead of
0.22 and 10), +0.2 to +0.5/findper search on average (the density check fires on 2–6% of searches without an
attack), and a search for content nobody provides now costs about 4 times as much (30/findinstead of 7: the
fallback). A slow node paces that fallback like its publish lookups (it counts as 2 of a round's lookups and runs
only if the pacing allows within the lookup's time), and counts widened lookups among its round's 10 (a flagged
key that doesn't fit waits for the next round). - a network size estimate: the median distance to the 10th-closest server over the node's last 16 complete
-
Fast nodes spare LoRa islands most lookup traffic (spec section 6, "Slow paths"; nothing on the wire changes):
a node that isn't slow asks servers behind a slow path (an RNS path of more than 3 hops, or its own last link to
the server within 24 hours had a round trip above 1 s and a rate estimate belowslow_bitrate) after every other
server but unreliable ones, in searches and publish lookups, and stores to them only to make up 10. They are still
asked when a lookup has too few others; the routing table,/findreplies, provider ranking and failure counts are
unchanged. In the topology simulation this cut the airtime internet nodes' lookups cost islands by 97% (27 s to
0.6 s each; a 30-server island needed 135–141% of its channel for light internet use before), and internet nodes
still found island content 98% of the time. Over the fake network oftests/test_topology.py(1,000 servers,
21% on islands): island/findper internet search 0.76 to 0, island stores per internet publish 2.5 to 0.07. A
server whose path was unknown is judged again once the query's path request is answered, before a link is opened.
rnfs peersis unchanged; the traffic counters now keep each peer's last link's rate estimate too (in memory). -
Slow nodes place records better: a slow node's lookups may make more path requests than its
alpha(1): up to
10 per publishing lookup (its 2 disjoint paths 5 each, and a widened lookup 10) and 3 per search (and 3 more over
the fallback's paths). A slow node hears only some servers' announces, so a publish lookup stopped at the closest
servers it knew: in the topology simulation an island publisher's records reached 1.3–2.7 of their 10 closest
servers, and internet nodes found island content 68–91% of the time; with the larger budget 3.7–4.4 and 99%, for
11–19% more airtime per publish. Over the fake network oftests/test_topology.py(1,000 servers): 3.6 to 6.9 of
the 10 closest. Nodes that aren't slow are unchanged (alphaper lookup), as is the keeper query's own budget. -
LoRa islands keep their own content findable (spec section 6, "LoRa islands"; nothing on the wire changes): a
node with a slow interface, or naminglocal_interfaces, keeps a set of local servers, those whose RNS path goes
out on a named interface, or (none named) withinlocal_hops(3) hops over an interface belowslow_bitrate,
judged from the path to each server as its announces arrive and from one read of the path table every 10 minutes
(throughrnsd, one RPC: a client's own path table starts empty). It also stores each key it provides on its
local_replicas(3, at most 5) closest local servers (two-level records: ordinary/provides, withdrawn like
the rest; a local server no link could be opened to, though a path was found, is tried once more at once, since over
LoRa one lost link request left the island without its replica until the next republish), and a search asks the closest local server first (3 of them with a hop radius), its answer seeding the
rest of the lookup. In the topology simulation an island neighbour found content during a partition 100% of the
time instead of 55–72%, and island-to-island lookups took 42% less airtime, for 12% more publish airtime. Fast
nodes with no slow interface and nolocal_interfacesare unchanged. New settingslocal_interfaces,
local_hops,local_replicas;rnfs statusgainslocal_servers. -
Partition mode (spec section 6, "LoRa islands"): a node with local servers that fails 3 times in a row to reach
off-island servers it knows, or hears no off-island announce or answer for over 4 times the usual gap between
off-island announces (at least 10 minutes), and then gets no answer from one it probes, uses its local servers
only for lookups and publishing, so a search during a gateway outage ends in the time its island takes instead of
minutes of link timeouts. Any off-island answer or fresh announce ends it, and a probe of one off-island server
every 5 minutes (on slow nodes after 15 minutes, doubling up to 2 hours) keeps it from sticking. Silence alone never
enters it, so a small connected network with sparse announces doesn't flap.rnfs statusgainspartitionand
partition_since. -
The heal republish: keys whose last publish round reached no server beyond the island (during a partition) are
marked, and republished as soon as partition mode ends, paced as a slow node's rounds are, so the rest of the
network finds them within hours instead of after their next republish (3 days on a slow node). The marks are kept
across restarts (a new table,published_heal).Together, over the fake network of
tests/test_topology.py(1,000 servers, 21% on 8 islands, an island's gateway
down): a neighbour found what an island node published before the partition 100% of the time instead of 67%, and
what it published during it 100% instead of 91%, with 0.2 timeouts and 33 s of simulated time per search instead
of 11.7 and 490 s; the backbone found what was published during the partition 0% of the time until the heal
republish, 99.5% after it. Over a simulated week with 5 or 20 servers beyond the island announcing every 6 hours,
partition mode was entered only while the gateway was down, and left within 49 minutes of its coming back. -
/provideto 16 servers at once: a node that isn't slow sends a publish round's/provides to up to 16
servers at once, each still over its own identified link with its batches in order (withdrawals first, stopping
at a refusal), so a round over many servers takes a fraction of the time. Slow nodes still send one at a time. -
Slow nodes pace withdrawals: unpinning many keys no longer sends every withdrawal at once over radio. A slow
node sends withdrawals along with its round's stores to the same servers, and each server only withdrawals
reach counts as one of the round's 10 lookups (up to 5 kept for them while any are queued), so the round waits
30 s for it before the next. Other nodes still send withdrawals at once. -
Kept names' keys take their fixed ±10% republish jitter from the key's first byte rather than the whole key: 256
slots still spread them out, and keys sharing their leading bits (the same servers are closest to them) come due
together. A key's interval may change once, by at most a fifth, at the upgrade; it stays below 3.5 days. -
On disk: records are read from the state database per
/findthrough an index, with a small cache, instead
of all held in memory (about 500 bytes each before): memory no longer grows with the records or their publishers.
Changes are written together every 30 s; a republish is written only when it moves a record's last-stored time by
more than 6 hours, and a refused store writes nothing. A flood of 300 000 fresh identities into a full store of
200 000 records cost about 70 µs a store and 112 transactions in all, on a desktop NVMe disk. -
A byte budget instead of a record count: records may take
record_budgetof disk (new, default 1 GiB: about
7 million records at the 150 bytes a record takes, measured from the database), never the free disk the blocks'
reserve needs.max_records(100 000 records) goes. -
No per-publisher caps while there is room: anyone stores freely until the budget is full, so a large
publisher (a mirror) is no longer held to 1% of the store, nor a new one to a tenth of that. The newcomer pool and
the memories of departed and evicted publishers go too. -
Storage tiers once the budget is full: Known (contacts of kind
node, private-mode members, bootstrap
nodes; 30% floor), Earned (publishers that refreshed a record on 3 distinct UTC days; 60%) and Newcomer (everyone
else; 10%). A store to a full node takes a record from a tier's largest holder: the lowest tier below the storer's
above its floor first, then, if the storer's own tier is below its floor, a higher tier above its own; else
within its own tier, refused for one holding as many as the largest, and for Earned publishers, which never evict
each other. While full, a Newcomer's record expires 4.5 days after it was last stored (otherwise every record
still livesPROVIDER_TTL, 7 days). A key still keeps at most 1000 publishers, now giving way by tier, with a
share kept for Newcomers (Security, above). -
Resumable and compressed block transfers (spec section 7,
/range): a new optional request,/range
([cid, offset, max_bytes, flags], answered[size, encoding, bytes]), the one addition to the wire in 0.2.0;
/blocksis unchanged. Over slow links a node fetches each raw chunk of 4 KiB or more in ranges of about
max(min_rate_sample, response_targetat the link's rate estimate) bytes on the wire (16 KiB at 1 kbit/s, about 2
minutes, instead of a whole 64–128 KiB chunk), compressed with bz2, and keeps what arrived in a partial block
(<rnfs dir>/state/partial, at most 64, deleted at start), verified whole against the CID before it is stored and
never served; a provider whose response stalled gets ranges on any link. The first request of a fetch, before any
search, uses ranges by the same rule when it goes to the hint, a remembered provider or a node known to answer
/range(an open link otherwise gets/blocks, as an older node's silence would hold the search up): on a radio it
is most often what brings a single-chunk file (over LoRa between two Heltec V3 RNodes at 3.5 kbit/s, a 24 KB text came whole in
88 s, where bz2 makes it 9.8 KB). Measured with the real fetcher over a fake
1 kbit/s link: a stall anywhere in a 64 KiB chunk now resends 8 KiB on average (1.1 minutes) instead of 32 KiB
(4.4 minutes), at most 15.6 KB instead of 64.7 KB; a Gutenberg book (Pride and Prejudice, 772 KB) takes 2.8–2.9×
less airtime (1.74 hours to 0.63, counting about 0.7 KB of RNS overhead per response). A requester decompresses an
answer itself, to at most the range it asked for plus one byte, and refuses anything else (a decompression bomb
ends there;tests/security/test_range_decompression.py,test_range_limits.py). A responder answers/range
exactly as/blocks: the same response slots, pools, cooldowns, upload pacing, airtime controls and
upload_budget, and compresses at most 256 KiB a second.
Nodes before 0.2.0 leave/rangeunanswered (RNS drops a request for a path without a handler, which costs them
nothing and counts against nothing): a node asks such a provider with/blocksfor the rest of the fetch after one
silence, which may be a lost packet, and records it as not answering after two silences in a row (in one fetch or
several; any answer clears a silence), in the newrange_supporttable, asking afresh after 7 days: an older node
costs two request timeouts a week.
Changed:
- A slow node no longer publishes what it downloads, by default (spec section 6, "Provide strategy"):
provide_strategygainsauto, the new default, which ispinnedon a slow node androots(as before) on
others. Over LoRa each download's root went to about 10 servers, an identified link each: at 977 bit/s the hour
after three 16 KB downloads the laptop transmitted 6.6% of the time, nearly all of it publishing their roots, and it
told every node which files its user fetched. A slow node set torootskeeps doing so. After the upgrade a slow node
with the setting unset withdraws its cached downloads' records (paced, as any withdrawal). provide_entriescovers the user's own content only (a changed default): the files and folders inside pins of
the user, the shared folder, names and apps are provided by their own CIDs, but not those inside pins other nodes
asked for (remote:) or followed names (follow:), so a node doesn't publish the file CIDs of content others
asked it to keep; a pin the user holds too counts as the user's. Its cap of 100,000 entry keys is a new setting,
provide_entries_max(applied at once, likeprovide_entries), as a large mirror passes it: a pin whose entries
would pass the cap provides none of them.- A pacing wait no longer closes the link: after a response on a slow link the fetcher waits as long again before
asking that provider again (the responder's upload pacing), and on LoRa a 64 KiB chunk takes about 9 minutes, so
the wait outlastedlink_idle_timeout(5 minutes) and the idle link was closed, taking its rate estimate with it:
the next response was sized and paced as on a new link. The link now counts as in use while its provider is
waited for. - Slow links are judged per provider, not per node: a gateway with a TCP and a LoRa interface isn't a slow node,
so it ran 3 parallel/blocks, probed open links for the root and duplicated any batch out for 30 s, all over
the radio. Now a provider is reached over a slow link when the link's rate estimate is belowslow_bitrate(or,
without one, the next-hop interface is, or the provider is behind a slow path); providers over slow links
behind one next-hop interface share one request at a time, a node that isn't slow probes no open link over a
slow link (a slow node still probes one, as before: a hit saves a lookup over the radio), nothing is duplicated
onto a slow link, and on fast links a batch is duplicated only once it has taken 1.5 times its blocks' transfer
time at the rate estimate (anddeadline_slack, as before). - A lost packet no longer drops a provider: a
/blocksrequest RNS times out (its packet or its answer's lost,
which RNS doesn't resend), whose link times out under it, or whose new link isn't established, failed its
provider for the rest of the fetch at once, so at 5% packet loss most 1 MiB fetches from one provider failed.
Now its link is closed (which starts no cooldown at the provider) and the request asked again on a new link
after the busy delay; the provider fails aftermax_failuressuch requests in a row. A link closed by either
end, no path, or a malformed answer still fails it at once. Measured over the fake link of
tests/test_fetcher.py'sLossyLinkTest(one provider, each request lost with probability 5%), 1 MiB fetches
succeeded 39% of the time before and 99.7% after (300 fetches each), or 97.6% when each new link's request and
proof are lost at 5% too. - Idle links close before RNS keeps them alive. The node that opens a link sends a keepalive after
max(5 s, RTT × 206)without a packet (at most 360 s) and the other node answers, and keepalives don't count as
traffic, so a link left open forlink_idle_timeout(5 minutes) on a 50 ms path carried about 58 packets, relayed
by every transport node on the way, against about 6 for the lookup that opened it. A link without a rate estimate
now closes just before its first keepalive (1 s and two round trips before the interval), if that is sooner:
reopening it costs about 4 packets. A link that carried/blocksand holds a rate estimate keeps
link_idle_timeout, since closing it resets the provider's estimate and RNS's transfer window. On radio paths
(RTT ≥ 1.75 s) nothing changes. Simulated over 8 hours of browsing through the gateway (lookups of 8 servers of
300, fetches from 1 of 40 providers, half with a rate estimate; mean of 5 runs) on 50 ms paths, links are opened
6,431 times rather than 4,490, 1,971 of them within 5 minutes of closing rather than 1,123, while keepalive
packets fall from 328,237 to 19,826, and link opening, closing and keepalive packets together from 346,199 to
45,548. - A node asked first for a fetch's first block (one with an open link, or a remote pin's asker) that had it was
asked for that block again in its next request, so it sent it twice: on LoRa, a whole chunk's airtime more. Now
it is asked for the blocks after it. - A DHT client that only fetches (it provides nothing, owns no names, accepts no remote pins and isn't in private
mode) announcesrnfs.dhtonly at startup, no longer everyannounce_interval; nothing needs to reach it. It
announces again at once when it starts to provide. Servers, and clients that provide, announce as before. - Blocks are stored in subdirectories (
blocks/<xy>/<cid>): one directory of millions of files is slow to list,
and FAT32, common on the USB drives added to a Raspberry Pi, holds only about 10,900 such names per directory
(a 59–64 character name takes 6 of its 65,536 entries). The cost is at the durability points: storing blocks
syncs each subdirectory written to (up to 1,024, 16 at a time) where it synced one directory. Adding 64 MiB
(1,029 blocks) to a new store on a desktop NVMe disk: 1.02 → 1.41 s; a tree of 1,024 8 KiB files 0.95 → 1.43 s;
fetching them (each block synced on its own) unchanged at about 13.5 s (tests/bench_block_writes.py,
today, median of 3). max_storagecounts what blocks take on disk: each block is a file of its own, so its size rounded up to the
filesystem's block or cluster (read once at the start: at least 4 KiB, 16–32 KiB on a FAT32 USB drive), not its
length. On FAT32 a store of small files took several timesmax_storage: 2,000 notes of 200 B–8 KB are 4.1 MiB
of blocks but 63 MiB of 32 KiB clusters (9.4 MiB at 4 KiB). Stores of large files barely change (64 KiB chunks
fill whole clusters: a mixed store of 297 MiB takes 298 MiB at 4 KiB, 312 MiB at 32 KiB), so cache is evicted a
little sooner.rnfs status(GET /api/status) gainsallocated_bytes, whatmax_storagecounts, and
allocation_unit;used_bytesstays the blocks' bytes. Quotas (follow_quota,remote_pin_quota) and
max_download_sizestill count bytes of content and blocks. Nothing is stored for it: it follows from the block
sizes rnfs keeps.rnfs.apiclient(the CLI,rnfs.Client) refuses a daemon older than 0.1.5 before sending it the token, rather
than sending the request on a second connection as it did for them until now (the fallback planned to go in
0.2.0): stop such a daemon and start the new one. Every daemon of 0.1.5 or later works as before.POST /api/import(andrnfs import) refuse anything but a regular file, without waiting on a FIFO.Config.validaterefusesapi_portequal to a non-zerogateway_port.- A request that times out raises
ConnectionFailedsaying the daemon "didn't answer within its timeout". Config.loadreports invalid TOML as " isn't valid TOML: …" (still aValueError).- A fetch to pin that fails keeps what it got:
pin add,dir setanddir rm, remote pins, followed names and
deny lists record it as a cached fetch, asgetandcatalready did, so trying again resumes rather than
starting over (a 4 MiB pin over LoRa failing at 90% fetched all of it again). Not when it failed on denied
content, an invalid DAG or a size bound or quota (TooLarge), which a retry can't get past. The record is cache
like any other (evictable, removed bygc), and never provided as a download of the user's. - Remote pins don't give up on content not yet published. The node asked to pin asks the node that asked first
(its node ID follows from the identity of the/pin's link), which serves what it added before its provider
records are out. A pin whose content can't be found waits and is tried again after 10 minutes, doubling up to 6
hours, for up to 3 days, instead of being dropped at once;pin remote ls --servedshows it waiting
(not_found) with when it is tried again (retry_at, a new field ofGET /api/pin/remote/served). Pins
accepted after it aren't held up, and a release ends the wait at once. - A
get,pinorexportjob submitted with the newretry: true(POST /api/jobs;retry=Truein the
library'sjobs.get,pinandexport) whose content can't be found is queued again the same way, keeping its
blocks, rather than failed:retry_at(andretry_sincewhile it waits) are new fields of a job, andrnfs jobs
shows it waiting. Not for a path its directory shows doesn't exist. Retrying is opt-in: the API's default is
0.1.6's, failing at once, so an app polling its job still sees it fail rather than stay queued for days.rnfs get,pin addandexportwith--backgroundopt in; in the foreground they still fail at once. - A running job says whether it is still looking for providers: on a radio a search takes minutes before any
byte arrives, and a job showed only that it was running. A job now hasproviders(how many its fetch uses; null
before it has any, and for add and import jobs) andsearching(true while its fetch searches the DHT), in
GET /api/jobs, itsjobevents (a search beginning or ending is published at once) and the library'sJob;
rnfs getandrnfs jobsshow "looking for providers" until one is found, andrnfs jobs showthe providers. - Path requests are paced node-wide: since RNS 1.2.5 every interface limits the path requests arriving on it (3 a
second while it is under 2 hours old, as a hub's interface for a newly connected client is), and a fresh node's
join and first publish round sent dozens at once, so its hub dropped some and the queries failed after
path_timeout, enough for partition mode on a gateway. Every path request rnfs sends (link opens, a lookup's path
check, name resolves, NomadNet adds) now waits its turn, at least 1 s after the last, and while the busiest
interface already sends more than 1.5 a second (other applications' requests included, at most 15 s); the wait
isn't counted inpath_timeout. A link that fails to establish costs a turn, as RNS then asks again by itself;
concurrent waits for one destination share one request, and none is sent again within 20 s of the last. In the
topology simulation a fresh fast node's hub no longer drops any of its requests and its first publish round takes
41–45 s instead of 7 s (22–25 s with drops); island rounds take 60–80 s more of 25–31 minutes. - A query that failed for want of a path no longer counts towards partition mode (it still counts in the routing
table): island relays in RNS's defaultfullmode never forward a request for a path they lack, and a hub drops
requests over its limit, so a node with local servers could enter partition mode on a connected network (in the
topology simulation 65–72% of island publish rounds withfullrelays). A real partition is still detected, a
few seconds later (the first failure for each server is a link failure). - Path requests only where the node can be: with its own RNS instance, a node asks for the path to a node a
/find
reply named over a fast interface (at leastslow_bitrate) only on that interface (a hub's or an AutoInterface's
peers count as one interface), so a gateway's lookups no longer send requests for internet nodes over its radio.
A node named over a slow interface, joins, names, providers and links to known nodes ask on every interface, and
nothing changes through a shared instance. - Shorter path waits for lookup candidates on nodes that aren't slow: a node measures how long its path requests take
to be answered, and once 20 are measured a lookup's path check waits their 95th percentile, at least RNS's own 15 s,
instead ofpath_timeout(30 s): a candidate whose path doesn't come costs half as long, as the lookup has others
to ask. Names, nodes the user names, stores, fetches, joins and slow nodes still waitpath_timeout. - A server closing a link (RNS's
DESTINATION_CLOSED: no room for another incoming link, or going away) is a
refusal, not a failure: a lookup's/findon such a link no longer counts against the server in the routing table,
its score or partition mode (a partition probe it ends counts as reaching the outside), and a/blocksprovider
that closes the link is busy, asked again on a new link after the busy delay and failed aftermax_failuresin a
row, instead of failing at once. A link that timed out, or that this node closed, counts as before. - A slow node paces by its radio's measured load: RNodes report their own airtime and the channel's load (over 15 s
and an hour, through rnsd too), which a slow node reads at most every 15 s while it has paced work. While the
channel's load is above 25% its path requests and publishing rounds (the heal republish's too) are spaced further
apart in proportion (twice as far at 50%, at most 4 times), never so far that a record would go unrepublished for
half its 7-day lifetime; while its own airtime over the last hour is above 50% it refuses/blocks(a new airtime
refusal,blocks_refused_airtimeinrnfs status,airtimeinGET /api/stats'srefused); and while its own
transmissions took over 2% of the channel in the last hour,provide_entries' default doesn't turn entries on. A
figure of 0.0 counts as unknown (RNodes report it before their first measurement); other nodes and interfaces
without the figures keep the estimates they had.
Faster:
- A long fetch looks for more providers. A fetch searched once (again only when every provider had failed), so a
slow provider was kept to the end: between cloud nodes a 30 MB fetch over a 4-hop route ran 29 minutes at 18 KB/s,
while a node 2 hops away, which had fetched the file in 26 s, provided it from then on. A node that isn't slow now
searches again while it has fewer providers thanmax_parallel_providers, a minute after the last search ended and
then twice as long each time, up to 10 minutes; what it finds joins the fetch, ranked as any provider. Slow nodes
don't (a search is a minute of airtime there), nor do fetches from a given node (--from). - Fetching something again doesn't look it up again. Nothing was kept between fetches, so once its links had closed
a gateway reload or apin addaftercatrepeated the lookup (about 13.5/findon a fast node). The nodes
that served a root are now remembered for 3 hours after they last did (in memory, up to 10 per root and 4,096
roots) and asked for its first block before the nodes with open links, after a remote pin's asker, under the same
slow-link rule; one that doesn't have it any more, or fails, is forgotten. Only nodes that served verified blocks
are remembered, never the providers a lookup reports. And a search that found nobody providing a root isn't
repeated for 10 minutes by the gateway or by the retries of followed names and deny lists, which then fail at
once;get,cat,pin add, the rest of the CLI and API, jobs and remote pins always search. - Range reads (
cat, the gateway, sealed reads) size their window by the providers' links: they fetched 32 chunks
(2 MiB) before handing over the first byte, about 5 hours at LoRa's 1 kbit/s. While no provider known is reached
over a fast link (or none is known yet), the window is one chunk with the next asked for ahead, so the first
byte comes after one chunk (about 9 minutes at 1 kbit/s); over fast links it stays 32. - A start no longer lists every stored block before the API answers: their sizes are kept in the state database
(block_sizes) and read from there after a clean stop. After a crash or power loss the start lists them all,
as before, and corrects the table. Opening storage on a desktop NVMe disk (warm cache), small blocks: 100 000
of them 0.50 → 0.13 s after a clean stop (0.65 s after an unclean one), a million 5.4 → 1.2 s (7.2 s after an
unclean one: 1,024 directories listed and the table compared). The first start, moving them out of 0.1.6's flat
layout, took 4.2 s and 74 s. A cold cache and a slow disk should gain more, as the table is one file read in
order (tests/bench_startup.py,--coldfor a cache dropped as after a reboot). - A start works out what the node provides faster, before the API answers. Its first refresh walked every pin's
DAG for the blocks the node serves, then read the folders of each of the user's own pins again for the files and
folders it provides (provide_entries); they now come from the first walk. That walk no longer remembers every
chunk it checked, only the DAG nodes, and DAG nodes decode without being encoded again to check they were written
in their one canonical form: the decoder takes each header only in its shortest form instead, which accepts
exactly the same blocks (a file node of 6 chunks 16.2 → 9.2 µs, of 256 chunks 588 → 346 µs, for every read).
Measured withtests/bench_refresh.py(565,000 blocks in 28 pins, one a Gutenberg-like folder of 70,000 files;
desktop NVMe disk, warm cache): the first refresh 6.26 → 3.81 s (5.30 s with the decoder alone, 4.33 s with the
one walk too), its peak memory 178 → 160 MB. A 2-vCPU cloud VM with 857,325 blocks spent about 79 s of an 88 s
start there: 54 s in the walk, 23 s reading the folders again. - The publisher's state is on disk: where and when each provided key was last published lives in the state
database (publish_items, indexed by when each is next due) and is read per round, rather than held in memory
for every key. A wake asks the index when the next key is due, and a round reads the due keys only, at most
50,000 of them (the rest in the rounds that follow at once, in slices of the key space, so the rounds need no
more region lookups than one would), so neither looks at every provided key; while a round runs only the stores
servers answered are saved, in a small table written in order, and each key's row is written once at its end.
Measured withtests/bench_publisher.py(a 2,000-server fake network, desktop NVMe disk), before → after:
memory per provided key 632 B → 213 B after a round and 1,326 → 187 B after a restart at 100,000 keys, 582 →
161 B and 1,287 → 158 B at 1M (what is left is the provide set itself: a 4 GB Raspberry Pi held about 1.5–2M
keys; what bounds a node now is how many it can republish a day); a wake (next_due, a round
with nothing due) 80 → 0.1 ms at 100,000 keys and about 1,100 → 0.1 ms at 1M; disk 278 → 290 B a key (the due
index); bytes written per round 55–83 KB → 0.8–0.95 KB a key (SQLite's WAL and checkpoints: rows are written in
key order, and an item's row once a round instead of at each save). A first round of 1M keys took 20 rounds of
50,000 and 175 region lookups, against one round of 157 (the same with the network size estimate given, as a
node's regions are sized: 176 and 159). - An answer that comes back before RNS has registered its request is no longer lost. RNS registers a request only
after sending it and drops an answer it can't match, so on a fast link with a busy process (about 1 request in
800 with 16 at once over local TCP and the CPU busy) the request waited out RNS's whole timeout, 11 s at least,
and failed. One such/findheld back a whole publishing round:tests/interop.pysometimes took 10 to 25 s
longer to find a node providing what it had just added. The answer is now kept and handed over once the request
is registered, onrnfs.dhtlinks and on the link of a NomadNet add, where a page small enough for one packet
(NomadNet's "Request Not Allowed" among them) could be lost the same way and the add fail.
Fixed:
rnfs.Clientno longer loads RNS for anrnfs:target with a path: checking the path's names imported the DAG
module, which loads RNS's bundled msgpack and so all of RNS. The name rule now has a module of its own
(rnfs/entry_names.py), which the DAG,rnfs:links and NomadNet links share.cache rmcounts only the cached fetch's own blocks as its own: blocks nothing used before (an add just
unpinned) are collected with it, and were all counted as the first CID's: on a cloud node a 435 B cached file
"removed 670 blocks, freed 42973080 bytes". They are counted apart, asunreferenced_blocksand
unreferenced_bytes(new in the answer ofPOST /api/cache/rmandUncached), andrnfs cache rmsays so.- A collection after a start no longer reads every pin's DAG again: a sealed folder's unpack ends with one, and on
a cloud node with 857k blocks the first unpack after a start held its result for 3 minutes, its files written at
once. The DAGs the start's first refresh found complete are taken whole without being read. - A file-hash deny entry stops a one-block file everywhere (spec section 8): a file of one block has its content's
hash as its CID's digest, and the specification says either entry stops it, but asha256:entry was only checked
where content is assembled. A cloud node with such an entry refused the file to its owncatand gateway, yet
served it toget --fromfrom another node and kept providing it. The raw CID a hash names is now denied with it. - The daemon's log reaches the journal as it is written. Under systemd (and launchd, or the Windows logon task)
stdout is a pipe or a file, which Python fills in 8 KiB blocks, sojournalctl --user -u rnfs -fshowed nothing for
hours on a quiet node, and lines turned up in bursts stamped hours earlier, all of them at a stop. Seen upgrading a
cloud node with 857k blocks: its first start moved them for minutes with nothing in the journal. The daemon now
writes stdout a line at a time. - A link the other end lost is closed after one unanswered request (spec section 3, Links). When the
rnsdof a
node, or of a transport node on the way, restarted, the links through it were gone at the far end but still up at
this one until RNS's keepalive noticed, 12 minutes on a radio path; every request on them waited out its timeout.
Over LoRa, after the gateway'srnsdrestarted, a slow node's every/findto the gateway took 21–23 s and failed,
so a search during the partition that followed found nothing in 3.5 minutes. A link that received nothing at all
while a request on it went unanswered is now closed, unless another request is using it or the request was a
/rangeto a node not known to answer it (an older node's silence, on a working link), and the next request opens a
new one. - No links accepted until the node can answer on them (spec section 3, Links): the
rnfs.dhtdestination
accepted links from its creation, at the start of the daemon, but requests only once the node was set up; RNS drops
a request without a handler, so through a start (80 s on a cloud node with 857k blocks working out what it
provides, minutes at the first start after this upgrade) every request other nodes sent waited out its timeout.
Over LoRa a slow node's join asked a gateway restarting meanwhile and waited 20 s for nothing. Links are refused
(RNS drops the link request, which fails in seconds) until the request server is in place. A stop now starts by
refusing new links and answering every requestFalse: a request arriving while the state was being closed failed
in its handler, logged as an error ("the state database ... is closed", seen restarting a cloud node). - What a node publishes after starting cut off is healed too (spec section 6, "The heal republish"): a node
that starts while its island is cut off never enters partition mode (its failures are all for want of a path, which
don't count) and its rounds find local servers only, so nothing marked their keys for the heal republish. Over LoRa
the gateway PC, restarted during an outage, stored a new key on its one neighbour, and once the backbone was back
nothing republished it beyond the island until its next republish, days later on a slow node. A round that reached
no off-island server now marks its keys while the routing table holds off-island servers but none has answered or
announced itself since the start; an island that knows of none, or a node that was heard from beyond it, marks
nothing more than before. - A provider out of reach for a minute isn't dropped from the fetch: one no path was found to failed at once and
was never asked again during the fetch. Over LoRa, a fetch from a file's one provider, whosernsdhad just
restarted, failed after its search reported the provider again, back by then. A provider with no path is now asked
again a minute later, and fails only if it is unreachable then too. - A fetch uses the provider records its own node holds (spec section 6, Finding providers): a search started
with nothing, though a DHT server holds records for the keys near it, and on an island a node holds the local
replicas of its neighbours' content. Over LoRa the laptop held the island record of a file and waited 51 s for its
search to find the provider it named. The providers this node's own/findanswer would list are now candidates
as the search starts; the search still runs, so a record held here can't hide other providers. - A job's progress counts a block as its ranges arrive:
rnfs jobsand the job's progress events counted a
block only once it was whole. Over LoRa, where a 64 KB chunk comes in/rangeresponses over two minutes, a job
resumed after a restart showed0 B/s ETA ?for 90 s while 56 KB of its next chunk had arrived. The bytes each
range brings now count towardsdone_bytesand the rate, once each. Agetjob also showed "finding the target"
until its first chunk was whole (it reads the file's start to tell whether it is sealed): its size, known from the
root, is now its total at once. - A node attached to a shared instance whose
rnsdrestarted logged a warning with a traceback each time it read
the radios' load meanwhile (the RPC refused). That is now a verbose line; other failures to read the stats stay
warnings. The node itself carries on through the restart, as before (tested on the laptop: a fetch over LoRa
12 s afterrnsdcame back). - An older node isn't taken for one that answers
/rangefor good (REL-5): a/rangewhose link the other end
closed before answering (a busy 0.1.6 node evicts the link first, as it doesn't count the unanswered/rangeas a
request) recorded the node as answering/range, a record never checked again, so every later fetch from it over a
slow link waited out its silences as lost requests and failed it. Only an answer, a refusal, a response started or
a malformed answer show a handler now. - A partial block the disk fails no longer loops (REL-6): a
/rangewhose bytes couldn't be written (a full or
read-only disk) left the partial empty but counted as progress, so the fetch asked for the same first range for
ever, a whole range of airtime each time over LoRa. That, or a partial that can't be read back, now has the block
asked whole with/blocksfor the rest of the fetch. - Damaged local state no longer stops the node (REL-18, found by fuzzing the state files, the state database and
rnfs.toml): each of these failed every start (or, in the provider store, every expiry pass and save), and is now
treated as damage: logged, and dropped, set aside or clamped.- A legacy JSON state file or a saved row nested about 10,000 levels deep raised RecursionError: the file is moved
to.damagedand the row deleted, as when they aren't JSON (anames.jsonstill stops startup, with a plain
error). Anrnfs.tomlnested a few hundred levels gave every CLI command a traceback: it is refused as invalid
TOML. - Infinite provider times in
providers.jsonor a 0.1.5 or 0.1.6 database failed the upgrade's provider standing
step (NOT NULL), leaving the schema as it was: those publishers now start as Newcomers, and their times are
clamped to now as any future time is. Finite times upgrade as before. - An LXMF inbox or outbox file whose
next_idwas below -2^63 failed its import: anext_idbelow 1 counts as
missing. - A provider record whose key isn't a 16-byte blob crashed the start, expiry and every later save of the records:
such rows are never read. A provider standing whose records, days or publisher isn't an integer (or an ID) in
range, or whose records summed past 2^63, stopped the start: it counts as no standing. - A cut-short publish round's saved item list holding an item that didn't parse stopped the start: the list counts
as malformed whole. - A file or a dangling link at
<state dir>/partialstopped the start: partial blocks are kept in memory, with a
warning. An empty partial block's first range was appended to a file left at its path: the file starts afresh.
- A legacy JSON state file or a saved row nested about 10,000 levels deep raised RecursionError: the file is moved
- A crash during a get job's write-out leaves nothing behind (REL-10):
node.getwrote under a random hidden name
beside the output (.rnfs-get-<random>.partial, up to the download's size, there for a sealed file's whole fetch),
which nothing removed after a crash. A get job's write-out is now named after the job
(.rnfs-job-<id>.write.partial), and the run that resumes it removes it, as it does the job's partial output. - A clock stepped forward doesn't end a retry (REL-11): a job or remote pin first not found while a Raspberry Pi
without a real-time clock still had the time it booted with gave up at its next try once NTP had stepped the
clock days on, as if it had been retried for 3 days. A try more than 6 hours after it was due (that, or the node
off) no longer counts the time between. - While a daemon was starting (minutes on a small node as RNS's transport starts), the CLI said "no API token …: is
the daemon running?", as the token is written once the API is up: it now says the daemon is starting, as it does
once the token is there and the port isn't yet. - A fast link's provider no longer refuses for a minute after a large response (REL-12): the default upload cap
set when the next response may start from the link's rate estimate, which small/rangeresponses had left below
slow_bitrate(RNS's measurement of a small transfer is mostly its ramp-up). After a 327 KB response that took 8 s
between two cloud nodes, Madrid refused the next for 57 s, and the requester, pacing by the 8 s, failed it after
three refusals, and the fetch failed (found by a soak of random fetches between the cloud nodes). Once a
response under the default cap concludes, the next may start twice the time it took after it started. - A name sync stops when its name's key is deleted by hand (REL-16): the key's pins were released at the next
start but its sync stayed, paused, and a new, unrelated name later made or imported under the alias got the old
folder published to it at the folder's next change. Such a sync is now dropped at the start, with a warning; one
whose key file is there but can't be read is kept, as its pins are. - A provider whose responses a route cuts is asked in ranges (REL-19): between cloud nodes, one route cut every
64 KiB/blocksresponse from Montreal at the same point (52 of 64 KiB); each counted as the provider being busy,
and after three the fetch failed it, though ranges, half a chunk each, would have come through. A response that
stopped arriving partway now has the next request of that provider made in ranges, as after a stall. announce_interval,republish_intervalandscan_intervalhad to be only above 0, so a typo such as
announce_interval = 1e-300would have announced in a loop, flooding a radio channel. They are now at least 60 s,
10 minutes and 1 s:config setrefuses less, and a smaller value already in rnfs.toml is raised to the minimum
with a warning, so an upgraded node still starts.- An
rnfs.tomlstarting with a UTF-8 byte-order mark, as some Windows editors save it, was refused as invalid TOML:
it is read, andconfig setwrites it back without the mark. - The API answered
HEAD(always refused, 405) with a JSON body after the headers, which a client reads as the start
of the next response: now the headers only. - The API's integer parameters, a request's
Content-Lengthandquiet_hourstook whatever Python'sint()takes:
1_0,+7, spaces, other scripts' digits. Only ASCII digits are taken now, and aContent-Lengthof 2**63 or more,
which overflowed the body reader, is refused (400, as a negative one is; one that isn't a number stays 411). - Path requests in a burst kept their spacing only as reserved: a thread that slept past its turn, as a loaded machine
does (80 ms on a CI runner), went with the next one's, two requests at once. The spacing now holds between the
requests that actually go. - A fetch running for days kept every search it made again (one each 10 minutes that found its providers again):
it now keeps its first and the latest 8. - A stalled response no longer fails the other requests on its link (REL-7): closing the link for it failed a
request waiting for its slot (another fetch from the same provider, which then failed the provider) and a/find
in progress on it. The link is closed only if nothing else uses it, and a request that wasn't sent because its link
closed meanwhile counts as lost, asked again on a new link. - A search isn't held for minutes by servers that don't answer (specification section 6, Finding providers):
RNS gives a server it has a long path to the whole of its link establishment timeout, and on a LoRa island two
servers, one 34 hops away by RNS's path, held two of a search's three query places 127 and 217 s each; the node
that knew the provider was asked only after them, 6 minutes in, and a gateway request in a later round ran out its
600 s. A search's query unanswered after 15 s (45 s on a slow node) now gives up its place, ranked with the
unreliable until it answers, and its answer still counts. A search that has found nobody still waits for such
queries, up to 3 times as long after each was sent, as one may hold the only record. Finding the closest nodes,
for publishing, is unchanged. - A get just after the add finds the content (specification section 6, Finding providers): content added
elsewhere is found only once its publisher's records are out, which between cloud nodes took over 20 s when the
publish lookup waited on a server that didn't answer, so agetof a CID shared as it was added failed with "no
provider found" (several fetches of the soak, asked 20 s after the add). A fetch the user asked for that found no
provider at all now searches once more, a twentieth oflookup_timeout(30 s) later, before it fails; the
gateway's and the node's own retries don't. - A provider named by an answer that came after its lookup ended is used (spec section 6, Finding providers): a
lookup completes once the closest nodes by rank have answered, with queries still out, and their answers' providers
were dropped. Over LoRa the PC searched for a file the laptop had just added: the laptop, one hop away on the radio
but ranked last as a slow path, answered with itself as provider 29 ms after the first lookup completed, and the
fetch failed a second later. Those providers are now reported once the search's runs are over. - One lost request no longer fails a provider at once: a request left unanswered on a link that heard nothing
meanwhile closes the link (0.2.0, above), and the request then counted as a failure rather than as lost, so the
provider was dropped from the fetch. Over LoRa a lone lost/rangelooks just so, and a fetch from the file's one
provider failed. It now counts as lost: asked again on a new link, failed onlymax_failurestimes in a row.