soulprint-network 0.3.9 → 0.4.1

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.
@@ -0,0 +1,49 @@
1
+ /**
2
+ * ProtocolThresholdsClient
3
+ * Lee los thresholds del protocolo desde Base Sepolia.
4
+ * - Cache con TTL (10 min por defecto)
5
+ * - Fallback transparente a constantes locales si la blockchain no es accesible
6
+ * - Solo el superAdmin puede actualizar on-chain; aquí solo leemos
7
+ */
8
+ import { ethers } from "ethers";
9
+ export declare const PROTOCOL_THRESHOLDS_ADDRESS = "0xD8f78d65b35806101672A49801b57F743f2D2ab1";
10
+ export declare const PROTOCOL_THRESHOLDS_CHAIN = "Base Sepolia (chainId: 84532)";
11
+ export declare const PROTOCOL_THRESHOLDS_RPC = "https://sepolia.base.org";
12
+ export interface ProtocolThresholds {
13
+ SCORE_FLOOR: number;
14
+ VERIFIED_SCORE_FLOOR: number;
15
+ MIN_ATTESTER_SCORE: number;
16
+ FACE_SIM_DOC_SELFIE: number;
17
+ FACE_SIM_SELFIE_SELFIE: number;
18
+ DEFAULT_REPUTATION: number;
19
+ IDENTITY_MAX: number;
20
+ REPUTATION_MAX: number;
21
+ VERIFY_RETRY_MAX: number;
22
+ source: "blockchain" | "local_fallback";
23
+ loadedAt: number;
24
+ superAdmin?: string;
25
+ }
26
+ export declare class ProtocolThresholdsClient {
27
+ private provider;
28
+ private contract;
29
+ private cache;
30
+ private cacheTTLMs;
31
+ private address;
32
+ constructor(opts?: {
33
+ rpc?: string;
34
+ address?: string;
35
+ cacheTTLMs?: number;
36
+ });
37
+ /** Carga thresholds desde blockchain — fallback transparente a local */
38
+ load(): Promise<ProtocolThresholds>;
39
+ /** Invalida la cache — el siguiente load() irá a blockchain */
40
+ invalidate(): void;
41
+ /** Lee un threshold individual (con cache) */
42
+ get(name: keyof Omit<ProtocolThresholds, "source" | "loadedAt" | "superAdmin">): Promise<number>;
43
+ /** Devuelve la dirección del contrato */
44
+ get contractAddress(): string;
45
+ /** Acceso directo al contrato para uso en tests */
46
+ get rawContract(): ethers.Contract;
47
+ }
48
+ /** Instancia global (singleton) para uso en el validador */
49
+ export declare const thresholdsClient: ProtocolThresholdsClient;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * ProtocolThresholdsClient
3
+ * Lee los thresholds del protocolo desde Base Sepolia.
4
+ * - Cache con TTL (10 min por defecto)
5
+ * - Fallback transparente a constantes locales si la blockchain no es accesible
6
+ * - Solo el superAdmin puede actualizar on-chain; aquí solo leemos
7
+ */
8
+ import { ethers } from "ethers";
9
+ import { PROTOCOL } from "soulprint-core";
10
+ export const PROTOCOL_THRESHOLDS_ADDRESS = "0xD8f78d65b35806101672A49801b57F743f2D2ab1";
11
+ export const PROTOCOL_THRESHOLDS_CHAIN = "Base Sepolia (chainId: 84532)";
12
+ export const PROTOCOL_THRESHOLDS_RPC = "https://sepolia.base.org";
13
+ const ABI = [
14
+ "function getThreshold(string calldata name) external view returns (uint256)",
15
+ "function superAdmin() external view returns (address)",
16
+ "function pendingSuperAdmin() external view returns (address)",
17
+ "function getAll() external view returns (string[] memory names, uint256[] memory values)",
18
+ "event ThresholdUpdated(bytes32 indexed key, uint256 oldValue, uint256 newValue, address indexed by, uint256 timestamp)",
19
+ ];
20
+ // Valores locales como fallback
21
+ function localFallback(source = "local_fallback") {
22
+ return {
23
+ SCORE_FLOOR: PROTOCOL.SCORE_FLOOR,
24
+ VERIFIED_SCORE_FLOOR: PROTOCOL.VERIFIED_SCORE_FLOOR,
25
+ MIN_ATTESTER_SCORE: PROTOCOL.MIN_ATTESTER_SCORE,
26
+ FACE_SIM_DOC_SELFIE: Math.round(PROTOCOL.FACE_SIM_DOC_SELFIE * 1000),
27
+ FACE_SIM_SELFIE_SELFIE: Math.round(PROTOCOL.FACE_SIM_SELFIE_SELFIE * 1000),
28
+ DEFAULT_REPUTATION: PROTOCOL.DEFAULT_REPUTATION,
29
+ IDENTITY_MAX: PROTOCOL.IDENTITY_MAX,
30
+ REPUTATION_MAX: PROTOCOL.REPUTATION_MAX,
31
+ VERIFY_RETRY_MAX: PROTOCOL.VERIFY_RETRY_MAX,
32
+ source,
33
+ loadedAt: Date.now(),
34
+ };
35
+ }
36
+ export class ProtocolThresholdsClient {
37
+ provider;
38
+ contract;
39
+ cache = null;
40
+ cacheTTLMs;
41
+ address;
42
+ constructor(opts) {
43
+ this.address = opts?.address ?? PROTOCOL_THRESHOLDS_ADDRESS;
44
+ this.cacheTTLMs = opts?.cacheTTLMs ?? 10 * 60 * 1000; // 10 min
45
+ const rpc = opts?.rpc ?? PROTOCOL_THRESHOLDS_RPC;
46
+ this.provider = new ethers.JsonRpcProvider(rpc);
47
+ this.contract = new ethers.Contract(this.address, ABI, this.provider);
48
+ }
49
+ /** Carga thresholds desde blockchain — fallback transparente a local */
50
+ async load() {
51
+ // Cache hit
52
+ if (this.cache && Date.now() - this.cache.loadedAt < this.cacheTTLMs) {
53
+ return this.cache;
54
+ }
55
+ try {
56
+ const [admin, floor, vfloor, minAtt, faceSim, faceSimSS, defRep, idMax, repMax, retryMax] = await Promise.all([
57
+ this.contract.superAdmin(),
58
+ this.contract.getThreshold("SCORE_FLOOR"),
59
+ this.contract.getThreshold("VERIFIED_SCORE_FLOOR"),
60
+ this.contract.getThreshold("MIN_ATTESTER_SCORE"),
61
+ this.contract.getThreshold("FACE_SIM_DOC_SELFIE"),
62
+ this.contract.getThreshold("FACE_SIM_SELFIE_SELFIE"),
63
+ this.contract.getThreshold("DEFAULT_REPUTATION"),
64
+ this.contract.getThreshold("IDENTITY_MAX"),
65
+ this.contract.getThreshold("REPUTATION_MAX"),
66
+ this.contract.getThreshold("VERIFY_RETRY_MAX"),
67
+ ]);
68
+ this.cache = {
69
+ SCORE_FLOOR: Number(floor),
70
+ VERIFIED_SCORE_FLOOR: Number(vfloor),
71
+ MIN_ATTESTER_SCORE: Number(minAtt),
72
+ FACE_SIM_DOC_SELFIE: Number(faceSim),
73
+ FACE_SIM_SELFIE_SELFIE: Number(faceSimSS),
74
+ DEFAULT_REPUTATION: Number(defRep),
75
+ IDENTITY_MAX: Number(idMax),
76
+ REPUTATION_MAX: Number(repMax),
77
+ VERIFY_RETRY_MAX: Number(retryMax),
78
+ source: "blockchain",
79
+ loadedAt: Date.now(),
80
+ superAdmin: admin,
81
+ };
82
+ return this.cache;
83
+ }
84
+ catch (err) {
85
+ console.warn(`[thresholds] ⚠️ Blockchain no disponible — usando fallback local (${err.shortMessage ?? err.message})`);
86
+ return localFallback("local_fallback");
87
+ }
88
+ }
89
+ /** Invalida la cache — el siguiente load() irá a blockchain */
90
+ invalidate() { this.cache = null; }
91
+ /** Lee un threshold individual (con cache) */
92
+ async get(name) {
93
+ const t = await this.load();
94
+ return t[name];
95
+ }
96
+ /** Devuelve la dirección del contrato */
97
+ get contractAddress() { return this.address; }
98
+ /** Acceso directo al contrato para uso en tests */
99
+ get rawContract() { return this.contract; }
100
+ }
101
+ /** Instancia global (singleton) para uso en el validador */
102
+ export const thresholdsClient = new ProtocolThresholdsClient();
@@ -1,11 +1,12 @@
1
1
  {
2
- "codeHash": "d4e94bb11d5974b343bad9b2298c7ab2d3b837a2b5f588dd6570c21aedb3f7c5",
3
- "codeHashHex": "0xd4e94bb11d5974b343bad9b2298c7ab2d3b837a2b5f588dd6570c21aedb3f7c5",
4
- "computedAt": "2026-02-25T01:22:35.030Z",
5
- "fileCount": 19,
2
+ "codeHash": "ea2e97f6d94c08b616fd9aea8833250624cb38e94d1d07aad7b3104d158abd9b",
3
+ "codeHashHex": "0xea2e97f6d94c08b616fd9aea8833250624cb38e94d1d07aad7b3104d158abd9b",
4
+ "computedAt": "2026-02-25T03:34:08.790Z",
5
+ "fileCount": 20,
6
6
  "files": [
7
7
  "blockchain/blockchain-anchor.ts",
8
8
  "blockchain/blockchain-client.ts",
9
+ "blockchain/protocol-thresholds-client.ts",
9
10
  "code-integrity.ts",
10
11
  "consensus/attestation-consensus.ts",
11
12
  "consensus/index.ts",
package/dist/p2p.d.ts CHANGED
@@ -35,3 +35,4 @@ export declare function publishAttestationP2P(node: SoulprintP2PNode, att: BotAt
35
35
  export declare function onAttestationReceived(node: SoulprintP2PNode, handler: (att: BotAttestation, fromPeer: string) => void): void;
36
36
  export declare function getP2PStats(node: SoulprintP2PNode): P2PStats;
37
37
  export declare function stopP2PNode(node: SoulprintP2PNode): Promise<void>;
38
+ export declare function dialP2PPeer(node: SoulprintP2PNode, maddrStr: string, timeoutMs?: number): Promise<boolean>;
package/dist/p2p.js CHANGED
@@ -114,3 +114,35 @@ export async function stopP2PNode(node) {
114
114
  }
115
115
  catch { /* ignorar */ }
116
116
  }
117
+ // ── dial a peer by multiaddr string (best-effort — HTTP gossip is the primary mechanism) ──
118
+ // Note: in WSL2/NAT environments mDNS multicast doesn't work.
119
+ // This function attempts libp2p TCP dial using the peer's advertised multiaddrs.
120
+ // Falls back gracefully — HTTP gossip layer (known_peers) handles message propagation.
121
+ export async function dialP2PPeer(node, maddrStr, timeoutMs = 5_000) {
122
+ try {
123
+ // Extract PeerId from the multiaddr string and add multiaddr to peer store
124
+ // so libp2p can use its own internal Multiaddr class (avoids version mismatch)
125
+ const peerIdStr = maddrStr.split("/p2p/")[1];
126
+ if (!peerIdStr)
127
+ return false;
128
+ // Use peer store to register the address, then dial by PeerId
129
+ // This lets libp2p use its own Multiaddr parsing internally
130
+ const { peerIdFromString } = await import("@libp2p/peer-id");
131
+ const peerId = peerIdFromString(peerIdStr);
132
+ // Register the address in the peer store
133
+ await node.peerStore.merge(peerId, {
134
+ multiaddrs: [maddrStr],
135
+ }).catch(() => {
136
+ // peerStore.merge signature varies by libp2p version — try patch
137
+ return node.peerStore.patch(peerId, { multiaddrs: [maddrStr] }).catch(() => { });
138
+ });
139
+ await Promise.race([
140
+ node.dial(peerId),
141
+ new Promise((_, rej) => setTimeout(() => rej(new Error("dial timeout")), timeoutMs)),
142
+ ]);
143
+ return true;
144
+ }
145
+ catch {
146
+ return false;
147
+ }
148
+ }
package/dist/server.js CHANGED
@@ -11,6 +11,7 @@ import { createSoulprintP2PNode, MAINNET_BOOTSTRAP, stopP2PNode } from "./p2p.js
11
11
  // ─── Config ──────────────────────────────────────────────────────────────────
12
12
  const HTTP_PORT = parseInt(process.env.SOULPRINT_PORT ?? "4888");
13
13
  const P2P_PORT = parseInt(process.env.SOULPRINT_P2P_PORT ?? String(HTTP_PORT + 2000));
14
+ globalThis._startTime = Date.now();
14
15
  // Bootstrap nodes: variables de entorno o mainnet predefinidos
15
16
  const bootstrapEnv = (process.env.SOULPRINT_BOOTSTRAP ?? "")
16
17
  .split(",")
@@ -41,6 +42,39 @@ catch (err) {
41
42
  console.warn(`⚠️ P2P no disponible — solo HTTP gossip activo`);
42
43
  console.warn(` Error: ${err?.message ?? String(err)}\n`);
43
44
  }
45
+ // ─── HTTP Bootstrap Peers (auto-registro) ─────────────────────────────────────
46
+ // SOULPRINT_BOOTSTRAP_HTTP=http://node1:4888,http://node2:4888
47
+ // Registra peers HTTP automáticamente al arrancar (útil en WSL2 / Docker / cloud)
48
+ const httpBootstraps = (process.env.SOULPRINT_BOOTSTRAP_HTTP ?? "")
49
+ .split(",").map(s => s.trim()).filter(s => s.startsWith("http"));
50
+ if (httpBootstraps.length > 0) {
51
+ console.log(`🔗 Bootstrap HTTP: ${httpBootstraps.length} peer(s) configurados`);
52
+ // Esperar 2s a que el HTTP server esté listo antes de registrar
53
+ setTimeout(async () => {
54
+ const PROTOCOL_HASH = process.env.SOULPRINT_PROTOCOL_HASH
55
+ ?? "dfe1ccca1270ec86f93308dc4b981bab1d6bd74bdcc334059f4380b407ca07ca";
56
+ for (const peerUrl of httpBootstraps) {
57
+ try {
58
+ const r = await fetch(`http://localhost:${HTTP_PORT}/peers/register`, {
59
+ method: "POST",
60
+ headers: { "Content-Type": "application/json" },
61
+ body: JSON.stringify({ url: peerUrl, protocol_hash: PROTOCOL_HASH }),
62
+ signal: AbortSignal.timeout(15_000),
63
+ });
64
+ const d = await r.json();
65
+ if (d.ok) {
66
+ console.log(` ✅ Bootstrap peer registrado: ${peerUrl} (total peers: ${d.peers})`);
67
+ }
68
+ else {
69
+ console.warn(` ⚠️ Bootstrap peer rechazado: ${peerUrl} — ${d.error ?? d.reason ?? "?"}`);
70
+ }
71
+ }
72
+ catch (e) {
73
+ console.warn(` ❌ No se pudo conectar a bootstrap peer: ${peerUrl} — ${e.message}`);
74
+ }
75
+ }
76
+ }, 2_000);
77
+ }
44
78
  // ─── Graceful shutdown ────────────────────────────────────────────────────────
45
79
  async function shutdown(signal) {
46
80
  console.log(`\n${signal} recibido — cerrando...`);
@@ -1,6 +1,17 @@
1
1
  import { IncomingMessage, ServerResponse } from "node:http";
2
2
  import { BotAttestation, BotReputation } from "soulprint-core";
3
3
  import { type SoulprintP2PNode } from "./p2p.js";
4
+ export declare function getLiveThresholds(): {
5
+ SCORE_FLOOR: number;
6
+ VERIFIED_SCORE_FLOOR: number;
7
+ MIN_ATTESTER_SCORE: number;
8
+ FACE_SIM_DOC_SELFIE: number;
9
+ FACE_SIM_SELFIE_SELFIE: number;
10
+ DEFAULT_REPUTATION: number;
11
+ IDENTITY_MAX: number;
12
+ REPUTATION_MAX: number;
13
+ source: "blockchain" | "local_fallback";
14
+ };
4
15
  /**
5
16
  * Inyecta el nodo libp2p al validador.
6
17
  * Cuando se llama:
@@ -13,7 +24,7 @@ export declare function setP2PNode(node: SoulprintP2PNode): void;
13
24
  *
14
25
  * PROTOCOL ENFORCEMENT:
15
26
  * - Si el bot tiene DocumentVerified, su score total nunca puede caer por
16
- * debajo de PROTOCOL.VERIFIED_SCORE_FLOOR (52) - inamovible.
27
+ * debajo de liveThresholds.VERIFIED_SCORE_FLOOR (52) - inamovible.
17
28
  * - Anti-replay: la misma attestation (mismo issuer + timestamp + context)
18
29
  * no se puede aplicar dos veces.
19
30
  *
package/dist/validator.js CHANGED
@@ -11,9 +11,10 @@ import { selectGossipPeers, routingStats } from "./crypto/peer-router.js";
11
11
  import { NullifierConsensus, AttestationConsensus, StateSyncManager } from "./consensus/index.js";
12
12
  import { BlockchainAnchor } from "./blockchain/blockchain-anchor.js";
13
13
  import { SoulprintBlockchainClient, loadBlockchainConfig, } from "./blockchain/blockchain-client.js";
14
+ import { thresholdsClient, PROTOCOL_THRESHOLDS_ADDRESS, PROTOCOL_THRESHOLDS_CHAIN, } from "./blockchain/protocol-thresholds-client.js";
14
15
  import { getCodeIntegrity, logCodeIntegrity, computeRuntimeHash } from "./code-integrity.js";
15
16
  import { getMCPEntry, getVerifiedMCPEntries, getAllMCPEntries, getRegistryInfo, verifyMCPOnChain, revokeMCPOnChain, registerMCPOnChain, } from "./mcp-registry-client.js";
16
- import { publishAttestationP2P, onAttestationReceived, getP2PStats, } from "./p2p.js";
17
+ import { publishAttestationP2P, onAttestationReceived, getP2PStats, dialP2PPeer, } from "./p2p.js";
17
18
  // ── Config ────────────────────────────────────────────────────────────────────
18
19
  const PORT = parseInt(process.env.SOULPRINT_PORT ?? String(PROTOCOL.DEFAULT_HTTP_PORT));
19
20
  const NODE_DIR = join(homedir(), ".soulprint", "node");
@@ -28,9 +29,45 @@ const MAX_BODY_BYTES = 64 * 1024;
28
29
  const RATE_LIMIT_MS = PROTOCOL.RATE_LIMIT_WINDOW_MS;
29
30
  const RATE_LIMIT_MAX = PROTOCOL.RATE_LIMIT_MAX;
30
31
  const CLOCK_SKEW_MAX = PROTOCOL.CLOCK_SKEW_MAX_SECONDS;
31
- const MIN_ATTESTER_SCORE = PROTOCOL.MIN_ATTESTER_SCORE; // 65 - inamovible
32
32
  const ATT_MAX_AGE_SECONDS = PROTOCOL.ATT_MAX_AGE_SECONDS;
33
33
  const GOSSIP_TIMEOUT_MS = PROTOCOL.GOSSIP_TIMEOUT_MS;
34
+ // ── Thresholds cargados desde blockchain al arrancar (fallback: local) ────────
35
+ // Usados en runtime — se pueden actualizar solo via superAdmin en el contrato.
36
+ // El validador recarga cada 10 minutos automáticamente.
37
+ let liveThresholds = {
38
+ SCORE_FLOOR: PROTOCOL.SCORE_FLOOR,
39
+ VERIFIED_SCORE_FLOOR: PROTOCOL.VERIFIED_SCORE_FLOOR,
40
+ MIN_ATTESTER_SCORE: PROTOCOL.MIN_ATTESTER_SCORE,
41
+ FACE_SIM_DOC_SELFIE: PROTOCOL.FACE_SIM_DOC_SELFIE,
42
+ FACE_SIM_SELFIE_SELFIE: PROTOCOL.FACE_SIM_SELFIE_SELFIE,
43
+ DEFAULT_REPUTATION: PROTOCOL.DEFAULT_REPUTATION,
44
+ IDENTITY_MAX: PROTOCOL.IDENTITY_MAX,
45
+ REPUTATION_MAX: PROTOCOL.REPUTATION_MAX,
46
+ source: "local_fallback",
47
+ };
48
+ export function getLiveThresholds() { return liveThresholds; }
49
+ async function refreshThresholds() {
50
+ try {
51
+ thresholdsClient.invalidate();
52
+ const t = await thresholdsClient.load();
53
+ liveThresholds = {
54
+ SCORE_FLOOR: t.SCORE_FLOOR,
55
+ VERIFIED_SCORE_FLOOR: t.VERIFIED_SCORE_FLOOR,
56
+ MIN_ATTESTER_SCORE: t.MIN_ATTESTER_SCORE,
57
+ FACE_SIM_DOC_SELFIE: t.FACE_SIM_DOC_SELFIE / 1000,
58
+ FACE_SIM_SELFIE_SELFIE: t.FACE_SIM_SELFIE_SELFIE / 1000,
59
+ DEFAULT_REPUTATION: t.DEFAULT_REPUTATION,
60
+ IDENTITY_MAX: t.IDENTITY_MAX,
61
+ REPUTATION_MAX: t.REPUTATION_MAX,
62
+ source: t.source,
63
+ };
64
+ console.log(`[thresholds] ✅ Cargados desde ${t.source === "blockchain" ? "blockchain" : "fallback local"}`);
65
+ if (t.source === "blockchain") {
66
+ console.log(`[thresholds] SCORE_FLOOR=${t.SCORE_FLOOR} VERIFIED_SCORE_FLOOR=${t.VERIFIED_SCORE_FLOOR} MIN_ATTESTER=${t.MIN_ATTESTER_SCORE}`);
67
+ }
68
+ }
69
+ catch { /* usa los valores actuales */ }
70
+ }
34
71
  // ── P2P Node (Phase 5) ────────────────────────────────────────────────────────
35
72
  let p2pNode = null;
36
73
  /**
@@ -118,7 +155,7 @@ function getReputation(did) {
118
155
  *
119
156
  * PROTOCOL ENFORCEMENT:
120
157
  * - Si el bot tiene DocumentVerified, su score total nunca puede caer por
121
- * debajo de PROTOCOL.VERIFIED_SCORE_FLOOR (52) - inamovible.
158
+ * debajo de liveThresholds.VERIFIED_SCORE_FLOOR (52) - inamovible.
122
159
  * - Anti-replay: la misma attestation (mismo issuer + timestamp + context)
123
160
  * no se puede aplicar dos veces.
124
161
  *
@@ -146,11 +183,11 @@ function applyAttestation(att) {
146
183
  const hasDocument = existing?.hasDocumentVerified ?? false;
147
184
  let finalRepScore = rep.score;
148
185
  if (hasDocument) {
149
- const minRepForFloor = Math.max(0, PROTOCOL.VERIFIED_SCORE_FLOOR - identityFromStore);
186
+ const minRepForFloor = Math.max(0, liveThresholds.VERIFIED_SCORE_FLOOR - identityFromStore);
150
187
  finalRepScore = Math.max(finalRepScore, minRepForFloor);
151
188
  if (finalRepScore !== rep.score) {
152
189
  console.log(`[floor] Reputation clamped for ${att.target_did.slice(0, 20)}...: ` +
153
- `${rep.score} → ${finalRepScore} (VERIFIED_SCORE_FLOOR=${PROTOCOL.VERIFIED_SCORE_FLOOR})`);
190
+ `${rep.score} → ${finalRepScore} (VERIFIED_SCORE_FLOOR=${liveThresholds.VERIFIED_SCORE_FLOOR})`);
154
191
  }
155
192
  }
156
193
  repStore[att.target_did] = {
@@ -293,7 +330,7 @@ function handleInfo(res, nodeKeypair) {
293
330
  * Expone las constantes de protocolo inamovibles.
294
331
  * Los clientes y otros nodos usan este endpoint para:
295
332
  * 1. Verificar compatibilidad de versión antes de conectarse
296
- * 2. Obtener los valores actuales de SCORE_FLOOR y MIN_ATTESTER_SCORE
333
+ * 2. Obtener los valores actuales de SCORE_FLOOR y liveThresholds.MIN_ATTESTER_SCORE
297
334
  * 3. Validar que el nodo no ha sido modificado para bajar los thresholds
298
335
  */
299
336
  function handleProtocol(res) {
@@ -304,9 +341,9 @@ function handleProtocol(res) {
304
341
  // Si PROTOCOL fue modificado (aunque sea un valor), este hash cambia.
305
342
  protocol_hash: PROTOCOL_HASH,
306
343
  // ── Score limits ────────────────────────────────────────────────────────
307
- score_floor: PROTOCOL.SCORE_FLOOR,
308
- verified_score_floor: PROTOCOL.VERIFIED_SCORE_FLOOR,
309
- min_attester_score: PROTOCOL.MIN_ATTESTER_SCORE,
344
+ score_floor: liveThresholds.SCORE_FLOOR,
345
+ verified_score_floor: liveThresholds.VERIFIED_SCORE_FLOOR,
346
+ min_attester_score: liveThresholds.MIN_ATTESTER_SCORE,
310
347
  identity_max: PROTOCOL.IDENTITY_MAX,
311
348
  reputation_max: PROTOCOL.REPUTATION_MAX,
312
349
  max_score: PROTOCOL.MAX_SCORE,
@@ -346,7 +383,7 @@ function handleGetReputation(res, did) {
346
383
  * }
347
384
  *
348
385
  * Validaciones:
349
- * 1. service_spt tiene score >= MIN_ATTESTER_SCORE (solo servicios verificados)
386
+ * 1. service_spt tiene score >= liveThresholds.MIN_ATTESTER_SCORE (solo servicios verificados)
350
387
  * 2. service_spt.did == attestation.issuer_did (el emisor es quien dice ser)
351
388
  * 3. Firma Ed25519 de la attestation es válida
352
389
  * 4. timestamp no tiene más de ATT_MAX_AGE_SECONDS de antigüedad
@@ -422,10 +459,10 @@ async function handleAttest(req, res, ip) {
422
459
  const serviceTok = decodeToken(service_spt);
423
460
  if (!serviceTok)
424
461
  return json(res, 401, { error: "Invalid or expired service_spt" });
425
- if (serviceTok.score < MIN_ATTESTER_SCORE) {
462
+ if (serviceTok.score < liveThresholds.MIN_ATTESTER_SCORE) {
426
463
  return json(res, 403, {
427
- error: `Service score too low (${serviceTok.score} < ${MIN_ATTESTER_SCORE})`,
428
- required: MIN_ATTESTER_SCORE,
464
+ error: `Service score too low (${serviceTok.score} < ${liveThresholds.MIN_ATTESTER_SCORE})`,
465
+ required: liveThresholds.MIN_ATTESTER_SCORE,
429
466
  got: serviceTok.score,
430
467
  });
431
468
  }
@@ -566,6 +603,32 @@ async function handlePeerRegister(req, res) {
566
603
  }
567
604
  peers.push(url);
568
605
  savePeers();
606
+ // ── Auto-dial libp2p layer ──────────────────────────────────────────────────
607
+ // WSL2 / NAT: mDNS no funciona → al registrar un peer HTTP, intentamos
608
+ // conectar también vía libp2p usando sus multiaddrs del /info endpoint.
609
+ if (p2pNode) {
610
+ setImmediate(async () => {
611
+ try {
612
+ const infoRes = await fetch(`${url}/info`, { signal: AbortSignal.timeout(3_000) });
613
+ if (infoRes.ok) {
614
+ const info = await infoRes.json();
615
+ const addrs = info?.p2p?.multiaddrs ?? [];
616
+ let dialed = false;
617
+ for (const ma of addrs) {
618
+ const ok = await dialP2PPeer(p2pNode, ma);
619
+ if (ok) {
620
+ console.log(`[peer] 🔗 P2P dial OK: ${ma}`);
621
+ dialed = true;
622
+ break;
623
+ }
624
+ }
625
+ if (!dialed)
626
+ console.log(`[peer] ℹ️ P2P dial failed for ${url} (mDNS fallback)`);
627
+ }
628
+ }
629
+ catch { /* non-critical — HTTP gossip is the fallback */ }
630
+ });
631
+ }
569
632
  json(res, 200, { ok: true, peers: peers.length, protocol_hash: PROTOCOL_HASH });
570
633
  }
571
634
  // ── GET /peers ─────────────────────────────────────────────────────────────────
@@ -721,7 +784,7 @@ async function handleTokenRenew(req, res, nodeKeypair) {
721
784
  const currentRep = repEntry
722
785
  ? computeTotalScoreWithFloor(repEntry.identityScore ?? 0, repEntry.score ?? 0, repEntry.hasDocumentVerified ?? false)
723
786
  : 0;
724
- const scoreFloor = PROTOCOL.VERIFIED_SCORE_FLOOR ?? 52;
787
+ const scoreFloor = liveThresholds.VERIFIED_SCORE_FLOOR ?? 52;
725
788
  if (currentRep < scoreFloor) {
726
789
  return json(res, 403, {
727
790
  error: "Score por debajo del floor - renovación denegada",
@@ -768,6 +831,14 @@ export function startValidatorNode(port = PORT) {
768
831
  loadPeers();
769
832
  loadAudit();
770
833
  const nodeKeypair = loadOrCreateNodeKeypair();
834
+ // ── Cargar thresholds desde blockchain al arrancar ────────────────────────
835
+ // No bloqueante — el nodo arranca con valores locales y los actualiza async
836
+ refreshThresholds().then(() => {
837
+ console.log(`[thresholds] 📡 Fuente: ${liveThresholds.source} | SCORE_FLOOR=${liveThresholds.SCORE_FLOOR}`);
838
+ console.log(`[thresholds] Contrato: ${PROTOCOL_THRESHOLDS_ADDRESS} (${PROTOCOL_THRESHOLDS_CHAIN})`);
839
+ });
840
+ // Refresco automático cada 10 minutos
841
+ setInterval(refreshThresholds, 10 * 60 * 1000);
771
842
  // ── Módulos de consenso P2P (sin EVM, sin gas fees) ──────────────────────
772
843
  const nullifierConsensus = new NullifierConsensus({
773
844
  selfDid: nodeKeypair.did,
@@ -898,6 +969,52 @@ export function startValidatorNode(port = PORT) {
898
969
  return handleInfo(res, nodeKeypair);
899
970
  if (cleanUrl === "/protocol" && req.method === "GET")
900
971
  return handleProtocol(res);
972
+ // GET /protocol/thresholds — thresholds live desde blockchain (superAdmin-mutable)
973
+ if (cleanUrl === "/protocol/thresholds" && req.method === "GET") {
974
+ return json(res, 200, {
975
+ source: liveThresholds.source,
976
+ contract: PROTOCOL_THRESHOLDS_ADDRESS,
977
+ chain: PROTOCOL_THRESHOLDS_CHAIN,
978
+ thresholds: {
979
+ SCORE_FLOOR: liveThresholds.SCORE_FLOOR,
980
+ VERIFIED_SCORE_FLOOR: liveThresholds.VERIFIED_SCORE_FLOOR,
981
+ MIN_ATTESTER_SCORE: liveThresholds.MIN_ATTESTER_SCORE,
982
+ FACE_SIM_DOC_SELFIE: liveThresholds.FACE_SIM_DOC_SELFIE,
983
+ FACE_SIM_SELFIE_SELFIE: liveThresholds.FACE_SIM_SELFIE_SELFIE,
984
+ DEFAULT_REPUTATION: liveThresholds.DEFAULT_REPUTATION,
985
+ IDENTITY_MAX: liveThresholds.IDENTITY_MAX,
986
+ REPUTATION_MAX: liveThresholds.REPUTATION_MAX,
987
+ },
988
+ last_loaded: new Date(Date.now()).toISOString(),
989
+ note: "Solo el superAdmin del contrato puede modificar estos valores on-chain",
990
+ });
991
+ }
992
+ // GET /network/stats — stats públicas para la landing page
993
+ if (cleanUrl === "/network/stats" && req.method === "GET") {
994
+ const p2pStats = p2pNode ? getP2PStats(p2pNode) : null;
995
+ const httpPeers = peers.length;
996
+ const libp2pPeers = p2pStats?.peers ?? 0;
997
+ return json(res, 200, {
998
+ node_did: nodeKeypair.did.slice(0, 20) + "...",
999
+ version: VERSION,
1000
+ protocol_hash: PROTOCOL_HASH.slice(0, 16) + "...",
1001
+ // identidades y reputación
1002
+ verified_identities: Object.keys(nullifiers).length,
1003
+ reputation_profiles: Object.keys(repStore).length,
1004
+ // peers — HTTP gossip (funciona en todos los entornos)
1005
+ known_peers: httpPeers,
1006
+ // peers — libp2p P2P (requiere multicast/bootstrap; 0 en WSL2)
1007
+ p2p_peers: libp2pPeers,
1008
+ p2p_pubsub_peers: p2pStats?.pubsubPeers ?? 0,
1009
+ p2p_enabled: !!p2pNode,
1010
+ // total = max de ambas capas (HTTP gossip es el piso mínimo garantizado)
1011
+ total_peers: Math.max(httpPeers, libp2pPeers),
1012
+ // estado general
1013
+ uptime_ms: Date.now() - (globalThis._startTime ?? Date.now()),
1014
+ timestamp: Date.now(),
1015
+ mcps_verified: null,
1016
+ });
1017
+ }
901
1018
  if (cleanUrl === "/verify" && req.method === "POST")
902
1019
  return handleVerify(req, res, nodeKeypair, ip);
903
1020
  if (cleanUrl === "/token/renew" && req.method === "POST")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "soulprint-network",
3
- "version": "0.3.9",
3
+ "version": "0.4.1",
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",
@@ -42,8 +42,10 @@
42
42
  "@libp2p/identify": "3.0.39",
43
43
  "@libp2p/kad-dht": "16.1.3",
44
44
  "@libp2p/mdns": "11.0.47",
45
+ "@libp2p/peer-id": "^6.0.4",
45
46
  "@libp2p/ping": "2.0.37",
46
47
  "@libp2p/tcp": "10.1.19",
48
+ "@multiformats/multiaddr": "^13.0.1",
47
49
  "ethers": "^6.16.0",
48
50
  "libp2p": "2.10.0",
49
51
  "nodemailer": "^8.0.1",
@@ -68,4 +70,4 @@
68
70
  "types": "./dist/index.d.ts"
69
71
  }
70
72
  }
71
- }
73
+ }