soulprint-network 0.2.2 → 0.2.3
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/dist/validator.js +58 -7
- package/package.json +3 -3
package/dist/validator.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createServer } from "node:http";
|
|
|
2
2
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
|
-
import { generateKeypair, keypairFromPrivateKey, decodeToken, sign, verifyAttestation, computeReputation, defaultReputation, PROTOCOL, checkFarming, recordApprovedGain, recordFarmingStrike, loadAuditStore, exportAuditStore, } from "soulprint-core";
|
|
5
|
+
import { generateKeypair, keypairFromPrivateKey, decodeToken, sign, verifyAttestation, computeReputation, defaultReputation, PROTOCOL, PROTOCOL_HASH, isProtocolHashCompatible, checkFarming, recordApprovedGain, recordFarmingStrike, loadAuditStore, exportAuditStore, } from "soulprint-core";
|
|
6
6
|
import { verifyProof, deserializeProof } from "soulprint-zkp";
|
|
7
7
|
import { handleCredentialRoute } from "./credentials/index.js";
|
|
8
8
|
import { publishAttestationP2P, onAttestationReceived, getP2PStats, } from "./p2p.js";
|
|
@@ -184,12 +184,17 @@ async function gossipAttestation(att, excludeUrl) {
|
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
186
|
// ── Canal 2: HTTP gossip (fallback para nodos legacy) ─────────────────────
|
|
187
|
+
// Incluye X-Protocol-Hash para que el peer receptor valide compatibilidad.
|
|
187
188
|
const targets = peers.filter(p => p !== excludeUrl);
|
|
188
189
|
for (const peerUrl of targets) {
|
|
189
190
|
fetch(`${peerUrl}/reputation/attest`, {
|
|
190
191
|
method: "POST",
|
|
191
|
-
headers: {
|
|
192
|
-
|
|
192
|
+
headers: {
|
|
193
|
+
"Content-Type": "application/json",
|
|
194
|
+
"X-Gossip": "1",
|
|
195
|
+
"X-Protocol-Hash": PROTOCOL_HASH, // ← el receptor valida esto
|
|
196
|
+
},
|
|
197
|
+
body: JSON.stringify({ attestation: att, from_peer: true }),
|
|
193
198
|
signal: AbortSignal.timeout(GOSSIP_TIMEOUT_MS),
|
|
194
199
|
}).catch(() => { });
|
|
195
200
|
}
|
|
@@ -248,11 +253,12 @@ function handleInfo(res, nodeKeypair) {
|
|
|
248
253
|
node_did: nodeKeypair.did,
|
|
249
254
|
version: VERSION,
|
|
250
255
|
protocol: PROTOCOL.VERSION,
|
|
256
|
+
protocol_hash: PROTOCOL_HASH, // ← cualquier modificación cambia este hash
|
|
251
257
|
total_verified: Object.keys(nullifiers).length,
|
|
252
258
|
total_reputation: Object.keys(repStore).length,
|
|
253
259
|
known_peers: peers.length,
|
|
254
260
|
supported_countries: ["CO"],
|
|
255
|
-
capabilities: ["zk-verify", "anti-sybil", "co-sign", "bot-reputation", "p2p-gossipsub"],
|
|
261
|
+
capabilities: ["zk-verify", "anti-sybil", "co-sign", "bot-reputation", "p2p-gossipsub", "credential-validators", "anti-farming"],
|
|
256
262
|
rate_limit: `${PROTOCOL.RATE_LIMIT_MAX} req/min per IP`,
|
|
257
263
|
// P2P stats (Phase 5)
|
|
258
264
|
p2p: p2pStats ? {
|
|
@@ -275,6 +281,11 @@ function handleInfo(res, nodeKeypair) {
|
|
|
275
281
|
function handleProtocol(res) {
|
|
276
282
|
json(res, 200, {
|
|
277
283
|
protocol_version: PROTOCOL.VERSION,
|
|
284
|
+
// ── Protocol Hash — IDENTIDAD DE LA RED ────────────────────────────────
|
|
285
|
+
// Cualquier nodo con un hash diferente es rechazado automáticamente.
|
|
286
|
+
// Si PROTOCOL fue modificado (aunque sea un valor), este hash cambia.
|
|
287
|
+
protocol_hash: PROTOCOL_HASH,
|
|
288
|
+
// ── Score limits ────────────────────────────────────────────────────────
|
|
278
289
|
score_floor: PROTOCOL.SCORE_FLOOR,
|
|
279
290
|
verified_score_floor: PROTOCOL.VERIFIED_SCORE_FLOOR,
|
|
280
291
|
min_attester_score: PROTOCOL.MIN_ATTESTER_SCORE,
|
|
@@ -282,11 +293,20 @@ function handleProtocol(res) {
|
|
|
282
293
|
reputation_max: PROTOCOL.REPUTATION_MAX,
|
|
283
294
|
max_score: PROTOCOL.MAX_SCORE,
|
|
284
295
|
default_reputation: PROTOCOL.DEFAULT_REPUTATION,
|
|
296
|
+
// ── Biometric thresholds ────────────────────────────────────────────────
|
|
297
|
+
face_sim_doc_selfie: PROTOCOL.FACE_SIM_DOC_SELFIE,
|
|
298
|
+
face_sim_selfie_selfie: PROTOCOL.FACE_SIM_SELFIE_SELFIE,
|
|
299
|
+
face_key_dims: PROTOCOL.FACE_KEY_DIMS,
|
|
300
|
+
face_key_precision: PROTOCOL.FACE_KEY_PRECISION,
|
|
301
|
+
// ── Retry / timing ──────────────────────────────────────────────────────
|
|
285
302
|
verify_retry_max: PROTOCOL.VERIFY_RETRY_MAX,
|
|
286
303
|
verify_retry_base_ms: PROTOCOL.VERIFY_RETRY_BASE_MS,
|
|
287
304
|
verify_retry_max_ms: PROTOCOL.VERIFY_RETRY_MAX_MS,
|
|
288
305
|
att_max_age_seconds: PROTOCOL.ATT_MAX_AGE_SECONDS,
|
|
289
|
-
|
|
306
|
+
// ── Enforcement notice ──────────────────────────────────────────────────
|
|
307
|
+
immutable: true,
|
|
308
|
+
enforcement: "p2p-hash", // ← la red rechaza nodos con hash diferente
|
|
309
|
+
note: "Nodes with a different protocol_hash are rejected by the network. Modifying any constant changes the hash and isolates the node.",
|
|
290
310
|
});
|
|
291
311
|
}
|
|
292
312
|
// ── GET /reputation/:did ──────────────────────────────────────────────────────
|
|
@@ -327,6 +347,21 @@ async function handleAttest(req, res, ip) {
|
|
|
327
347
|
const { attestation, service_spt, from_peer } = body ?? {};
|
|
328
348
|
if (!attestation)
|
|
329
349
|
return json(res, 400, { error: "Missing field: attestation" });
|
|
350
|
+
// ── Protocol Hash Enforcement (gossip desde peers) ────────────────────────
|
|
351
|
+
// Si la attestation viene de un peer (X-Gossip: 1), validamos que el peer
|
|
352
|
+
// opera con las mismas constantes de protocolo.
|
|
353
|
+
// Un nodo con constantes modificadas no puede inyectar attestations en la red.
|
|
354
|
+
if (from_peer) {
|
|
355
|
+
const peerHash = req.headers["x-protocol-hash"];
|
|
356
|
+
if (peerHash && !isProtocolHashCompatible(peerHash)) {
|
|
357
|
+
console.warn(`[protocol] Gossip rechazado de ${ip} — hash incompatible: ${peerHash?.slice(0, 16)}...`);
|
|
358
|
+
return json(res, 409, {
|
|
359
|
+
error: "Protocol mismatch — gossip rejected",
|
|
360
|
+
our_hash: PROTOCOL_HASH,
|
|
361
|
+
their_hash: peerHash,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
}
|
|
330
365
|
const att = attestation;
|
|
331
366
|
// ── Validaciones básicas de la attestation ────────────────────────────────
|
|
332
367
|
if (typeof att.issuer_did !== "string")
|
|
@@ -422,16 +457,30 @@ async function handlePeerRegister(req, res) {
|
|
|
422
457
|
catch (e) {
|
|
423
458
|
return json(res, 400, { error: e.message });
|
|
424
459
|
}
|
|
425
|
-
const { url } = body ?? {};
|
|
460
|
+
const { url, protocol_hash } = body ?? {};
|
|
426
461
|
if (!url || typeof url !== "string")
|
|
427
462
|
return json(res, 400, { error: "Missing field: url" });
|
|
428
463
|
if (!/^https?:\/\//.test(url))
|
|
429
464
|
return json(res, 400, { error: "url must start with http:// or https://" });
|
|
465
|
+
// ── Protocol Hash Enforcement — INAMOVIBLE POR LA RED ────────────────────
|
|
466
|
+
// Si el peer envía un hash, DEBE coincidir con el nuestro.
|
|
467
|
+
// Si no envía hash → se acepta (nodos legacy / primeras versiones).
|
|
468
|
+
// En versiones futuras, el hash será OBLIGATORIO.
|
|
469
|
+
if (protocol_hash && !isProtocolHashCompatible(protocol_hash)) {
|
|
470
|
+
return json(res, 409, {
|
|
471
|
+
error: "Protocol mismatch — node rejected",
|
|
472
|
+
reason: "The peer is running with different protocol constants. This breaks network consensus.",
|
|
473
|
+
our_hash: PROTOCOL_HASH,
|
|
474
|
+
their_hash: protocol_hash,
|
|
475
|
+
our_version: PROTOCOL.VERSION,
|
|
476
|
+
resolution: "Update soulprint-network to the latest version, or join a compatible network.",
|
|
477
|
+
});
|
|
478
|
+
}
|
|
430
479
|
if (peers.includes(url))
|
|
431
480
|
return json(res, 200, { ok: true, peers: peers.length, msg: "Already registered" });
|
|
432
481
|
peers.push(url);
|
|
433
482
|
savePeers();
|
|
434
|
-
json(res, 200, { ok: true, peers: peers.length });
|
|
483
|
+
json(res, 200, { ok: true, peers: peers.length, protocol_hash: PROTOCOL_HASH });
|
|
435
484
|
}
|
|
436
485
|
// ── GET /peers ─────────────────────────────────────────────────────────────────
|
|
437
486
|
function handleGetPeers(res) {
|
|
@@ -579,6 +628,8 @@ export function startValidatorNode(port = PORT) {
|
|
|
579
628
|
console.log(`\n🌐 Soulprint Validator Node v${VERSION}`);
|
|
580
629
|
console.log(` Node DID: ${nodeKeypair.did}`);
|
|
581
630
|
console.log(` Listening: http://0.0.0.0:${port}`);
|
|
631
|
+
console.log(` Protocol: ${PROTOCOL.VERSION} | hash: ${PROTOCOL_HASH.slice(0, 16)}...`);
|
|
632
|
+
console.log(` ⚠️ Hash mismatch with peers → connection rejected (P2P enforcement)`);
|
|
582
633
|
console.log(` Nullifiers: ${Object.keys(nullifiers).length}`);
|
|
583
634
|
console.log(` Reputations: ${Object.keys(repStore).length}`);
|
|
584
635
|
console.log(` Known peers: ${peers.length}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "soulprint-network",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Soulprint validator node — HTTP server that verifies ZK proofs, co-signs SPTs, anti-Sybil registry",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"otpauth": "^9.5.0",
|
|
45
45
|
"otplib": "^13.3.0",
|
|
46
46
|
"uint8arrays": "5.1.0",
|
|
47
|
-
"soulprint-core": "0.1.
|
|
48
|
-
"soulprint-zkp": "0.1.
|
|
47
|
+
"soulprint-core": "0.1.7",
|
|
48
|
+
"soulprint-zkp": "0.1.4"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@types/node": "^20.0.0",
|