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.
- package/dist/blockchain/blockchain-client.d.ts +76 -0
- package/dist/blockchain/blockchain-client.js +222 -2
- package/dist/code-hash.json +25 -0
- package/dist/code-integrity.d.ts +44 -0
- package/dist/code-integrity.js +104 -0
- package/dist/validator.js +116 -0
- package/package.json +7 -5
|
@@ -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(
|
|
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.
|
|
4
|
-
"description": "Soulprint validator node
|
|
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
|
+
}
|