soulprint-network 0.4.3 → 0.4.5
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/NullifierRegistryClient.d.ts +46 -0
- package/dist/blockchain/NullifierRegistryClient.js +157 -0
- package/dist/blockchain/ReputationRegistryClient.d.ts +45 -0
- package/dist/blockchain/ReputationRegistryClient.js +151 -0
- package/dist/code-hash.json +7 -4
- package/dist/server.js +54 -1
- package/dist/state/StateStore.d.ts +22 -0
- package/dist/state/StateStore.js +61 -0
- package/dist/validator.d.ts +29 -0
- package/dist/validator.js +220 -4
- package/package.json +4 -4
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export declare const NULLIFIER_REGISTRY_RPC = "https://sepolia.base.org";
|
|
2
|
+
export declare const NULLIFIER_REGISTRY_ADDRESS: string;
|
|
3
|
+
export interface NullifierEntry {
|
|
4
|
+
nullifier: string;
|
|
5
|
+
did: string;
|
|
6
|
+
score: number;
|
|
7
|
+
timestamp: number;
|
|
8
|
+
}
|
|
9
|
+
export declare class NullifierRegistryClient {
|
|
10
|
+
private provider;
|
|
11
|
+
private contract;
|
|
12
|
+
private wallet?;
|
|
13
|
+
private cache;
|
|
14
|
+
private cacheAt;
|
|
15
|
+
private cacheTTLMs;
|
|
16
|
+
private address;
|
|
17
|
+
constructor(opts?: {
|
|
18
|
+
rpc?: string;
|
|
19
|
+
address?: string;
|
|
20
|
+
privateKey?: string;
|
|
21
|
+
cacheTTLMs?: number;
|
|
22
|
+
});
|
|
23
|
+
/**
|
|
24
|
+
* Sign a nullifier payload with the validator's private key and register on-chain.
|
|
25
|
+
* Non-blocking — logs warning on failure.
|
|
26
|
+
*
|
|
27
|
+
* The contract verifies the ECDSA signature over keccak256(nullifier, did, score).
|
|
28
|
+
*/
|
|
29
|
+
registerNullifier(opts: {
|
|
30
|
+
nullifier: string;
|
|
31
|
+
did: string;
|
|
32
|
+
score?: number;
|
|
33
|
+
}): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Check if a nullifier is registered on-chain.
|
|
36
|
+
*/
|
|
37
|
+
isRegistered(nullifier: string): Promise<boolean>;
|
|
38
|
+
/**
|
|
39
|
+
* Get all registered nullifiers (cached 2 min).
|
|
40
|
+
*/
|
|
41
|
+
getAllNullifiers(): Promise<NullifierEntry[]>;
|
|
42
|
+
refreshNullifiers(): Promise<NullifierEntry[]>;
|
|
43
|
+
getCount(): Promise<number>;
|
|
44
|
+
get contractAddress(): string;
|
|
45
|
+
}
|
|
46
|
+
export declare const nullifierRegistryClient: NullifierRegistryClient;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NullifierRegistryClient
|
|
3
|
+
* On-chain nullifier registry for Soulprint (Base Sepolia).
|
|
4
|
+
*
|
|
5
|
+
* The soulprint.digital validator (authorizedValidator) signs every nullifier
|
|
6
|
+
* before writing it on-chain. Read-only access is public — anyone can verify
|
|
7
|
+
* isRegistered(nullifier) without trust in any third party.
|
|
8
|
+
*
|
|
9
|
+
* Architecture (v0.5.0):
|
|
10
|
+
* - WRITE: only soulprint.digital validator (ADMIN_PRIVATE_KEY)
|
|
11
|
+
* - READ: anyone, cached 2 min
|
|
12
|
+
*/
|
|
13
|
+
import { ethers } from "ethers";
|
|
14
|
+
import { readFileSync } from "node:fs";
|
|
15
|
+
import { join, dirname } from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
export const NULLIFIER_REGISTRY_RPC = "https://sepolia.base.org";
|
|
19
|
+
function loadAddress() {
|
|
20
|
+
try {
|
|
21
|
+
const f = join(__dirname, "addresses.json");
|
|
22
|
+
const d = JSON.parse(readFileSync(f, "utf8"));
|
|
23
|
+
return d["NullifierRegistry"] ?? "";
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return "";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export const NULLIFIER_REGISTRY_ADDRESS = loadAddress();
|
|
30
|
+
const ABI = [
|
|
31
|
+
"function registerNullifier(bytes32 nullifier, string did, uint256 score, bytes sig) external",
|
|
32
|
+
"function isRegistered(bytes32 nullifier) external view returns (bool)",
|
|
33
|
+
"function getNullifier(bytes32 nullifier) external view returns (bytes32 nul, string did, uint256 score, uint256 timestamp)",
|
|
34
|
+
"function getAllNullifiers() external view returns (tuple(bytes32 nullifier, string did, uint256 score, uint256 timestamp)[])",
|
|
35
|
+
"function getNullifierCount() external view returns (uint256)",
|
|
36
|
+
"function authorizedValidator() external view returns (address)",
|
|
37
|
+
"event NullifierRegistered(bytes32 indexed nullifier, string did, uint256 score, uint256 timestamp)",
|
|
38
|
+
];
|
|
39
|
+
export class NullifierRegistryClient {
|
|
40
|
+
provider;
|
|
41
|
+
contract;
|
|
42
|
+
wallet;
|
|
43
|
+
cache = null;
|
|
44
|
+
cacheAt = 0;
|
|
45
|
+
cacheTTLMs;
|
|
46
|
+
address;
|
|
47
|
+
constructor(opts) {
|
|
48
|
+
this.address = opts?.address ?? NULLIFIER_REGISTRY_ADDRESS;
|
|
49
|
+
this.cacheTTLMs = opts?.cacheTTLMs ?? 2 * 60 * 1000; // 2 min
|
|
50
|
+
const rpc = opts?.rpc ?? NULLIFIER_REGISTRY_RPC;
|
|
51
|
+
this.provider = new ethers.JsonRpcProvider(rpc);
|
|
52
|
+
if (!this.address) {
|
|
53
|
+
console.warn("[nullifier-registry] ⚠️ No contract address — registry disabled");
|
|
54
|
+
this.contract = null;
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (opts?.privateKey) {
|
|
58
|
+
this.wallet = new ethers.Wallet(opts.privateKey, this.provider);
|
|
59
|
+
this.contract = new ethers.Contract(this.address, ABI, this.wallet);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
this.contract = new ethers.Contract(this.address, ABI, this.provider);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Sign a nullifier payload with the validator's private key and register on-chain.
|
|
67
|
+
* Non-blocking — logs warning on failure.
|
|
68
|
+
*
|
|
69
|
+
* The contract verifies the ECDSA signature over keccak256(nullifier, did, score).
|
|
70
|
+
*/
|
|
71
|
+
async registerNullifier(opts) {
|
|
72
|
+
if (!this.wallet) {
|
|
73
|
+
console.warn("[nullifier-registry] ⚠️ No private key — cannot register nullifier");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (!this.address) {
|
|
77
|
+
console.warn("[nullifier-registry] ⚠️ No contract address — skipping registration");
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
const nullifierBytes = opts.nullifier.startsWith("0x")
|
|
82
|
+
? opts.nullifier
|
|
83
|
+
: `0x${opts.nullifier}`;
|
|
84
|
+
const score = BigInt(opts.score ?? 0);
|
|
85
|
+
// Create the message hash matching what the contract verifies
|
|
86
|
+
const msgHash = ethers.keccak256(ethers.solidityPacked(["bytes32", "string", "uint256"], [nullifierBytes, opts.did, score]));
|
|
87
|
+
// Sign with Ethereum personal_sign prefix (matches contract's \x19Ethereum Signed Message:\n32)
|
|
88
|
+
const sig = await this.wallet.signMessage(ethers.getBytes(msgHash));
|
|
89
|
+
const feeData = await this.provider.getFeeData();
|
|
90
|
+
const tx = await this.contract.registerNullifier(nullifierBytes, opts.did, score, sig, { gasPrice: feeData.gasPrice });
|
|
91
|
+
console.log(`[nullifier-registry] 📡 Registering nullifier on-chain... tx: ${tx.hash}`);
|
|
92
|
+
await tx.wait();
|
|
93
|
+
console.log(`[nullifier-registry] ✅ Nullifier registered: ${nullifierBytes.slice(0, 18)}... did=${opts.did.slice(0, 20)}...`);
|
|
94
|
+
// Invalidate cache
|
|
95
|
+
this.cache = null;
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
console.warn(`[nullifier-registry] ⚠️ Registration failed (non-fatal): ${err.shortMessage ?? err.message}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Check if a nullifier is registered on-chain.
|
|
103
|
+
*/
|
|
104
|
+
async isRegistered(nullifier) {
|
|
105
|
+
if (!this.address)
|
|
106
|
+
return false;
|
|
107
|
+
try {
|
|
108
|
+
const n = nullifier.startsWith("0x") ? nullifier : `0x${nullifier}`;
|
|
109
|
+
return await this.contract.isRegistered(n);
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
console.warn(`[nullifier-registry] ⚠️ isRegistered failed: ${err.shortMessage ?? err.message}`);
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Get all registered nullifiers (cached 2 min).
|
|
118
|
+
*/
|
|
119
|
+
async getAllNullifiers() {
|
|
120
|
+
if (this.cache && Date.now() - this.cacheAt < this.cacheTTLMs) {
|
|
121
|
+
return this.cache;
|
|
122
|
+
}
|
|
123
|
+
return this.refreshNullifiers();
|
|
124
|
+
}
|
|
125
|
+
async refreshNullifiers() {
|
|
126
|
+
if (!this.address)
|
|
127
|
+
return [];
|
|
128
|
+
try {
|
|
129
|
+
const raw = await this.contract.getAllNullifiers();
|
|
130
|
+
this.cache = raw.map((e) => ({
|
|
131
|
+
nullifier: e.nullifier,
|
|
132
|
+
did: e.did,
|
|
133
|
+
score: Number(e.score),
|
|
134
|
+
timestamp: Number(e.timestamp),
|
|
135
|
+
}));
|
|
136
|
+
this.cacheAt = Date.now();
|
|
137
|
+
return this.cache;
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
console.warn(`[nullifier-registry] ⚠️ getAllNullifiers failed: ${err.shortMessage ?? err.message}`);
|
|
141
|
+
return this.cache ?? [];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async getCount() {
|
|
145
|
+
if (!this.address)
|
|
146
|
+
return 0;
|
|
147
|
+
try {
|
|
148
|
+
const n = await this.contract.getNullifierCount();
|
|
149
|
+
return Number(n);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return 0;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
get contractAddress() { return this.address; }
|
|
156
|
+
}
|
|
157
|
+
export const nullifierRegistryClient = new NullifierRegistryClient();
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export declare const REPUTATION_REGISTRY_RPC = "https://sepolia.base.org";
|
|
2
|
+
export declare const REPUTATION_REGISTRY_ADDRESS: string;
|
|
3
|
+
export interface ScoreEntry {
|
|
4
|
+
did: string;
|
|
5
|
+
score: number;
|
|
6
|
+
context: string;
|
|
7
|
+
updatedAt: number;
|
|
8
|
+
}
|
|
9
|
+
export declare class ReputationRegistryClient {
|
|
10
|
+
private provider;
|
|
11
|
+
private contract;
|
|
12
|
+
private wallet?;
|
|
13
|
+
private cache;
|
|
14
|
+
private cacheAt;
|
|
15
|
+
private cacheTTLMs;
|
|
16
|
+
private address;
|
|
17
|
+
constructor(opts?: {
|
|
18
|
+
rpc?: string;
|
|
19
|
+
address?: string;
|
|
20
|
+
privateKey?: string;
|
|
21
|
+
cacheTTLMs?: number;
|
|
22
|
+
});
|
|
23
|
+
/**
|
|
24
|
+
* Set or update a reputation score for a DID.
|
|
25
|
+
* Only works if the wallet is the authorized validator.
|
|
26
|
+
* Non-blocking — logs warning on failure.
|
|
27
|
+
*/
|
|
28
|
+
setScore(opts: {
|
|
29
|
+
did: string;
|
|
30
|
+
score: number;
|
|
31
|
+
context?: string;
|
|
32
|
+
}): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Get reputation score for a specific DID.
|
|
35
|
+
*/
|
|
36
|
+
getScore(did: string): Promise<ScoreEntry | null>;
|
|
37
|
+
/**
|
|
38
|
+
* Get all DID scores (cached 2 min).
|
|
39
|
+
*/
|
|
40
|
+
getAllScores(): Promise<ScoreEntry[]>;
|
|
41
|
+
refreshScores(): Promise<ScoreEntry[]>;
|
|
42
|
+
getCount(): Promise<number>;
|
|
43
|
+
get contractAddress(): string;
|
|
44
|
+
}
|
|
45
|
+
export declare const reputationRegistryClient: ReputationRegistryClient;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ReputationRegistryClient
|
|
3
|
+
* On-chain reputation scores for DIDs (Base Sepolia).
|
|
4
|
+
*
|
|
5
|
+
* Only the soulprint.digital validator (authorizedValidator) can write scores.
|
|
6
|
+
* Anyone can read scores from the public ledger.
|
|
7
|
+
*
|
|
8
|
+
* Architecture (v0.5.0):
|
|
9
|
+
* - WRITE: only soulprint.digital validator wallet (ADMIN_PRIVATE_KEY)
|
|
10
|
+
* - READ: anyone, cached 2 min
|
|
11
|
+
*/
|
|
12
|
+
import { ethers } from "ethers";
|
|
13
|
+
import { readFileSync } from "node:fs";
|
|
14
|
+
import { join, dirname } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
export const REPUTATION_REGISTRY_RPC = "https://sepolia.base.org";
|
|
18
|
+
function loadAddress() {
|
|
19
|
+
try {
|
|
20
|
+
const f = join(__dirname, "addresses.json");
|
|
21
|
+
const d = JSON.parse(readFileSync(f, "utf8"));
|
|
22
|
+
return d["ReputationRegistry"] ?? "";
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return "";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export const REPUTATION_REGISTRY_ADDRESS = loadAddress();
|
|
29
|
+
const ABI = [
|
|
30
|
+
"function setScore(string did, uint256 score, string context) external",
|
|
31
|
+
"function getScore(string did) external view returns (string retDid, uint256 score, string context, uint256 updatedAt)",
|
|
32
|
+
"function getAllScores() external view returns (tuple(string did, uint256 score, string context, uint256 updatedAt)[])",
|
|
33
|
+
"function getScoreCount() external view returns (uint256)",
|
|
34
|
+
"function authorizedValidators(address) external view returns (bool)",
|
|
35
|
+
"event ScoreUpdated(string indexed did, uint256 score, string context, uint256 updatedAt)",
|
|
36
|
+
];
|
|
37
|
+
export class ReputationRegistryClient {
|
|
38
|
+
provider;
|
|
39
|
+
contract;
|
|
40
|
+
wallet;
|
|
41
|
+
cache = null;
|
|
42
|
+
cacheAt = 0;
|
|
43
|
+
cacheTTLMs;
|
|
44
|
+
address;
|
|
45
|
+
constructor(opts) {
|
|
46
|
+
this.address = opts?.address ?? REPUTATION_REGISTRY_ADDRESS;
|
|
47
|
+
this.cacheTTLMs = opts?.cacheTTLMs ?? 2 * 60 * 1000; // 2 min
|
|
48
|
+
const rpc = opts?.rpc ?? REPUTATION_REGISTRY_RPC;
|
|
49
|
+
this.provider = new ethers.JsonRpcProvider(rpc);
|
|
50
|
+
if (!this.address) {
|
|
51
|
+
console.warn("[reputation-registry] ⚠️ No contract address — registry disabled");
|
|
52
|
+
this.contract = null;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (opts?.privateKey) {
|
|
56
|
+
this.wallet = new ethers.Wallet(opts.privateKey, this.provider);
|
|
57
|
+
this.contract = new ethers.Contract(this.address, ABI, this.wallet);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
this.contract = new ethers.Contract(this.address, ABI, this.provider);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Set or update a reputation score for a DID.
|
|
65
|
+
* Only works if the wallet is the authorized validator.
|
|
66
|
+
* Non-blocking — logs warning on failure.
|
|
67
|
+
*/
|
|
68
|
+
async setScore(opts) {
|
|
69
|
+
if (!this.wallet) {
|
|
70
|
+
console.warn("[reputation-registry] ⚠️ No private key — cannot write score");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (!this.address) {
|
|
74
|
+
console.warn("[reputation-registry] ⚠️ No contract address — skipping");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const feeData = await this.provider.getFeeData();
|
|
79
|
+
const tx = await this.contract.setScore(opts.did, BigInt(Math.round(opts.score)), opts.context ?? "soulprint:v1", { gasPrice: feeData.gasPrice });
|
|
80
|
+
console.log(`[reputation-registry] 📡 Setting score on-chain... tx: ${tx.hash}`);
|
|
81
|
+
await tx.wait();
|
|
82
|
+
console.log(`[reputation-registry] ✅ Score set: did=${opts.did.slice(0, 20)}... score=${opts.score}`);
|
|
83
|
+
// Invalidate cache
|
|
84
|
+
this.cache = null;
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
console.warn(`[reputation-registry] ⚠️ setScore failed (non-fatal): ${err.shortMessage ?? err.message}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Get reputation score for a specific DID.
|
|
92
|
+
*/
|
|
93
|
+
async getScore(did) {
|
|
94
|
+
if (!this.address)
|
|
95
|
+
return null;
|
|
96
|
+
try {
|
|
97
|
+
const r = await this.contract.getScore(did);
|
|
98
|
+
return {
|
|
99
|
+
did: r.retDid,
|
|
100
|
+
score: Number(r.score),
|
|
101
|
+
context: r.context,
|
|
102
|
+
updatedAt: Number(r.updatedAt),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
console.warn(`[reputation-registry] ⚠️ getScore failed: ${err.shortMessage ?? err.message}`);
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Get all DID scores (cached 2 min).
|
|
112
|
+
*/
|
|
113
|
+
async getAllScores() {
|
|
114
|
+
if (this.cache && Date.now() - this.cacheAt < this.cacheTTLMs) {
|
|
115
|
+
return this.cache;
|
|
116
|
+
}
|
|
117
|
+
return this.refreshScores();
|
|
118
|
+
}
|
|
119
|
+
async refreshScores() {
|
|
120
|
+
if (!this.address)
|
|
121
|
+
return [];
|
|
122
|
+
try {
|
|
123
|
+
const raw = await this.contract.getAllScores();
|
|
124
|
+
this.cache = raw.map((e) => ({
|
|
125
|
+
did: e.did,
|
|
126
|
+
score: Number(e.score),
|
|
127
|
+
context: e.context,
|
|
128
|
+
updatedAt: Number(e.updatedAt),
|
|
129
|
+
}));
|
|
130
|
+
this.cacheAt = Date.now();
|
|
131
|
+
return this.cache;
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
console.warn(`[reputation-registry] ⚠️ getAllScores failed: ${err.shortMessage ?? err.message}`);
|
|
135
|
+
return this.cache ?? [];
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async getCount() {
|
|
139
|
+
if (!this.address)
|
|
140
|
+
return 0;
|
|
141
|
+
try {
|
|
142
|
+
const n = await this.contract.getScoreCount();
|
|
143
|
+
return Number(n);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
get contractAddress() { return this.address; }
|
|
150
|
+
}
|
|
151
|
+
export const reputationRegistryClient = new ReputationRegistryClient();
|
package/dist/code-hash.json
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
{
|
|
2
|
-
"codeHash": "
|
|
3
|
-
"codeHashHex": "
|
|
4
|
-
"computedAt": "2026-03-
|
|
5
|
-
"fileCount":
|
|
2
|
+
"codeHash": "c256c2701bea12bbb2f50552b268bda6065a58d187f7bde6d6be86d82eb73f06",
|
|
3
|
+
"codeHashHex": "0xc256c2701bea12bbb2f50552b268bda6065a58d187f7bde6d6be86d82eb73f06",
|
|
4
|
+
"computedAt": "2026-03-01T02:53:10.893Z",
|
|
5
|
+
"fileCount": 24,
|
|
6
6
|
"files": [
|
|
7
|
+
"blockchain/NullifierRegistryClient.ts",
|
|
7
8
|
"blockchain/PeerRegistryClient.ts",
|
|
9
|
+
"blockchain/ReputationRegistryClient.ts",
|
|
8
10
|
"blockchain/blockchain-anchor.ts",
|
|
9
11
|
"blockchain/blockchain-client.ts",
|
|
10
12
|
"blockchain/protocol-thresholds-client.ts",
|
|
@@ -24,6 +26,7 @@
|
|
|
24
26
|
"p2p.ts",
|
|
25
27
|
"peer-challenge.ts",
|
|
26
28
|
"server.ts",
|
|
29
|
+
"state/StateStore.ts",
|
|
27
30
|
"validator.ts"
|
|
28
31
|
]
|
|
29
32
|
}
|
package/dist/server.js
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* 1. HTTP server (port 4888) — clientes y legado
|
|
7
7
|
* 2. libp2p P2P node (port 6888) — Kademlia DHT + GossipSub + mDNS
|
|
8
8
|
*/
|
|
9
|
-
import { startValidatorNode, setP2PNode, setPeerRegistryClient } from "./validator.js";
|
|
9
|
+
import { startValidatorNode, setP2PNode, setPeerRegistryClient, getNodeState, setLastSyncTs } from "./validator.js";
|
|
10
|
+
import { computeHash, saveState } from "./state/StateStore.js";
|
|
10
11
|
import { createSoulprintP2PNode, MAINNET_BOOTSTRAP, stopP2PNode } from "./p2p.js";
|
|
11
12
|
import { PeerRegistryClient } from "./blockchain/PeerRegistryClient.js";
|
|
12
13
|
// ─── Config ──────────────────────────────────────────────────────────────────
|
|
@@ -148,6 +149,58 @@ if (httpBootstraps.length > 0) {
|
|
|
148
149
|
}
|
|
149
150
|
}, 2_000);
|
|
150
151
|
}
|
|
152
|
+
// ─── Anti-entropy sync loop (v0.4.4) ─────────────────────────────────────────
|
|
153
|
+
// Every 60 seconds: compare state hash with each known peer.
|
|
154
|
+
// If diverged, fetch full state and merge locally.
|
|
155
|
+
setInterval(async () => {
|
|
156
|
+
const { nullifiers, repStore, peers: knownPeers } = getNodeState();
|
|
157
|
+
if (knownPeers.length === 0)
|
|
158
|
+
return;
|
|
159
|
+
const localHash = computeHash(Object.keys(nullifiers));
|
|
160
|
+
for (const peerUrl of knownPeers) {
|
|
161
|
+
try {
|
|
162
|
+
const hashRes = await fetch(`${peerUrl}/state/hash`, { signal: AbortSignal.timeout(5_000) });
|
|
163
|
+
if (!hashRes.ok)
|
|
164
|
+
continue;
|
|
165
|
+
const hashData = await hashRes.json();
|
|
166
|
+
const peerHash = hashData.hash;
|
|
167
|
+
if (peerHash === localHash) {
|
|
168
|
+
console.log(`[sync] peer ${peerUrl}: hash match ✅`);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
// Hashes differ — fetch full state and merge
|
|
172
|
+
const exportRes = await fetch(`${peerUrl}/state/export`, { signal: AbortSignal.timeout(10_000) });
|
|
173
|
+
if (!exportRes.ok) {
|
|
174
|
+
console.warn(`[sync] peer ${peerUrl}: export failed (${exportRes.status})`);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const peerState = await exportRes.json();
|
|
178
|
+
const mergeRes = await fetch(`http://localhost:${HTTP_PORT}/state/merge`, {
|
|
179
|
+
method: "POST",
|
|
180
|
+
headers: { "Content-Type": "application/json" },
|
|
181
|
+
body: JSON.stringify(peerState),
|
|
182
|
+
signal: AbortSignal.timeout(5_000),
|
|
183
|
+
});
|
|
184
|
+
const merged = await mergeRes.json();
|
|
185
|
+
console.log(`[sync] peer ${peerUrl}: diverged → merged ${merged.new_nullifiers ?? 0} nullifiers, ${merged.new_attestations ?? 0} attestations`);
|
|
186
|
+
// Persist updated state
|
|
187
|
+
const { nullifiers: n2, repStore: r2, peers: p2 } = getNodeState();
|
|
188
|
+
const ts = Date.now();
|
|
189
|
+
saveState({
|
|
190
|
+
nullifiers: Object.keys(n2),
|
|
191
|
+
reputation: Object.fromEntries(Object.entries(r2).map(([d, e]) => [d, e.score])),
|
|
192
|
+
attestations: Object.values(r2).flatMap((e) => e.attestations ?? []),
|
|
193
|
+
peers: p2,
|
|
194
|
+
lastSync: ts,
|
|
195
|
+
stateHash: computeHash(Object.keys(n2)),
|
|
196
|
+
}, 0);
|
|
197
|
+
setLastSyncTs(ts);
|
|
198
|
+
}
|
|
199
|
+
catch (e) {
|
|
200
|
+
console.warn(`[sync] peer ${peerUrl}: error — ${e.message}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}, 60_000);
|
|
151
204
|
// ─── Graceful shutdown ────────────────────────────────────────────────────────
|
|
152
205
|
async function shutdown(signal) {
|
|
153
206
|
console.log(`\n${signal} recibido — cerrando...`);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface StoredAttestation {
|
|
2
|
+
issuer_did: string;
|
|
3
|
+
target_did: string;
|
|
4
|
+
value: number;
|
|
5
|
+
context: string;
|
|
6
|
+
timestamp: number;
|
|
7
|
+
sig: string;
|
|
8
|
+
}
|
|
9
|
+
export interface NodeState {
|
|
10
|
+
nullifiers: string[];
|
|
11
|
+
reputation: Record<string, number>;
|
|
12
|
+
attestations: StoredAttestation[];
|
|
13
|
+
peers: string[];
|
|
14
|
+
lastSync: number;
|
|
15
|
+
stateHash: string;
|
|
16
|
+
}
|
|
17
|
+
/** sha256(JSON.stringify(nullifiers.sort())) */
|
|
18
|
+
export declare function computeHash(nullifiers: string[]): string;
|
|
19
|
+
/** Load state from disk. Returns default if file missing or corrupt. */
|
|
20
|
+
export declare function loadState(): NodeState;
|
|
21
|
+
/** Write state to disk — debounced 2 s by default. */
|
|
22
|
+
export declare function saveState(state: NodeState, debounceMs?: number): void;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StateStore — persistent state for P2P sync
|
|
3
|
+
* Saves/loads full node state to disk with debounced writes.
|
|
4
|
+
*/
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
const STATE_PATH = process.env.SOULPRINT_STATE_PATH
|
|
10
|
+
?? join(homedir(), ".soulprint", "node", "state.json");
|
|
11
|
+
const DEFAULT_STATE = {
|
|
12
|
+
nullifiers: [],
|
|
13
|
+
reputation: {},
|
|
14
|
+
attestations: [],
|
|
15
|
+
peers: [],
|
|
16
|
+
lastSync: 0,
|
|
17
|
+
stateHash: "",
|
|
18
|
+
};
|
|
19
|
+
let saveTimer = null;
|
|
20
|
+
/** sha256(JSON.stringify(nullifiers.sort())) */
|
|
21
|
+
export function computeHash(nullifiers) {
|
|
22
|
+
const sorted = [...nullifiers].sort();
|
|
23
|
+
return createHash("sha256").update(JSON.stringify(sorted)).digest("hex");
|
|
24
|
+
}
|
|
25
|
+
/** Load state from disk. Returns default if file missing or corrupt. */
|
|
26
|
+
export function loadState() {
|
|
27
|
+
if (!existsSync(STATE_PATH)) {
|
|
28
|
+
return { ...DEFAULT_STATE, stateHash: computeHash([]) };
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const raw = JSON.parse(readFileSync(STATE_PATH, "utf8"));
|
|
32
|
+
const nullifiers = raw.nullifiers ?? [];
|
|
33
|
+
return {
|
|
34
|
+
nullifiers,
|
|
35
|
+
reputation: raw.reputation ?? {},
|
|
36
|
+
attestations: raw.attestations ?? [],
|
|
37
|
+
peers: raw.peers ?? [],
|
|
38
|
+
lastSync: raw.lastSync ?? 0,
|
|
39
|
+
stateHash: raw.stateHash ?? computeHash(nullifiers),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return { ...DEFAULT_STATE, stateHash: computeHash([]) };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Write state to disk — debounced 2 s by default. */
|
|
47
|
+
export function saveState(state, debounceMs = 2000) {
|
|
48
|
+
if (saveTimer)
|
|
49
|
+
clearTimeout(saveTimer);
|
|
50
|
+
saveTimer = setTimeout(() => {
|
|
51
|
+
try {
|
|
52
|
+
const dir = dirname(STATE_PATH);
|
|
53
|
+
if (!existsSync(dir))
|
|
54
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
55
|
+
writeFileSync(STATE_PATH, JSON.stringify(state, null, 2));
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
console.error("[state] Failed to save state:", e);
|
|
59
|
+
}
|
|
60
|
+
}, debounceMs);
|
|
61
|
+
}
|
package/dist/validator.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { IncomingMessage, ServerResponse } from "node:http";
|
|
|
2
2
|
import { BotAttestation, BotReputation } from "soulprint-core";
|
|
3
3
|
import { type SoulprintP2PNode } from "./p2p.js";
|
|
4
4
|
import { PeerRegistryClient } from "./blockchain/PeerRegistryClient.js";
|
|
5
|
+
import { NullifierRegistryClient } from "./blockchain/NullifierRegistryClient.js";
|
|
6
|
+
import { ReputationRegistryClient } from "./blockchain/ReputationRegistryClient.js";
|
|
5
7
|
export declare function getLiveThresholds(): {
|
|
6
8
|
SCORE_FLOOR: number;
|
|
7
9
|
VERIFIED_SCORE_FLOOR: number;
|
|
@@ -14,6 +16,10 @@ export declare function getLiveThresholds(): {
|
|
|
14
16
|
source: "blockchain" | "local_fallback";
|
|
15
17
|
};
|
|
16
18
|
export declare function setPeerRegistryClient(client: PeerRegistryClient): void;
|
|
19
|
+
export declare function setNullifierRegistry(client: NullifierRegistryClient): void;
|
|
20
|
+
export declare function setReputationRegistry(client: ReputationRegistryClient): void;
|
|
21
|
+
export declare function getNullifierRegistry(): NullifierRegistryClient | null;
|
|
22
|
+
export declare function getReputationRegistry(): ReputationRegistryClient | null;
|
|
17
23
|
/**
|
|
18
24
|
* Inyecta el nodo libp2p al validador.
|
|
19
25
|
* Cuando se llama:
|
|
@@ -21,6 +27,18 @@ export declare function setPeerRegistryClient(client: PeerRegistryClient): void;
|
|
|
21
27
|
* 2. Desde ese momento, gossipAttestation() también publica por P2P
|
|
22
28
|
*/
|
|
23
29
|
export declare function setP2PNode(node: SoulprintP2PNode): void;
|
|
30
|
+
/**
|
|
31
|
+
* Per-DID reputation: score (0-20) + attestation history.
|
|
32
|
+
* Persisted to disk - survives node restarts.
|
|
33
|
+
*/
|
|
34
|
+
interface ReputeEntry {
|
|
35
|
+
score: number;
|
|
36
|
+
base: number;
|
|
37
|
+
attestations: BotAttestation[];
|
|
38
|
+
last_updated: number;
|
|
39
|
+
identityScore: number;
|
|
40
|
+
hasDocumentVerified: boolean;
|
|
41
|
+
}
|
|
24
42
|
/**
|
|
25
43
|
* Aplica una nueva attestation al DID objetivo y persiste.
|
|
26
44
|
*
|
|
@@ -50,3 +68,14 @@ export declare function getBotReputation(nodeUrl: string, did: string): Promise<
|
|
|
50
68
|
export declare function getNodeInfo(nodeUrl: string): Promise<any>;
|
|
51
69
|
export declare const BOOTSTRAP_NODES: string[];
|
|
52
70
|
export { applyAttestation };
|
|
71
|
+
/** Used by anti-entropy loop in server.ts */
|
|
72
|
+
export declare function getNodeState(): {
|
|
73
|
+
nullifiers: Record<string, {
|
|
74
|
+
did: string;
|
|
75
|
+
verified_at: number;
|
|
76
|
+
}>;
|
|
77
|
+
repStore: Record<string, ReputeEntry>;
|
|
78
|
+
peers: string[];
|
|
79
|
+
lastSyncTs: number;
|
|
80
|
+
};
|
|
81
|
+
export declare function setLastSyncTs(ts: number): void;
|
package/dist/validator.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createServer } from "node:http";
|
|
|
2
2
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
|
+
import { computeHash, loadState, saveState } from "./state/StateStore.js";
|
|
5
6
|
import { generateKeypair, keypairFromPrivateKey, decodeToken, sign, createToken, TOKEN_LIFETIME_SECONDS, TOKEN_RENEW_PREEMPTIVE_SECS, TOKEN_RENEW_GRACE_SECS, TOKEN_RENEW_COOLDOWN_SECS, NonceStore, verifyAttestation, computeReputation, defaultReputation, PROTOCOL, PROTOCOL_HASH, isProtocolHashCompatible, computeTotalScoreWithFloor, checkFarming, recordApprovedGain, recordFarmingStrike, loadAuditStore, exportAuditStore, } from "soulprint-core";
|
|
6
7
|
import { verifyProof, deserializeProof } from "soulprint-zkp";
|
|
7
8
|
import { buildChallengeResponse, verifyPeerBehavior, } from "./peer-challenge.js";
|
|
@@ -75,6 +76,23 @@ let peerRegistryClient = null;
|
|
|
75
76
|
export function setPeerRegistryClient(client) {
|
|
76
77
|
peerRegistryClient = client;
|
|
77
78
|
}
|
|
79
|
+
// ── On-Chain Registries (v0.5.0) ───────────────────────────────────────────────
|
|
80
|
+
// The blockchain IS the shared state. These are the single source of truth.
|
|
81
|
+
// Only soulprint.digital validator can WRITE; anyone can READ.
|
|
82
|
+
let nullifierRegistry = null;
|
|
83
|
+
let reputationRegistry = null;
|
|
84
|
+
export function setNullifierRegistry(client) {
|
|
85
|
+
nullifierRegistry = client;
|
|
86
|
+
}
|
|
87
|
+
export function setReputationRegistry(client) {
|
|
88
|
+
reputationRegistry = client;
|
|
89
|
+
}
|
|
90
|
+
export function getNullifierRegistry() {
|
|
91
|
+
return nullifierRegistry;
|
|
92
|
+
}
|
|
93
|
+
export function getReputationRegistry() {
|
|
94
|
+
return reputationRegistry;
|
|
95
|
+
}
|
|
78
96
|
/**
|
|
79
97
|
* Inyecta el nodo libp2p al validador.
|
|
80
98
|
* Cuando se llama:
|
|
@@ -204,8 +222,18 @@ function applyAttestation(att) {
|
|
|
204
222
|
hasDocumentVerified: hasDocument,
|
|
205
223
|
};
|
|
206
224
|
saveReputation();
|
|
225
|
+
// ── Write reputation to on-chain ReputationRegistry (v0.5.0) — non-blocking
|
|
226
|
+
if (reputationRegistry) {
|
|
227
|
+
reputationRegistry.setScore({
|
|
228
|
+
did: att.target_did,
|
|
229
|
+
score: finalRepScore,
|
|
230
|
+
context: "soulprint:v1",
|
|
231
|
+
}).catch(e => console.warn("[reputation-registry] ⚠️ On-chain write failed:", e.message));
|
|
232
|
+
}
|
|
207
233
|
return { score: finalRepScore, attestations: allAtts.length, last_updated: rep.last_updated };
|
|
208
234
|
}
|
|
235
|
+
// ── P2P state sync metadata ───────────────────────────────────────────────────
|
|
236
|
+
let lastSyncTs = 0; // timestamp (ms) of last successful anti-entropy sync
|
|
209
237
|
// ── Peers registry (P2P gossip) ───────────────────────────────────────────────
|
|
210
238
|
let peers = []; // URLs de otros nodos (ej: "http://node2.example.com:4888")
|
|
211
239
|
function loadPeers() {
|
|
@@ -242,6 +270,9 @@ async function gossipAttestation(att, excludeUrl) {
|
|
|
242
270
|
if (targets.length < peers.length - (excludeUrl ? 1 : 0)) {
|
|
243
271
|
console.log(routingStats(peers.length, targets.length, att.target_did));
|
|
244
272
|
}
|
|
273
|
+
if (targets.length > 0) {
|
|
274
|
+
console.log(`[gossip] broadcasted to ${targets.length} peers`);
|
|
275
|
+
}
|
|
245
276
|
// Cifrar el payload con AES-256-GCM antes de enviar
|
|
246
277
|
// Solo nodos con PROTOCOL_HASH correcto pueden descifrar
|
|
247
278
|
const encrypted = encryptGossip({ attestation: att, from_peer: true });
|
|
@@ -686,6 +717,17 @@ async function handleVerify(req, res, nodeKeypair, ip) {
|
|
|
686
717
|
else {
|
|
687
718
|
nullifiers[zkResult.nullifier] = { did: token.did, verified_at: now };
|
|
688
719
|
saveNullifiers();
|
|
720
|
+
// ── Write to on-chain NullifierRegistry (v0.5.0) — non-blocking ──────────
|
|
721
|
+
// soulprint.digital validator signs and certifies the identity on-chain.
|
|
722
|
+
// Anyone can now verify isRegistered(nullifier) without trusting this node.
|
|
723
|
+
if (nullifierRegistry) {
|
|
724
|
+
const score = repStore[token.did]?.score ?? 0;
|
|
725
|
+
nullifierRegistry.registerNullifier({
|
|
726
|
+
nullifier: zkResult.nullifier,
|
|
727
|
+
did: token.did,
|
|
728
|
+
score,
|
|
729
|
+
}).catch(e => console.warn("[nullifier-registry] ⚠️ On-chain write failed:", e.message));
|
|
730
|
+
}
|
|
689
731
|
}
|
|
690
732
|
const coSig = sign({ nullifier: zkResult.nullifier, did: token.did, timestamp: now }, nodeKeypair.privateKey);
|
|
691
733
|
// Incluir reputación actual del bot en la respuesta
|
|
@@ -831,6 +873,30 @@ function handleNullifierCheck(res, nullifier) {
|
|
|
831
873
|
}
|
|
832
874
|
// ── Server ────────────────────────────────────────────────────────────────────
|
|
833
875
|
export function startValidatorNode(port = PORT) {
|
|
876
|
+
// ── Load persistent state from disk (v0.4.4) ───────────────────────────────
|
|
877
|
+
const persisted = loadState();
|
|
878
|
+
if (persisted.nullifiers.length > 0 || Object.keys(persisted.reputation).length > 0) {
|
|
879
|
+
console.log(`[state] Loaded ${persisted.nullifiers.length} nullifiers, ${Object.keys(persisted.reputation).length} reputation entries from disk`);
|
|
880
|
+
// Merge persisted nullifiers into in-memory store
|
|
881
|
+
for (const n of persisted.nullifiers) {
|
|
882
|
+
if (!nullifiers[n]) {
|
|
883
|
+
nullifiers[n] = { did: `did:soulprint:recovered:${n.slice(0, 8)}`, verified_at: persisted.lastSync || Date.now() };
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
// Merge persisted reputation
|
|
887
|
+
for (const [did, score] of Object.entries(persisted.reputation)) {
|
|
888
|
+
if (!repStore[did]) {
|
|
889
|
+
repStore[did] = {
|
|
890
|
+
score,
|
|
891
|
+
base: 10,
|
|
892
|
+
attestations: [],
|
|
893
|
+
last_updated: persisted.lastSync || Date.now(),
|
|
894
|
+
identityScore: 0,
|
|
895
|
+
hasDocumentVerified: false,
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
834
900
|
loadNullifiers();
|
|
835
901
|
loadReputation();
|
|
836
902
|
loadPeers();
|
|
@@ -1020,6 +1086,8 @@ export function startValidatorNode(port = PORT) {
|
|
|
1020
1086
|
const httpPeers = peers.length;
|
|
1021
1087
|
const libp2pPeers = p2pStats?.peers ?? 0;
|
|
1022
1088
|
let registeredPeers = 0;
|
|
1089
|
+
let nullifiersOnchain = 0;
|
|
1090
|
+
let reputationOnchain = 0;
|
|
1023
1091
|
try {
|
|
1024
1092
|
if (peerRegistryClient) {
|
|
1025
1093
|
const chainPeers = await peerRegistryClient.getAllPeers();
|
|
@@ -1027,23 +1095,38 @@ export function startValidatorNode(port = PORT) {
|
|
|
1027
1095
|
}
|
|
1028
1096
|
}
|
|
1029
1097
|
catch { /* non-fatal */ }
|
|
1098
|
+
try {
|
|
1099
|
+
if (nullifierRegistry)
|
|
1100
|
+
nullifiersOnchain = await nullifierRegistry.getCount();
|
|
1101
|
+
}
|
|
1102
|
+
catch { /* non-fatal */ }
|
|
1103
|
+
try {
|
|
1104
|
+
if (reputationRegistry)
|
|
1105
|
+
reputationOnchain = await reputationRegistry.getCount();
|
|
1106
|
+
}
|
|
1107
|
+
catch { /* non-fatal */ }
|
|
1030
1108
|
return json(res, 200, {
|
|
1031
1109
|
node_did: nodeKeypair.did.slice(0, 20) + "...",
|
|
1032
1110
|
version: VERSION,
|
|
1033
1111
|
protocol_hash: PROTOCOL_HASH.slice(0, 16) + "...",
|
|
1034
|
-
// identidades y reputación
|
|
1112
|
+
// identidades y reputación (in-memory cache)
|
|
1035
1113
|
verified_identities: Object.keys(nullifiers).length,
|
|
1036
1114
|
reputation_profiles: Object.keys(repStore).length,
|
|
1037
|
-
//
|
|
1115
|
+
// on-chain state (v0.5.0) — blockchain IS the shared state
|
|
1116
|
+
nullifiers_onchain: nullifiersOnchain,
|
|
1117
|
+
reputation_onchain: reputationOnchain,
|
|
1118
|
+
// peers — HTTP gossip
|
|
1038
1119
|
known_peers: httpPeers,
|
|
1039
|
-
// peers — libp2p P2P
|
|
1120
|
+
// peers — libp2p P2P
|
|
1040
1121
|
p2p_peers: libp2pPeers,
|
|
1041
1122
|
p2p_pubsub_peers: p2pStats?.pubsubPeers ?? 0,
|
|
1042
1123
|
p2p_enabled: !!p2pNode,
|
|
1043
|
-
// total = max de ambas capas (HTTP gossip es el piso mínimo garantizado)
|
|
1044
1124
|
total_peers: Math.max(httpPeers, libp2pPeers),
|
|
1045
1125
|
// on-chain registered peers (PeerRegistry)
|
|
1046
1126
|
registered_peers: registeredPeers,
|
|
1127
|
+
// state sync (v0.5.0 — blockchain is source of truth)
|
|
1128
|
+
state_hash: computeHash(Object.keys(nullifiers)).slice(0, 16) + "...",
|
|
1129
|
+
last_sync: lastSyncTs,
|
|
1047
1130
|
// estado general
|
|
1048
1131
|
uptime_ms: Date.now() - (globalThis._startTime ?? Date.now()),
|
|
1049
1132
|
timestamp: Date.now(),
|
|
@@ -1052,6 +1135,134 @@ export function startValidatorNode(port = PORT) {
|
|
|
1052
1135
|
}
|
|
1053
1136
|
if (cleanUrl === "/verify" && req.method === "POST")
|
|
1054
1137
|
return handleVerify(req, res, nodeKeypair, ip);
|
|
1138
|
+
// ── State endpoints (v0.5.0) — blockchain IS the shared state ────────────
|
|
1139
|
+
// GET /state/hash — hash of on-chain nullifier count + reputation count
|
|
1140
|
+
if (cleanUrl === "/state/hash" && req.method === "GET") {
|
|
1141
|
+
const currentNullifiers = Object.keys(nullifiers);
|
|
1142
|
+
let onchainNullifiers = currentNullifiers.length;
|
|
1143
|
+
let onchainReputation = Object.keys(repStore).length;
|
|
1144
|
+
try {
|
|
1145
|
+
if (nullifierRegistry)
|
|
1146
|
+
onchainNullifiers = await nullifierRegistry.getCount();
|
|
1147
|
+
}
|
|
1148
|
+
catch { }
|
|
1149
|
+
try {
|
|
1150
|
+
if (reputationRegistry)
|
|
1151
|
+
onchainReputation = await reputationRegistry.getCount();
|
|
1152
|
+
}
|
|
1153
|
+
catch { }
|
|
1154
|
+
// Hash includes on-chain counts for consensus
|
|
1155
|
+
const hash = computeHash([...currentNullifiers, `onchain:${onchainNullifiers}:${onchainReputation}`]);
|
|
1156
|
+
return json(res, 200, {
|
|
1157
|
+
hash,
|
|
1158
|
+
nullifier_count: currentNullifiers.length,
|
|
1159
|
+
nullifier_count_onchain: onchainNullifiers,
|
|
1160
|
+
reputation_count: Object.keys(repStore).length,
|
|
1161
|
+
reputation_count_onchain: onchainReputation,
|
|
1162
|
+
attestation_count: Object.values(repStore).reduce((n, e) => n + (e.attestations?.length ?? 0), 0),
|
|
1163
|
+
timestamp: Date.now(),
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
// GET /state/export — full state export including on-chain data
|
|
1167
|
+
if (cleanUrl === "/state/export" && req.method === "GET") {
|
|
1168
|
+
const allAttestations = Object.values(repStore).flatMap(e => e.attestations ?? []);
|
|
1169
|
+
// Read on-chain data (cached — won't hit RPC on every call)
|
|
1170
|
+
const [onchainNullifiers, onchainScores] = await Promise.all([
|
|
1171
|
+
nullifierRegistry ? nullifierRegistry.getAllNullifiers().catch(() => []) : Promise.resolve([]),
|
|
1172
|
+
reputationRegistry ? reputationRegistry.getAllScores().catch(() => []) : Promise.resolve([]),
|
|
1173
|
+
]);
|
|
1174
|
+
return json(res, 200, {
|
|
1175
|
+
// Local in-memory state
|
|
1176
|
+
nullifiers: Object.keys(nullifiers),
|
|
1177
|
+
reputation: Object.fromEntries(Object.entries(repStore).map(([did, e]) => [did, e.score])),
|
|
1178
|
+
attestations: allAttestations,
|
|
1179
|
+
peers,
|
|
1180
|
+
lastSync: lastSyncTs,
|
|
1181
|
+
stateHash: computeHash(Object.keys(nullifiers)),
|
|
1182
|
+
timestamp: Date.now(),
|
|
1183
|
+
// On-chain state (v0.5.0) — canonical source of truth
|
|
1184
|
+
onchain: {
|
|
1185
|
+
nullifiers: onchainNullifiers,
|
|
1186
|
+
reputation: onchainScores,
|
|
1187
|
+
nullifier_count: onchainNullifiers.length,
|
|
1188
|
+
reputation_count: onchainScores.length,
|
|
1189
|
+
},
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
// POST /state/merge — merge partial state from a peer
|
|
1193
|
+
if (cleanUrl === "/state/merge" && req.method === "POST") {
|
|
1194
|
+
let body;
|
|
1195
|
+
try {
|
|
1196
|
+
body = await readBody(req);
|
|
1197
|
+
}
|
|
1198
|
+
catch (e) {
|
|
1199
|
+
return json(res, 400, { error: e.message });
|
|
1200
|
+
}
|
|
1201
|
+
const incoming = body ?? {};
|
|
1202
|
+
let newNullifiers = 0;
|
|
1203
|
+
let newAttestations = 0;
|
|
1204
|
+
// Merge nullifiers (union)
|
|
1205
|
+
if (Array.isArray(incoming.nullifiers)) {
|
|
1206
|
+
for (const n of incoming.nullifiers) {
|
|
1207
|
+
if (typeof n === "string" && !nullifiers[n]) {
|
|
1208
|
+
nullifiers[n] = { did: `did:soulprint:synced:${n.slice(0, 8)}`, verified_at: Date.now() };
|
|
1209
|
+
newNullifiers++;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
if (newNullifiers > 0)
|
|
1213
|
+
saveNullifiers();
|
|
1214
|
+
}
|
|
1215
|
+
// Merge reputation (take max score)
|
|
1216
|
+
if (incoming.reputation && typeof incoming.reputation === "object") {
|
|
1217
|
+
for (const [did, score] of Object.entries(incoming.reputation)) {
|
|
1218
|
+
if (typeof score !== "number")
|
|
1219
|
+
continue;
|
|
1220
|
+
if (!repStore[did]) {
|
|
1221
|
+
repStore[did] = { score, base: 10, attestations: [], last_updated: Date.now(), identityScore: 0, hasDocumentVerified: false };
|
|
1222
|
+
}
|
|
1223
|
+
else if (score > repStore[did].score) {
|
|
1224
|
+
repStore[did].score = score;
|
|
1225
|
+
repStore[did].last_updated = Date.now();
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
if (Object.keys(incoming.reputation).length > 0)
|
|
1229
|
+
saveReputation();
|
|
1230
|
+
}
|
|
1231
|
+
// Merge attestations (dedup by issuer+timestamp+context)
|
|
1232
|
+
if (Array.isArray(incoming.attestations)) {
|
|
1233
|
+
for (const att of incoming.attestations) {
|
|
1234
|
+
if (!att?.issuer_did || !att?.target_did)
|
|
1235
|
+
continue;
|
|
1236
|
+
const existing = repStore[att.target_did];
|
|
1237
|
+
const prevAtts = existing?.attestations ?? [];
|
|
1238
|
+
const isDup = prevAtts.some(a => a.issuer_did === att.issuer_did &&
|
|
1239
|
+
a.timestamp === att.timestamp &&
|
|
1240
|
+
a.context === att.context);
|
|
1241
|
+
if (!isDup) {
|
|
1242
|
+
applyAttestation(att);
|
|
1243
|
+
newAttestations++;
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
// Persist new unified state
|
|
1248
|
+
if (newNullifiers > 0 || newAttestations > 0) {
|
|
1249
|
+
const snapshot = {
|
|
1250
|
+
nullifiers: Object.keys(nullifiers),
|
|
1251
|
+
reputation: Object.fromEntries(Object.entries(repStore).map(([d, e]) => [d, e.score])),
|
|
1252
|
+
attestations: Object.values(repStore).flatMap(e => e.attestations ?? []),
|
|
1253
|
+
peers,
|
|
1254
|
+
lastSync: Date.now(),
|
|
1255
|
+
stateHash: computeHash(Object.keys(nullifiers)),
|
|
1256
|
+
};
|
|
1257
|
+
saveState(snapshot);
|
|
1258
|
+
lastSyncTs = Date.now();
|
|
1259
|
+
}
|
|
1260
|
+
return json(res, 200, {
|
|
1261
|
+
ok: true,
|
|
1262
|
+
new_nullifiers: newNullifiers,
|
|
1263
|
+
new_attestations: newAttestations,
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1055
1266
|
if (cleanUrl === "/token/renew" && req.method === "POST")
|
|
1056
1267
|
return handleTokenRenew(req, res, nodeKeypair);
|
|
1057
1268
|
if (cleanUrl === "/challenge" && req.method === "POST")
|
|
@@ -1433,3 +1644,8 @@ export async function getNodeInfo(nodeUrl) {
|
|
|
1433
1644
|
}
|
|
1434
1645
|
export const BOOTSTRAP_NODES = [];
|
|
1435
1646
|
export { applyAttestation };
|
|
1647
|
+
/** Used by anti-entropy loop in server.ts */
|
|
1648
|
+
export function getNodeState() {
|
|
1649
|
+
return { nullifiers, repStore, peers, lastSyncTs };
|
|
1650
|
+
}
|
|
1651
|
+
export function setLastSyncTs(ts) { lastSyncTs = ts; }
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "soulprint-network",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"description": "Soulprint validator node
|
|
3
|
+
"version": "0.4.5",
|
|
4
|
+
"description": "Soulprint validator node \u2014 HTTP server that verifies ZK proofs, co-signs SPTs, anti-Sybil registry",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"bin": {
|
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
"nodemailer": "^8.0.1",
|
|
52
52
|
"otpauth": "^9.5.0",
|
|
53
53
|
"otplib": "^13.3.0",
|
|
54
|
-
"soulprint-core": "
|
|
55
|
-
"soulprint-zkp": "
|
|
54
|
+
"soulprint-core": "0.1.11",
|
|
55
|
+
"soulprint-zkp": "0.1.5",
|
|
56
56
|
"uint8arrays": "5.1.0"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|