soulprint-network 0.4.4 → 0.4.6

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,46 @@
1
+ export declare const NULLIFIER_REGISTRY_RPC = "https://sepolia.base.org";
2
+ export declare const NULLIFIER_REGISTRY_ADDRESS: string;
3
+ export interface NullifierEntry {
4
+ nullifier: string;
5
+ did: string;
6
+ score: number;
7
+ timestamp: number;
8
+ }
9
+ export declare class NullifierRegistryClient {
10
+ private provider;
11
+ private contract;
12
+ private wallet?;
13
+ private cache;
14
+ private cacheAt;
15
+ private cacheTTLMs;
16
+ private address;
17
+ constructor(opts?: {
18
+ rpc?: string;
19
+ address?: string;
20
+ privateKey?: string;
21
+ cacheTTLMs?: number;
22
+ });
23
+ /**
24
+ * Sign a nullifier payload with the validator's private key and register on-chain.
25
+ * Non-blocking — logs warning on failure.
26
+ *
27
+ * The contract verifies the ECDSA signature over keccak256(nullifier, did, score).
28
+ */
29
+ registerNullifier(opts: {
30
+ nullifier: string;
31
+ did: string;
32
+ score?: number;
33
+ }): Promise<void>;
34
+ /**
35
+ * Check if a nullifier is registered on-chain.
36
+ */
37
+ isRegistered(nullifier: string): Promise<boolean>;
38
+ /**
39
+ * Get all registered nullifiers (cached 2 min).
40
+ */
41
+ getAllNullifiers(): Promise<NullifierEntry[]>;
42
+ refreshNullifiers(): Promise<NullifierEntry[]>;
43
+ getCount(): Promise<number>;
44
+ get contractAddress(): string;
45
+ }
46
+ export declare const nullifierRegistryClient: NullifierRegistryClient;
@@ -0,0 +1,157 @@
1
+ /**
2
+ * NullifierRegistryClient
3
+ * On-chain nullifier registry for Soulprint (Base Sepolia).
4
+ *
5
+ * The soulprint.digital validator (authorizedValidator) signs every nullifier
6
+ * before writing it on-chain. Read-only access is public — anyone can verify
7
+ * isRegistered(nullifier) without trust in any third party.
8
+ *
9
+ * Architecture (v0.5.0):
10
+ * - WRITE: only soulprint.digital validator (ADMIN_PRIVATE_KEY)
11
+ * - READ: anyone, cached 2 min
12
+ */
13
+ import { ethers } from "ethers";
14
+ import { readFileSync } from "node:fs";
15
+ import { join, dirname } from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+ const __dirname = dirname(fileURLToPath(import.meta.url));
18
+ export const NULLIFIER_REGISTRY_RPC = "https://sepolia.base.org";
19
+ function loadAddress() {
20
+ try {
21
+ const f = join(__dirname, "addresses.json");
22
+ const d = JSON.parse(readFileSync(f, "utf8"));
23
+ return d["NullifierRegistry"] ?? "";
24
+ }
25
+ catch {
26
+ return "";
27
+ }
28
+ }
29
+ export const NULLIFIER_REGISTRY_ADDRESS = loadAddress();
30
+ const ABI = [
31
+ "function registerNullifier(bytes32 nullifier, string did, uint256 score, bytes sig) external",
32
+ "function isRegistered(bytes32 nullifier) external view returns (bool)",
33
+ "function getNullifier(bytes32 nullifier) external view returns (bytes32 nul, string did, uint256 score, uint256 timestamp)",
34
+ "function getAllNullifiers() external view returns (tuple(bytes32 nullifier, string did, uint256 score, uint256 timestamp)[])",
35
+ "function getNullifierCount() external view returns (uint256)",
36
+ "function authorizedValidator() external view returns (address)",
37
+ "event NullifierRegistered(bytes32 indexed nullifier, string did, uint256 score, uint256 timestamp)",
38
+ ];
39
+ export class NullifierRegistryClient {
40
+ provider;
41
+ contract;
42
+ wallet;
43
+ cache = null;
44
+ cacheAt = 0;
45
+ cacheTTLMs;
46
+ address;
47
+ constructor(opts) {
48
+ this.address = opts?.address ?? NULLIFIER_REGISTRY_ADDRESS;
49
+ this.cacheTTLMs = opts?.cacheTTLMs ?? 2 * 60 * 1000; // 2 min
50
+ const rpc = opts?.rpc ?? NULLIFIER_REGISTRY_RPC;
51
+ this.provider = new ethers.JsonRpcProvider(rpc);
52
+ if (!this.address) {
53
+ console.warn("[nullifier-registry] ⚠️ No contract address — registry disabled");
54
+ this.contract = null;
55
+ return;
56
+ }
57
+ if (opts?.privateKey) {
58
+ this.wallet = new ethers.Wallet(opts.privateKey, this.provider);
59
+ this.contract = new ethers.Contract(this.address, ABI, this.wallet);
60
+ }
61
+ else {
62
+ this.contract = new ethers.Contract(this.address, ABI, this.provider);
63
+ }
64
+ }
65
+ /**
66
+ * Sign a nullifier payload with the validator's private key and register on-chain.
67
+ * Non-blocking — logs warning on failure.
68
+ *
69
+ * The contract verifies the ECDSA signature over keccak256(nullifier, did, score).
70
+ */
71
+ async registerNullifier(opts) {
72
+ if (!this.wallet) {
73
+ console.warn("[nullifier-registry] ⚠️ No private key — cannot register nullifier");
74
+ return;
75
+ }
76
+ if (!this.address) {
77
+ console.warn("[nullifier-registry] ⚠️ No contract address — skipping registration");
78
+ return;
79
+ }
80
+ try {
81
+ const nullifierBytes = opts.nullifier.startsWith("0x")
82
+ ? opts.nullifier
83
+ : `0x${opts.nullifier}`;
84
+ const score = BigInt(opts.score ?? 0);
85
+ // Create the message hash matching what the contract verifies
86
+ const msgHash = ethers.keccak256(ethers.solidityPacked(["bytes32", "string", "uint256"], [nullifierBytes, opts.did, score]));
87
+ // Sign with Ethereum personal_sign prefix (matches contract's \x19Ethereum Signed Message:\n32)
88
+ const sig = await this.wallet.signMessage(ethers.getBytes(msgHash));
89
+ const feeData = await this.provider.getFeeData();
90
+ const tx = await this.contract.registerNullifier(nullifierBytes, opts.did, score, sig, { gasPrice: feeData.gasPrice });
91
+ console.log(`[nullifier-registry] 📡 Registering nullifier on-chain... tx: ${tx.hash}`);
92
+ await tx.wait();
93
+ console.log(`[nullifier-registry] ✅ Nullifier registered: ${nullifierBytes.slice(0, 18)}... did=${opts.did.slice(0, 20)}...`);
94
+ // Invalidate cache
95
+ this.cache = null;
96
+ }
97
+ catch (err) {
98
+ console.warn(`[nullifier-registry] ⚠️ Registration failed (non-fatal): ${err.shortMessage ?? err.message}`);
99
+ }
100
+ }
101
+ /**
102
+ * Check if a nullifier is registered on-chain.
103
+ */
104
+ async isRegistered(nullifier) {
105
+ if (!this.address)
106
+ return false;
107
+ try {
108
+ const n = nullifier.startsWith("0x") ? nullifier : `0x${nullifier}`;
109
+ return await this.contract.isRegistered(n);
110
+ }
111
+ catch (err) {
112
+ console.warn(`[nullifier-registry] ⚠️ isRegistered failed: ${err.shortMessage ?? err.message}`);
113
+ return false;
114
+ }
115
+ }
116
+ /**
117
+ * Get all registered nullifiers (cached 2 min).
118
+ */
119
+ async getAllNullifiers() {
120
+ if (this.cache && Date.now() - this.cacheAt < this.cacheTTLMs) {
121
+ return this.cache;
122
+ }
123
+ return this.refreshNullifiers();
124
+ }
125
+ async refreshNullifiers() {
126
+ if (!this.address)
127
+ return [];
128
+ try {
129
+ const raw = await this.contract.getAllNullifiers();
130
+ this.cache = raw.map((e) => ({
131
+ nullifier: e.nullifier,
132
+ did: e.did,
133
+ score: Number(e.score),
134
+ timestamp: Number(e.timestamp),
135
+ }));
136
+ this.cacheAt = Date.now();
137
+ return this.cache;
138
+ }
139
+ catch (err) {
140
+ console.warn(`[nullifier-registry] ⚠️ getAllNullifiers failed: ${err.shortMessage ?? err.message}`);
141
+ return this.cache ?? [];
142
+ }
143
+ }
144
+ async getCount() {
145
+ if (!this.address)
146
+ return 0;
147
+ try {
148
+ const n = await this.contract.getNullifierCount();
149
+ return Number(n);
150
+ }
151
+ catch {
152
+ return 0;
153
+ }
154
+ }
155
+ get contractAddress() { return this.address; }
156
+ }
157
+ export const nullifierRegistryClient = new NullifierRegistryClient();
@@ -0,0 +1,45 @@
1
+ export declare const REPUTATION_REGISTRY_RPC = "https://sepolia.base.org";
2
+ export declare const REPUTATION_REGISTRY_ADDRESS: string;
3
+ export interface ScoreEntry {
4
+ did: string;
5
+ score: number;
6
+ context: string;
7
+ updatedAt: number;
8
+ }
9
+ export declare class ReputationRegistryClient {
10
+ private provider;
11
+ private contract;
12
+ private wallet?;
13
+ private cache;
14
+ private cacheAt;
15
+ private cacheTTLMs;
16
+ private address;
17
+ constructor(opts?: {
18
+ rpc?: string;
19
+ address?: string;
20
+ privateKey?: string;
21
+ cacheTTLMs?: number;
22
+ });
23
+ /**
24
+ * Set or update a reputation score for a DID.
25
+ * Only works if the wallet is the authorized validator.
26
+ * Non-blocking — logs warning on failure.
27
+ */
28
+ setScore(opts: {
29
+ did: string;
30
+ score: number;
31
+ context?: string;
32
+ }): Promise<void>;
33
+ /**
34
+ * Get reputation score for a specific DID.
35
+ */
36
+ getScore(did: string): Promise<ScoreEntry | null>;
37
+ /**
38
+ * Get all DID scores (cached 2 min).
39
+ */
40
+ getAllScores(): Promise<ScoreEntry[]>;
41
+ refreshScores(): Promise<ScoreEntry[]>;
42
+ getCount(): Promise<number>;
43
+ get contractAddress(): string;
44
+ }
45
+ export declare const reputationRegistryClient: ReputationRegistryClient;
@@ -0,0 +1,151 @@
1
+ /**
2
+ * ReputationRegistryClient
3
+ * On-chain reputation scores for DIDs (Base Sepolia).
4
+ *
5
+ * Only the soulprint.digital validator (authorizedValidator) can write scores.
6
+ * Anyone can read scores from the public ledger.
7
+ *
8
+ * Architecture (v0.5.0):
9
+ * - WRITE: only soulprint.digital validator wallet (ADMIN_PRIVATE_KEY)
10
+ * - READ: anyone, cached 2 min
11
+ */
12
+ import { ethers } from "ethers";
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
+ export const REPUTATION_REGISTRY_RPC = "https://sepolia.base.org";
18
+ function loadAddress() {
19
+ try {
20
+ const f = join(__dirname, "addresses.json");
21
+ const d = JSON.parse(readFileSync(f, "utf8"));
22
+ return d["ReputationRegistry"] ?? "";
23
+ }
24
+ catch {
25
+ return "";
26
+ }
27
+ }
28
+ export const REPUTATION_REGISTRY_ADDRESS = loadAddress();
29
+ const ABI = [
30
+ "function setScore(string did, uint256 score, string context) external",
31
+ "function getScore(string did) external view returns (string retDid, uint256 score, string context, uint256 updatedAt)",
32
+ "function getAllScores() external view returns (tuple(string did, uint256 score, string context, uint256 updatedAt)[])",
33
+ "function getScoreCount() external view returns (uint256)",
34
+ "function authorizedValidators(address) external view returns (bool)",
35
+ "event ScoreUpdated(string indexed did, uint256 score, string context, uint256 updatedAt)",
36
+ ];
37
+ export class ReputationRegistryClient {
38
+ provider;
39
+ contract;
40
+ wallet;
41
+ cache = null;
42
+ cacheAt = 0;
43
+ cacheTTLMs;
44
+ address;
45
+ constructor(opts) {
46
+ this.address = opts?.address ?? REPUTATION_REGISTRY_ADDRESS;
47
+ this.cacheTTLMs = opts?.cacheTTLMs ?? 2 * 60 * 1000; // 2 min
48
+ const rpc = opts?.rpc ?? REPUTATION_REGISTRY_RPC;
49
+ this.provider = new ethers.JsonRpcProvider(rpc);
50
+ if (!this.address) {
51
+ console.warn("[reputation-registry] ⚠️ No contract address — registry disabled");
52
+ this.contract = null;
53
+ return;
54
+ }
55
+ if (opts?.privateKey) {
56
+ this.wallet = new ethers.Wallet(opts.privateKey, this.provider);
57
+ this.contract = new ethers.Contract(this.address, ABI, this.wallet);
58
+ }
59
+ else {
60
+ this.contract = new ethers.Contract(this.address, ABI, this.provider);
61
+ }
62
+ }
63
+ /**
64
+ * Set or update a reputation score for a DID.
65
+ * Only works if the wallet is the authorized validator.
66
+ * Non-blocking — logs warning on failure.
67
+ */
68
+ async setScore(opts) {
69
+ if (!this.wallet) {
70
+ console.warn("[reputation-registry] ⚠️ No private key — cannot write score");
71
+ return;
72
+ }
73
+ if (!this.address) {
74
+ console.warn("[reputation-registry] ⚠️ No contract address — skipping");
75
+ return;
76
+ }
77
+ try {
78
+ const feeData = await this.provider.getFeeData();
79
+ const tx = await this.contract.setScore(opts.did, BigInt(Math.round(opts.score)), opts.context ?? "soulprint:v1", { gasPrice: feeData.gasPrice });
80
+ console.log(`[reputation-registry] 📡 Setting score on-chain... tx: ${tx.hash}`);
81
+ await tx.wait();
82
+ console.log(`[reputation-registry] ✅ Score set: did=${opts.did.slice(0, 20)}... score=${opts.score}`);
83
+ // Invalidate cache
84
+ this.cache = null;
85
+ }
86
+ catch (err) {
87
+ console.warn(`[reputation-registry] ⚠️ setScore failed (non-fatal): ${err.shortMessage ?? err.message}`);
88
+ }
89
+ }
90
+ /**
91
+ * Get reputation score for a specific DID.
92
+ */
93
+ async getScore(did) {
94
+ if (!this.address)
95
+ return null;
96
+ try {
97
+ const r = await this.contract.getScore(did);
98
+ return {
99
+ did: r.retDid,
100
+ score: Number(r.score),
101
+ context: r.context,
102
+ updatedAt: Number(r.updatedAt),
103
+ };
104
+ }
105
+ catch (err) {
106
+ console.warn(`[reputation-registry] ⚠️ getScore failed: ${err.shortMessage ?? err.message}`);
107
+ return null;
108
+ }
109
+ }
110
+ /**
111
+ * Get all DID scores (cached 2 min).
112
+ */
113
+ async getAllScores() {
114
+ if (this.cache && Date.now() - this.cacheAt < this.cacheTTLMs) {
115
+ return this.cache;
116
+ }
117
+ return this.refreshScores();
118
+ }
119
+ async refreshScores() {
120
+ if (!this.address)
121
+ return [];
122
+ try {
123
+ const raw = await this.contract.getAllScores();
124
+ this.cache = raw.map((e) => ({
125
+ did: e.did,
126
+ score: Number(e.score),
127
+ context: e.context,
128
+ updatedAt: Number(e.updatedAt),
129
+ }));
130
+ this.cacheAt = Date.now();
131
+ return this.cache;
132
+ }
133
+ catch (err) {
134
+ console.warn(`[reputation-registry] ⚠️ getAllScores failed: ${err.shortMessage ?? err.message}`);
135
+ return this.cache ?? [];
136
+ }
137
+ }
138
+ async getCount() {
139
+ if (!this.address)
140
+ return 0;
141
+ try {
142
+ const n = await this.contract.getScoreCount();
143
+ return Number(n);
144
+ }
145
+ catch {
146
+ return 0;
147
+ }
148
+ }
149
+ get contractAddress() { return this.address; }
150
+ }
151
+ export const reputationRegistryClient = new ReputationRegistryClient();
@@ -1,10 +1,12 @@
1
1
  {
2
- "codeHash": "39a29e119e7cb485d5b7dc5bdbe1533318617ec05a4fc4da321c94253da9d7f9",
3
- "codeHashHex": "0x39a29e119e7cb485d5b7dc5bdbe1533318617ec05a4fc4da321c94253da9d7f9",
4
- "computedAt": "2026-03-01T02:07:26.276Z",
5
- "fileCount": 22,
2
+ "codeHash": "c256c2701bea12bbb2f50552b268bda6065a58d187f7bde6d6be86d82eb73f06",
3
+ "codeHashHex": "0xc256c2701bea12bbb2f50552b268bda6065a58d187f7bde6d6be86d82eb73f06",
4
+ "computedAt": "2026-03-01T03:11:08.326Z",
5
+ "fileCount": 24,
6
6
  "files": [
7
+ "blockchain/NullifierRegistryClient.ts",
7
8
  "blockchain/PeerRegistryClient.ts",
9
+ "blockchain/ReputationRegistryClient.ts",
8
10
  "blockchain/blockchain-anchor.ts",
9
11
  "blockchain/blockchain-client.ts",
10
12
  "blockchain/protocol-thresholds-client.ts",
@@ -2,6 +2,8 @@ import { IncomingMessage, ServerResponse } from "node:http";
2
2
  import { BotAttestation, BotReputation } from "soulprint-core";
3
3
  import { type SoulprintP2PNode } from "./p2p.js";
4
4
  import { PeerRegistryClient } from "./blockchain/PeerRegistryClient.js";
5
+ import { NullifierRegistryClient } from "./blockchain/NullifierRegistryClient.js";
6
+ import { ReputationRegistryClient } from "./blockchain/ReputationRegistryClient.js";
5
7
  export declare function getLiveThresholds(): {
6
8
  SCORE_FLOOR: number;
7
9
  VERIFIED_SCORE_FLOOR: number;
@@ -14,6 +16,10 @@ export declare function getLiveThresholds(): {
14
16
  source: "blockchain" | "local_fallback";
15
17
  };
16
18
  export declare function setPeerRegistryClient(client: PeerRegistryClient): void;
19
+ export declare function setNullifierRegistry(client: NullifierRegistryClient): void;
20
+ export declare function setReputationRegistry(client: ReputationRegistryClient): void;
21
+ export declare function getNullifierRegistry(): NullifierRegistryClient | null;
22
+ export declare function getReputationRegistry(): ReputationRegistryClient | null;
17
23
  /**
18
24
  * Inyecta el nodo libp2p al validador.
19
25
  * Cuando se llama:
package/dist/validator.js CHANGED
@@ -76,6 +76,23 @@ let peerRegistryClient = null;
76
76
  export function setPeerRegistryClient(client) {
77
77
  peerRegistryClient = client;
78
78
  }
79
+ // ── On-Chain Registries (v0.5.0) ───────────────────────────────────────────────
80
+ // The blockchain IS the shared state. These are the single source of truth.
81
+ // Only soulprint.digital validator can WRITE; anyone can READ.
82
+ let nullifierRegistry = null;
83
+ let reputationRegistry = null;
84
+ export function setNullifierRegistry(client) {
85
+ nullifierRegistry = client;
86
+ }
87
+ export function setReputationRegistry(client) {
88
+ reputationRegistry = client;
89
+ }
90
+ export function getNullifierRegistry() {
91
+ return nullifierRegistry;
92
+ }
93
+ export function getReputationRegistry() {
94
+ return reputationRegistry;
95
+ }
79
96
  /**
80
97
  * Inyecta el nodo libp2p al validador.
81
98
  * Cuando se llama:
@@ -205,6 +222,14 @@ function applyAttestation(att) {
205
222
  hasDocumentVerified: hasDocument,
206
223
  };
207
224
  saveReputation();
225
+ // ── Write reputation to on-chain ReputationRegistry (v0.5.0) — non-blocking
226
+ if (reputationRegistry) {
227
+ reputationRegistry.setScore({
228
+ did: att.target_did,
229
+ score: finalRepScore,
230
+ context: "soulprint:v1",
231
+ }).catch(e => console.warn("[reputation-registry] ⚠️ On-chain write failed:", e.message));
232
+ }
208
233
  return { score: finalRepScore, attestations: allAtts.length, last_updated: rep.last_updated };
209
234
  }
210
235
  // ── P2P state sync metadata ───────────────────────────────────────────────────
@@ -692,6 +717,17 @@ async function handleVerify(req, res, nodeKeypair, ip) {
692
717
  else {
693
718
  nullifiers[zkResult.nullifier] = { did: token.did, verified_at: now };
694
719
  saveNullifiers();
720
+ // ── Write to on-chain NullifierRegistry (v0.5.0) — non-blocking ──────────
721
+ // soulprint.digital validator signs and certifies the identity on-chain.
722
+ // Anyone can now verify isRegistered(nullifier) without trusting this node.
723
+ if (nullifierRegistry) {
724
+ const score = repStore[token.did]?.score ?? 0;
725
+ nullifierRegistry.registerNullifier({
726
+ nullifier: zkResult.nullifier,
727
+ did: token.did,
728
+ score,
729
+ }).catch(e => console.warn("[nullifier-registry] ⚠️ On-chain write failed:", e.message));
730
+ }
695
731
  }
696
732
  const coSig = sign({ nullifier: zkResult.nullifier, did: token.did, timestamp: now }, nodeKeypair.privateKey);
697
733
  // Incluir reputación actual del bot en la respuesta
@@ -1050,6 +1086,8 @@ export function startValidatorNode(port = PORT) {
1050
1086
  const httpPeers = peers.length;
1051
1087
  const libp2pPeers = p2pStats?.peers ?? 0;
1052
1088
  let registeredPeers = 0;
1089
+ let nullifiersOnchain = 0;
1090
+ let reputationOnchain = 0;
1053
1091
  try {
1054
1092
  if (peerRegistryClient) {
1055
1093
  const chainPeers = await peerRegistryClient.getAllPeers();
@@ -1057,24 +1095,36 @@ export function startValidatorNode(port = PORT) {
1057
1095
  }
1058
1096
  }
1059
1097
  catch { /* non-fatal */ }
1098
+ try {
1099
+ if (nullifierRegistry)
1100
+ nullifiersOnchain = await nullifierRegistry.getCount();
1101
+ }
1102
+ catch { /* non-fatal */ }
1103
+ try {
1104
+ if (reputationRegistry)
1105
+ reputationOnchain = await reputationRegistry.getCount();
1106
+ }
1107
+ catch { /* non-fatal */ }
1060
1108
  return json(res, 200, {
1061
1109
  node_did: nodeKeypair.did.slice(0, 20) + "...",
1062
1110
  version: VERSION,
1063
1111
  protocol_hash: PROTOCOL_HASH.slice(0, 16) + "...",
1064
- // identidades y reputación
1112
+ // identidades y reputación (in-memory cache)
1065
1113
  verified_identities: Object.keys(nullifiers).length,
1066
1114
  reputation_profiles: Object.keys(repStore).length,
1067
- // peers HTTP gossip (funciona en todos los entornos)
1115
+ // on-chain state (v0.5.0) blockchain IS the shared state
1116
+ nullifiers_onchain: nullifiersOnchain,
1117
+ reputation_onchain: reputationOnchain,
1118
+ // peers — HTTP gossip
1068
1119
  known_peers: httpPeers,
1069
- // peers — libp2p P2P (requiere multicast/bootstrap; 0 en WSL2)
1120
+ // peers — libp2p P2P
1070
1121
  p2p_peers: libp2pPeers,
1071
1122
  p2p_pubsub_peers: p2pStats?.pubsubPeers ?? 0,
1072
1123
  p2p_enabled: !!p2pNode,
1073
- // total = max de ambas capas (HTTP gossip es el piso mínimo garantizado)
1074
1124
  total_peers: Math.max(httpPeers, libp2pPeers),
1075
1125
  // on-chain registered peers (PeerRegistry)
1076
1126
  registered_peers: registeredPeers,
1077
- // state sync (v0.4.4)
1127
+ // state sync (v0.5.0 — blockchain is source of truth)
1078
1128
  state_hash: computeHash(Object.keys(nullifiers)).slice(0, 16) + "...",
1079
1129
  last_sync: lastSyncTs,
1080
1130
  // estado general
@@ -1085,23 +1135,44 @@ export function startValidatorNode(port = PORT) {
1085
1135
  }
1086
1136
  if (cleanUrl === "/verify" && req.method === "POST")
1087
1137
  return handleVerify(req, res, nodeKeypair, ip);
1088
- // ── State sync endpoints (v0.4.4) ─────────────────────────────────────────
1089
- // GET /state/hash — quick hash comparison for anti-entropy
1138
+ // ── State endpoints (v0.5.0) — blockchain IS the shared state ────────────
1139
+ // GET /state/hash — hash of on-chain nullifier count + reputation count
1090
1140
  if (cleanUrl === "/state/hash" && req.method === "GET") {
1091
1141
  const currentNullifiers = Object.keys(nullifiers);
1092
- const hash = computeHash(currentNullifiers);
1142
+ let onchainNullifiers = currentNullifiers.length;
1143
+ let onchainReputation = Object.keys(repStore).length;
1144
+ try {
1145
+ if (nullifierRegistry)
1146
+ onchainNullifiers = await nullifierRegistry.getCount();
1147
+ }
1148
+ catch { }
1149
+ try {
1150
+ if (reputationRegistry)
1151
+ onchainReputation = await reputationRegistry.getCount();
1152
+ }
1153
+ catch { }
1154
+ // Hash includes on-chain counts for consensus
1155
+ const hash = computeHash([...currentNullifiers, `onchain:${onchainNullifiers}:${onchainReputation}`]);
1093
1156
  return json(res, 200, {
1094
1157
  hash,
1095
1158
  nullifier_count: currentNullifiers.length,
1159
+ nullifier_count_onchain: onchainNullifiers,
1096
1160
  reputation_count: Object.keys(repStore).length,
1161
+ reputation_count_onchain: onchainReputation,
1097
1162
  attestation_count: Object.values(repStore).reduce((n, e) => n + (e.attestations?.length ?? 0), 0),
1098
1163
  timestamp: Date.now(),
1099
1164
  });
1100
1165
  }
1101
- // GET /state/export — full state export for sync
1166
+ // GET /state/export — full state export including on-chain data
1102
1167
  if (cleanUrl === "/state/export" && req.method === "GET") {
1103
1168
  const allAttestations = Object.values(repStore).flatMap(e => e.attestations ?? []);
1169
+ // Read on-chain data (cached — won't hit RPC on every call)
1170
+ const [onchainNullifiers, onchainScores] = await Promise.all([
1171
+ nullifierRegistry ? nullifierRegistry.getAllNullifiers().catch(() => []) : Promise.resolve([]),
1172
+ reputationRegistry ? reputationRegistry.getAllScores().catch(() => []) : Promise.resolve([]),
1173
+ ]);
1104
1174
  return json(res, 200, {
1175
+ // Local in-memory state
1105
1176
  nullifiers: Object.keys(nullifiers),
1106
1177
  reputation: Object.fromEntries(Object.entries(repStore).map(([did, e]) => [did, e.score])),
1107
1178
  attestations: allAttestations,
@@ -1109,6 +1180,13 @@ export function startValidatorNode(port = PORT) {
1109
1180
  lastSync: lastSyncTs,
1110
1181
  stateHash: computeHash(Object.keys(nullifiers)),
1111
1182
  timestamp: Date.now(),
1183
+ // On-chain state (v0.5.0) — canonical source of truth
1184
+ onchain: {
1185
+ nullifiers: onchainNullifiers,
1186
+ reputation: onchainScores,
1187
+ nullifier_count: onchainNullifiers.length,
1188
+ reputation_count: onchainScores.length,
1189
+ },
1112
1190
  });
1113
1191
  }
1114
1192
  // POST /state/merge — merge partial state from a peer
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "soulprint-network",
3
- "version": "0.4.4",
4
- "description": "Soulprint validator node HTTP server that verifies ZK proofs, co-signs SPTs, anti-Sybil registry",
3
+ "version": "0.4.6",
4
+ "description": "Soulprint validator node \u2014 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",
7
7
  "bin": {
@@ -51,8 +51,8 @@
51
51
  "nodemailer": "^8.0.1",
52
52
  "otpauth": "^9.5.0",
53
53
  "otplib": "^13.3.0",
54
- "soulprint-core": "workspace:*",
55
- "soulprint-zkp": "workspace:*",
54
+ "soulprint-core": "0.1.11",
55
+ "soulprint-zkp": "0.1.6",
56
56
  "uint8arrays": "5.1.0"
57
57
  },
58
58
  "devDependencies": {