Skip to content

fix(model): count only providers holding a Public provide key - #408

Closed
Ryanmello07 wants to merge 5 commits into
urnetwork:mainfrom
Ryanmello07:feat/provider-count-provide-mode-upstream
Closed

fix(model): count only providers holding a Public provide key#408
Ryanmello07 wants to merge 5 commits into
urnetwork:mainfrom
Ryanmello07:feat/provider-count-provide-mode-upstream

Conversation

@Ryanmello07

Copy link
Copy Markdown
Contributor

A provider without a provide_mode=3 key cannot accept a contract from a client outside its own network, so counting it advertises unreachable supply. Measured on a beta deployment: 39 US providers advertised, 2 reachable.

Filters both UpdateClientLocations and UpdateClientScores, since GetProviderLocations gates on loadLocationStables (populated by UpdateClientScores) as well — filtering only the first leaves the symptom in place.

New test TestUpdateClientLocationsCountsOnlyPublicProviders: three connected+valid clients in the same country, only one holding a Public key. Fails with ClientCount = 3 before the change, passes with 1 after.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QtgqtCmKJRXdsQ5ktiqwkg

Ryanmello07 and others added 5 commits July 26, 2026 06:37
A provider without a provide_mode=3 key cannot accept a contract from a
client outside its own network, so counting it advertises supply nobody
can reach. On beta this reported 39 US providers when 2 were reachable.

Filters both UpdateClientLocations and UpdateClientScores: GetProviderLocations
gates on loadLocationStables as well, so fixing only the first leaves the
symptom in place.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QtgqtCmKJRXdsQ5ktiqwkg
…e score filter

Review follow-ups to "count only providers holding a Public provide key".

1. The location *group* source query in UpdateClientScores was still bare
   `connected = true AND valid = true`. It fills locationGroupClientScores ->
   the clientScoreLocationGroup* redis keys -> loadClientScores ->
   FindProviders2 whenever a spec carries a LocationGroupId, so a user
   selecting a promoted group (e.g. "Strong Privacy Laws") still received
   providers that cannot accept their contract, and CreateContract rejected
   them with NoPermission. Same EXISTS predicate as the other two queries.

2. The UpdateClientScores filter had no test of its own -- every existing
   test that reaches it gives all its clients a Public key, so deleting the
   clause broke nothing. Two new tests set up connected+valid providers that
   are identical except for the Public provide key:
   - TestUpdateClientScoresCountsOnlyPublicProviders covers the per-location
     query through loadLocationStables (the gate GetProviderLocations uses)
     and loadClientScores.
   - TestUpdateClientScoresGroupCountsOnlyPublicProviders covers the group
     query through the group cache.
   Both were verified to fail with their respective EXISTS clause removed.

   Because UpdateClientScores inner joins client_connection_reliability_score
   here, each provider also gets a reliability score row
   (AddClientReliabilityStats + UpdateClientReliabilityScores at
   server.NowUtc(), not NowUtc().Add(time.Hour), which produces no
   lookback_index = 0 row), and the fixture levels both clients'
   independent_reliability_weight so a single sample's ~0.016 weight does not
   fail the strict minimums for both and make the stability assertion pass
   for the wrong reason.

3. The SQL comments claimed the destination "must hold a key for exactly
   that mode". That is not an invariant the code holds:
   resolveNonCompanionProvideMode lets a Stream-only destination settle a
   cross-network contract as a companion stream. Reworded to state the
   intent -- a companion-only destination is a return path, not general
   provider supply, so excluding it from provider counts is deliberate.
   No SQL change.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QtgqtCmKJRXdsQ5ktiqwkg
…ders discoverable to their own network

Two production risks in this change, found by a backward-compatibility
audit of the PR.

1. Country-only clients were counted three times.

A client whose geo lookup resolved neither a city nor a region is stored
with city_location_id = region_location_id = country_location_id --
SetConnectionLocation writes the coarsest available id into the NOT NULL
city/region columns. Both fan-out loops then walked city, region and
country unconditionally, so one such client added 3 to its own country's
provider count and was inserted three times into its country's scoring
map. The inflation is worst exactly where geo resolution is coarsest:
datacenter, mobile and VPN egress.

Both loops now go through the distinct set of location ids, so a client
counts once per location it is actually in. A genuinely city-granular
client still rolls up into its region and its country unchanged -- that
half is asserted explicitly, because a dedupe keyed on the client rather
than on (client, location) would silently stop city clients counting
toward their country, a worse regression than the one being fixed.

(The per-location and per-group scoring maps are keyed by client id, so
they already absorbed the repeat; the count was the live defect. The
loops are made explicit anyway so the two stay in step.)

2. Network-only providers vanished from their own network's discovery.

The Public-only filter is right for the public provider count -- a
stranger genuinely cannot use a ProvideModeNetwork provider -- but wrong
for the candidate pool. Such a provider serves same-network sources today
via the working CreateContractNoEscrow path and is discoverable to them
now; filtering the pool to Public made it undiscoverable to exactly the
users it exists for. That is a live regression for anyone running
providers for their own organisation.

The pool must also not hand a cross-network caller a provider whose
contract will be refused, so this is not a revert:

  - UpdateClientLocations keeps the Public-only filter. That number is
    shown to everyone and should reflect what a stranger can reach.
  - The UpdateClientScores source queries now admit Public or Network --
    exactly the two modes GetProvideRelationship can return, hence
    exactly what CreateContract can accept -- and record NetworkOnly on
    each ClientScore.
  - FindProviders2 filters at request time: a candidate is eligible if it
    is publicly usable, or if its NetworkId equals the requesting
    session's network. This cannot be baked into the cache: the client
    score redis entries are keyed by (forceMinimum, rankMode, locationId,
    callerLocationId) with no network component, so one cached set is
    shared by callers from every network.

NetworkOnly is stored negated deliberately. The score cache is gob
encoded with a 5h ttl, so entries written before the field existed decode
with the zero value; the zero value therefore has to mean "publicly
usable", or every provider would be treated as network-only until the
cache turned over.

The ClientFilter exported alongside the samples still counts only
publicly usable providers. It is read solely by loadLocationStables,
which decides the public Stable flag GetProviderLocations publishes -- a
public surface, so it follows the same rule as the provider count. A
location whose only supply is network-only is not stable and reports no
providers.

No Stream carve-out: resolveNonCompanionProvideMode does return
companion=true for a Stream-only destination, but that dead-ends at
CreateCompanionTransferEscrow, which requires a pre-existing
reverse-direction origin contract, so a Stream-only destination can never
bootstrap a cross-network session. The inline comments that implied
Stream was the only excluded case are corrected.

Tests: a country-only provider counted exactly once and a city provider
still counted at all three granularities, for both UpdateClientLocations
and UpdateClientScores; and all four visibility cases in FindProviders2
(Public and Network-only providers x same-network and other-network
callers). The two existing pool assertions that encoded the Public-only
pool are updated to the new two-tier contract; their location-stability
assertions are unchanged and still pass.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QtgqtCmKJRXdsQ5ktiqwkg
`/network/provider-locations` (UpdateClientLocations) counts only providers
holding a Public provide key: GetProvideRelationship returns
ProvideModePublic for a cross-network pair, so without a Public key a
provider can only ever serve requesters inside its own network. As the
owner put it -- "for provider list only do public. network ones can only be
used by their own network so they are basically private."

`/stats/providers-map` (GetProvidersMap) and CountProviderCountries applied
no provide-mode filter at all. Once the provide-mode change ships, those two
public numbers report supply that is not in the list users can pick from --
the map's per-region counts and the country count both exceed
/network/provider-locations, in the same deployment, for the same
population. The country count is worse than an inflated number: a country
whose only providers are network-only or hold no provide key at all counts
as a covered country while offering a stranger nothing.

Both now apply the same
`EXISTS (SELECT 1 FROM provide_key WHERE client_id = ... AND provide_mode =
$N)` predicate UpdateClientLocations uses, mirrored exactly rather than
varied, so every public provider number answers one question: what a
stranger can actually reach.

TestProviderStatsCountOnlyPublicProviders covers both surfaces in one run
(Errorf, not Fatalf, on the first so the second is still exercised): a
Public provider in au counts, a Network-only and a keyless provider in jp
do not, so the map has no jp entry and the country count is 1 not 2.
TestGetProvidersMapAggregatesRegions' helper now gives its providers a
Public key, since every provider it builds is meant to count.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QtgqtCmKJRXdsQ5ktiqwkg
(cherry picked from commit d06fc68)
…test that could not fail

Two review findings on the provide-mode filtering work.

1. Deleting either provide-mode predicate left CI green.

Both UpdateClientScores source queries carry
`EXISTS (... provide_mode IN (Public, Network))`, and nothing failed when
either one was removed. The existing provide-mode tests build Public and
Network-only providers, both of which the predicate admits, so a
deletion changes nothing they look at.

What a deletion actually does is refill the candidate pool with the
entire consumer fleet: every client registers ProvideMode_Stream on
connect (connect/transfer_contract_manager.go), and a Stream-only client
can never settle a contract -- resolveNonCompanionProvideMode can
resolve it as a companion, but that dead-ends at
CreateCompanionTransferEscrow, which needs a pre-existing reverse origin
contract. That is precisely the 39-advertised-against-2-usable failure
this work exists to fix.

Two tests now build the populations that would flood back in -- a
Stream-only provider and a keyless one, alongside a Public and a
Network-only provider in the same location -- and assert the pool holds
exactly the two that can serve a contract, each tagged correctly. One
covers the per-location query, one the location-group query, so a
deletion in either statement produces its own signal:

  per-location predicate deleted -> per-location test fails
                                    ("candidate pool holds 4 clients,
                                     want exactly the 2 that can settle
                                     a contract"), group test passes
  group predicate deleted        -> the reverse

2. TestUpdateClientScoresCountsEachClientOncePerLocation was vacuous.

Its per-location accumulator is a map keyed by client id, so a repeated
client is absorbed whether or not the loop dedupes: it returned 1 before
the fix and 1 after, and could not fail.

Deleted, and replaced with
TestUpdateClientScoresRollsCityProviderUpToRegionAndCountry, which
asserts the property the code CAN violate -- that a city-granular
provider reaches its region's and its country's pools, and that a
provider does not leak into another country's. Membership is asserted by
client id rather than by count, so it also catches leakage. Keying the
dedupe on the client instead of on (client, location) -- the tempting
simplification, since the map already absorbs repeats -- empties the
region and country pools and fails it.

Also corrects the scope claimed for the 3x dedupe. The commit message
for that change describes the triple-count as a live defect; that is
true on beta, which has SetConnectionLocation's coarsest-granularity
fallback, but not against upstream/main, which has no country-only
fallback and raises on the NULL city instead, so no row with
city = region = country is written there today. The fallback arrives
upstream with PR urnetwork#407, which now carries its own copy of the dedupe.
distinctIds, the counting loop and the fan-out test all say so.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QtgqtCmKJRXdsQ5ktiqwkg
Ryanmello07 added a commit to Ryanmello07/server that referenced this pull request Jul 27, 2026
…finement

Brings in the review fixes for PR urnetwork#408: tests that fail when either
UpdateClientScores provide-mode predicate is deleted, and the
replacement for the vacuous per-location dedupe test.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QtgqtCmKJRXdsQ5ktiqwkg

# Conflicts:
#	model/network_client_location_model_test.go
@Ryanmello07

Copy link
Copy Markdown
Contributor Author

Superseded by #425, which consolidates this stack into a single branch rebased onto current main. The work is unchanged and included there — this PR's commits are in #425's history (or, where the chain rewrote them, their final form is). Closing to keep review in one place.

@Ryanmello07 Ryanmello07 closed this Aug 4, 2026
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