diff --git a/config.example.json b/config.example.json index 64bd00c0..5d7ff0f9 100644 --- a/config.example.json +++ b/config.example.json @@ -224,8 +224,8 @@ "_comment_observerThresholds": "Observer health classification. Online: last_seen < observerOnlineMinutes ago. Stale: between Online and observerStaleMinutes. Offline: beyond observerStaleMinutes. Defaults 60 / 1440 (1h / 24h) match the node thresholds for consistency and eliminate flap on low-traffic / CDN-fronted instances (#1552). Operators who want the old aggressive 10-min Online threshold can set observerOnlineMinutes: 10." }, "pathTrust": { - "minHashBytesForMapping": 2, - "_comment_pathTrust": "Minimum path-hash prefix length, in bytes, trusted as mapping/topology evidence (issue #1784). MeshCore path hops are hashed pubkey prefixes of 1, 2, or 3 bytes (firmware hash_size = (pathByte>>6)+1); valid range is 1-3. Default 2 excludes 1-byte prefixes (256-value collision space) from mapping evidence — operator-confirmed to reduce false positives on denser meshes. Set to 1 for backward-compatible trust-all behavior. Set to 3 for the strictest mode (only 3-byte evidence trusted). This does not affect raw storage — packets/paths are always stored as received." + "minHashBytesForMapping": 1, + "_comment_pathTrust": "Minimum path-hash prefix length, in bytes, trusted as mapping/topology evidence (issue #1784). MeshCore path hops are hashed pubkey prefixes of 1, 2 or 3 bytes (firmware hash_size = (pathByte>>6)+1); valid range is 1-3. Default 1 keeps the pre-#1784 behaviour, where every prefix length counts. Set 2 to exclude 1-byte prefixes (256-value collision space, so a currently-unique match can still be a false positive on a dense mesh), or 3 to require the strongest evidence. NOTE: there is no UI control for this - it is config-only and needs a restart, and raising it can remove a large share of your neighbour-graph edges and resolved paths with nothing in the UI explaining why. This does not affect raw storage: packets and paths are always stored as received." }, "defaultRegion": "SJC", "mapDefaults": { diff --git a/internal/packetpath/trust.go b/internal/packetpath/trust.go index 06daa3e0..b33f1eb2 100644 --- a/internal/packetpath/trust.go +++ b/internal/packetpath/trust.go @@ -20,7 +20,19 @@ type TrustConfig struct { MinHashBytesForMapping int `json:"minHashBytesForMapping,omitempty"` } -const DefaultMinHashBytesForMapping = 2 +// DefaultMinHashBytesForMapping is the backward-compatible default: every +// prefix length counts as mapping evidence, exactly as before #1784. +// +// It is deliberately 1 rather than the stricter 2. There is no UI control for +// this threshold, it can only be changed in config.json and that needs a +// restart, so shipping 2 would tighten every instance on upgrade with nothing +// in the UI explaining why the neighbour graph shrank. Measured on a live +// network, 56% of path-hop observations carry a 1-byte prefix and 41% of +// repeaters use a 1-byte hash, so that is not a marginal change. Issue #1784's +// own first acceptance criterion is that the default stays backward compatible. +// +// Operators who want the stricter behaviour set minHashBytesForMapping: 2 or 3. +const DefaultMinHashBytesForMapping = 1 const MaxHashBytes = 3 diff --git a/internal/packetpath/trust_test.go b/internal/packetpath/trust_test.go index 08f3a0cb..4462b354 100644 --- a/internal/packetpath/trust_test.go +++ b/internal/packetpath/trust_test.go @@ -3,13 +3,16 @@ package packetpath import "testing" func TestMeetsPathTrust_DefaultThreshold(t *testing.T) { - // nil cfg → DefaultMinHashBytesForMapping (2): 0-byte and 1-byte excluded. + // nil cfg → DefaultMinHashBytesForMapping (1): everything is trusted, which + // is the pre-#1784 behaviour. This test is the guard on that promise: if the + // default is ever raised, upgrading instances silently lose a large share of + // their mapping evidence with no UI control to opt back out. cases := []struct { prefixBytes int want bool }{ - {0, false}, - {1, false}, + {0, true}, + {1, true}, {2, true}, {3, true}, } @@ -49,17 +52,25 @@ func TestMeetsPathTrust_Exclude1Byte(t *testing.T) { } func TestMeetsPathTrust_ZeroValueOptIn(t *testing.T) { - // A zero-value MinHashBytesForMapping (unset) must fall back to default (2), - // so bucket-0 and 1-byte prefixes fail; 2-byte and above pass. - cfg := &TrustConfig{} - if MeetsPathTrust(1, cfg) { - t.Errorf("MeetsPathTrust(1, %+v) = true, want false (unset uses default=2, 1-byte excluded)", cfg) + // A zero-value MinHashBytesForMapping (the JSON field absent) must resolve to + // DefaultMinHashBytesForMapping, not be read as an explicit opt-in to some + // other number. Asserted against the constant rather than against a literal + // so this keeps testing the property if the default is ever changed again. + unset := &TrustConfig{} + if got := unset.MinHashBytesOrDefault(); got != DefaultMinHashBytesForMapping { + t.Errorf("unset.MinHashBytesOrDefault() = %d, want %d", got, DefaultMinHashBytesForMapping) } - if MeetsPathTrust(0, cfg) { - t.Errorf("MeetsPathTrust(0, %+v) = true, want false (bucket-0 excluded at default threshold)", cfg) + // And it must behave identically to a config that names the default outright. + explicit := &TrustConfig{MinHashBytesForMapping: DefaultMinHashBytesForMapping} + for _, prefixBytes := range []int{0, 1, 2, 3} { + if MeetsPathTrust(prefixBytes, unset) != MeetsPathTrust(prefixBytes, explicit) { + t.Errorf("prefixBytes=%d: unset and explicit-default disagree", prefixBytes) + } } - if !MeetsPathTrust(2, cfg) { - t.Errorf("MeetsPathTrust(2, %+v) = false, want true (2-byte passes at default threshold)", cfg) + // An explicit stricter setting must still be honoured over the default. + strict := &TrustConfig{MinHashBytesForMapping: 2} + if MeetsPathTrust(1, strict) { + t.Error("MeetsPathTrust(1, minBytes=2) = true, want false — an explicit setting must win") } } diff --git a/public/customize-v2.js b/public/customize-v2.js index 7c9c1ea1..68fb33e6 100644 --- a/public/customize-v2.js +++ b/public/customize-v2.js @@ -1604,7 +1604,7 @@ try { on = localStorage.getItem('meshcore-hide-1byte-hops') === 'true'; } catch (_e) {} var trustThreshold = (typeof window.MC_getPathTrustThreshold === 'function') ? window.MC_getPathTrustThreshold() - : 2; + : 1; var trustDesc = trustThreshold >= 2 ? '1-byte path-hash prefixes collide ~8-way at ~2k relays and are excluded from topology/mapping evidence (minHashBytesForMapping: ' + trustThreshold + '). Routes below this threshold show as speculative or are excluded. Change via pathTrust.minHashBytesForMapping in config.json.' : '1-byte path-hash prefixes collide ~8-way at ~2k relays — many polylines and rows they produce are visual noise. Hide them here without changing what\'s stored. Set pathTrust.minHashBytesForMapping in config.json to 2 or 3 for stricter server-side evidence requirements.'; diff --git a/public/hop-filter.js b/public/hop-filter.js index 73c283fa..c2ccac3e 100644 --- a/public/hop-filter.js +++ b/public/hop-filter.js @@ -56,7 +56,7 @@ if (typeof window !== 'undefined' && typeof window.PATH_TRUST === 'number') { return window.PATH_TRUST; } - return 2; + return 1; } // #1784 — whether a hop meets the server-side path trust threshold. diff --git a/test-issue-1633-hide-1byte-hops.js b/test-issue-1633-hide-1byte-hops.js index b7754b5c..74068890 100644 --- a/test-issue-1633-hide-1byte-hops.js +++ b/test-issue-1633-hide-1byte-hops.js @@ -451,12 +451,18 @@ test('[kb #1] anti-tautology: tests reference the actual production files (not i console.log('\n=== #1784: path trust threshold ==='); -test('#1784: MC_getPathTrustThreshold default is 2 (operator-confirmed)', () => { +test('#1784: MC_getPathTrustThreshold defaults to 1 (backward compatible)', () => { + // Deliberately 1, matching DefaultMinHashBytesForMapping in + // internal/packetpath/trust.go. There is no UI control for this threshold — + // it is config.json only and needs a restart — so a stricter default would + // silently tighten every instance on upgrade with nothing in the UI saying + // why the neighbour graph shrank. Operators opt in with + // pathTrust.minHashBytesForMapping: 2 or 3. const ctx = makeSandbox(); load(ctx, 'public/hop-filter.js'); delete ctx.window.PATH_TRUST; - assert.strictEqual(ctx.window.MC_getPathTrustThreshold(), 2, - 'default path trust threshold must be 2 (operator-confirmed, excludes 1-byte)'); + assert.strictEqual(ctx.window.MC_getPathTrustThreshold(), 1, + 'default path trust threshold must be 1 so an upgrade changes nothing'); }); test('#1784: MC_getPathTrustThreshold reads window.PATH_TRUST', () => {