soulprint-network 0.4.1 → 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.
- package/dist/blockchain/PeerRegistryClient.d.ts +40 -0
- package/dist/blockchain/PeerRegistryClient.js +125 -0
- package/dist/blockchain/addresses.json +3 -0
- package/dist/code-hash.json +5 -4
- package/dist/server.js +39 -1
- package/dist/validator.d.ts +2 -0
- package/dist/validator.js +35 -0
- package/package.json +2 -2
|
@@ -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();
|
package/dist/code-hash.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
|
-
"codeHash": "
|
|
3
|
-
"codeHashHex": "
|
|
4
|
-
"computedAt": "2026-
|
|
5
|
-
"fileCount":
|
|
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",
|
|
9
10
|
"blockchain/protocol-thresholds-client.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)
|
package/dist/validator.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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";
|
|
4
5
|
export declare function getLiveThresholds(): {
|
|
5
6
|
SCORE_FLOOR: number;
|
|
6
7
|
VERIFIED_SCORE_FLOOR: number;
|
|
@@ -12,6 +13,7 @@ export declare function getLiveThresholds(): {
|
|
|
12
13
|
REPUTATION_MAX: number;
|
|
13
14
|
source: "blockchain" | "local_fallback";
|
|
14
15
|
};
|
|
16
|
+
export declare function setPeerRegistryClient(client: PeerRegistryClient): void;
|
|
15
17
|
/**
|
|
16
18
|
* Inyecta el nodo libp2p al validador.
|
|
17
19
|
* Cuando se llama:
|
package/dist/validator.js
CHANGED
|
@@ -70,6 +70,11 @@ async function refreshThresholds() {
|
|
|
70
70
|
}
|
|
71
71
|
// ── P2P Node (Phase 5) ────────────────────────────────────────────────────────
|
|
72
72
|
let p2pNode = null;
|
|
73
|
+
// ── PeerRegistry Client ───────────────────────────────────────────────────────
|
|
74
|
+
let peerRegistryClient = null;
|
|
75
|
+
export function setPeerRegistryClient(client) {
|
|
76
|
+
peerRegistryClient = client;
|
|
77
|
+
}
|
|
73
78
|
/**
|
|
74
79
|
* Inyecta el nodo libp2p al validador.
|
|
75
80
|
* Cuando se llama:
|
|
@@ -831,6 +836,8 @@ export function startValidatorNode(port = PORT) {
|
|
|
831
836
|
loadPeers();
|
|
832
837
|
loadAudit();
|
|
833
838
|
const nodeKeypair = loadOrCreateNodeKeypair();
|
|
839
|
+
// Expose DID globally so server.ts can use it for peer registration
|
|
840
|
+
globalThis._nodeDid = nodeKeypair.did;
|
|
834
841
|
// ── Cargar thresholds desde blockchain al arrancar ────────────────────────
|
|
835
842
|
// No bloqueante — el nodo arranca con valores locales y los actualiza async
|
|
836
843
|
refreshThresholds().then(() => {
|
|
@@ -989,11 +996,37 @@ export function startValidatorNode(port = PORT) {
|
|
|
989
996
|
note: "Solo el superAdmin del contrato puede modificar estos valores on-chain",
|
|
990
997
|
});
|
|
991
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
|
+
}
|
|
992
1017
|
// GET /network/stats — stats públicas para la landing page
|
|
993
1018
|
if (cleanUrl === "/network/stats" && req.method === "GET") {
|
|
994
1019
|
const p2pStats = p2pNode ? getP2PStats(p2pNode) : null;
|
|
995
1020
|
const httpPeers = peers.length;
|
|
996
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 */ }
|
|
997
1030
|
return json(res, 200, {
|
|
998
1031
|
node_did: nodeKeypair.did.slice(0, 20) + "...",
|
|
999
1032
|
version: VERSION,
|
|
@@ -1009,6 +1042,8 @@ export function startValidatorNode(port = PORT) {
|
|
|
1009
1042
|
p2p_enabled: !!p2pNode,
|
|
1010
1043
|
// total = max de ambas capas (HTTP gossip es el piso mínimo garantizado)
|
|
1011
1044
|
total_peers: Math.max(httpPeers, libp2pPeers),
|
|
1045
|
+
// on-chain registered peers (PeerRegistry)
|
|
1046
|
+
registered_peers: registeredPeers,
|
|
1012
1047
|
// estado general
|
|
1013
1048
|
uptime_ms: Date.now() - (globalThis._startTime ?? Date.now()),
|
|
1014
1049
|
timestamp: Date.now(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "soulprint-network",
|
|
3
|
-
"version": "0.4.
|
|
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",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"README.md"
|
|
13
13
|
],
|
|
14
14
|
"scripts": {
|
|
15
|
-
"build": "tsc && node scripts/compute-code-hash.mjs",
|
|
15
|
+
"build": "tsc && node scripts/compute-code-hash.mjs && cp -f src/blockchain/addresses.json dist/blockchain/addresses.json 2>/dev/null || true",
|
|
16
16
|
"start": "node dist/server.js",
|
|
17
17
|
"build:hash": "node scripts/compute-code-hash.mjs"
|
|
18
18
|
},
|