soulprint-network 0.2.3 → 0.3.0

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,118 @@
1
+ /**
2
+ * blockchain-client.ts — Cliente para interactuar con los contratos Soulprint on-chain.
3
+ *
4
+ * MODO HÍBRIDO:
5
+ * El nodo validador opera en dos modos:
6
+ *
7
+ * 1. ONLINE (blockchain activo):
8
+ * - nullifierUsed() → consulta SoulprintRegistry on-chain
9
+ * - registerIdentity() → registra nullifier + ZK proof on-chain
10
+ * - attest() → escribe attestation en AttestationLedger
11
+ * - getReputation() → lee score on-chain
12
+ *
13
+ * 2. OFFLINE (sin conexión RPC):
14
+ * - Fallback a stores locales (nullifiers.json, repStore.json)
15
+ * - Los datos se sincronizan con blockchain cuando vuelve la conexión
16
+ * - Garantiza disponibilidad incluso sin internet
17
+ *
18
+ * CONFIGURACIÓN:
19
+ * SOULPRINT_RPC_URL=https://sepolia.base.org
20
+ * SOULPRINT_PRIVATE_KEY=0x...
21
+ * SOULPRINT_REGISTRY_ADDR=0x...
22
+ * SOULPRINT_LEDGER_ADDR=0x...
23
+ */
24
+ export interface BlockchainConfig {
25
+ rpcUrl: string;
26
+ privateKey: string;
27
+ registryAddr: string;
28
+ ledgerAddr: string;
29
+ validatorRegAddr?: string;
30
+ protocolHash: string;
31
+ }
32
+ export interface OnChainReputation {
33
+ score: number;
34
+ totalPositive: number;
35
+ totalNegative: number;
36
+ lastUpdated: number;
37
+ source: "blockchain" | "local";
38
+ }
39
+ export declare function loadBlockchainConfig(): BlockchainConfig | null;
40
+ export declare class SoulprintBlockchainClient {
41
+ private config;
42
+ private provider;
43
+ private signer;
44
+ private registry;
45
+ private ledger;
46
+ private validatorReg;
47
+ private connected;
48
+ constructor(config: BlockchainConfig);
49
+ /**
50
+ * Inicializa la conexión con la blockchain.
51
+ * Lanza error si ethers no está disponible (opcional dependency).
52
+ */
53
+ connect(): Promise<boolean>;
54
+ get isConnected(): boolean;
55
+ /**
56
+ * Verifica si un nullifier ya está registrado (anti-sybil on-chain).
57
+ */
58
+ isNullifierUsed(nullifier: string): Promise<boolean | null>;
59
+ /**
60
+ * Registra una identidad on-chain con ZK proof.
61
+ * @returns txHash o null si falla
62
+ */
63
+ registerIdentity(params: {
64
+ nullifier: string;
65
+ did: string;
66
+ documentVerified: boolean;
67
+ faceVerified: boolean;
68
+ zkProof: {
69
+ a: [bigint, bigint];
70
+ b: [[bigint, bigint], [bigint, bigint]];
71
+ c: [bigint, bigint];
72
+ inputs: [bigint, bigint];
73
+ };
74
+ }): Promise<string | null>;
75
+ /**
76
+ * Retorna el identity score on-chain de un DID.
77
+ */
78
+ getIdentityScore(did: string): Promise<number | null>;
79
+ /**
80
+ * Escribe una attestation on-chain.
81
+ * @returns txHash o null si falla
82
+ */
83
+ attest(params: {
84
+ issuerDid: string;
85
+ targetDid: string;
86
+ value: 1 | -1;
87
+ context: string;
88
+ signature: string;
89
+ }): Promise<string | null>;
90
+ /**
91
+ * Obtiene la reputación on-chain de un DID.
92
+ */
93
+ getReputation(did: string): Promise<OnChainReputation | null>;
94
+ /**
95
+ * Score total on-chain (identity + reputation).
96
+ */
97
+ getTotalScore(did: string): Promise<number | null>;
98
+ /**
99
+ * Registra este nodo en el ValidatorRegistry on-chain.
100
+ */
101
+ registerNode(params: {
102
+ url: string;
103
+ did: string;
104
+ protocolHash: string;
105
+ }): Promise<string | null>;
106
+ /**
107
+ * Envía heartbeat on-chain.
108
+ */
109
+ heartbeat(did: string, totalVerified: number): Promise<void>;
110
+ /**
111
+ * Lista nodos activos on-chain (para peer discovery).
112
+ */
113
+ getActiveNodes(): Promise<Array<{
114
+ url: string;
115
+ did: string;
116
+ compatible: boolean;
117
+ }>>;
118
+ }
@@ -0,0 +1,280 @@
1
+ /**
2
+ * blockchain-client.ts — Cliente para interactuar con los contratos Soulprint on-chain.
3
+ *
4
+ * MODO HÍBRIDO:
5
+ * El nodo validador opera en dos modos:
6
+ *
7
+ * 1. ONLINE (blockchain activo):
8
+ * - nullifierUsed() → consulta SoulprintRegistry on-chain
9
+ * - registerIdentity() → registra nullifier + ZK proof on-chain
10
+ * - attest() → escribe attestation en AttestationLedger
11
+ * - getReputation() → lee score on-chain
12
+ *
13
+ * 2. OFFLINE (sin conexión RPC):
14
+ * - Fallback a stores locales (nullifiers.json, repStore.json)
15
+ * - Los datos se sincronizan con blockchain cuando vuelve la conexión
16
+ * - Garantiza disponibilidad incluso sin internet
17
+ *
18
+ * CONFIGURACIÓN:
19
+ * SOULPRINT_RPC_URL=https://sepolia.base.org
20
+ * SOULPRINT_PRIVATE_KEY=0x...
21
+ * SOULPRINT_REGISTRY_ADDR=0x...
22
+ * SOULPRINT_LEDGER_ADDR=0x...
23
+ */
24
+ import { existsSync, readFileSync } from "node:fs";
25
+ import { join } from "node:path";
26
+ // ── ABI mínimos para interactuar con los contratos ────────────────────────────
27
+ // Solo las funciones que usa el nodo validador
28
+ const REGISTRY_ABI = [
29
+ "function isRegistered(bytes32 nullifier) view returns (bool)",
30
+ "function identityScore(string did) view returns (uint8)",
31
+ "function registerIdentity(bytes32 nullifier, string did, bool docVerified, bool faceVerified, uint256[2] a, uint256[2][2] b, uint256[2] c, uint256[2] inputs) external",
32
+ "event IdentityRegistered(bytes32 indexed nullifier, string indexed did, uint8 identityScore, uint64 timestamp)",
33
+ "function PROTOCOL_HASH() view returns (bytes32)",
34
+ ];
35
+ const LEDGER_ABI = [
36
+ "function attest(string issuerDid, string targetDid, int8 value, string context, bytes signature) external",
37
+ "function getTotalScore(string did) view returns (uint16)",
38
+ "function getReputation(string did) view returns (tuple(int16 score, uint16 totalPositive, uint16 totalNegative, uint64 lastUpdated))",
39
+ "function getAttestations(string did) view returns (tuple(string issuerDid, string targetDid, int8 value, string context, uint64 timestamp, bytes signature)[])",
40
+ "function canAttest(string issuerDid, string targetDid) view returns (bool allowed, uint64 nextTs)",
41
+ "event AttestationRecorded(string indexed targetDid, string issuerDid, int8 value, string context, uint64 timestamp)",
42
+ ];
43
+ const VALIDATOR_REG_ABI = [
44
+ "function registerNode(string url, string did, bytes32 protocolHash) external",
45
+ "function heartbeat(string did, uint32 totalVerified) external",
46
+ "function getActiveNodes() view returns (tuple(string url, string did, bytes32 protocolHash, uint64 registeredAt, uint64 lastSeen, uint32 totalVerified, bool active, bool compatible)[])",
47
+ "function PROTOCOL_HASH() view returns (bytes32)",
48
+ ];
49
+ // ── Load config from env + deployments ───────────────────────────────────────
50
+ export function loadBlockchainConfig() {
51
+ const rpcUrl = process.env.SOULPRINT_RPC_URL;
52
+ const privateKey = process.env.SOULPRINT_PRIVATE_KEY;
53
+ const network = process.env.SOULPRINT_NETWORK ?? "base-sepolia";
54
+ if (!rpcUrl || !privateKey)
55
+ return null;
56
+ // Buscar direcciones en deployments/
57
+ const deploymentsDir = join(__dirname, "..", "..", "blockchain", "deployments");
58
+ const deployFile = join(deploymentsDir, `${network}.json`);
59
+ if (!existsSync(deployFile)) {
60
+ console.warn(`[blockchain] No deployment found for ${network}. Run: npx hardhat run scripts/deploy.ts --network ${network}`);
61
+ return null;
62
+ }
63
+ const deployment = JSON.parse(readFileSync(deployFile, "utf8"));
64
+ return {
65
+ rpcUrl,
66
+ privateKey,
67
+ registryAddr: deployment.contracts.SoulprintRegistry,
68
+ ledgerAddr: deployment.contracts.AttestationLedger,
69
+ validatorRegAddr: deployment.contracts.ValidatorRegistry,
70
+ protocolHash: deployment.protocolHash,
71
+ };
72
+ }
73
+ // ── Blockchain Client ─────────────────────────────────────────────────────────
74
+ export class SoulprintBlockchainClient {
75
+ config;
76
+ provider = null;
77
+ signer = null;
78
+ registry = null;
79
+ ledger = null;
80
+ validatorReg = null;
81
+ connected = false;
82
+ constructor(config) {
83
+ this.config = config;
84
+ }
85
+ /**
86
+ * Inicializa la conexión con la blockchain.
87
+ * Lanza error si ethers no está disponible (opcional dependency).
88
+ */
89
+ async connect() {
90
+ try {
91
+ // @ts-ignore — ethers is an optional peer dependency
92
+ const ethersModule = await import("ethers").catch(() => null);
93
+ if (!ethersModule)
94
+ return false;
95
+ const ethers = ethersModule.ethers;
96
+ this.provider = new ethers.JsonRpcProvider(this.config.rpcUrl);
97
+ this.signer = new ethers.Wallet(this.config.privateKey, this.provider);
98
+ this.registry = new ethers.Contract(this.config.registryAddr, REGISTRY_ABI, this.signer);
99
+ this.ledger = new ethers.Contract(this.config.ledgerAddr, LEDGER_ABI, this.signer);
100
+ if (this.config.validatorRegAddr) {
101
+ this.validatorReg = new ethers.Contract(this.config.validatorRegAddr, VALIDATOR_REG_ABI, this.signer);
102
+ }
103
+ // Verificar que el contrato tiene el mismo PROTOCOL_HASH
104
+ const onChainHash = await this.registry.PROTOCOL_HASH();
105
+ if (onChainHash.toLowerCase() !== this.config.protocolHash.toLowerCase()) {
106
+ console.error(`[blockchain] ❌ PROTOCOL_HASH mismatch!`);
107
+ console.error(` On-chain: ${onChainHash}`);
108
+ console.error(` Expected: ${this.config.protocolHash}`);
109
+ return false;
110
+ }
111
+ this.connected = true;
112
+ const network = await this.provider.getNetwork();
113
+ console.log(`[blockchain] ✅ Connected to chain ${network.chainId} (${network.name})`);
114
+ console.log(`[blockchain] Registry: ${this.config.registryAddr}`);
115
+ console.log(`[blockchain] Ledger: ${this.config.ledgerAddr}`);
116
+ return true;
117
+ }
118
+ catch (err) {
119
+ console.warn(`[blockchain] Offline mode — ${err.message?.slice(0, 60)}`);
120
+ return false;
121
+ }
122
+ }
123
+ get isConnected() { return this.connected; }
124
+ // ── Registry ───────────────────────────────────────────────────────────────
125
+ /**
126
+ * Verifica si un nullifier ya está registrado (anti-sybil on-chain).
127
+ */
128
+ async isNullifierUsed(nullifier) {
129
+ if (!this.connected)
130
+ return null;
131
+ try {
132
+ return await this.registry.isRegistered(nullifier);
133
+ }
134
+ catch {
135
+ return null;
136
+ }
137
+ }
138
+ /**
139
+ * Registra una identidad on-chain con ZK proof.
140
+ * @returns txHash o null si falla
141
+ */
142
+ async registerIdentity(params) {
143
+ if (!this.connected)
144
+ return null;
145
+ try {
146
+ const tx = await this.registry.registerIdentity(params.nullifier, params.did, params.documentVerified, params.faceVerified, params.zkProof.a, params.zkProof.b, params.zkProof.c, params.zkProof.inputs);
147
+ const receipt = await tx.wait();
148
+ console.log(`[blockchain] ✅ Identity registered | tx: ${receipt.hash}`);
149
+ return receipt.hash;
150
+ }
151
+ catch (err) {
152
+ console.error(`[blockchain] registerIdentity failed: ${err.message?.slice(0, 80)}`);
153
+ return null;
154
+ }
155
+ }
156
+ /**
157
+ * Retorna el identity score on-chain de un DID.
158
+ */
159
+ async getIdentityScore(did) {
160
+ if (!this.connected)
161
+ return null;
162
+ try {
163
+ return Number(await this.registry.identityScore(did));
164
+ }
165
+ catch {
166
+ return null;
167
+ }
168
+ }
169
+ // ── Attestation Ledger ─────────────────────────────────────────────────────
170
+ /**
171
+ * Escribe una attestation on-chain.
172
+ * @returns txHash o null si falla
173
+ */
174
+ async attest(params) {
175
+ if (!this.connected)
176
+ return null;
177
+ try {
178
+ const tx = await this.ledger.attest(params.issuerDid, params.targetDid, params.value, params.context, params.signature);
179
+ const receipt = await tx.wait();
180
+ console.log(`[blockchain] ✅ Attestation recorded | tx: ${receipt.hash}`);
181
+ return receipt.hash;
182
+ }
183
+ catch (err) {
184
+ // Manejar CooldownActive gracefully
185
+ if (err.message?.includes("CooldownActive")) {
186
+ console.warn(`[blockchain] Attestation cooldown active for ${params.issuerDid} → ${params.targetDid}`);
187
+ }
188
+ else {
189
+ console.error(`[blockchain] attest failed: ${err.message?.slice(0, 80)}`);
190
+ }
191
+ return null;
192
+ }
193
+ }
194
+ /**
195
+ * Obtiene la reputación on-chain de un DID.
196
+ */
197
+ async getReputation(did) {
198
+ if (!this.connected)
199
+ return null;
200
+ try {
201
+ const rep = await this.ledger.getReputation(did);
202
+ return {
203
+ score: Number(rep.score),
204
+ totalPositive: Number(rep.totalPositive),
205
+ totalNegative: Number(rep.totalNegative),
206
+ lastUpdated: Number(rep.lastUpdated),
207
+ source: "blockchain",
208
+ };
209
+ }
210
+ catch {
211
+ return null;
212
+ }
213
+ }
214
+ /**
215
+ * Score total on-chain (identity + reputation).
216
+ */
217
+ async getTotalScore(did) {
218
+ if (!this.connected)
219
+ return null;
220
+ try {
221
+ return Number(await this.ledger.getTotalScore(did));
222
+ }
223
+ catch {
224
+ return null;
225
+ }
226
+ }
227
+ // ── Validator Registry ─────────────────────────────────────────────────────
228
+ /**
229
+ * Registra este nodo en el ValidatorRegistry on-chain.
230
+ */
231
+ async registerNode(params) {
232
+ if (!this.connected || !this.validatorReg)
233
+ return null;
234
+ try {
235
+ const tx = await this.validatorReg.registerNode(params.url, params.did, params.protocolHash);
236
+ const receipt = await tx.wait();
237
+ console.log(`[blockchain] ✅ Node registered | tx: ${receipt.hash}`);
238
+ return receipt.hash;
239
+ }
240
+ catch (err) {
241
+ if (err.message?.includes("AlreadyRegistered")) {
242
+ console.log(`[blockchain] Node already registered`);
243
+ }
244
+ else {
245
+ console.error(`[blockchain] registerNode failed: ${err.message?.slice(0, 80)}`);
246
+ }
247
+ return null;
248
+ }
249
+ }
250
+ /**
251
+ * Envía heartbeat on-chain.
252
+ */
253
+ async heartbeat(did, totalVerified) {
254
+ if (!this.connected || !this.validatorReg)
255
+ return;
256
+ try {
257
+ const tx = await this.validatorReg.heartbeat(did, totalVerified);
258
+ await tx.wait();
259
+ }
260
+ catch { /* non-critical */ }
261
+ }
262
+ /**
263
+ * Lista nodos activos on-chain (para peer discovery).
264
+ */
265
+ async getActiveNodes() {
266
+ if (!this.connected || !this.validatorReg)
267
+ return [];
268
+ try {
269
+ const nodes = await this.validatorReg.getActiveNodes();
270
+ return nodes.map((n) => ({
271
+ url: n.url,
272
+ did: n.did,
273
+ compatible: n.compatible,
274
+ }));
275
+ }
276
+ catch {
277
+ return [];
278
+ }
279
+ }
280
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * attestation-consensus.ts — Attestations firmadas + propagadas por P2P.
3
+ *
4
+ * DISEÑO (sin EVM):
5
+ * ─────────────────────────────────────────────────────────────────────────────
6
+ * Las attestations NO necesitan consenso multi-ronda porque:
7
+ * 1. Están firmadas con Ed25519 (no repudiables)
8
+ * 2. El issuer es verificable (score >= MIN_ATTESTER_SCORE)
9
+ * 3. El anti-farming es local + determinista
10
+ *
11
+ * FLUJO:
12
+ * Issuer firma attest → broadcast a red → cada nodo valida firma + cooldown
13
+ * → si válido: guarda en store + propaga → estado eventualmente consistente
14
+ *
15
+ * ANTI-FARMING ON-CHAIN (sin EVM):
16
+ * • Map<issuer:target → lastTs> con cooldown de 24h
17
+ * • Anti-farming de soulprint-core (FARMING_RULES) para patrones robóticos
18
+ * • FARMING_RULES Object.freeze → constantes inmutables
19
+ *
20
+ * CONSISTENCIA:
21
+ * • Eventual: todos los nodos convergen al mismo estado en segundos
22
+ * • Cada attest lleva firma + timestamp → reordenable en merge
23
+ * • Sin conflictos: misma firma = misma attestation (idempotente)
24
+ */
25
+ import { EventEmitter } from "node:events";
26
+ export interface AttestationMsg {
27
+ type: "ATTEST";
28
+ issuerDid: string;
29
+ targetDid: string;
30
+ value: 1 | -1;
31
+ context: string;
32
+ ts: number;
33
+ protocolHash: string;
34
+ sig: string;
35
+ }
36
+ export interface AttestEntry {
37
+ issuerDid: string;
38
+ targetDid: string;
39
+ value: 1 | -1;
40
+ context: string;
41
+ ts: number;
42
+ sig: string;
43
+ msgHash: string;
44
+ }
45
+ export interface Reputation {
46
+ score: number;
47
+ totalPositive: number;
48
+ totalNegative: number;
49
+ lastUpdated: number;
50
+ }
51
+ export interface AttestationConsensusOptions {
52
+ selfDid: string;
53
+ sign: (data: string) => Promise<string>;
54
+ verify: (data: string, sig: string, did: string) => Promise<boolean>;
55
+ broadcast: (msg: AttestationMsg) => Promise<void>;
56
+ getScore: (did: string) => number;
57
+ storePath: string;
58
+ repStorePath: string;
59
+ }
60
+ export declare class AttestationConsensus extends EventEmitter {
61
+ private opts;
62
+ private history;
63
+ private reps;
64
+ private cooldowns;
65
+ private seen;
66
+ constructor(opts: AttestationConsensusOptions);
67
+ /**
68
+ * Emite una attestation desde este nodo.
69
+ * Firma, guarda localmente, y hace broadcast.
70
+ */
71
+ attest(params: {
72
+ issuerDid: string;
73
+ targetDid: string;
74
+ value: 1 | -1;
75
+ context: string;
76
+ }): Promise<AttestEntry>;
77
+ /**
78
+ * Procesa un mensaje ATTEST recibido de la red.
79
+ */
80
+ handleMessage(msg: AttestationMsg): Promise<void>;
81
+ /**
82
+ * Retorna la reputación de un DID.
83
+ */
84
+ getReputation(targetDid: string): Reputation;
85
+ /**
86
+ * Retorna el historial de attestations de un DID.
87
+ */
88
+ getHistory(targetDid: string): AttestEntry[];
89
+ /**
90
+ * Verifica si un par puede atestar ahora (no en cooldown).
91
+ */
92
+ canAttest(issuerDid: string, targetDid: string): {
93
+ allowed: boolean;
94
+ nextTs?: number;
95
+ };
96
+ /**
97
+ * Exporta estado para sync con nuevos nodos.
98
+ */
99
+ exportState(): {
100
+ history: Record<string, AttestEntry[]>;
101
+ reps: Record<string, Reputation>;
102
+ };
103
+ /**
104
+ * Importa estado de otro nodo al arrancar.
105
+ */
106
+ importState(state: {
107
+ history: Record<string, AttestEntry[]>;
108
+ reps: Record<string, Reputation>;
109
+ }): number;
110
+ private applyAttest;
111
+ private hashMsg;
112
+ private loadStores;
113
+ private saveStores;
114
+ }