@steve02081504/fount-p2p 0.0.0
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/README.md +62 -0
- package/core/bytes_codec.mjs +51 -0
- package/core/canonical_json.mjs +21 -0
- package/core/composite_key.mjs +91 -0
- package/core/constants.mjs +53 -0
- package/core/entity_id.mjs +53 -0
- package/core/entity_id_parse.mjs +40 -0
- package/core/evfs_logical_path.mjs +15 -0
- package/core/hexIds.mjs +73 -0
- package/core/hlc.mjs +114 -0
- package/core/random_id.mjs +9 -0
- package/crypto/channel.mjs +101 -0
- package/crypto/checkpoint_sign.mjs +67 -0
- package/crypto/crypto.mjs +158 -0
- package/crypto/key.mjs +431 -0
- package/dag/canonicalize_row.mjs +65 -0
- package/dag/event_query.mjs +15 -0
- package/dag/index.mjs +335 -0
- package/dag/storage.mjs +333 -0
- package/dag/strip_extensions.mjs +18 -0
- package/dag/tunables.json +6 -0
- package/discovery/bt.mjs +260 -0
- package/discovery/index.mjs +134 -0
- package/discovery/mdns.mjs +138 -0
- package/discovery/nostr.mjs +273 -0
- package/entity/files/acl.mjs +38 -0
- package/entity/files/evfs.mjs +189 -0
- package/entity/files/evfs_ref.mjs +37 -0
- package/entity/files/manifest_acl_registry.mjs +93 -0
- package/entity/files/replica_host_cache.mjs +46 -0
- package/entity/files/url.mjs +19 -0
- package/entity/hosting_registry.mjs +47 -0
- package/entity/localized_core.mjs +141 -0
- package/entity/logical_entity.mjs +25 -0
- package/entity/logical_entity_id_registry.mjs +40 -0
- package/entity/node_hash.mjs +15 -0
- package/entity/presentation_registry.mjs +44 -0
- package/entity/profile.mjs +256 -0
- package/entity/replica.mjs +20 -0
- package/entity/session_snapshot_registry.mjs +38 -0
- package/federation/chunk_fetch_pending.mjs +128 -0
- package/federation/chunk_fetch_scheduler.mjs +97 -0
- package/federation/dag_order_cache.mjs +124 -0
- package/federation/dedupe_slot.mjs +33 -0
- package/federation/message_rate_limit.mjs +23 -0
- package/federation/operator_key_chain.mjs +200 -0
- package/federation/topo_order_memo.mjs +44 -0
- package/federation/volatile_streams.mjs +52 -0
- package/federation/want_ids.mjs +158 -0
- package/files/assemble.mjs +220 -0
- package/files/assemble_stream.mjs +129 -0
- package/files/chunk_fetch.mjs +113 -0
- package/files/chunk_fetch_verify.mjs +23 -0
- package/files/chunk_provider_registry.mjs +70 -0
- package/files/chunk_responder.mjs +79 -0
- package/files/chunk_store.mjs +87 -0
- package/files/manifest.mjs +99 -0
- package/files/transfer_key.mjs +87 -0
- package/files/transfer_key_registry.mjs +108 -0
- package/governance/branch.mjs +230 -0
- package/governance/join_pow.mjs +165 -0
- package/governance/owner_succession_ballot.mjs +57 -0
- package/governance/tunables.json +7 -0
- package/index.mjs +39 -0
- package/link/channel_mux.mjs +222 -0
- package/link/frame.mjs +246 -0
- package/link/handshake.mjs +175 -0
- package/link/link.mjs +617 -0
- package/link/rtc.mjs +109 -0
- package/link/sdp_fingerprint.mjs +25 -0
- package/mailbox/consumer_registry.mjs +71 -0
- package/mailbox/deliver_or_store.mjs +190 -0
- package/mailbox/importance.mjs +72 -0
- package/mailbox/parse.mjs +73 -0
- package/mailbox/prune.mjs +106 -0
- package/mailbox/rate.mjs +86 -0
- package/mailbox/settings.mjs +53 -0
- package/mailbox/store.mjs +256 -0
- package/mailbox/tunables.json +12 -0
- package/mailbox/wire.mjs +40 -0
- package/node/denylist.mjs +309 -0
- package/node/entity_store.mjs +214 -0
- package/node/identity.mjs +99 -0
- package/node/instance.mjs +115 -0
- package/node/network.mjs +306 -0
- package/node/personal_block.mjs +286 -0
- package/node/reputation_store.mjs +326 -0
- package/node/retention_policy.mjs +82 -0
- package/node/signaling_config.mjs +32 -0
- package/node/storage.mjs +30 -0
- package/node/storage_plugins.mjs +71 -0
- package/node/user_paths.mjs +10 -0
- package/overlay/index.mjs +191 -0
- package/package.json +73 -0
- package/permissions/bitmask.mjs +35 -0
- package/permissions/evaluator.mjs +90 -0
- package/permissions/index.mjs +12 -0
- package/permissions/layered.mjs +39 -0
- package/registries/action.mjs +94 -0
- package/registries/event_type.mjs +75 -0
- package/registries/inbound.mjs +90 -0
- package/registries/p2p_viewer.mjs +35 -0
- package/registries/part_path.mjs +35 -0
- package/registries/room_provider.mjs +47 -0
- package/reputation/engine.mjs +458 -0
- package/reputation/math.mjs +73 -0
- package/reputation/pick_score.mjs +11 -0
- package/reputation/relay_dedupe.mjs +22 -0
- package/reputation/tunables.json +36 -0
- package/rooms/scoped_link.mjs +185 -0
- package/schemas/discovery.mjs +110 -0
- package/schemas/federation_pull.mjs +102 -0
- package/schemas/mailbox.mjs +37 -0
- package/schemas/remote_event.mjs +17 -0
- package/timeline/append_core.mjs +54 -0
- package/timeline/materialize_runner.mjs +26 -0
- package/timeline/prune.mjs +39 -0
- package/timeline/retention.mjs +54 -0
- package/timeline/retention_runner.mjs +35 -0
- package/timeline/verify_remote.mjs +25 -0
- package/transport/group_link_set.mjs +324 -0
- package/transport/ice_servers.mjs +72 -0
- package/transport/link_registry.mjs +749 -0
- package/transport/peer_identity_maps.mjs +94 -0
- package/transport/peer_pool.mjs +355 -0
- package/transport/remote_user_room.mjs +109 -0
- package/transport/room_scopes.mjs +2 -0
- package/transport/room_wire_action.mjs +19 -0
- package/transport/rtc_connection_budget.mjs +180 -0
- package/transport/rtc_mdns_filter.mjs +118 -0
- package/transport/stale_peer_log.mjs +50 -0
- package/transport/user_room.mjs +263 -0
- package/trust_graph/build.mjs +99 -0
- package/trust_graph/cache.mjs +44 -0
- package/trust_graph/engine.mjs +184 -0
- package/trust_graph/registry.mjs +38 -0
- package/trust_graph/resolve.mjs +105 -0
- package/trust_graph/send.mjs +88 -0
- package/trust_graph/tunables.json +13 -0
- package/utils/async_mutex.mjs +69 -0
- package/utils/debug_log.mjs +16 -0
- package/utils/duration.mjs +28 -0
- package/utils/json_io.mjs +57 -0
- package/utils/lru.mjs +22 -0
- package/utils/map_pool.mjs +27 -0
- package/wire/group_part.mjs +43 -0
- package/wire/ingress.mjs +58 -0
- package/wire/part_common.mjs +55 -0
- package/wire/part_fanout.mjs +71 -0
- package/wire/part_ingress.mjs +172 -0
- package/wire/part_invoke.mjs +81 -0
- package/wire/rate_bucket.mjs +49 -0
- package/wire/volatile_signature.mjs +76 -0
- package/wire/wait.mjs +127 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { assertHex64, isHex64, normalizeHex64 } from '../core/hexIds.mjs'
|
|
2
|
+
import { recordWantIdsBackoff, wantIdsPeerKey } from '../federation/want_ids.mjs'
|
|
3
|
+
import {
|
|
4
|
+
applyDecayCollusionAfterSlashPure,
|
|
5
|
+
applyReputationResetToScoresPure,
|
|
6
|
+
applySubjectiveSlashPure,
|
|
7
|
+
bumpChunkStorageReputationPure,
|
|
8
|
+
bumpReputationOnRelayPure,
|
|
9
|
+
ensureReputationShape,
|
|
10
|
+
isQuarantinedPure,
|
|
11
|
+
observeBehaviorSamplePure,
|
|
12
|
+
penalizeArchiveServeMismatchPure,
|
|
13
|
+
penalizeChunkStorageFailurePure,
|
|
14
|
+
pruneReputationFile,
|
|
15
|
+
recordGossipAllUnknownWantPure,
|
|
16
|
+
recordMessageRateViolationPure,
|
|
17
|
+
resolveSlashAlertTtlMsPure,
|
|
18
|
+
seedMemberReputationFromIntroducerPure,
|
|
19
|
+
} from '../reputation/engine.mjs'
|
|
20
|
+
import { pickNodeScoreFromReputation } from '../reputation/pick_score.mjs'
|
|
21
|
+
import reputationTunables from '../reputation/tunables.json' with { type: 'json' }
|
|
22
|
+
import { invalidateTrustGraphCache } from '../trust_graph/cache.mjs'
|
|
23
|
+
import { withAsyncMutex } from '../utils/async_mutex.mjs'
|
|
24
|
+
|
|
25
|
+
import { getNodeDir, getNodeLogger, isNodeInitialized } from './instance.mjs'
|
|
26
|
+
import { readNodeJsonSync, writeNodeJsonSync } from './storage.mjs'
|
|
27
|
+
|
|
28
|
+
const DATA_NAME = 'reputation'
|
|
29
|
+
|
|
30
|
+
/** @type {((opts: object) => Promise<boolean>) | null} */
|
|
31
|
+
let blockReputationHandler = null
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Shell Load 时注册:公开 block/unblock → 信誉传导。
|
|
35
|
+
* @param {(opts: object) => Promise<boolean>} handler block/unblock 信誉传导回调
|
|
36
|
+
* @returns {void}
|
|
37
|
+
*/
|
|
38
|
+
export function registerBlockReputationHandler(handler) {
|
|
39
|
+
blockReputationHandler = handler
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** @returns {void} */
|
|
43
|
+
export function unregisterBlockReputationHandler() {
|
|
44
|
+
blockReputationHandler = null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @typedef {{
|
|
49
|
+
* byNodeHash: Record<string, { score: number, offenseStreak?: number, lastOffenseAt?: number, blockPenalties?: Record<string, { penalty: number, appliedAt: number, decayedRefund?: number }> }>
|
|
50
|
+
* wantUnknownHits: Array<{ peerNodeHash: string, t: number }>
|
|
51
|
+
* relayBumpSeen: Array<{ peerNodeHash: string, key: string, t: number }>
|
|
52
|
+
* }} ReputationFile
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
/** @type {ReputationFile | null} */
|
|
56
|
+
let reputationCache = null
|
|
57
|
+
|
|
58
|
+
/** @type {string | null} */
|
|
59
|
+
let reputationCacheNodeDir = null
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {(data: ReputationFile) => void | Promise<void>} mutator 突变
|
|
63
|
+
* @returns {Promise<void>}
|
|
64
|
+
*/
|
|
65
|
+
export function mutateReputation(mutator) {
|
|
66
|
+
return withAsyncMutex('reputation', async () => {
|
|
67
|
+
const data = loadReputation()
|
|
68
|
+
await mutator(data)
|
|
69
|
+
saveReputation(data)
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @returns {ReputationFile} 节点级信誉表
|
|
75
|
+
*/
|
|
76
|
+
export function loadReputation() {
|
|
77
|
+
const nodeDir = isNodeInitialized() ? getNodeDir() : ''
|
|
78
|
+
if (reputationCache && reputationCacheNodeDir === nodeDir) return reputationCache
|
|
79
|
+
reputationCacheNodeDir = nodeDir
|
|
80
|
+
reputationCache = ensureReputationShape(readNodeJsonSync(DATA_NAME) || {})
|
|
81
|
+
return reputationCache
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {ReputationFile} data 信誉表
|
|
86
|
+
* @returns {void}
|
|
87
|
+
*/
|
|
88
|
+
export function saveReputation(data) {
|
|
89
|
+
pruneReputationFile(data)
|
|
90
|
+
writeNodeJsonSync(DATA_NAME, data)
|
|
91
|
+
reputationCache = data
|
|
92
|
+
invalidateTrustGraphCache()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @param {string} peerNodeHash 对端节点
|
|
97
|
+
* @param {string} [dedupeKey] 去重键
|
|
98
|
+
* @returns {Promise<void>}
|
|
99
|
+
*/
|
|
100
|
+
export function bumpReputationOnRelay(peerNodeHash, dedupeKey) {
|
|
101
|
+
return mutateReputation(data => {
|
|
102
|
+
bumpReputationOnRelayPure(data, peerNodeHash, dedupeKey)
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @param {string} groupId 群 ID(仅用于 want 去重键)
|
|
108
|
+
* @param {string} peerNodeHash 请求方
|
|
109
|
+
* @returns {Promise<void>}
|
|
110
|
+
*/
|
|
111
|
+
export function recordGossipAllUnknownWant(groupId, peerNodeHash) {
|
|
112
|
+
return mutateReputation(data => {
|
|
113
|
+
if (recordGossipAllUnknownWantPure(data, peerNodeHash))
|
|
114
|
+
recordWantIdsBackoff(wantIdsPeerKey(groupId, peerNodeHash))
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @param {string} peerNodeHash 对端
|
|
120
|
+
* @param {number} [excessRatio=1] 超速超额比例 0..1
|
|
121
|
+
* @returns {Promise<void>}
|
|
122
|
+
*/
|
|
123
|
+
export function recordMessageRateViolation(peerNodeHash, excessRatio = 1) {
|
|
124
|
+
const id = String(peerNodeHash || '').trim()
|
|
125
|
+
if (!id) return Promise.resolve()
|
|
126
|
+
return mutateReputation(data => {
|
|
127
|
+
recordMessageRateViolationPure(data, id, undefined, excessRatio)
|
|
128
|
+
observeBehaviorSamplePure(data, id, 1)
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* @param {string} peerNodeHash 对端
|
|
134
|
+
* @param {number} sample 行为样本强度
|
|
135
|
+
* @returns {Promise<boolean>} 是否触发异常隔离
|
|
136
|
+
*/
|
|
137
|
+
export async function observePeerBehavior(peerNodeHash, sample) {
|
|
138
|
+
const id = String(peerNodeHash || '').trim()
|
|
139
|
+
if (!id) return false
|
|
140
|
+
let anomaly = false
|
|
141
|
+
await mutateReputation(data => {
|
|
142
|
+
const result = observeBehaviorSamplePure(data, id, sample)
|
|
143
|
+
anomaly = result.anomaly
|
|
144
|
+
})
|
|
145
|
+
return anomaly
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* @param {string} peerNodeHash 对端
|
|
150
|
+
* @returns {boolean} 是否处于本地隔离
|
|
151
|
+
*/
|
|
152
|
+
export function isPeerQuarantined(peerNodeHash) {
|
|
153
|
+
return isQuarantinedPure(loadReputation(), peerNodeHash)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* @param {string} storagePeerKey 责任方
|
|
158
|
+
* @returns {void}
|
|
159
|
+
*/
|
|
160
|
+
export function bumpChunkStorageReputation(storagePeerKey) {
|
|
161
|
+
const id = String(storagePeerKey || '').trim()
|
|
162
|
+
if (!id) return
|
|
163
|
+
void mutateReputation(data => {
|
|
164
|
+
bumpChunkStorageReputationPure(data, id)
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* @param {string} blamePeerKey 责任方
|
|
170
|
+
* @returns {void}
|
|
171
|
+
*/
|
|
172
|
+
export function penalizeChunkStorageFailure(blamePeerKey) {
|
|
173
|
+
const id = String(blamePeerKey || '').trim()
|
|
174
|
+
if (!id) return
|
|
175
|
+
void mutateReputation(data => {
|
|
176
|
+
penalizeChunkStorageFailurePure(data, id)
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* @param {string} peerNodeHash 对端 nodeHash
|
|
182
|
+
* @returns {void}
|
|
183
|
+
*/
|
|
184
|
+
export function penalizeArchiveServeMismatch(peerNodeHash) {
|
|
185
|
+
const id = String(peerNodeHash || '').trim()
|
|
186
|
+
if (!id) return
|
|
187
|
+
void mutateReputation(data => {
|
|
188
|
+
penalizeArchiveServeMismatchPure(data, id)
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* @param {object | null | undefined} groupSettings 群设置
|
|
194
|
+
* @returns {number} 毫秒
|
|
195
|
+
*/
|
|
196
|
+
export function resolveSlashAlertTtlMs(groupSettings) {
|
|
197
|
+
return resolveSlashAlertTtlMsPure(groupSettings)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* @param {object} alert VOLATILE slash 载荷
|
|
202
|
+
* @returns {boolean} 是否已应用
|
|
203
|
+
*/
|
|
204
|
+
export async function applyVolatileSlashAlert(alert) {
|
|
205
|
+
const expiresAt = Number(alert?.expiresAt)
|
|
206
|
+
if (Number.isFinite(expiresAt) && Date.now() > expiresAt) return false
|
|
207
|
+
const target = normalizeHex64(alert?.targetPubKeyHash)
|
|
208
|
+
const sender = normalizeHex64(alert?.sender)
|
|
209
|
+
if (!isHex64(target) || !isHex64(sender)) return false
|
|
210
|
+
const claim = Number.isFinite(Number(alert?.claim)) ? Number(alert.claim) : reputationTunables.slashDefaultClaim
|
|
211
|
+
await mutateReputation(data => {
|
|
212
|
+
applySubjectiveSlashPure(data, target, sender, claim, false)
|
|
213
|
+
})
|
|
214
|
+
return true
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* @param {string} senderPubKeyHash 签发者
|
|
219
|
+
* @param {{ targetPubKeyHash: string, claim?: number }} content Slash 内容
|
|
220
|
+
* @param {object} [groupSettings] 群设置
|
|
221
|
+
* @returns {object} VOLATILE 载荷
|
|
222
|
+
*/
|
|
223
|
+
export function buildAndApplyUnverifiedSlashAlert(senderPubKeyHash, content, groupSettings = {}) {
|
|
224
|
+
const targetPubKeyHash = assertHex64(content.targetPubKeyHash, 'slash target')
|
|
225
|
+
const claim = Number.isFinite(Number(content.claim)) ? Number(content.claim) : reputationTunables.slashDefaultClaim
|
|
226
|
+
const sender = assertHex64(senderPubKeyHash, 'slash sender')
|
|
227
|
+
const ttl = resolveSlashAlertTtlMs(groupSettings)
|
|
228
|
+
return {
|
|
229
|
+
type: 'reputation_slash_alert',
|
|
230
|
+
targetPubKeyHash,
|
|
231
|
+
sender,
|
|
232
|
+
claim,
|
|
233
|
+
expiresAt: Date.now() + ttl,
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* @param {string} username 用户(readEvents 回调用)
|
|
239
|
+
* @param {string} groupId 群
|
|
240
|
+
* @param {object} event reputation_slash 事件
|
|
241
|
+
* @param {(username: string, groupId: string) => Promise<object[]>} readEvents 读 DAG 事件
|
|
242
|
+
* @returns {Promise<void>}
|
|
243
|
+
*/
|
|
244
|
+
export async function applySubjectiveSlashFromEvent(username, groupId, event, readEvents) {
|
|
245
|
+
if (event.type !== 'reputation_slash') return
|
|
246
|
+
const { content } = event
|
|
247
|
+
const target = content.targetPubKeyHash.trim().toLowerCase()
|
|
248
|
+
const sender = event.sender.trim().toLowerCase()
|
|
249
|
+
|
|
250
|
+
await mutateReputation(async data => {
|
|
251
|
+
const verified = content.verified && await verifySlashProof(username, groupId, content, readEvents)
|
|
252
|
+
const claim = Number(content.claim ?? (verified ? reputationTunables.slashVerifiedDefaultClaim : reputationTunables.slashUnverifiedDefaultClaim))
|
|
253
|
+
applySubjectiveSlashPure(data, target, sender, claim, verified)
|
|
254
|
+
})
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* @param {string} username 用户
|
|
259
|
+
* @param {string} groupId 群
|
|
260
|
+
* @param {object} content slash 内容
|
|
261
|
+
* @param {(username: string, groupId: string) => Promise<object[]>} readEvents 读 DAG
|
|
262
|
+
* @returns {Promise<boolean>} 是否找到 proof 对应事件
|
|
263
|
+
*/
|
|
264
|
+
async function verifySlashProof(username, groupId, content, readEvents) {
|
|
265
|
+
const eventId = content?.proof?.eventId?.trim().toLowerCase()
|
|
266
|
+
if (!eventId) return false
|
|
267
|
+
const events = await readEvents(username, groupId)
|
|
268
|
+
return events.some(event => event.id === eventId)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* @param {string} targetPubKeyHash 目标
|
|
273
|
+
* @param {Array<{ from?: string, to?: string }>} inviteEdges 邀请边
|
|
274
|
+
* @returns {void}
|
|
275
|
+
*/
|
|
276
|
+
export function applyDecayCollusionAfterSlash(targetPubKeyHash, inviteEdges) {
|
|
277
|
+
void mutateReputation(data => {
|
|
278
|
+
const applied = applyDecayCollusionAfterSlashPure(data, targetPubKeyHash, inviteEdges)
|
|
279
|
+
if (applied.length)
|
|
280
|
+
getNodeLogger().warn?.('reputation: collusion decay after slash', {
|
|
281
|
+
target: targetPubKeyHash.trim().toLowerCase(),
|
|
282
|
+
upstreamCount: applied.length,
|
|
283
|
+
hops: applied.map(row => row.hop),
|
|
284
|
+
})
|
|
285
|
+
})
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* @param {string} targetPubKeyHash 目标
|
|
290
|
+
* @returns {void}
|
|
291
|
+
*/
|
|
292
|
+
export function applyReputationResetToScores(targetPubKeyHash) {
|
|
293
|
+
void mutateReputation(data => {
|
|
294
|
+
applyReputationResetToScoresPure(data, targetPubKeyHash)
|
|
295
|
+
})
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* @param {string} memberPubKeyHash 新成员
|
|
300
|
+
* @param {string} [introducerPubKeyHash] 介绍者
|
|
301
|
+
* @param {number} [repEdge] 边信任
|
|
302
|
+
* @param {number} [powBonus=0] 入群 PoW 自愿封顶加成
|
|
303
|
+
* @returns {void}
|
|
304
|
+
*/
|
|
305
|
+
export function seedMemberReputationFromIntroducer(memberPubKeyHash, introducerPubKeyHash, repEdge, powBonus = 0) {
|
|
306
|
+
void mutateReputation(data => {
|
|
307
|
+
seedMemberReputationFromIntroducerPure(data, memberPubKeyHash, introducerPubKeyHash, repEdge, undefined, powBonus)
|
|
308
|
+
})
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* @param {string} nodeId 64 位十六进制
|
|
313
|
+
* @returns {number} 信誉分
|
|
314
|
+
*/
|
|
315
|
+
export function pickNodeScore(nodeId) {
|
|
316
|
+
return pickNodeScoreFromReputation(loadReputation(), nodeId)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* @param {object} opts applyFollowedBlockSignal 参数
|
|
321
|
+
* @returns {Promise<boolean>} 是否已应用
|
|
322
|
+
*/
|
|
323
|
+
export async function applyBlockReputationSignal(opts) {
|
|
324
|
+
if (!blockReputationHandler) return false
|
|
325
|
+
return blockReputationHandler(opts)
|
|
326
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { sortedPrevEventIds } from '../dag/index.mjs'
|
|
2
|
+
import {
|
|
3
|
+
authzFoldOrderIds,
|
|
4
|
+
descendantClosureFromTip,
|
|
5
|
+
} from '../governance/branch.mjs'
|
|
6
|
+
import { getPermissionAnchorTypes } from '../registries/event_type.mjs'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {string[]} order 拓扑序 id 列表
|
|
10
|
+
* @param {Map<string, object>} byId id → 事件
|
|
11
|
+
* @returns {number} 最早须保留的事件下标
|
|
12
|
+
*/
|
|
13
|
+
export function permissionAnchorStartIndex(order, byId) {
|
|
14
|
+
let anchor = order.length
|
|
15
|
+
for (let index = order.length - 1; index >= 0; index--) {
|
|
16
|
+
const ev = byId.get(order[index])
|
|
17
|
+
if (ev && getPermissionAnchorTypes().has(ev.type)) anchor = index
|
|
18
|
+
}
|
|
19
|
+
return anchor
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 在共识分支上计算须保留的事件 id(连通子图,不用拓扑下标切片)。
|
|
24
|
+
* @param {string[]} order 规范拓扑序
|
|
25
|
+
* @param {Map<string, object>} byId id → 事件
|
|
26
|
+
* @param {object} opts 保留策略
|
|
27
|
+
* @param {number} opts.maxDepth 分支上最大事件深度
|
|
28
|
+
* @param {number} opts.cutoffWall 最早保留的 HLC wall
|
|
29
|
+
* @param {Set<string>} opts.anchorTypes 权限锚点事件类型
|
|
30
|
+
* @param {string | null} [opts.checkpointTipId] checkpoint 尖
|
|
31
|
+
* @param {string | null} [opts.branchTipId] 共识分支尖
|
|
32
|
+
* @returns {Set<string>} 保留 id
|
|
33
|
+
*/
|
|
34
|
+
export function computeRetentionKeepIds(order, byId, opts) {
|
|
35
|
+
const { maxDepth, cutoffWall, anchorTypes, checkpointTipId, branchTipId } = opts
|
|
36
|
+
const branchOrder = authzFoldOrderIds(order, byId, branchTipId)
|
|
37
|
+
const branchSet = new Set(branchOrder)
|
|
38
|
+
if (!branchSet.size) return new Set()
|
|
39
|
+
|
|
40
|
+
/** @type {Set<string>} */
|
|
41
|
+
const keep = new Set()
|
|
42
|
+
/** @type {Set<string>} */
|
|
43
|
+
const ancestorSeeds = new Set()
|
|
44
|
+
|
|
45
|
+
if (checkpointTipId && branchSet.has(checkpointTipId))
|
|
46
|
+
for (const id of descendantClosureFromTip(checkpointTipId, byId))
|
|
47
|
+
if (branchSet.has(id)) keep.add(id)
|
|
48
|
+
|
|
49
|
+
for (let index = branchOrder.length - 1; index >= 0; index--) {
|
|
50
|
+
const ev = byId.get(branchOrder[index])
|
|
51
|
+
if (ev && anchorTypes.has(ev.type)) {
|
|
52
|
+
ancestorSeeds.add(branchOrder[index])
|
|
53
|
+
break
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (const id of branchOrder) {
|
|
58
|
+
const ev = byId.get(id)
|
|
59
|
+
const wall = Number(ev?.hlc?.wall ?? 0)
|
|
60
|
+
if (wall >= cutoffWall) ancestorSeeds.add(id)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (branchOrder.length > maxDepth)
|
|
64
|
+
for (const id of branchOrder.slice(-maxDepth))
|
|
65
|
+
ancestorSeeds.add(id)
|
|
66
|
+
|
|
67
|
+
const stack = [...ancestorSeeds]
|
|
68
|
+
while (stack.length) {
|
|
69
|
+
const id = stack.pop()
|
|
70
|
+
if (!id || !branchSet.has(id) || keep.has(id)) continue
|
|
71
|
+
keep.add(id)
|
|
72
|
+
const event = byId.get(id)
|
|
73
|
+
if (!event) continue
|
|
74
|
+
for (const parentId of sortedPrevEventIds(event.prev_event_ids))
|
|
75
|
+
if (branchSet.has(parentId)) stack.push(parentId)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!keep.size)
|
|
79
|
+
for (const id of branchOrder) keep.add(id)
|
|
80
|
+
|
|
81
|
+
return keep
|
|
82
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import process from 'node:process'
|
|
2
|
+
|
|
3
|
+
/** @typedef {'none' | 'rewrite-loopback' | 'drop'} MdnsCandidatePolicy */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {{
|
|
7
|
+
* relayOverride: string[] | null
|
|
8
|
+
* mdnsPolicy: MdnsCandidatePolicy
|
|
9
|
+
* trickleIceOff: boolean
|
|
10
|
+
* }} SignalingRuntimeConfig
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 生产默认:win32 丢弃 .local host candidate;其它平台不过滤。
|
|
15
|
+
* @returns {SignalingRuntimeConfig} 生产默认信令配置
|
|
16
|
+
*/
|
|
17
|
+
export function defaultSignalingRuntimeConfig() {
|
|
18
|
+
const mdnsPolicy = process.platform === 'win32' ? 'drop' : 'none'
|
|
19
|
+
return {
|
|
20
|
+
relayOverride: null,
|
|
21
|
+
mdnsPolicy,
|
|
22
|
+
trickleIceOff: mdnsPolicy !== 'none',
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 生产默认信令配置;测试通过 initNode({ signaling }) 注入。
|
|
28
|
+
* @returns {SignalingRuntimeConfig} 生产默认信令配置
|
|
29
|
+
*/
|
|
30
|
+
export function resolveSignalingRuntimeConfig() {
|
|
31
|
+
return defaultSignalingRuntimeConfig()
|
|
32
|
+
}
|
package/node/storage.mjs
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
|
|
3
|
+
import { readJsonFileSync, writeJsonFileSync } from '../utils/json_io.mjs'
|
|
4
|
+
|
|
5
|
+
import { getNodeDir } from './instance.mjs'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {string} name 不含扩展名的配置文件名
|
|
9
|
+
* @returns {string} 绝对路径
|
|
10
|
+
*/
|
|
11
|
+
export function nodeJsonPath(name) {
|
|
12
|
+
return path.join(getNodeDir(), `${name}.json`)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {string} name 配置文件名
|
|
17
|
+
* @returns {object | null} 解析后的 JSON 或 null
|
|
18
|
+
*/
|
|
19
|
+
export function readNodeJsonSync(name) {
|
|
20
|
+
return readJsonFileSync(nodeJsonPath(name))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} name 配置文件名
|
|
25
|
+
* @param {unknown} data 数据
|
|
26
|
+
* @returns {void}
|
|
27
|
+
*/
|
|
28
|
+
export function writeNodeJsonSync(name, data) {
|
|
29
|
+
writeJsonFileSync(nodeJsonPath(name), data)
|
|
30
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { mkdir, writeFile, readFile, unlink } from 'node:fs/promises'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 群组分块存储插件:put/get/delete + 不透明 storageLocator。
|
|
6
|
+
* S3、多副本等后端由 shell 注入实现,本包只提供本地参考实现。
|
|
7
|
+
*
|
|
8
|
+
* @typedef {{
|
|
9
|
+
* putChunk: (groupId: string, chunkHash: string, data: Uint8Array) => Promise<{ storageLocator: string }>,
|
|
10
|
+
* getChunk: (locator: string) => Promise<Uint8Array>,
|
|
11
|
+
* deleteChunk: (locator: string) => Promise<void>,
|
|
12
|
+
* }} GroupStoragePlugin
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {unknown} error 存储删除错误
|
|
17
|
+
* @returns {boolean} 是否为「文件不存在」
|
|
18
|
+
*/
|
|
19
|
+
function isEnoent(error) {
|
|
20
|
+
return /** @type {{ code?: string }} */ error?.code === 'ENOENT'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 默认:本地目录 {baseDir}/groups/{groupId}/chunks/(baseDir 一般为用户 shells/chat)
|
|
25
|
+
*
|
|
26
|
+
* @param {string} baseDir 绝对路径
|
|
27
|
+
* @returns {GroupStoragePlugin} 本地文件系统实现的 put/get/delete
|
|
28
|
+
*/
|
|
29
|
+
export function createLocalStoragePlugin(baseDir) {
|
|
30
|
+
return {
|
|
31
|
+
storagePeerId: 'local',
|
|
32
|
+
/**
|
|
33
|
+
* @param {string} groupId 群组 id
|
|
34
|
+
* @param {string} chunkHash 分块内容哈希(文件名)
|
|
35
|
+
* @param {Uint8Array} data 原始字节
|
|
36
|
+
* @returns {Promise<{ storageLocator: string }>} `local:` 前缀的定位符
|
|
37
|
+
*/
|
|
38
|
+
async putChunk(groupId, chunkHash, data) {
|
|
39
|
+
const dir = join(baseDir, 'groups', groupId, 'chunks')
|
|
40
|
+
await mkdir(dir, { recursive: true })
|
|
41
|
+
const name = `${chunkHash}.bin`
|
|
42
|
+
await writeFile(join(dir, name), data)
|
|
43
|
+
return { storageLocator: `local:${groupId}/chunks/${name}` }
|
|
44
|
+
},
|
|
45
|
+
/**
|
|
46
|
+
* @param {string} locator `local:...` 格式
|
|
47
|
+
* @returns {Promise<Uint8Array>} 文件内容
|
|
48
|
+
*/
|
|
49
|
+
async getChunk(locator) {
|
|
50
|
+
const localLocatorMatch = String(locator).match(/^local:([^/]+)\/chunks\/(.+)$/)
|
|
51
|
+
if (!localLocatorMatch) throw new Error('Invalid local locator')
|
|
52
|
+
const chunkPath = join(baseDir, 'groups', localLocatorMatch[1], 'chunks', localLocatorMatch[2])
|
|
53
|
+
return new Uint8Array(await readFile(chunkPath))
|
|
54
|
+
},
|
|
55
|
+
/**
|
|
56
|
+
* @param {string} locator `local:...` 格式
|
|
57
|
+
* @returns {Promise<void>}
|
|
58
|
+
*/
|
|
59
|
+
async deleteChunk(locator) {
|
|
60
|
+
const localLocatorMatch = String(locator).match(/^local:([^/]+)\/chunks\/(.+)$/)
|
|
61
|
+
if (!localLocatorMatch) return
|
|
62
|
+
const chunkPath = join(baseDir, 'groups', localLocatorMatch[1], 'chunks', localLocatorMatch[2])
|
|
63
|
+
try {
|
|
64
|
+
await unlink(chunkPath)
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (!isEnoent(error)) throw error
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
}
|