@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,259 @@
1
+ /**
2
+ * 发现拓扑子模型:单 relay rendezvous + RTC 连接预算 + explore 源配额 + trusted 锚点。
3
+ */
4
+ import { resolveRtcBudgetLimits } from '../transport/rtc_connection_budget.mjs'
5
+
6
+ import { buildRankedNeighborAdj } from './graph_adj.mjs'
7
+
8
+ /**
9
+ * 单 explore 源最多占用的 room 槽位(对齐 peer_pool EXPLORE_MAX_PER_SOURCE)。
10
+ * peer_pool 会拖入 node 存储的 fs 依赖,不宜进 sim 热路径,故此处保留常量,
11
+ * 由 fidelity.test.mjs 断言其与真实值一致以守住漂移。
12
+ */
13
+ export const EXPLORE_MAX_PER_SOURCE = 3
14
+
15
+ /** 默认 RTC room 并发连接上限(派生自真实 rtc_connection_budget 默认 maxActive) */
16
+ export const DEFAULT_RTC_MAX_ACTIVE = resolveRtcBudgetLimits().maxActive
17
+
18
+ /**
19
+ * @typedef {{
20
+ * trustedAnchorsByObserver: Map<string, Set<string>>,
21
+ * exploreByObserver: Map<string, Set<string>>,
22
+ * poisonedByAttacker: Map<string, Set<string>>,
23
+ * roomSlotsByObserver: Map<string, { active: Set<string>, sourceByPeer: Map<string, string>, trustedReserved: Set<string> }>,
24
+ * rtcMaxActive: number,
25
+ * }} DiscoveryState
26
+ */
27
+
28
+ /**
29
+ * @param {DiscoveryState} state 发现状态
30
+ * @param {string} observerId 观察者
31
+ * @returns {Set<string>} 该观察者的 trusted 锚点
32
+ */
33
+ function observerTrustedAnchors(state, observerId) {
34
+ return state.trustedAnchorsByObserver.get(observerId) ?? new Set()
35
+ }
36
+
37
+ /**
38
+ * @returns {DiscoveryState} 空发现状态
39
+ */
40
+ export function createDiscoveryState() {
41
+ return {
42
+ trustedAnchorsByObserver: new Map(),
43
+ exploreByObserver: new Map(),
44
+ poisonedByAttacker: new Map(),
45
+ roomSlotsByObserver: new Map(),
46
+ rtcMaxActive: DEFAULT_RTC_MAX_ACTIVE,
47
+ }
48
+ }
49
+
50
+ /**
51
+ * @param {DiscoveryState} state 发现状态
52
+ * @param {string} observerId 观察者
53
+ * @returns {{ active: Set<string>, sourceByPeer: Map<string, string>, trustedReserved: Set<string> }} room 槽位桶
54
+ */
55
+ function roomBucket(state, observerId) {
56
+ let bucket = state.roomSlotsByObserver.get(observerId)
57
+ if (!bucket) {
58
+ bucket = { active: new Set(), sourceByPeer: new Map(), trustedReserved: new Set() }
59
+ state.roomSlotsByObserver.set(observerId, bucket)
60
+ }
61
+ return bucket
62
+ }
63
+
64
+ /**
65
+ * @param {DiscoveryState} state 发现状态
66
+ * @param {string} observerId 观察者
67
+ * @param {string[]} trusted 锚点
68
+ * @param {string[]} roster 在线名册
69
+ * @param {() => number} rng 随机源
70
+ * @param {number} exploreCap explore 容量
71
+ * @param {boolean} [coldBootstrap=false] K=0 冷启动:不预填 explore
72
+ * @returns {Set<string>} 观察者可见 peer 集
73
+ */
74
+ export function initObserverDiscovery(state, observerId, trusted, roster, rng, exploreCap = 8, coldBootstrap = false) {
75
+ state.trustedAnchorsByObserver.set(observerId, new Set(trusted))
76
+ const explore = new Set(trusted)
77
+ const pool = roster.filter(id => id !== observerId && !explore.has(id))
78
+ const minExplore = coldBootstrap
79
+ ? 0
80
+ : trusted.length ? exploreCap : Math.max(exploreCap, 4)
81
+ while (explore.size < minExplore + trusted.length && pool.length) {
82
+ const i = Math.floor(rng() * pool.length)
83
+ explore.add(pool.splice(i, 1)[0])
84
+ }
85
+ state.exploreByObserver.set(observerId, explore)
86
+
87
+ const bucket = roomBucket(state, observerId)
88
+ for (const id of trusted) {
89
+ bucket.trustedReserved.add(id)
90
+ bucket.active.add(id)
91
+ bucket.sourceByPeer.set(id, 'trusted')
92
+ }
93
+ for (const id of explore) {
94
+ if (bucket.active.size >= state.rtcMaxActive) break
95
+ if (bucket.active.has(id)) continue
96
+ takeRoomSlot(state, observerId, id, 'explore', trusted)
97
+ }
98
+ return explore
99
+ }
100
+
101
+ /**
102
+ * K=0 冷启动:模拟 listVisible + connectToNode 填充 explore 槽。
103
+ * @param {DiscoveryState} state 发现状态
104
+ * @param {string} observerId 观察者
105
+ * @param {string[]} roster 在线名册
106
+ * @param {() => number} rng 随机源
107
+ * @param {number} [batch=4] 每轮新增 explore 上限
108
+ * @returns {void}
109
+ */
110
+ export function coldStartDiscoveryJoin(state, observerId, roster, rng, batch = 4) {
111
+ const trusted = [...observerTrustedAnchors(state, observerId)]
112
+ const explore = state.exploreByObserver.get(observerId) ?? new Set()
113
+ const pool = roster.filter(id => id !== observerId && !explore.has(id))
114
+ let added = 0
115
+ while (added < batch && pool.length) {
116
+ const i = Math.floor(rng() * pool.length)
117
+ const id = pool.splice(i, 1)[0]
118
+ explore.add(id)
119
+ takeRoomSlot(state, observerId, id, 'mesh:scan', trusted)
120
+ added++
121
+ }
122
+ state.exploreByObserver.set(observerId, explore)
123
+ }
124
+
125
+ /**
126
+ * 占用 RTC room 槽位;trusted 锚点优先保留,单源 explore 有配额。
127
+ * @param {DiscoveryState} state 发现状态
128
+ * @param {string} observerId 观察者
129
+ * @param {string} peerId 对端
130
+ * @param {string} sourceId 来源标识
131
+ * @param {string[]} [trustedIds=[]] trusted 锚点
132
+ * @returns {boolean} 是否成功占槽
133
+ */
134
+ export function takeRoomSlot(state, observerId, peerId, sourceId, trustedIds = []) {
135
+ const bucket = roomBucket(state, observerId)
136
+ if (bucket.active.has(peerId)) return true
137
+
138
+ const isTrusted = trustedIds.includes(peerId) || bucket.trustedReserved.has(peerId)
139
+ if (!isTrusted) {
140
+ const sourceCount = [...bucket.sourceByPeer.values()].filter(s => s === sourceId).length
141
+ if (sourceCount >= EXPLORE_MAX_PER_SOURCE) return false
142
+ }
143
+
144
+ const maxActive = state.rtcMaxActive
145
+ const trustedCount = bucket.trustedReserved.size
146
+ const nonTrustedActive = [...bucket.active].filter(id => !bucket.trustedReserved.has(id)).length
147
+ const maxNonTrusted = Math.max(0, maxActive - trustedCount)
148
+
149
+ if (!isTrusted && nonTrustedActive >= maxNonTrusted) return false
150
+
151
+ if (bucket.active.size >= maxActive && !isTrusted) return false
152
+
153
+ bucket.active.add(peerId)
154
+ bucket.sourceByPeer.set(peerId, sourceId)
155
+ if (isTrusted) bucket.trustedReserved.add(peerId)
156
+ return true
157
+ }
158
+
159
+ /**
160
+ * eclipse 攻击:Sybil 灌满 room 槽位并污染 explore。
161
+ * @param {DiscoveryState} state 发现状态
162
+ * @param {string} victimObserverId 受害观察者
163
+ * @param {string} attackerId 攻击者
164
+ * @param {string[]} sybilIds 同簇 sybil
165
+ * @param {number} focus 填充强度 0..1
166
+ * @returns {void}
167
+ */
168
+ export function eclipseFillExplore(state, victimObserverId, attackerId, sybilIds, focus = 0.7) {
169
+ const explore = state.exploreByObserver.get(victimObserverId) ?? new Set()
170
+ const cap = Math.max(2, Math.round(explore.size * focus))
171
+ let added = 0
172
+ const sybilSource = `eclipse:${attackerId}`
173
+ for (const id of [attackerId, ...sybilIds]) {
174
+ if (added >= cap) break
175
+ if (!explore.has(id)) {
176
+ explore.add(id)
177
+ added++
178
+ }
179
+ takeRoomSlot(state, victimObserverId, id, sybilSource, [...observerTrustedAnchors(state, victimObserverId)])
180
+ }
181
+ const poison = state.poisonedByAttacker.get(victimObserverId) ?? new Set()
182
+ poison.add(attackerId)
183
+ state.poisonedByAttacker.set(victimObserverId, poison)
184
+ state.exploreByObserver.set(victimObserverId, explore)
185
+ }
186
+
187
+ /**
188
+ * @param {DiscoveryState} state 发现状态
189
+ * @param {string} observerId 观察者
190
+ * @param {string[]} friendlyIds 友善节点
191
+ * @param {(id: string) => number} scoreOf 信誉分
192
+ * @param {number} maxHop 最大跳
193
+ * @param {boolean} [ignorePoison=false] 反事实:忽略 poison/sybil 槽位污染
194
+ * @returns {number} 可达友善节点比例 0..1
195
+ */
196
+ export function discoveryReach(state, observerId, friendlyIds, scoreOf, maxHop = 4, ignorePoison = false) {
197
+ const bucket = roomBucket(state, observerId)
198
+ const roomPeers = [...bucket.active].filter(id => friendlyIds.includes(id))
199
+ const anchorsSet = observerTrustedAnchors(state, observerId)
200
+ const explore = state.exploreByObserver.get(observerId) ?? anchorsSet
201
+ const anchors = [...anchorsSet].filter(id => friendlyIds.includes(id))
202
+ const adj = buildRankedNeighborAdj(friendlyIds, scoreOf, 6)
203
+ const start = new Set([...anchors, ...roomPeers, ...explore].filter(id => friendlyIds.includes(id)))
204
+ if (!start.size) return 0
205
+ const visited = new Set(start)
206
+ let frontier = [...start]
207
+ for (let hop = 0; hop < maxHop && frontier.length; hop++) {
208
+ const next = []
209
+ for (const id of frontier)
210
+ for (const peer of adj.get(id) ?? []) {
211
+ if (visited.has(peer)) continue
212
+ visited.add(peer)
213
+ next.push(peer)
214
+ }
215
+ frontier = next
216
+ }
217
+ const target = friendlyIds.filter(id => id !== observerId)
218
+ const rawReach = target.length ? [...visited].filter(id => target.includes(id)).length / target.length : 0
219
+ if (ignorePoison) return rawReach
220
+ const slotFill = bucket.active.size / Math.max(1, state.rtcMaxActive)
221
+ const sybilDominated = [...bucket.sourceByPeer.values()].filter(s => String(s).startsWith('eclipse:')).length
222
+ const sybilRatio = sybilDominated / Math.max(1, bucket.active.size)
223
+ return rawReach * Math.max(0.05, 1 - sybilRatio * 0.85) * Math.max(0.2, 1 - Math.max(0, slotFill - 0.75) * 2)
224
+ }
225
+
226
+ /**
227
+ * trusted 锚点恢复:清空 poison、释放非 trusted 槽位并重新注入锚点 explore。
228
+ * @param {DiscoveryState} state 发现状态
229
+ * @param {string} observerId 观察者
230
+ * @returns {void}
231
+ */
232
+ export function recoverDiscoveryFromAnchors(state, observerId) {
233
+ state.poisonedByAttacker.delete(observerId)
234
+ const keep = observerTrustedAnchors(state, observerId)
235
+ const priorExplore = state.exploreByObserver.get(observerId) ?? new Set()
236
+ const explore = new Set(keep)
237
+ for (const id of priorExplore)
238
+ if (!keep.has(id)) explore.add(id)
239
+ state.exploreByObserver.set(observerId, explore)
240
+ const bucket = roomBucket(state, observerId)
241
+ bucket.active = new Set(keep)
242
+ bucket.sourceByPeer = new Map([...keep].map(id => [id, 'trusted']))
243
+ bucket.trustedReserved = new Set(keep)
244
+ for (const id of explore) {
245
+ if (keep.has(id)) continue
246
+ takeRoomSlot(state, observerId, id, 'explore', [...keep])
247
+ }
248
+ }
249
+
250
+ /**
251
+ * @param {DiscoveryState} state 发现状态
252
+ * @param {string} observerId 观察者
253
+ * @returns {number} room 槽位占用率 0..1
254
+ */
255
+ export function roomSlotFillRate(state, observerId) {
256
+ const bucket = state.roomSlotsByObserver.get(observerId)
257
+ if (!bucket) return 0
258
+ return bucket.active.size / Math.max(1, state.rtcMaxActive)
259
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * 墙钟时长解析(挖矿 `--duration`)。
3
+ */
4
+
5
+ /**
6
+ * @param {string | boolean | undefined} raw CLI 原始值
7
+ * @returns {number | null} 毫秒;未指定或无效则 null
8
+ */
9
+ export function parseDurationMs(raw) {
10
+ if (raw === undefined || raw === true || raw === '') return null
11
+ const text = String(raw).trim().toLowerCase()
12
+ if (!text) return null
13
+
14
+ const match = /^(\d+(?:\.\d+)?)(ms|s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hr|hrs|hour|hours)?$/u.exec(text)
15
+ if (!match) return null
16
+
17
+ const value = Number(match[1])
18
+ if (!Number.isFinite(value) || value <= 0) return null
19
+
20
+ const unit = match[2] || 's'
21
+ switch (unit) {
22
+ case 'ms':
23
+ return Math.floor(value)
24
+ case 's':
25
+ case 'sec':
26
+ case 'secs':
27
+ case 'second':
28
+ case 'seconds':
29
+ return Math.floor(value * 1000)
30
+ case 'm':
31
+ case 'min':
32
+ case 'mins':
33
+ case 'minute':
34
+ case 'minutes':
35
+ return Math.floor(value * 60_000)
36
+ case 'h':
37
+ case 'hr':
38
+ case 'hrs':
39
+ case 'hour':
40
+ case 'hours':
41
+ return Math.floor(value * 3_600_000)
42
+ default:
43
+ return null
44
+ }
45
+ }
46
+
47
+ /**
48
+ * @param {number | null} durationMs 时长上限
49
+ * @param {number} [now=Date.now()] 当前时间
50
+ * @returns {number | null} 截止时间戳;无上限则 null
51
+ */
52
+ export function deadlineFromDuration(durationMs, now = Date.now()) {
53
+ if (durationMs == null || durationMs <= 0) return null
54
+ return now + durationMs
55
+ }
56
+
57
+ /**
58
+ * @param {number | null} deadline 截止时间戳
59
+ * @param {number} [now=Date.now()] 当前时间
60
+ * @returns {boolean} 是否已到或超过截止(当前代结束后应停止)
61
+ */
62
+ export function pastDeadline(deadline, now = Date.now()) {
63
+ return deadline != null && now >= deadline
64
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * 按信誉分排序后的一次性邻接构建(避免每节点重复 sort)。
3
+ */
4
+
5
+ /**
6
+ * @param {string[]} ids 参与构图的节点 id
7
+ * @param {(id: string) => number} scoreOf 信誉分
8
+ * @param {number} maxPeers 每节点最多邻居数
9
+ * @returns {Map<string, string[]>} id → 邻居 id 列表(按 scoreOf 降序取前 maxPeers)
10
+ */
11
+ export function buildRankedNeighborAdj(ids, scoreOf, maxPeers) {
12
+ const scored = ids.map(id => ({ id, score: scoreOf(id) }))
13
+ scored.sort((a, b) => b.score - a.score)
14
+ const sorted = scored.map(s => s.id)
15
+ /** @type {Map<string, string[]>} */
16
+ const adj = new Map()
17
+ for (const id of ids) {
18
+ /** @type {string[]} */
19
+ const peers = []
20
+ for (const peer of sorted) {
21
+ if (peer === id) continue
22
+ peers.push(peer)
23
+ if (peers.length >= maxPeers) break
24
+ }
25
+ adj.set(id, peers)
26
+ }
27
+ return adj
28
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * 密码学完整性 vs 主观信誉:持副本观察者独立证伪,无副本者才受信誉博弈影响。
3
+ */
4
+
5
+ /**
6
+ * @param {import('./model.mjs').SimObserver} observer 观察者
7
+ * @param {import('./scenarios.mjs').SimScenario} scenario 场景
8
+ * @returns {boolean} 是否持有本地归档副本(可独立重算 digest)
9
+ */
10
+ export function observerHasLocalReplica(observer, scenario) {
11
+ const groupSize = scenario.groupSize ?? 8
12
+ const honestShare = (scenario.honestCount ?? 0) / Math.max(1, groupSize)
13
+ const archiveHeavy = observer.trustedPeers.length >= 2
14
+ && (scenario.behaviorDist?.archiveSubmitRate?.mean ?? 0) >= 0.15
15
+ return (honestShare >= 0.75 && observer.trustedPeers.length >= 1) || archiveHeavy
16
+ }
17
+
18
+ /**
19
+ * @param {import('./model.mjs').SimNode} attacker 攻击节点
20
+ * @param {import('./model.mjs').SimObserver} observer 观察者
21
+ * @param {import('./scenarios.mjs').SimScenario} scenario 场景
22
+ * @param {object} simulationContext 仿真上下文
23
+ * @returns {boolean} 完整性层是否已证伪该攻击(defense≈1)
24
+ */
25
+ export function integrityDefendsAgainst(attacker, observer, scenario, simulationContext) {
26
+ if (!observerHasLocalReplica(observer, scenario)) return false
27
+ if (attacker.attack === 'equivocator') {
28
+ const key = `${attacker.id}:${observer.id}`
29
+ return (simulationContext.equivocationByObserver?.get(key) ?? 0) > 0
30
+ }
31
+ if (attacker.attack === 'archive_forger' || attacker.attack === 'lazy_chunk')
32
+ return simulationContext.verifiedForgeryByObserver?.get(observer.id)?.has(attacker.id) === true
33
+ return false
34
+ }
35
+
36
+ /**
37
+ * @param {import('./model.mjs').SimObserver[]} observers 观察者列表
38
+ * @param {import('./scenarios.mjs').SimScenario} scenario 场景
39
+ * @returns {number} 持副本观察者比例 0..1
40
+ */
41
+ export function replicaObserverFraction(observers, scenario) {
42
+ if (!observers.length) return 0
43
+ let n = 0
44
+ for (const obs of observers)
45
+ if (observerHasLocalReplica(obs, scenario)) n++
46
+ return n / observers.length
47
+ }
48
+
49
+ /**
50
+ * 归档仲裁正确率:持副本者独立重算;无副本者走信誉 quorum。
51
+ * @param {number} quorumAccuracy 纯信誉 quorum 准确率
52
+ * @param {number} replicaFraction 持副本观察者比例
53
+ * @returns {number} 综合准确率
54
+ */
55
+ export function blendArchiveQuorumAccuracy(quorumAccuracy, replicaFraction) {
56
+ const replicaBoost = replicaFraction * 0.85
57
+ return Math.max(quorumAccuracy, replicaBoost)
58
+ }