soulprint-network 0.3.7 → 0.3.8
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/code-hash.json +3 -3
- package/dist/validator.d.ts +1 -1
- package/dist/validator.js +35 -33
- package/package.json +1 -1
package/dist/code-hash.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"codeHash": "
|
|
3
|
-
"codeHashHex": "
|
|
4
|
-
"computedAt": "2026-02-24T22:
|
|
2
|
+
"codeHash": "cede7ea2b4acfb3c25dd603fa290f41ea12f02394d617857e0c3247f30d0e0a3",
|
|
3
|
+
"codeHashHex": "0xcede7ea2b4acfb3c25dd603fa290f41ea12f02394d617857e0c3247f30d0e0a3",
|
|
4
|
+
"computedAt": "2026-02-24T22:57:27.288Z",
|
|
5
5
|
"fileCount": 18,
|
|
6
6
|
"files": [
|
|
7
7
|
"blockchain/blockchain-anchor.ts",
|
package/dist/validator.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export declare function setP2PNode(node: SoulprintP2PNode): void;
|
|
|
13
13
|
*
|
|
14
14
|
* PROTOCOL ENFORCEMENT:
|
|
15
15
|
* - Si el bot tiene DocumentVerified, su score total nunca puede caer por
|
|
16
|
-
* debajo de PROTOCOL.VERIFIED_SCORE_FLOOR (52)
|
|
16
|
+
* debajo de PROTOCOL.VERIFIED_SCORE_FLOOR (52) - inamovible.
|
|
17
17
|
* - Anti-replay: la misma attestation (mismo issuer + timestamp + context)
|
|
18
18
|
* no se puede aplicar dos veces.
|
|
19
19
|
*
|
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, createToken, TOKEN_LIFETIME_SECONDS, TOKEN_RENEW_PREEMPTIVE_SECS, TOKEN_RENEW_GRACE_SECS, TOKEN_RENEW_COOLDOWN_SECS, verifyAttestation, computeReputation, defaultReputation, PROTOCOL, PROTOCOL_HASH, isProtocolHashCompatible, computeTotalScoreWithFloor, checkFarming, recordApprovedGain, recordFarmingStrike, loadAuditStore, exportAuditStore, } from "soulprint-core";
|
|
5
|
+
import { generateKeypair, keypairFromPrivateKey, decodeToken, sign, createToken, TOKEN_LIFETIME_SECONDS, TOKEN_RENEW_PREEMPTIVE_SECS, TOKEN_RENEW_GRACE_SECS, TOKEN_RENEW_COOLDOWN_SECS, NonceStore, verifyAttestation, computeReputation, defaultReputation, PROTOCOL, PROTOCOL_HASH, isProtocolHashCompatible, computeTotalScoreWithFloor, checkFarming, recordApprovedGain, recordFarmingStrike, loadAuditStore, exportAuditStore, } from "soulprint-core";
|
|
6
6
|
import { verifyProof, deserializeProof } from "soulprint-zkp";
|
|
7
7
|
import { buildChallengeResponse, verifyPeerBehavior, } from "./peer-challenge.js";
|
|
8
8
|
import { handleCredentialRoute } from "./credentials/index.js";
|
|
@@ -23,11 +23,11 @@ const PEERS_DB = join(NODE_DIR, "peers.json");
|
|
|
23
23
|
const AUDIT_DB = join(NODE_DIR, "audit.json");
|
|
24
24
|
const VERSION = "0.2.0";
|
|
25
25
|
const MAX_BODY_BYTES = 64 * 1024;
|
|
26
|
-
// ── Protocol constants (inamovibles
|
|
26
|
+
// ── Protocol constants (inamovibles - no cambiar directamente aquí) ───────────
|
|
27
27
|
const RATE_LIMIT_MS = PROTOCOL.RATE_LIMIT_WINDOW_MS;
|
|
28
28
|
const RATE_LIMIT_MAX = PROTOCOL.RATE_LIMIT_MAX;
|
|
29
29
|
const CLOCK_SKEW_MAX = PROTOCOL.CLOCK_SKEW_MAX_SECONDS;
|
|
30
|
-
const MIN_ATTESTER_SCORE = PROTOCOL.MIN_ATTESTER_SCORE; // 65
|
|
30
|
+
const MIN_ATTESTER_SCORE = PROTOCOL.MIN_ATTESTER_SCORE; // 65 - inamovible
|
|
31
31
|
const ATT_MAX_AGE_SECONDS = PROTOCOL.ATT_MAX_AGE_SECONDS;
|
|
32
32
|
const GOSSIP_TIMEOUT_MS = PROTOCOL.GOSSIP_TIMEOUT_MS;
|
|
33
33
|
// ── P2P Node (Phase 5) ────────────────────────────────────────────────────────
|
|
@@ -44,7 +44,7 @@ export function setP2PNode(node) {
|
|
|
44
44
|
onAttestationReceived(node, (att, fromPeer) => {
|
|
45
45
|
// Validar firma antes de aplicar
|
|
46
46
|
if (!verifyAttestation(att)) {
|
|
47
|
-
console.warn(`[p2p] Attestation inválida de peer ${fromPeer.slice(0, 16)}...
|
|
47
|
+
console.warn(`[p2p] Attestation inválida de peer ${fromPeer.slice(0, 16)}... - descartada`);
|
|
48
48
|
return;
|
|
49
49
|
}
|
|
50
50
|
// Anti-replay ya está dentro de applyAttestation()
|
|
@@ -55,6 +55,8 @@ export function setP2PNode(node) {
|
|
|
55
55
|
}
|
|
56
56
|
// ── Rate limiter ──────────────────────────────────────────────────────────────
|
|
57
57
|
const rateLimits = new Map();
|
|
58
|
+
// ── DPoP Nonce Store — anti-replay para request signing ──────────────────────
|
|
59
|
+
const dpopNonces = new NonceStore();
|
|
58
60
|
function checkRateLimit(ip) {
|
|
59
61
|
const now = Date.now();
|
|
60
62
|
const entry = rateLimits.get(ip);
|
|
@@ -115,7 +117,7 @@ function getReputation(did) {
|
|
|
115
117
|
*
|
|
116
118
|
* PROTOCOL ENFORCEMENT:
|
|
117
119
|
* - Si el bot tiene DocumentVerified, su score total nunca puede caer por
|
|
118
|
-
* debajo de PROTOCOL.VERIFIED_SCORE_FLOOR (52)
|
|
120
|
+
* debajo de PROTOCOL.VERIFIED_SCORE_FLOOR (52) - inamovible.
|
|
119
121
|
* - Anti-replay: la misma attestation (mismo issuer + timestamp + context)
|
|
120
122
|
* no se puede aplicar dos veces.
|
|
121
123
|
*
|
|
@@ -177,8 +179,8 @@ function savePeers() { writeFileSync(PEERS_DB, JSON.stringify(peers, null, 2));
|
|
|
177
179
|
* Gossip: propaga la attestation a la red.
|
|
178
180
|
*
|
|
179
181
|
* Estrategia:
|
|
180
|
-
* 1. P2P GossipSub (Phase 5)
|
|
181
|
-
* 2. HTTP fire-and-forget (Phase 3)
|
|
182
|
+
* 1. P2P GossipSub (Phase 5) - si el nodo libp2p está activo
|
|
183
|
+
* 2. HTTP fire-and-forget (Phase 3) - fallback para nodos legacy sin libp2p
|
|
182
184
|
*
|
|
183
185
|
* Ambos canales son fire-and-forget: no bloquean la respuesta al cliente.
|
|
184
186
|
*/
|
|
@@ -296,7 +298,7 @@ function handleInfo(res, nodeKeypair) {
|
|
|
296
298
|
function handleProtocol(res) {
|
|
297
299
|
json(res, 200, {
|
|
298
300
|
protocol_version: PROTOCOL.VERSION,
|
|
299
|
-
// ── Protocol Hash
|
|
301
|
+
// ── Protocol Hash - IDENTIDAD DE LA RED ────────────────────────────────
|
|
300
302
|
// Cualquier nodo con un hash diferente es rechazado automáticamente.
|
|
301
303
|
// Si PROTOCOL fue modificado (aunque sea un valor), este hash cambia.
|
|
302
304
|
protocol_hash: PROTOCOL_HASH,
|
|
@@ -383,9 +385,9 @@ async function handleAttest(req, res, ip) {
|
|
|
383
385
|
if (from_peer) {
|
|
384
386
|
const peerHash = req.headers["x-protocol-hash"];
|
|
385
387
|
if (peerHash && !isProtocolHashCompatible(peerHash)) {
|
|
386
|
-
console.warn(`[protocol] Gossip rechazado de ${ip}
|
|
388
|
+
console.warn(`[protocol] Gossip rechazado de ${ip} - hash incompatible: ${peerHash?.slice(0, 16)}...`);
|
|
387
389
|
return json(res, 409, {
|
|
388
|
-
error: "Protocol mismatch
|
|
390
|
+
error: "Protocol mismatch - gossip rejected",
|
|
389
391
|
our_hash: PROTOCOL_HASH,
|
|
390
392
|
their_hash: peerHash,
|
|
391
393
|
});
|
|
@@ -415,7 +417,7 @@ async function handleAttest(req, res, ip) {
|
|
|
415
417
|
// Si viene del exterior, exigir service_spt
|
|
416
418
|
if (!from_peer) {
|
|
417
419
|
if (!service_spt)
|
|
418
|
-
return json(res, 401, { error: "Missing service_spt
|
|
420
|
+
return json(res, 401, { error: "Missing service_spt - only verified services can attest" });
|
|
419
421
|
const serviceTok = decodeToken(service_spt);
|
|
420
422
|
if (!serviceTok)
|
|
421
423
|
return json(res, 401, { error: "Invalid or expired service_spt" });
|
|
@@ -446,7 +448,7 @@ async function handleAttest(req, res, ip) {
|
|
|
446
448
|
const session = {
|
|
447
449
|
did: att.target_did,
|
|
448
450
|
startTime: (att.timestamp - 60) * 1000, // estimar inicio de sesión 60s antes
|
|
449
|
-
events: [], // no tenemos eventos individuales aquí
|
|
451
|
+
events: [], // no tenemos eventos individuales aquí - se evalúa en withTracking()
|
|
450
452
|
issuerDid: att.issuer_did,
|
|
451
453
|
};
|
|
452
454
|
const farmResult = checkFarming(session, prevAtts);
|
|
@@ -530,13 +532,13 @@ async function handlePeerRegister(req, res) {
|
|
|
530
532
|
return json(res, 400, { error: "Missing field: url" });
|
|
531
533
|
if (!/^https?:\/\//.test(url))
|
|
532
534
|
return json(res, 400, { error: "url must start with http:// or https://" });
|
|
533
|
-
// ── Protocol Hash Enforcement
|
|
535
|
+
// ── Protocol Hash Enforcement - INAMOVIBLE POR LA RED ────────────────────
|
|
534
536
|
// Si el peer envía un hash, DEBE coincidir con el nuestro.
|
|
535
537
|
// Si no envía hash → se acepta (nodos legacy / primeras versiones).
|
|
536
538
|
// En versiones futuras, el hash será OBLIGATORIO.
|
|
537
539
|
if (protocol_hash && !isProtocolHashCompatible(protocol_hash)) {
|
|
538
540
|
return json(res, 409, {
|
|
539
|
-
error: "Protocol mismatch
|
|
541
|
+
error: "Protocol mismatch - node rejected",
|
|
540
542
|
reason: "The peer is running with different protocol constants. This breaks network consensus.",
|
|
541
543
|
our_hash: PROTOCOL_HASH,
|
|
542
544
|
their_hash: protocol_hash,
|
|
@@ -667,7 +669,7 @@ async function handleTokenRenew(req, res, nodeKeypair) {
|
|
|
667
669
|
// Decodificar sin verificar expiración (queremos ver el DID aunque esté expirado)
|
|
668
670
|
const token = decodeToken(body.spt);
|
|
669
671
|
if (!token)
|
|
670
|
-
return json(res, 401, { error: "Invalid SPT
|
|
672
|
+
return json(res, 401, { error: "Invalid SPT - cannot decode" });
|
|
671
673
|
const nowSecs = Math.floor(Date.now() / 1000);
|
|
672
674
|
const secsUntilExpiry = token.expires - nowSecs;
|
|
673
675
|
const secsAfterExpiry = nowSecs - token.expires;
|
|
@@ -683,13 +685,13 @@ async function handleTokenRenew(req, res, nodeKeypair) {
|
|
|
683
685
|
if (!inPreemptWindow && !inGraceWindow) {
|
|
684
686
|
if (!isExpired) {
|
|
685
687
|
return json(res, 400, {
|
|
686
|
-
error: "Token válido
|
|
688
|
+
error: "Token válido - no necesita renovación aún",
|
|
687
689
|
expires_in: secsUntilExpiry,
|
|
688
690
|
renew_after: secsUntilExpiry - RENEW_PREEMPT,
|
|
689
691
|
});
|
|
690
692
|
}
|
|
691
693
|
return json(res, 401, {
|
|
692
|
-
error: "Token expirado hace más de 7 días
|
|
694
|
+
error: "Token expirado hace más de 7 días - requiere re-verificación completa",
|
|
693
695
|
expired_ago: secsAfterExpiry,
|
|
694
696
|
max_grace: RENEW_GRACE,
|
|
695
697
|
});
|
|
@@ -699,7 +701,7 @@ async function handleTokenRenew(req, res, nodeKeypair) {
|
|
|
699
701
|
const lastRenew = repStore[token.did]?._lastRenew ?? 0;
|
|
700
702
|
if (nowSecs - lastRenew < RENEW_COOLDOWN) {
|
|
701
703
|
return json(res, 429, {
|
|
702
|
-
error: "Renovación muy frecuente
|
|
704
|
+
error: "Renovación muy frecuente - espera 60s entre renovaciones",
|
|
703
705
|
retry_in: RENEW_COOLDOWN - (nowSecs - lastRenew),
|
|
704
706
|
});
|
|
705
707
|
}
|
|
@@ -708,7 +710,7 @@ async function handleTokenRenew(req, res, nodeKeypair) {
|
|
|
708
710
|
const nullifierEntry = nullifierPair?.[1];
|
|
709
711
|
if (!nullifierEntry) {
|
|
710
712
|
return json(res, 403, {
|
|
711
|
-
error: "DID no registrado en este nodo
|
|
713
|
+
error: "DID no registrado en este nodo - requiere re-verificación",
|
|
712
714
|
did: token.did,
|
|
713
715
|
});
|
|
714
716
|
}
|
|
@@ -721,7 +723,7 @@ async function handleTokenRenew(req, res, nodeKeypair) {
|
|
|
721
723
|
const scoreFloor = PROTOCOL.VERIFIED_SCORE_FLOOR ?? 52;
|
|
722
724
|
if (currentRep < scoreFloor) {
|
|
723
725
|
return json(res, 403, {
|
|
724
|
-
error: "Score por debajo del floor
|
|
726
|
+
error: "Score por debajo del floor - renovación denegada",
|
|
725
727
|
score: currentRep,
|
|
726
728
|
floor: scoreFloor,
|
|
727
729
|
hint: "El bot necesita más attestations positivas",
|
|
@@ -938,7 +940,7 @@ export function startValidatorNode(port = PORT) {
|
|
|
938
940
|
return json(res, 200, anchor.getStats());
|
|
939
941
|
}
|
|
940
942
|
// ── Consensus P2P endpoints (sin EVM, sin gas) ─────────────────────────
|
|
941
|
-
// GET /consensus/state-info
|
|
943
|
+
// GET /consensus/state-info - handshake para state-sync
|
|
942
944
|
if (cleanUrl === "/consensus/state-info" && req.method === "GET") {
|
|
943
945
|
return json(res, 200, {
|
|
944
946
|
nullifierCount: nullifierConsensus.getAllNullifiers().length,
|
|
@@ -948,7 +950,7 @@ export function startValidatorNode(port = PORT) {
|
|
|
948
950
|
nodeVersion: VERSION,
|
|
949
951
|
});
|
|
950
952
|
}
|
|
951
|
-
// GET /consensus/state?page=N&limit=500&since=TS
|
|
953
|
+
// GET /consensus/state?page=N&limit=500&since=TS - bulk state sync
|
|
952
954
|
if (cleanUrl === "/consensus/state" && req.method === "GET") {
|
|
953
955
|
const params = new URLSearchParams(url.split("?")[1] ?? "");
|
|
954
956
|
const page = parseInt(params.get("page") ?? "0");
|
|
@@ -967,7 +969,7 @@ export function startValidatorNode(port = PORT) {
|
|
|
967
969
|
protocolHash: PROTOCOL_HASH,
|
|
968
970
|
});
|
|
969
971
|
}
|
|
970
|
-
// POST /consensus/message
|
|
972
|
+
// POST /consensus/message - recibir mensaje de consenso cifrado
|
|
971
973
|
if (cleanUrl === "/consensus/message" && req.method === "POST") {
|
|
972
974
|
const body = await readBody(req);
|
|
973
975
|
if (!body?.payload)
|
|
@@ -990,7 +992,7 @@ export function startValidatorNode(port = PORT) {
|
|
|
990
992
|
}
|
|
991
993
|
}
|
|
992
994
|
// ── Governance endpoints ──────────────────────────────────────────────────
|
|
993
|
-
// GET /governance
|
|
995
|
+
// GET /governance - estado del hash aprobado + propuestas activas
|
|
994
996
|
if (cleanUrl === "/governance" && req.method === "GET") {
|
|
995
997
|
const [currentHash, active, history] = await Promise.all([
|
|
996
998
|
client?.getCurrentApprovedHash() ?? null,
|
|
@@ -1005,14 +1007,14 @@ export function startValidatorNode(port = PORT) {
|
|
|
1005
1007
|
nodeCompatible: !currentHash || currentHash.toLowerCase() === ("0x" + PROTOCOL_HASH).toLowerCase(),
|
|
1006
1008
|
});
|
|
1007
1009
|
}
|
|
1008
|
-
// GET /governance/proposals
|
|
1010
|
+
// GET /governance/proposals - lista de propuestas activas
|
|
1009
1011
|
if (cleanUrl === "/governance/proposals" && req.method === "GET") {
|
|
1010
1012
|
if (!client?.isConnected)
|
|
1011
1013
|
return json(res, 503, { error: "Blockchain not connected" });
|
|
1012
1014
|
const proposals = await client.getActiveProposals();
|
|
1013
1015
|
return json(res, 200, { proposals, total: proposals.length });
|
|
1014
1016
|
}
|
|
1015
|
-
// GET /governance/proposal/:id
|
|
1017
|
+
// GET /governance/proposal/:id - detalle de una propuesta
|
|
1016
1018
|
if (cleanUrl.match(/^\/governance\/proposal\/\d+$/) && req.method === "GET") {
|
|
1017
1019
|
if (!client?.isConnected)
|
|
1018
1020
|
return json(res, 503, { error: "Blockchain not connected" });
|
|
@@ -1023,7 +1025,7 @@ export function startValidatorNode(port = PORT) {
|
|
|
1023
1025
|
const remaining = await client.getTimelockRemaining(proposalId);
|
|
1024
1026
|
return json(res, 200, { ...proposal, timelockRemainingSeconds: remaining });
|
|
1025
1027
|
}
|
|
1026
|
-
// POST /governance/propose
|
|
1028
|
+
// POST /governance/propose - proponer upgrade del PROTOCOL_HASH
|
|
1027
1029
|
// Body: { did, newHash, rationale }
|
|
1028
1030
|
if (cleanUrl === "/governance/propose" && req.method === "POST") {
|
|
1029
1031
|
if (!client?.isConnected)
|
|
@@ -1038,10 +1040,10 @@ export function startValidatorNode(port = PORT) {
|
|
|
1038
1040
|
rationale: body.rationale,
|
|
1039
1041
|
});
|
|
1040
1042
|
if (!result)
|
|
1041
|
-
return json(res, 500, { error: "Proposal failed
|
|
1043
|
+
return json(res, 500, { error: "Proposal failed - check validator logs" });
|
|
1042
1044
|
return json(res, 201, result);
|
|
1043
1045
|
}
|
|
1044
|
-
// POST /governance/vote
|
|
1046
|
+
// POST /governance/vote - votar en una propuesta
|
|
1045
1047
|
// Body: { proposalId, did, approve }
|
|
1046
1048
|
if (cleanUrl === "/governance/vote" && req.method === "POST") {
|
|
1047
1049
|
if (!client?.isConnected)
|
|
@@ -1056,10 +1058,10 @@ export function startValidatorNode(port = PORT) {
|
|
|
1056
1058
|
approve: Boolean(body.approve),
|
|
1057
1059
|
});
|
|
1058
1060
|
if (!txHash)
|
|
1059
|
-
return json(res, 500, { error: "Vote failed
|
|
1061
|
+
return json(res, 500, { error: "Vote failed - check validator logs" });
|
|
1060
1062
|
return json(res, 200, { txHash, proposalId: body.proposalId, approve: body.approve });
|
|
1061
1063
|
}
|
|
1062
|
-
// POST /governance/execute
|
|
1064
|
+
// POST /governance/execute - ejecutar propuesta post-timelock
|
|
1063
1065
|
// Body: { proposalId }
|
|
1064
1066
|
if (cleanUrl === "/governance/execute" && req.method === "POST") {
|
|
1065
1067
|
if (!client?.isConnected)
|
|
@@ -1069,7 +1071,7 @@ export function startValidatorNode(port = PORT) {
|
|
|
1069
1071
|
return json(res, 400, { error: "Required: proposalId" });
|
|
1070
1072
|
const txHash = await client.executeProposal(Number(body.proposalId));
|
|
1071
1073
|
if (!txHash)
|
|
1072
|
-
return json(res, 500, { error: "Execute failed
|
|
1074
|
+
return json(res, 500, { error: "Execute failed - timelock not expired or proposal not approved" });
|
|
1073
1075
|
return json(res, 200, { txHash, proposalId: body.proposalId, executed: true });
|
|
1074
1076
|
}
|
|
1075
1077
|
json(res, 404, { error: "Not found" });
|
|
@@ -1101,7 +1103,7 @@ export function startValidatorNode(port = PORT) {
|
|
|
1101
1103
|
console.log(` POST /credentials/phone/verify`);
|
|
1102
1104
|
console.log(` GET /credentials/github/start → GitHub OAuth (native fetch)`);
|
|
1103
1105
|
console.log(` GET /credentials/github/callback`);
|
|
1104
|
-
console.log(`\n Anti-farming: ON
|
|
1106
|
+
console.log(`\n Anti-farming: ON - max +1/day, pattern detection, cooldowns`);
|
|
1105
1107
|
console.log(`\n Consensus P2P (sin EVM, sin gas):`);
|
|
1106
1108
|
console.log(` GET /consensus/state-info handshake para state-sync`);
|
|
1107
1109
|
console.log(` GET /consensus/state bulk state sync paginado`);
|
package/package.json
CHANGED