@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,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
|
+
}
|
package/sim/duration.mjs
ADDED
|
@@ -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
|
+
}
|