OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay commits / 9d6960a

fix: stop publishing operator identity — this is an anonymity network
utah is currently serving `operator: "hive-foundation-utah"` from its unauthenticated /.well-known/hiverelay.json, and my earlier plan would have put `hive-foundation` on all 14 boxes. That hands any passive observer a ready-made cluster of every relay one party runs, tied to a real-world entity that can be subpoenaed, pressured or blocked wholesale — the exact correlation the Tor path, blind cells and split transport exist to prevent. The mistake was conflating two questions. Quorum safety needs to know "could these relays collude or be compelled together?" It never needs to know WHO. I published the second. - `operator` is redefined as an OPAQUE same-party token. Not a name, not an organisation, nothing that resolves to a legal entity. Declaring it is opt-in and publishes a linkage set; the docs now say so plainly. - The fleet declares nothing. quorum-selector already treats undeclared as `__undeclared__` and never counts it toward independence, so the fleet forfeits diversity credit instead of leaking identity to claim it. For a single-owner fleet that credit was false anyway — this loses nothing real. - deploy-vps.sh omits the --operator flag entirely when unset, rather than passing an empty string that would publish an empty identity. - Failure domains become opaque pseudonyms (fd-a1, fd-b2) instead of cloudzy-utah / cloudzy-singapore-1. Clients need only inequality — "do these two die together?" — never the provider or the datacenter. The conservative direction is the correct one: a quorum that cannot prove independence should assume it has none. Still outstanding: utah is live with the identity in its systemd ExecStart and needs the flag removed and the unit restarted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
9d6960a111af763be102a5e4b8c767553451848b
by bigdestiny2 <33146784+bigdestiny2@users.noreply.github.com> · 2026-07-28T19:31:32.000Z

Changed files (4) +82 -39

packages/core/config/default.js +5 -4
……@@ -210,10 +210,11 @@ export default {
210210 // 9125f3c missed: that commit set 'anonymous' in relay-node/index.js and
211211 // bare-relay.js, but the value here is merged in AFTER those defaults, so it
212212 // won. A default install was silently back on 'authenticated-user'.
213 // Who owns this relay. The TRUST axis: quorum diversity, and any claim that
214 // a quorum spans independent parties, is counted from this. Boxes under one
215 // owner must share one value even when they are in different datacenters —
216 // use failureDomain to keep them scheduled apart.
213 // Opaque same-party token. NOT a name. See capability-doc.js — declaring it
214 // publishes a linkage set for every relay you run, on an unauthenticated
215 // endpoint. Null (the default) is the anonymous and conservative choice:
216 // undeclared is never counted toward quorum independence, so you forfeit
217 // diversity credit rather than leak identity to claim it.
217218 operator: null,
218219 // Where this relay fails together. The SCHEDULING axis: distinct values make
219220 // the replica scheduler spread across hosts/racks/datacenters. Safe to make
packages/core/core/capability-doc.js +17 -10
……@@ -304,18 +304,25 @@ export function buildCapabilityDoc (opts = {}) {
304304 // Region — operators configure via regions[]. First entry is canonical.
305305 const region = (Array.isArray(config.regions) && config.regions[0]) || null
306306
307 // Operator identity. Additive (schemaVersion stays 1); null when unset.
307 // Operator — an OPAQUE same-party token. Not a name, not an organisation,
308 // not anything that resolves to a legal entity.
308309 //
309 // Load-bearing for quorum diversity, not cosmetic. quorum-selector's
310 // selectDiverse() keys on `r.operator || r.pubkey`, so an absent operator
311 // makes every relay its own operator — and a quorum drawn from five boxes in
312 // one datacenter under one owner scores as five independent operators. The
313 // CLI has accepted --operator all along and stores it in config; it simply
314 // was never published, so no client could ever see it.
310 // Quorum safety needs one bit: "could these relays collude or be compelled
311 // together?" It never needs to know WHO. Publishing an identity instead of a
312 // token hands a passive observer a ready-made cluster of every relay one
313 // party runs, on an unauthenticated endpoint, which is the opposite of what
314 // the Tor path, blind cells and split transport exist to achieve.
315315 //
316 // Publishing it makes concentration VISIBLE, which will make some current
317 // quorums honestly fail their minOperators floor. That is the point: the
318 // floor was being met by an accounting artefact.
316 // DECLARING IS OPT-IN AND LEAKS A LINKAGE SET. Omitting it is the safe
317 // default: quorum-selector treats undeclared as `__undeclared__`, which is
318 // never counted toward independence, so an anonymous operator forfeits
319 // diversity credit rather than silently manufacturing it. That trade is the
320 // correct direction — a quorum that cannot prove independence should assume
321 // it has none.
322 //
323 // If you do declare, use a random token (e.g. 32 hex) shared across your own
324 // relays, and rotate it if the linkage set becomes sensitive. Never encode a
325 // hostname, provider, organisation or region here.
319326 const operator = (typeof config.operator === 'string' && config.operator.trim()) || null
320327
321328 // failureDomain — the SCHEDULING axis, kept distinct from `operator`.
scripts/deploy-vps.sh +38 -25
……@@ -25,34 +25,38 @@ SINGAPORE_API_KEY="${SINGAPORE_API_KEY:-}"
2525SINGAPORE2_API_KEY="${SINGAPORE2_API_KEY:-}"
2626BERN_API_KEY="${BERN_API_KEY:-}"
2727
28# Operator identity — WHO OWNS THE RELAY. This is the trust axis: quorum
29# independence, and any claim that a quorum spans parties who cannot collude,
30# is counted from this value. Every box below is run by one operator, so they
31# all declare the SAME id. A fourteen-box single-owner fleet is one operator,
32# and saying so is what makes a future independence claim believable.
28# Operator — DELIBERATELY UNSET. This is an anonymity network.
3329#
34# This previously assigned a distinct id per box (hive-foundation-utah,
35# -utah-us, -singapore, -singapore-2 ...) with a comment explaining that they
36# existed so the replica scheduler would treat the boxes as separate nodes.
37# That is the right goal and was the wrong field: it made one owner report as
38# up to fourteen operators and satisfy any minOperators floor automatically.
39# Spreading now lives in FAILURE_DOMAIN below.
40HIVE_OPERATOR="${HIVE_OPERATOR:-hive-foundation}"
30# The capability doc is unauthenticated: anything set here is served to anyone
31# who asks. Declaring an operator publishes a linkage set naming every relay
32# one party runs — precisely the correlation the Tor path, blind cells and
33# split transport exist to prevent. A named value ("hive-foundation-utah") is
34# worse still: it links the fleet to a real-world entity that can be
35# subpoenaed, pressured or blocked wholesale.
36#
37# Leaving it unset is not a gap. quorum-selector treats undeclared as
38# `__undeclared__` and never counts it toward independence, so this fleet
39# forfeits diversity credit instead of leaking identity to claim it. For a
40# single-owner fleet that credit would have been false anyway.
41#
42# If a future deployment must be countable, set an OPAQUE random token shared
43# across that operator's relays — never a name. See capability-doc.js.
44HIVE_OPERATOR="${HIVE_OPERATOR:-}"
4145UTAH_OPERATOR="${UTAH_OPERATOR:-$HIVE_OPERATOR}"
4246UTAH_US_OPERATOR="${UTAH_US_OPERATOR:-$HIVE_OPERATOR}"
4347SINGAPORE_OPERATOR="${SINGAPORE_OPERATOR:-$HIVE_OPERATOR}"
4448SINGAPORE2_OPERATOR="${SINGAPORE2_OPERATOR:-$HIVE_OPERATOR}"
4549BERN_OPERATOR="${BERN_OPERATOR:-$HIVE_OPERATOR}"
4650
47# Failure domain — WHAT DIES TOGETHER. This is the scheduling axis, and it is
48# per-box on purpose: losing one host should not lose every replica. Distinct
49# values here assert nothing about independence, so they are free to be as
50# granular as the topology warrants.
51UTAH_FAILURE_DOMAIN="${UTAH_FAILURE_DOMAIN:-cloudzy-utah}"
52UTAH_US_FAILURE_DOMAIN="${UTAH_US_FAILURE_DOMAIN:-cloudzy-utah-us}"
53SINGAPORE_FAILURE_DOMAIN="${SINGAPORE_FAILURE_DOMAIN:-cloudzy-singapore-1}"
54SINGAPORE2_FAILURE_DOMAIN="${SINGAPORE2_FAILURE_DOMAIN:-cloudzy-singapore-2}"
55BERN_FAILURE_DOMAIN="${BERN_FAILURE_DOMAIN:-cloudzy-bern}"
51# Failure domain — the SCHEDULING axis, and also published, so keep it opaque.
52# Clients need only inequality ("these two die together / they do not"); they
53# never need to learn the provider or the datacenter. Values are per-box
54# pseudonyms rather than "cloudzy-utah", which would leak both.
55UTAH_FAILURE_DOMAIN="${UTAH_FAILURE_DOMAIN:-fd-a1}"
56UTAH_US_FAILURE_DOMAIN="${UTAH_US_FAILURE_DOMAIN:-fd-a2}"
57SINGAPORE_FAILURE_DOMAIN="${SINGAPORE_FAILURE_DOMAIN:-fd-b1}"
58SINGAPORE2_FAILURE_DOMAIN="${SINGAPORE2_FAILURE_DOMAIN:-fd-b2}"
59BERN_FAILURE_DOMAIN="${BERN_FAILURE_DOMAIN:-fd-c1}"
5660
5761deploy_server() {
5862 local IP=$1
……@@ -60,7 +64,10 @@ deploy_server() {
6064 local REGION=$3
6165 local MAX_MEM=$4 # systemd MemoryMax (e.g., 384M, 1G)
6266 local HEAP=$5 # Node --max-old-space-size in MB
63 local OPERATOR=${6:-hive-foundation} # operator identifier for AutoHeal v2 sybil resistance
67 # Empty by default: unset means the relay declares no operator, which is
68 # the anonymous and conservative choice (see the header). Pass an OPAQUE
69 # token only if this deployment must be countable for quorum diversity.
70 local OPERATOR=${6:-}
6471 local API_KEY_OVERRIDE=${7:-} # optional per-relay API key override
6572
6673 # Resolve effective API key (per-relay override beats env var)
……@@ -68,7 +75,7 @@ deploy_server() {
6875 validate_deploy_value "relay name" "$NAME" '^[A-Za-z0-9._-]{1,64}$'
6976 validate_deploy_value "relay IP/host" "$IP" '^[A-Za-z0-9:._-]{1,255}$'
7077 validate_deploy_value "relay region" "$REGION" '^[A-Za-z0-9._-]{1,32}$'
71 validate_deploy_value "relay operator" "$OPERATOR" '^[A-Za-z0-9._-]{1,64}$'
78 [ -n "$OPERATOR" ] && validate_deploy_value "relay operator" "$OPERATOR" '^[A-Za-z0-9._-]{1,64}$'
7279 validate_deploy_value "heap size" "$HEAP" '^[0-9]{2,6}$'
7380 validate_deploy_value "memory limit" "$MAX_MEM" '^[0-9]+[MG]$'
7481 validate_api_key "$EFFECTIVE_KEY"
……@@ -153,7 +160,7 @@ Wants=network-online.target
153160[Service]
154161Type=simple
155162WorkingDirectory=/root/hiverelay
156ExecStart=/usr/bin/node --max-old-space-size=HEAP_PLACEHOLDER packages/core/cli/index.js start --mode public --region REGION_PLACEHOLDER --operator OPERATOR_PLACEHOLDER --auto-heal
163ExecStart=/usr/bin/node --max-old-space-size=HEAP_PLACEHOLDER packages/core/cli/index.js start --mode public --region REGION_PLACEHOLDER OPERATOR_FLAG_PLACEHOLDER --auto-heal
157164Restart=always
158165RestartSec=15
159166KillSignal=SIGTERM
……@@ -177,7 +184,13 @@ SYSTEMD_UNIT
177184 # Replace placeholders
178185 sed -i "s/HEAP_PLACEHOLDER/${HEAP}/" /etc/systemd/system/hiverelay.service
179186 sed -i "s/REGION_PLACEHOLDER/${REGION}/" /etc/systemd/system/hiverelay.service
180 sed -i "s/OPERATOR_PLACEHOLDER/${OPERATOR}/" /etc/systemd/system/hiverelay.service
187 # Substitute the whole flag, so an unset operator emits nothing at all
188 # rather than `--operator ""` (which would publish an empty identity).
189 if [ -n "${OPERATOR}" ]; then
190 sed -i "s|OPERATOR_FLAG_PLACEHOLDER|--operator ${OPERATOR}|" /etc/systemd/system/hiverelay.service
191 else
192 sed -i "s| OPERATOR_FLAG_PLACEHOLDER||" /etc/systemd/system/hiverelay.service
193 fi
181194 sed -i "s/MEM_PLACEHOLDER/${MAX_MEM}/" /etc/systemd/system/hiverelay.service
182195 sed -i "s/MEMHIGH_PLACEHOLDER/${MEMORY_HIGH}/" /etc/systemd/system/hiverelay.service
183196
test/unit/quorum-selector.test.js +22 -0
……@@ -289,3 +289,25 @@ test('an undeclared operator is unknown, not independent', async (t) => {
289289 t.is(selected.diversityWarning.operatorsUndeclared, true)
290290 t.is(desc.failureDomains.length, 3, 'they are still spread, falling back to pubkey')
291291})
292
293test('an anonymous fleet forfeits diversity credit rather than leaking identity', async (t) => {
294 // This is an anonymity network, and the capability doc is unauthenticated.
295 // Declaring an operator publishes a linkage set naming every relay one party
296 // runs — the exact correlation the Tor path and blind cells exist to prevent.
297 // So the fleet declares nothing, and the selector must respond by refusing to
298 // credit independence, NOT by inventing it from relay keys.
299 const anonFleet = [
300 { pubkey: 'd1', region: 'NA', failureDomain: 'fd-a1', score: 0.9 },
301 { pubkey: 'd2', region: 'EU', failureDomain: 'fd-c1', score: 0.8 },
302 { pubkey: 'd3', region: 'APAC', failureDomain: 'fd-b1', score: 0.7 }
303 ]
304 const selected = selectQuorum(anonFleet, { strategy: 'diverse', size: 3, minRegions: 3, minOperators: 2 })
305 const desc = describeQuorum(selected)
306
307 t.is(desc.regions.length, 3, 'region diversity is real and still counted')
308 t.is(desc.failureDomains.length, 3, 'spreading still works with no operator declared')
309 t.is(desc.operators.length, 1, 'three anonymous relays are one unknown, not three operators')
310 t.is(selected.diversityWarning.reason, 'insufficient-operator-diversity')
311 t.is(selected.diversityWarning.operatorsUndeclared, true,
312 'the caller can tell "undeclared" from "declared but concentrated"')
313})