@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,90 @@
|
|
|
1
|
+
/** @typedef {import('../wire/part_invoke.mjs').PartInvokeResponse} PartInvokeResponse */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {{
|
|
5
|
+
* replicaUsername?: string
|
|
6
|
+
* requesterNodeHash?: string | null
|
|
7
|
+
* groupId?: string
|
|
8
|
+
* peerId?: string
|
|
9
|
+
* }} InboundContext
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {(ctx: InboundContext, message: object) => Promise<PartInvokeResponse | null>} RpcInboundHandler
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {(ctx: InboundContext, message: object) => Promise<void>} DeliveryInboundHandler
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** @type {Map<string, RpcInboundHandler>} */
|
|
21
|
+
const rpcHandlers = new Map()
|
|
22
|
+
|
|
23
|
+
/** @type {Map<string, DeliveryInboundHandler>} */
|
|
24
|
+
const deliveryHandlers = new Map()
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {string} type 入站 RPC 类型(part_invoke 等)
|
|
28
|
+
* @param {RpcInboundHandler} handler 处理器
|
|
29
|
+
* @returns {void}
|
|
30
|
+
*/
|
|
31
|
+
export function registerRpcInboundHandler(type, handler) {
|
|
32
|
+
rpcHandlers.set(String(type || '').trim(), handler)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {string} type 入站投递类型(part_timeline_put 等)
|
|
37
|
+
* @param {DeliveryInboundHandler} handler 处理器
|
|
38
|
+
* @returns {void}
|
|
39
|
+
*/
|
|
40
|
+
export function registerDeliveryInboundHandler(type, handler) {
|
|
41
|
+
deliveryHandlers.set(String(type || '').trim(), handler)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {string} type 入站类型
|
|
46
|
+
* @returns {void}
|
|
47
|
+
*/
|
|
48
|
+
export function unregisterRpcInboundHandler(type) {
|
|
49
|
+
rpcHandlers.delete(String(type || '').trim())
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {string} type 入站类型
|
|
54
|
+
* @returns {void}
|
|
55
|
+
*/
|
|
56
|
+
export function unregisterDeliveryInboundHandler(type) {
|
|
57
|
+
deliveryHandlers.delete(String(type || '').trim())
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @param {InboundContext} ctx 入站上下文
|
|
62
|
+
* @param {object} message 已校验的线载荷(含 type)
|
|
63
|
+
* @returns {Promise<PartInvokeResponse | null>} 处理器返回值
|
|
64
|
+
*/
|
|
65
|
+
export async function dispatchRpcInbound(ctx, message) {
|
|
66
|
+
const type = String(message?.type || '').trim()
|
|
67
|
+
if (!type) return null
|
|
68
|
+
const handler = rpcHandlers.get(type)
|
|
69
|
+
if (!handler) return null
|
|
70
|
+
return handler(ctx, message)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @param {InboundContext} ctx 入站上下文
|
|
75
|
+
* @param {object} message 已校验的线载荷(含 type)
|
|
76
|
+
* @returns {Promise<void>}
|
|
77
|
+
*/
|
|
78
|
+
export async function dispatchDeliveryInbound(ctx, message) {
|
|
79
|
+
const type = String(message?.type || '').trim()
|
|
80
|
+
if (!type) return
|
|
81
|
+
const handler = deliveryHandlers.get(type)
|
|
82
|
+
if (!handler) return
|
|
83
|
+
await handler(ctx, message)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** @returns {void} 测试用 */
|
|
87
|
+
export function clearInboundHandlers() {
|
|
88
|
+
rpcHandlers.clear()
|
|
89
|
+
deliveryHandlers.clear()
|
|
90
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** @type {Map<string, (replicaUsername: string, groupId: string) => Promise<string>>} */
|
|
2
|
+
const groupMemberEntityByOwner = new Map()
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {string} ownerId 注册方
|
|
6
|
+
* @param {(replicaUsername: string, groupId: string) => Promise<string>} resolver 群成员 entityHash
|
|
7
|
+
* @returns {void}
|
|
8
|
+
*/
|
|
9
|
+
export function registerGroupMemberEntityResolver(ownerId, resolver) {
|
|
10
|
+
groupMemberEntityByOwner.set(String(ownerId), resolver)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {string} ownerId 注册方
|
|
15
|
+
* @returns {void}
|
|
16
|
+
*/
|
|
17
|
+
export function unregisterGroupMemberEntityResolver(ownerId) {
|
|
18
|
+
groupMemberEntityByOwner.delete(String(ownerId))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {string} replicaUsername 副本用户名
|
|
23
|
+
* @param {string} groupId 群 ID
|
|
24
|
+
* @returns {Promise<string | null>} entityHash;无注册方时 null
|
|
25
|
+
*/
|
|
26
|
+
export async function resolveGroupMemberEntityHash(replicaUsername, groupId) {
|
|
27
|
+
for (const resolver of groupMemberEntityByOwner.values())
|
|
28
|
+
try {
|
|
29
|
+
const eh = await resolver(replicaUsername, groupId)
|
|
30
|
+
if (eh) return eh
|
|
31
|
+
}
|
|
32
|
+
catch { /* next */ }
|
|
33
|
+
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { normalizePartpath } from '../wire/part_invoke.mjs'
|
|
2
|
+
|
|
3
|
+
/** @type {Map<string, string>} shell 逻辑名 → partpath */
|
|
4
|
+
const shellPartpaths = new Map()
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Shell Load 时注册本 part 的 partpath(P2P 层不硬编码 shells/*)。
|
|
8
|
+
* @param {string} shellKey 如 social、chat
|
|
9
|
+
* @param {string} partpath 如 shells/social
|
|
10
|
+
* @returns {void}
|
|
11
|
+
*/
|
|
12
|
+
export function registerShellPartpath(shellKey, partpath) {
|
|
13
|
+
const key = String(shellKey || '').trim()
|
|
14
|
+
const path = normalizePartpath(partpath)
|
|
15
|
+
if (!key || !path) throw new Error('invalid_shell_partpath_registration')
|
|
16
|
+
shellPartpaths.set(key, path)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {string} shellKey 如 social、chat
|
|
21
|
+
* @returns {void}
|
|
22
|
+
*/
|
|
23
|
+
export function unregisterShellPartpath(shellKey) {
|
|
24
|
+
shellPartpaths.delete(String(shellKey || '').trim())
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {string} shellKey 如 social
|
|
29
|
+
* @returns {string} 已注册的 partpath
|
|
30
|
+
*/
|
|
31
|
+
export function getShellPartpath(shellKey) {
|
|
32
|
+
const path = shellPartpaths.get(String(shellKey || '').trim())
|
|
33
|
+
if (!path) throw new Error(`shell partpath not registered: ${shellKey}`)
|
|
34
|
+
return path
|
|
35
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 联邦房间 Provider 注册表:P2P 层通过 Provider 获取群房间,不 import Chat Shell。
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {{
|
|
7
|
+
* groupId: string
|
|
8
|
+
* getRoster: () => Array<{ peerId: string, remoteNodeHash?: string }>
|
|
9
|
+
* getPeerIdByNodeHash: (nodeHash: string) => string | null
|
|
10
|
+
* sendToPeer: (peerId: string, actionName: string, payload: unknown) => void
|
|
11
|
+
* pickFallbackPeerIds?: (selfNodeHash: string) => Promise<string[]>
|
|
12
|
+
* }} FederationRoomSlot
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** @type {Map<string, (username: string) => FederationRoomSlot[] | Promise<FederationRoomSlot[]>>} */
|
|
16
|
+
const providers = new Map()
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {string} ownerId 注册方(如 chat)
|
|
20
|
+
* @param {(username: string) => FederationRoomSlot[] | Promise<FederationRoomSlot[]>} enumerateRooms 枚举活跃房间
|
|
21
|
+
* @returns {void}
|
|
22
|
+
*/
|
|
23
|
+
export function registerFederationRoomProvider(ownerId, enumerateRooms) {
|
|
24
|
+
providers.set(String(ownerId), enumerateRooms)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {string} ownerId 注册方
|
|
29
|
+
* @returns {void}
|
|
30
|
+
*/
|
|
31
|
+
export function unregisterFederationRoomProvider(ownerId) {
|
|
32
|
+
providers.delete(String(ownerId))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {string} username 副本用户名 登录名
|
|
37
|
+
* @returns {Promise<FederationRoomSlot[]>} 所有 Provider 提供的活跃 sync 房间
|
|
38
|
+
*/
|
|
39
|
+
export async function listFederationRoomSlots(username) {
|
|
40
|
+
/** @type {FederationRoomSlot[]} */
|
|
41
|
+
const slots = []
|
|
42
|
+
for (const enumerate of providers.values()) {
|
|
43
|
+
const batch = await enumerate(username)
|
|
44
|
+
if (batch?.length) slots.push(...batch)
|
|
45
|
+
}
|
|
46
|
+
return slots
|
|
47
|
+
}
|
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 信誉纯内存算子(模拟器与磁盘 store 共用)。
|
|
3
|
+
*/
|
|
4
|
+
import {
|
|
5
|
+
clampReputationScore,
|
|
6
|
+
computeRepMaxEff,
|
|
7
|
+
seedReputationFromIntro,
|
|
8
|
+
subjectiveSlashPenalty,
|
|
9
|
+
} from './math.mjs'
|
|
10
|
+
import { relayBumpIsDuplicate } from './relay_dedupe.mjs'
|
|
11
|
+
import reputationTunables from './tunables.json' with { type: 'json' }
|
|
12
|
+
|
|
13
|
+
/** @typedef {import('../node/reputation_store.mjs').ReputationFile} ReputationFile */
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @returns {typeof reputationTunables} 默认 reputation tunables 副本
|
|
17
|
+
*/
|
|
18
|
+
export function defaultReputationTunables() {
|
|
19
|
+
return reputationTunables
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {ReputationFile} data 信誉表
|
|
24
|
+
* @param {string} nodeId 64 位十六进制
|
|
25
|
+
* @returns {{ score: number, offenseStreak?: number, lastOffenseAt?: number, blockPenalties?: Record<string, object> }} 节点行
|
|
26
|
+
*/
|
|
27
|
+
function repRow(data, nodeId) {
|
|
28
|
+
const row = data.byNodeHash[nodeId] || { score: 0 }
|
|
29
|
+
const out = { score: Number(row.score ?? 0) }
|
|
30
|
+
if (Number.isFinite(row.offenseStreak))
|
|
31
|
+
out.offenseStreak = row.offenseStreak
|
|
32
|
+
if (Number.isFinite(row.lastOffenseAt))
|
|
33
|
+
out.lastOffenseAt = row.lastOffenseAt
|
|
34
|
+
if (row.blockPenalties) out.blockPenalties = row.blockPenalties
|
|
35
|
+
if (row.baseline) out.baseline = row.baseline
|
|
36
|
+
if (Number.isFinite(row.quarantinedUntil))
|
|
37
|
+
out.quarantinedUntil = row.quarantinedUntil
|
|
38
|
+
return out
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {number} streak 连续作恶次数
|
|
43
|
+
* @param {typeof reputationTunables} tunables 参数
|
|
44
|
+
* @returns {number} 惩罚乘子 ≥ 1
|
|
45
|
+
*/
|
|
46
|
+
export function computeRecidivismMultiplier(streak, tunables = reputationTunables) {
|
|
47
|
+
const step = Number(tunables.recidivismMultiplierStep ?? 0)
|
|
48
|
+
const maxMult = Number(tunables.recidivismMax ?? 1)
|
|
49
|
+
if (!Number.isFinite(streak) || streak <= 0 || step <= 0 || maxMult <= 1)
|
|
50
|
+
return 1
|
|
51
|
+
return Math.min(maxMult, 1 + step * streak)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {ReputationFile} data 信誉表
|
|
56
|
+
* @param {string} nodeId 64 位十六进制
|
|
57
|
+
* @returns {NonNullable<ReputationFile['byNodeHash'][string]>} 节点行(可变引用)
|
|
58
|
+
*/
|
|
59
|
+
function ensureRow(data, nodeId) {
|
|
60
|
+
if (!data.byNodeHash[nodeId]) data.byNodeHash[nodeId] = { score: 0 }
|
|
61
|
+
return data.byNodeHash[nodeId]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {number} badCount 坏邀请计数
|
|
66
|
+
* @param {typeof reputationTunables} tunables 参数
|
|
67
|
+
* @returns {number} 惩罚乘子 ≥ 1
|
|
68
|
+
*/
|
|
69
|
+
export function computeInviteEscalation(badCount, tunables = reputationTunables) {
|
|
70
|
+
const step = Number(tunables.inviteBadEscalationStep ?? 0)
|
|
71
|
+
const maxMult = Number(tunables.inviteBadEscalationMax ?? 1)
|
|
72
|
+
if (!Number.isFinite(badCount) || badCount <= 0 || step <= 0 || maxMult <= 1)
|
|
73
|
+
return 1
|
|
74
|
+
return Math.min(maxMult, 1 + step * badCount)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {object} row byNodeHash 行
|
|
79
|
+
* @param {number} gain 正向收益
|
|
80
|
+
* @param {typeof reputationTunables} tunables 参数
|
|
81
|
+
* @returns {void}
|
|
82
|
+
*/
|
|
83
|
+
function applyRedemptionFromGain(row, gain, tunables = reputationTunables) {
|
|
84
|
+
if (!Number.isFinite(gain) || gain <= 0) return
|
|
85
|
+
const perStreak = Number(tunables.redemptionCreditPerStreakLevel ?? 0)
|
|
86
|
+
const perBad = Number(tunables.inviteRedemptionCreditPerBad ?? 0)
|
|
87
|
+
row.redemptionCredit = Number(row.redemptionCredit ?? 0) + gain
|
|
88
|
+
if (perStreak > 0) {
|
|
89
|
+
while ((row.offenseStreak ?? 0) > 0 && row.redemptionCredit >= perStreak) {
|
|
90
|
+
row.redemptionCredit -= perStreak
|
|
91
|
+
row.offenseStreak = Math.max(0, (row.offenseStreak ?? 0) - 1)
|
|
92
|
+
}
|
|
93
|
+
if ((row.offenseStreak ?? 0) <= 0) {
|
|
94
|
+
delete row.offenseStreak
|
|
95
|
+
delete row.lastOffenseAt
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (perBad > 0) {
|
|
99
|
+
while ((row.badInviteeCount ?? 0) > 0 && row.redemptionCredit >= perBad) {
|
|
100
|
+
row.redemptionCredit -= perBad
|
|
101
|
+
row.badInviteeCount = Math.max(0, (row.badInviteeCount ?? 0) - 1)
|
|
102
|
+
}
|
|
103
|
+
if ((row.badInviteeCount ?? 0) <= 0)
|
|
104
|
+
delete row.badInviteeCount
|
|
105
|
+
}
|
|
106
|
+
if (!Number.isFinite(row.redemptionCredit) || row.redemptionCredit <= 1e-9)
|
|
107
|
+
delete row.redemptionCredit
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @param {ReputationFile} data 信誉表
|
|
112
|
+
* @param {string} nodeId 64 位十六进制
|
|
113
|
+
* @param {number} delta 增量
|
|
114
|
+
* @param {number} [now] 当前时间
|
|
115
|
+
* @param {typeof reputationTunables} [tunables] tunables
|
|
116
|
+
* @returns {void}
|
|
117
|
+
*/
|
|
118
|
+
export function adjustNodeReputation(data, nodeId, delta, now = Date.now(), tunables = reputationTunables) {
|
|
119
|
+
const row = ensureRow(data, nodeId)
|
|
120
|
+
let effectiveDelta = delta
|
|
121
|
+
if (delta < 0) {
|
|
122
|
+
const streak = (row.offenseStreak ?? 0) + 1
|
|
123
|
+
row.offenseStreak = streak
|
|
124
|
+
row.lastOffenseAt = now
|
|
125
|
+
effectiveDelta = delta * computeRecidivismMultiplier(streak, tunables)
|
|
126
|
+
}
|
|
127
|
+
else if (delta > 0)
|
|
128
|
+
applyRedemptionFromGain(row, delta, tunables)
|
|
129
|
+
|
|
130
|
+
row.score = clampReputationScore(Number(row.score ?? 0) + effectiveDelta)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* @param {ReputationFile} data 信誉表
|
|
135
|
+
* @param {string} nodeId 64 位十六进制
|
|
136
|
+
* @param {number} badDelta 坏邀请计数增量
|
|
137
|
+
* @returns {void}
|
|
138
|
+
*/
|
|
139
|
+
export function incrementBadInviteeCount(data, nodeId, badDelta = 1) {
|
|
140
|
+
if (badDelta <= 0) return
|
|
141
|
+
const row = ensureRow(data, nodeId)
|
|
142
|
+
row.badInviteeCount = Math.max(0, Math.floor(Number(row.badInviteeCount ?? 0) + badDelta))
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* @param {ReputationFile} data 磁盘 JSON(可信)
|
|
147
|
+
* @returns {ReputationFile} 补齐字段后的同一对象
|
|
148
|
+
*/
|
|
149
|
+
export function ensureReputationShape(data) {
|
|
150
|
+
data.byNodeHash ??= {}
|
|
151
|
+
data.wantUnknownHits ??= []
|
|
152
|
+
data.relayBumpSeen ??= []
|
|
153
|
+
return data
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* @param {ReputationFile} data 信誉表
|
|
158
|
+
* @param {typeof reputationTunables} [tunables] tunables
|
|
159
|
+
* @param {number} [now] 当前时间
|
|
160
|
+
* @returns {ReputationFile} 裁剪后的同一对象
|
|
161
|
+
*/
|
|
162
|
+
export function pruneReputationFile(data, tunables = reputationTunables, now = Date.now()) {
|
|
163
|
+
data.wantUnknownHits = data.wantUnknownHits.filter(hit => now - hit.t <= tunables.wantUnknownWindowMs)
|
|
164
|
+
data.relayBumpSeen = data.relayBumpSeen.filter(hit => now - hit.t <= tunables.relayBumpDedupeMs)
|
|
165
|
+
if (data.relayBumpSeen.length > tunables.maxRelayBumpSeen)
|
|
166
|
+
data.relayBumpSeen = data.relayBumpSeen.slice(-tunables.maxRelayBumpSeen)
|
|
167
|
+
return data
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* @param {ReputationFile} data 信誉表
|
|
172
|
+
* @param {string} peerNodeHash 对端节点
|
|
173
|
+
* @param {string} [dedupeKey] 去重键
|
|
174
|
+
* @param {number} [now] 当前时间
|
|
175
|
+
* @param {typeof reputationTunables} [tunables] tunables
|
|
176
|
+
* @returns {boolean} 是否已加分
|
|
177
|
+
*/
|
|
178
|
+
export function bumpReputationOnRelayPure(data, peerNodeHash, dedupeKey, now = Date.now(), tunables = reputationTunables) {
|
|
179
|
+
const id = String(peerNodeHash || '').trim()
|
|
180
|
+
if (!id) return false
|
|
181
|
+
const key = String(dedupeKey || `conn:${id}`).trim()
|
|
182
|
+
data.relayBumpSeen = data.relayBumpSeen.filter(hit => now - hit.t <= tunables.relayBumpDedupeMs)
|
|
183
|
+
if (relayBumpIsDuplicate(data.relayBumpSeen, id, key, now, tunables.relayBumpDedupeMs)) return false
|
|
184
|
+
data.relayBumpSeen.push({ peerNodeHash: id, key, t: now })
|
|
185
|
+
adjustNodeReputation(data, id, tunables.relayRepBump, now, tunables)
|
|
186
|
+
return true
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* @param {ReputationFile} data 信誉表
|
|
191
|
+
* @param {string} peerNodeHash 请求方
|
|
192
|
+
* @param {number} [now] 当前时间
|
|
193
|
+
* @param {typeof reputationTunables} [tunables] tunables
|
|
194
|
+
* @returns {boolean} 是否触发惩罚
|
|
195
|
+
*/
|
|
196
|
+
export function recordGossipAllUnknownWantPure(data, peerNodeHash, now = Date.now(), tunables = reputationTunables) {
|
|
197
|
+
data.wantUnknownHits = data.wantUnknownHits.filter(h => now - h.t <= tunables.wantUnknownWindowMs)
|
|
198
|
+
data.wantUnknownHits.push({ peerNodeHash, t: now })
|
|
199
|
+
const recent = data.wantUnknownHits.filter(h => h.peerNodeHash === peerNodeHash)
|
|
200
|
+
if (recent.length >= tunables.wantUnknownThreshold) {
|
|
201
|
+
adjustNodeReputation(data, peerNodeHash, -tunables.penaltyUnknownWant)
|
|
202
|
+
data.wantUnknownHits = data.wantUnknownHits.filter(h => h.peerNodeHash !== peerNodeHash)
|
|
203
|
+
return true
|
|
204
|
+
}
|
|
205
|
+
return false
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* @param {ReputationFile} data 信誉表
|
|
210
|
+
* @param {string} peerNodeHash 对端
|
|
211
|
+
* @param {typeof reputationTunables} [tunables] tunables
|
|
212
|
+
* @param {number} [excessRatio=1] 超速超额比例 0..1
|
|
213
|
+
* @returns {void}
|
|
214
|
+
*/
|
|
215
|
+
export function recordMessageRateViolationPure(data, peerNodeHash, tunables = reputationTunables, excessRatio = 1) {
|
|
216
|
+
const id = String(peerNodeHash || '').trim()
|
|
217
|
+
if (!id) return
|
|
218
|
+
const ratio = Math.max(0, Math.min(1, Number(excessRatio) || 0))
|
|
219
|
+
if (ratio <= 0) return
|
|
220
|
+
adjustNodeReputation(data, id, -tunables.penaltyMessageRate * ratio)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* @param {ReputationFile} data 信誉表
|
|
225
|
+
* @param {string} storagePeerKey 责任方
|
|
226
|
+
* @param {typeof reputationTunables} [tunables] tunables
|
|
227
|
+
* @returns {void}
|
|
228
|
+
*/
|
|
229
|
+
export function bumpChunkStorageReputationPure(data, storagePeerKey, tunables = reputationTunables) {
|
|
230
|
+
const id = String(storagePeerKey || '').trim()
|
|
231
|
+
if (!id) return
|
|
232
|
+
adjustNodeReputation(data, id, tunables.chunkStoreRepBump)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* @param {ReputationFile} data 信誉表
|
|
237
|
+
* @param {string} blamePeerKey 责任方
|
|
238
|
+
* @param {typeof reputationTunables} [tunables] tunables
|
|
239
|
+
* @returns {void}
|
|
240
|
+
*/
|
|
241
|
+
export function penalizeChunkStorageFailurePure(data, blamePeerKey, tunables = reputationTunables) {
|
|
242
|
+
const id = String(blamePeerKey || '').trim()
|
|
243
|
+
if (!id) return
|
|
244
|
+
adjustNodeReputation(data, id, -tunables.chunkFetchFailPenalty)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* @param {ReputationFile} data 信誉表
|
|
249
|
+
* @param {string} peerNodeHash 对端 nodeHash
|
|
250
|
+
* @param {typeof reputationTunables} [tunables] tunables
|
|
251
|
+
* @returns {void}
|
|
252
|
+
*/
|
|
253
|
+
export function penalizeArchiveServeMismatchPure(data, peerNodeHash, tunables = reputationTunables) {
|
|
254
|
+
const id = String(peerNodeHash || '').trim()
|
|
255
|
+
if (!id) return
|
|
256
|
+
adjustNodeReputation(data, id, -tunables.archiveServeMismatchPenalty)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* @param {ReputationFile} data 信誉表
|
|
261
|
+
* @param {string} target 目标 nodeHash
|
|
262
|
+
* @param {string} sender 发送方 nodeHash
|
|
263
|
+
* @param {number} claim 主张强度
|
|
264
|
+
* @param {boolean} verified 是否可验证
|
|
265
|
+
* @param {typeof reputationTunables} [tunables] tunables
|
|
266
|
+
* @returns {void}
|
|
267
|
+
*/
|
|
268
|
+
export function applySubjectiveSlashPure(data, target, sender, claim, verified, tunables = reputationTunables) {
|
|
269
|
+
const repMaxEff = computeRepMaxEff(data)
|
|
270
|
+
const repSender = Number(data.byNodeHash[sender]?.score ?? 0)
|
|
271
|
+
const penalty = subjectiveSlashPenalty(claim, repSender, repMaxEff, verified, tunables)
|
|
272
|
+
adjustNodeReputation(data, target, -penalty)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* @param {ReputationFile} data 信誉表
|
|
277
|
+
* @param {string} targetPubKeyHash 目标
|
|
278
|
+
* @param {Array<{ from?: string, to?: string }>} inviteEdges 邀请边
|
|
279
|
+
* @param {typeof reputationTunables} [tunables] tunables
|
|
280
|
+
* @returns {Array<{ hop: number, node: string, dRep: number }>} 已应用的上游惩罚
|
|
281
|
+
*/
|
|
282
|
+
export function applyDecayCollusionAfterSlashPure(data, targetPubKeyHash, inviteEdges, tunables = reputationTunables) {
|
|
283
|
+
const target = targetPubKeyHash.trim().toLowerCase()
|
|
284
|
+
/** @type {Array<{ hop: number, node: string, dRep: number }>} */
|
|
285
|
+
const applied = []
|
|
286
|
+
let frontier = new Set([target])
|
|
287
|
+
let maxBadSeen = 0
|
|
288
|
+
const baseMaxHop = Number(tunables.collusionMaxHop ?? 6)
|
|
289
|
+
const hopBonusEvery = Math.max(1, Number(tunables.inviteHopBonusEvery ?? 2))
|
|
290
|
+
const hopBonusMax = Number(tunables.inviteHopBonusMax ?? 0)
|
|
291
|
+
const deltaBoostPerBad = Number(tunables.inviteDeltaBoostPerBad ?? 0)
|
|
292
|
+
const deltaBoostMax = Number(tunables.inviteDeltaBoostMax ?? 0)
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* @returns {number} 当前有效最大跳数
|
|
296
|
+
*/
|
|
297
|
+
function effectiveMaxHop() {
|
|
298
|
+
return baseMaxHop + Math.min(hopBonusMax, Math.floor(maxBadSeen / hopBonusEvery))
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
for (let hop = 1; hop <= effectiveMaxHop(); hop++) {
|
|
302
|
+
const upstream = new Set()
|
|
303
|
+
for (const edge of inviteEdges) {
|
|
304
|
+
const to = String(edge.to ?? '').trim().toLowerCase()
|
|
305
|
+
const from = String(edge.from ?? '').trim().toLowerCase()
|
|
306
|
+
if (frontier.has(to) && from) upstream.add(from)
|
|
307
|
+
}
|
|
308
|
+
if (!upstream.size) break
|
|
309
|
+
|
|
310
|
+
for (const node of upstream) {
|
|
311
|
+
incrementBadInviteeCount(data, node, 1)
|
|
312
|
+
const row = ensureRow(data, node)
|
|
313
|
+
const badCount = Number(row.badInviteeCount ?? 0)
|
|
314
|
+
maxBadSeen = Math.max(maxBadSeen, badCount)
|
|
315
|
+
const boostedDelta = Math.min(1, Number(tunables.collusionDelta ?? 0.62) + Math.min(deltaBoostMax, deltaBoostPerBad * badCount))
|
|
316
|
+
const baseDRep = Number(tunables.collusionLambda ?? 0.07) * boostedDelta ** hop
|
|
317
|
+
const dRep = baseDRep * computeInviteEscalation(badCount, tunables)
|
|
318
|
+
const before = Number(row.score ?? 0)
|
|
319
|
+
adjustNodeReputation(data, node, -dRep, Date.now(), tunables)
|
|
320
|
+
const after = Number(data.byNodeHash[node]?.score ?? 0)
|
|
321
|
+
applied.push({ hop, node, dRep: before - after })
|
|
322
|
+
}
|
|
323
|
+
frontier = upstream
|
|
324
|
+
}
|
|
325
|
+
return applied
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* @param {ReputationFile} data 信誉表
|
|
330
|
+
* @param {string} targetPubKeyHash 目标
|
|
331
|
+
* @returns {void}
|
|
332
|
+
*/
|
|
333
|
+
export function applyReputationResetToScoresPure(data, targetPubKeyHash) {
|
|
334
|
+
const t = targetPubKeyHash.trim().toLowerCase()
|
|
335
|
+
const row = repRow(data, t)
|
|
336
|
+
row.score = 0
|
|
337
|
+
delete row.offenseStreak
|
|
338
|
+
delete row.lastOffenseAt
|
|
339
|
+
delete row.badInviteeCount
|
|
340
|
+
delete row.redemptionCredit
|
|
341
|
+
data.byNodeHash[t] = row
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* @param {ReputationFile} data 信誉表
|
|
346
|
+
* @param {string} memberPubKeyHash 新成员
|
|
347
|
+
* @param {string} [introducerPubKeyHash] 介绍者
|
|
348
|
+
* @param {number} [repEdge] 边信任;省略则用 tunable introducerSeedEdge
|
|
349
|
+
* @param {typeof reputationTunables} [tunables] tunables
|
|
350
|
+
* @param {number} [powBonus=0] 入群 PoW 自愿封顶加成
|
|
351
|
+
* @returns {void}
|
|
352
|
+
*/
|
|
353
|
+
export function seedMemberReputationFromIntroducerPure(data, memberPubKeyHash, introducerPubKeyHash, repEdge, tunables = reputationTunables, powBonus = 0) {
|
|
354
|
+
const memberKey = memberPubKeyHash.trim().toLowerCase()
|
|
355
|
+
const introducerKey = introducerPubKeyHash?.trim().toLowerCase() || ''
|
|
356
|
+
if (data.byNodeHash[memberKey]) return
|
|
357
|
+
const introducerReputation = introducerKey
|
|
358
|
+
? Number(data.byNodeHash[introducerKey]?.score ?? 0)
|
|
359
|
+
: 0
|
|
360
|
+
data.byNodeHash[memberKey] = { score: seedReputationFromIntro(introducerReputation, repEdge, tunables, powBonus) }
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* @param {object | null | undefined} groupSettings 群设置
|
|
365
|
+
* @param {typeof reputationTunables} [tunables] tunables
|
|
366
|
+
* @returns {number} 毫秒
|
|
367
|
+
*/
|
|
368
|
+
export function resolveSlashAlertTtlMsPure(groupSettings, tunables = reputationTunables) {
|
|
369
|
+
const n = Number(groupSettings?.slashAlertTtl)
|
|
370
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : tunables.defaultSlashAlertTtlMs
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* @param {object} row byNodeHash 行
|
|
375
|
+
* @returns {{ mean: number, m2: number, count: number }} 行为基线
|
|
376
|
+
*/
|
|
377
|
+
function baselineRow(row) {
|
|
378
|
+
const baseline = row.baseline || { mean: 0, m2: 0, count: 0 }
|
|
379
|
+
return {
|
|
380
|
+
mean: Number(baseline.mean ?? 0),
|
|
381
|
+
m2: Number(baseline.m2 ?? 0),
|
|
382
|
+
count: Number(baseline.count ?? 0),
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* @param {ReputationFile} data 信誉表
|
|
388
|
+
* @param {string} peerNodeHash 对端
|
|
389
|
+
* @param {number} sample 观测值
|
|
390
|
+
* @param {number} [now] 当前时间
|
|
391
|
+
* @param {typeof reputationTunables} [tunables] tunables
|
|
392
|
+
* @returns {{ z: number, anomaly: boolean }} 偏离度
|
|
393
|
+
*/
|
|
394
|
+
export function observeBehaviorSamplePure(data, peerNodeHash, sample, now = Date.now(), tunables = reputationTunables) {
|
|
395
|
+
const id = String(peerNodeHash || '').trim()
|
|
396
|
+
if (!id) return { z: 0, anomaly: false }
|
|
397
|
+
const row = repRow(data, id)
|
|
398
|
+
const baseline = baselineRow(row)
|
|
399
|
+
const alpha = Number(tunables.baselineAlpha ?? 0.08)
|
|
400
|
+
const value = Number(sample)
|
|
401
|
+
if (!Number.isFinite(value)) return { z: 0, anomaly: false }
|
|
402
|
+
|
|
403
|
+
const prevMean = baseline.mean
|
|
404
|
+
const prevCount = baseline.count
|
|
405
|
+
const newCount = prevCount + 1
|
|
406
|
+
const newMean = prevCount === 0 ? value : prevMean + alpha * (value - prevMean)
|
|
407
|
+
const diff = value - newMean
|
|
408
|
+
const newM2 = prevCount === 0 ? 0 : (1 - alpha) * (baseline.m2 + alpha * (value - prevMean) ** 2)
|
|
409
|
+
row.baseline = { mean: newMean, m2: newM2, count: newCount }
|
|
410
|
+
data.byNodeHash[id] = row
|
|
411
|
+
|
|
412
|
+
const variance = newCount > 1 ? Math.max(newM2, 1e-6) : 0
|
|
413
|
+
const std = Math.sqrt(variance)
|
|
414
|
+
const z = std > 0 ? Math.abs(value - newMean) / std : 0
|
|
415
|
+
const minSamples = Number(tunables.baselineMinSamples ?? 6)
|
|
416
|
+
const anomaly = newCount >= minSamples && z >= Number(tunables.anomalyZThreshold ?? 2.8)
|
|
417
|
+
if (anomaly)
|
|
418
|
+
applyQuarantinePure(data, id, now, tunables)
|
|
419
|
+
return { z, anomaly }
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* @param {ReputationFile} data 信誉表
|
|
424
|
+
* @param {string} peerNodeHash 对端
|
|
425
|
+
* @param {number} [now] 当前时间
|
|
426
|
+
* @param {typeof reputationTunables} [tunables] tunables
|
|
427
|
+
* @returns {void}
|
|
428
|
+
*/
|
|
429
|
+
export function applyQuarantinePure(data, peerNodeHash, now = Date.now(), tunables = reputationTunables) {
|
|
430
|
+
const id = String(peerNodeHash || '').trim()
|
|
431
|
+
if (!id) return
|
|
432
|
+
const row = repRow(data, id)
|
|
433
|
+
row.quarantinedUntil = now + Number(tunables.quarantineTtlMs ?? 900_000)
|
|
434
|
+
data.byNodeHash[id] = row
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* @param {ReputationFile} data 信誉表
|
|
439
|
+
* @param {string} peerNodeHash 对端
|
|
440
|
+
* @param {number} [now] 当前时间
|
|
441
|
+
* @returns {boolean} 是否处于本地隔离
|
|
442
|
+
*/
|
|
443
|
+
export function isQuarantinedPure(data, peerNodeHash, now = Date.now()) {
|
|
444
|
+
const id = String(peerNodeHash || '').trim()
|
|
445
|
+
const until = Number(data.byNodeHash?.[id]?.quarantinedUntil ?? 0)
|
|
446
|
+
return Number.isFinite(until) && until > now
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* @param {ReputationFile} data 信誉表
|
|
451
|
+
* @param {string} peerNodeHash 对端
|
|
452
|
+
* @param {number} [now] 当前时间
|
|
453
|
+
* @param {typeof reputationTunables} [tunables] tunables
|
|
454
|
+
* @returns {boolean} 是否检测到异常并触发隔离
|
|
455
|
+
*/
|
|
456
|
+
export function detectAnomalyPure(data, peerNodeHash, now = Date.now(), tunables = reputationTunables) {
|
|
457
|
+
return isQuarantinedPure(data, peerNodeHash, now)
|
|
458
|
+
}
|