forge-trust-chain 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +368 -0
- package/package.json +55 -0
- package/src/cli/index.js +547 -0
- package/src/core/chain.js +186 -0
- package/src/core/merkle.js +131 -0
- package/src/core/trust-atom.js +125 -0
- package/src/core/trust-pixel.js +81 -0
- package/src/core/witness.js +377 -0
- package/src/mcp/server.js +534 -0
- package/src/scanner/index.js +437 -0
- package/src/store/store.js +133 -0
- package/src/test.js +266 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FORGE Witness Module — Existence Proof System
|
|
3
|
+
*
|
|
4
|
+
* Trust = Certainty × Existence
|
|
5
|
+
*
|
|
6
|
+
* Hash provides Certainty (mathematical, deterministic).
|
|
7
|
+
* Witness provides Existence (physical, social, temporal).
|
|
8
|
+
*
|
|
9
|
+
* This module implements the witness hierarchy:
|
|
10
|
+
* Level 1 — Self: Only you hold the hash (can be deleted)
|
|
11
|
+
* Level 2 — Bilateral: Two parties hold the hash (one can't deny)
|
|
12
|
+
* Level 3 — Public: Timestamped by 3rd party (independent attestation)
|
|
13
|
+
* Level 4 — Anchored: Embedded in public blockchain (computationally undeletable)
|
|
14
|
+
*
|
|
15
|
+
* OpenTimestamps integration provides Level 3 → Level 4 path:
|
|
16
|
+
* 1. Submit Merkle root to OTS calendar servers (free, no API key)
|
|
17
|
+
* 2. Calendar servers aggregate hashes into Bitcoin transactions
|
|
18
|
+
* 3. After ~2 hours, receipt upgrades to Bitcoin block attestation
|
|
19
|
+
* 4. Anyone can independently verify without trusting any party
|
|
20
|
+
*
|
|
21
|
+
* Calendar servers used:
|
|
22
|
+
* - https://a.pool.opentimestamps.org
|
|
23
|
+
* - https://b.pool.opentimestamps.org
|
|
24
|
+
* - https://a.pool.eternitywall.com
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
28
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
29
|
+
import { join, dirname } from "node:path";
|
|
30
|
+
import { homedir } from "node:os";
|
|
31
|
+
|
|
32
|
+
/* ================================================================
|
|
33
|
+
CONSTANTS
|
|
34
|
+
================================================================ */
|
|
35
|
+
|
|
36
|
+
const OTS_CALENDARS = [
|
|
37
|
+
"https://a.pool.opentimestamps.org",
|
|
38
|
+
"https://b.pool.opentimestamps.org",
|
|
39
|
+
"https://a.pool.eternitywall.com",
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const OTS_HEADER = Buffer.from([
|
|
43
|
+
0x00, 0x4f, 0x70, 0x65, 0x6e, 0x54, 0x69, 0x6d,
|
|
44
|
+
0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, 0x00,
|
|
45
|
+
0x00, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x00, 0xbf,
|
|
46
|
+
0x89, 0xe2, 0xe8, 0x84, 0xe8, 0x92, 0x94, 0x01,
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
// OTS opcodes
|
|
50
|
+
const OTS_OP = {
|
|
51
|
+
SHA256: 0x08,
|
|
52
|
+
APPEND: 0xf0,
|
|
53
|
+
PREPEND: 0xf1,
|
|
54
|
+
ATTESTATION_PENDING: 0x83,
|
|
55
|
+
ATTESTATION_BITCOIN: 0x05,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const WITNESS_DIR = join(homedir(), ".forge", "witnesses");
|
|
59
|
+
|
|
60
|
+
/* ================================================================
|
|
61
|
+
WITNESS STORE
|
|
62
|
+
================================================================ */
|
|
63
|
+
|
|
64
|
+
function ensureWitnessDir() {
|
|
65
|
+
if (!existsSync(WITNESS_DIR)) {
|
|
66
|
+
mkdirSync(WITNESS_DIR, { recursive: true });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Save a witness receipt to disk.
|
|
72
|
+
* @param {string} merkleRoot - The Merkle root hash being witnessed
|
|
73
|
+
* @param {object} witness - Witness data
|
|
74
|
+
*/
|
|
75
|
+
export function saveWitness(merkleRoot, witness) {
|
|
76
|
+
ensureWitnessDir();
|
|
77
|
+
const path = join(WITNESS_DIR, `${merkleRoot}.json`);
|
|
78
|
+
|
|
79
|
+
// Load existing witnesses for this root
|
|
80
|
+
let existing = [];
|
|
81
|
+
if (existsSync(path)) {
|
|
82
|
+
try { existing = JSON.parse(readFileSync(path, "utf8")); } catch {}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
existing.push(witness);
|
|
86
|
+
writeFileSync(path, JSON.stringify(existing, null, 2));
|
|
87
|
+
return path;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Load all witnesses for a Merkle root.
|
|
92
|
+
*/
|
|
93
|
+
export function loadWitnesses(merkleRoot) {
|
|
94
|
+
const path = join(WITNESS_DIR, `${merkleRoot}.json`);
|
|
95
|
+
if (!existsSync(path)) return [];
|
|
96
|
+
try { return JSON.parse(readFileSync(path, "utf8")); } catch { return []; }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get the highest witness level for a Merkle root.
|
|
101
|
+
*/
|
|
102
|
+
export function witnessLevel(merkleRoot) {
|
|
103
|
+
const witnesses = loadWitnesses(merkleRoot);
|
|
104
|
+
if (witnesses.length === 0) return { level: 1, label: "self", description: "Local only — can be deleted" };
|
|
105
|
+
|
|
106
|
+
let maxLevel = 1;
|
|
107
|
+
let bestWitness = null;
|
|
108
|
+
|
|
109
|
+
for (const w of witnesses) {
|
|
110
|
+
const lvl = w.level || 1;
|
|
111
|
+
if (lvl > maxLevel) {
|
|
112
|
+
maxLevel = lvl;
|
|
113
|
+
bestWitness = w;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const labels = {
|
|
118
|
+
1: { label: "self", description: "Local only — can be deleted" },
|
|
119
|
+
2: { label: "bilateral", description: "Two parties hold hash — one can't deny" },
|
|
120
|
+
3: { label: "public", description: "Calendar server attested — independent verification possible" },
|
|
121
|
+
4: { label: "anchored", description: "Bitcoin blockchain — computationally undeletable" },
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
return { level: maxLevel, ...labels[maxLevel], witness: bestWitness };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* ================================================================
|
|
128
|
+
LEVEL 3: OPENTIMESTAMPS CALENDAR SUBMISSION
|
|
129
|
+
================================================================ */
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Submit a SHA-256 hash to OpenTimestamps calendar servers.
|
|
133
|
+
*
|
|
134
|
+
* The OTS protocol is simple:
|
|
135
|
+
* 1. Generate a random nonce for privacy
|
|
136
|
+
* 2. Hash: SHA256(hash + nonce) → digest
|
|
137
|
+
* 3. POST digest (32 raw bytes) to calendar/digest endpoint
|
|
138
|
+
* 4. Calendar returns operations to get from digest → calendar's Merkle root
|
|
139
|
+
* 5. Store the receipt (nonce + calendar response) as pending attestation
|
|
140
|
+
*
|
|
141
|
+
* After ~2 hours, the calendar's Merkle root will be embedded in a Bitcoin
|
|
142
|
+
* transaction, and the receipt can be upgraded to a full attestation.
|
|
143
|
+
*
|
|
144
|
+
* @param {string} hashHex - 64-character hex SHA-256 hash (e.g., Merkle root)
|
|
145
|
+
* @returns {Promise<object>} - Submission result with calendar responses
|
|
146
|
+
*/
|
|
147
|
+
export async function submitToOTS(hashHex) {
|
|
148
|
+
if (!hashHex || hashHex.length !== 64) {
|
|
149
|
+
throw new Error(`Invalid hash: expected 64-char hex, got ${hashHex?.length || 0}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Generate nonce for privacy (calendar never sees real hash)
|
|
153
|
+
const nonce = randomBytes(16);
|
|
154
|
+
const hashBytes = Buffer.from(hashHex, "hex");
|
|
155
|
+
|
|
156
|
+
// Compute: SHA256(nonce || hash)
|
|
157
|
+
const digest = createHash("sha256")
|
|
158
|
+
.update(Buffer.concat([nonce, hashBytes]))
|
|
159
|
+
.digest();
|
|
160
|
+
|
|
161
|
+
const results = [];
|
|
162
|
+
const errors = [];
|
|
163
|
+
|
|
164
|
+
// Submit to each calendar server in parallel
|
|
165
|
+
const submissions = OTS_CALENDARS.map(async (calendarUrl) => {
|
|
166
|
+
try {
|
|
167
|
+
const url = `${calendarUrl}/digest`;
|
|
168
|
+
const response = await fetch(url, {
|
|
169
|
+
method: "POST",
|
|
170
|
+
headers: {
|
|
171
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
172
|
+
"User-Agent": "forge-trust-chain/0.1",
|
|
173
|
+
"Accept": "application/vnd.opentimestamps.v1",
|
|
174
|
+
},
|
|
175
|
+
body: digest,
|
|
176
|
+
signal: AbortSignal.timeout(10000),
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
if (!response.ok) {
|
|
180
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Calendar returns binary OTS operations
|
|
184
|
+
const responseBuffer = Buffer.from(await response.arrayBuffer());
|
|
185
|
+
|
|
186
|
+
results.push({
|
|
187
|
+
calendar: calendarUrl,
|
|
188
|
+
status: "submitted",
|
|
189
|
+
response_hex: responseBuffer.toString("hex"),
|
|
190
|
+
response_length: responseBuffer.length,
|
|
191
|
+
submitted_at: new Date().toISOString(),
|
|
192
|
+
});
|
|
193
|
+
} catch (err) {
|
|
194
|
+
errors.push({
|
|
195
|
+
calendar: calendarUrl,
|
|
196
|
+
status: "error",
|
|
197
|
+
error: err.message,
|
|
198
|
+
submitted_at: new Date().toISOString(),
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
await Promise.allSettled(submissions);
|
|
204
|
+
|
|
205
|
+
const receipt = {
|
|
206
|
+
type: "ots_pending",
|
|
207
|
+
level: results.length > 0 ? 3 : 1,
|
|
208
|
+
original_hash: hashHex,
|
|
209
|
+
nonce: nonce.toString("hex"),
|
|
210
|
+
digest: digest.toString("hex"),
|
|
211
|
+
calendars: [...results, ...errors],
|
|
212
|
+
successful_submissions: results.length,
|
|
213
|
+
total_calendars: OTS_CALENDARS.length,
|
|
214
|
+
created_at: new Date().toISOString(),
|
|
215
|
+
note: results.length > 0
|
|
216
|
+
? "Pending Bitcoin confirmation. Run 'forge anchor --upgrade' after ~2 hours to check."
|
|
217
|
+
: "All calendar submissions failed. Check network connectivity.",
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// Save witness
|
|
221
|
+
if (results.length > 0) {
|
|
222
|
+
saveWitness(hashHex, receipt);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return receipt;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Check if a pending OTS attestation has been confirmed in Bitcoin.
|
|
230
|
+
*
|
|
231
|
+
* This queries the calendar server to see if the pending attestation
|
|
232
|
+
* has been upgraded to a Bitcoin block header attestation.
|
|
233
|
+
*
|
|
234
|
+
* @param {string} merkleRoot - The Merkle root to check
|
|
235
|
+
* @returns {Promise<object>} - Upgrade status
|
|
236
|
+
*/
|
|
237
|
+
export async function checkOTSUpgrade(merkleRoot) {
|
|
238
|
+
const witnesses = loadWitnesses(merkleRoot);
|
|
239
|
+
const pending = witnesses.find(w => w.type === "ots_pending");
|
|
240
|
+
|
|
241
|
+
if (!pending) {
|
|
242
|
+
return { status: "no_pending", message: "No pending OTS attestation found for this root." };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const upgraded = [];
|
|
246
|
+
const still_pending = [];
|
|
247
|
+
|
|
248
|
+
for (const cal of pending.calendars) {
|
|
249
|
+
if (cal.status !== "submitted") continue;
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
// Query calendar for upgrade
|
|
253
|
+
const url = `${cal.calendar}/timestamp/${pending.digest}`;
|
|
254
|
+
const response = await fetch(url, {
|
|
255
|
+
headers: { "Accept": "application/vnd.opentimestamps.v1" },
|
|
256
|
+
signal: AbortSignal.timeout(10000),
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
if (response.ok) {
|
|
260
|
+
const body = Buffer.from(await response.arrayBuffer());
|
|
261
|
+
// Check if response contains Bitcoin attestation (0x05 marker)
|
|
262
|
+
if (body.includes(Buffer.from([OTS_OP.ATTESTATION_BITCOIN]))) {
|
|
263
|
+
upgraded.push({
|
|
264
|
+
calendar: cal.calendar,
|
|
265
|
+
status: "confirmed",
|
|
266
|
+
proof_hex: body.toString("hex"),
|
|
267
|
+
confirmed_at: new Date().toISOString(),
|
|
268
|
+
});
|
|
269
|
+
} else {
|
|
270
|
+
still_pending.push({ calendar: cal.calendar, status: "still_pending" });
|
|
271
|
+
}
|
|
272
|
+
} else {
|
|
273
|
+
still_pending.push({ calendar: cal.calendar, status: "unavailable", http: response.status });
|
|
274
|
+
}
|
|
275
|
+
} catch (err) {
|
|
276
|
+
still_pending.push({ calendar: cal.calendar, status: "error", error: err.message });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const result = {
|
|
281
|
+
merkle_root: merkleRoot,
|
|
282
|
+
upgraded: upgraded.length,
|
|
283
|
+
pending: still_pending.length,
|
|
284
|
+
details: [...upgraded, ...still_pending],
|
|
285
|
+
checked_at: new Date().toISOString(),
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// If upgraded, save as Level 4 witness
|
|
289
|
+
if (upgraded.length > 0) {
|
|
290
|
+
const anchorWitness = {
|
|
291
|
+
type: "ots_confirmed",
|
|
292
|
+
level: 4,
|
|
293
|
+
original_hash: merkleRoot,
|
|
294
|
+
bitcoin_attestations: upgraded,
|
|
295
|
+
confirmed_at: new Date().toISOString(),
|
|
296
|
+
};
|
|
297
|
+
saveWitness(merkleRoot, anchorWitness);
|
|
298
|
+
result.new_level = 4;
|
|
299
|
+
result.message = "✓ Upgraded to Level 4 (Bitcoin anchored). Computationally undeletable.";
|
|
300
|
+
} else {
|
|
301
|
+
result.message = "Still pending Bitcoin confirmation. Try again later.";
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return result;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/* ================================================================
|
|
308
|
+
LEVEL 2: BILATERAL WITNESS
|
|
309
|
+
================================================================ */
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Generate a bilateral witness receipt for sharing with a counterparty.
|
|
313
|
+
* Both parties holding the same receipt means neither can deny the hash existed.
|
|
314
|
+
*
|
|
315
|
+
* @param {string} merkleRoot - The Merkle root to witness
|
|
316
|
+
* @param {string} counterparty - Identifier of the other party
|
|
317
|
+
* @returns {object} - Receipt to share
|
|
318
|
+
*/
|
|
319
|
+
export function createBilateralWitness(merkleRoot, counterparty) {
|
|
320
|
+
const receipt = {
|
|
321
|
+
type: "bilateral",
|
|
322
|
+
level: 2,
|
|
323
|
+
merkle_root: merkleRoot,
|
|
324
|
+
counterparty,
|
|
325
|
+
created_at: new Date().toISOString(),
|
|
326
|
+
// HMAC so counterparty can verify this was generated by FORGE
|
|
327
|
+
receipt_hash: createHash("sha256")
|
|
328
|
+
.update(`bilateral:${merkleRoot}:${counterparty}:${Date.now()}`)
|
|
329
|
+
.digest("hex"),
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
saveWitness(merkleRoot, receipt);
|
|
333
|
+
return receipt;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/* ================================================================
|
|
337
|
+
SUMMARY
|
|
338
|
+
================================================================ */
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Get a complete witness summary for a Merkle root.
|
|
342
|
+
*/
|
|
343
|
+
export function witnessSummary(merkleRoot) {
|
|
344
|
+
const witnesses = loadWitnesses(merkleRoot);
|
|
345
|
+
const level = witnessLevel(merkleRoot);
|
|
346
|
+
|
|
347
|
+
return {
|
|
348
|
+
merkle_root: merkleRoot,
|
|
349
|
+
current_level: level,
|
|
350
|
+
witness_count: witnesses.length,
|
|
351
|
+
witnesses: witnesses.map(w => ({
|
|
352
|
+
type: w.type,
|
|
353
|
+
level: w.level,
|
|
354
|
+
created_at: w.created_at || w.confirmed_at,
|
|
355
|
+
})),
|
|
356
|
+
upgrade_path: getUpgradePath(level.level),
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function getUpgradePath(currentLevel) {
|
|
361
|
+
const paths = [];
|
|
362
|
+
if (currentLevel < 2) {
|
|
363
|
+
paths.push("Level 2: Share Merkle root with counterparty via 'forge witness --bilateral <party>'");
|
|
364
|
+
}
|
|
365
|
+
if (currentLevel < 3) {
|
|
366
|
+
paths.push("Level 3: Submit to OTS calendars via 'forge anchor'");
|
|
367
|
+
}
|
|
368
|
+
if (currentLevel < 4) {
|
|
369
|
+
paths.push("Level 4: Wait for Bitcoin confirmation, then 'forge anchor --upgrade'");
|
|
370
|
+
}
|
|
371
|
+
if (currentLevel >= 4) {
|
|
372
|
+
paths.push("Maximum level reached. Hash is anchored to Bitcoin blockchain.");
|
|
373
|
+
}
|
|
374
|
+
return paths;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export { OTS_CALENDARS, WITNESS_DIR };
|