@steve02081504/fount-p2p 0.0.35 → 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 +6 -18
- 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 +21 -21
- 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 +12 -12
- package/node/personal_block.mjs +4 -4
- package/node/reputation_store.mjs +13 -13
- package/node/reputation_sync.mjs +9 -10
- 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'
|
|
@@ -57,7 +57,7 @@ export function normalizeNetwork(raw) {
|
|
|
57
57
|
)]
|
|
58
58
|
const hints = (file.hints || [])
|
|
59
59
|
.map(hint => ({
|
|
60
|
-
nodeHash:
|
|
60
|
+
nodeHash: hint.nodeHash || '',
|
|
61
61
|
source: String(hint.source || ''),
|
|
62
62
|
kind: String(hint.kind || ''),
|
|
63
63
|
weight: Number.isFinite(Number(hint.weight)) ? Number(hint.weight) : 0.1,
|
|
@@ -126,8 +126,8 @@ export function saveNetwork(data) {
|
|
|
126
126
|
* @returns {void}
|
|
127
127
|
*/
|
|
128
128
|
export function applyNetworkHint(hint) {
|
|
129
|
-
const nodeHash =
|
|
130
|
-
if (!
|
|
129
|
+
const nodeHash = isHex64(hint?.nodeHash)
|
|
130
|
+
if (!nodeHash) return
|
|
131
131
|
const net = loadNetwork()
|
|
132
132
|
const now = Date.now()
|
|
133
133
|
const ttlMs = Number.isFinite(hint.ttlMs) ? hint.ttlMs : DEFAULT_EXPLORE_TTL_MS
|
|
@@ -160,8 +160,8 @@ export function widenExploreFromTrustedAnchors() {
|
|
|
160
160
|
const net = loadNetwork()
|
|
161
161
|
const now = Date.now()
|
|
162
162
|
for (const raw of net.trustedPeers.slice(0, 12)) {
|
|
163
|
-
const nodeHash =
|
|
164
|
-
if (!
|
|
163
|
+
const nodeHash = isHex64(raw)
|
|
164
|
+
if (!nodeHash) continue
|
|
165
165
|
if (!net.explorePeers.includes(nodeHash))
|
|
166
166
|
net.explorePeers.push(nodeHash)
|
|
167
167
|
net.hints.push({
|
|
@@ -185,12 +185,12 @@ export function widenExploreFromTrustedAnchors() {
|
|
|
185
185
|
export function mergeNetworkPeerPools(patch = {}) {
|
|
186
186
|
const net = loadNetwork()
|
|
187
187
|
for (const raw of patch.trustedPeers || []) {
|
|
188
|
-
const id =
|
|
189
|
-
if (
|
|
188
|
+
const id = isHex64(raw)
|
|
189
|
+
if (id && !net.trustedPeers.includes(id)) net.trustedPeers.push(id)
|
|
190
190
|
}
|
|
191
191
|
for (const raw of patch.explorePeers || []) {
|
|
192
|
-
const id =
|
|
193
|
-
if (
|
|
192
|
+
const id = isHex64(raw)
|
|
193
|
+
if (id && !net.explorePeers.includes(id)) net.explorePeers.push(id)
|
|
194
194
|
}
|
|
195
195
|
net.lastRosterAt = Date.now()
|
|
196
196
|
saveNetwork(net)
|
|
@@ -203,8 +203,8 @@ export function mergeNetworkPeerPools(patch = {}) {
|
|
|
203
203
|
*/
|
|
204
204
|
export function promoteExplorePeer(nodeHash) {
|
|
205
205
|
const net = loadNetwork()
|
|
206
|
-
const id =
|
|
207
|
-
if (!
|
|
206
|
+
const id = isHex64(nodeHash)
|
|
207
|
+
if (!id) return
|
|
208
208
|
net.explorePeers = net.explorePeers.filter(peer => peer !== id)
|
|
209
209
|
if (!net.trustedPeers.includes(id)) net.trustedPeers.push(id)
|
|
210
210
|
net.lastRosterAt = Date.now()
|
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,7 +72,7 @@ function loadSyncConfig() {
|
|
|
73
72
|
* @returns {string[]} 规范化去重后的 64-hex 列表
|
|
74
73
|
*/
|
|
75
74
|
function normalizeHashList(list) {
|
|
76
|
-
return [...new Set(
|
|
75
|
+
return [...new Set(list?.map?.(isHex64)?.filter?.(Boolean) || [])]
|
|
77
76
|
}
|
|
78
77
|
|
|
79
78
|
/** 将当前 sync 配置写盘 */
|
|
@@ -91,8 +90,8 @@ export async function setReputationTable(table) {
|
|
|
91
90
|
await mutateReputation(data => {
|
|
92
91
|
data.byNodeHash = data.byNodeHash || {}
|
|
93
92
|
for (const [nodeHash, row] of Object.entries(incoming)) {
|
|
94
|
-
const id =
|
|
95
|
-
if (!id
|
|
93
|
+
const id = isHex64(nodeHash)
|
|
94
|
+
if (!id) continue
|
|
96
95
|
const score = Number(row?.score ?? row)
|
|
97
96
|
if (!Number.isFinite(score)) continue
|
|
98
97
|
data.byNodeHash[id] = { ...data.byNodeHash[id] || {}, score }
|
|
@@ -223,7 +222,7 @@ export function attachReputationSyncWire() {
|
|
|
223
222
|
* @param {string} peerId 对端 nodeHash
|
|
224
223
|
*/
|
|
225
224
|
rep_sync_req(payload, peerId) {
|
|
226
|
-
const requester =
|
|
225
|
+
const requester = peerId
|
|
227
226
|
if (!requester || !getReputationExportAllowlist().includes(requester)) return
|
|
228
227
|
try {
|
|
229
228
|
wire.send('rep_sync_res', {
|
|
@@ -241,7 +240,7 @@ export function attachReputationSyncWire() {
|
|
|
241
240
|
const requestId = String(payload?.requestId || '')
|
|
242
241
|
const pending = pendingPulls.get(requestId)
|
|
243
242
|
if (!pending) return
|
|
244
|
-
if (
|
|
243
|
+
if (peerId !== pending.donor) return
|
|
245
244
|
clearTimeout(pending.timer)
|
|
246
245
|
pendingPulls.delete(requestId)
|
|
247
246
|
pending.resolve(payload)
|
|
@@ -271,8 +270,8 @@ export function detachReputationSyncWire() {
|
|
|
271
270
|
* @returns {Promise<object>} donor 返回的信誉表
|
|
272
271
|
*/
|
|
273
272
|
export async function pullReputationFromNode(nodeHash) {
|
|
274
|
-
const donor =
|
|
275
|
-
if (!donor
|
|
273
|
+
const donor = isHex64(nodeHash)
|
|
274
|
+
if (!donor) throw new Error('p2p: pullReputationFromNode requires valid nodeHash')
|
|
276
275
|
if (!getTrustSyncDonors().includes(donor))
|
|
277
276
|
throw new Error('p2p: node not in trustSyncDonors')
|
|
278
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
|
}
|
package/schemas/part_query.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer'
|
|
2
2
|
|
|
3
3
|
import { canonicalStringify } from '../core/canonical_json.mjs'
|
|
4
|
-
import { isHex64
|
|
4
|
+
import { isHex64 } from '../core/hexIds.mjs'
|
|
5
5
|
import { isPlainObject } from '../core/object.mjs'
|
|
6
6
|
import { parsePartpath } from '../core/partpath.mjs'
|
|
7
7
|
|
|
@@ -99,8 +99,8 @@ export function parsePartQueryReq(value, tunables = partQueryTunables) {
|
|
|
99
99
|
if (!isPlainObject(value)) return null
|
|
100
100
|
const requestId = normalizeRequestId(value.requestId)
|
|
101
101
|
if (!requestId) return null
|
|
102
|
-
const originNodeHash =
|
|
103
|
-
if (!
|
|
102
|
+
const originNodeHash = isHex64(value.originNodeHash)
|
|
103
|
+
if (!originNodeHash) return null
|
|
104
104
|
const partpath = parsePartpath(value.partpath)
|
|
105
105
|
if (!partpath) return null
|
|
106
106
|
const kind = String(value.kind || '')
|
|
@@ -130,8 +130,8 @@ export function parsePartQueryRes(value, tunables = partQueryTunables) {
|
|
|
130
130
|
if (!isPlainObject(value)) return null
|
|
131
131
|
const requestId = normalizeRequestId(value.requestId)
|
|
132
132
|
if (!requestId) return null
|
|
133
|
-
const fromNodeHash =
|
|
134
|
-
if (!
|
|
133
|
+
const fromNodeHash = isHex64(value.fromNodeHash)
|
|
134
|
+
if (!fromNodeHash) return null
|
|
135
135
|
const rows = clampPartQueryRows(value.rows, tunables.maxHits, tunables.maxRowsBytes)
|
|
136
136
|
if (!rows) return null
|
|
137
137
|
return { requestId, fromNodeHash, rows }
|