@steve02081504/fount-p2p 0.0.34 → 0.0.36
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 +1 -1
- package/core/entity_id.mjs +6 -6
- package/core/entity_id_parse.mjs +7 -7
- package/core/hexIds.mjs +9 -19
- package/crypto/checkpoint_sign.mjs +4 -4
- package/dag/canonicalize_row.mjs +4 -7
- package/discovery/adverts.mjs +1 -2
- package/discovery/bt/index.mjs +11 -11
- package/discovery/bt/peer_hints.mjs +4 -7
- package/discovery/index.mjs +8 -12
- package/discovery/internal/signal_crypto.mjs +1 -2
- package/discovery/lan.mjs +7 -9
- package/discovery/lan_peer_hints.mjs +5 -8
- package/discovery/nostr.mjs +19 -20
- package/discovery/peer_clue.mjs +1 -4
- package/federation/entity_key_chain.mjs +10 -9
- package/federation/message_rate_limit.mjs +1 -3
- package/files/manifest/normalize.mjs +2 -2
- package/files/manifest/public.mjs +5 -5
- package/governance/branch.mjs +1 -2
- package/governance/join_pow.mjs +2 -4
- package/governance/owner_succession_ballot.mjs +5 -5
- package/infra/priority.mjs +1 -2
- package/link/handshake.mjs +22 -23
- package/link/pipe.mjs +1 -2
- package/link/providers/ble_gatt.mjs +2 -3
- package/link/providers/lan_tcp.mjs +2 -3
- package/link/providers/link_id_pipe.mjs +3 -3
- package/link/providers/nostr.mjs +6 -6
- package/mailbox/deliver_or_store.mjs +11 -11
- package/mailbox/prune.mjs +3 -4
- package/mailbox/rate.mjs +3 -2
- package/mailbox/store.mjs +6 -9
- package/node/denylist.mjs +25 -28
- package/node/identity.mjs +6 -6
- package/node/network.mjs +15 -17
- package/node/personal_block.mjs +4 -4
- package/node/reputation_store.mjs +13 -13
- package/node/reputation_sync.mjs +9 -12
- package/package.json +1 -1
- package/reputation/engine.mjs +14 -16
- package/schemas/discovery.mjs +5 -6
- package/schemas/federation_pull.mjs +7 -7
- package/schemas/mailbox.mjs +3 -3
- package/schemas/part_query.mjs +5 -5
- package/timeline/verify_remote.mjs +7 -7
- package/transport/link_registry.mjs +16 -19
- package/transport/mesh_keepalive.mjs +8 -11
- package/transport/offer_answer.mjs +3 -3
- package/transport/peer_health.mjs +12 -15
- package/trust_graph/send.mjs +3 -3
package/node/denylist.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { compositeKey } from '../core/composite_key.mjs'
|
|
2
2
|
import { isEntityHash128 } from '../core/entity_id.mjs'
|
|
3
|
-
import { isHex64
|
|
3
|
+
import { isHex64 } from '../core/hexIds.mjs'
|
|
4
4
|
import { withAsyncMutex } from '../utils/async_mutex.mjs'
|
|
5
5
|
|
|
6
6
|
import { readNodeJsonSync, writeNodeJsonSync } from './storage.mjs'
|
|
@@ -79,15 +79,15 @@ export function normalizeDenylist(raw) {
|
|
|
79
79
|
const groupId = String(entry.groupId || '')
|
|
80
80
|
if (!scope) continue
|
|
81
81
|
if (scope === 'entity') {
|
|
82
|
-
const value =
|
|
83
|
-
if (
|
|
82
|
+
const value = isEntityHash128(entry?.value)
|
|
83
|
+
if (value)
|
|
84
84
|
blocked.push({ scope: 'entity', value })
|
|
85
85
|
continue
|
|
86
86
|
}
|
|
87
|
-
const value =
|
|
88
|
-
if (scope === 'node' &&
|
|
87
|
+
const value = isHex64(entry?.value)
|
|
88
|
+
if (scope === 'node' && value)
|
|
89
89
|
blocked.push({ scope: 'node', value, ...groupId ? { groupId } : {} })
|
|
90
|
-
else if (scope === 'subject' &&
|
|
90
|
+
else if (scope === 'subject' && value)
|
|
91
91
|
blocked.push({ scope: 'subject', value, ...groupId ? { groupId } : {} })
|
|
92
92
|
}
|
|
93
93
|
return { blocked }
|
|
@@ -116,12 +116,12 @@ export function saveDenylist(list) {
|
|
|
116
116
|
* @returns {boolean} 是否命中群级 ban 集合
|
|
117
117
|
*/
|
|
118
118
|
export function isSubjectBannedByState(state, subject) {
|
|
119
|
-
const pk =
|
|
120
|
-
if (
|
|
121
|
-
const entity =
|
|
122
|
-
if (
|
|
123
|
-
const node =
|
|
124
|
-
if (
|
|
119
|
+
const pk = isHex64(subject?.pubKeyHash)
|
|
120
|
+
if (pk && state?.bannedMembers?.has?.(pk)) return true
|
|
121
|
+
const entity = isEntityHash128(subject?.entityHash)
|
|
122
|
+
if (entity && state?.bannedEntities?.has?.(entity)) return true
|
|
123
|
+
const node = isHex64(subject?.nodeHash)
|
|
124
|
+
if (node && state?.bannedNodes?.has?.(node)) return true
|
|
125
125
|
return false
|
|
126
126
|
}
|
|
127
127
|
|
|
@@ -132,17 +132,17 @@ export function isSubjectBannedByState(state, subject) {
|
|
|
132
132
|
* @returns {boolean} 是否命中
|
|
133
133
|
*/
|
|
134
134
|
function matchesDenylistIndex(index, subject, groupId = '') {
|
|
135
|
-
const pk =
|
|
135
|
+
const pk = isHex64(subject?.pubKeyHash)
|
|
136
136
|
const entity = String(subject?.entityHash || '')
|
|
137
|
-
const node =
|
|
137
|
+
const node = isHex64(subject?.nodeHash)
|
|
138
138
|
const { keys } = index
|
|
139
139
|
|
|
140
140
|
if (entity && keys.has(denyKey('entity', '*', entity))) return true
|
|
141
|
-
if (
|
|
142
|
-
if (
|
|
141
|
+
if (node && keys.has(denyKey('node', '*', node))) return true
|
|
142
|
+
if (pk && keys.has(denyKey('subject', '*', pk))) return true
|
|
143
143
|
if (!groupId) return false
|
|
144
|
-
if (
|
|
145
|
-
if (
|
|
144
|
+
if (pk && keys.has(denyKey('subject', groupId, pk))) return true
|
|
145
|
+
if (node && keys.has(denyKey('node', groupId, node))) return true
|
|
146
146
|
return false
|
|
147
147
|
}
|
|
148
148
|
|
|
@@ -161,8 +161,8 @@ export function isSubjectBlocked(subject, groupId = '') {
|
|
|
161
161
|
* @returns {boolean} 是否拉黑
|
|
162
162
|
*/
|
|
163
163
|
export function isPeerKeyBlocked(groupId, peerKey) {
|
|
164
|
-
const key =
|
|
165
|
-
if (!
|
|
164
|
+
const key = isHex64(peerKey)
|
|
165
|
+
if (!key) return false
|
|
166
166
|
const index = getDenylistIndex()
|
|
167
167
|
const { keys } = index
|
|
168
168
|
if (keys.has(denyKey('subject', '*', key))) return true
|
|
@@ -200,9 +200,7 @@ export function addDenylistEntry(entry) {
|
|
|
200
200
|
throw new Error('scope and value required')
|
|
201
201
|
if (scope === 'entity' && entry.groupId)
|
|
202
202
|
throw new Error('entity scope does not use groupId')
|
|
203
|
-
const normValue =
|
|
204
|
-
? normalizeHex64(entry.value)
|
|
205
|
-
: entry.value
|
|
203
|
+
const normValue = entry.value
|
|
206
204
|
if (!normValue)
|
|
207
205
|
throw new Error('scope and value required')
|
|
208
206
|
if (scope === 'subject' && !isHex64(normValue))
|
|
@@ -234,8 +232,8 @@ export async function addDenylistFromBanContent(banContent, groupId) {
|
|
|
234
232
|
await addDenylistEntry({ scope: 'entity', value: banContent.targetEntityHash })
|
|
235
233
|
if (scope === 'node' && banContent?.targetNodeHash)
|
|
236
234
|
await addDenylistEntry({ scope: 'node', value: banContent.targetNodeHash })
|
|
237
|
-
const pk =
|
|
238
|
-
if (
|
|
235
|
+
const pk = isHex64(banContent?.targetPubKeyHash)
|
|
236
|
+
if (pk)
|
|
239
237
|
await addDenylistEntry({ scope: 'subject', value: pk, ...sourceGroupId ? { groupId: sourceGroupId } : {} })
|
|
240
238
|
}
|
|
241
239
|
|
|
@@ -259,12 +257,11 @@ export function addGroupBlockedPeer(groupId, scope, value) {
|
|
|
259
257
|
* @returns {Promise<void>}
|
|
260
258
|
*/
|
|
261
259
|
export function removeGroupBlockedPeer(groupId, scope, value) {
|
|
262
|
-
|
|
263
|
-
if (!scope || !id) return Promise.resolve()
|
|
260
|
+
if (!scope || !value) return Promise.resolve()
|
|
264
261
|
return mutateDenylist(() => {
|
|
265
262
|
const list = loadDenylist()
|
|
266
263
|
list.blocked = list.blocked.filter(entry => {
|
|
267
|
-
if (entry.scope !== scope || entry.value !==
|
|
264
|
+
if (entry.scope !== scope || entry.value !== value) return true
|
|
268
265
|
if (scope === 'entity') return false
|
|
269
266
|
return entry.groupId !== groupId
|
|
270
267
|
})
|
package/node/identity.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { Buffer } from 'node:buffer'
|
|
|
2
2
|
import { randomBytes } from 'node:crypto'
|
|
3
3
|
|
|
4
4
|
import { entityHashFromRecoveryPubKeyHex, parseEntityHash } from '../core/entity_id.mjs'
|
|
5
|
-
import { isHex64
|
|
5
|
+
import { isHex64 } from '../core/hexIds.mjs'
|
|
6
6
|
import { keyPairFromSeed, pubKeyHash } from '../crypto/crypto.mjs'
|
|
7
7
|
import { normalizeMailboxSettings } from '../mailbox/settings.mjs'
|
|
8
8
|
|
|
@@ -17,7 +17,7 @@ const NODE_JSON = 'node'
|
|
|
17
17
|
* @returns {string} 节点哈希
|
|
18
18
|
*/
|
|
19
19
|
export function nodeHashFromSeed(seedHex) {
|
|
20
|
-
const seed = Buffer.from(
|
|
20
|
+
const seed = Buffer.from(seedHex, 'hex')
|
|
21
21
|
if (seed.length !== 32) throw new Error('invalid node seed')
|
|
22
22
|
const { publicKey } = keyPairFromSeed(seed)
|
|
23
23
|
return pubKeyHash(publicKey)
|
|
@@ -46,8 +46,8 @@ function saveNodeFile(patch) {
|
|
|
46
46
|
*/
|
|
47
47
|
export function ensureNodeSeed() {
|
|
48
48
|
const data = loadNodeFile()
|
|
49
|
-
const existing =
|
|
50
|
-
if (
|
|
49
|
+
const existing = isHex64(data.nodeSeedHex)
|
|
50
|
+
if (existing) return existing
|
|
51
51
|
const nodeSeedHex = randomBytes(32).toString('hex')
|
|
52
52
|
saveNodeFile({ nodeSeedHex })
|
|
53
53
|
return nodeSeedHex
|
|
@@ -105,8 +105,8 @@ export function ensureNodeDefaults() {
|
|
|
105
105
|
* @returns {string | null} entityHash(非法 hex 时 null)
|
|
106
106
|
*/
|
|
107
107
|
export function entityHashFromKeys(nodeHash, recoveryPubKeyHex) {
|
|
108
|
-
const pub =
|
|
109
|
-
if (!isHex64(nodeHash) || !
|
|
108
|
+
const pub = isHex64(recoveryPubKeyHex)
|
|
109
|
+
if (!isHex64(nodeHash) || !pub) return null
|
|
110
110
|
return entityHashFromRecoveryPubKeyHex(nodeHash, pub)
|
|
111
111
|
}
|
|
112
112
|
|
package/node/network.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isEntityHash128 } from '../core/entity_id.mjs'
|
|
2
|
-
import { isHex64
|
|
2
|
+
import { isHex64 } from '../core/hexIds.mjs'
|
|
3
3
|
|
|
4
4
|
import { loadDenylist } from './denylist.mjs'
|
|
5
5
|
import { getNodeDir, isNodeInitialized } from './instance.mjs'
|
|
@@ -53,13 +53,11 @@ export function normalizeNetwork(raw) {
|
|
|
53
53
|
* @returns {string[]} 去重 nodeHash 列表
|
|
54
54
|
*/
|
|
55
55
|
const pickIds = key => [...new Set(
|
|
56
|
-
(file[key] || [])
|
|
57
|
-
.map(id => normalizeHex64(id))
|
|
58
|
-
.filter(id => isHex64(id)),
|
|
56
|
+
(file[key] || []).map(isHex64).filter(Boolean),
|
|
59
57
|
)]
|
|
60
58
|
const hints = (file.hints || [])
|
|
61
59
|
.map(hint => ({
|
|
62
|
-
nodeHash:
|
|
60
|
+
nodeHash: hint.nodeHash || '',
|
|
63
61
|
source: String(hint.source || ''),
|
|
64
62
|
kind: String(hint.kind || ''),
|
|
65
63
|
weight: Number.isFinite(Number(hint.weight)) ? Number(hint.weight) : 0.1,
|
|
@@ -128,8 +126,8 @@ export function saveNetwork(data) {
|
|
|
128
126
|
* @returns {void}
|
|
129
127
|
*/
|
|
130
128
|
export function applyNetworkHint(hint) {
|
|
131
|
-
const nodeHash =
|
|
132
|
-
if (!
|
|
129
|
+
const nodeHash = isHex64(hint?.nodeHash)
|
|
130
|
+
if (!nodeHash) return
|
|
133
131
|
const net = loadNetwork()
|
|
134
132
|
const now = Date.now()
|
|
135
133
|
const ttlMs = Number.isFinite(hint.ttlMs) ? hint.ttlMs : DEFAULT_EXPLORE_TTL_MS
|
|
@@ -162,8 +160,8 @@ export function widenExploreFromTrustedAnchors() {
|
|
|
162
160
|
const net = loadNetwork()
|
|
163
161
|
const now = Date.now()
|
|
164
162
|
for (const raw of net.trustedPeers.slice(0, 12)) {
|
|
165
|
-
const nodeHash =
|
|
166
|
-
if (!
|
|
163
|
+
const nodeHash = isHex64(raw)
|
|
164
|
+
if (!nodeHash) continue
|
|
167
165
|
if (!net.explorePeers.includes(nodeHash))
|
|
168
166
|
net.explorePeers.push(nodeHash)
|
|
169
167
|
net.hints.push({
|
|
@@ -187,12 +185,12 @@ export function widenExploreFromTrustedAnchors() {
|
|
|
187
185
|
export function mergeNetworkPeerPools(patch = {}) {
|
|
188
186
|
const net = loadNetwork()
|
|
189
187
|
for (const raw of patch.trustedPeers || []) {
|
|
190
|
-
const id =
|
|
191
|
-
if (
|
|
188
|
+
const id = isHex64(raw)
|
|
189
|
+
if (id && !net.trustedPeers.includes(id)) net.trustedPeers.push(id)
|
|
192
190
|
}
|
|
193
191
|
for (const raw of patch.explorePeers || []) {
|
|
194
|
-
const id =
|
|
195
|
-
if (
|
|
192
|
+
const id = isHex64(raw)
|
|
193
|
+
if (id && !net.explorePeers.includes(id)) net.explorePeers.push(id)
|
|
196
194
|
}
|
|
197
195
|
net.lastRosterAt = Date.now()
|
|
198
196
|
saveNetwork(net)
|
|
@@ -205,8 +203,8 @@ export function mergeNetworkPeerPools(patch = {}) {
|
|
|
205
203
|
*/
|
|
206
204
|
export function promoteExplorePeer(nodeHash) {
|
|
207
205
|
const net = loadNetwork()
|
|
208
|
-
const id =
|
|
209
|
-
if (!
|
|
206
|
+
const id = isHex64(nodeHash)
|
|
207
|
+
if (!id) return
|
|
210
208
|
net.explorePeers = net.explorePeers.filter(peer => peer !== id)
|
|
211
209
|
if (!net.trustedPeers.includes(id)) net.trustedPeers.push(id)
|
|
212
210
|
net.lastRosterAt = Date.now()
|
|
@@ -221,9 +219,9 @@ export function promoteExplorePeer(nodeHash) {
|
|
|
221
219
|
export function replaceNetworkPeerPools(pools = {}) {
|
|
222
220
|
const net = loadNetwork()
|
|
223
221
|
if (Array.isArray(pools.trustedPeers))
|
|
224
|
-
net.trustedPeers = pools.trustedPeers.map(
|
|
222
|
+
net.trustedPeers = pools.trustedPeers.map(isHex64).filter(Boolean)
|
|
225
223
|
if (Array.isArray(pools.explorePeers))
|
|
226
|
-
net.explorePeers = pools.explorePeers.map(
|
|
224
|
+
net.explorePeers = pools.explorePeers.map(isHex64).filter(Boolean)
|
|
227
225
|
net.lastRosterAt = Date.now()
|
|
228
226
|
saveNetwork(net)
|
|
229
227
|
}
|
package/node/personal_block.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* **hide**:纯本地隐藏(personal_hide.json,永不联邦)。
|
|
5
5
|
*/
|
|
6
6
|
import { parseEntityHash } from '../core/entity_id.mjs'
|
|
7
|
-
import { isHex64
|
|
7
|
+
import { isHex64 } from '../core/hexIds.mjs'
|
|
8
8
|
|
|
9
9
|
import { isWritableLocalEntity } from './identity.mjs'
|
|
10
10
|
import { isNodeInitialized, getEntityStore } from './instance.mjs'
|
|
@@ -45,8 +45,8 @@ export function normalizePersonalListEntries(raw) {
|
|
|
45
45
|
byKey.set(`entity:${parsed.entityHash}`, { scope: 'entity', value: parsed.entityHash })
|
|
46
46
|
}
|
|
47
47
|
else if (scope === 'subject') {
|
|
48
|
-
const value =
|
|
49
|
-
if (
|
|
48
|
+
const value = isHex64(entry?.value)
|
|
49
|
+
if (value)
|
|
50
50
|
byKey.set(`subject:${value}`, { scope: 'subject', value })
|
|
51
51
|
}
|
|
52
52
|
}
|
|
@@ -64,7 +64,7 @@ export function normalizePersonalListEntries(raw) {
|
|
|
64
64
|
*/
|
|
65
65
|
export function matchesPersonalListEntries(entries, subject) {
|
|
66
66
|
const entity = subject?.entityHash || ''
|
|
67
|
-
const pk =
|
|
67
|
+
const pk = subject?.pubKeyHash || subject?.subjectHash || ''
|
|
68
68
|
if (entity) {
|
|
69
69
|
for (const entry of entries)
|
|
70
70
|
if (entry.scope === 'entity' && entry.value === entity) return true
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { assertHex64, isHex64
|
|
1
|
+
import { assertHex64, isHex64 } from '../core/hexIds.mjs'
|
|
2
2
|
import { recordWantIdsBackoff, wantIdsPeerKey } from '../federation/want_ids.mjs'
|
|
3
3
|
import {
|
|
4
4
|
applyDecayCollusionAfterSlashPure,
|
|
@@ -120,7 +120,7 @@ export function recordGossipAllUnknownWant(groupId, peerNodeHash) {
|
|
|
120
120
|
* @returns {Promise<void>}
|
|
121
121
|
*/
|
|
122
122
|
export function recordMessageRateViolation(peerNodeHash, excessRatio = 1) {
|
|
123
|
-
const id =
|
|
123
|
+
const id = peerNodeHash
|
|
124
124
|
if (!id) return Promise.resolve()
|
|
125
125
|
return mutateReputation(data => {
|
|
126
126
|
recordMessageRateViolationPure(data, id, undefined, excessRatio)
|
|
@@ -134,7 +134,7 @@ export function recordMessageRateViolation(peerNodeHash, excessRatio = 1) {
|
|
|
134
134
|
* @returns {Promise<boolean>} 是否触发异常隔离
|
|
135
135
|
*/
|
|
136
136
|
export async function observePeerBehavior(peerNodeHash, sample) {
|
|
137
|
-
const id =
|
|
137
|
+
const id = peerNodeHash
|
|
138
138
|
if (!id) return false
|
|
139
139
|
let anomaly = false
|
|
140
140
|
await mutateReputation(data => {
|
|
@@ -149,7 +149,7 @@ export async function observePeerBehavior(peerNodeHash, sample) {
|
|
|
149
149
|
* @returns {void}
|
|
150
150
|
*/
|
|
151
151
|
export function bumpChunkStorageReputation(storagePeerKey) {
|
|
152
|
-
const id =
|
|
152
|
+
const id = storagePeerKey
|
|
153
153
|
if (!id) return
|
|
154
154
|
void mutateReputation(data => {
|
|
155
155
|
bumpChunkStorageReputationPure(data, id)
|
|
@@ -161,7 +161,7 @@ export function bumpChunkStorageReputation(storagePeerKey) {
|
|
|
161
161
|
* @returns {void}
|
|
162
162
|
*/
|
|
163
163
|
export function penalizeChunkStorageFailure(blamePeerKey) {
|
|
164
|
-
const id =
|
|
164
|
+
const id = blamePeerKey
|
|
165
165
|
if (!id) return
|
|
166
166
|
void mutateReputation(data => {
|
|
167
167
|
penalizeChunkStorageFailurePure(data, id)
|
|
@@ -173,7 +173,7 @@ export function penalizeChunkStorageFailure(blamePeerKey) {
|
|
|
173
173
|
* @returns {void}
|
|
174
174
|
*/
|
|
175
175
|
export function penalizeArchiveServeMismatch(peerNodeHash) {
|
|
176
|
-
const id =
|
|
176
|
+
const id = peerNodeHash
|
|
177
177
|
if (!id) return
|
|
178
178
|
void mutateReputation(data => {
|
|
179
179
|
penalizeArchiveServeMismatchPure(data, id)
|
|
@@ -187,9 +187,9 @@ export function penalizeArchiveServeMismatch(peerNodeHash) {
|
|
|
187
187
|
export async function applyVolatileSlashAlert(alert) {
|
|
188
188
|
const expiresAt = Number(alert?.expiresAt)
|
|
189
189
|
if (Number.isFinite(expiresAt) && Date.now() > expiresAt) return false
|
|
190
|
-
const target =
|
|
191
|
-
const sender =
|
|
192
|
-
if (!
|
|
190
|
+
const target = isHex64(alert?.targetPubKeyHash)
|
|
191
|
+
const sender = isHex64(alert?.sender)
|
|
192
|
+
if (!target || !sender) return false
|
|
193
193
|
const claim = Number.isFinite(Number(alert?.claim)) ? Number(alert.claim) : reputationTunables.slashDefaultClaim
|
|
194
194
|
await mutateReputation(data => {
|
|
195
195
|
applySubjectiveSlashPure(data, target, sender, claim, false)
|
|
@@ -227,8 +227,8 @@ export function buildUnverifiedSlashAlert(senderPubKeyHash, content, groupSettin
|
|
|
227
227
|
export async function applySubjectiveSlashFromEvent(username, groupId, event, readEvents) {
|
|
228
228
|
if (event.type !== 'reputation_slash') return
|
|
229
229
|
const { content } = event
|
|
230
|
-
const target =
|
|
231
|
-
const sender =
|
|
230
|
+
const target = content.targetPubKeyHash
|
|
231
|
+
const sender = event.sender
|
|
232
232
|
|
|
233
233
|
await mutateReputation(async data => {
|
|
234
234
|
const verified = content.verified && await verifySlashProof(username, groupId, content, readEvents)
|
|
@@ -245,7 +245,7 @@ export async function applySubjectiveSlashFromEvent(username, groupId, event, re
|
|
|
245
245
|
* @returns {Promise<boolean>} 是否找到 proof 对应事件
|
|
246
246
|
*/
|
|
247
247
|
async function verifySlashProof(username, groupId, content, readEvents) {
|
|
248
|
-
const eventId =
|
|
248
|
+
const eventId = content?.proof?.eventId
|
|
249
249
|
if (!eventId) return false
|
|
250
250
|
const events = await readEvents(username, groupId)
|
|
251
251
|
return events.some(event => event.id === eventId)
|
|
@@ -261,7 +261,7 @@ export function applyDecayCollusionAfterSlash(targetPubKeyHash, inviteEdges) {
|
|
|
261
261
|
const applied = applyDecayCollusionAfterSlashPure(data, targetPubKeyHash, inviteEdges)
|
|
262
262
|
if (applied.length)
|
|
263
263
|
getNodeLogger()?.warn?.('reputation: collusion decay after slash', {
|
|
264
|
-
target:
|
|
264
|
+
target: targetPubKeyHash,
|
|
265
265
|
upstreamCount: applied.length,
|
|
266
266
|
hops: applied.map(row => row.hop),
|
|
267
267
|
})
|
package/node/reputation_sync.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto'
|
|
2
2
|
|
|
3
|
-
import { isHex64
|
|
3
|
+
import { isHex64 } from '../core/hexIds.mjs'
|
|
4
4
|
import { sendToNodeLink } from '../transport/link_registry.mjs'
|
|
5
5
|
import { attachNodeScopeFeature } from '../transport/node_scope/features.mjs'
|
|
6
6
|
import { ensureNodeScope, getNodeScopeWire } from '../transport/node_scope/wire.mjs'
|
|
@@ -55,9 +55,8 @@ function loadSyncConfig() {
|
|
|
55
55
|
? raw.lockedMaxPrevByNodeHash
|
|
56
56
|
: {}
|
|
57
57
|
for (const [nodeHash, score] of Object.entries(prevRaw)) {
|
|
58
|
-
const id = normalizeHex64(nodeHash)
|
|
59
58
|
const n = Number(score)
|
|
60
|
-
if (
|
|
59
|
+
if (isHex64(nodeHash) && Number.isFinite(n)) lockedMaxPrevByNodeHash[nodeHash] = n
|
|
61
60
|
}
|
|
62
61
|
syncConfig = {
|
|
63
62
|
trustSyncDonors: normalizeHashList(raw.trustSyncDonors),
|
|
@@ -73,9 +72,7 @@ function loadSyncConfig() {
|
|
|
73
72
|
* @returns {string[]} 规范化去重后的 64-hex 列表
|
|
74
73
|
*/
|
|
75
74
|
function normalizeHashList(list) {
|
|
76
|
-
return [...new Set((
|
|
77
|
-
.map(id => normalizeHex64(id))
|
|
78
|
-
.filter(id => isHex64(id)))]
|
|
75
|
+
return [...new Set(list?.map?.(isHex64)?.filter?.(Boolean) || [])]
|
|
79
76
|
}
|
|
80
77
|
|
|
81
78
|
/** 将当前 sync 配置写盘 */
|
|
@@ -93,8 +90,8 @@ export async function setReputationTable(table) {
|
|
|
93
90
|
await mutateReputation(data => {
|
|
94
91
|
data.byNodeHash = data.byNodeHash || {}
|
|
95
92
|
for (const [nodeHash, row] of Object.entries(incoming)) {
|
|
96
|
-
const id =
|
|
97
|
-
if (!id
|
|
93
|
+
const id = isHex64(nodeHash)
|
|
94
|
+
if (!id) continue
|
|
98
95
|
const score = Number(row?.score ?? row)
|
|
99
96
|
if (!Number.isFinite(score)) continue
|
|
100
97
|
data.byNodeHash[id] = { ...data.byNodeHash[id] || {}, score }
|
|
@@ -225,7 +222,7 @@ export function attachReputationSyncWire() {
|
|
|
225
222
|
* @param {string} peerId 对端 nodeHash
|
|
226
223
|
*/
|
|
227
224
|
rep_sync_req(payload, peerId) {
|
|
228
|
-
const requester =
|
|
225
|
+
const requester = peerId
|
|
229
226
|
if (!requester || !getReputationExportAllowlist().includes(requester)) return
|
|
230
227
|
try {
|
|
231
228
|
wire.send('rep_sync_res', {
|
|
@@ -243,7 +240,7 @@ export function attachReputationSyncWire() {
|
|
|
243
240
|
const requestId = String(payload?.requestId || '')
|
|
244
241
|
const pending = pendingPulls.get(requestId)
|
|
245
242
|
if (!pending) return
|
|
246
|
-
if (
|
|
243
|
+
if (peerId !== pending.donor) return
|
|
247
244
|
clearTimeout(pending.timer)
|
|
248
245
|
pendingPulls.delete(requestId)
|
|
249
246
|
pending.resolve(payload)
|
|
@@ -273,8 +270,8 @@ export function detachReputationSyncWire() {
|
|
|
273
270
|
* @returns {Promise<object>} donor 返回的信誉表
|
|
274
271
|
*/
|
|
275
272
|
export async function pullReputationFromNode(nodeHash) {
|
|
276
|
-
const donor =
|
|
277
|
-
if (!donor
|
|
273
|
+
const donor = isHex64(nodeHash)
|
|
274
|
+
if (!donor) throw new Error('p2p: pullReputationFromNode requires valid nodeHash')
|
|
278
275
|
if (!getTrustSyncDonors().includes(donor))
|
|
279
276
|
throw new Error('p2p: node not in trustSyncDonors')
|
|
280
277
|
attachReputationSyncWire()
|
package/package.json
CHANGED
package/reputation/engine.mjs
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 信誉纯内存算子(模拟器与磁盘 store 共用)。
|
|
3
3
|
*/
|
|
4
|
-
import { normalizeHex64 } from '../core/hexIds.mjs'
|
|
5
|
-
|
|
6
4
|
import {
|
|
7
5
|
clampReputationScore,
|
|
8
6
|
computeRepMaxEff,
|
|
@@ -178,7 +176,7 @@ export function pruneReputationFile(data, tunables = reputationTunables, now = D
|
|
|
178
176
|
* @returns {boolean} 是否已加分
|
|
179
177
|
*/
|
|
180
178
|
export function bumpReputationOnRelayPure(data, peerNodeHash, dedupeKey, now = Date.now(), tunables = reputationTunables) {
|
|
181
|
-
const id =
|
|
179
|
+
const id = peerNodeHash
|
|
182
180
|
if (!id) return false
|
|
183
181
|
const key = dedupeKey || `conn:${id}`
|
|
184
182
|
data.relayBumpSeen = data.relayBumpSeen.filter(hit => now - hit.t <= tunables.relayBumpDedupeMs)
|
|
@@ -215,7 +213,7 @@ export function recordGossipAllUnknownWantPure(data, peerNodeHash, now = Date.no
|
|
|
215
213
|
* @returns {void}
|
|
216
214
|
*/
|
|
217
215
|
export function recordMessageRateViolationPure(data, peerNodeHash, tunables = reputationTunables, excessRatio = 1) {
|
|
218
|
-
const id =
|
|
216
|
+
const id = peerNodeHash
|
|
219
217
|
if (!id) return
|
|
220
218
|
const ratio = Math.max(0, Math.min(1, Number(excessRatio) || 0))
|
|
221
219
|
if (ratio <= 0) return
|
|
@@ -229,7 +227,7 @@ export function recordMessageRateViolationPure(data, peerNodeHash, tunables = re
|
|
|
229
227
|
* @returns {void}
|
|
230
228
|
*/
|
|
231
229
|
export function bumpChunkStorageReputationPure(data, storagePeerKey, tunables = reputationTunables) {
|
|
232
|
-
const id =
|
|
230
|
+
const id = storagePeerKey
|
|
233
231
|
if (!id) return
|
|
234
232
|
adjustNodeReputation(data, id, tunables.chunkStoreRepBump)
|
|
235
233
|
}
|
|
@@ -241,7 +239,7 @@ export function bumpChunkStorageReputationPure(data, storagePeerKey, tunables =
|
|
|
241
239
|
* @returns {void}
|
|
242
240
|
*/
|
|
243
241
|
export function penalizeChunkStorageFailurePure(data, blamePeerKey, tunables = reputationTunables) {
|
|
244
|
-
const id =
|
|
242
|
+
const id = blamePeerKey
|
|
245
243
|
if (!id) return
|
|
246
244
|
adjustNodeReputation(data, id, -tunables.chunkFetchFailPenalty)
|
|
247
245
|
}
|
|
@@ -253,7 +251,7 @@ export function penalizeChunkStorageFailurePure(data, blamePeerKey, tunables = r
|
|
|
253
251
|
* @returns {void}
|
|
254
252
|
*/
|
|
255
253
|
export function penalizeArchiveServeMismatchPure(data, peerNodeHash, tunables = reputationTunables) {
|
|
256
|
-
const id =
|
|
254
|
+
const id = peerNodeHash
|
|
257
255
|
if (!id) return
|
|
258
256
|
adjustNodeReputation(data, id, -tunables.archiveServeMismatchPenalty)
|
|
259
257
|
}
|
|
@@ -282,7 +280,7 @@ export function applySubjectiveSlashPure(data, target, sender, claim, verified,
|
|
|
282
280
|
* @returns {Array<{ hop: number, node: string, dRep: number }>} 已应用的上游惩罚
|
|
283
281
|
*/
|
|
284
282
|
export function applyDecayCollusionAfterSlashPure(data, targetPubKeyHash, inviteEdges, tunables = reputationTunables) {
|
|
285
|
-
const target =
|
|
283
|
+
const target = targetPubKeyHash
|
|
286
284
|
/** @type {Array<{ hop: number, node: string, dRep: number }>} */
|
|
287
285
|
const applied = []
|
|
288
286
|
let frontier = new Set([target])
|
|
@@ -303,8 +301,8 @@ export function applyDecayCollusionAfterSlashPure(data, targetPubKeyHash, invite
|
|
|
303
301
|
for (let hop = 1; hop <= effectiveMaxHop(); hop++) {
|
|
304
302
|
const upstream = new Set()
|
|
305
303
|
for (const edge of inviteEdges) {
|
|
306
|
-
const to =
|
|
307
|
-
const from =
|
|
304
|
+
const to = edge.to
|
|
305
|
+
const from = edge.from
|
|
308
306
|
if (frontier.has(to) && from) upstream.add(from)
|
|
309
307
|
}
|
|
310
308
|
if (!upstream.size) break
|
|
@@ -333,7 +331,7 @@ export function applyDecayCollusionAfterSlashPure(data, targetPubKeyHash, invite
|
|
|
333
331
|
* @returns {void}
|
|
334
332
|
*/
|
|
335
333
|
export function applyReputationResetToScoresPure(data, targetPubKeyHash) {
|
|
336
|
-
const t =
|
|
334
|
+
const t = targetPubKeyHash
|
|
337
335
|
const row = repRow(data, t)
|
|
338
336
|
row.score = 0
|
|
339
337
|
delete row.offenseStreak
|
|
@@ -353,8 +351,8 @@ export function applyReputationResetToScoresPure(data, targetPubKeyHash) {
|
|
|
353
351
|
* @returns {void}
|
|
354
352
|
*/
|
|
355
353
|
export function seedMemberReputationFromIntroducerPure(data, memberPubKeyHash, introducerPubKeyHash, repEdge, tunables = reputationTunables, powBonus = 0) {
|
|
356
|
-
const memberKey =
|
|
357
|
-
const introducerKey =
|
|
354
|
+
const memberKey = memberPubKeyHash
|
|
355
|
+
const introducerKey = introducerPubKeyHash
|
|
358
356
|
if (data.byNodeHash[memberKey]) return
|
|
359
357
|
const introducerReputation = introducerKey
|
|
360
358
|
? Number(data.byNodeHash[introducerKey]?.score ?? 0)
|
|
@@ -394,7 +392,7 @@ function baselineRow(row) {
|
|
|
394
392
|
* @returns {{ z: number, anomaly: boolean }} 偏离度
|
|
395
393
|
*/
|
|
396
394
|
export function observeBehaviorSamplePure(data, peerNodeHash, sample, now = Date.now(), tunables = reputationTunables) {
|
|
397
|
-
const id =
|
|
395
|
+
const id = peerNodeHash
|
|
398
396
|
if (!id) return { z: 0, anomaly: false }
|
|
399
397
|
const row = repRow(data, id)
|
|
400
398
|
const baseline = baselineRow(row)
|
|
@@ -429,7 +427,7 @@ export function observeBehaviorSamplePure(data, peerNodeHash, sample, now = Date
|
|
|
429
427
|
* @returns {void}
|
|
430
428
|
*/
|
|
431
429
|
export function applyQuarantinePure(data, peerNodeHash, now = Date.now(), tunables = reputationTunables) {
|
|
432
|
-
const id =
|
|
430
|
+
const id = peerNodeHash
|
|
433
431
|
if (!id) return
|
|
434
432
|
const row = repRow(data, id)
|
|
435
433
|
row.quarantinedUntil = now + Number(tunables.quarantineTtlMs ?? 900_000)
|
|
@@ -443,7 +441,7 @@ export function applyQuarantinePure(data, peerNodeHash, now = Date.now(), tunabl
|
|
|
443
441
|
* @returns {boolean} 是否处于本地隔离
|
|
444
442
|
*/
|
|
445
443
|
export function isQuarantinedPure(data, peerNodeHash, now = Date.now()) {
|
|
446
|
-
const id =
|
|
444
|
+
const id = peerNodeHash
|
|
447
445
|
const until = Number(data.byNodeHash?.[id]?.quarantinedUntil ?? 0)
|
|
448
446
|
return Number.isFinite(until) && until > now
|
|
449
447
|
}
|
package/schemas/discovery.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { assertHex64, isHex64, isSignatureHex128
|
|
1
|
+
import { assertHex64, isHex64, isSignatureHex128 } from '../core/hexIds.mjs'
|
|
2
2
|
import { isPlainObject } from '../core/object.mjs'
|
|
3
3
|
|
|
4
4
|
const MAX_DISCOVERY_ADS = 64
|
|
@@ -10,16 +10,15 @@ const MAX_DISCOVERY_ADS = 64
|
|
|
10
10
|
function sanitizeDiscoveryAdvertisement(ad) {
|
|
11
11
|
if (!isPlainObject(ad)) return null
|
|
12
12
|
const groupId = String(ad.groupId || '')
|
|
13
|
-
const advertiserPubKeyHash =
|
|
14
|
-
const signature =
|
|
15
|
-
if (!groupId || !
|
|
16
|
-
const advertiserNodeHash = normalizeHex64(ad.advertiserNodeHash)
|
|
13
|
+
const advertiserPubKeyHash = isHex64(ad.advertiserPubKeyHash)
|
|
14
|
+
const signature = isSignatureHex128(ad.signature)
|
|
15
|
+
if (!groupId || !advertiserPubKeyHash || !signature) return null
|
|
17
16
|
const body = {
|
|
18
17
|
groupId,
|
|
19
18
|
title: String(ad.title || '').slice(0, 200),
|
|
20
19
|
blurb: String(ad.blurb || '').slice(0, 500),
|
|
21
20
|
advertiserPubKeyHash,
|
|
22
|
-
advertiserNodeHash: isHex64(
|
|
21
|
+
advertiserNodeHash: isHex64(ad.advertiserNodeHash) || '',
|
|
23
22
|
observedAt: Number(ad.observedAt) || 0,
|
|
24
23
|
signature,
|
|
25
24
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 联邦补拉 attestation / HPKE 响应 wire 解析(无 attestation/envelope 即丢弃)。
|
|
3
3
|
*/
|
|
4
|
-
import { isHex64, isSignatureHex128
|
|
4
|
+
import { isHex64, isSignatureHex128 } from '../core/hexIds.mjs'
|
|
5
5
|
import { isPlainObject } from '../core/object.mjs'
|
|
6
6
|
import { EVENT_ID_HEX } from '../dag/index.mjs'
|
|
7
7
|
|
|
@@ -23,15 +23,15 @@ function isEnvelopeBlob(value) {
|
|
|
23
23
|
*/
|
|
24
24
|
export function parsePullAttestation(attestation) {
|
|
25
25
|
if (!isPlainObject(attestation)) return null
|
|
26
|
-
const requesterPubKeyHash =
|
|
26
|
+
const requesterPubKeyHash = isHex64(attestation.requesterPubKeyHash)
|
|
27
27
|
const groupId = String(attestation.groupId || '')
|
|
28
28
|
const requestId = String(attestation.requestId || '')
|
|
29
29
|
const timestamp = Number(attestation.timestamp)
|
|
30
|
-
const signature =
|
|
31
|
-
if (!
|
|
30
|
+
const signature = isSignatureHex128(attestation.signature)
|
|
31
|
+
if (!requesterPubKeyHash || !groupId || !Number.isFinite(timestamp) || !signature)
|
|
32
32
|
return null
|
|
33
33
|
const wantIds = attestation.wantIds?.length
|
|
34
|
-
? [...new Set(attestation.wantIds.
|
|
34
|
+
? [...new Set(attestation.wantIds.filter(id => EVENT_ID_HEX.test(id)))]
|
|
35
35
|
: undefined
|
|
36
36
|
return {
|
|
37
37
|
requesterPubKeyHash,
|
|
@@ -50,13 +50,13 @@ export function parsePullAttestation(attestation) {
|
|
|
50
50
|
export function parsePullResponseEnvelope(envelope) {
|
|
51
51
|
if (!isPlainObject(envelope)) return null
|
|
52
52
|
const requestId = String(envelope.requestId || '')
|
|
53
|
-
const requesterPubKeyHash =
|
|
53
|
+
const requesterPubKeyHash = isHex64(envelope.requesterPubKeyHash)
|
|
54
54
|
const requesterNodeHash = String(envelope.requesterNodeHash || '')
|
|
55
55
|
const ephemPub = String(envelope.ephemPub || '')
|
|
56
56
|
const iv = String(envelope.iv || '')
|
|
57
57
|
const ciphertext = String(envelope.ciphertext || '')
|
|
58
58
|
const authTag = String(envelope.authTag || '')
|
|
59
|
-
if (!requestId || !
|
|
59
|
+
if (!requestId || !requesterPubKeyHash || !requesterNodeHash) return null
|
|
60
60
|
if (!isEnvelopeBlob(ephemPub) || !isEnvelopeBlob(iv) || !isEnvelopeBlob(ciphertext) || !isEnvelopeBlob(authTag))
|
|
61
61
|
return null
|
|
62
62
|
return {
|
package/schemas/mailbox.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isHex64 } from '../core/hexIds.mjs'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @typedef {{ ok: true, value: string }} MailboxWireOk
|
|
@@ -17,8 +17,8 @@ import { normalizeHex64, HEX_ID_64 } from '../core/hexIds.mjs'
|
|
|
17
17
|
* @returns {MailboxWireResult} 规范化 hex64 或结构化错误
|
|
18
18
|
*/
|
|
19
19
|
export function assertMailboxPubKeyHash(value) {
|
|
20
|
-
const normalized =
|
|
21
|
-
if (!
|
|
20
|
+
const normalized = isHex64(value)
|
|
21
|
+
if (!normalized)
|
|
22
22
|
return { ok: false, code: 'invalid_hex64', field: 'toPubKeyHash' }
|
|
23
23
|
return { ok: true, value: normalized }
|
|
24
24
|
}
|