Documentation (14 pages)
Status: this is the published design and the configuration documented for self-hosters. The migration of the reference relay off IP-keyed limits is a relay-side change tracked in the crypto register (row A09-30 / D-20); the website documents the target configuration and the challenge spec, and the relay binary follow-up is explicitly pending, not silently omitted.
What runs on the reference relay today: its config has no
rate_limit_modekey, so the proof-of-work path below is not armed — the relay still applies its documented IP-keyed join limiter (rate_limit = 100room joins/min per IP plus 5 concurrent connections; see Self-hosting). Everything below is the target configuration, not the live behaviour.
Rate Limits (Proof-of-Work)
A privacy-preserving service cannot rate-limit by address. Keying abuse control on the client IP means every connection carries an identifier that can be logged, correlated and subpoenaed — the exact thing Tallow is built to avoid. So the reference control is a proof-of-work challenge (Hashcash-lite): the client spends a small, bounded amount of CPU before the relay will accept the connection, and the relay never needs to know who the client is.
Why not IP limits
- IP addresses are personal data in most jurisdictions and are the first field a log pipeline leaks.
- NAT and mobile networks collapse many honest users onto one address, so IP limits punish the wrong people (a shared office is throttled, a botnet with a /64 is not).
- A limit that cannot be enforced without identifying users is a design contradiction for a zero-knowledge relay.
Challenge specification (Hashcash-lite)
Algorithm. SHA-256 over a length-prefixed preamble:
pre_challenge = H( room_id || relay_epoch || client_nonce )challenge = sha256( "tallow-pow-v1:" || pre_challenge || difficulty_bits )accept iff leading_zero_bits( challenge ) >= difficulty_bitsFields.
| Field | Size | Notes |
|---|---|---|
room_id |
32 B | the one-way room identifier; never the plaintext code |
relay_epoch |
8 B | rotates every 300 s — bounds the replay window |
client_nonce |
16 B | client-chosen, random; the search variable |
difficulty_bits |
1 B | required leading zero bits, relay-advertised |
counter |
8 B | monotonic search counter, included in the hash input |
Difficulty. The relay advertises difficulty_bits per epoch. Baseline for
the reference deployment is 20 bits (≈10⁶ hashes, well under a second in
WASM on a mid-tier phone; ≈1 s on a single-threaded attacker with no
parallelism advantage). The relay raises the advertised value — up to 28 bits —
when it sees connection pressure, and lowers it back as pressure decays so the
honest client cost stays bounded.
Verification. The relay recomputes the hash for the submitted
(client_nonce, counter) and checks the leading-zero bound. Solutions are
accepted only for the current epoch: relay_epoch is part of the preamble, so
a solved challenge is valid for ≤300 s and is bound to one room_id. Each
solution is single-use (a bloom filter over accepted (room_id, client_nonce)
pairs covers an epoch, ≈2 MB at reference load).
Failure modes.
| Condition | Behaviour |
|---|---|
| No solution supplied | 421 rate-limit: proof-of-work required + advertised difficulty |
| Solution below difficulty | Rejected, difficulty re-advertised (no penalty, no drop) |
Stale relay_epoch |
Rejected; the client re-solves against the current epoch |
| Replayed solution | Rejected (single-use filter); connection continues to be challenged |
| Difficulty spikes past the client budget | Client backs off with jittered retry; the relay never blocks a room for more than one epoch |
Why this is privacy-safe. The challenge binds to the room and the epoch, which are already ephemeral and are destroyed with the room. Nothing in the challenge identifies the client beyond the socket it is already using, no address is stored, and solving a challenge leaves no correlatable artefact across epochs.
Reference relay configuration
The hardened configuration the reference relay targets — proof-of-work first, IP keying removed as the primary control:
# Rate limiting — abuse control without identityrate_limit_mode = "pow" # "pow" (default) | "legacy_ip" (compat only)pow_difficulty_bits = 20 # baseline; relay raises to <= 28 under loadpow_epoch_secs = 300 # solution validity window (and replay bound)pow_single_use = true # reject replays within an epoch
# Legacy IP keying — kept ONLY as an explicit, documented exception.# (A09-30: the published config no longer relies on this; leave it unset to# run the privacy-preserving path.)# max_connections_per_ip = 10Related
- Security model — the threat model this control serves.
- Self-hosting — the full relay configuration walkthrough.
- Transparency — the standing counters, published quarterly.