Bring the changelog current and correct four documentation defects

The Unreleased block is rebuilt from a walk of all 117 commits since
v0.4.1 rather than from what the block already held, which is how six
gaps surfaced. Two of them were whole missing effects. The identity
write path discarded six results, so a node configured for a persistent
identity could fall through to an ephemeral one in silence and change
its npub, routing address and mesh address on every start. And the
OpenWrt zig download verification was described only in part.

Chronological fix sequences are collapsed to their net state, and fixes
for bugs introduced and closed inside this cycle are folded away rather
than described, since no user ever saw them. Sixty-one CI and harness
commits are summarized rather than left out, because they change what a
contributor running the local pipeline sees. The flat lists are
reorganized into subsections by area. Everything from 0.4.1 down is
untouched.

Two entries carry effects no commit message mentioned. Clearing every
copy of private key material added Drop to four public types, so their
fields can no longer be moved out, which is source-breaking for anyone
using the crate as a library and is reachable through node.identity on
the public config. And the responder-side rekey narrowing covers five
call sites, not the four the original entry claimed; the ack initiator
arm is the one that deliberately still abandons the whole rekey.

The four test-harness fixes landed since then get entries under the CI
and test-harness heading, written as what a contributor sees: a failing
harness that names the condition instead of exiting bare, dns-resolver
scenarios that stop burning the full boot timeout on a container that
booted correctly, and a chaos harness that checks its teardown and node
stops actually happened rather than assuming it.

Four documentation defects are fixed alongside. Two sent macOS readers
to paths that do not exist there: the configuration reference stated the
highest-priority system config path as /etc/fips/fips.yaml
unconditionally, where the macOS package installs under
/usr/local/etc/fips/, and the persistent-identity tutorial had the same
problem throughout with nothing saying its paths were Linux ones. It now
opens with the substitution table, notes that the daemon derives the key
directory from whichever config it loaded, and points at the migration
recipe for a host already carrying keys at the old path.

The other two described test coverage that does not exist. The testing
readme claimed twenty chaos scenarios where ten exist, and the chaos
readme documented three that are in neither runner nor tree. Both
scenario tables are rewritten from the files, and bloom-storm is
described honestly as retired from both runners with no replacement,
which is a coverage gap rather than a migration to other tests.

The readme's Rust badge asserted 1.85+ while rust-toolchain.toml pins
something else, so the badge no longer carries a version and the
toolchain file is the only place that states one.
This commit is contained in:
Johnathan Corgan
2026-08-22 09:34:38 +01:00
parent 7e9ad6e213
commit 6305287491
6 changed files with 746 additions and 475 deletions
+609 -425
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -2,7 +2,7 @@
![banner](docs/logos/fips_banner.png)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![Rust](https://img.shields.io/badge/rust-1.85%2B-orange.svg)](https://www.rust-lang.org/)
[![Rust](https://img.shields.io/badge/rust-orange.svg)](https://www.rust-lang.org/)
[![Status](https://img.shields.io/badge/status-v0.4.2--dev-green.svg)](#status--roadmap)
A self-organizing encrypted mesh network built on Nostr identities,
+4 -2
View File
@@ -20,8 +20,10 @@ locations, lowest to highest priority:
All found files are loaded and merged in priority order. Values from higher
priority files override those from lower priority files. This allows a system
administrator to set site-wide defaults in `/etc/fips/fips.yaml` while
individual deployments override specific values in `./fips.yaml`.
administrator to set site-wide defaults in the priority 1 path above,
`/usr/local/etc/fips/fips.yaml` on macOS and `/etc/fips/fips.yaml` on other
Unix systems, while individual deployments override specific values in
`./fips.yaml`.
### CLI Option
+45 -9
View File
@@ -28,6 +28,11 @@ The whole exercise should take about ten minutes.
your stable nsec / npub
```
The diagram shows the Linux layout. On macOS the same three files
live under `/usr/local/etc/fips/`; read
[Where these files live](#where-these-files-live) before running any
command below.
After this tutorial your node will have:
- A keypair on disk that the daemon reuses across restarts.
@@ -36,6 +41,28 @@ After this tutorial your node will have:
- A clear understanding of which file holds the secret and how
to keep it that way.
## Where these files live
Every path in this tutorial is written in its Linux form. The macOS
package (`.pkg`) installs config and keys under
`/usr/local/etc/fips/` instead of `/etc/fips/`, so on macOS
substitute as you go:
| Linux / other Unix | macOS |
| --- | --- |
| `/etc/fips/fips.yaml` | `/usr/local/etc/fips/fips.yaml` |
| `/etc/fips/fips.key` | `/usr/local/etc/fips/fips.key` |
| `/etc/fips/fips.pub` | `/usr/local/etc/fips/fips.pub` |
`fipsctl keygen` writes to `/usr/local/etc/fips/` by default on
macOS. The daemon still probes `/etc/fips/fips.yaml` as a fallback,
so an existing install is not broken by an upgrade, but the macOS
packaging only installs files under `/usr/local/etc/fips/`. If a
macOS host already carries key files at the old `/etc/fips/` path,
the daemon uses the old key and warns rather than minting a new
identity; the migration recipe is in the
[how-to guide](../how-to/persistent-identity.md).
## Why a stable identity matters
In FIPS your Nostr keypair *is* your node's identity in the most
@@ -68,7 +95,8 @@ The daemon supports two ways of holding that keypair:
> identity unless you explicitly ask for one.
> - *Persistent*: the daemon reads (or, on first start,
> generates and writes) a keypair stored at
> `/etc/fips/fips.key`. The npub stays the same across
> `/etc/fips/fips.key` (`/usr/local/etc/fips/fips.key` on
> macOS). The npub stays the same across
> restarts, reboots, and reinstalls as long as that file is
> preserved. You take on the cost of protecting an on-disk
> secret in exchange for being addressable by a stable name.
@@ -117,9 +145,9 @@ Make a note of it. We expect this to change.
## Step 2: Enable persistent identity in the config
Open `/etc/fips/fips.yaml` and find the `node:` block. The
shipped default has the relevant fragment commented out; make it
look like this:
Open `/etc/fips/fips.yaml` (`/usr/local/etc/fips/fips.yaml` on
macOS) and find the `node:` block. The shipped default has the
relevant fragment commented out; make it look like this:
```yaml
node:
@@ -137,6 +165,9 @@ The daemon's behavior on the next restart:
`/etc/fips/fips.{key,pub}` with the correct file modes, and
use that.
The daemon derives the key directory from whichever config file it
loaded, so on macOS both files land in `/usr/local/etc/fips/`.
## Step 3: Restart the daemon
```sh
@@ -164,6 +195,9 @@ The daemon wrote two files:
sudo ls -l /etc/fips/fips.key /etc/fips/fips.pub
```
On macOS, list `/usr/local/etc/fips/fips.key` and
`/usr/local/etc/fips/fips.pub` instead.
Expect:
```text
@@ -239,7 +273,7 @@ old one will be stale.
persistent identity.
- **Where it lives.** `/etc/fips/fips.key` and
`/etc/fips/fips.pub`, mode `0600` and `0644`, owned
`root:root`.
`root:root`; under `/usr/local/etc/fips/` on macOS.
- **What to share.** `fips.pub` is public; `fips.key` is not.
- **What it buys you.** A npub other operators can add to their
`peers:` list once, and that addresses the services your node
@@ -250,11 +284,13 @@ old one will be stale.
If the post-restart npub does not match `fips.pub`:
- **Check file permissions.**
`sudo ls -l /etc/fips/fips.key`. If the mode is not `0600` or
the owner is not `root:root`, the daemon may have refused to
read it. Restore with
`sudo ls -l /etc/fips/fips.key`, or
`sudo ls -l /usr/local/etc/fips/fips.key` on macOS. If the mode
is not `0600` or the owner is not `root:root`, the daemon may
have refused to read it. Restore with
`sudo chmod 0600 /etc/fips/fips.key && sudo chown root:root
/etc/fips/fips.key`.
/etc/fips/fips.key`, substituting the macOS path where it
applies.
- **Check the journal.** `sudo journalctl -u fips -n 100` after
the restart will show one of:
- `Loaded persistent identity from key file path=...` — good.
+2 -2
View File
@@ -45,8 +45,8 @@ and a local STUN responder.
Automated network testing with configurable node counts, topology
algorithms (random geometric, Erdos-Renyi, chain, explicit), and fault
injection (netem mutation, link flaps, traffic generation, node
churn). 20 scenarios covering general stress testing, cost-based parent
selection, mixed link technologies (fiber/Bluetooth/WiFi),
churn). 10 scenarios covering general stress and node churn, discovery
over sparse topologies, spanning-tree and bloom-propagation regression,
transport-specific validation (UDP, TCP, Ethernet), and ECN/congestion
testing. Scenarios are
defined in YAML and executed via a Python harness that manages the full
+85 -36
View File
@@ -3,10 +3,11 @@
Automated network testing for FIPS. Generates random or explicit
topologies, spins up Docker containers, and applies configurable
stressors (network impairment, link flaps, traffic generation, node
churn) over a timed simulation run. Scenarios cover general stress
testing, cost-based parent selection, mixed link technologies
(fiber/Bluetooth/WiFi), and transport-specific validation (UDP, TCP,
Ethernet). Logs are collected and analyzed automatically.
churn) over a timed simulation run. Scenarios cover general stress and
node churn, discovery over sparse topologies, spanning-tree and
bloom-propagation regression, transport-specific validation (UDP, TCP,
Ethernet), and ECN/congestion testing. Logs are collected and analyzed
automatically.
## Prerequisites
@@ -23,24 +24,56 @@ Ethernet). Logs are collected and analyzed automatically.
## Available Scenarios
### General stress tests
### General stress and churn
Random topologies with increasing stressor intensity.
Random topologies with increasing stressor intensity. All three enable
netem mutation, link flaps, iperf traffic, node churn and bandwidth
tiers, and differ in transport mix, density, and whether the peer set
itself churns. Each takes a `--nodes N` override, so the node counts
below are defaults rather than fixed sizes.
| Scenario | Nodes | Topology | Duration | Netem | Link Flaps | Traffic | Node Churn | Bandwidth |
| -------- | ----- | ---------------- | -------- | ----- | ---------- | ------- | ---------- | --------- |
| chaos-10 | 10 | random_geometric | 120s | yes | yes | yes | -- | -- |
| churn-10 | 10 | random_geometric | 600s | yes | yes | yes | yes | -- |
| churn-20 | 20 | erdos_renyi | 600s | yes | yes | yes | yes | yes |
| Scenario | Nodes | Topology | Duration | Peer churn |
| ---------------- | ----- | ---------------- | -------- | ---------- |
| churn-mixed | 20 | erdos_renyi | 600s | -- |
| maelstrom | 20 | erdos_renyi | 600s | yes |
| maelstrom-sparse | 50 | random_geometric | 600s | yes |
- **chaos-10**: Network degradation (5-50ms delay, 0-2% loss), link flaps (max 2
down, 10-30s), and iperf traffic (max 3 concurrent). Netem mutates 30% of
links every 15-30s between normal and degraded policies.
- **churn-10**: Extended run with node churn (1 node down at a time, 30-90s).
Tests tree re-convergence after node departure/rejoin.
- **churn-20**: Aggressive scale test. Erdos-Renyi topology, up to 5 nodes down
simultaneously, bandwidth tiers (1/10/100/1000 Mbps), `protect_connectivity`
disabled (partitions allowed).
- **churn-mixed**: Mixed transports on one mesh (60% UDP, 20% Ethernet, 20%
TCP). Netem mutates 30% of links every 20-45s between normal and degraded
policies; link flaps (max 3 down, 10-30s, connectivity protected); node
churn (max 5 down, 30-90s, partitions allowed); bandwidth tiers
(1/10/100/1000 Mbps). Carries baseline assertions, so a run in which the
mesh never formed cannot report success. Local CI runs it as
`churn-mixed --nodes 10 --duration 120`, which is the invocation its
thresholds are calibrated for.
- **maelstrom**: The same stressors plus peer-level topology mutation
(connect/disconnect every 8-12s) and ephemeral identities on half the
nodes, with `coord_ttl_secs: 10` so coordinate cache entries expire during
the run. Tests re-convergence when the peer set and the identities behind
it both move.
- **maelstrom-sparse**: 50-node sparse random geometric graph (radius 0.20,
roughly 3-4 peers per node), which forces multi-hop routing and heavy
discovery use. The short coordinate TTL expires transit-warmed entries, so
nodes must rediscover rather than coast on the cache.
### Spanning-tree and bloom propagation
Explicit topology with an induced parent flap. **No runner invokes this
scenario.** It was retired from both the local and the cloud runner and is
hand-run only; the files remain in the tree and the retirement is recorded as a
coverage gap rather than as a migration to other tests.
| Scenario | Nodes | Topology | Duration | What it tests |
| ----------- | ----- | -------- | -------- | ------------------------------- |
| bloom-storm | 6 | explicit | 180s | Bloom rate under sustained flap |
- **bloom-storm**: Six-node depth-4 mesh. The two candidate uplinks at depth 2
swap netem delay (5ms against 100ms) every 4s with parent-flap dampening
disabled, so the node switches parents each round. Asserts a ceiling on the
`stats.bloom.sent` delta per node over the trailing 30s, and a floor of 10
parent switches so a harness that never produced a real switch cannot pass
trivially. `scenarios/bloom-storm.README.md` carries the bug-class
description and the threshold derivation.
### Cost-based parent selection — retired, now sans-IO unit tests
@@ -65,7 +98,7 @@ Explicit topologies exercising non-UDP transports.
| Scenario | Nodes | Transport | Shape | Duration | Netem | Link Flaps | What it tests |
| ------------- | ----- | -------------- | ----- | -------- | ----- | ---------- | ------------------------------------------ |
| ethernet-only | 4 | Ethernet | Ring | 90s | yes | -- | AF_PACKET transport with beacon discovery |
| ethernet-only | 4 | Ethernet | Ring | 30s | yes | -- | AF_PACKET transport with beacon discovery |
| ethernet-mesh | 6 | UDP + Ethernet | Mesh | 120s | yes | yes | Mixed UDP/Ethernet, netem mutation + flaps |
| tcp-mesh | 6 | UDP + TCP | Mesh | 120s | yes | yes | Mixed UDP/TCP, netem mutation + flaps |
@@ -137,27 +170,32 @@ scenario runs.
| `--duration secs` | Override the scenario's duration |
| `--list` | List available scenarios |
The scenario argument accepts either a name (`churn-10`) or a file
path (`scenarios/churn-10.yaml`).
The scenario argument accepts either a name (`churn-mixed`) or a file
path (`scenarios/churn-mixed.yaml`). `--list` prints the names that
resolve.
## Scenario YAML Format
Annotated example based on `churn-10.yaml`:
Annotated example based on `churn-mixed.yaml`:
```yaml
scenario:
name: "churn-10"
name: "churn-mixed"
seed: 42 # deterministic RNG seed
duration_secs: 600 # total simulation time
topology:
num_nodes: 10
algorithm: random_geometric # or erdos_renyi, chain
num_nodes: 20
algorithm: erdos_renyi # or random_geometric, chain, explicit
params:
radius: 0.5 # algorithm-specific parameter
p: 0.3 # algorithm-specific parameter
ensure_connected: true # retry until graph is connected
subnet: "172.20.0.0/24"
subnet: "172.20.0.0/16"
ip_start: 10 # first node gets .10
transport_mix: # fraction of edges per transport
udp: 0.6
ethernet: 0.2
tcp: 0.2
netem:
enabled: true
@@ -180,33 +218,44 @@ netem:
link_flaps:
enabled: true
interval_secs: { min: 30, max: 60 }
max_down_links: 2
max_down_links: 3
down_duration_secs: { min: 10, max: 30 }
protect_connectivity: true # never partition the graph
traffic:
enabled: true
max_concurrent: 3
interval_secs: { min: 10, max: 30 }
duration_secs: { min: 5, max: 15 }
max_concurrent: 10
interval_secs: { min: 0, max: 30 }
duration_secs: { min: 5, max: 90 }
parallel_streams: 4
node_churn:
enabled: true
interval_secs: { min: 60, max: 180 }
max_down_nodes: 1
interval_secs: { min: 60, max: 90 }
max_down_nodes: 5
down_duration_secs: { min: 30, max: 90 }
protect_connectivity: true # never kill the last path
protect_connectivity: false # partitions allowed
bandwidth:
enabled: false # per-link HTB rate limiting
enabled: true # per-link HTB rate limiting
tiers_mbps: [1, 10, 100, 1000] # each link randomly assigned a tier
assertions: # evaluated after the run
baseline:
min_nodes_reporting: 10
max_roots: 6
min_nodes_parented: 4
min_sessions: 10
logging:
rust_log: "debug"
output_dir: "./sim-results"
```
The assertion thresholds in the shipped file are calibrated against
recorded runs at the invocation CI uses, and the file's own comments say
what they were derived from. Read those before retuning them.
## Topology Algorithms
| Algorithm | Parameters | Description |