soulprint-network 0.4.0 → 0.4.2

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,40 @@
1
+ export declare const PEER_REGISTRY_RPC = "https://sepolia.base.org";
2
+ export declare const PEER_REGISTRY_ADDRESS: string;
3
+ export interface PeerEntry {
4
+ peerDid: string;
5
+ peerId: string;
6
+ multiaddr: string;
7
+ score: number;
8
+ lastSeen: number;
9
+ }
10
+ export declare class PeerRegistryClient {
11
+ private provider;
12
+ private contract;
13
+ private wallet?;
14
+ private cache;
15
+ private cacheAt;
16
+ private cacheTTLMs;
17
+ private address;
18
+ constructor(opts?: {
19
+ rpc?: string;
20
+ address?: string;
21
+ privateKey?: string;
22
+ cacheTTLMs?: number;
23
+ });
24
+ /** Get all registered peers (cached, TTL 5 min) */
25
+ getAllPeers(): Promise<PeerEntry[]>;
26
+ /** Force refresh from blockchain */
27
+ refreshPeers(): Promise<PeerEntry[]>;
28
+ /** Register this node on-chain. Non-blocking — logs warning on failure. */
29
+ registerSelf(opts: {
30
+ peerDid: string;
31
+ peerId: string;
32
+ multiaddr: string;
33
+ score?: number;
34
+ }): Promise<void>;
35
+ /** Returns multiaddrs of all peers (for bootstrap) */
36
+ getBootstrapMultiaddrs(): Promise<string[]>;
37
+ get contractAddress(): string;
38
+ }
39
+ /** Singleton instance (read-only) */
40
+ export declare const peerRegistryClient: PeerRegistryClient;
@@ -0,0 +1,125 @@
1
+ /**
2
+ * PeerRegistryClient
3
+ * On-chain peer discovery for Soulprint validator nodes (Base Sepolia).
4
+ *
5
+ * - On startup: reads all peers from contract → used as bootstrap multiaddrs
6
+ * - On startup: registers self (DID, peerId, multiaddr, score=0)
7
+ * - Cache TTL 5 min, refreshPeers() to invalidate
8
+ * - Non-blocking: RPC failures log a warning and continue
9
+ */
10
+ import { ethers } from "ethers";
11
+ export const PEER_REGISTRY_RPC = "https://sepolia.base.org";
12
+ // Load address from addresses.json
13
+ import { readFileSync } from "node:fs";
14
+ import { join, dirname } from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+ const __dirname = dirname(fileURLToPath(import.meta.url));
17
+ function loadAddress() {
18
+ try {
19
+ const f = join(__dirname, "addresses.json");
20
+ const d = JSON.parse(readFileSync(f, "utf8"));
21
+ return d["PeerRegistry"] ?? "";
22
+ }
23
+ catch {
24
+ return "";
25
+ }
26
+ }
27
+ export const PEER_REGISTRY_ADDRESS = loadAddress();
28
+ const ABI = [
29
+ "function registerPeer(string peerDid, string peerId, string multiaddr, uint256 score) external",
30
+ "function removePeer(string peerDid) external",
31
+ "function getPeer(string peerDid) external view returns (string did, string peerId, string multiaddr, uint256 score, uint256 lastSeen)",
32
+ "function getAllPeers() external view returns (tuple(string peerDid, string peerId, string multiaddr, uint256 score, uint256 lastSeen, address registrant)[])",
33
+ "function peerCount() external view returns (uint256)",
34
+ "event PeerRegistered(string indexed peerDid, string peerId, string multiaddr, uint256 score, address indexed registrant, uint256 timestamp)",
35
+ "event PeerUpdated(string indexed peerDid, string peerId, string multiaddr, uint256 score, address indexed registrant, uint256 timestamp)",
36
+ "event PeerRemoved(string indexed peerDid, address indexed removedBy, uint256 timestamp)",
37
+ ];
38
+ export class PeerRegistryClient {
39
+ provider;
40
+ contract;
41
+ wallet;
42
+ cache = null;
43
+ cacheAt = 0;
44
+ cacheTTLMs;
45
+ address;
46
+ constructor(opts) {
47
+ this.address = opts?.address ?? PEER_REGISTRY_ADDRESS;
48
+ this.cacheTTLMs = opts?.cacheTTLMs ?? 5 * 60 * 1000; // 5 min
49
+ const rpc = opts?.rpc ?? PEER_REGISTRY_RPC;
50
+ this.provider = new ethers.JsonRpcProvider(rpc);
51
+ if (!this.address) {
52
+ console.warn("[peer-registry] ⚠️ No contract address — peer registry disabled");
53
+ this.contract = null;
54
+ return;
55
+ }
56
+ if (opts?.privateKey) {
57
+ this.wallet = new ethers.Wallet(opts.privateKey, this.provider);
58
+ this.contract = new ethers.Contract(this.address, ABI, this.wallet);
59
+ }
60
+ else {
61
+ this.contract = new ethers.Contract(this.address, ABI, this.provider);
62
+ }
63
+ }
64
+ /** Get all registered peers (cached, TTL 5 min) */
65
+ async getAllPeers() {
66
+ if (this.cache && Date.now() - this.cacheAt < this.cacheTTLMs) {
67
+ return this.cache;
68
+ }
69
+ return this.refreshPeers();
70
+ }
71
+ /** Force refresh from blockchain */
72
+ async refreshPeers() {
73
+ if (!this.address) {
74
+ console.warn("[peer-registry] ⚠️ No contract address configured — skipping peer fetch");
75
+ return [];
76
+ }
77
+ try {
78
+ const raw = await this.contract.getAllPeers();
79
+ this.cache = raw.map((p) => ({
80
+ peerDid: p.peerDid,
81
+ peerId: p.peerId,
82
+ multiaddr: p.multiaddr,
83
+ score: Number(p.score),
84
+ lastSeen: Number(p.lastSeen),
85
+ }));
86
+ this.cacheAt = Date.now();
87
+ return this.cache;
88
+ }
89
+ catch (err) {
90
+ console.warn(`[peer-registry] ⚠️ Could not fetch peers from chain: ${err.shortMessage ?? err.message}`);
91
+ return this.cache ?? [];
92
+ }
93
+ }
94
+ /** Register this node on-chain. Non-blocking — logs warning on failure. */
95
+ async registerSelf(opts) {
96
+ if (!this.wallet) {
97
+ console.warn("[peer-registry] ⚠️ No private key — cannot register self on-chain");
98
+ return;
99
+ }
100
+ if (!this.address) {
101
+ console.warn("[peer-registry] ⚠️ No contract address — skipping self-registration");
102
+ return;
103
+ }
104
+ try {
105
+ const feeData = await this.provider.getFeeData();
106
+ const tx = await this.contract.registerPeer(opts.peerDid, opts.peerId, opts.multiaddr, BigInt(opts.score ?? 0), { gasPrice: feeData.gasPrice });
107
+ console.log(`[peer-registry] 📡 Registering self on-chain... tx: ${tx.hash}`);
108
+ await tx.wait();
109
+ console.log(`[peer-registry] ✅ Registered: did=${opts.peerDid.slice(0, 20)}... multiaddr=${opts.multiaddr}`);
110
+ // Invalidate cache so next getAllPeers() reflects the new entry
111
+ this.cache = null;
112
+ }
113
+ catch (err) {
114
+ console.warn(`[peer-registry] ⚠️ Self-registration failed (non-fatal): ${err.shortMessage ?? err.message}`);
115
+ }
116
+ }
117
+ /** Returns multiaddrs of all peers (for bootstrap) */
118
+ async getBootstrapMultiaddrs() {
119
+ const peers = await this.getAllPeers();
120
+ return peers.map(p => p.multiaddr).filter(Boolean);
121
+ }
122
+ get contractAddress() { return this.address; }
123
+ }
124
+ /** Singleton instance (read-only) */
125
+ export const peerRegistryClient = new PeerRegistryClient();
@@ -0,0 +1,3 @@
1
+ {
2
+ "PeerRegistry": "0x452fb66159dFCfC13f2fD9627aA4c56886BfB15b"
3
+ }
@@ -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,13 @@
1
1
  {
2
- "codeHash": "71e41f8c443a60f95aa13ae942aa7c36a242ca69b02c43cdb973aaed92f004d5",
3
- "codeHashHex": "0x71e41f8c443a60f95aa13ae942aa7c36a242ca69b02c43cdb973aaed92f004d5",
4
- "computedAt": "2026-02-25T03:16:23.336Z",
5
- "fileCount": 19,
2
+ "codeHash": "740841a4e4069e7576a21b2da277ae7bb5b65bed028121cbc3277c865f58544f",
3
+ "codeHashHex": "0x740841a4e4069e7576a21b2da277ae7bb5b65bed028121cbc3277c865f58544f",
4
+ "computedAt": "2026-03-01T01:46:41.787Z",
5
+ "fileCount": 21,
6
6
  "files": [
7
+ "blockchain/PeerRegistryClient.ts",
7
8
  "blockchain/blockchain-anchor.ts",
8
9
  "blockchain/blockchain-client.ts",
10
+ "blockchain/protocol-thresholds-client.ts",
9
11
  "code-integrity.ts",
10
12
  "consensus/attestation-consensus.ts",
11
13
  "consensus/index.ts",
package/dist/server.js CHANGED
@@ -6,8 +6,9 @@
6
6
  * 1. HTTP server (port 4888) — clientes y legado
7
7
  * 2. libp2p P2P node (port 6888) — Kademlia DHT + GossipSub + mDNS
8
8
  */
9
- import { startValidatorNode, setP2PNode } from "./validator.js";
9
+ import { startValidatorNode, setP2PNode, setPeerRegistryClient } from "./validator.js";
10
10
  import { createSoulprintP2PNode, MAINNET_BOOTSTRAP, stopP2PNode } from "./p2p.js";
11
+ import { PeerRegistryClient } from "./blockchain/PeerRegistryClient.js";
11
12
  // ─── Config ──────────────────────────────────────────────────────────────────
12
13
  const HTTP_PORT = parseInt(process.env.SOULPRINT_PORT ?? "4888");
13
14
  const P2P_PORT = parseInt(process.env.SOULPRINT_P2P_PORT ?? String(HTTP_PORT + 2000));
@@ -42,6 +43,43 @@ catch (err) {
42
43
  console.warn(`⚠️ P2P no disponible — solo HTTP gossip activo`);
43
44
  console.warn(` Error: ${err?.message ?? String(err)}\n`);
44
45
  }
46
+ // ─── On-chain PeerRegistry (auto-registro) ────────────────────────────────────
47
+ const adminPrivateKey = process.env.ADMIN_PRIVATE_KEY;
48
+ const peerRegistry = new PeerRegistryClient({
49
+ privateKey: adminPrivateKey,
50
+ });
51
+ setPeerRegistryClient(peerRegistry);
52
+ // Register self after P2P is ready (non-blocking)
53
+ setTimeout(async () => {
54
+ try {
55
+ if (!adminPrivateKey) {
56
+ console.warn("[peer-registry] ⚠️ ADMIN_PRIVATE_KEY not set — skipping on-chain registration");
57
+ return;
58
+ }
59
+ // Get node identity from validator (fetched via HTTP)
60
+ const infoRes = await fetch(`http://localhost:${HTTP_PORT}/health`, { signal: AbortSignal.timeout(5000) });
61
+ const info = await infoRes.json();
62
+ // Get P2P multiaddr (first non-localhost if available)
63
+ let multiaddr = `http://localhost:${HTTP_PORT}`;
64
+ if (p2pNode) {
65
+ const addrs = p2pNode.getMultiaddrs().map((m) => m.toString());
66
+ const publicAddr = addrs.find(a => !a.includes("127.0.0.1") && !a.includes("/ip4/0.0.0.0"));
67
+ multiaddr = publicAddr ?? addrs[0] ?? multiaddr;
68
+ }
69
+ // Use node DID from keypair (read from health endpoint) and P2P peerId
70
+ const nodeDid = globalThis._nodeDid ?? `did:soulprint:node:${Date.now()}`;
71
+ const nodePeer = p2pNode?.peerId?.toString() ?? "";
72
+ await peerRegistry.registerSelf({
73
+ peerDid: nodeDid,
74
+ peerId: nodePeer,
75
+ multiaddr,
76
+ score: 0,
77
+ });
78
+ }
79
+ catch (e) {
80
+ console.warn(`[peer-registry] ⚠️ Could not register self: ${e.message}`);
81
+ }
82
+ }, 3_000);
45
83
  // ─── HTTP Bootstrap Peers (auto-registro) ─────────────────────────────────────
46
84
  // SOULPRINT_BOOTSTRAP_HTTP=http://node1:4888,http://node2:4888
47
85
  // Registra peers HTTP automáticamente al arrancar (útil en WSL2 / Docker / cloud)
@@ -1,6 +1,19 @@
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
+ import { PeerRegistryClient } from "./blockchain/PeerRegistryClient.js";
5
+ export declare function getLiveThresholds(): {
6
+ SCORE_FLOOR: number;
7
+ VERIFIED_SCORE_FLOOR: number;
8
+ MIN_ATTESTER_SCORE: number;
9
+ FACE_SIM_DOC_SELFIE: number;
10
+ FACE_SIM_SELFIE_SELFIE: number;
11
+ DEFAULT_REPUTATION: number;
12
+ IDENTITY_MAX: number;
13
+ REPUTATION_MAX: number;
14
+ source: "blockchain" | "local_fallback";
15
+ };
16
+ export declare function setPeerRegistryClient(client: PeerRegistryClient): void;
4
17
  /**
5
18
  * Inyecta el nodo libp2p al validador.
6
19
  * Cuando se llama:
@@ -13,7 +26,7 @@ export declare function setP2PNode(node: SoulprintP2PNode): void;
13
26
  *
14
27
  * PROTOCOL ENFORCEMENT:
15
28
  * - Si el bot tiene DocumentVerified, su score total nunca puede caer por
16
- * debajo de PROTOCOL.VERIFIED_SCORE_FLOOR (52) - inamovible.
29
+ * debajo de liveThresholds.VERIFIED_SCORE_FLOOR (52) - inamovible.
17
30
  * - Anti-replay: la misma attestation (mismo issuer + timestamp + context)
18
31
  * no se puede aplicar dos veces.
19
32
  *
package/dist/validator.js CHANGED
@@ -11,6 +11,7 @@ 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
17
  import { publishAttestationP2P, onAttestationReceived, getP2PStats, dialP2PPeer, } from "./p2p.js";
@@ -28,11 +29,52 @@ 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;
73
+ // ── PeerRegistry Client ───────────────────────────────────────────────────────
74
+ let peerRegistryClient = null;
75
+ export function setPeerRegistryClient(client) {
76
+ peerRegistryClient = client;
77
+ }
36
78
  /**
37
79
  * Inyecta el nodo libp2p al validador.
38
80
  * Cuando se llama:
@@ -118,7 +160,7 @@ function getReputation(did) {
118
160
  *
119
161
  * PROTOCOL ENFORCEMENT:
120
162
  * - Si el bot tiene DocumentVerified, su score total nunca puede caer por
121
- * debajo de PROTOCOL.VERIFIED_SCORE_FLOOR (52) - inamovible.
163
+ * debajo de liveThresholds.VERIFIED_SCORE_FLOOR (52) - inamovible.
122
164
  * - Anti-replay: la misma attestation (mismo issuer + timestamp + context)
123
165
  * no se puede aplicar dos veces.
124
166
  *
@@ -146,11 +188,11 @@ function applyAttestation(att) {
146
188
  const hasDocument = existing?.hasDocumentVerified ?? false;
147
189
  let finalRepScore = rep.score;
148
190
  if (hasDocument) {
149
- const minRepForFloor = Math.max(0, PROTOCOL.VERIFIED_SCORE_FLOOR - identityFromStore);
191
+ const minRepForFloor = Math.max(0, liveThresholds.VERIFIED_SCORE_FLOOR - identityFromStore);
150
192
  finalRepScore = Math.max(finalRepScore, minRepForFloor);
151
193
  if (finalRepScore !== rep.score) {
152
194
  console.log(`[floor] Reputation clamped for ${att.target_did.slice(0, 20)}...: ` +
153
- `${rep.score} → ${finalRepScore} (VERIFIED_SCORE_FLOOR=${PROTOCOL.VERIFIED_SCORE_FLOOR})`);
195
+ `${rep.score} → ${finalRepScore} (VERIFIED_SCORE_FLOOR=${liveThresholds.VERIFIED_SCORE_FLOOR})`);
154
196
  }
155
197
  }
156
198
  repStore[att.target_did] = {
@@ -293,7 +335,7 @@ function handleInfo(res, nodeKeypair) {
293
335
  * Expone las constantes de protocolo inamovibles.
294
336
  * Los clientes y otros nodos usan este endpoint para:
295
337
  * 1. Verificar compatibilidad de versión antes de conectarse
296
- * 2. Obtener los valores actuales de SCORE_FLOOR y MIN_ATTESTER_SCORE
338
+ * 2. Obtener los valores actuales de SCORE_FLOOR y liveThresholds.MIN_ATTESTER_SCORE
297
339
  * 3. Validar que el nodo no ha sido modificado para bajar los thresholds
298
340
  */
299
341
  function handleProtocol(res) {
@@ -304,9 +346,9 @@ function handleProtocol(res) {
304
346
  // Si PROTOCOL fue modificado (aunque sea un valor), este hash cambia.
305
347
  protocol_hash: PROTOCOL_HASH,
306
348
  // ── Score limits ────────────────────────────────────────────────────────
307
- score_floor: PROTOCOL.SCORE_FLOOR,
308
- verified_score_floor: PROTOCOL.VERIFIED_SCORE_FLOOR,
309
- min_attester_score: PROTOCOL.MIN_ATTESTER_SCORE,
349
+ score_floor: liveThresholds.SCORE_FLOOR,
350
+ verified_score_floor: liveThresholds.VERIFIED_SCORE_FLOOR,
351
+ min_attester_score: liveThresholds.MIN_ATTESTER_SCORE,
310
352
  identity_max: PROTOCOL.IDENTITY_MAX,
311
353
  reputation_max: PROTOCOL.REPUTATION_MAX,
312
354
  max_score: PROTOCOL.MAX_SCORE,
@@ -346,7 +388,7 @@ function handleGetReputation(res, did) {
346
388
  * }
347
389
  *
348
390
  * Validaciones:
349
- * 1. service_spt tiene score >= MIN_ATTESTER_SCORE (solo servicios verificados)
391
+ * 1. service_spt tiene score >= liveThresholds.MIN_ATTESTER_SCORE (solo servicios verificados)
350
392
  * 2. service_spt.did == attestation.issuer_did (el emisor es quien dice ser)
351
393
  * 3. Firma Ed25519 de la attestation es válida
352
394
  * 4. timestamp no tiene más de ATT_MAX_AGE_SECONDS de antigüedad
@@ -422,10 +464,10 @@ async function handleAttest(req, res, ip) {
422
464
  const serviceTok = decodeToken(service_spt);
423
465
  if (!serviceTok)
424
466
  return json(res, 401, { error: "Invalid or expired service_spt" });
425
- if (serviceTok.score < MIN_ATTESTER_SCORE) {
467
+ if (serviceTok.score < liveThresholds.MIN_ATTESTER_SCORE) {
426
468
  return json(res, 403, {
427
- error: `Service score too low (${serviceTok.score} < ${MIN_ATTESTER_SCORE})`,
428
- required: MIN_ATTESTER_SCORE,
469
+ error: `Service score too low (${serviceTok.score} < ${liveThresholds.MIN_ATTESTER_SCORE})`,
470
+ required: liveThresholds.MIN_ATTESTER_SCORE,
429
471
  got: serviceTok.score,
430
472
  });
431
473
  }
@@ -747,7 +789,7 @@ async function handleTokenRenew(req, res, nodeKeypair) {
747
789
  const currentRep = repEntry
748
790
  ? computeTotalScoreWithFloor(repEntry.identityScore ?? 0, repEntry.score ?? 0, repEntry.hasDocumentVerified ?? false)
749
791
  : 0;
750
- const scoreFloor = PROTOCOL.VERIFIED_SCORE_FLOOR ?? 52;
792
+ const scoreFloor = liveThresholds.VERIFIED_SCORE_FLOOR ?? 52;
751
793
  if (currentRep < scoreFloor) {
752
794
  return json(res, 403, {
753
795
  error: "Score por debajo del floor - renovación denegada",
@@ -794,6 +836,16 @@ export function startValidatorNode(port = PORT) {
794
836
  loadPeers();
795
837
  loadAudit();
796
838
  const nodeKeypair = loadOrCreateNodeKeypair();
839
+ // Expose DID globally so server.ts can use it for peer registration
840
+ globalThis._nodeDid = nodeKeypair.did;
841
+ // ── Cargar thresholds desde blockchain al arrancar ────────────────────────
842
+ // No bloqueante — el nodo arranca con valores locales y los actualiza async
843
+ refreshThresholds().then(() => {
844
+ console.log(`[thresholds] 📡 Fuente: ${liveThresholds.source} | SCORE_FLOOR=${liveThresholds.SCORE_FLOOR}`);
845
+ console.log(`[thresholds] Contrato: ${PROTOCOL_THRESHOLDS_ADDRESS} (${PROTOCOL_THRESHOLDS_CHAIN})`);
846
+ });
847
+ // Refresco automático cada 10 minutos
848
+ setInterval(refreshThresholds, 10 * 60 * 1000);
797
849
  // ── Módulos de consenso P2P (sin EVM, sin gas fees) ──────────────────────
798
850
  const nullifierConsensus = new NullifierConsensus({
799
851
  selfDid: nodeKeypair.did,
@@ -924,11 +976,57 @@ export function startValidatorNode(port = PORT) {
924
976
  return handleInfo(res, nodeKeypair);
925
977
  if (cleanUrl === "/protocol" && req.method === "GET")
926
978
  return handleProtocol(res);
979
+ // GET /protocol/thresholds — thresholds live desde blockchain (superAdmin-mutable)
980
+ if (cleanUrl === "/protocol/thresholds" && req.method === "GET") {
981
+ return json(res, 200, {
982
+ source: liveThresholds.source,
983
+ contract: PROTOCOL_THRESHOLDS_ADDRESS,
984
+ chain: PROTOCOL_THRESHOLDS_CHAIN,
985
+ thresholds: {
986
+ SCORE_FLOOR: liveThresholds.SCORE_FLOOR,
987
+ VERIFIED_SCORE_FLOOR: liveThresholds.VERIFIED_SCORE_FLOOR,
988
+ MIN_ATTESTER_SCORE: liveThresholds.MIN_ATTESTER_SCORE,
989
+ FACE_SIM_DOC_SELFIE: liveThresholds.FACE_SIM_DOC_SELFIE,
990
+ FACE_SIM_SELFIE_SELFIE: liveThresholds.FACE_SIM_SELFIE_SELFIE,
991
+ DEFAULT_REPUTATION: liveThresholds.DEFAULT_REPUTATION,
992
+ IDENTITY_MAX: liveThresholds.IDENTITY_MAX,
993
+ REPUTATION_MAX: liveThresholds.REPUTATION_MAX,
994
+ },
995
+ last_loaded: new Date(Date.now()).toISOString(),
996
+ note: "Solo el superAdmin del contrato puede modificar estos valores on-chain",
997
+ });
998
+ }
999
+ // GET /network/peers — all peers from on-chain PeerRegistry
1000
+ if (cleanUrl === "/network/peers" && req.method === "GET") {
1001
+ try {
1002
+ const chainPeers = peerRegistryClient
1003
+ ? await peerRegistryClient.getAllPeers()
1004
+ : [];
1005
+ return json(res, 200, {
1006
+ ok: true,
1007
+ peers: chainPeers,
1008
+ count: chainPeers.length,
1009
+ contract: peerRegistryClient?.contractAddress ?? null,
1010
+ timestamp: Date.now(),
1011
+ });
1012
+ }
1013
+ catch (err) {
1014
+ return json(res, 500, { ok: false, error: err.message });
1015
+ }
1016
+ }
927
1017
  // GET /network/stats — stats públicas para la landing page
928
1018
  if (cleanUrl === "/network/stats" && req.method === "GET") {
929
1019
  const p2pStats = p2pNode ? getP2PStats(p2pNode) : null;
930
1020
  const httpPeers = peers.length;
931
1021
  const libp2pPeers = p2pStats?.peers ?? 0;
1022
+ let registeredPeers = 0;
1023
+ try {
1024
+ if (peerRegistryClient) {
1025
+ const chainPeers = await peerRegistryClient.getAllPeers();
1026
+ registeredPeers = chainPeers.length;
1027
+ }
1028
+ }
1029
+ catch { /* non-fatal */ }
932
1030
  return json(res, 200, {
933
1031
  node_did: nodeKeypair.did.slice(0, 20) + "...",
934
1032
  version: VERSION,
@@ -944,6 +1042,8 @@ export function startValidatorNode(port = PORT) {
944
1042
  p2p_enabled: !!p2pNode,
945
1043
  // total = max de ambas capas (HTTP gossip es el piso mínimo garantizado)
946
1044
  total_peers: Math.max(httpPeers, libp2pPeers),
1045
+ // on-chain registered peers (PeerRegistry)
1046
+ registered_peers: registeredPeers,
947
1047
  // estado general
948
1048
  uptime_ms: Date.now() - (globalThis._startTime ?? Date.now()),
949
1049
  timestamp: Date.now(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "soulprint-network",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
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",
@@ -11,6 +11,11 @@
11
11
  "dist",
12
12
  "README.md"
13
13
  ],
14
+ "scripts": {
15
+ "build": "tsc && node scripts/compute-code-hash.mjs && cp -f src/blockchain/addresses.json dist/blockchain/addresses.json 2>/dev/null || true",
16
+ "start": "node dist/server.js",
17
+ "build:hash": "node scripts/compute-code-hash.mjs"
18
+ },
14
19
  "publishConfig": {
15
20
  "access": "public"
16
21
  },
@@ -46,9 +51,9 @@
46
51
  "nodemailer": "^8.0.1",
47
52
  "otpauth": "^9.5.0",
48
53
  "otplib": "^13.3.0",
49
- "uint8arrays": "5.1.0",
50
- "soulprint-core": "0.1.11",
51
- "soulprint-zkp": "0.1.5"
54
+ "soulprint-core": "workspace:*",
55
+ "soulprint-zkp": "workspace:*",
56
+ "uint8arrays": "5.1.0"
52
57
  },
53
58
  "devDependencies": {
54
59
  "@types/node": "^20.0.0",
@@ -64,10 +69,5 @@
64
69
  "import": "./dist/index.js",
65
70
  "types": "./dist/index.d.ts"
66
71
  }
67
- },
68
- "scripts": {
69
- "build": "tsc && node scripts/compute-code-hash.mjs",
70
- "start": "node dist/server.js",
71
- "build:hash": "node scripts/compute-code-hash.mjs"
72
72
  }
73
73
  }