Skip to content

v0.2.136

Choose a tag to compare

@sanity sanity released this 21 Sep 18:46
· 184 commits to main since this release
7fa2c66

freenet-core v0.2.136 — release notes

PRE-CUT GATES — all resolved 2026-09-21

  • Merge commit for #5709: 7b2972b42 — "fix(contract): persist container
    parameters only after the contract is verified (#5709)", merged to main
    11:18 CDT via the merge queue. This is also the release base.
  • Soak SHA: a7c941326d3a0f7c788e19b7def0afdc529f9097 — the exact content
    the ~21 h live gateway watch ran.
  • Identity gate: git diff a7c941326d3a 7b2972b42 is EMPTY. Confirmed
    independently twice (11:2x and 12:28 CDT). The bytes that soaked are
    byte-identical to the bytes being tagged. Nothing else landed on main.
  • Connection gate re-scored 10:33 CDT against its 19:45/09-20
    pre-registration — all three tests PASS
    (gw1 166.5 vs thresholds 100 and
    112.7; gw2 142.5 vs 115.6). Scored at T+21.3 h. See the routing section.
  • Review lenses: the adversarial regression lens never completed. This is
    not a clean gate and is not recorded as one — see "Review coverage for
    #5709 — stated plainly". It ships as Ian's explicit decision.
  • Soak scaffolding removed from both gateways 12:29 CDT and both confirmed
    running /usr/local/bin/freenet via /proc/<pid>/exe.
  • Tag v0.2.136 -> 7fa2c6605b9933f329c7b3a8d20bca5ae2e5f753, cut 13:17 CDT.
    The tag sits one commit above the merge commit: 7fa2c6605 is the automated
    version bump (#5713), and that bump touches only Cargo.lock and two
    Cargo.toml files — version strings and fdev download URLs, zero source
    changes
    (verified by diff, not assumed). So the code carried by this tag is
    still byte-identical to the soak binary a7c941326d3a.

Everything else below is final.


Headline

The hierarchical routing estimator replaces the legacy Renegade/isotonic/EWMA stack
as the model that picks peers, and it is on by default. This release also
carries a contract-parameter integrity fix, three changes to the update/rollback
path, and one breaking removal of delegate host functions.


Payload

33 commits, v0.2.135..main, spanning 09-10 to 09-18 — plus #5709. This is not
the routing work alone.

Routing (the headline change)

  • #5708 enable the hierarchical routing estimator by default
  • #5702 contract-level term in the hierarchical estimator
  • #5701 log each routing decision's candidate set to the dataset
  • #5655 hierarchical empirical-Bayes routing estimator (shadow)
  • #5653 record NotFound and per-attempt failures as routing failures
  • #5662 keep the isotonic fit exact instead of patching it
  • #5642 evidence-weighted residual correction replacing the fixed 50:50 blend
  • #5648 opt-in local recording of route events and peer attributes
  • #5682 / #5695 an earlier default-flip and its revert, superseded by #5708

Contract integrity

  • #5709 persist container parameters only after the contract is verified

Update / rollback path — three commits, read the Rollback section

  • #5679 supervised, self-updating Nix node package, and fix the no-VCS build
  • #5649 three defects in the post-update probation announcement
  • #5240 announce the post-update probation commit on stderr (#5232)

Operations and correctness

  • #5696 allow fullscreen in the contract sandbox iframe
  • #5678 report streaming relay failures upstream instead of going silent
  • #5660 retries try a different first hop after NotFound
  • #5656 don't reap a gateway link the unjoined peer is still using
  • #5583 retry the existing-process port check past a dying corpse's socket
  • #5651 document that --max-hosting-storage is how an operator contributes disk
  • #5646 export the resident-overhead eviction axis, and stop calling contract state "RAM used"
  • #5641 bound evidence size and make the evidence decoder strict
  • #5578 sniff legacy evidence and pin wire framing layout
  • #5677 scope the sim packet-delivery callback to its own network
  • #5668 watch the real git paths so freenet stops recompiling
  • #5670 release test scans tracked files, not the working directory

Breaking

  • #5638 remove the synchronous delegate contract-write host functions

Dependencies — curve25519-dalek 4.1.3→5.0.0, argon2 0.5.3→0.6.0, dirs 6.0.0→7.0.0,
wasmparser 0.257.1→0.258.0, rstest 0.26.1→0.27.0, actions/checkout 6→7, and an npm
minor/patch group.


BREAKING: delegate contract-write host functions removed (#5638)

b62cd07a0 removed the host implementations of three delegate host functions.
freenet-stdlib 0.10.0 on crates.io still exposes them (delegate_host.rs:783,
809, 890):

  • put_contract_state
  • update_contract_state
  • subscribe_contract

Symptom. A delegate built today against the published SDK that calls any of the
three fails to instantiate. It fails as an ordinary error, not a crash — so
the node stays up and looks healthy while the application is broken, and the
crash-loop rollback described below cannot fire for this failure mode. That is
the part worth knowing: there is no automatic recovery and no obvious signal.

Nothing on our infrastructure calls these functions. A third-party delegate cannot
be ruled out.

If you maintain a delegate: check for these three calls before upgrading a node
your delegate runs on. The SDK surface needs a bump or a deprecation; until then the
published SDK and the node disagree.


Routing: what the evidence actually says

The pre-registered gate (PLAN-v2) is INSUFFICIENT on both primary measures and
PASS on the secondary
. Final scoring 2026-09-19 on all recorded data:

measure result verdict
M1, within-contract ranking +0.114 [−0.094, 0.203] INSUFFICIENT — short of the −0.05 margin by 0.044, unresolved
M2, dead-contract pollution 0.0633 [0.0300, 0.1044] against a bar of 0.1008 INSUFFICIENT — short by 0.0036, unresolved
M3, failure Brier vs legacy 1.008 [0.959, 1.056] PASS, no slice flagged

This ships as a deliberate decision, not because the gate authorised it. The gate
did not pass. Two of its three measures remain unresolved, and shipping on that
evidence was a judgement call made with the numbers in view.

For contrast, the estimator that shipped before the contract term fails M3 at
1.244–1.322 in every window. So the change is clearly better than what is deployed
today, and unproven against legacy on ranking.

Known residuals, all recorded on #5700 and #5704: the learning bias is real,
larger on gate data than first measured, reduced about 20% by the fixes and not
removed; the contract term can act on about 36% of refits; its reach into the failure
population is small and compounds; and on quieter nodes the components are
unestimable on 73% of refits, so the term is mostly inert there. The quiet-node
limitation is about benefit, not safety — the estimator routes everywhere
regardless, and an inert term is the low-risk case.

Live watch actually achieved

About 21 hours on both production gateways, on the exact release binary
a7c941326d3a, estimator on, zero restarts (NRestarts=0 since 2026-09-20
13:17:32), zero panics, recording left on. PLAN-v2 registered ≥3 h and 2 h was
authorised, so the watch overran its registered length by roughly 7×. It was not
planned that way: it was extended overnight because the connection criterion below
could not be scored at six hours, and the extra hours are what resolved it. The
relayed-GET figures quoted below are from the first 5.96 h, the window that was
scored against the registered criterion.

Relayed-GET failures per attempt came in below baseline on both gateways —
gw1 844/1365 = 0.6183 vs 2003/3127 = 0.6406, gw2 746/1182 = 0.6311 vs 1175/1857 =
0.6327 — and inside the registered 0.25 pp band. This is weak evidence, not a
clean result.
The 95% minimum detectable difference over the real window is
3.08 pp (gw1) and 3.52 pp (gw2), so the band sits 12–14× below the measurement's own
resolution. It is a criterion that cannot fail informatively, and it is reported here
in the same register as the INSUFFICIENT gate results rather than as a pass.

Limit 1 — the window. Six hours covers gross breakage: panic, restart loop,
connection collapse. It does not cover slow-onset failure — memory growth,
connection decay, drift in failure rates over days.

Limit 2 — the watch never isolated the change under test. The binary running
immediately before the watch was a different RC build, not v0.2.135 and not the
watch binary. So the "before" and "after" states differ by more than #5709's five
files, and no measurement in this release cleanly attributes anything to that
change
. This holds regardless of how the connection question below resolves, and
it is the single largest gap in the evidence.

Limit 3 — connections were not a clean pass. Both gateways were restarted onto
the watch binary from a cold ring. At matched uptime (T+4–6 h) both sat below their
own previous post-restart ramp: gw1 79.8 vs 113.7 (−30%), gw2 101.0 vs 121.8 (−19%).
The pre-registered 20% band was discarded as unusable — it was derived from a
four-day-old gw1 sitting saturated at its configured max-number-of-connections = 200 ceiling, a level a freshly restarted node could not reach at six hours on the
previous binary either. A metric resting on its ceiling cannot express a decline.

Two further checks, each pre-registered before its data, kept this from being read
as a regression in this release:

  • It is not the network shrinking. Fleet size was flat across 09-15…09-20 and
    09-20 was the largest of the six days (1444.0 peers vs 1392.4 on 09-16), with
    version mix unchanged.
  • It is not this build. Compared like-for-like against itself immediately before
    the restart, on the previous binary, gw2 went 72.1 → 97.3 — 35% higher on the
    build being released. A build that degraded connection acquisition does not produce
    that.

What remains is an unexplained multi-day downward drift in gateway connection counts
that predates this build and does not track fleet size. It is filed as #5711
and is not a regression in this release.

The replacement question was re-registered at 19:45 on 09-20, before any 09-21 data
existed, and scored at 10:33 on 09-21 at T+21.3 h — all three tests PASS:

test requirement result
(a) SHAPE — primary gw1 ≥ 100 at T+16–18 h 166.5 (n=4, 164–170) — PASS
(b) LEVEL gw1 ≥ 112.7 166.5, i.e. 103% of the Sep-16 reference of 161.0 — PASS
(c) CONTROL gw2 ≥ 115.6 142.5 (n=2), 86% of its 165.1 reference — PASS

gw1 went 79.8 at T+4–6 h → 166.5 at T+16–18 h, so it had not stopped acquiring
connections; it was simply behind on accumulation at six hours. The six-hour
shortfall was an artefact of reading a cold ring against a saturated reference. The
connection question is closed.


Rollback, and its limits

FREENET_ROUTING_HIERARCHICAL=0 restores the previous routing behaviour exactly.
This is pinned non-vacuously: with the flag off, 2,000 poisoning observations move
zero prediction bits, and a flag-on arm proves the same swap does move them. The
legacy stack stays warm while hierarchical routes, so its model does not rot.

A second switch, FREENET_ROUTING_LEGACY_LABELS=1, reverts #5653's failure
labelling. The first changes which peer is picked; the second changes what the
router learns from
. Both together restore the routing stack to 0.2.135 end to end.

On Linux. Write a drop-in — never edit the unit, it is overwritten by install —
at ~/.config/systemd/user/freenet.service.d/routing-rollback.conf:

[Service]
Environment=FREENET_ROUTING_HIERARCHICAL=0

then systemctl --user daemon-reload && systemctl --user restart freenet.

Confirm from the node's own log at ~/.local/state/freenet/freenet.$(date +%F).log
— not the journal, not freenet.error.*: the line is INFO. Absence of the line is
not proof
, because the flag resolves lazily on the first routing event. Expect
distance-based routing for about 50 events after restart.

There is no config-file or CLI surface for either flag. So on macOS (the
generated plist has no environment section) and Windows (autostart is a bare
registry key) there is no lever at all — the only remedy is shipping a revert.
If you are on either platform and need the previous routing behaviour, you are
waiting on a release, not turning a knob.

Auto-update crash-loop rollback

Verified intact for this release by inspection: the crash-loop machinery in
crates/core/src/bin/commands/service/wrapper.rs is unchanged across
v0.2.135..main. The only file in the update path that changed is
crates/core/src/bin/commands/rollback.rs (+663 lines, predominantly tests), from
#5649 and #5240, both of which concern the post-update probation announcement.

The gate is a probation marker plus an in-process crash classifier, so it fires on
panics, signal deaths and non-zero exits during probation.

It does not fire for #5638. A delegate calling a removed host function produces
an ordinary error, not a crash, so the node never enters the classifier's path. See
the BREAKING section.


Review coverage for #5709 — stated plainly

#5709 is a contract-integrity fix and it ships in this release. What reviewed it, and
what did not, is recorded here rather than summarised as "reviewed", because the gap
is in the dimension that matters most.

What DID cover it:

  • Mutation testing, three runs, conclusive. Baseline 11 tests passed. An
    add-variant and a move-variant each produced 7 passed / 4 failed — the same four
    predicted tests — with the count guard and the ordering guard each observed
    failing separately
    . The tests are not ornamental: they demonstrably bite when the
    fix is removed.
  • codex review (external, non-Claude): clean, no findings. Its blind spots do
    not correlate with the model that wrote the code, which is the point of running it.
  • A code-first Claude lens reported three findings, all dismissed with written
    reasoning.
    Two were low-severity items in the warn-dedup, left unfixed
    deliberately to preserve the soak identity, and are filed as #5712. The third
    concerned an earlier commit in the series whose implementation was later replaced;
    HEAD was verified correct. That third finding is independent evidence the guard is
    doing real work
    rather than sitting inert.
  • Eleven params_integrity tests pass, including three repair/compatibility tests
    (valid_container_restores_missing_params_on_merge,
    local_reput_repairs_a_refused_params_row,
    stored_params_not_deriving_the_instance_are_not_used) covering the upgrade and
    recovery paths the authors anticipated.

What did NOT cover it:

  • No adversarial regression lens completed. Two lenses were commissioned on the
    evening of 09-20 (authorization completeness, regression risk) and neither reported
    — four lenses failed to report that day, consistent with quota. A third was
    commissioned on the morning of 09-21 against the exact release content with a
    20-minute box and a 10-minute extension; it was still running when the cut
    proceeded and did not report either.
  • So the specific question "can a legitimate contract now FAIL where it previously
    succeeded?"
    — older params rows written before this change, in-flight
    initialisation, same-state re-PUT, fail-closed error paths, concurrent params
    writes — was never swept by a reviewer hunting regressions rather than confirming
    the fix.
    There is also no independent enumeration of every code path that writes
    the params row, so a write path the authors did not think of would not have been
    caught.

This ships as Ian's explicit decision, made with both lists in view, weighing the
mutation and codex evidence against an acknowledged gap in exactly the dimension he
weights most heavily. It is not a claim that the change was fully reviewed.


Upgrade notes

  • Routing changes behaviour for every node that upgrades. There is no opt-in period.
  • If you run a delegate, read the BREAKING section before upgrading.
  • Operators contributing disk: --max-hosting-storage is the flag (#5651).
  • Telemetry consumers: the resident-overhead eviction axis is now exported, and the
    field previously labelled "RAM used" for contract state has been renamed (#5646).
    Dashboards reading the old label need updating.

Follow-ups tracked

#5711 (gateway ring_connections drifting down over days, independent of fleet
size — found while scoring this release's watch, predates this build, not a
regression in it), #5700 (era-coupling and the bar asymmetry), #5704 (the df >= 2 gate leaving the
contract term inert on most refits), #4822 (Renegade training stalls the router lock
up to ~0.5 s; independent of the estimator, still unfixed), the deferred per-decision
hoist of shared_effect, and #5712 (the two low-severity findings from the #5709
review, filed with the reasoning for leaving them unfixed).

#5681, which deletes the legacy stack, stays parked. That stack is the rollback
path for this release.