@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,614 @@
1
+ /**
2
+ * 恶意节点行为原型。
3
+ */
4
+ import { resolveAttackParams } from './attack_space.mjs'
5
+ import { SYBIL_REP_EARN_COST_ROUNDS } from './constants.mjs'
6
+ import { eclipseFillExplore } from './discovery.mjs'
7
+ import { enqueueSlash } from './propagation.mjs'
8
+ import { takeTransportJoinSlot, transportHintWeight } from './transport.mjs'
9
+
10
+ /** @typedef {'sybil' | 'collusion' | 'spammer' | 'false_accuser' | 'eclipse' | 'lazy_chunk' | 'social_mob' | 'archive_forger' | 'relay_farmer' | 'whitewasher' | 'report_flooder' | 'oscillator' | 'hint_poisoner' | 'key_thief' | 'sleeper' | 'equivocator' | 'targeted_eclipse' | 'rep_pump' | 'slow_drip_spammer' | 'signaling_flood' | 'signaling_eclipse'} AttackKind */
11
+
12
+ /**
13
+ * @param {object} simulationContext 仿真上下文
14
+ * @param {import('./model.mjs').SimNode} node 恶意节点
15
+ * @returns {ReturnType<typeof resolveAttackParams>} 有效攻击参数
16
+ */
17
+ function attackParams(simulationContext, node) {
18
+ return resolveAttackParams(node.attack ?? 'sybil', simulationContext.attackGenome)
19
+ }
20
+
21
+ /**
22
+ * @param {object} simulationContext 仿真上下文
23
+ * @param {import('./model.mjs').SimObserver} observer 观察者
24
+ * @param {string} targetId 目标
25
+ * @param {string} senderId 发送者
26
+ * @param {number} claim 索赔
27
+ * @param {boolean} verified 是否已验证
28
+ * @param {number} round 回合
29
+ * @param {import('./tunables_bundle.mjs').TunablesBundle} tunables 参数
30
+ * @returns {void}
31
+ */
32
+ function queueSlash(simulationContext, observer, targetId, senderId, claim, verified, round, tunables) {
33
+ const state = simulationContext.propagationByObserver?.get(observer.id)
34
+ if (state) {
35
+ enqueueSlash(state, { targetId, senderId, claim, verified, birthRound: round, spread: 0 })
36
+ return
37
+ }
38
+ simulationContext.engine.applySubjectiveSlashPure(observer.reputation, targetId, senderId, claim, verified, tunables.reputation)
39
+ }
40
+
41
+ /**
42
+ * 第一人称:观察者仅因**直接观测到**该 peer 的中继行为而加分。
43
+ * @param {object} simulationContext 仿真上下文
44
+ * @param {import('./model.mjs').SimNode} node 被观测 peer
45
+ * @param {import('./model.mjs').SimObserver} observer 观察者
46
+ * @param {() => number} rng 随机源
47
+ * @param {number} rate 触发概率
48
+ * @param {string} dedupeKey 去重键
49
+ * @param {import('./tunables_bundle.mjs').TunablesBundle} tunables 参数
50
+ * @returns {void}
51
+ */
52
+ function observeSelfRelayBump(simulationContext, node, observer, rng, rate, dedupeKey, tunables) {
53
+ if (rng() >= rate) return
54
+ simulationContext.engine.bumpReputationOnRelayPure(
55
+ observer.reputation,
56
+ node.id,
57
+ dedupeKey,
58
+ simulationContext.now,
59
+ tunables.reputation,
60
+ )
61
+ }
62
+
63
+ /**
64
+ * 同伙互吹:经 hint 发现通道间接抬分,收益随 sender 主观信誉折扣(低信誉 sender 的 hint 几乎无效)。
65
+ * @param {object} simulationContext 仿真上下文
66
+ * @param {import('./model.mjs').SimObserver} observer 观察者
67
+ * @param {string} allyId 被抬分目标
68
+ * @param {string} senderId 声称来源
69
+ * @param {number} weightMul 相对 hintDefaultWeight 的倍数
70
+ * @param {import('./tunables_bundle.mjs').TunablesBundle} tunables 参数
71
+ * @returns {void}
72
+ */
73
+ function injectAllyHint(simulationContext, observer, allyId, senderId, weightMul, tunables) {
74
+ const senderRep = observer.reputation.byNodeHash[senderId]?.score ?? 0
75
+ const senderTrust = Math.max(0.05, (senderRep + 1) / 2)
76
+ const weight = tunables.trustGraph.hintDefaultWeight * weightMul * senderTrust * 0.35
77
+ const existing = observer.injectedHints.find(h => h.nodeHash === allyId && h.source === `ally:${senderId}`)
78
+ if (existing)
79
+ existing.weight = (existing.weight ?? weight) + weight
80
+ else
81
+ observer.injectedHints.push({ nodeHash: allyId, source: `ally:${senderId}`, weight })
82
+ }
83
+
84
+ /**
85
+ * @param {object} simulationContext 仿真上下文
86
+ * @param {import('./model.mjs').SimNode} node 恶意节点
87
+ * @param {import('./model.mjs').SimObserver} observer 诚实观察者
88
+ * @param {() => number} rng 随机源
89
+ * @param {number} round 当前回合
90
+ * @param {import('./tunables_bundle.mjs').TunablesBundle} tunables 候选参数
91
+ * @returns {void}
92
+ */
93
+ export function runAttack(simulationContext, node, observer, rng, round, tunables) {
94
+ switch (node.attack) {
95
+ case 'sybil':
96
+ runSybil(simulationContext, node, observer, rng, tunables)
97
+ break
98
+ case 'collusion':
99
+ runCollusion(simulationContext, node, observer, rng, round, tunables)
100
+ break
101
+ case 'spammer':
102
+ runSpammer(simulationContext, node, observer, tunables)
103
+ break
104
+ case 'false_accuser':
105
+ runFalseAccuser(simulationContext, node, observer, rng, round, tunables)
106
+ break
107
+ case 'eclipse':
108
+ runEclipse(simulationContext, node, observer, rng, round, tunables)
109
+ break
110
+ case 'lazy_chunk':
111
+ runLazyChunk(simulationContext, node, observer, tunables)
112
+ break
113
+ case 'social_mob':
114
+ runSocialMob(simulationContext, node, observer, rng, tunables)
115
+ break
116
+ case 'archive_forger':
117
+ runArchiveForger(simulationContext, node, observer, tunables)
118
+ break
119
+ case 'relay_farmer':
120
+ runRelayFarmer(simulationContext, node, observer, round, tunables)
121
+ break
122
+ case 'whitewasher':
123
+ runWhitewasher(simulationContext, node, observer, rng, round, tunables)
124
+ break
125
+ case 'report_flooder':
126
+ runReportFlooder(simulationContext, node, observer, rng, round, tunables)
127
+ break
128
+ case 'oscillator':
129
+ runOscillator(simulationContext, node, observer, round, tunables)
130
+ break
131
+ case 'hint_poisoner':
132
+ runHintPoisoner(simulationContext, node, observer, tunables)
133
+ break
134
+ case 'key_thief':
135
+ runKeyThief(simulationContext, node, observer, rng, round, tunables)
136
+ break
137
+ case 'sleeper':
138
+ runSleeper(simulationContext, node, observer, rng, round, tunables)
139
+ break
140
+ case 'equivocator':
141
+ runEquivocator(simulationContext, node, observer, tunables)
142
+ break
143
+ case 'targeted_eclipse':
144
+ runTargetedEclipse(simulationContext, node, observer, rng, round, tunables)
145
+ break
146
+ case 'rep_pump':
147
+ runRepPump(simulationContext, node, observer, rng, round, tunables)
148
+ break
149
+ case 'slow_drip_spammer':
150
+ runSlowDripSpammer(simulationContext, node, observer, round, tunables)
151
+ break
152
+ case 'signaling_flood':
153
+ runSignalingFlood(simulationContext, node, observer, round, tunables)
154
+ break
155
+ case 'signaling_eclipse':
156
+ runSignalingEclipse(simulationContext, node, observer, tunables)
157
+ break
158
+ default:
159
+ break
160
+ }
161
+ }
162
+
163
+ /**
164
+ * @param {object} simulationContext 仿真上下文
165
+ * @param {import('./model.mjs').SimNode} node 恶意节点
166
+ * @param {import('./model.mjs').SimObserver} observer 诚实观察者
167
+ * @param {() => number} rng 随机源
168
+ * @param {import('./tunables_bundle.mjs').TunablesBundle} tunables 候选参数
169
+ * @returns {void}
170
+ */
171
+ function runSybil(simulationContext, node, observer, rng, tunables) {
172
+ if ((simulationContext.round ?? 0) < SYBIL_REP_EARN_COST_ROUNDS) return
173
+ const p = attackParams(simulationContext, node)
174
+ observeSelfRelayBump(simulationContext, node, observer, rng, p.activationRate, `sybil:${node.id}`, tunables)
175
+ for (const sybil of simulationContext.sybilCluster(node))
176
+ if (sybil.id !== node.id && rng() < p.activationRate * p.collusionAllyRate)
177
+ injectAllyHint(simulationContext, observer, sybil.id, node.id, 0.58, tunables)
178
+ }
179
+
180
+ /**
181
+ * @param {object} simulationContext 仿真上下文
182
+ * @param {import('./model.mjs').SimNode} node 恶意节点
183
+ * @param {import('./model.mjs').SimObserver} observer 诚实观察者
184
+ * @param {() => number} rng 随机源
185
+ * @param {number} round 当前回合
186
+ * @param {import('./tunables_bundle.mjs').TunablesBundle} tunables 候选参数
187
+ * @returns {void}
188
+ */
189
+ function runCollusion(simulationContext, node, observer, rng, round, tunables) {
190
+ const p = attackParams(simulationContext, node)
191
+ observeSelfRelayBump(simulationContext, node, observer, rng, p.activationRate, `collusion:${round}:${node.id}`, tunables)
192
+ const ring = simulationContext.collusionRing(node)
193
+ for (const ally of ring)
194
+ if (ally.id !== node.id && rng() < p.collusionAllyRate)
195
+ injectAllyHint(simulationContext, observer, ally.id, node.id, p.collusionAllyRate, tunables)
196
+ const victim = ring.find(n => n.id !== node.id)
197
+ if (victim && rng() < p.activationRate * 0.4)
198
+ queueSlash(simulationContext, observer, victim.id, node.id, tunables.reputation.slashUnverifiedDefaultClaim, false, round, tunables)
199
+ }
200
+
201
+ /**
202
+ * @param {object} simulationContext 仿真上下文
203
+ * @param {import('./model.mjs').SimNode} node 恶意节点
204
+ * @param {import('./model.mjs').SimObserver} observer 诚实观察者
205
+ * @param {import('./tunables_bundle.mjs').TunablesBundle} tunables 候选参数
206
+ * @returns {void}
207
+ */
208
+ function runSpammer(simulationContext, node, observer, tunables) {
209
+ const { recordMessageRateViolationPure } = simulationContext.engine
210
+ const burst = attackParams(simulationContext, node).burstSize
211
+ for (let i = 0; i < burst; i++)
212
+ recordMessageRateViolationPure(observer.reputation, node.id, tunables.reputation)
213
+ }
214
+
215
+ /**
216
+ * @param {object} simulationContext 仿真上下文
217
+ * @param {import('./model.mjs').SimNode} node 恶意节点
218
+ * @param {import('./model.mjs').SimObserver} observer 诚实观察者
219
+ * @param {() => number} rng 随机源
220
+ * @param {number} round 当前回合
221
+ * @param {import('./tunables_bundle.mjs').TunablesBundle} tunables 候选参数
222
+ * @returns {void}
223
+ */
224
+ function runFalseAccuser(simulationContext, node, observer, rng, round, tunables) {
225
+ const honest = pickHonestTarget(simulationContext, observer, rng, attackParams(simulationContext, node).targetBiasHighRep)
226
+ if (!honest) return
227
+ queueSlash(
228
+ simulationContext,
229
+ observer,
230
+ honest.id,
231
+ node.id,
232
+ tunables.reputation.slashUnverifiedDefaultClaim * 2,
233
+ false,
234
+ round,
235
+ tunables,
236
+ )
237
+ }
238
+
239
+ /**
240
+ * @param {object} simulationContext 仿真上下文
241
+ * @param {import('./model.mjs').SimNode} node 恶意节点
242
+ * @param {import('./model.mjs').SimObserver} observer 诚实观察者
243
+ * @param {() => number} rng 随机源
244
+ * @param {number} round 当前回合
245
+ * @param {import('./tunables_bundle.mjs').TunablesBundle} tunables 候选参数
246
+ * @returns {void}
247
+ */
248
+ function runEclipse(simulationContext, node, observer, rng, round, tunables) {
249
+ const { recordGossipAllUnknownWantPure } = simulationContext.engine
250
+ const p = attackParams(simulationContext, node)
251
+ for (let i = 0; i < p.burstSize; i++)
252
+ recordGossipAllUnknownWantPure(observer.reputation, node.id, simulationContext.now + i, tunables.reputation)
253
+ const clusterIds = simulationContext.eclipseCluster(node).map(n => n.id).filter(id => id !== node.id)
254
+ eclipseFillExplore(simulationContext.discovery, observer.id, node.id, clusterIds, p.eclipseFocus)
255
+ }
256
+
257
+ /**
258
+ * @param {object} simulationContext 仿真上下文
259
+ * @param {import('./model.mjs').SimNode} node 恶意节点
260
+ * @param {import('./model.mjs').SimObserver} observer 诚实观察者
261
+ * @param {import('./tunables_bundle.mjs').TunablesBundle} tunables 候选参数
262
+ * @returns {void}
263
+ */
264
+ function runLazyChunk(simulationContext, node, observer, tunables) {
265
+ const { penalizeChunkStorageFailurePure } = simulationContext.engine
266
+ penalizeChunkStorageFailurePure(observer.reputation, node.id, tunables.reputation)
267
+ }
268
+
269
+ /**
270
+ * @param {object} simulationContext 仿真上下文
271
+ * @param {import('./model.mjs').SimNode} node 恶意节点
272
+ * @param {import('./model.mjs').SimObserver} observer 诚实观察者
273
+ * @param {() => number} rng 随机源
274
+ * @param {import('./tunables_bundle.mjs').TunablesBundle} tunables 候选参数
275
+ * @returns {void}
276
+ */
277
+ function runSocialMob(simulationContext, node, observer, rng, tunables) {
278
+ const { applyFollowedBlockSignalPure } = simulationContext.socialEngine
279
+ const honest = pickHonestTarget(simulationContext, observer, rng)
280
+ if (!honest) return
281
+ applyFollowedBlockSignalPure(
282
+ observer.reputation,
283
+ {
284
+ followerNodeHash: node.id,
285
+ targetNodeHash: honest.id,
286
+ voterKey: `${node.id}entity`,
287
+ action: 'block',
288
+ selfTrust: false,
289
+ },
290
+ simulationContext.now,
291
+ tunables.social,
292
+ )
293
+ }
294
+
295
+ /**
296
+ * @param {object} simulationContext 仿真上下文
297
+ * @param {import('./model.mjs').SimNode} node 恶意节点
298
+ * @param {import('./model.mjs').SimObserver} observer 诚实观察者
299
+ * @param {import('./tunables_bundle.mjs').TunablesBundle} tunables 候选参数
300
+ * @returns {void}
301
+ */
302
+ function runArchiveForger(simulationContext, node, observer, tunables) {
303
+ const { penalizeArchiveServeMismatchPure } = simulationContext.engine
304
+ penalizeArchiveServeMismatchPure(observer.reputation, node.id, tunables.reputation)
305
+ }
306
+
307
+ /**
308
+ * @param {object} simulationContext 仿真上下文
309
+ * @param {import('./model.mjs').SimNode} node 恶意节点
310
+ * @param {import('./model.mjs').SimObserver} observer 诚实观察者
311
+ * @param {number} round 当前回合
312
+ * @param {import('./tunables_bundle.mjs').TunablesBundle} tunables 候选参数
313
+ * @returns {void}
314
+ */
315
+ function runRelayFarmer(simulationContext, node, observer, round, tunables) {
316
+ const { bumpReputationOnRelayPure } = simulationContext.engine
317
+ const burst = attackParams(simulationContext, node).burstSize
318
+ for (let i = 0; i < burst; i++)
319
+ bumpReputationOnRelayPure(observer.reputation, node.id, `relay-farm:${round}:${i}`, simulationContext.now + i, tunables.reputation)
320
+ }
321
+
322
+ /**
323
+ * @param {object} simulationContext 仿真上下文
324
+ * @param {import('./model.mjs').SimNode} node 恶意节点
325
+ * @param {import('./model.mjs').SimObserver} observer 诚实观察者
326
+ * @param {() => number} rng 随机源
327
+ * @param {number} round 当前回合
328
+ * @param {import('./tunables_bundle.mjs').TunablesBundle} tunables 候选参数
329
+ * @returns {void}
330
+ */
331
+ function runWhitewasher(simulationContext, node, observer, rng, round, tunables) {
332
+ const { applySubjectiveSlashPure, seedMemberReputationFromIntroducerPure } = simulationContext.engine
333
+ const stage = node.whitewashStage ?? 0
334
+ if (stage === 0 && round >= 4) {
335
+ applySubjectiveSlashPure(
336
+ observer.reputation,
337
+ node.id,
338
+ observer.id,
339
+ tunables.reputation.slashUnverifiedDefaultClaim,
340
+ false,
341
+ tunables.reputation,
342
+ )
343
+ node.whitewashStage = 1
344
+ }
345
+ else if (stage === 1 && round % 7 === 0) {
346
+ const intro = pickHonestTarget(simulationContext, observer, rng)
347
+ if (intro) {
348
+ // 用 tunable introducerSeedEdge(不再写死 0.6):边权越低,洗白者重新入场时
349
+ // 从诚实介绍者继承的信誉越少,越难「洗白复活」。
350
+ delete observer.reputation.byNodeHash[node.id]
351
+ seedMemberReputationFromIntroducerPure(observer.reputation, node.id, intro.id, undefined, tunables.reputation)
352
+ node.whitewashStage = 2
353
+ }
354
+ }
355
+ }
356
+
357
+ /**
358
+ * @param {object} simulationContext 仿真上下文
359
+ * @param {import('./model.mjs').SimNode} node 恶意节点
360
+ * @param {import('./model.mjs').SimObserver} observer 诚实观察者
361
+ * @param {() => number} rng 随机源
362
+ * @param {number} round 当前回合
363
+ * @param {import('./tunables_bundle.mjs').TunablesBundle} tunables 候选参数
364
+ * @returns {void}
365
+ */
366
+ function runReportFlooder(simulationContext, node, observer, rng, round, tunables) {
367
+ const targets = simulationContext.honestNodes.filter(n => n.id !== observer.id)
368
+ const burst = Math.min(attackParams(simulationContext, node).burstSize, targets.length)
369
+ for (let i = 0; i < burst; i++) {
370
+ const target = targets[Math.floor(rng() * targets.length)]
371
+ if (!target) continue
372
+ queueSlash(
373
+ simulationContext,
374
+ observer,
375
+ target.id,
376
+ node.id,
377
+ tunables.reputation.slashUnverifiedDefaultClaim * 1.5,
378
+ false,
379
+ round,
380
+ tunables,
381
+ )
382
+ }
383
+ }
384
+
385
+ /**
386
+ * @param {object} simulationContext 仿真上下文
387
+ * @param {import('./model.mjs').SimNode} node 恶意节点
388
+ * @param {import('./model.mjs').SimObserver} observer 诚实观察者
389
+ * @param {number} round 当前回合
390
+ * @param {import('./tunables_bundle.mjs').TunablesBundle} tunables 候选参数
391
+ * @returns {void}
392
+ */
393
+ function runOscillator(simulationContext, node, observer, round, tunables) {
394
+ const { bumpChunkStorageReputationPure, penalizeChunkStorageFailurePure } = simulationContext.engine
395
+ if (round % 2 === 0)
396
+ bumpChunkStorageReputationPure(observer.reputation, node.id, tunables.reputation)
397
+ else
398
+ penalizeChunkStorageFailurePure(observer.reputation, node.id, tunables.reputation)
399
+ }
400
+
401
+ /**
402
+ * 提示投毒:向观察者注入指向自己的发现提示,权重随 hintDefaultWeight 缩放。
403
+ * @param {object} simulationContext 仿真上下文
404
+ * @param {import('./model.mjs').SimNode} node 恶意节点
405
+ * @param {import('./model.mjs').SimObserver} observer 诚实观察者
406
+ * @param {import('./tunables_bundle.mjs').TunablesBundle} tunables 候选参数
407
+ * @returns {void}
408
+ */
409
+ function runHintPoisoner(simulationContext, node, observer, tunables) {
410
+ const mul = attackParams(simulationContext, node).hintWeightMul
411
+ const weight = tunables.trustGraph.hintDefaultWeight * mul
412
+ const existing = observer.injectedHints.find(h => h.nodeHash === node.id)
413
+ if (existing)
414
+ existing.weight = (existing.weight ?? weight) + weight
415
+ else
416
+ observer.injectedHints.push({ nodeHash: node.id, source: 'poison', weight })
417
+ }
418
+
419
+ /**
420
+ * 盗号/同 hash:继承受害者已积累的信誉后作恶。
421
+ * @param {object} simulationContext 仿真上下文
422
+ * @param {import('./model.mjs').SimNode} node 恶意节点
423
+ * @param {import('./model.mjs').SimObserver} observer 诚实观察者
424
+ * @param {() => number} rng 随机源
425
+ * @param {number} round 当前回合
426
+ * @param {import('./tunables_bundle.mjs').TunablesBundle} tunables 候选参数
427
+ * @returns {void}
428
+ */
429
+ function runKeyThief(simulationContext, node, observer, rng, round, tunables) {
430
+ const { bumpReputationOnRelayPure, recordMessageRateViolationPure, penalizeArchiveServeMismatchPure } = simulationContext.engine
431
+ const victimId = node.stolenFromId
432
+ if (!victimId) return
433
+
434
+ // 首次回合:复制受害者信誉分
435
+ if (round === 0 && observer.reputation.byNodeHash[victimId]?.score != null) {
436
+ const victimScore = observer.reputation.byNodeHash[victimId].score
437
+ observer.reputation.byNodeHash[node.id] = { score: victimScore }
438
+ }
439
+
440
+ if (round < 3) {
441
+ if (rng() < 0.5)
442
+ bumpReputationOnRelayPure(observer.reputation, node.id, `key-thief-warm:${round}`, simulationContext.now, tunables.reputation)
443
+ return
444
+ }
445
+
446
+ // 盗号后作恶
447
+ if (rng() < 0.7)
448
+ recordMessageRateViolationPure(observer.reputation, node.id, tunables.reputation)
449
+ if (rng() < 0.4)
450
+ penalizeArchiveServeMismatchPure(observer.reputation, node.id, tunables.reputation)
451
+ }
452
+
453
+ /**
454
+ * 肉鸡突变:前期诚实刷信誉,turnRound 后突然转恶。
455
+ * @param {object} simulationContext 仿真上下文
456
+ * @param {import('./model.mjs').SimNode} node 恶意节点
457
+ * @param {import('./model.mjs').SimObserver} observer 诚实观察者
458
+ * @param {() => number} rng 随机源
459
+ * @param {number} round 当前回合
460
+ * @param {import('./tunables_bundle.mjs').TunablesBundle} tunables 候选参数
461
+ * @returns {void}
462
+ */
463
+ function runSleeper(simulationContext, node, observer, rng, round, tunables) {
464
+ const { bumpReputationOnRelayPure, bumpChunkStorageReputationPure, recordMessageRateViolationPure } = simulationContext.engine
465
+ const p = attackParams(simulationContext, node)
466
+ const turnRound = node.sleeperTurnRound ?? simulationContext.sleeperTurnRound ?? 15
467
+
468
+ if (round < turnRound) {
469
+ if (rng() < p.activationRate)
470
+ bumpReputationOnRelayPure(observer.reputation, node.id, `sleeper-good:${round}`, simulationContext.now, tunables.reputation)
471
+ if (rng() < 0.4)
472
+ bumpChunkStorageReputationPure(observer.reputation, node.id, tunables.reputation)
473
+ return
474
+ }
475
+
476
+ for (let i = 0; i < Math.max(1, Math.floor(p.burstSize / 2)); i++)
477
+ recordMessageRateViolationPure(observer.reputation, node.id, tunables.reputation)
478
+ const honest = pickHonestTarget(simulationContext, observer, rng, p.targetBiasHighRep)
479
+ if (honest && rng() < p.activationRate * 0.5)
480
+ queueSlash(simulationContext, observer, honest.id, node.id, tunables.reputation.slashUnverifiedDefaultClaim, false, round, tunables)
481
+ }
482
+
483
+ /**
484
+ * digest 等价欺骗:对不同观察者上报不同归档摘要(记录在 simulationContext.equivocationByObserver)。
485
+ * @param {object} simulationContext 仿真上下文
486
+ * @param {import('./model.mjs').SimNode} node 恶意节点
487
+ * @param {import('./model.mjs').SimObserver} observer 诚实观察者
488
+ * @param {import('./tunables_bundle.mjs').TunablesBundle} tunables 候选参数
489
+ * @returns {void}
490
+ */
491
+ function runEquivocator(simulationContext, node, observer, tunables) {
492
+ const { penalizeArchiveServeMismatchPure } = simulationContext.engine
493
+ penalizeArchiveServeMismatchPure(observer.reputation, node.id, tunables.reputation)
494
+ if (!simulationContext.equivocationByObserver) simulationContext.equivocationByObserver = new Map()
495
+ const key = `${node.id}:${observer.id}`
496
+ simulationContext.equivocationByObserver.set(key, (simulationContext.equivocationByObserver.get(key) ?? 0) + 1)
497
+ }
498
+
499
+ /**
500
+ * 定向 eclipse:对单一受害诚实节点做分区(标记 simulationContext.eclipseTargets)。
501
+ * @param {object} simulationContext 仿真上下文
502
+ * @param {import('./model.mjs').SimNode} node 恶意节点
503
+ * @param {import('./model.mjs').SimObserver} observer 诚实观察者
504
+ * @param {() => number} rng 随机源
505
+ * @param {number} round 当前回合
506
+ * @param {import('./tunables_bundle.mjs').TunablesBundle} tunables 候选参数
507
+ * @returns {void}
508
+ */
509
+ function runTargetedEclipse(simulationContext, node, observer, rng, round, tunables) {
510
+ const { recordGossipAllUnknownWantPure } = simulationContext.engine
511
+ const p = attackParams(simulationContext, node)
512
+ const targetId = node.eclipseTargetId ?? simulationContext.eclipseTargetId
513
+ if (targetId)
514
+ simulationContext.eclipseVictims = simulationContext.eclipseVictims ?? new Set()
515
+ for (let i = 0; i < p.burstSize; i++)
516
+ recordGossipAllUnknownWantPure(observer.reputation, node.id, simulationContext.now + i, tunables.reputation)
517
+ if (targetId) {
518
+ simulationContext.eclipseVictims.add(targetId)
519
+ eclipseFillExplore(simulationContext.discovery, observer.id, node.id, [node.id], p.eclipseFocus)
520
+ }
521
+ }
522
+
523
+ /**
524
+ * pump-and-dump:sybil 簇交叉抬升一个身份进 topK 后再伪造。
525
+ * @param {object} simulationContext 仿真上下文
526
+ * @param {import('./model.mjs').SimNode} node 恶意节点
527
+ * @param {import('./model.mjs').SimObserver} observer 诚实观察者
528
+ * @param {() => number} rng 随机源
529
+ * @param {number} round 当前回合
530
+ * @param {import('./tunables_bundle.mjs').TunablesBundle} tunables 候选参数
531
+ * @returns {void}
532
+ */
533
+ function runRepPump(simulationContext, node, observer, rng, round, tunables) {
534
+ const { penalizeArchiveServeMismatchPure } = simulationContext.engine
535
+ const p = attackParams(simulationContext, node)
536
+ const pumpRounds = Math.max(8, Math.round((simulationContext.scenario?.rounds ?? 40) * p.sleeperTurnFrac))
537
+ if (round < pumpRounds) {
538
+ observeSelfRelayBump(simulationContext, node, observer, rng, p.activationRate, `pump:${round}:${node.id}`, tunables)
539
+ for (const sybil of simulationContext.sybilCluster(node))
540
+ if (sybil.id !== node.id && rng() < p.activationRate * p.collusionAllyRate)
541
+ injectAllyHint(simulationContext, observer, sybil.id, node.id, 0.5, tunables)
542
+ return
543
+ }
544
+ if (rng() < p.activationRate)
545
+ penalizeArchiveServeMismatchPure(observer.reputation, node.id, tunables.reputation)
546
+ }
547
+
548
+ /**
549
+ * 慢滴 spammer:紧贴速率阈值下方刷量。
550
+ * @param {object} simulationContext 仿真上下文
551
+ * @param {import('./model.mjs').SimNode} node 恶意节点
552
+ * @param {import('./model.mjs').SimObserver} observer 诚实观察者
553
+ * @param {number} round 当前回合
554
+ * @param {import('./tunables_bundle.mjs').TunablesBundle} tunables 候选参数
555
+ * @returns {void}
556
+ */
557
+ function runSlowDripSpammer(simulationContext, node, observer, round, tunables) {
558
+ const { recordMessageRateViolationPure } = simulationContext.engine
559
+ if (round % 3 !== 0) return
560
+ recordMessageRateViolationPure(observer.reputation, node.id, tunables.reputation)
561
+ }
562
+
563
+ /**
564
+ * 信令 join 洪水:占满 RTC 槽位触发过载冷却。
565
+ * @param {object} simulationContext 仿真上下文
566
+ * @param {import('./model.mjs').SimNode} node 恶意节点
567
+ * @param {import('./model.mjs').SimObserver} observer 诚实观察者
568
+ * @param {number} round 当前回合
569
+ * @param {import('./tunables_bundle.mjs').TunablesBundle} tunables 参数
570
+ * @returns {void}
571
+ */
572
+ function runSignalingFlood(simulationContext, node, observer, round, tunables) {
573
+ const burst = attackParams(simulationContext, node).burstSize
574
+ const transport = simulationContext.transportByObserver?.get(observer.id)
575
+ if (!transport) return
576
+ for (let i = 0; i < burst; i++)
577
+ takeTransportJoinSlot(transport, `${node.id}:flood:${round}:${i}`, `flood:${node.id}`, simulationContext.now + i)
578
+ void tunables
579
+ }
580
+
581
+ /**
582
+ * 信令 eclipse:单源占槽 + 低权重 hint(单源仍可连,多源有软加成)。
583
+ * @param {object} simulationContext 仿真上下文
584
+ * @param {import('./model.mjs').SimNode} node 恶意节点
585
+ * @param {import('./model.mjs').SimObserver} observer 诚实观察者
586
+ * @param {import('./tunables_bundle.mjs').TunablesBundle} tunables 参数
587
+ * @returns {void}
588
+ */
589
+ function runSignalingEclipse(simulationContext, node, observer, tunables) {
590
+ const transport = simulationContext.transportByObserver?.get(observer.id)
591
+ if (!transport) return
592
+ takeTransportJoinSlot(transport, node.id, `sig:${node.id}`, simulationContext.now)
593
+ transportHintWeight(transport, observer.id, node.id, `sig:${node.id}`, tunables.trustGraph.hintDefaultWeight * 0.5)
594
+ }
595
+
596
+ /**
597
+ * @param {object} simulationContext 仿真上下文
598
+ * @param {import('./model.mjs').SimObserver} observer 诚实观察者
599
+ * @param {() => number} rng 随机源
600
+ * @param {number} [highRepBias=0] 高信誉目标偏好 0..1
601
+ * @returns {import('./model.mjs').SimNode | null} 随机诚实目标,无则 null
602
+ */
603
+ function pickHonestTarget(simulationContext, observer, rng, highRepBias = 0) {
604
+ const honest = simulationContext.honestNodes.filter(n => n.id !== observer.id)
605
+ if (!honest.length) return null
606
+ if (highRepBias > 0 && rng() < highRepBias) {
607
+ const scored = honest
608
+ .map(n => ({ n, s: observer.reputation.byNodeHash[n.id]?.score ?? 0 }))
609
+ .sort((a, b) => b.s - a.s)
610
+ const top = scored.slice(0, Math.max(1, Math.ceil(honest.length * 0.25)))
611
+ return top[Math.floor(rng() * top.length)]?.n ?? honest[Math.floor(rng() * honest.length)]
612
+ }
613
+ return honest[Math.floor(rng() * honest.length)]
614
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * 诚实节点浮点行为向量(每维 [0,1])。
3
+ */
4
+
5
+ /** @typedef {'postRate' | 'likeRate' | 'replyRate' | 'relayRate' | 'chunkServeRate' | 'onlineStability' | 'blockProneness' | 'archiveSubmitRate' | 'mentionRate' | 'burstPostRate'} BehaviorKey */
6
+
7
+ /**
8
+ * @typedef {Record<BehaviorKey, number>} NodeBehavior
9
+ */
10
+
11
+ /**
12
+ * @typedef {Partial<Record<BehaviorKey, { min?: number, max?: number, mean?: number }>>} BehaviorDist
13
+ */
14
+
15
+ /** @type {BehaviorKey[]} */
16
+ export const BEHAVIOR_KEYS = Object.freeze([
17
+ 'postRate',
18
+ 'likeRate',
19
+ 'replyRate',
20
+ 'relayRate',
21
+ 'chunkServeRate',
22
+ 'onlineStability',
23
+ 'blockProneness',
24
+ 'archiveSubmitRate',
25
+ 'mentionRate',
26
+ 'burstPostRate',
27
+ ])
28
+
29
+ /** 均衡默认分布 */
30
+ export const DEFAULT_BEHAVIOR_DIST = Object.freeze({
31
+ postRate: { mean: 0.45, min: 0.1, max: 0.9 },
32
+ likeRate: { mean: 0.35, min: 0, max: 0.95 },
33
+ replyRate: { mean: 0.3, min: 0, max: 0.85 },
34
+ relayRate: { mean: 0.4, min: 0.05, max: 0.9 },
35
+ chunkServeRate: { mean: 0.35, min: 0, max: 0.8 },
36
+ onlineStability: { mean: 0.75, min: 0.3, max: 1 },
37
+ blockProneness: { mean: 0.08, min: 0, max: 0.4 },
38
+ archiveSubmitRate: { mean: 0.25, min: 0, max: 0.7 },
39
+ mentionRate: { mean: 0.15, min: 0, max: 0.6 },
40
+ burstPostRate: { mean: 0, min: 0, max: 0.5 },
41
+ })
42
+
43
+ /**
44
+ * @param {() => number} rng 随机源
45
+ * @param {BehaviorDist} [dist] 分布
46
+ * @returns {NodeBehavior} 采样行为向量
47
+ */
48
+ export function sampleBehavior(rng, dist = {}) {
49
+ /** @type {Partial<NodeBehavior>} */
50
+ const behavior = {}
51
+ for (const key of BEHAVIOR_KEYS) {
52
+ const spec = dist[key] ?? DEFAULT_BEHAVIOR_DIST[key] ?? { mean: 0.5 }
53
+ const min = spec.min ?? 0
54
+ const max = spec.max ?? 1
55
+ const mean = spec.mean ?? (min + max) / 2
56
+ const spread = (max - min) * 0.5
57
+ behavior[key] = Math.max(min, Math.min(max, mean + (rng() - 0.5) * spread))
58
+ }
59
+ return /** @type {NodeBehavior} */ behavior
60
+ }
61
+
62
+ /**
63
+ * @param {NodeBehavior} behavior 行为向量
64
+ * @returns {boolean} 是否「只点赞不发帖」型静默用户
65
+ */
66
+ export function isQuietHonestBehavior(behavior) {
67
+ return behavior.postRate < 0.15 && behavior.likeRate >= 0.4 && behavior.replyRate < 0.25
68
+ }
69
+
70
+ /**
71
+ * @param {() => number} rng 随机源
72
+ * @param {NodeBehavior} behavior 行为向量
73
+ * @param {BehaviorKey} key 维度
74
+ * @returns {boolean} 本回合是否触发
75
+ */
76
+ export function behaviorRoll(rng, behavior, key) {
77
+ return rng() < (behavior[key] ?? 0)
78
+ }