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