Skip to content

Fix organization safety threshold reporting zero - #67

Open
tmosleyIII wants to merge 2 commits into
mainfrom
bugfix/tmosley/org-safety-threshold-zero
Open

tmosleyIII wants to merge 2 commits into
mainfrom
bugfix/tmosley/org-safety-threshold-zero

Conversation

@tmosleyIII

Copy link
Copy Markdown
Contributor

The Safety thresholds card on production has an Organization line pinned at zero, next to a healthy top-tier figure. The verdict card shows the contradiction directly: "Core forks if 4 orgs collude" sitting above "Nodes cut off by 0 orgs".

The network is fine. The reporting is not.

What the scans show

[PythonFbas] Network-wide org splitting set: { organizations: 23, minSize: 0 }

Every scan reports it, while the restricted answers in those same scans are healthy — splittingSetsMinSize of 4, 5 and 2 across the aggregated levels, and splittingSetsTopTierMinSize of 4–6. Only the Organization series is affected; Node, Country and ISP are untouched.

Why zero

min_size: 0 is not an error code and not a safety threshold. It is python-fbas reporting that the FBAS it was handed has no quorum intersection: two disjoint quorums already exist, so zero organizations have to fail for safety to break. app.py turns "No splitting set found" into an error precisely so it never arrives as a zero, so zero always means "already split".

The FBAS being handed over is already split because of how the wider set aggregates. mergeQuorumSetsForOrganizations maps each trusted validator to its organization and skips the ones that belong to none, then always adds a self-reference and clamps the threshold down to the surviving count. An organization whose validators trust keys that no organization claims therefore comes out as threshold 1, trusts [itself] — a quorum of one, disjoint from every other quorum.

Confirmed against the real aggregator:

RESTRICTED (core only):            Core A: 2 of [Core A, Core B]
                                   Core B: 2 of [Core A, Core B]

NETWORK-WIDE (core + peripheral):  Core A: 2 of [Core A, Core B]
                                   Core B: 2 of [Core A, Core B]
                                   Peripheral: 1 of [Peripheral]   ← self-satisfying

NetworkScanner feeds this path every validating node (103 → 23 orgs) rather than the 33 core nodes (→ 11 orgs), so the wider set is exactly where these peripheral organizations enter. Nothing caught it either: validateAggregatedNodes only rejects threshold <= 0 and empty quorum sets, and threshold: 1, validators: [self] passes clean — which is why the logs show no aggregation warning.

The two fixes

a1a22b0 — stop the aggregation manufacturing a quorum of one. withoutSelfTrustingGroups drops organizations left trusting nothing but themselves. Deliberately a separate step rather than folded into aggregateByOrganization: the narrow top-tier aggregation has no such groups, and filtering there would silently move a figure that is currently answering its question correctly. Organizations that still depend on the core are kept, so the feature keeps doing its job.

8cf6dc1 — never publish zero as a threshold. The override only skipped undefined, so 0 !== undefined meant zero beat the real restricted answer. It now reports "not computed" and lets the restricted figure stand, and logs when it fires so a recurrence is visible rather than silent. Every neighbouring path in that file already avoids emitting zero for this exact reason.

The first commit removes the known cause; the second is the backstop for any other degenerate shape.

Scope note

This is a reporting bug only — no measurement, scan or persistence behaviour changes, and the restricted org-level analysis is byte-identical. Users of the wider figure should see 4–5 instead of 0 on the next scan.

Verification

  • Both fixes mutation-checked: neutralising the zero guard fails keeps the restricted answer rather than publishing zero; neutralising the filter fails 2 aggregator tests. Neither test is vacuous.
  • 6 new tests covering both fixes, including that a real non-zero wider answer is still used, and that an aggregation with nothing to drop is untouched.
  • Full unit suite green: 1175 tests, 245 suites. tsc --build clean, eslint clean, prettier applied.

Not fixed here: the 1Y/30D views will still show the dip back to 2026-10-01, when #65 introduced the override — this corrects the figure going forward, it does not backfill stored measurements.

🤖 Generated with Claude Code

tmosleyIII and others added 2 commits November 10, 2026 10:08
Aggregating by organization maps every trusted validator to its
organization and skips the ones that belong to none. When nothing
survives that mapping, the self-reference each group receives is all
that is left, and the threshold clamp makes it satisfiable: an
organization trusting only itself, with a threshold of one.

That is a quorum of one. It is disjoint from every other quorum, so the
FBAS containing it has no quorum intersection, so python-fbas answers
every splitting set with zero however the real validators are wired.

It is also an artifact rather than a trust declaration. The
organization's validators do trust other validators; those validators
just belong to no organization in the set being aggregated, so the
mapping dropped them.

withoutSelfTrustingGroups leaves such a group out. It is deliberately a
separate step rather than part of aggregateByOrganization: the narrow
top-tier aggregation has no such groups, and dropping them there would
silently move a figure that is answering its question correctly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
python-fbas answers with a minimal splitting-set cardinality of zero
when the FBAS it was handed has no quorum intersection: two disjoint
quorums already exist, so no organization has to fail for safety to
break. It is a statement about the FBAS, not a safety threshold.

analyzeNetworkWideOrgSplittingSet returned it anyway, and the override
below only skips undefined, so zero won over a perfectly good restricted
answer. On the dashboard that rendered as a Safety thresholds chart whose
Organization line sat at zero, and a verdict card reading "Core forks if
4 orgs collude" directly above "Nodes cut off by 0 orgs".

Every neighbouring path in this file already avoids emitting zero for
exactly this reason, and "No splitting set found" arrives as an error
rather than a zero, so there is no real answer of zero being discarded
here. Report it as not computed and let the restricted figure stand.

The preceding commit removes the known cause; this is the backstop for
any other degenerate shape, and it logs when it fires so a recurrence is
visible rather than silent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@tmosleyIII
tmosleyIII deployed to obsrvr-radar-production November 10, 2026 15:14 — with GitHub Actions Active
@tmosleyIII
tmosleyIII temporarily deployed to obsrvr-radar-staging November 10, 2026 15:14 — with GitHub Actions Inactive
@tmosleyIII
tmosleyIII deployed to obsrvr-radar-staging November 10, 2026 15:15 — with GitHub Actions Active

This branch was successfully deployed

2 active deployments
obsrvr-radar-staging — 8cf6dc1b Deployed Nov 10, 2026 by tmosleyIII via deploy-staging #291
obsrvr-radar-production — 8cf6dc1b Deployed Nov 10, 2026 by tmosleyIII via Plan Production Environment #291
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant