@steve02081504/fount-p2p 0.0.30 → 0.0.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/AGENTS.md +79 -0
  2. package/docs/evfs.md +32 -0
  3. package/docs/infra.md +25 -0
  4. package/docs/mesh.md +67 -0
  5. package/docs/reputation.md +22 -0
  6. package/docs/runtime.md +62 -0
  7. package/docs/signaling.md +23 -0
  8. package/docs/transports.md +92 -0
  9. package/docs/wire.md +21 -0
  10. package/link/providers/webrtc.mjs +5 -2
  11. package/link/rtc/ice_local_hostname.mjs +15 -47
  12. package/link/rtc/polyfill.mjs +3 -15
  13. package/package.json +2 -9
  14. package/sim/AGENTS.md +29 -0
  15. package/sim/admission.mjs +56 -0
  16. package/sim/anomaly.mjs +39 -0
  17. package/sim/apply.mjs +77 -0
  18. package/sim/attack_space.mjs +113 -0
  19. package/sim/attacks.mjs +614 -0
  20. package/sim/behavior.mjs +78 -0
  21. package/sim/cli.mjs +282 -0
  22. package/sim/coevolution.mjs +245 -0
  23. package/sim/cold_start.md +13 -0
  24. package/sim/constants.mjs +44 -0
  25. package/sim/discovery.mjs +259 -0
  26. package/sim/duration.mjs +64 -0
  27. package/sim/graph_adj.mjs +28 -0
  28. package/sim/integrity.mjs +58 -0
  29. package/sim/metrics.mjs +423 -0
  30. package/sim/model.mjs +1176 -0
  31. package/sim/optimizer.mjs +250 -0
  32. package/sim/propagation.mjs +57 -0
  33. package/sim/report.mjs +176 -0
  34. package/sim/reputation_social.tunables.json +6 -0
  35. package/sim/rng.mjs +62 -0
  36. package/sim/scenarios.mjs +345 -0
  37. package/sim/sim_pool.mjs +169 -0
  38. package/sim/sim_worker.mjs +31 -0
  39. package/sim/social_reputation.mjs +30 -0
  40. package/sim/space.mjs +312 -0
  41. package/sim/test/admission.test.mjs +25 -0
  42. package/sim/test/attack_space.test.mjs +39 -0
  43. package/sim/test/behavior_vector.test.mjs +37 -0
  44. package/sim/test/coevolution.test.mjs +32 -0
  45. package/sim/test/cold_start.test.mjs +46 -0
  46. package/sim/test/defense.test.mjs +320 -0
  47. package/sim/test/discovery.test.mjs +68 -0
  48. package/sim/test/duration.test.mjs +20 -0
  49. package/sim/test/fidelity.test.mjs +151 -0
  50. package/sim/test/integrity.test.mjs +56 -0
  51. package/sim/test/optimizer.test.mjs +24 -0
  52. package/sim/test/propagation.test.mjs +29 -0
  53. package/sim/test/sim_model_regressions.test.mjs +110 -0
  54. package/sim/test/slow_drip.test.mjs +28 -0
  55. package/sim/test/smoke.test.mjs +52 -0
  56. package/sim/test/transport.test.mjs +55 -0
  57. package/sim/test/tunables_apply.test.mjs +64 -0
  58. package/sim/test/tunables_param_space.test.mjs +74 -0
  59. package/sim/test/vulnerability.test.mjs +127 -0
  60. package/sim/transport.mjs +135 -0
  61. package/sim/tunables_bundle.mjs +71 -0
  62. package/sim/vulnerability.mjs +532 -0
  63. package/deno.json +0 -7
  64. package/link/rtc/w3c_bridge.mjs +0 -152
@@ -0,0 +1,345 @@
1
+ /**
2
+ * 预设节点组合场景。
3
+ */
4
+
5
+ /** @typedef {import('./attacks.mjs').AttackKind} AttackKind */
6
+ /** @typedef {import('./behavior.mjs').BehaviorDist} BehaviorDist */
7
+
8
+ /**
9
+ * @typedef {{
10
+ * id: string,
11
+ * label: string,
12
+ * honestCount: number,
13
+ * attacks: Partial<Record<AttackKind, number>>,
14
+ * relayCount?: number,
15
+ * lurkerCount?: number,
16
+ * newcomerCount?: number,
17
+ * behaviorDist?: BehaviorDist,
18
+ * rounds?: number,
19
+ * groupSize?: number,
20
+ * decayWindows?: number,
21
+ * churnRate?: number,
22
+ * offlineRate?: number,
23
+ * eclipseTargetCount?: number,
24
+ * sleeperTurnRound?: number,
25
+ * keyRecoveryRound?: number,
26
+ * coldStartObserver?: boolean,
27
+ * }} SimScenario
28
+ */
29
+
30
+ /** 社交侧偏重 */
31
+ const SOCIAL_HEAVY = Object.freeze({
32
+ postRate: { mean: 0.55, min: 0.25, max: 0.85 },
33
+ likeRate: { mean: 0.45, min: 0.15, max: 0.9 },
34
+ replyRate: { mean: 0.35, min: 0.1, max: 0.7 },
35
+ relayRate: { mean: 0.12, min: 0, max: 0.3 },
36
+ chunkServeRate: { mean: 0.1, min: 0, max: 0.25 },
37
+ })
38
+
39
+ /** 聊天侧偏重 */
40
+ const CHAT_HEAVY = Object.freeze({
41
+ postRate: { mean: 0.12, min: 0, max: 0.25 },
42
+ likeRate: { mean: 0.15, min: 0, max: 0.35 },
43
+ replyRate: { mean: 0.2, min: 0, max: 0.4 },
44
+ relayRate: { mean: 0.55, min: 0.25, max: 0.85 },
45
+ chunkServeRate: { mean: 0.45, min: 0.15, max: 0.75 },
46
+ })
47
+
48
+ /** 静默点赞型 */
49
+ const QUIET_LIKER = Object.freeze({
50
+ postRate: { mean: 0.08, min: 0, max: 0.15 },
51
+ likeRate: { mean: 0.58, min: 0.4, max: 0.85 },
52
+ replyRate: { mean: 0.12, min: 0, max: 0.22 },
53
+ relayRate: { mean: 0.15, min: 0.05, max: 0.3 },
54
+ })
55
+
56
+ /** 均衡混合 */
57
+ const MIXED = Object.freeze({
58
+ postRate: { mean: 0.35, min: 0.1, max: 0.7 },
59
+ likeRate: { mean: 0.3, min: 0.05, max: 0.75 },
60
+ replyRate: { mean: 0.28, min: 0.05, max: 0.6 },
61
+ relayRate: { mean: 0.35, min: 0.1, max: 0.65 },
62
+ })
63
+
64
+ /** @type {SimScenario[]} */
65
+ export const SCENARIOS = [
66
+ {
67
+ id: 'balanced',
68
+ label: '均衡诚实+混合恶意',
69
+ honestCount: 12,
70
+ relayCount: 2,
71
+ lurkerCount: 2,
72
+ newcomerCount: 3,
73
+ behaviorDist: MIXED,
74
+ attacks: {
75
+ sybil: 6, collusion: 5, spammer: 3, false_accuser: 3, eclipse: 3,
76
+ lazy_chunk: 3, social_mob: 3, archive_forger: 3, relay_farmer: 3, hint_poisoner: 3,
77
+ rep_pump: 2,
78
+ },
79
+ rounds: 40,
80
+ groupSize: 8,
81
+ },
82
+ {
83
+ id: 'sybil_heavy',
84
+ label: 'Sybil 洪水',
85
+ honestCount: 8,
86
+ relayCount: 2,
87
+ newcomerCount: 4,
88
+ attacks: { sybil: 20, eclipse: 5, relay_farmer: 5, hint_poisoner: 5, rep_pump: 4 },
89
+ rounds: 50,
90
+ groupSize: 6,
91
+ },
92
+ {
93
+ id: 'collusion_ring',
94
+ label: '邀请链共谋环',
95
+ honestCount: 10,
96
+ relayCount: 1,
97
+ attacks: { collusion: 14, whitewasher: 6, rep_pump: 3 },
98
+ rounds: 45,
99
+ groupSize: 10,
100
+ decayWindows: 6,
101
+ },
102
+ {
103
+ id: 'social_war',
104
+ label: 'Social 群体拉黑',
105
+ honestCount: 10,
106
+ behaviorDist: SOCIAL_HEAVY,
107
+ attacks: { social_mob: 8, false_accuser: 4, report_flooder: 3 },
108
+ rounds: 35,
109
+ groupSize: 8,
110
+ decayWindows: 6,
111
+ },
112
+ {
113
+ id: 'spam_eclipse',
114
+ label: '刷消息 + eclipse',
115
+ honestCount: 10,
116
+ lurkerCount: 2,
117
+ behaviorDist: CHAT_HEAVY,
118
+ attacks: { spammer: 8, eclipse: 8, lazy_chunk: 5, oscillator: 4, rep_pump: 2 },
119
+ rounds: 40,
120
+ groupSize: 6,
121
+ },
122
+ {
123
+ id: 'relay_mesh',
124
+ label: 'subfount 中转网格',
125
+ honestCount: 6,
126
+ relayCount: 8,
127
+ lurkerCount: 2,
128
+ behaviorDist: CHAT_HEAVY,
129
+ attacks: { relay_farmer: 4, sybil: 3, eclipse: 2 },
130
+ rounds: 45,
131
+ groupSize: 10,
132
+ },
133
+ {
134
+ id: 'usage_mix',
135
+ label: '使用画像混合',
136
+ honestCount: 14,
137
+ relayCount: 2,
138
+ lurkerCount: 2,
139
+ newcomerCount: 3,
140
+ behaviorDist: {
141
+ ...MIXED,
142
+ postRate: { mean: 0.25, min: 0.05, max: 0.55 },
143
+ likeRate: { mean: 0.4, min: 0.15, max: 0.75 },
144
+ },
145
+ attacks: { social_mob: 3, lazy_chunk: 4, archive_forger: 3, false_accuser: 3, hint_poisoner: 3, equivocator: 2 },
146
+ rounds: 40,
147
+ groupSize: 8,
148
+ },
149
+ {
150
+ id: 'quiet_honest',
151
+ label: '静默点赞用户',
152
+ honestCount: 12,
153
+ behaviorDist: QUIET_LIKER,
154
+ attacks: { false_accuser: 4, social_mob: 3, spammer: 2 },
155
+ rounds: 35,
156
+ groupSize: 8,
157
+ },
158
+ {
159
+ id: 'churn_storm',
160
+ label: '高 churn + 掉线风暴',
161
+ honestCount: 10,
162
+ relayCount: 4,
163
+ lurkerCount: 2,
164
+ attacks: { sybil: 3, eclipse: 3, relay_farmer: 2 },
165
+ rounds: 45,
166
+ groupSize: 8,
167
+ churnRate: 0.22,
168
+ offlineRate: 0.15,
169
+ },
170
+ {
171
+ id: 'key_compromise',
172
+ label: '高信誉身份被盗',
173
+ honestCount: 10,
174
+ relayCount: 2,
175
+ attacks: { key_thief: 4, archive_forger: 3, spammer: 3, equivocator: 2 },
176
+ rounds: 40,
177
+ groupSize: 8,
178
+ },
179
+ {
180
+ id: 'sleeper_turn',
181
+ label: '肉鸡突变',
182
+ honestCount: 10,
183
+ relayCount: 2,
184
+ attacks: { sleeper: 4, collusion: 2 },
185
+ rounds: 45,
186
+ groupSize: 8,
187
+ sleeperTurnRound: 12,
188
+ },
189
+ {
190
+ id: 'digest_equivocation',
191
+ label: '归档 digest 等价欺骗',
192
+ honestCount: 8,
193
+ relayCount: 2,
194
+ attacks: { equivocator: 6, archive_forger: 3 },
195
+ rounds: 35,
196
+ groupSize: 6,
197
+ },
198
+ {
199
+ id: 'eclipse_targeted',
200
+ label: '定向 eclipse 分区',
201
+ honestCount: 10,
202
+ relayCount: 2,
203
+ attacks: { targeted_eclipse: 5, eclipse: 3, rep_pump: 4 },
204
+ rounds: 40,
205
+ groupSize: 8,
206
+ eclipseTargetCount: 3,
207
+ churnRate: 0.12,
208
+ offlineRate: 0.08,
209
+ },
210
+ {
211
+ id: 'key_recovery',
212
+ label: 'recovery 钥吊销被盗活跃钥',
213
+ honestCount: 10,
214
+ relayCount: 2,
215
+ attacks: { key_thief: 4, spammer: 2 },
216
+ rounds: 40,
217
+ groupSize: 8,
218
+ keyRecoveryRound: 18,
219
+ },
220
+ {
221
+ id: 'sleeper_anomaly',
222
+ label: '肉鸡突变 + 异常隔离',
223
+ honestCount: 10,
224
+ relayCount: 2,
225
+ attacks: { sleeper: 5, collusion: 2 },
226
+ rounds: 45,
227
+ groupSize: 8,
228
+ sleeperTurnRound: 10,
229
+ },
230
+ {
231
+ id: 'social_mob_cascade',
232
+ label: 'Social 群体拉黑级联',
233
+ honestCount: 12,
234
+ behaviorDist: SOCIAL_HEAVY,
235
+ attacks: { social_mob: 6, false_accuser: 3, hint_poisoner: 2 },
236
+ rounds: 35,
237
+ groupSize: 8,
238
+ },
239
+ {
240
+ id: 'large_balanced',
241
+ label: '大群均衡(30 诚实)',
242
+ honestCount: 30,
243
+ relayCount: 4,
244
+ lurkerCount: 4,
245
+ newcomerCount: 6,
246
+ behaviorDist: MIXED,
247
+ attacks: {
248
+ sybil: 8, collusion: 6, spammer: 4, false_accuser: 3, eclipse: 4,
249
+ lazy_chunk: 4, social_mob: 3, archive_forger: 3, relay_farmer: 3, hint_poisoner: 3,
250
+ rep_pump: 3, equivocator: 2,
251
+ },
252
+ rounds: 45,
253
+ groupSize: 12,
254
+ },
255
+ {
256
+ id: 'large_sybil_storm',
257
+ label: '大群 Sybil 风暴(40 诚实)',
258
+ honestCount: 40,
259
+ relayCount: 6,
260
+ newcomerCount: 8,
261
+ attacks: { sybil: 24, eclipse: 8, relay_farmer: 6, hint_poisoner: 6, rep_pump: 5, whitewasher: 4 },
262
+ rounds: 50,
263
+ groupSize: 14,
264
+ churnRate: 0.1,
265
+ },
266
+ {
267
+ id: 'slow_drip_spam',
268
+ label: '慢速 drip 刷屏',
269
+ honestCount: 10,
270
+ behaviorDist: CHAT_HEAVY,
271
+ attacks: { slow_drip_spammer: 6, spammer: 2 },
272
+ rounds: 40,
273
+ groupSize: 8,
274
+ },
275
+ {
276
+ id: 'transport_siege',
277
+ label: '信令 join 洪水',
278
+ honestCount: 10,
279
+ relayCount: 2,
280
+ behaviorDist: CHAT_HEAVY,
281
+ attacks: { signaling_flood: 8, eclipse: 3 },
282
+ rounds: 40,
283
+ groupSize: 8,
284
+ },
285
+ {
286
+ id: 'tiny_group',
287
+ label: '小群归档(3 人)',
288
+ honestCount: 4,
289
+ relayCount: 1,
290
+ behaviorDist: { ...MIXED, archiveSubmitRate: { mean: 0.35, min: 0.15, max: 0.6 } },
291
+ attacks: { archive_forger: 2, equivocator: 1 },
292
+ rounds: 30,
293
+ groupSize: 3,
294
+ },
295
+ {
296
+ id: 'cold_start',
297
+ label: 'K=0 冷启动观察者',
298
+ honestCount: 10,
299
+ relayCount: 2,
300
+ attacks: { sybil: 2, eclipse: 1 },
301
+ rounds: 20,
302
+ groupSize: 6,
303
+ coldStartObserver: true,
304
+ },
305
+ {
306
+ id: 'pair_group',
307
+ label: '双人群(2 人)',
308
+ honestCount: 2,
309
+ behaviorDist: MIXED,
310
+ attacks: { archive_forger: 1 },
311
+ rounds: 25,
312
+ groupSize: 2,
313
+ },
314
+ {
315
+ id: 'honest_burst',
316
+ label: '诚实突发限速误触',
317
+ honestCount: 10,
318
+ behaviorDist: {
319
+ ...CHAT_HEAVY,
320
+ burstPostRate: { mean: 0.25, min: 0.1, max: 0.45 },
321
+ },
322
+ attacks: { spammer: 3 },
323
+ rounds: 35,
324
+ groupSize: 8,
325
+ },
326
+ {
327
+ id: 'fresh_join_eclipse',
328
+ label: '新节点信令 eclipse',
329
+ honestCount: 8,
330
+ newcomerCount: 4,
331
+ attacks: { signaling_eclipse: 6, targeted_eclipse: 3 },
332
+ rounds: 35,
333
+ groupSize: 6,
334
+ },
335
+ ]
336
+
337
+ /**
338
+ * @param {string} [id] 场景 id(`all` 或省略返回全部)
339
+ * @returns {SimScenario[]} 匹配的场景列表
340
+ */
341
+ export function resolveScenarios(id) {
342
+ if (!id || id === 'all') return SCENARIOS
343
+ const one = SCENARIOS.find(s => s.id === id)
344
+ return one ? [one] : SCENARIOS
345
+ }
@@ -0,0 +1,169 @@
1
+ /**
2
+ * 仿真任务 Worker 池:常驻 Worker 跨评估复用。
3
+ */
4
+ /* global Deno */
5
+ import { runSimulation } from './model.mjs'
6
+ import { resolveScenarios } from './scenarios.mjs'
7
+
8
+ /**
9
+ * @typedef {{
10
+ * id: number,
11
+ * scenarioId: string,
12
+ * seed: number,
13
+ * tunables: import('./tunables_bundle.mjs').TunablesBundle,
14
+ * attackGenome?: import('./attack_space.mjs').AttackGenome,
15
+ * }} SimJob
16
+ */
17
+
18
+ /**
19
+ * @typedef {{
20
+ * id: number,
21
+ * snapshot?: import('./metrics.mjs').SimSnapshot,
22
+ * error?: string,
23
+ * }} SimJobResult
24
+ */
25
+
26
+ /**
27
+ * @typedef {{
28
+ * worker: Worker,
29
+ * busy: boolean,
30
+ * }} PoolSlot
31
+ */
32
+
33
+ /** @type {PoolSlot[] | null} */
34
+ let poolSlots = null
35
+
36
+ const workerUrl = new URL('./sim_worker.mjs', import.meta.url)
37
+
38
+ /**
39
+ * 默认 Worker 并发度:尽量占满逻辑 CPU(无人工上限)。
40
+ * @returns {number} 并发 Worker 数
41
+ */
42
+ export function defaultConcurrency() {
43
+ let cores = 0
44
+ if (typeof Deno !== 'undefined' && typeof Deno.systemCpuInfo === 'function')
45
+ try {
46
+ cores = Deno.systemCpuInfo().cores ?? 0
47
+ }
48
+ catch { /* 非 Deno 或权限不足 */ }
49
+
50
+ const hw = typeof navigator !== 'undefined' && navigator.hardwareConcurrency
51
+ if (hw) cores = Math.max(cores, hw)
52
+ return Math.max(1, cores || 4)
53
+ }
54
+
55
+ /**
56
+ * 按曾请求过的最大并发度扩容常驻 Worker 池。
57
+ * @param {number} size 本次请求并发度
58
+ * @returns {PoolSlot[] | null} 池槽位;无 Worker 环境时 null
59
+ */
60
+ function ensurePool(size) {
61
+ if (typeof Worker === 'undefined') return null
62
+ const target = Math.max(1, size)
63
+ if (!poolSlots) poolSlots = []
64
+ while (poolSlots.length < target)
65
+ poolSlots.push({ worker: new Worker(workerUrl, { type: 'module' }), busy: false })
66
+ return poolSlots
67
+ }
68
+
69
+ /**
70
+ * 终止所有常驻 Worker 并清空池(CLI / 测试收尾时调用)。
71
+ * @returns {void}
72
+ */
73
+ export function shutdownSimPool() {
74
+ if (!poolSlots) return
75
+ for (const slot of poolSlots) slot.worker.terminate()
76
+ poolSlots = null
77
+ }
78
+
79
+ /**
80
+ * @param {SimJob} job 仿真任务
81
+ * @returns {SimJobResult} 快照结果
82
+ */
83
+ function runJobLocal(job) {
84
+ const scenario = resolveScenarios(job.scenarioId)[0]
85
+ if (!scenario)
86
+ return { id: job.id, error: `unknown scenario: ${job.scenarioId}` }
87
+ try {
88
+ return {
89
+ id: job.id,
90
+ snapshot: runSimulation(scenario, job.seed, job.tunables, job.attackGenome),
91
+ }
92
+ }
93
+ catch (error) {
94
+ return { id: job.id, error: String(error) }
95
+ }
96
+ }
97
+
98
+ /**
99
+ * @param {SimJob[]} jobs 任务列表
100
+ * @param {{ concurrency?: number }} [options] 选项
101
+ * @returns {Promise<SimJobResult[]>} 与 jobs 同序的结果
102
+ */
103
+ export async function runSimulationJobs(jobs, options = {}) {
104
+ if (!jobs.length) return []
105
+
106
+ const concurrency = options.concurrency ?? defaultConcurrency()
107
+ if (concurrency <= 1 || typeof Worker === 'undefined')
108
+ return jobs.map(runJobLocal)
109
+
110
+ const slots = ensurePool(concurrency)
111
+ if (!slots) return jobs.map(runJobLocal)
112
+
113
+ const activeCount = Math.min(concurrency, jobs.length, slots.length)
114
+ /** @type {PoolSlot[]} */
115
+ const batchSlots = slots.slice(0, activeCount)
116
+
117
+ /** @type {SimJobResult[]} */
118
+ const results = new Array(jobs.length)
119
+ let nextJob = 0
120
+ let settled = 0
121
+ let failed = false
122
+
123
+ return new Promise((resolve, reject) => {
124
+ /**
125
+ * @param {number} slotIndex 槽位下标
126
+ * @returns {void}
127
+ */
128
+ function dispatch(slotIndex) {
129
+ if (failed || nextJob >= jobs.length) return
130
+ const slot = batchSlots[slotIndex]
131
+ if (slot.busy) return
132
+
133
+ const index = nextJob++
134
+ const job = { ...jobs[index], id: index }
135
+ slot.busy = true
136
+
137
+ /**
138
+ * @param {MessageEvent<SimJobResult>} event 工作线程消息
139
+ */
140
+ function onWorkerMessage(event) {
141
+ results[index] = event.data
142
+ settled++
143
+ slot.busy = false
144
+ slot.worker.onmessage = null
145
+ slot.worker.onerror = null
146
+
147
+ if (settled >= jobs.length) {
148
+ resolve(results)
149
+ return
150
+ }
151
+ for (let i = 0; i < batchSlots.length; i++) dispatch(i)
152
+ }
153
+
154
+ /**
155
+ * @param {ErrorEvent} errorEvent Worker 错误
156
+ */
157
+ function onWorkerError(errorEvent) {
158
+ failed = true
159
+ reject(errorEvent)
160
+ }
161
+
162
+ slot.worker.onmessage = onWorkerMessage
163
+ slot.worker.onerror = onWorkerError
164
+ slot.worker.postMessage(job)
165
+ }
166
+
167
+ for (let i = 0; i < activeCount; i++) dispatch(i)
168
+ })
169
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Deno Worker:在独立线程中运行单次仿真。
3
+ */
4
+ import { runSimulation } from './model.mjs'
5
+ import { resolveScenarios } from './scenarios.mjs'
6
+
7
+ /**
8
+ * @typedef {{
9
+ * id: number,
10
+ * scenarioId: string,
11
+ * seed: number,
12
+ * tunables: import('./tunables_bundle.mjs').TunablesBundle,
13
+ * attackGenome?: import('./attack_space.mjs').AttackGenome,
14
+ * }} SimWorkerJob
15
+ */
16
+
17
+ self.addEventListener('message', (/** @type {MessageEvent<SimWorkerJob>} */ event) => {
18
+ const job = event.data
19
+ const scenario = resolveScenarios(job.scenarioId)[0]
20
+ if (!scenario) {
21
+ self.postMessage({ id: job.id, error: `unknown scenario: ${job.scenarioId}` })
22
+ return
23
+ }
24
+ try {
25
+ const snapshot = runSimulation(scenario, job.seed, job.tunables, job.attackGenome)
26
+ self.postMessage({ id: job.id, snapshot })
27
+ }
28
+ catch (error) {
29
+ self.postMessage({ id: job.id, error: String(error) })
30
+ }
31
+ })
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Social follow-block 信誉信号(仿真用纯函数;生产逻辑在 social shell)。
3
+ * @param {import('../node/reputation_store.mjs').ReputationFile} reputation 节点信誉文件
4
+ * @param {{ followerNodeHash: string, targetNodeHash: string, voterKey: string, action: 'block' | 'unblock', selfTrust?: boolean }} signal follow-block 信号
5
+ * @param {number} now 当前时间戳(毫秒)
6
+ * @param {{ followedBlockSelfTrustPenalty?: number, followedBlockPenalty?: number }} tunables 惩罚系数
7
+ * @returns {void}
8
+ */
9
+ export function applyFollowedBlockSignalPure(reputation, signal, now, tunables = {}) {
10
+ const { targetNodeHash, voterKey, action, selfTrust = false } = signal
11
+ if (!targetNodeHash || !voterKey) return
12
+ if (!reputation.byNodeHash[targetNodeHash])
13
+ reputation.byNodeHash[targetNodeHash] = { score: 0 }
14
+ const row = reputation.byNodeHash[targetNodeHash]
15
+ if (!row.blockPenalties) row.blockPenalties = {}
16
+ if (action === 'block') {
17
+ if (row.blockPenalties[voterKey]) return
18
+ const penalty = selfTrust
19
+ ? tunables.followedBlockSelfTrustPenalty ?? 0.12
20
+ : tunables.followedBlockPenalty ?? 0.06
21
+ row.score = (row.score ?? 0) - penalty
22
+ row.blockPenalties[voterKey] = { penalty, at: now }
23
+ }
24
+ else if (action === 'unblock') {
25
+ const entry = row.blockPenalties[voterKey]
26
+ if (!entry) return
27
+ row.score = (row.score ?? 0) + entry.penalty
28
+ delete row.blockPenalties[voterKey]
29
+ }
30
+ }