@powforge/identity
Advanced tools
+28
-1
| # Changelog | ||
| ## 0.7.2 | ||
| ### Fixed | ||
| - `scoreAccess` (access dimension) was linear in `totalPow`, which let a | ||
| PoW farm dominate the multi-dim score. Median Sybil PoW farm scored | ||
| ~5,386 vs genuine median ~346 in H-DOI-1 v2 discrimination tests — | ||
| a 16x grindable advantage that collapsed multi-dim AUC from a target | ||
| ~0.95 down to 0.800. Fixed by switching to log2 scaling (each PoW bit | ||
| is already exponentially costly per NIP-13, so log2 of the bit-sum is | ||
| the honest measure of order-of-magnitude effort). | ||
| - Added `PER_DIMENSION_SCORE_CAP = 100` defense-in-depth on | ||
| `scoreAccess` so even a pathological farm cannot exceed the natural | ||
| peak of other log-bounded dimensions. | ||
| - Post-fix multi-dim AUC = 1.000 with FPR = 0% at TPR = 90% on the | ||
| reference 50-genuine + 50-Sybil population. PoW farm median dropped | ||
| from 5,386 to ~94, a 57x reduction. | ||
| ### Compat | ||
| - Score values for genuine users with PoW activity will decrease (linear | ||
| → log2 means a high-PoW user previously scoring ~1,000 in the access | ||
| dim now scores ~50-90). Multi-dim totals are correspondingly lower | ||
| but rank order across genuine users is preserved. Anyone who had | ||
| pinned an absolute weight threshold should re-calibrate against the | ||
| new distribution; AUC-based or relative thresholds are unaffected. | ||
| ## 0.7.1 | ||
@@ -110,3 +137,3 @@ | ||
| ### Motivation | ||
| - Fubz directive msg 1510 (2026-04-23): "wire @powforge/identity chaintip | ||
| - Principal directive msg 1510 (2026-04-23): "wire @powforge/identity chaintip | ||
| + forge-tick bitcoin_pulse to use the internal Bitcoin node at | ||
@@ -113,0 +140,0 @@ lightning.lan:8332." Local RPC means lower latency, no mempool.space |
+2
-2
| { | ||
| "name": "@powforge/identity", | ||
| "version": "0.7.1", | ||
| "version": "0.7.2", | ||
| "description": "Depth-of-Identity SDK for Nostr. Measures accumulated irreversible work across four dimensions of irreversible work (social, access, vouch, economic). Try it live at powforge.dev/explorer.", | ||
@@ -18,3 +18,3 @@ "keywords": [ | ||
| "scripts": { | ||
| "test": "node --test tests/smoke.test.js tests/publish.test.js tests/temporal.test.js tests/zap-bolt11-dedup.test.js tests/zap-receipt-verify.test.js tests/vouch-cycle.test.js tests/indirect-vouch-cycle.test.js tests/chaintip.test.js tests/spatial-deprecation.test.js tests/freshness.test.js" | ||
| "test": "node --test tests/smoke.test.js tests/publish.test.js tests/temporal.test.js tests/zap-bolt11-dedup.test.js tests/zap-receipt-verify.test.js tests/vouch-cycle.test.js tests/indirect-vouch-cycle.test.js tests/chaintip.test.js tests/spatial-deprecation.test.js tests/freshness.test.js tests/access-pow-farm-cap.test.js" | ||
| }, | ||
@@ -21,0 +21,0 @@ "license": "MIT", |
+0
-1
@@ -47,3 +47,2 @@ # @powforge/identity | ||
| > are grind-cheap and Sybil-trivial; the dimension was marketing, not measurement. | ||
| > See [`research/spatial-dim-protocol-apr21.md`](https://gitlab.com/powforge/sats-challenge/-/blob/master/research/spatial-dim-protocol-apr21.md). | ||
| > `scoreSpatial` remains exported for backwards compatibility. Pass | ||
@@ -50,0 +49,0 @@ > `dimensions: ['spatial', 'social', 'access', 'vouch', 'economic']` |
+40
-4
@@ -225,6 +225,37 @@ /** | ||
| /** | ||
| * Per-dimension hard cap. No single dimension may contribute more than this | ||
| * to a pubkey's identity score. Defense-in-depth against any future scoring | ||
| * formula whose growth term escapes its intended range. The cap is set to | ||
| * match the natural peak of the other log-scaled dimensions in genuine | ||
| * populations (social/vouch/economic peak ~80-130 in test data), so honest | ||
| * users are unaffected while a runaway grinder is bounded. | ||
| * | ||
| * H-DOI-1 v2 (tick 360) found access dim was emitting scores up to ~6000 | ||
| * because totalPow was applied linearly. The fix below switches access to | ||
| * log2 internally; this cap is the second layer of insurance. | ||
| */ | ||
| const PER_DIMENSION_SCORE_CAP = 100; | ||
| /** | ||
| * Compute chain depth for access dimension (PoW events) | ||
| * Excludes events already counted by other dimensions (kind 3333, 1, 7, 33335) | ||
| * to prevent cross-contamination / double-counting of PoW. | ||
| * Uses log2 scaling on event count to prevent linear grinding. | ||
| * | ||
| * v0.7.2 — H-DOI-1 v2 fix. The previous formula `totalPow * 2 + log2(events+1)` | ||
| * was linear in totalPow, which let a PoW farm accumulate a score one to two | ||
| * orders of magnitude above any other dimension. Median Sybil PoW-farm score | ||
| * was ~5,386 vs genuine median 346 — a 16x advantage that collapsed multi-dim | ||
| * AUC from a target ~0.95 down to 0.800. See research/depth-of-identity-hypotheses.md. | ||
| * | ||
| * The fix: log2-scale totalPow so doubling work adds a constant, not a linear | ||
| * increase. Each PoW bit is *already* exponentially costly (NIP-13 — each | ||
| * leading zero bit doubles expected work), so log2 of the bit-sum collapses | ||
| * the grindable advantage while still rewarding genuine PoW. The dimension | ||
| * is then clamped via PER_DIMENSION_SCORE_CAP so even an extreme farm cannot | ||
| * dominate the multi-dim total. | ||
| * | ||
| * Calibration (validated by scripts/test-hdoi1-discrimination.js): | ||
| * genuine totalPow ~50-200 → log2*6 + count_bonus → ~35-55, well below cap | ||
| * farm totalPow ~2000+ → log2*6 + count_bonus → ~70-95, capped at 100 | ||
| * Farm/genuine ratio drops from ~16x to ~2x, restoring discrimination AUC. | ||
| */ | ||
@@ -258,2 +289,9 @@ function scoreAccess(events) { | ||
| // Log2 over both totalPow and event count. Each PoW bit already represents | ||
| // exponential work (NIP-13), so the log term measures order-of-magnitude | ||
| // effort rather than raw bit count. Coefficients calibrated against genuine | ||
| // multi-dim profiles to land in the same 30-90 range as social/vouch/economic. | ||
| const rawScore = Math.log2(totalPow + 1) * 6 + Math.log2(powEventCount + 1) * 2; | ||
| const cappedScore = Math.min(rawScore, PER_DIMENSION_SCORE_CAP); | ||
| return { | ||
@@ -264,5 +302,3 @@ dimension: 'access', | ||
| maxDifficulty, | ||
| // PoW bits are already exponentially costly (each bit doubles work), | ||
| // so we keep totalPow linear but apply log2 to event count | ||
| score: Math.round(totalPow * 2 + Math.log2(powEventCount + 1)), | ||
| score: Math.round(cappedScore), | ||
| }; | ||
@@ -269,0 +305,0 @@ } |
74097
4.59%1208
2.9%167
-0.6%