@orangecheck/stamp-ots 0.1.1 → 0.2.1
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/README.md +17 -19
- package/dist/index.d.mts +66 -4
- package/dist/index.d.ts +66 -4
- package/dist/index.js +373 -106
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +363 -107
- package/dist/index.mjs.map +1 -1
- package/package.json +60 -64
- package/src/anchor.test.ts +72 -0
- package/src/anchor.ts +78 -135
- package/src/calendar.test.ts +37 -0
- package/src/calendar.ts +8 -9
- package/src/index.ts +19 -2
- package/src/ots-fixture.test-helper.ts +24 -0
- package/src/ots.test.ts +34 -0
- package/src/ots.ts +320 -0
- package/src/submit.test.ts +0 -17
- package/src/types.ts +7 -6
- package/src/upgrade.test.ts +110 -0
- package/src/upgrade.ts +72 -67
package/dist/index.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var sha2 = require('@noble/hashes/sha2');
|
|
4
|
+
var legacy = require('@noble/hashes/legacy');
|
|
5
|
+
var sha3 = require('@noble/hashes/sha3');
|
|
6
|
+
|
|
3
7
|
// src/types.ts
|
|
4
8
|
function toStampOts(p) {
|
|
5
9
|
return {
|
|
@@ -117,7 +121,6 @@ function createCalendarClient(url, opts = {}) {
|
|
|
117
121
|
const ac = withTimeout(signal, timeoutMs);
|
|
118
122
|
const resp = await fetchImpl(`${base}/digest`, {
|
|
119
123
|
method: "POST",
|
|
120
|
-
headers: { "Content-Type": "application/vnd.opentimestamps.v1" },
|
|
121
124
|
body: digest,
|
|
122
125
|
signal: ac.signal
|
|
123
126
|
});
|
|
@@ -128,12 +131,12 @@ function createCalendarClient(url, opts = {}) {
|
|
|
128
131
|
const buf = await resp.arrayBuffer();
|
|
129
132
|
return new Uint8Array(buf);
|
|
130
133
|
},
|
|
131
|
-
async fetchProof(
|
|
132
|
-
if (
|
|
133
|
-
throw new Error("OTS:
|
|
134
|
+
async fetchProof(commitment, signal) {
|
|
135
|
+
if (commitment.byteLength === 0 || commitment.byteLength > 64) {
|
|
136
|
+
throw new Error("OTS: commitment must be 1-64 bytes");
|
|
134
137
|
}
|
|
135
138
|
const ac = withTimeout(signal, timeoutMs);
|
|
136
|
-
const resp = await fetchImpl(`${base}/timestamp/${hexEncode(
|
|
139
|
+
const resp = await fetchImpl(`${base}/timestamp/${hexEncode(commitment)}`, {
|
|
137
140
|
method: "GET",
|
|
138
141
|
headers: { Accept: "application/vnd.opentimestamps.v1" },
|
|
139
142
|
signal: ac.signal
|
|
@@ -202,71 +205,300 @@ async function submitToCalendars(idHex, opts = {}) {
|
|
|
202
205
|
upgradedAt: null
|
|
203
206
|
};
|
|
204
207
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
208
|
+
var HEADER_MAGIC = new Uint8Array([
|
|
209
|
+
0,
|
|
210
|
+
79,
|
|
211
|
+
112,
|
|
212
|
+
101,
|
|
213
|
+
110,
|
|
214
|
+
84,
|
|
215
|
+
105,
|
|
216
|
+
109,
|
|
217
|
+
101,
|
|
218
|
+
115,
|
|
219
|
+
116,
|
|
220
|
+
97,
|
|
221
|
+
109,
|
|
222
|
+
112,
|
|
223
|
+
115,
|
|
224
|
+
0,
|
|
225
|
+
0,
|
|
226
|
+
80,
|
|
227
|
+
114,
|
|
228
|
+
111,
|
|
229
|
+
111,
|
|
230
|
+
102,
|
|
231
|
+
0,
|
|
232
|
+
191,
|
|
233
|
+
137,
|
|
234
|
+
226,
|
|
235
|
+
232,
|
|
236
|
+
132,
|
|
237
|
+
232,
|
|
238
|
+
146,
|
|
239
|
+
148
|
|
240
|
+
]);
|
|
241
|
+
var TAG_BITCOIN = "0588960d73d71901";
|
|
242
|
+
var TAG_PENDING = "83dfe30d2ef90c8e";
|
|
243
|
+
var MAX_DEPTH = 256;
|
|
244
|
+
var MAX_MSG = 4096;
|
|
245
|
+
var Reader = class {
|
|
246
|
+
constructor(buf) {
|
|
247
|
+
this.buf = buf;
|
|
248
|
+
this.pos = 0;
|
|
215
249
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
};
|
|
250
|
+
byte() {
|
|
251
|
+
if (this.pos >= this.buf.length) throw new Error("ots: unexpected end of proof");
|
|
252
|
+
return this.buf[this.pos++];
|
|
253
|
+
}
|
|
254
|
+
bytes(n) {
|
|
255
|
+
if (n < 0 || this.pos + n > this.buf.length) throw new Error("ots: unexpected end of proof");
|
|
256
|
+
const out = this.buf.subarray(this.pos, this.pos + n);
|
|
257
|
+
this.pos += n;
|
|
258
|
+
return out;
|
|
259
|
+
}
|
|
260
|
+
varuint() {
|
|
261
|
+
let value = 0;
|
|
262
|
+
let shift = 0;
|
|
263
|
+
for (; ; ) {
|
|
264
|
+
const b = this.byte();
|
|
265
|
+
value += (b & 127) * 2 ** shift;
|
|
266
|
+
if (!(b & 128)) return value;
|
|
267
|
+
shift += 7;
|
|
268
|
+
if (shift > 49) throw new Error("ots: varuint too large");
|
|
236
269
|
}
|
|
237
270
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
if (
|
|
241
|
-
|
|
242
|
-
|
|
271
|
+
varbytes(max) {
|
|
272
|
+
const n = this.varuint();
|
|
273
|
+
if (n > max) throw new Error("ots: varbytes too long");
|
|
274
|
+
return this.bytes(n);
|
|
275
|
+
}
|
|
276
|
+
get done() {
|
|
277
|
+
return this.pos === this.buf.length;
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
var UNARY = {
|
|
281
|
+
2: "sha1",
|
|
282
|
+
3: "ripemd160",
|
|
283
|
+
8: "sha256",
|
|
284
|
+
103: "keccak256",
|
|
285
|
+
242: "reverse",
|
|
286
|
+
243: "hexlify"
|
|
287
|
+
};
|
|
288
|
+
function applyOp(op, msg) {
|
|
289
|
+
switch (op.kind) {
|
|
290
|
+
case "sha256":
|
|
291
|
+
return sha2.sha256(msg);
|
|
292
|
+
case "ripemd160":
|
|
293
|
+
return legacy.ripemd160(msg);
|
|
294
|
+
case "sha1":
|
|
295
|
+
return legacy.sha1(msg);
|
|
296
|
+
case "keccak256":
|
|
297
|
+
return sha3.keccak_256(msg);
|
|
298
|
+
case "reverse":
|
|
299
|
+
return new Uint8Array(msg).reverse();
|
|
300
|
+
case "hexlify":
|
|
301
|
+
return new TextEncoder().encode(hexEncode(msg));
|
|
302
|
+
case "append":
|
|
303
|
+
return concat(msg, op.arg);
|
|
304
|
+
case "prepend":
|
|
305
|
+
return concat(op.arg, msg);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
function readOp(r, tag) {
|
|
309
|
+
const unary = UNARY[tag];
|
|
310
|
+
if (unary) return { kind: unary };
|
|
311
|
+
if (tag === 240) return { kind: "append", arg: r.varbytes(MAX_MSG) };
|
|
312
|
+
if (tag === 241) return { kind: "prepend", arg: r.varbytes(MAX_MSG) };
|
|
313
|
+
throw new Error(`ots: unknown op tag 0x${tag.toString(16)}`);
|
|
314
|
+
}
|
|
315
|
+
function readAttestation(r) {
|
|
316
|
+
const tag = hexEncode(r.bytes(8));
|
|
317
|
+
const payload = r.varbytes(8192);
|
|
318
|
+
const p = new Reader(payload);
|
|
319
|
+
if (tag === TAG_BITCOIN) return { kind: "bitcoin", height: p.varuint() };
|
|
320
|
+
if (tag === TAG_PENDING) return { kind: "pending", uri: new TextDecoder().decode(p.varbytes(1e3)) };
|
|
321
|
+
return { kind: "unknown", tag, payload };
|
|
322
|
+
}
|
|
323
|
+
function readTimestamp(r, msg, depth) {
|
|
324
|
+
if (depth > MAX_DEPTH) throw new Error("ots: proof too deep");
|
|
325
|
+
const node = { msg, attestations: [], ops: [] };
|
|
326
|
+
const item = (tag2) => {
|
|
327
|
+
if (tag2 === 0) {
|
|
328
|
+
node.attestations.push(readAttestation(r));
|
|
329
|
+
return;
|
|
243
330
|
}
|
|
331
|
+
const op = readOp(r, tag2);
|
|
332
|
+
const next = applyOp(op, msg);
|
|
333
|
+
if (next.length > MAX_MSG) throw new Error("ots: message too long");
|
|
334
|
+
node.ops.push({ op, child: readTimestamp(r, next, depth + 1) });
|
|
335
|
+
};
|
|
336
|
+
let tag = r.byte();
|
|
337
|
+
while (tag === 255) {
|
|
338
|
+
item(r.byte());
|
|
339
|
+
tag = r.byte();
|
|
244
340
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
341
|
+
item(tag);
|
|
342
|
+
return node;
|
|
343
|
+
}
|
|
344
|
+
function parseTimestamp(bytes, digest) {
|
|
345
|
+
const r = new Reader(bytes);
|
|
346
|
+
const ts = readTimestamp(r, digest, 0);
|
|
347
|
+
if (!r.done) throw new Error("ots: trailing bytes after timestamp");
|
|
348
|
+
return ts;
|
|
349
|
+
}
|
|
350
|
+
function isDetached(bytes) {
|
|
351
|
+
return bytes.length > HEADER_MAGIC.length && bytesEqual(bytes.subarray(0, HEADER_MAGIC.length), HEADER_MAGIC);
|
|
352
|
+
}
|
|
353
|
+
function parseDetached(bytes) {
|
|
354
|
+
if (!isDetached(bytes)) throw new Error("ots: missing detached-file header");
|
|
355
|
+
const r = new Reader(bytes.subarray(HEADER_MAGIC.length));
|
|
356
|
+
const major = r.varuint();
|
|
357
|
+
if (major !== 1) throw new Error(`ots: unsupported file version ${major}`);
|
|
358
|
+
if (r.byte() !== 8) throw new Error("ots: only sha256 file digests are supported");
|
|
359
|
+
const digest = new Uint8Array(r.bytes(32));
|
|
360
|
+
const timestamp = readTimestamp(r, digest, 0);
|
|
361
|
+
if (!r.done) throw new Error("ots: trailing bytes after timestamp");
|
|
362
|
+
return { digest, timestamp };
|
|
363
|
+
}
|
|
364
|
+
function parseProof(bytes, digest) {
|
|
365
|
+
if (!isDetached(bytes)) return parseTimestamp(bytes, digest);
|
|
366
|
+
const d = parseDetached(bytes);
|
|
367
|
+
if (!bytesEqual(d.digest, digest)) throw new Error("ots: proof commits to a different digest");
|
|
368
|
+
return d.timestamp;
|
|
369
|
+
}
|
|
370
|
+
function attestations(ts) {
|
|
371
|
+
const out = [];
|
|
372
|
+
const walk = (n) => {
|
|
373
|
+
for (const a of n.attestations) out.push({ msg: n.msg, attestation: a });
|
|
374
|
+
for (const { child } of n.ops) walk(child);
|
|
375
|
+
};
|
|
376
|
+
walk(ts);
|
|
377
|
+
return out;
|
|
378
|
+
}
|
|
379
|
+
function bitcoinAnchors(ts) {
|
|
380
|
+
return attestations(ts).flatMap(
|
|
381
|
+
({ msg, attestation }) => attestation.kind === "bitcoin" ? [{ blockHeight: attestation.height, merkleRoot: msg }] : []
|
|
382
|
+
).sort((a, b) => a.blockHeight - b.blockHeight);
|
|
383
|
+
}
|
|
384
|
+
function pendingCommitments(ts) {
|
|
385
|
+
return attestations(ts).flatMap(
|
|
386
|
+
({ msg, attestation }) => attestation.kind === "pending" ? [{ uri: attestation.uri, commitment: msg }] : []
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
function mergeAt(ts, upgrade) {
|
|
390
|
+
if (bytesEqual(ts.msg, upgrade.msg)) {
|
|
391
|
+
for (const a of upgrade.attestations) {
|
|
392
|
+
if (!ts.attestations.some((b) => attestationKey(b) === attestationKey(a))) ts.attestations.push(a);
|
|
256
393
|
}
|
|
394
|
+
for (const { op, child } of upgrade.ops) {
|
|
395
|
+
const same = ts.ops.find((o) => opKey(o.op) === opKey(op));
|
|
396
|
+
if (same) mergeAt(same.child, child);
|
|
397
|
+
else ts.ops.push({ op, child });
|
|
398
|
+
}
|
|
399
|
+
return true;
|
|
257
400
|
}
|
|
258
|
-
return
|
|
401
|
+
return ts.ops.some(({ child }) => mergeAt(child, upgrade));
|
|
402
|
+
}
|
|
403
|
+
function writeVaruint(out, n) {
|
|
404
|
+
do {
|
|
405
|
+
let b = n % 128;
|
|
406
|
+
n = Math.floor(n / 128);
|
|
407
|
+
if (n > 0) b |= 128;
|
|
408
|
+
out.push(b);
|
|
409
|
+
} while (n > 0);
|
|
410
|
+
}
|
|
411
|
+
function writeVarbytes(out, bytes) {
|
|
412
|
+
writeVaruint(out, bytes.length);
|
|
413
|
+
out.push(...bytes);
|
|
414
|
+
}
|
|
415
|
+
var OP_TAG = {
|
|
416
|
+
sha1: 2,
|
|
417
|
+
ripemd160: 3,
|
|
418
|
+
sha256: 8,
|
|
419
|
+
keccak256: 103,
|
|
420
|
+
append: 240,
|
|
421
|
+
prepend: 241,
|
|
422
|
+
reverse: 242,
|
|
423
|
+
hexlify: 243
|
|
424
|
+
};
|
|
425
|
+
function writeAttestation(out, a) {
|
|
426
|
+
const payload = [];
|
|
427
|
+
let tag;
|
|
428
|
+
if (a.kind === "bitcoin") {
|
|
429
|
+
tag = TAG_BITCOIN;
|
|
430
|
+
writeVaruint(payload, a.height);
|
|
431
|
+
} else if (a.kind === "pending") {
|
|
432
|
+
tag = TAG_PENDING;
|
|
433
|
+
writeVarbytes(payload, new TextEncoder().encode(a.uri));
|
|
434
|
+
} else {
|
|
435
|
+
tag = a.tag;
|
|
436
|
+
payload.push(...a.payload);
|
|
437
|
+
}
|
|
438
|
+
out.push(0);
|
|
439
|
+
for (let i = 0; i < 16; i += 2) out.push(parseInt(tag.slice(i, i + 2), 16));
|
|
440
|
+
writeVarbytes(out, new Uint8Array(payload));
|
|
441
|
+
}
|
|
442
|
+
function writeTimestamp(out, ts) {
|
|
443
|
+
const items = [
|
|
444
|
+
...ts.attestations.map((a) => () => writeAttestation(out, a)),
|
|
445
|
+
...ts.ops.map(({ op, child }) => () => {
|
|
446
|
+
out.push(OP_TAG[op.kind]);
|
|
447
|
+
if (op.kind === "append" || op.kind === "prepend") writeVarbytes(out, op.arg);
|
|
448
|
+
writeTimestamp(out, child);
|
|
449
|
+
})
|
|
450
|
+
];
|
|
451
|
+
if (items.length === 0) throw new Error("ots: cannot serialize an empty timestamp node");
|
|
452
|
+
items.forEach((write, i) => {
|
|
453
|
+
if (i < items.length - 1) out.push(255);
|
|
454
|
+
write();
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
function serializeTimestamp(ts) {
|
|
458
|
+
const out = [];
|
|
459
|
+
writeTimestamp(out, ts);
|
|
460
|
+
return new Uint8Array(out);
|
|
461
|
+
}
|
|
462
|
+
function attestationKey(a) {
|
|
463
|
+
if (a.kind === "bitcoin") return `b:${a.height}`;
|
|
464
|
+
if (a.kind === "pending") return `p:${a.uri}`;
|
|
465
|
+
return `u:${a.tag}:${hexEncode(a.payload)}`;
|
|
466
|
+
}
|
|
467
|
+
function opKey(op) {
|
|
468
|
+
return "arg" in op ? `${op.kind}:${hexEncode(op.arg)}` : op.kind;
|
|
259
469
|
}
|
|
260
|
-
function
|
|
261
|
-
const out =
|
|
262
|
-
|
|
470
|
+
function concat(a, b) {
|
|
471
|
+
const out = new Uint8Array(a.length + b.length);
|
|
472
|
+
out.set(a, 0);
|
|
473
|
+
out.set(b, a.length);
|
|
263
474
|
return out;
|
|
264
475
|
}
|
|
476
|
+
function bytesEqual(a, b) {
|
|
477
|
+
if (a.byteLength !== b.byteLength) return false;
|
|
478
|
+
for (let i = 0; i < a.byteLength; i++) if (a[i] !== b[i]) return false;
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
265
481
|
|
|
266
482
|
// src/anchor.ts
|
|
483
|
+
async function walkOtsProof(input) {
|
|
484
|
+
try {
|
|
485
|
+
const anchors = bitcoinAnchors(parseProof(input.proofBytes, input.digest));
|
|
486
|
+
return anchors.find((a) => a.blockHeight === input.blockHeight) ?? anchors[0] ?? null;
|
|
487
|
+
} catch {
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
function blockHashOf(header) {
|
|
492
|
+
return hexEncode(sha2.sha256(sha2.sha256(header)).reverse());
|
|
493
|
+
}
|
|
267
494
|
function makeAnchorVerifier(cfg) {
|
|
495
|
+
const walkProof = cfg.walkProof ?? walkOtsProof;
|
|
268
496
|
const verifier = async (input) => {
|
|
269
|
-
const walked = await
|
|
497
|
+
const walked = await walkProof({
|
|
498
|
+
proofBytes: input.proofBytes,
|
|
499
|
+
digest: input.digest,
|
|
500
|
+
blockHeight: input.blockHeight
|
|
501
|
+
});
|
|
270
502
|
if (!walked) {
|
|
271
503
|
return { ok: false, reason: "proof could not be walked to a Bitcoin commitment" };
|
|
272
504
|
}
|
|
@@ -277,87 +509,122 @@ function makeAnchorVerifier(cfg) {
|
|
|
277
509
|
};
|
|
278
510
|
}
|
|
279
511
|
const header = await cfg.headerSource.getHeaderAt(input.blockHeight);
|
|
280
|
-
if (!header) {
|
|
512
|
+
if (!header || header.byteLength !== 80) {
|
|
281
513
|
return { ok: false, reason: `no block header available at height ${input.blockHeight}` };
|
|
282
514
|
}
|
|
283
|
-
if (header
|
|
284
|
-
return { ok: false, reason: "
|
|
515
|
+
if (blockHashOf(header) !== input.blockHash.toLowerCase()) {
|
|
516
|
+
return { ok: false, reason: "block header does not hash to the declared block hash" };
|
|
285
517
|
}
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
const matchesLE = bytesEqual(headerMerkleLE, walked.merkleRoot);
|
|
289
|
-
const matchesBE = bytesEqual(headerMerkleBE, walked.merkleRoot);
|
|
290
|
-
if (!matchesLE && !matchesBE) {
|
|
518
|
+
const headerMerkle = header.subarray(36, 68);
|
|
519
|
+
if (!bytesEqual(headerMerkle, walked.merkleRoot)) {
|
|
291
520
|
return { ok: false, reason: "block header Merkle root does not match walked proof root" };
|
|
292
521
|
}
|
|
293
|
-
return { ok: true, merkleRoot: hexEncode(
|
|
522
|
+
return { ok: true, merkleRoot: hexEncode(headerMerkle) };
|
|
294
523
|
};
|
|
295
524
|
return async (proofB64, blockHeight, blockHash, envelopeId) => {
|
|
296
525
|
const r = await verifier({
|
|
297
526
|
proofBytes: base64Decode(proofB64),
|
|
298
527
|
digest: hexDecode(envelopeId),
|
|
299
|
-
blockHeight
|
|
528
|
+
blockHeight,
|
|
529
|
+
blockHash
|
|
530
|
+
});
|
|
300
531
|
return r.ok;
|
|
301
532
|
};
|
|
302
533
|
}
|
|
303
534
|
async function makeDefaultAnchorVerifier(opts) {
|
|
304
|
-
|
|
305
|
-
return makeAnchorVerifier({ walkProof, headerSource: opts.headerSource });
|
|
535
|
+
return makeAnchorVerifier({ headerSource: opts.headerSource });
|
|
306
536
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const modAny = mod;
|
|
321
|
-
const DetachedCtor = modAny.DetachedTimestampFile ?? modAny.default?.DetachedTimestampFile;
|
|
322
|
-
if (!DetachedCtor || typeof DetachedCtor.deserialize !== "function") {
|
|
323
|
-
throw new Error(
|
|
324
|
-
"@orangecheck/stamp-ots: unexpected shape from `opentimestamps` package \u2014 expected DetachedTimestampFile.deserialize. If you are using a non-standard build, pass your own walkProof via makeAnchorVerifier instead."
|
|
325
|
-
);
|
|
326
|
-
}
|
|
327
|
-
return async ({ proofBytes }) => {
|
|
328
|
-
try {
|
|
329
|
-
const detached = DetachedCtor.deserialize(proofBytes);
|
|
330
|
-
const ts = detached.timestamp;
|
|
331
|
-
const attestations = await ts.verify();
|
|
332
|
-
const first = attestations.values().next();
|
|
333
|
-
if (first.done) return null;
|
|
334
|
-
const { height, timestamp } = first.value;
|
|
335
|
-
if (typeof height !== "number" || !timestamp) return null;
|
|
336
|
-
const merkleRoot = timestamp.msg;
|
|
337
|
-
if (!merkleRoot) return null;
|
|
338
|
-
return { merkleRoot, blockHeight: height };
|
|
339
|
-
} catch {
|
|
340
|
-
return null;
|
|
537
|
+
function mempoolHeaderSource(opts = {}) {
|
|
538
|
+
const base = (opts.baseUrl ?? "https://mempool.space/api").replace(/\/+$/, "");
|
|
539
|
+
const fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);
|
|
540
|
+
return {
|
|
541
|
+
async getHeaderAt(height) {
|
|
542
|
+
const h = await fetchImpl(`${base}/block-height/${height}`);
|
|
543
|
+
if (!h.ok) return null;
|
|
544
|
+
const hash = (await h.text()).trim();
|
|
545
|
+
if (!/^[0-9a-f]{64}$/.test(hash)) return null;
|
|
546
|
+
const r = await fetchImpl(`${base}/block/${hash}/header`);
|
|
547
|
+
if (!r.ok) return null;
|
|
548
|
+
const hex = (await r.text()).trim();
|
|
549
|
+
return /^[0-9a-f]{160}$/.test(hex) ? hexDecode(hex) : null;
|
|
341
550
|
}
|
|
342
551
|
};
|
|
343
552
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
553
|
+
|
|
554
|
+
// src/upgrade.ts
|
|
555
|
+
var norm = (u) => u.replace(/\/+$/, "");
|
|
556
|
+
async function upgradeProof(current, idHex, opts) {
|
|
557
|
+
if (current.status === "confirmed") return current;
|
|
558
|
+
if (!opts.headerSource && !opts.parseAnchor) {
|
|
559
|
+
throw new Error("upgradeProof: pass headerSource or parseAnchor");
|
|
560
|
+
}
|
|
561
|
+
const before = base64Decode(current.proof);
|
|
562
|
+
const tree = parseProof(before, hexDecode(idHex));
|
|
563
|
+
const listed = new Set(current.calendars.map(norm));
|
|
564
|
+
const pending = pendingCommitments(tree).filter((p) => listed.has(norm(p.uri)));
|
|
565
|
+
if (pending.length === 0 && bitcoinAnchors(tree).length === 0) {
|
|
566
|
+
throw new Error("upgradeProof: proof has no pending attestation from a listed calendar");
|
|
567
|
+
}
|
|
568
|
+
const answered = [];
|
|
569
|
+
await Promise.allSettled(
|
|
570
|
+
pending.map(async ({ uri, commitment }) => {
|
|
571
|
+
const client = createCalendarClient(uri, { fetch: opts.fetch, timeoutMs: opts.timeoutMs });
|
|
572
|
+
const bytes = await client.fetchProof(commitment, opts.signal);
|
|
573
|
+
if (!bytes) return;
|
|
574
|
+
if (mergeAt(tree, parseTimestamp(bytes, commitment))) answered.push(norm(uri));
|
|
575
|
+
})
|
|
576
|
+
);
|
|
577
|
+
const merged = serializeTimestamp(tree);
|
|
578
|
+
const anchor = await resolveAnchor(merged, tree, opts);
|
|
579
|
+
const calendars = [...answered, ...current.calendars.filter((c) => !answered.includes(norm(c)))];
|
|
580
|
+
if (anchor) {
|
|
581
|
+
return {
|
|
582
|
+
status: "confirmed",
|
|
583
|
+
proof: base64Encode(merged),
|
|
584
|
+
calendars,
|
|
585
|
+
blockHeight: anchor.blockHeight,
|
|
586
|
+
blockHash: anchor.blockHash,
|
|
587
|
+
upgradedAt: (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "Z")
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
if (!bytesEqual(merged, before)) {
|
|
591
|
+
return { ...current, proof: base64Encode(merged), calendars };
|
|
592
|
+
}
|
|
593
|
+
return current;
|
|
594
|
+
}
|
|
595
|
+
async function resolveAnchor(merged, tree, opts) {
|
|
596
|
+
if (opts.parseAnchor) return opts.parseAnchor(merged);
|
|
597
|
+
for (const a of bitcoinAnchors(tree)) {
|
|
598
|
+
const header = await opts.headerSource.getHeaderAt(a.blockHeight);
|
|
599
|
+
if (header && header.byteLength === 80 && bytesEqual(header.subarray(36, 68), a.merkleRoot)) {
|
|
600
|
+
return { blockHeight: a.blockHeight, blockHash: blockHashOf(header) };
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return null;
|
|
348
604
|
}
|
|
349
605
|
|
|
350
606
|
exports.DEFAULT_CALENDARS = DEFAULT_CALENDARS;
|
|
607
|
+
exports.attestations = attestations;
|
|
351
608
|
exports.base64Decode = base64Decode;
|
|
352
609
|
exports.base64Encode = base64Encode;
|
|
610
|
+
exports.bitcoinAnchors = bitcoinAnchors;
|
|
611
|
+
exports.blockHashOf = blockHashOf;
|
|
353
612
|
exports.createCalendarClient = createCalendarClient;
|
|
354
613
|
exports.fromStampOts = fromStampOts;
|
|
355
614
|
exports.hexDecode = hexDecode;
|
|
356
615
|
exports.hexEncode = hexEncode;
|
|
357
616
|
exports.makeAnchorVerifier = makeAnchorVerifier;
|
|
358
617
|
exports.makeDefaultAnchorVerifier = makeDefaultAnchorVerifier;
|
|
618
|
+
exports.mempoolHeaderSource = mempoolHeaderSource;
|
|
619
|
+
exports.mergeAt = mergeAt;
|
|
620
|
+
exports.parseDetached = parseDetached;
|
|
621
|
+
exports.parseProof = parseProof;
|
|
622
|
+
exports.parseTimestamp = parseTimestamp;
|
|
623
|
+
exports.pendingCommitments = pendingCommitments;
|
|
624
|
+
exports.serializeTimestamp = serializeTimestamp;
|
|
359
625
|
exports.submitToCalendars = submitToCalendars;
|
|
360
626
|
exports.toStampOts = toStampOts;
|
|
361
627
|
exports.upgradeProof = upgradeProof;
|
|
628
|
+
exports.walkOtsProof = walkOtsProof;
|
|
362
629
|
//# sourceMappingURL=index.js.map
|
|
363
630
|
//# sourceMappingURL=index.js.map
|