soulprint-network 0.3.2 → 0.3.3

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
  }
package/dist/validator.js CHANGED
@@ -9,6 +9,7 @@ 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";
12
13
  import { publishAttestationP2P, onAttestationReceived, getP2PStats, } from "./p2p.js";
13
14
  // ── Config ────────────────────────────────────────────────────────────────────
14
15
  const PORT = parseInt(process.env.SOULPRINT_PORT ?? String(PROTOCOL.DEFAULT_HTTP_PORT));
@@ -668,8 +669,15 @@ export function startValidatorNode(port = PORT) {
668
669
  const anchor = new BlockchainAnchor({
669
670
  storePath: join(NODE_DIR, "blockchain-queue"),
670
671
  });
672
+ // Cliente blockchain directo (para governance)
673
+ const bcConfig = loadBlockchainConfig();
674
+ const client = bcConfig
675
+ ? new SoulprintBlockchainClient(bcConfig)
676
+ : null;
671
677
  // Conectar en background (no bloquea el arranque del nodo)
672
678
  anchor.connect().catch(() => { });
679
+ if (client)
680
+ client.connect().catch(() => { });
673
681
  // Escuchar eventos del consenso y anclar async
674
682
  nullifierConsensus.on("committed", (entry) => {
675
683
  anchor.anchorNullifier({
@@ -796,6 +804,89 @@ export function startValidatorNode(port = PORT) {
796
804
  return json(res, 400, { error: "Invalid consensus message" });
797
805
  }
798
806
  }
807
+ // ── Governance endpoints ──────────────────────────────────────────────────
808
+ // GET /governance — estado del hash aprobado + propuestas activas
809
+ if (cleanUrl === "/governance" && req.method === "GET") {
810
+ const [currentHash, active, history] = await Promise.all([
811
+ client?.getCurrentApprovedHash() ?? null,
812
+ client?.getActiveProposals() ?? [],
813
+ client?.getHashHistory() ?? [],
814
+ ]);
815
+ return json(res, 200, {
816
+ currentApprovedHash: currentHash ?? PROTOCOL_HASH,
817
+ blockchainConnected: !!client?.isConnected,
818
+ activeProposals: active.length,
819
+ hashHistory: history,
820
+ nodeCompatible: !currentHash || currentHash.toLowerCase() === ("0x" + PROTOCOL_HASH).toLowerCase(),
821
+ });
822
+ }
823
+ // GET /governance/proposals — lista de propuestas activas
824
+ if (cleanUrl === "/governance/proposals" && req.method === "GET") {
825
+ if (!client?.isConnected)
826
+ return json(res, 503, { error: "Blockchain not connected" });
827
+ const proposals = await client.getActiveProposals();
828
+ return json(res, 200, { proposals, total: proposals.length });
829
+ }
830
+ // GET /governance/proposal/:id — detalle de una propuesta
831
+ if (cleanUrl.match(/^\/governance\/proposal\/\d+$/) && req.method === "GET") {
832
+ if (!client?.isConnected)
833
+ return json(res, 503, { error: "Blockchain not connected" });
834
+ const proposalId = parseInt(cleanUrl.split("/").pop());
835
+ const proposal = await client.getProposal(proposalId);
836
+ if (!proposal)
837
+ return json(res, 404, { error: "Proposal not found" });
838
+ const remaining = await client.getTimelockRemaining(proposalId);
839
+ return json(res, 200, { ...proposal, timelockRemainingSeconds: remaining });
840
+ }
841
+ // POST /governance/propose — proponer upgrade del PROTOCOL_HASH
842
+ // Body: { did, newHash, rationale }
843
+ if (cleanUrl === "/governance/propose" && req.method === "POST") {
844
+ if (!client?.isConnected)
845
+ return json(res, 503, { error: "Blockchain not connected" });
846
+ const body = await readBody(req);
847
+ if (!body?.did || !body?.newHash || !body?.rationale) {
848
+ return json(res, 400, { error: "Required: did, newHash, rationale" });
849
+ }
850
+ const result = await client.proposeUpgrade({
851
+ did: body.did,
852
+ newHash: body.newHash,
853
+ rationale: body.rationale,
854
+ });
855
+ if (!result)
856
+ return json(res, 500, { error: "Proposal failed — check validator logs" });
857
+ return json(res, 201, result);
858
+ }
859
+ // POST /governance/vote — votar en una propuesta
860
+ // Body: { proposalId, did, approve }
861
+ if (cleanUrl === "/governance/vote" && req.method === "POST") {
862
+ if (!client?.isConnected)
863
+ return json(res, 503, { error: "Blockchain not connected" });
864
+ const body = await readBody(req);
865
+ if (body?.proposalId === undefined || !body?.did || body?.approve === undefined) {
866
+ return json(res, 400, { error: "Required: proposalId, did, approve" });
867
+ }
868
+ const txHash = await client.voteOnProposal({
869
+ proposalId: Number(body.proposalId),
870
+ did: body.did,
871
+ approve: Boolean(body.approve),
872
+ });
873
+ if (!txHash)
874
+ return json(res, 500, { error: "Vote failed — check validator logs" });
875
+ return json(res, 200, { txHash, proposalId: body.proposalId, approve: body.approve });
876
+ }
877
+ // POST /governance/execute — ejecutar propuesta post-timelock
878
+ // Body: { proposalId }
879
+ if (cleanUrl === "/governance/execute" && req.method === "POST") {
880
+ if (!client?.isConnected)
881
+ return json(res, 503, { error: "Blockchain not connected" });
882
+ const body = await readBody(req);
883
+ if (body?.proposalId === undefined)
884
+ return json(res, 400, { error: "Required: proposalId" });
885
+ const txHash = await client.executeProposal(Number(body.proposalId));
886
+ if (!txHash)
887
+ return json(res, 500, { error: "Execute failed — timelock not expired or proposal not approved" });
888
+ return json(res, 200, { txHash, proposalId: body.proposalId, executed: true });
889
+ }
799
890
  json(res, 404, { error: "Not found" });
800
891
  });
801
892
  server.listen(port, () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "soulprint-network",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
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",
@@ -43,6 +43,7 @@
43
43
  "@libp2p/mdns": "11.0.47",
44
44
  "@libp2p/ping": "2.0.37",
45
45
  "@libp2p/tcp": "10.1.19",
46
+ "ethers": "^6.16.0",
46
47
  "libp2p": "2.10.0",
47
48
  "nodemailer": "^8.0.1",
48
49
  "otpauth": "^9.5.0",