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