@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.
- package/AGENTS.md +79 -0
- package/docs/evfs.md +32 -0
- package/docs/infra.md +25 -0
- package/docs/mesh.md +67 -0
- package/docs/reputation.md +22 -0
- package/docs/runtime.md +62 -0
- package/docs/signaling.md +23 -0
- package/docs/transports.md +92 -0
- package/docs/wire.md +21 -0
- package/link/providers/webrtc.mjs +5 -2
- package/link/rtc/ice_local_hostname.mjs +15 -47
- package/link/rtc/polyfill.mjs +3 -15
- package/package.json +2 -9
- package/sim/AGENTS.md +29 -0
- package/sim/admission.mjs +56 -0
- package/sim/anomaly.mjs +39 -0
- package/sim/apply.mjs +77 -0
- package/sim/attack_space.mjs +113 -0
- package/sim/attacks.mjs +614 -0
- package/sim/behavior.mjs +78 -0
- package/sim/cli.mjs +282 -0
- package/sim/coevolution.mjs +245 -0
- package/sim/cold_start.md +13 -0
- package/sim/constants.mjs +44 -0
- package/sim/discovery.mjs +259 -0
- package/sim/duration.mjs +64 -0
- package/sim/graph_adj.mjs +28 -0
- package/sim/integrity.mjs +58 -0
- package/sim/metrics.mjs +423 -0
- package/sim/model.mjs +1176 -0
- package/sim/optimizer.mjs +250 -0
- package/sim/propagation.mjs +57 -0
- package/sim/report.mjs +176 -0
- package/sim/reputation_social.tunables.json +6 -0
- package/sim/rng.mjs +62 -0
- package/sim/scenarios.mjs +345 -0
- package/sim/sim_pool.mjs +169 -0
- package/sim/sim_worker.mjs +31 -0
- package/sim/social_reputation.mjs +30 -0
- package/sim/space.mjs +312 -0
- package/sim/test/admission.test.mjs +25 -0
- package/sim/test/attack_space.test.mjs +39 -0
- package/sim/test/behavior_vector.test.mjs +37 -0
- package/sim/test/coevolution.test.mjs +32 -0
- package/sim/test/cold_start.test.mjs +46 -0
- package/sim/test/defense.test.mjs +320 -0
- package/sim/test/discovery.test.mjs +68 -0
- package/sim/test/duration.test.mjs +20 -0
- package/sim/test/fidelity.test.mjs +151 -0
- package/sim/test/integrity.test.mjs +56 -0
- package/sim/test/optimizer.test.mjs +24 -0
- package/sim/test/propagation.test.mjs +29 -0
- package/sim/test/sim_model_regressions.test.mjs +110 -0
- package/sim/test/slow_drip.test.mjs +28 -0
- package/sim/test/smoke.test.mjs +52 -0
- package/sim/test/transport.test.mjs +55 -0
- package/sim/test/tunables_apply.test.mjs +64 -0
- package/sim/test/tunables_param_space.test.mjs +74 -0
- package/sim/test/vulnerability.test.mjs +127 -0
- package/sim/transport.mjs +135 -0
- package/sim/tunables_bundle.mjs +71 -0
- package/sim/vulnerability.mjs +532 -0
- package/deno.json +0 -7
- 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
|
+
}
|
package/sim/sim_pool.mjs
ADDED
|
@@ -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
|
+
}
|