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.
@@ -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
+ }
@@ -1,9 +1,10 @@
1
1
  {
2
- "codeHash": "ea2e97f6d94c08b616fd9aea8833250624cb38e94d1d07aad7b3104d158abd9b",
3
- "codeHashHex": "0xea2e97f6d94c08b616fd9aea8833250624cb38e94d1d07aad7b3104d158abd9b",
4
- "computedAt": "2026-02-25T03:34:08.790Z",
5
- "fileCount": 20,
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)
@@ -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.1",
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
  },