soulprint-network 0.4.3 → 0.4.4

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.
@@ -1,8 +1,8 @@
1
1
  {
2
- "codeHash": "7f708d77d85f66abb7951d5e2761d23b3f07a64c5e71fdb4db02d6bdb958bbd9",
3
- "codeHashHex": "0x7f708d77d85f66abb7951d5e2761d23b3f07a64c5e71fdb4db02d6bdb958bbd9",
4
- "computedAt": "2026-03-01T01:59:34.396Z",
5
- "fileCount": 21,
2
+ "codeHash": "39a29e119e7cb485d5b7dc5bdbe1533318617ec05a4fc4da321c94253da9d7f9",
3
+ "codeHashHex": "0x39a29e119e7cb485d5b7dc5bdbe1533318617ec05a4fc4da321c94253da9d7f9",
4
+ "computedAt": "2026-03-01T02:07:26.276Z",
5
+ "fileCount": 22,
6
6
  "files": [
7
7
  "blockchain/PeerRegistryClient.ts",
8
8
  "blockchain/blockchain-anchor.ts",
@@ -24,6 +24,7 @@
24
24
  "p2p.ts",
25
25
  "peer-challenge.ts",
26
26
  "server.ts",
27
+ "state/StateStore.ts",
27
28
  "validator.ts"
28
29
  ]
29
30
  }
package/dist/server.js CHANGED
@@ -6,7 +6,8 @@
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, setPeerRegistryClient } from "./validator.js";
9
+ import { startValidatorNode, setP2PNode, setPeerRegistryClient, getNodeState, setLastSyncTs } from "./validator.js";
10
+ import { computeHash, saveState } from "./state/StateStore.js";
10
11
  import { createSoulprintP2PNode, MAINNET_BOOTSTRAP, stopP2PNode } from "./p2p.js";
11
12
  import { PeerRegistryClient } from "./blockchain/PeerRegistryClient.js";
12
13
  // ─── Config ──────────────────────────────────────────────────────────────────
@@ -148,6 +149,58 @@ if (httpBootstraps.length > 0) {
148
149
  }
149
150
  }, 2_000);
150
151
  }
152
+ // ─── Anti-entropy sync loop (v0.4.4) ─────────────────────────────────────────
153
+ // Every 60 seconds: compare state hash with each known peer.
154
+ // If diverged, fetch full state and merge locally.
155
+ setInterval(async () => {
156
+ const { nullifiers, repStore, peers: knownPeers } = getNodeState();
157
+ if (knownPeers.length === 0)
158
+ return;
159
+ const localHash = computeHash(Object.keys(nullifiers));
160
+ for (const peerUrl of knownPeers) {
161
+ try {
162
+ const hashRes = await fetch(`${peerUrl}/state/hash`, { signal: AbortSignal.timeout(5_000) });
163
+ if (!hashRes.ok)
164
+ continue;
165
+ const hashData = await hashRes.json();
166
+ const peerHash = hashData.hash;
167
+ if (peerHash === localHash) {
168
+ console.log(`[sync] peer ${peerUrl}: hash match ✅`);
169
+ continue;
170
+ }
171
+ // Hashes differ — fetch full state and merge
172
+ const exportRes = await fetch(`${peerUrl}/state/export`, { signal: AbortSignal.timeout(10_000) });
173
+ if (!exportRes.ok) {
174
+ console.warn(`[sync] peer ${peerUrl}: export failed (${exportRes.status})`);
175
+ continue;
176
+ }
177
+ const peerState = await exportRes.json();
178
+ const mergeRes = await fetch(`http://localhost:${HTTP_PORT}/state/merge`, {
179
+ method: "POST",
180
+ headers: { "Content-Type": "application/json" },
181
+ body: JSON.stringify(peerState),
182
+ signal: AbortSignal.timeout(5_000),
183
+ });
184
+ const merged = await mergeRes.json();
185
+ console.log(`[sync] peer ${peerUrl}: diverged → merged ${merged.new_nullifiers ?? 0} nullifiers, ${merged.new_attestations ?? 0} attestations`);
186
+ // Persist updated state
187
+ const { nullifiers: n2, repStore: r2, peers: p2 } = getNodeState();
188
+ const ts = Date.now();
189
+ saveState({
190
+ nullifiers: Object.keys(n2),
191
+ reputation: Object.fromEntries(Object.entries(r2).map(([d, e]) => [d, e.score])),
192
+ attestations: Object.values(r2).flatMap((e) => e.attestations ?? []),
193
+ peers: p2,
194
+ lastSync: ts,
195
+ stateHash: computeHash(Object.keys(n2)),
196
+ }, 0);
197
+ setLastSyncTs(ts);
198
+ }
199
+ catch (e) {
200
+ console.warn(`[sync] peer ${peerUrl}: error — ${e.message}`);
201
+ }
202
+ }
203
+ }, 60_000);
151
204
  // ─── Graceful shutdown ────────────────────────────────────────────────────────
152
205
  async function shutdown(signal) {
153
206
  console.log(`\n${signal} recibido — cerrando...`);
@@ -0,0 +1,22 @@
1
+ export interface StoredAttestation {
2
+ issuer_did: string;
3
+ target_did: string;
4
+ value: number;
5
+ context: string;
6
+ timestamp: number;
7
+ sig: string;
8
+ }
9
+ export interface NodeState {
10
+ nullifiers: string[];
11
+ reputation: Record<string, number>;
12
+ attestations: StoredAttestation[];
13
+ peers: string[];
14
+ lastSync: number;
15
+ stateHash: string;
16
+ }
17
+ /** sha256(JSON.stringify(nullifiers.sort())) */
18
+ export declare function computeHash(nullifiers: string[]): string;
19
+ /** Load state from disk. Returns default if file missing or corrupt. */
20
+ export declare function loadState(): NodeState;
21
+ /** Write state to disk — debounced 2 s by default. */
22
+ export declare function saveState(state: NodeState, debounceMs?: number): void;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * StateStore — persistent state for P2P sync
3
+ * Saves/loads full node state to disk with debounced writes.
4
+ */
5
+ import { createHash } from "node:crypto";
6
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
7
+ import { dirname, join } from "node:path";
8
+ import { homedir } from "node:os";
9
+ const STATE_PATH = process.env.SOULPRINT_STATE_PATH
10
+ ?? join(homedir(), ".soulprint", "node", "state.json");
11
+ const DEFAULT_STATE = {
12
+ nullifiers: [],
13
+ reputation: {},
14
+ attestations: [],
15
+ peers: [],
16
+ lastSync: 0,
17
+ stateHash: "",
18
+ };
19
+ let saveTimer = null;
20
+ /** sha256(JSON.stringify(nullifiers.sort())) */
21
+ export function computeHash(nullifiers) {
22
+ const sorted = [...nullifiers].sort();
23
+ return createHash("sha256").update(JSON.stringify(sorted)).digest("hex");
24
+ }
25
+ /** Load state from disk. Returns default if file missing or corrupt. */
26
+ export function loadState() {
27
+ if (!existsSync(STATE_PATH)) {
28
+ return { ...DEFAULT_STATE, stateHash: computeHash([]) };
29
+ }
30
+ try {
31
+ const raw = JSON.parse(readFileSync(STATE_PATH, "utf8"));
32
+ const nullifiers = raw.nullifiers ?? [];
33
+ return {
34
+ nullifiers,
35
+ reputation: raw.reputation ?? {},
36
+ attestations: raw.attestations ?? [],
37
+ peers: raw.peers ?? [],
38
+ lastSync: raw.lastSync ?? 0,
39
+ stateHash: raw.stateHash ?? computeHash(nullifiers),
40
+ };
41
+ }
42
+ catch {
43
+ return { ...DEFAULT_STATE, stateHash: computeHash([]) };
44
+ }
45
+ }
46
+ /** Write state to disk — debounced 2 s by default. */
47
+ export function saveState(state, debounceMs = 2000) {
48
+ if (saveTimer)
49
+ clearTimeout(saveTimer);
50
+ saveTimer = setTimeout(() => {
51
+ try {
52
+ const dir = dirname(STATE_PATH);
53
+ if (!existsSync(dir))
54
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
55
+ writeFileSync(STATE_PATH, JSON.stringify(state, null, 2));
56
+ }
57
+ catch (e) {
58
+ console.error("[state] Failed to save state:", e);
59
+ }
60
+ }, debounceMs);
61
+ }
@@ -21,6 +21,18 @@ export declare function setPeerRegistryClient(client: PeerRegistryClient): void;
21
21
  * 2. Desde ese momento, gossipAttestation() también publica por P2P
22
22
  */
23
23
  export declare function setP2PNode(node: SoulprintP2PNode): void;
24
+ /**
25
+ * Per-DID reputation: score (0-20) + attestation history.
26
+ * Persisted to disk - survives node restarts.
27
+ */
28
+ interface ReputeEntry {
29
+ score: number;
30
+ base: number;
31
+ attestations: BotAttestation[];
32
+ last_updated: number;
33
+ identityScore: number;
34
+ hasDocumentVerified: boolean;
35
+ }
24
36
  /**
25
37
  * Aplica una nueva attestation al DID objetivo y persiste.
26
38
  *
@@ -50,3 +62,14 @@ export declare function getBotReputation(nodeUrl: string, did: string): Promise<
50
62
  export declare function getNodeInfo(nodeUrl: string): Promise<any>;
51
63
  export declare const BOOTSTRAP_NODES: string[];
52
64
  export { applyAttestation };
65
+ /** Used by anti-entropy loop in server.ts */
66
+ export declare function getNodeState(): {
67
+ nullifiers: Record<string, {
68
+ did: string;
69
+ verified_at: number;
70
+ }>;
71
+ repStore: Record<string, ReputeEntry>;
72
+ peers: string[];
73
+ lastSyncTs: number;
74
+ };
75
+ export declare function setLastSyncTs(ts: number): void;
package/dist/validator.js CHANGED
@@ -2,6 +2,7 @@ import { createServer } from "node:http";
2
2
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { homedir } from "node:os";
5
+ import { computeHash, loadState, saveState } from "./state/StateStore.js";
5
6
  import { generateKeypair, keypairFromPrivateKey, decodeToken, sign, createToken, TOKEN_LIFETIME_SECONDS, TOKEN_RENEW_PREEMPTIVE_SECS, TOKEN_RENEW_GRACE_SECS, TOKEN_RENEW_COOLDOWN_SECS, NonceStore, verifyAttestation, computeReputation, defaultReputation, PROTOCOL, PROTOCOL_HASH, isProtocolHashCompatible, computeTotalScoreWithFloor, checkFarming, recordApprovedGain, recordFarmingStrike, loadAuditStore, exportAuditStore, } from "soulprint-core";
6
7
  import { verifyProof, deserializeProof } from "soulprint-zkp";
7
8
  import { buildChallengeResponse, verifyPeerBehavior, } from "./peer-challenge.js";
@@ -206,6 +207,8 @@ function applyAttestation(att) {
206
207
  saveReputation();
207
208
  return { score: finalRepScore, attestations: allAtts.length, last_updated: rep.last_updated };
208
209
  }
210
+ // ── P2P state sync metadata ───────────────────────────────────────────────────
211
+ let lastSyncTs = 0; // timestamp (ms) of last successful anti-entropy sync
209
212
  // ── Peers registry (P2P gossip) ───────────────────────────────────────────────
210
213
  let peers = []; // URLs de otros nodos (ej: "http://node2.example.com:4888")
211
214
  function loadPeers() {
@@ -242,6 +245,9 @@ async function gossipAttestation(att, excludeUrl) {
242
245
  if (targets.length < peers.length - (excludeUrl ? 1 : 0)) {
243
246
  console.log(routingStats(peers.length, targets.length, att.target_did));
244
247
  }
248
+ if (targets.length > 0) {
249
+ console.log(`[gossip] broadcasted to ${targets.length} peers`);
250
+ }
245
251
  // Cifrar el payload con AES-256-GCM antes de enviar
246
252
  // Solo nodos con PROTOCOL_HASH correcto pueden descifrar
247
253
  const encrypted = encryptGossip({ attestation: att, from_peer: true });
@@ -831,6 +837,30 @@ function handleNullifierCheck(res, nullifier) {
831
837
  }
832
838
  // ── Server ────────────────────────────────────────────────────────────────────
833
839
  export function startValidatorNode(port = PORT) {
840
+ // ── Load persistent state from disk (v0.4.4) ───────────────────────────────
841
+ const persisted = loadState();
842
+ if (persisted.nullifiers.length > 0 || Object.keys(persisted.reputation).length > 0) {
843
+ console.log(`[state] Loaded ${persisted.nullifiers.length} nullifiers, ${Object.keys(persisted.reputation).length} reputation entries from disk`);
844
+ // Merge persisted nullifiers into in-memory store
845
+ for (const n of persisted.nullifiers) {
846
+ if (!nullifiers[n]) {
847
+ nullifiers[n] = { did: `did:soulprint:recovered:${n.slice(0, 8)}`, verified_at: persisted.lastSync || Date.now() };
848
+ }
849
+ }
850
+ // Merge persisted reputation
851
+ for (const [did, score] of Object.entries(persisted.reputation)) {
852
+ if (!repStore[did]) {
853
+ repStore[did] = {
854
+ score,
855
+ base: 10,
856
+ attestations: [],
857
+ last_updated: persisted.lastSync || Date.now(),
858
+ identityScore: 0,
859
+ hasDocumentVerified: false,
860
+ };
861
+ }
862
+ }
863
+ }
834
864
  loadNullifiers();
835
865
  loadReputation();
836
866
  loadPeers();
@@ -1044,6 +1074,9 @@ export function startValidatorNode(port = PORT) {
1044
1074
  total_peers: Math.max(httpPeers, libp2pPeers),
1045
1075
  // on-chain registered peers (PeerRegistry)
1046
1076
  registered_peers: registeredPeers,
1077
+ // state sync (v0.4.4)
1078
+ state_hash: computeHash(Object.keys(nullifiers)).slice(0, 16) + "...",
1079
+ last_sync: lastSyncTs,
1047
1080
  // estado general
1048
1081
  uptime_ms: Date.now() - (globalThis._startTime ?? Date.now()),
1049
1082
  timestamp: Date.now(),
@@ -1052,6 +1085,106 @@ export function startValidatorNode(port = PORT) {
1052
1085
  }
1053
1086
  if (cleanUrl === "/verify" && req.method === "POST")
1054
1087
  return handleVerify(req, res, nodeKeypair, ip);
1088
+ // ── State sync endpoints (v0.4.4) ─────────────────────────────────────────
1089
+ // GET /state/hash — quick hash comparison for anti-entropy
1090
+ if (cleanUrl === "/state/hash" && req.method === "GET") {
1091
+ const currentNullifiers = Object.keys(nullifiers);
1092
+ const hash = computeHash(currentNullifiers);
1093
+ return json(res, 200, {
1094
+ hash,
1095
+ nullifier_count: currentNullifiers.length,
1096
+ reputation_count: Object.keys(repStore).length,
1097
+ attestation_count: Object.values(repStore).reduce((n, e) => n + (e.attestations?.length ?? 0), 0),
1098
+ timestamp: Date.now(),
1099
+ });
1100
+ }
1101
+ // GET /state/export — full state export for sync
1102
+ if (cleanUrl === "/state/export" && req.method === "GET") {
1103
+ const allAttestations = Object.values(repStore).flatMap(e => e.attestations ?? []);
1104
+ return json(res, 200, {
1105
+ nullifiers: Object.keys(nullifiers),
1106
+ reputation: Object.fromEntries(Object.entries(repStore).map(([did, e]) => [did, e.score])),
1107
+ attestations: allAttestations,
1108
+ peers,
1109
+ lastSync: lastSyncTs,
1110
+ stateHash: computeHash(Object.keys(nullifiers)),
1111
+ timestamp: Date.now(),
1112
+ });
1113
+ }
1114
+ // POST /state/merge — merge partial state from a peer
1115
+ if (cleanUrl === "/state/merge" && req.method === "POST") {
1116
+ let body;
1117
+ try {
1118
+ body = await readBody(req);
1119
+ }
1120
+ catch (e) {
1121
+ return json(res, 400, { error: e.message });
1122
+ }
1123
+ const incoming = body ?? {};
1124
+ let newNullifiers = 0;
1125
+ let newAttestations = 0;
1126
+ // Merge nullifiers (union)
1127
+ if (Array.isArray(incoming.nullifiers)) {
1128
+ for (const n of incoming.nullifiers) {
1129
+ if (typeof n === "string" && !nullifiers[n]) {
1130
+ nullifiers[n] = { did: `did:soulprint:synced:${n.slice(0, 8)}`, verified_at: Date.now() };
1131
+ newNullifiers++;
1132
+ }
1133
+ }
1134
+ if (newNullifiers > 0)
1135
+ saveNullifiers();
1136
+ }
1137
+ // Merge reputation (take max score)
1138
+ if (incoming.reputation && typeof incoming.reputation === "object") {
1139
+ for (const [did, score] of Object.entries(incoming.reputation)) {
1140
+ if (typeof score !== "number")
1141
+ continue;
1142
+ if (!repStore[did]) {
1143
+ repStore[did] = { score, base: 10, attestations: [], last_updated: Date.now(), identityScore: 0, hasDocumentVerified: false };
1144
+ }
1145
+ else if (score > repStore[did].score) {
1146
+ repStore[did].score = score;
1147
+ repStore[did].last_updated = Date.now();
1148
+ }
1149
+ }
1150
+ if (Object.keys(incoming.reputation).length > 0)
1151
+ saveReputation();
1152
+ }
1153
+ // Merge attestations (dedup by issuer+timestamp+context)
1154
+ if (Array.isArray(incoming.attestations)) {
1155
+ for (const att of incoming.attestations) {
1156
+ if (!att?.issuer_did || !att?.target_did)
1157
+ continue;
1158
+ const existing = repStore[att.target_did];
1159
+ const prevAtts = existing?.attestations ?? [];
1160
+ const isDup = prevAtts.some(a => a.issuer_did === att.issuer_did &&
1161
+ a.timestamp === att.timestamp &&
1162
+ a.context === att.context);
1163
+ if (!isDup) {
1164
+ applyAttestation(att);
1165
+ newAttestations++;
1166
+ }
1167
+ }
1168
+ }
1169
+ // Persist new unified state
1170
+ if (newNullifiers > 0 || newAttestations > 0) {
1171
+ const snapshot = {
1172
+ nullifiers: Object.keys(nullifiers),
1173
+ reputation: Object.fromEntries(Object.entries(repStore).map(([d, e]) => [d, e.score])),
1174
+ attestations: Object.values(repStore).flatMap(e => e.attestations ?? []),
1175
+ peers,
1176
+ lastSync: Date.now(),
1177
+ stateHash: computeHash(Object.keys(nullifiers)),
1178
+ };
1179
+ saveState(snapshot);
1180
+ lastSyncTs = Date.now();
1181
+ }
1182
+ return json(res, 200, {
1183
+ ok: true,
1184
+ new_nullifiers: newNullifiers,
1185
+ new_attestations: newAttestations,
1186
+ });
1187
+ }
1055
1188
  if (cleanUrl === "/token/renew" && req.method === "POST")
1056
1189
  return handleTokenRenew(req, res, nodeKeypair);
1057
1190
  if (cleanUrl === "/challenge" && req.method === "POST")
@@ -1433,3 +1566,8 @@ export async function getNodeInfo(nodeUrl) {
1433
1566
  }
1434
1567
  export const BOOTSTRAP_NODES = [];
1435
1568
  export { applyAttestation };
1569
+ /** Used by anti-entropy loop in server.ts */
1570
+ export function getNodeState() {
1571
+ return { nullifiers, repStore, peers, lastSyncTs };
1572
+ }
1573
+ export function setLastSyncTs(ts) { lastSyncTs = ts; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "soulprint-network",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
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",