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.
- package/dist/blockchain/protocol-thresholds-client.d.ts +49 -0
- package/dist/blockchain/protocol-thresholds-client.js +102 -0
- package/dist/code-hash.json +5 -4
- package/dist/p2p.d.ts +1 -0
- package/dist/p2p.js +32 -0
- package/dist/server.js +34 -0
- package/dist/validator.d.ts +12 -1
- package/dist/validator.js +131 -14
- package/package.json +4 -2
|
@@ -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();
|
package/dist/code-hash.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
|
-
"codeHash": "
|
|
3
|
-
"codeHashHex": "
|
|
4
|
-
"computedAt": "2026-02-
|
|
5
|
-
"fileCount":
|
|
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...`);
|
package/dist/validator.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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,
|
|
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=${
|
|
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:
|
|
308
|
-
verified_score_floor:
|
|
309
|
-
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 =
|
|
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
|
+
"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
|
+
}
|