@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,423 @@
1
+ /**
2
+ * 仿真指标与适应度(全部内生,无外生规则惩罚)。
3
+ */
4
+ import { runSimulation } from './model.mjs'
5
+ import { runSimulationJobs, defaultConcurrency } from './sim_pool.mjs'
6
+
7
+ /**
8
+ * @typedef {{
9
+ * malSuppressionRate: number,
10
+ * honestPreservationRate: number,
11
+ * falsePositiveRate: number,
12
+ * fanoutReachRate: number,
13
+ * federationReachRate: number,
14
+ * fanoutCostRatio: number,
15
+ * collusionCollapseRate: number,
16
+ * relayPreservationRate: number,
17
+ * profilePreservationRate: number,
18
+ * sybilContainmentRate: number,
19
+ * archiveDefenseRate: number,
20
+ * mailboxReachRate: number,
21
+ * mailboxCostRatio: number,
22
+ * nodeOverloadRate: number,
23
+ * archiveQuorumAccuracy: number,
24
+ * churnReachRate: number,
25
+ * compromiseContainmentRate: number,
26
+ * sleeperReactionRate: number,
27
+ * equivocationDefenseRate: number,
28
+ * transportReachRate: number,
29
+ * signalingDiversityRate: number,
30
+ * joinThrottleEffectiveness: number,
31
+ * honestJoinDelayPenalty: number,
32
+ * observerCount: number,
33
+ * maliciousCount: number,
34
+ * honestCount: number,
35
+ * groupSize: number,
36
+ * quietHonestPreservationRate: number,
37
+ * byAttackDefense?: Partial<Record<string, { defended: number, total: number, rate: number }>>,
38
+ * byAttackImpact?: Partial<Record<string, { topKCapture: number, reachCollapse: number }>>,
39
+ * }} SimSnapshot
40
+ */
41
+
42
+ /** @typedef {Partial<Record<keyof SimSnapshot, number>>} MetricWeights */
43
+
44
+ /** @typedef {{ concurrency?: number, serial?: boolean }} EvalOptions */
45
+
46
+ /** 参与适应度加权的速率类指标 */
47
+ export const RATE_METRIC_KEYS = Object.freeze([
48
+ 'malSuppressionRate',
49
+ 'honestPreservationRate',
50
+ 'falsePositiveRate',
51
+ 'fanoutReachRate',
52
+ 'federationReachRate',
53
+ 'fanoutCostRatio',
54
+ 'collusionCollapseRate',
55
+ 'relayPreservationRate',
56
+ 'profilePreservationRate',
57
+ 'quietHonestPreservationRate',
58
+ 'sybilContainmentRate',
59
+ 'archiveDefenseRate',
60
+ 'mailboxReachRate',
61
+ 'mailboxCostRatio',
62
+ 'nodeOverloadRate',
63
+ 'archiveQuorumAccuracy',
64
+ 'churnReachRate',
65
+ 'compromiseContainmentRate',
66
+ 'sleeperReactionRate',
67
+ 'equivocationDefenseRate',
68
+ 'transportReachRate',
69
+ 'signalingDiversityRate',
70
+ 'joinThrottleEffectiveness',
71
+ 'honestJoinDelayPenalty',
72
+ ])
73
+
74
+ /** 默认适应度加权系数 */
75
+ export const DEFAULT_WEIGHTS = Object.freeze({
76
+ malSuppressionRate: 0.18,
77
+ honestPreservationRate: 0.15,
78
+ collusionCollapseRate: 0.08,
79
+ sybilContainmentRate: 0.06,
80
+ archiveDefenseRate: 0.07,
81
+ relayPreservationRate: 0.05,
82
+ profilePreservationRate: 0.03,
83
+ quietHonestPreservationRate: 0.05,
84
+ federationReachRate: 0.22,
85
+ fanoutReachRate: 0.02,
86
+ mailboxReachRate: 0.07,
87
+ archiveQuorumAccuracy: 0.06,
88
+ churnReachRate: 0.14,
89
+ transportReachRate: 0.04,
90
+ signalingDiversityRate: 0.02,
91
+ joinThrottleEffectiveness: 0.03,
92
+ compromiseContainmentRate: 0.05,
93
+ sleeperReactionRate: 0.04,
94
+ equivocationDefenseRate: 0.04,
95
+ honestJoinDelayPenalty: -0.03,
96
+ falsePositiveRate: -0.16,
97
+ fanoutCostRatio: -0.04,
98
+ mailboxCostRatio: -0.045,
99
+ nodeOverloadRate: -0.08,
100
+ })
101
+
102
+ /**
103
+ * @param {SimSnapshot} snap 单次快照
104
+ * @param {MetricWeights} [weights] 权重
105
+ * @returns {number} 适应度(越高越好)
106
+ */
107
+ export function fitnessFromSnapshot(snap, weights = DEFAULT_WEIGHTS) {
108
+ let score = 0
109
+ for (const [key, weight] of Object.entries(weights)) {
110
+ const v = snap[/** @type {keyof SimSnapshot} */ key]
111
+ if (typeof v === 'number' && typeof weight === 'number')
112
+ score += v * weight
113
+ }
114
+ return score
115
+ }
116
+
117
+ /**
118
+ * @param {SimSnapshot[]} snaps 多次快照
119
+ * @param {MetricWeights} [weights] 权重
120
+ * @returns {{ mean: number, min: number, max: number, std: number, fitness: number, snapshots: SimSnapshot[] }} 聚合统计
121
+ */
122
+ export function aggregateSnapshots(snaps, weights = DEFAULT_WEIGHTS) {
123
+ const fitnesses = snaps.map(s => fitnessFromSnapshot(s, weights))
124
+ const mean = fitnesses.reduce((a, b) => a + b, 0) / Math.max(1, fitnesses.length)
125
+ const min = Math.min(...fitnesses)
126
+ const max = Math.max(...fitnesses)
127
+ const variance = fitnesses.reduce((s, f) => s + (f - mean) ** 2, 0) / Math.max(1, fitnesses.length)
128
+ return {
129
+ mean,
130
+ min,
131
+ max,
132
+ std: Math.sqrt(variance),
133
+ fitness: mean - 0.5 * Math.sqrt(variance),
134
+ snapshots: snaps,
135
+ }
136
+ }
137
+
138
+ /**
139
+ * @param {import('./scenarios.mjs').SimScenario[]} scenarios 场景列表
140
+ * @param {number[]} seeds 种子列表
141
+ * @param {import('./tunables_bundle.mjs').TunablesBundle} tunables 参数
142
+ * @param {(scenario: import('./scenarios.mjs').SimScenario, seed: number, tunables: import('./tunables_bundle.mjs').TunablesBundle) => SimSnapshot} runSim 仿真函数
143
+ * @param {MetricWeights} [weights] 权重
144
+ * @param {EvalOptions} [options] 评估选项
145
+ * @returns {Promise<{ fitness: number, mean: number, min: number, max: number, std: number, byScenario: Record<string, ReturnType<typeof aggregateSnapshots>> }>} 跨场景评估结果
146
+ */
147
+ export async function evaluateTunables(scenarios, seeds, tunables, runSim, weights = DEFAULT_WEIGHTS, options = {}) {
148
+ return evaluateTunablesAgainstAttacks(scenarios, seeds, tunables, [undefined], (scenario, seed, tunablesBundle, attackGenome) =>
149
+ runSim(scenario, seed, tunablesBundle, attackGenome), weights, options)
150
+ }
151
+
152
+ /**
153
+ * @typedef {{
154
+ * fitness: number,
155
+ * mean: number,
156
+ * min: number,
157
+ * max: number,
158
+ * std: number,
159
+ * byScenario: Record<string, ReturnType<typeof aggregateSnapshots>>,
160
+ * }} EvalResult
161
+ */
162
+
163
+ /**
164
+ * @typedef {{
165
+ * tunables: import('./tunables_bundle.mjs').TunablesBundle,
166
+ * attackPanel?: Array<import('./attack_space.mjs').AttackGenome | undefined>,
167
+ * }} EvalCandidate
168
+ */
169
+
170
+ /**
171
+ * @param {import('./scenarios.mjs').SimScenario[]} scenarios 场景
172
+ * @param {number[]} seeds 种子
173
+ * @param {Array<import('./attack_space.mjs').AttackGenome | undefined>} panel 攻击面板
174
+ * @param {Map<string, Map<number, { attackIndex: number, snap: SimSnapshot }[]>>} snapsByScenarioSeed 分组快照
175
+ * @param {MetricWeights} weights 权重
176
+ * @returns {EvalResult} 跨场景面板最差评估
177
+ */
178
+ function buildEvalFromGroupedSnaps(scenarios, seeds, panel, snapsByScenarioSeed, weights) {
179
+ /** @type {Record<string, ReturnType<typeof aggregateSnapshots>>} */
180
+ const byScenario = {}
181
+ let totalFitness = 0
182
+ let totalMean = 0
183
+ let worstMin = Infinity
184
+ let bestMax = -Infinity
185
+ let totalStd = 0
186
+
187
+ for (const scenario of scenarios) {
188
+ const bySeed = snapsByScenarioSeed.get(scenario.id) ?? new Map()
189
+ /** @type {SimSnapshot[]} */
190
+ const snaps = []
191
+ for (const seed of seeds) {
192
+ const rows = bySeed.get(seed) ?? []
193
+ let worstFit = Infinity
194
+ /** @type {SimSnapshot | null} */
195
+ let worstSnap = null
196
+ for (const row of rows) {
197
+ const f = fitnessFromSnapshot(row.snap, weights)
198
+ if (f < worstFit) {
199
+ worstFit = f
200
+ worstSnap = row.snap
201
+ }
202
+ }
203
+ if (worstSnap) snaps.push(worstSnap)
204
+ }
205
+ const agg = aggregateSnapshots(snaps, weights)
206
+ byScenario[scenario.id] = agg
207
+ totalFitness += agg.fitness
208
+ totalMean += agg.mean
209
+ worstMin = Math.min(worstMin, agg.min)
210
+ bestMax = Math.max(bestMax, agg.max)
211
+ totalStd += agg.std
212
+ }
213
+
214
+ const n = Math.max(1, scenarios.length)
215
+ return {
216
+ fitness: totalFitness / n,
217
+ mean: totalMean / n,
218
+ min: worstMin,
219
+ max: bestMax,
220
+ std: totalStd / n,
221
+ byScenario,
222
+ }
223
+ }
224
+
225
+ /**
226
+ * 多候选批量评估:一次派发全部 job,按候选归并适应度。
227
+ * @param {import('./scenarios.mjs').SimScenario[]} scenarios 场景
228
+ * @param {number[]} seeds 种子
229
+ * @param {EvalCandidate[]} candidates 候选列表(各自 tunables + attackPanel)
230
+ * @param {(scenario: import('./scenarios.mjs').SimScenario, seed: number, tunables: import('./tunables_bundle.mjs').TunablesBundle, attackGenome?: import('./attack_space.mjs').AttackGenome) => SimSnapshot} runSim 仿真
231
+ * @param {MetricWeights} [weights] 权重
232
+ * @param {EvalOptions} [options] 评估选项
233
+ * @returns {Promise<EvalResult[]>} 与 candidates 同序的评估结果
234
+ */
235
+ export async function evaluateManyAgainstAttacks(scenarios, seeds, candidates, runSim, weights = DEFAULT_WEIGHTS, options = {}) {
236
+ if (!candidates.length) return []
237
+
238
+ const concurrency = options.concurrency ?? defaultConcurrency()
239
+ const usePool = !options.serial && concurrency > 1 && runSim === runSimulation
240
+
241
+ if (!usePool) {
242
+ const out = []
243
+ for (const cand of candidates) {
244
+ const panel = cand.attackPanel?.length ? cand.attackPanel : [undefined]
245
+ out.push(await evaluateTunablesAgainstAttacks(scenarios, seeds, cand.tunables, panel, runSim, weights, options))
246
+ }
247
+ return out
248
+ }
249
+
250
+ /** @type {Array<{ candidateIndex: number, scenarioId: string, seed: number }>} */
251
+ const jobMeta = []
252
+ /** @type {import('./sim_pool.mjs').SimJob[]} */
253
+ const jobs = []
254
+ let jobId = 0
255
+ for (let ci = 0; ci < candidates.length; ci++) {
256
+ const cand = candidates[ci]
257
+ const panel = cand.attackPanel?.length ? cand.attackPanel : [undefined]
258
+ for (const scenario of scenarios)
259
+ for (const seed of seeds)
260
+ for (let ai = 0; ai < panel.length; ai++) {
261
+ jobMeta.push({ candidateIndex: ci, scenarioId: scenario.id, seed })
262
+ jobs.push({
263
+ id: jobId++,
264
+ scenarioId: scenario.id,
265
+ seed,
266
+ tunables: cand.tunables,
267
+ attackGenome: panel[ai] ?? undefined,
268
+ })
269
+ }
270
+ }
271
+
272
+ const poolResults = await runSimulationJobs(jobs, { concurrency })
273
+ for (const poolResult of poolResults)
274
+ if (poolResult.error)
275
+ throw new Error(`sim job ${poolResult.id} failed: ${poolResult.error}`)
276
+
277
+ /** @type {Map<number, Map<string, Map<number, { attackIndex: number, snap: SimSnapshot }[]>>>} */
278
+ const byCandidate = new Map()
279
+ for (let i = 0; i < jobMeta.length; i++) {
280
+ const meta = jobMeta[i]
281
+ const snap = poolResults[i].snapshot
282
+ if (!snap) throw new Error(`sim job ${i} missing snapshot`)
283
+ if (!byCandidate.has(meta.candidateIndex))
284
+ byCandidate.set(meta.candidateIndex, new Map())
285
+ const byScenario = byCandidate.get(meta.candidateIndex)
286
+ if (!byScenario.has(meta.scenarioId))
287
+ byScenario.set(meta.scenarioId, new Map())
288
+ const bySeed = byScenario.get(meta.scenarioId)
289
+ if (!bySeed.has(meta.seed))
290
+ bySeed.set(meta.seed, [])
291
+ bySeed.get(meta.seed).push({ attackIndex: i, snap })
292
+ }
293
+
294
+ return candidates.map((cand, ci) => {
295
+ const panel = cand.attackPanel?.length ? cand.attackPanel : [undefined]
296
+ return buildEvalFromGroupedSnaps(scenarios, seeds, panel, byCandidate.get(ci) ?? new Map(), weights)
297
+ })
298
+ }
299
+
300
+ /**
301
+ * 蓝队对攻击者面板的评估:每个 (scenario, seed) 取面板内最差适应度。
302
+ * @param {import('./scenarios.mjs').SimScenario[]} scenarios 场景
303
+ * @param {number[]} seeds 种子
304
+ * @param {import('./tunables_bundle.mjs').TunablesBundle} tunables 参数
305
+ * @param {Array<import('./attack_space.mjs').AttackGenome | undefined>} attackPanel 攻击者面板
306
+ * @param {(scenario: import('./scenarios.mjs').SimScenario, seed: number, tunables: import('./tunables_bundle.mjs').TunablesBundle, attackGenome?: import('./attack_space.mjs').AttackGenome) => SimSnapshot} runSim 仿真
307
+ * @param {MetricWeights} [weights] 权重
308
+ * @param {EvalOptions} [options] 评估选项(concurrency / serial)
309
+ * @returns {Promise<{ fitness: number, mean: number, min: number, max: number, std: number, byScenario: Record<string, ReturnType<typeof aggregateSnapshots>> }>} 跨场景面板最差评估
310
+ */
311
+ export async function evaluateTunablesAgainstAttacks(scenarios, seeds, tunables, attackPanel, runSim, weights = DEFAULT_WEIGHTS, options = {}) {
312
+ const panel = attackPanel?.length ? attackPanel : [undefined]
313
+ const concurrency = options.concurrency ?? defaultConcurrency()
314
+ const usePool = !options.serial && concurrency > 1 && runSim === runSimulation
315
+
316
+ /** @type {Record<string, ReturnType<typeof aggregateSnapshots>>} */
317
+ const byScenario = {}
318
+ let totalFitness = 0
319
+ let totalMean = 0
320
+ let worstMin = Infinity
321
+ let bestMax = -Infinity
322
+ let totalStd = 0
323
+
324
+ if (usePool) {
325
+ /** @type {Array<{ scenarioId: string, seed: number, attackIndex: number, attackGenome?: import('./attack_space.mjs').AttackGenome }>} */
326
+ const jobMeta = []
327
+ /** @type {import('./sim_pool.mjs').SimJob[]} */
328
+ const jobs = []
329
+ let jobId = 0
330
+ for (const scenario of scenarios)
331
+ for (const seed of seeds)
332
+ for (let ai = 0; ai < panel.length; ai++) {
333
+ const attackGenome = panel[ai] ?? undefined
334
+ jobMeta.push({ scenarioId: scenario.id, seed, attackIndex: ai, attackGenome })
335
+ jobs.push({
336
+ id: jobId++,
337
+ scenarioId: scenario.id,
338
+ seed,
339
+ tunables,
340
+ attackGenome,
341
+ })
342
+ }
343
+
344
+ const poolResults = await runSimulationJobs(jobs, { concurrency })
345
+ for (const poolResult of poolResults)
346
+ if (poolResult.error)
347
+ throw new Error(`sim job ${poolResult.id} failed: ${poolResult.error}`)
348
+
349
+ /** @type {Map<string, Map<number, { attackIndex: number, snap: SimSnapshot }[]>>} */
350
+ const snapsByScenarioSeed = new Map()
351
+ for (let i = 0; i < jobMeta.length; i++) {
352
+ const meta = jobMeta[i]
353
+ const snap = poolResults[i].snapshot
354
+ if (!snap) throw new Error(`sim job ${i} missing snapshot`)
355
+ if (!snapsByScenarioSeed.has(meta.scenarioId))
356
+ snapsByScenarioSeed.set(meta.scenarioId, new Map())
357
+ const bySeed = snapsByScenarioSeed.get(meta.scenarioId)
358
+ if (!bySeed.has(meta.seed))
359
+ bySeed.set(meta.seed, [])
360
+ bySeed.get(meta.seed).push({ attackIndex: meta.attackIndex, snap })
361
+ }
362
+
363
+ return buildEvalFromGroupedSnaps(scenarios, seeds, panel, snapsByScenarioSeed, weights)
364
+ }
365
+ else
366
+ for (const scenario of scenarios) {
367
+ /** @type {SimSnapshot[]} */
368
+ const snaps = []
369
+ for (const seed of seeds) {
370
+ let worstFit = Infinity
371
+ /** @type {SimSnapshot | null} */
372
+ let worstSnap = null
373
+ for (const attackGenome of panel) {
374
+ const snap = runSim(scenario, seed, tunables, attackGenome ?? undefined)
375
+ const f = fitnessFromSnapshot(snap, weights)
376
+ if (f < worstFit) {
377
+ worstFit = f
378
+ worstSnap = snap
379
+ }
380
+ }
381
+ if (worstSnap) snaps.push(worstSnap)
382
+ }
383
+ const agg = aggregateSnapshots(snaps, weights)
384
+ byScenario[scenario.id] = agg
385
+ totalFitness += agg.fitness
386
+ totalMean += agg.mean
387
+ worstMin = Math.min(worstMin, agg.min)
388
+ bestMax = Math.max(bestMax, agg.max)
389
+ totalStd += agg.std
390
+ }
391
+
392
+ const n = Math.max(1, scenarios.length)
393
+ return {
394
+ fitness: totalFitness / n,
395
+ mean: totalMean / n,
396
+ min: worstMin,
397
+ max: bestMax,
398
+ std: totalStd / n,
399
+ byScenario,
400
+ }
401
+ }
402
+
403
+ /**
404
+ * @param {Record<string, ReturnType<typeof aggregateSnapshots>>} byScenario 分场景结果
405
+ * @returns {number} 面板最差 min 适应度
406
+ */
407
+ export function minPanelFitness(byScenario) {
408
+ const mins = Object.values(byScenario).map(a => a.min)
409
+ return mins.length ? Math.min(...mins) : 0
410
+ }
411
+
412
+ /**
413
+ * @param {SimSnapshot} baseline 基线快照
414
+ * @param {SimSnapshot} best 最优快照
415
+ * @returns {Array<{ key: string, baseline: number, best: number }>} 各维度对比行
416
+ */
417
+ export function snapshotMetricRows(baseline, best) {
418
+ return RATE_METRIC_KEYS.map(key => ({
419
+ key,
420
+ baseline: baseline[key],
421
+ best: best[key],
422
+ }))
423
+ }