soulprint-network 0.3.2 → 0.3.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.
@@ -21,12 +21,35 @@
21
21
  * SOULPRINT_REGISTRY_ADDR=0x...
22
22
  * SOULPRINT_LEDGER_ADDR=0x...
23
23
  */
24
+ export declare const ProposalState: {
25
+ readonly ACTIVE: 0;
26
+ readonly APPROVED: 1;
27
+ readonly EXECUTED: 2;
28
+ readonly REJECTED: 3;
29
+ readonly EXPIRED: 4;
30
+ };
31
+ export type ProposalStateType = typeof ProposalState[keyof typeof ProposalState];
32
+ export interface GovernanceProposal {
33
+ id: number;
34
+ newHash: string;
35
+ rationale: string;
36
+ proposerDid: string;
37
+ proposerAddr: string;
38
+ createdAt: number;
39
+ approvedAt: number;
40
+ executedAt: number;
41
+ votesFor: number;
42
+ votesAgainst: number;
43
+ state: ProposalStateType;
44
+ stateName: string;
45
+ }
24
46
  export interface BlockchainConfig {
25
47
  rpcUrl: string;
26
48
  privateKey: string;
27
49
  registryAddr: string;
28
50
  ledgerAddr: string;
29
51
  validatorRegAddr?: string;
52
+ governanceAddr?: string;
30
53
  protocolHash: string;
31
54
  }
32
55
  export interface OnChainReputation {
@@ -44,6 +67,7 @@ export declare class SoulprintBlockchainClient {
44
67
  private registry;
45
68
  private ledger;
46
69
  private validatorReg;
70
+ private governance;
47
71
  private connected;
48
72
  constructor(config: BlockchainConfig);
49
73
  /**
@@ -115,4 +139,56 @@ export declare class SoulprintBlockchainClient {
115
139
  did: string;
116
140
  compatible: boolean;
117
141
  }>>;
142
+ /**
143
+ * Retorna el hash actualmente aprobado por governance.
144
+ * Los nodos deben comparar esto con su PROTOCOL_HASH al arrancar.
145
+ */
146
+ getCurrentApprovedHash(): Promise<string | null>;
147
+ /**
148
+ * Verifica si el hash actual del nodo está aprobado por governance.
149
+ */
150
+ isHashApproved(hash: string): Promise<boolean>;
151
+ /**
152
+ * Propone un upgrade del PROTOCOL_HASH.
153
+ * Solo validadores con identidad verificada pueden proponer.
154
+ *
155
+ * @returns txHash + proposalId, o null si falla
156
+ */
157
+ proposeUpgrade(params: {
158
+ did: string;
159
+ newHash: string;
160
+ rationale: string;
161
+ }): Promise<{
162
+ txHash: string;
163
+ proposalId: number;
164
+ } | null>;
165
+ /**
166
+ * Vota en una propuesta de governance.
167
+ * @param approve true = a favor | false = en contra / veto
168
+ */
169
+ voteOnProposal(params: {
170
+ proposalId: number;
171
+ did: string;
172
+ approve: boolean;
173
+ }): Promise<string | null>;
174
+ /**
175
+ * Ejecuta una propuesta aprobada una vez que el timelock expiró.
176
+ */
177
+ executeProposal(proposalId: number): Promise<string | null>;
178
+ /**
179
+ * Retorna estado completo de una propuesta.
180
+ */
181
+ getProposal(proposalId: number): Promise<GovernanceProposal | null>;
182
+ /**
183
+ * Lista todas las propuestas activas o en timelock.
184
+ */
185
+ getActiveProposals(): Promise<GovernanceProposal[]>;
186
+ /**
187
+ * Historial de todos los hashes aprobados (auditoría).
188
+ */
189
+ getHashHistory(): Promise<string[]>;
190
+ /**
191
+ * Segundos restantes del timelock de una propuesta aprobada.
192
+ */
193
+ getTimelockRemaining(proposalId: number): Promise<number>;
118
194
  }
@@ -22,7 +22,10 @@
22
22
  * SOULPRINT_LEDGER_ADDR=0x...
23
23
  */
24
24
  import { existsSync, readFileSync } from "node:fs";
25
- import { join } from "node:path";
25
+ import { join, dirname } from "node:path";
26
+ import { fileURLToPath } from "node:url";
27
+ // ESM-compatible __dirname
28
+ const __dir = dirname(fileURLToPath(import.meta.url));
26
29
  // ── ABI mínimos para interactuar con los contratos ────────────────────────────
27
30
  // Solo las funciones que usa el nodo validador
28
31
  const REGISTRY_ABI = [
@@ -45,7 +48,43 @@ const VALIDATOR_REG_ABI = [
45
48
  "function heartbeat(string did, uint32 totalVerified) external",
46
49
  "function getActiveNodes() view returns (tuple(string url, string did, bytes32 protocolHash, uint64 registeredAt, uint64 lastSeen, uint32 totalVerified, bool active, bool compatible)[])",
47
50
  "function PROTOCOL_HASH() view returns (bytes32)",
51
+ "function compatibleNodes() view returns (uint256)",
48
52
  ];
53
+ const GOVERNANCE_ABI = [
54
+ // State
55
+ "function currentApprovedHash() view returns (bytes32)",
56
+ "function totalProposals() view returns (uint256)",
57
+ "function APPROVAL_THRESHOLD_BPS() view returns (uint256)",
58
+ "function VETO_THRESHOLD_BPS() view returns (uint256)",
59
+ "function TIMELOCK_DELAY() view returns (uint64)",
60
+ "function MINIMUM_QUORUM() view returns (uint32)",
61
+ // Actions
62
+ "function proposeUpgrade(string did, bytes32 newHash, string rationale) external returns (uint256)",
63
+ "function voteOnProposal(uint256 proposalId, string did, bool approve) external",
64
+ "function executeProposal(uint256 proposalId) external",
65
+ // Views
66
+ "function getProposal(uint256 id) view returns (tuple(uint256 id, bytes32 newHash, string rationale, string proposerDid, address proposerAddr, uint64 createdAt, uint64 approvedAt, uint64 executedAt, uint32 votesFor, uint32 votesAgainst, uint8 state))",
67
+ "function getActiveProposals() view returns (tuple(uint256 id, bytes32 newHash, string rationale, string proposerDid, address proposerAddr, uint64 createdAt, uint64 approvedAt, uint64 executedAt, uint32 votesFor, uint32 votesAgainst, uint8 state)[])",
68
+ "function getHashHistory() view returns (bytes32[])",
69
+ "function isCurrentHashValid(bytes32 hash) view returns (bool)",
70
+ "function getApprovalPercentage(uint256 id) view returns (uint256 forPct, uint256 againstPct, uint32 activeNodes)",
71
+ "function timelockRemaining(uint256 id) view returns (uint64)",
72
+ // Events
73
+ "event ProposalCreated(uint256 indexed id, bytes32 newHash, string proposerDid, string rationale, uint64 expiresAt)",
74
+ "event VoteCast(uint256 indexed proposalId, string voterDid, bool approve, uint32 totalFor, uint32 totalAgainst)",
75
+ "event ProposalApproved(uint256 indexed id, bytes32 newHash, uint64 executeAfter)",
76
+ "event ProposalExecuted(uint256 indexed id, bytes32 oldHash, bytes32 newHash, uint64 timestamp)",
77
+ "event ProposalRejected(uint256 indexed id, string reason)",
78
+ "event EmergencyVeto(uint256 indexed id, uint32 vetoes, uint32 totalVoters)",
79
+ ];
80
+ // ── ProposalState enum (mirror de Solidity) ───────────────────────────────────
81
+ export const ProposalState = {
82
+ ACTIVE: 0,
83
+ APPROVED: 1,
84
+ EXECUTED: 2,
85
+ REJECTED: 3,
86
+ EXPIRED: 4,
87
+ };
49
88
  // ── Load config from env + deployments ───────────────────────────────────────
50
89
  export function loadBlockchainConfig() {
51
90
  const rpcUrl = process.env.SOULPRINT_RPC_URL;
@@ -54,7 +93,7 @@ export function loadBlockchainConfig() {
54
93
  if (!rpcUrl || !privateKey)
55
94
  return null;
56
95
  // Buscar direcciones en deployments/
57
- const deploymentsDir = join(__dirname, "..", "..", "blockchain", "deployments");
96
+ const deploymentsDir = join(__dir, "..", "..", "..", "blockchain", "deployments");
58
97
  const deployFile = join(deploymentsDir, `${network}.json`);
59
98
  if (!existsSync(deployFile)) {
60
99
  console.warn(`[blockchain] No deployment found for ${network}. Run: npx hardhat run scripts/deploy.ts --network ${network}`);
@@ -67,6 +106,7 @@ export function loadBlockchainConfig() {
67
106
  registryAddr: deployment.contracts.SoulprintRegistry,
68
107
  ledgerAddr: deployment.contracts.AttestationLedger,
69
108
  validatorRegAddr: deployment.contracts.ValidatorRegistry,
109
+ governanceAddr: deployment.contracts.GovernanceModule,
70
110
  protocolHash: deployment.protocolHash,
71
111
  };
72
112
  }
@@ -78,6 +118,7 @@ export class SoulprintBlockchainClient {
78
118
  registry = null;
79
119
  ledger = null;
80
120
  validatorReg = null;
121
+ governance = null;
81
122
  connected = false;
82
123
  constructor(config) {
83
124
  this.config = config;
@@ -100,6 +141,12 @@ export class SoulprintBlockchainClient {
100
141
  if (this.config.validatorRegAddr) {
101
142
  this.validatorReg = new ethers.Contract(this.config.validatorRegAddr, VALIDATOR_REG_ABI, this.signer);
102
143
  }
144
+ if (this.config.governanceAddr) {
145
+ this.governance = new ethers.Contract(this.config.governanceAddr, GOVERNANCE_ABI, this.signer);
146
+ const govHash = await this.governance.currentApprovedHash();
147
+ console.log(`[blockchain] Governance: ${this.config.governanceAddr}`);
148
+ console.log(`[blockchain] Current approved hash: ${govHash.slice(0, 10)}...`);
149
+ }
103
150
  // Verificar que el contrato tiene el mismo PROTOCOL_HASH
104
151
  const onChainHash = await this.registry.PROTOCOL_HASH();
105
152
  if (onChainHash.toLowerCase() !== this.config.protocolHash.toLowerCase()) {
@@ -277,4 +324,177 @@ export class SoulprintBlockchainClient {
277
324
  return [];
278
325
  }
279
326
  }
327
+ // ── Governance ─────────────────────────────────────────────────────────────
328
+ /**
329
+ * Retorna el hash actualmente aprobado por governance.
330
+ * Los nodos deben comparar esto con su PROTOCOL_HASH al arrancar.
331
+ */
332
+ async getCurrentApprovedHash() {
333
+ if (!this.connected || !this.governance)
334
+ return null;
335
+ try {
336
+ return await this.governance.currentApprovedHash();
337
+ }
338
+ catch {
339
+ return null;
340
+ }
341
+ }
342
+ /**
343
+ * Verifica si el hash actual del nodo está aprobado por governance.
344
+ */
345
+ async isHashApproved(hash) {
346
+ if (!this.connected || !this.governance)
347
+ return true; // fallback: asumir OK
348
+ try {
349
+ return await this.governance.isCurrentHashValid(hash);
350
+ }
351
+ catch {
352
+ return true;
353
+ }
354
+ }
355
+ /**
356
+ * Propone un upgrade del PROTOCOL_HASH.
357
+ * Solo validadores con identidad verificada pueden proponer.
358
+ *
359
+ * @returns txHash + proposalId, o null si falla
360
+ */
361
+ async proposeUpgrade(params) {
362
+ if (!this.connected || !this.governance)
363
+ return null;
364
+ try {
365
+ const tx = await this.governance.proposeUpgrade(params.did, params.newHash, params.rationale);
366
+ const receipt = await tx.wait();
367
+ // Extraer proposalId del evento ProposalCreated
368
+ const log = receipt.logs?.find((l) => l.fragment?.name === "ProposalCreated");
369
+ const proposalId = log ? Number(log.args[0]) : -1;
370
+ console.log(`[governance] ✅ Proposal #${proposalId} created | tx: ${receipt.hash}`);
371
+ return { txHash: receipt.hash, proposalId };
372
+ }
373
+ catch (err) {
374
+ console.error(`[governance] proposeUpgrade failed: ${err.message?.slice(0, 100)}`);
375
+ return null;
376
+ }
377
+ }
378
+ /**
379
+ * Vota en una propuesta de governance.
380
+ * @param approve true = a favor | false = en contra / veto
381
+ */
382
+ async voteOnProposal(params) {
383
+ if (!this.connected || !this.governance)
384
+ return null;
385
+ try {
386
+ const tx = await this.governance.voteOnProposal(params.proposalId, params.did, params.approve);
387
+ const receipt = await tx.wait();
388
+ const action = params.approve ? "✅ FOR" : "🚫 AGAINST";
389
+ console.log(`[governance] ${action} Proposal #${params.proposalId} | tx: ${receipt.hash}`);
390
+ return receipt.hash;
391
+ }
392
+ catch (err) {
393
+ console.error(`[governance] voteOnProposal failed: ${err.message?.slice(0, 100)}`);
394
+ return null;
395
+ }
396
+ }
397
+ /**
398
+ * Ejecuta una propuesta aprobada una vez que el timelock expiró.
399
+ */
400
+ async executeProposal(proposalId) {
401
+ if (!this.connected || !this.governance)
402
+ return null;
403
+ try {
404
+ const tx = await this.governance.executeProposal(proposalId);
405
+ const receipt = await tx.wait();
406
+ console.log(`[governance] ✅ Proposal #${proposalId} EXECUTED | tx: ${receipt.hash}`);
407
+ return receipt.hash;
408
+ }
409
+ catch (err) {
410
+ console.error(`[governance] executeProposal failed: ${err.message?.slice(0, 100)}`);
411
+ return null;
412
+ }
413
+ }
414
+ /**
415
+ * Retorna estado completo de una propuesta.
416
+ */
417
+ async getProposal(proposalId) {
418
+ if (!this.connected || !this.governance)
419
+ return null;
420
+ try {
421
+ // Verificar que la propuesta existe (Solidity retorna zero-struct si no existe)
422
+ const total = Number(await this.governance.totalProposals());
423
+ if (proposalId >= total)
424
+ return null;
425
+ const STATE_NAMES = ["ACTIVE", "APPROVED", "EXECUTED", "REJECTED", "EXPIRED"];
426
+ const p = await this.governance.getProposal(proposalId);
427
+ return {
428
+ id: Number(p.id),
429
+ newHash: p.newHash,
430
+ rationale: p.rationale,
431
+ proposerDid: p.proposerDid,
432
+ proposerAddr: p.proposerAddr,
433
+ createdAt: Number(p.createdAt),
434
+ approvedAt: Number(p.approvedAt),
435
+ executedAt: Number(p.executedAt),
436
+ votesFor: Number(p.votesFor),
437
+ votesAgainst: Number(p.votesAgainst),
438
+ state: Number(p.state),
439
+ stateName: STATE_NAMES[Number(p.state)] ?? "UNKNOWN",
440
+ };
441
+ }
442
+ catch {
443
+ return null;
444
+ }
445
+ }
446
+ /**
447
+ * Lista todas las propuestas activas o en timelock.
448
+ */
449
+ async getActiveProposals() {
450
+ if (!this.connected || !this.governance)
451
+ return [];
452
+ try {
453
+ const STATE_NAMES = ["ACTIVE", "APPROVED", "EXECUTED", "REJECTED", "EXPIRED"];
454
+ const proposals = await this.governance.getActiveProposals();
455
+ return proposals.map((p) => ({
456
+ id: Number(p.id),
457
+ newHash: p.newHash,
458
+ rationale: p.rationale,
459
+ proposerDid: p.proposerDid,
460
+ proposerAddr: p.proposerAddr,
461
+ createdAt: Number(p.createdAt),
462
+ approvedAt: Number(p.approvedAt),
463
+ executedAt: Number(p.executedAt),
464
+ votesFor: Number(p.votesFor),
465
+ votesAgainst: Number(p.votesAgainst),
466
+ state: Number(p.state),
467
+ stateName: STATE_NAMES[Number(p.state)] ?? "UNKNOWN",
468
+ }));
469
+ }
470
+ catch {
471
+ return [];
472
+ }
473
+ }
474
+ /**
475
+ * Historial de todos los hashes aprobados (auditoría).
476
+ */
477
+ async getHashHistory() {
478
+ if (!this.connected || !this.governance)
479
+ return [];
480
+ try {
481
+ return await this.governance.getHashHistory();
482
+ }
483
+ catch {
484
+ return [];
485
+ }
486
+ }
487
+ /**
488
+ * Segundos restantes del timelock de una propuesta aprobada.
489
+ */
490
+ async getTimelockRemaining(proposalId) {
491
+ if (!this.connected || !this.governance)
492
+ return 0;
493
+ try {
494
+ return Number(await this.governance.timelockRemaining(proposalId));
495
+ }
496
+ catch {
497
+ return 0;
498
+ }
499
+ }
280
500
  }
@@ -0,0 +1,25 @@
1
+ {
2
+ "codeHash": "35b73961e9b592ba5c735379ce44c5ef8dda1db97da632afcb275da4e571a065",
3
+ "codeHashHex": "0x35b73961e9b592ba5c735379ce44c5ef8dda1db97da632afcb275da4e571a065",
4
+ "computedAt": "2026-02-24T20:55:30.860Z",
5
+ "fileCount": 17,
6
+ "files": [
7
+ "blockchain/blockchain-anchor.ts",
8
+ "blockchain/blockchain-client.ts",
9
+ "code-integrity.ts",
10
+ "consensus/attestation-consensus.ts",
11
+ "consensus/index.ts",
12
+ "consensus/nullifier-consensus.ts",
13
+ "consensus/state-sync.ts",
14
+ "credentials/email.ts",
15
+ "credentials/github.ts",
16
+ "credentials/index.ts",
17
+ "credentials/phone.ts",
18
+ "crypto/gossip-cipher.ts",
19
+ "crypto/peer-router.ts",
20
+ "index.ts",
21
+ "p2p.ts",
22
+ "server.ts",
23
+ "validator.ts"
24
+ ]
25
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * code-integrity.ts — Fix 2: Runtime code integrity verification
3
+ *
4
+ * Carga el hash computado en build-time y lo expone para:
5
+ * 1. Logging al arranque del nodo
6
+ * 2. GET /health (peers pueden verificar)
7
+ * 3. Comparación con hashes aprobados on-chain por governance
8
+ *
9
+ * GARANTÍA:
10
+ * - Si alguien modifica src/ y recompila → el hash cambia
11
+ * - Los peers pueden detectar que un nodo corre código diferente
12
+ * - El GovernanceModule puede registrar hashes aprobados on-chain
13
+ * - Si el hash no coincide con el aprobado → el nodo queda marcado como no confiable
14
+ *
15
+ * LIMITACIÓN (sin TEE):
16
+ * - Un atacante con root puede modificar dist/ directamente o falsificar el hash
17
+ * - La protección completa requiere TEE (Intel SGX / AMD SEV) — fase futura
18
+ */
19
+ export interface CodeIntegrityInfo {
20
+ codeHash: string;
21
+ codeHashHex: string;
22
+ computedAt: string;
23
+ fileCount: number;
24
+ available: boolean;
25
+ }
26
+ /**
27
+ * Carga el code-hash.json generado en build time.
28
+ */
29
+ export declare function getCodeIntegrity(): CodeIntegrityInfo;
30
+ /**
31
+ * Verifica que el code hash del nodo actual coincide con uno aprobado.
32
+ * @param approvedHashes Lista de hashes aprobados por governance
33
+ * @returns true si el hash actual está en la lista de aprobados
34
+ */
35
+ export declare function isCodeApproved(approvedHashes: string[]): boolean;
36
+ /**
37
+ * Computa un hash rápido del propio binario en tiempo real (fallback).
38
+ * Menos preciso que el hash del código fuente pero no requiere build step.
39
+ */
40
+ export declare function computeRuntimeHash(): string;
41
+ /**
42
+ * Log de integridad al arranque — llamar desde startServer().
43
+ */
44
+ export declare function logCodeIntegrity(): void;
@@ -0,0 +1,104 @@
1
+ /**
2
+ * code-integrity.ts — Fix 2: Runtime code integrity verification
3
+ *
4
+ * Carga el hash computado en build-time y lo expone para:
5
+ * 1. Logging al arranque del nodo
6
+ * 2. GET /health (peers pueden verificar)
7
+ * 3. Comparación con hashes aprobados on-chain por governance
8
+ *
9
+ * GARANTÍA:
10
+ * - Si alguien modifica src/ y recompila → el hash cambia
11
+ * - Los peers pueden detectar que un nodo corre código diferente
12
+ * - El GovernanceModule puede registrar hashes aprobados on-chain
13
+ * - Si el hash no coincide con el aprobado → el nodo queda marcado como no confiable
14
+ *
15
+ * LIMITACIÓN (sin TEE):
16
+ * - Un atacante con root puede modificar dist/ directamente o falsificar el hash
17
+ * - La protección completa requiere TEE (Intel SGX / AMD SEV) — fase futura
18
+ */
19
+ import { existsSync, readFileSync } from "node:fs";
20
+ import { join, dirname } from "node:path";
21
+ import { fileURLToPath } from "node:url";
22
+ import { createHash } from "node:crypto";
23
+ const __dir = dirname(fileURLToPath(import.meta.url));
24
+ let _cached = null;
25
+ /**
26
+ * Carga el code-hash.json generado en build time.
27
+ */
28
+ export function getCodeIntegrity() {
29
+ if (_cached)
30
+ return _cached;
31
+ const hashFile = join(__dir, "code-hash.json");
32
+ if (!existsSync(hashFile)) {
33
+ _cached = {
34
+ codeHash: "unavailable",
35
+ codeHashHex: "0x0000000000000000000000000000000000000000000000000000000000000000",
36
+ computedAt: new Date().toISOString(),
37
+ fileCount: 0,
38
+ available: false,
39
+ };
40
+ return _cached;
41
+ }
42
+ try {
43
+ const raw = JSON.parse(readFileSync(hashFile, "utf8"));
44
+ _cached = {
45
+ codeHash: raw.codeHash ?? "unknown",
46
+ codeHashHex: raw.codeHashHex ?? ("0x" + raw.codeHash),
47
+ computedAt: raw.computedAt ?? "unknown",
48
+ fileCount: raw.fileCount ?? 0,
49
+ available: true,
50
+ };
51
+ }
52
+ catch {
53
+ _cached = {
54
+ codeHash: "parse-error",
55
+ codeHashHex: "0x0000000000000000000000000000000000000000000000000000000000000001",
56
+ computedAt: new Date().toISOString(),
57
+ fileCount: 0,
58
+ available: false,
59
+ };
60
+ }
61
+ return _cached;
62
+ }
63
+ /**
64
+ * Verifica que el code hash del nodo actual coincide con uno aprobado.
65
+ * @param approvedHashes Lista de hashes aprobados por governance
66
+ * @returns true si el hash actual está en la lista de aprobados
67
+ */
68
+ export function isCodeApproved(approvedHashes) {
69
+ const info = getCodeIntegrity();
70
+ if (!info.available)
71
+ return false; // si no hay hash, mejor denegar
72
+ const h = info.codeHash.toLowerCase().replace("0x", "");
73
+ return approvedHashes.some(a => a.toLowerCase().replace("0x", "") === h);
74
+ }
75
+ /**
76
+ * Computa un hash rápido del propio binario en tiempo real (fallback).
77
+ * Menos preciso que el hash del código fuente pero no requiere build step.
78
+ */
79
+ export function computeRuntimeHash() {
80
+ try {
81
+ const selfPath = join(__dir, "validator.js");
82
+ if (!existsSync(selfPath))
83
+ return "no-binary";
84
+ const content = readFileSync(selfPath);
85
+ return createHash("sha256").update(content).digest("hex");
86
+ }
87
+ catch {
88
+ return "hash-error";
89
+ }
90
+ }
91
+ /**
92
+ * Log de integridad al arranque — llamar desde startServer().
93
+ */
94
+ export function logCodeIntegrity() {
95
+ const info = getCodeIntegrity();
96
+ if (info.available) {
97
+ console.log(`[integrity] ✅ Code hash: ${info.codeHash.slice(0, 16)}... (${info.fileCount} files)`);
98
+ console.log(`[integrity] Built at: ${info.computedAt}`);
99
+ }
100
+ else {
101
+ console.warn(`[integrity] ⚠️ Code hash unavailable — run 'pnpm build' to compute`);
102
+ console.warn(`[integrity] Without a code hash, peers cannot verify this node's integrity`);
103
+ }
104
+ }
package/dist/validator.js CHANGED
@@ -9,6 +9,8 @@ import { encryptGossip, decryptGossip } from "./crypto/gossip-cipher.js";
9
9
  import { selectGossipPeers, routingStats } from "./crypto/peer-router.js";
10
10
  import { NullifierConsensus, AttestationConsensus, StateSyncManager } from "./consensus/index.js";
11
11
  import { BlockchainAnchor } from "./blockchain/blockchain-anchor.js";
12
+ import { SoulprintBlockchainClient, loadBlockchainConfig, } from "./blockchain/blockchain-client.js";
13
+ import { getCodeIntegrity, logCodeIntegrity, computeRuntimeHash } from "./code-integrity.js";
12
14
  import { publishAttestationP2P, onAttestationReceived, getP2PStats, } from "./p2p.js";
13
15
  // ── Config ────────────────────────────────────────────────────────────────────
14
16
  const PORT = parseInt(process.env.SOULPRINT_PORT ?? String(PROTOCOL.DEFAULT_HTTP_PORT));
@@ -668,8 +670,15 @@ export function startValidatorNode(port = PORT) {
668
670
  const anchor = new BlockchainAnchor({
669
671
  storePath: join(NODE_DIR, "blockchain-queue"),
670
672
  });
673
+ // Cliente blockchain directo (para governance)
674
+ const bcConfig = loadBlockchainConfig();
675
+ const client = bcConfig
676
+ ? new SoulprintBlockchainClient(bcConfig)
677
+ : null;
671
678
  // Conectar en background (no bloquea el arranque del nodo)
672
679
  anchor.connect().catch(() => { });
680
+ if (client)
681
+ client.connect().catch(() => { });
673
682
  // Escuchar eventos del consenso y anclar async
674
683
  nullifierConsensus.on("committed", (entry) => {
675
684
  anchor.anchorNullifier({
@@ -740,6 +749,28 @@ export function startValidatorNode(port = PORT) {
740
749
  return handleGetReputation(res, decodeURIComponent(cleanUrl.replace("/reputation/", "")));
741
750
  if (cleanUrl.startsWith("/nullifier/") && req.method === "GET")
742
751
  return handleNullifierCheck(res, decodeURIComponent(cleanUrl.replace("/nullifier/", "")));
752
+ // ── Code integrity + health ───────────────────────────────────────────────
753
+ if (cleanUrl === "/health" && req.method === "GET") {
754
+ const integrity = getCodeIntegrity();
755
+ const govHash = await client?.getCurrentApprovedHash() ?? null;
756
+ return json(res, 200, {
757
+ status: "ok",
758
+ version: VERSION,
759
+ protocolHash: PROTOCOL_HASH,
760
+ codeHash: integrity.codeHash,
761
+ codeHashHex: integrity.codeHashHex,
762
+ codeHashAvailable: integrity.available,
763
+ codeBuiltAt: integrity.computedAt,
764
+ codeFileCount: integrity.fileCount,
765
+ runtimeHash: computeRuntimeHash(),
766
+ governanceApprovedHash: govHash,
767
+ blockchainConnected: !!client?.isConnected,
768
+ nodeCompatible: !govHash ||
769
+ govHash.toLowerCase() === ("0x" + PROTOCOL_HASH).toLowerCase(),
770
+ uptime: process.uptime(),
771
+ ts: Date.now(),
772
+ });
773
+ }
743
774
  // ── Blockchain anchor status ──────────────────────────────────────────────
744
775
  if (cleanUrl === "/anchor/stats" && req.method === "GET") {
745
776
  return json(res, 200, anchor.getStats());
@@ -796,9 +827,93 @@ export function startValidatorNode(port = PORT) {
796
827
  return json(res, 400, { error: "Invalid consensus message" });
797
828
  }
798
829
  }
830
+ // ── Governance endpoints ──────────────────────────────────────────────────
831
+ // GET /governance — estado del hash aprobado + propuestas activas
832
+ if (cleanUrl === "/governance" && req.method === "GET") {
833
+ const [currentHash, active, history] = await Promise.all([
834
+ client?.getCurrentApprovedHash() ?? null,
835
+ client?.getActiveProposals() ?? [],
836
+ client?.getHashHistory() ?? [],
837
+ ]);
838
+ return json(res, 200, {
839
+ currentApprovedHash: currentHash ?? PROTOCOL_HASH,
840
+ blockchainConnected: !!client?.isConnected,
841
+ activeProposals: active.length,
842
+ hashHistory: history,
843
+ nodeCompatible: !currentHash || currentHash.toLowerCase() === ("0x" + PROTOCOL_HASH).toLowerCase(),
844
+ });
845
+ }
846
+ // GET /governance/proposals — lista de propuestas activas
847
+ if (cleanUrl === "/governance/proposals" && req.method === "GET") {
848
+ if (!client?.isConnected)
849
+ return json(res, 503, { error: "Blockchain not connected" });
850
+ const proposals = await client.getActiveProposals();
851
+ return json(res, 200, { proposals, total: proposals.length });
852
+ }
853
+ // GET /governance/proposal/:id — detalle de una propuesta
854
+ if (cleanUrl.match(/^\/governance\/proposal\/\d+$/) && req.method === "GET") {
855
+ if (!client?.isConnected)
856
+ return json(res, 503, { error: "Blockchain not connected" });
857
+ const proposalId = parseInt(cleanUrl.split("/").pop());
858
+ const proposal = await client.getProposal(proposalId);
859
+ if (!proposal)
860
+ return json(res, 404, { error: "Proposal not found" });
861
+ const remaining = await client.getTimelockRemaining(proposalId);
862
+ return json(res, 200, { ...proposal, timelockRemainingSeconds: remaining });
863
+ }
864
+ // POST /governance/propose — proponer upgrade del PROTOCOL_HASH
865
+ // Body: { did, newHash, rationale }
866
+ if (cleanUrl === "/governance/propose" && req.method === "POST") {
867
+ if (!client?.isConnected)
868
+ return json(res, 503, { error: "Blockchain not connected" });
869
+ const body = await readBody(req);
870
+ if (!body?.did || !body?.newHash || !body?.rationale) {
871
+ return json(res, 400, { error: "Required: did, newHash, rationale" });
872
+ }
873
+ const result = await client.proposeUpgrade({
874
+ did: body.did,
875
+ newHash: body.newHash,
876
+ rationale: body.rationale,
877
+ });
878
+ if (!result)
879
+ return json(res, 500, { error: "Proposal failed — check validator logs" });
880
+ return json(res, 201, result);
881
+ }
882
+ // POST /governance/vote — votar en una propuesta
883
+ // Body: { proposalId, did, approve }
884
+ if (cleanUrl === "/governance/vote" && req.method === "POST") {
885
+ if (!client?.isConnected)
886
+ return json(res, 503, { error: "Blockchain not connected" });
887
+ const body = await readBody(req);
888
+ if (body?.proposalId === undefined || !body?.did || body?.approve === undefined) {
889
+ return json(res, 400, { error: "Required: proposalId, did, approve" });
890
+ }
891
+ const txHash = await client.voteOnProposal({
892
+ proposalId: Number(body.proposalId),
893
+ did: body.did,
894
+ approve: Boolean(body.approve),
895
+ });
896
+ if (!txHash)
897
+ return json(res, 500, { error: "Vote failed — check validator logs" });
898
+ return json(res, 200, { txHash, proposalId: body.proposalId, approve: body.approve });
899
+ }
900
+ // POST /governance/execute — ejecutar propuesta post-timelock
901
+ // Body: { proposalId }
902
+ if (cleanUrl === "/governance/execute" && req.method === "POST") {
903
+ if (!client?.isConnected)
904
+ return json(res, 503, { error: "Blockchain not connected" });
905
+ const body = await readBody(req);
906
+ if (body?.proposalId === undefined)
907
+ return json(res, 400, { error: "Required: proposalId" });
908
+ const txHash = await client.executeProposal(Number(body.proposalId));
909
+ if (!txHash)
910
+ return json(res, 500, { error: "Execute failed — timelock not expired or proposal not approved" });
911
+ return json(res, 200, { txHash, proposalId: body.proposalId, executed: true });
912
+ }
799
913
  json(res, 404, { error: "Not found" });
800
914
  });
801
915
  server.listen(port, () => {
916
+ logCodeIntegrity();
802
917
  console.log(`\n🌐 Soulprint Validator Node v${VERSION}`);
803
918
  console.log(` Node DID: ${nodeKeypair.did}`);
804
919
  console.log(` Listening: http://0.0.0.0:${port}`);
@@ -809,6 +924,7 @@ export function startValidatorNode(port = PORT) {
809
924
  console.log(` Known peers: ${peers.length}`);
810
925
  console.log(`\n Core endpoints:`);
811
926
  console.log(` POST /verify verify ZK proof + co-sign`);
927
+ console.log(` GET /health code integrity + governance status`);
812
928
  console.log(` GET /info node info`);
813
929
  console.log(` GET /protocol protocol constants (immutable)`);
814
930
  console.log(` GET /nullifier/:n anti-sybil check`);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "soulprint-network",
3
- "version": "0.3.2",
4
- "description": "Soulprint validator node HTTP server that verifies ZK proofs, co-signs SPTs, anti-Sybil registry",
3
+ "version": "0.3.4",
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": {
@@ -12,8 +12,9 @@
12
12
  "README.md"
13
13
  ],
14
14
  "scripts": {
15
- "build": "tsc",
16
- "start": "node dist/server.js"
15
+ "build": "tsc && node scripts/compute-code-hash.mjs",
16
+ "start": "node dist/server.js",
17
+ "build:hash": "node scripts/compute-code-hash.mjs"
17
18
  },
18
19
  "publishConfig": {
19
20
  "access": "public"
@@ -43,6 +44,7 @@
43
44
  "@libp2p/mdns": "11.0.47",
44
45
  "@libp2p/ping": "2.0.37",
45
46
  "@libp2p/tcp": "10.1.19",
47
+ "ethers": "^6.16.0",
46
48
  "libp2p": "2.10.0",
47
49
  "nodemailer": "^8.0.1",
48
50
  "otpauth": "^9.5.0",
@@ -66,4 +68,4 @@
66
68
  "types": "./dist/index.d.ts"
67
69
  }
68
70
  }
69
- }
71
+ }