@otskit/client 0.2.0 → 0.4.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/README.md +50 -13
- package/dist/index.cjs +335 -1563
- package/dist/index.d.cts +89 -18
- package/dist/index.d.ts +89 -18
- package/dist/index.js +332 -1552
- package/package.json +4 -3
package/dist/index.cjs
CHANGED
|
@@ -29,7 +29,7 @@ __export(index_exports, {
|
|
|
29
29
|
DEFAULT_CALENDARS: () => DEFAULT_CALENDARS,
|
|
30
30
|
DEFAULT_CALENDAR_WHITELIST: () => DEFAULT_CALENDAR_WHITELIST,
|
|
31
31
|
DEFAULT_RESILIENCE: () => DEFAULT_RESILIENCE,
|
|
32
|
-
DetachedTimestampFile: () => DetachedTimestampFile,
|
|
32
|
+
DetachedTimestampFile: () => import_core5.DetachedTimestampFile,
|
|
33
33
|
EsploraClient: () => EsploraClient,
|
|
34
34
|
EsploraResponseError: () => EsploraResponseError,
|
|
35
35
|
MAX_CALENDAR_RESPONSE_SIZE: () => MAX_CALENDAR_RESPONSE_SIZE,
|
|
@@ -39,25 +39,25 @@ __export(index_exports, {
|
|
|
39
39
|
OpenTimestampsClientError: () => OpenTimestampsClientError,
|
|
40
40
|
PUBLIC_ESPLORA_URL: () => PUBLIC_ESPLORA_URL,
|
|
41
41
|
ResilientNetworkLayer: () => ResilientNetworkLayer,
|
|
42
|
+
SizeLimitExceededError: () => SizeLimitExceededError,
|
|
42
43
|
StampError: () => StampError,
|
|
43
|
-
Timestamp: () => Timestamp,
|
|
44
|
+
Timestamp: () => import_core5.Timestamp,
|
|
44
45
|
UpgradeError: () => UpgradeError,
|
|
45
46
|
UrlWhitelist: () => UrlWhitelist,
|
|
46
47
|
ValidationError: () => ValidationError,
|
|
48
|
+
assertSafeCalendarUrl: () => assertSafeCalendarUrl,
|
|
47
49
|
hashBuffer: () => hashBuffer,
|
|
48
50
|
hashFile: () => hashFile,
|
|
49
|
-
|
|
51
|
+
isVerified: () => isVerified,
|
|
52
|
+
verifyAgainstBlockheader: () => import_core6.verifyAgainstBlockheader,
|
|
50
53
|
verifyTimestampAttestation: () => verifyTimestampAttestation
|
|
51
54
|
});
|
|
52
55
|
module.exports = __toCommonJS(index_exports);
|
|
53
56
|
|
|
54
57
|
// src/types.ts
|
|
55
|
-
var
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
"https://finney.calendar.eternitywall.com",
|
|
59
|
-
"https://btc.calendar.catallaxy.com"
|
|
60
|
-
];
|
|
58
|
+
var import_core = require("@otskit/core");
|
|
59
|
+
var isVerified = (r) => r.status === "verified";
|
|
60
|
+
var DEFAULT_CALENDARS = [...import_core.DEFAULT_CALENDAR_URLS];
|
|
61
61
|
var DEFAULT_RESILIENCE = {
|
|
62
62
|
totalTimeoutMs: 3e4,
|
|
63
63
|
connectTimeoutMs: 5e3,
|
|
@@ -121,6 +121,18 @@ var CalendarResponseTooLargeError = class extends NetworkError {
|
|
|
121
121
|
};
|
|
122
122
|
var EsploraResponseError = class extends NetworkError {
|
|
123
123
|
};
|
|
124
|
+
var SizeLimitExceededError = class extends NetworkError {
|
|
125
|
+
maxBytes;
|
|
126
|
+
actualBytes;
|
|
127
|
+
constructor(maxBytes, actualBytes, options) {
|
|
128
|
+
super(
|
|
129
|
+
actualBytes === void 0 ? `Response size exceeds limit of ${maxBytes} bytes` : `Response size ${actualBytes} bytes exceeds limit of ${maxBytes} bytes`,
|
|
130
|
+
options
|
|
131
|
+
);
|
|
132
|
+
this.maxBytes = maxBytes;
|
|
133
|
+
this.actualBytes = actualBytes;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
124
136
|
|
|
125
137
|
// src/network/circuit-breaker.ts
|
|
126
138
|
var CircuitState = /* @__PURE__ */ ((CircuitState2) => {
|
|
@@ -309,33 +321,62 @@ async function withRetry(fn, options, logger, signal) {
|
|
|
309
321
|
}
|
|
310
322
|
|
|
311
323
|
// src/adapters/fetch-adapter.ts
|
|
312
|
-
|
|
324
|
+
function getDeclaredContentLength(response) {
|
|
325
|
+
const value = response.headers.get("content-length");
|
|
326
|
+
if (value === null || !/^\d+$/.test(value)) return void 0;
|
|
327
|
+
const n = Number(value);
|
|
328
|
+
return Number.isSafeInteger(n) ? n : void 0;
|
|
329
|
+
}
|
|
330
|
+
async function readStreamLimited(body, maxBytes, status) {
|
|
331
|
+
const reader = body.getReader();
|
|
332
|
+
const buffer = new Uint8Array(maxBytes);
|
|
333
|
+
let received = 0;
|
|
334
|
+
try {
|
|
335
|
+
while (true) {
|
|
336
|
+
const { done, value } = await reader.read();
|
|
337
|
+
if (done) return buffer.subarray(0, received);
|
|
338
|
+
const next = received + value.byteLength;
|
|
339
|
+
if (next > maxBytes) {
|
|
340
|
+
await reader.cancel();
|
|
341
|
+
throw new SizeLimitExceededError(maxBytes, next, { status });
|
|
342
|
+
}
|
|
343
|
+
buffer.set(value, received);
|
|
344
|
+
received = next;
|
|
345
|
+
}
|
|
346
|
+
} finally {
|
|
347
|
+
reader.releaseLock();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
async function readResponseBody(response, maxBytes) {
|
|
351
|
+
const contentLength = getDeclaredContentLength(response);
|
|
352
|
+
if (contentLength !== void 0 && contentLength > maxBytes) {
|
|
353
|
+
throw new SizeLimitExceededError(maxBytes, contentLength, { status: response.status });
|
|
354
|
+
}
|
|
355
|
+
if (response.body === null) {
|
|
356
|
+
const ab = await response.arrayBuffer();
|
|
357
|
+
if (ab.byteLength > maxBytes) {
|
|
358
|
+
throw new SizeLimitExceededError(maxBytes, ab.byteLength, { status: response.status });
|
|
359
|
+
}
|
|
360
|
+
return new Uint8Array(ab);
|
|
361
|
+
}
|
|
362
|
+
return readStreamLimited(response.body, maxBytes, response.status);
|
|
363
|
+
}
|
|
364
|
+
async function executeRequest(request, maxBytes) {
|
|
313
365
|
try {
|
|
314
366
|
const response = await globalThis.fetch(request.url, {
|
|
315
367
|
method: request.method,
|
|
316
|
-
headers: {
|
|
317
|
-
"Content-Type": "application/octet-stream",
|
|
318
|
-
...request.headers
|
|
319
|
-
},
|
|
368
|
+
headers: { "Content-Type": "application/octet-stream", ...request.headers },
|
|
320
369
|
body: request.body,
|
|
321
370
|
signal: request.signal
|
|
322
371
|
});
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
return {
|
|
326
|
-
ok: response.ok,
|
|
327
|
-
status: response.status,
|
|
328
|
-
statusText: response.statusText,
|
|
329
|
-
data
|
|
330
|
-
};
|
|
372
|
+
const data = await readResponseBody(response, maxBytes);
|
|
373
|
+
return { ok: response.ok, status: response.status, statusText: response.statusText, data };
|
|
331
374
|
} catch (error) {
|
|
375
|
+
if (error instanceof SizeLimitExceededError) throw error;
|
|
376
|
+
if (error instanceof NetworkError) throw error;
|
|
332
377
|
if (error instanceof Error) {
|
|
333
|
-
if (error.name === "AbortError") {
|
|
334
|
-
|
|
335
|
-
}
|
|
336
|
-
if (error.message.includes("timeout")) {
|
|
337
|
-
throw new NetworkError("Request timeout", { cause: error });
|
|
338
|
-
}
|
|
378
|
+
if (error.name === "AbortError") throw new NetworkError("Request aborted", { cause: error });
|
|
379
|
+
if (error.message.includes("timeout")) throw new NetworkError("Request timeout", { cause: error });
|
|
339
380
|
throw new NetworkError(`Network request failed: ${error.message}`, { cause: error });
|
|
340
381
|
}
|
|
341
382
|
throw new NetworkError("Unknown network error");
|
|
@@ -343,23 +384,24 @@ async function executeRequest(request) {
|
|
|
343
384
|
}
|
|
344
385
|
function createTimeoutController(timeoutMs, parentSignal) {
|
|
345
386
|
const controller = new AbortController();
|
|
346
|
-
const timeout = setTimeout(() =>
|
|
347
|
-
|
|
348
|
-
|
|
387
|
+
const timeout = setTimeout(() => controller.abort(new Error("Timeout")), timeoutMs);
|
|
388
|
+
const onParentAbort = () => {
|
|
389
|
+
clearTimeout(timeout);
|
|
390
|
+
parentSignal?.removeEventListener("abort", onParentAbort);
|
|
391
|
+
controller.abort(parentSignal?.reason);
|
|
392
|
+
};
|
|
393
|
+
controller.signal.addEventListener("abort", () => {
|
|
394
|
+
clearTimeout(timeout);
|
|
395
|
+
parentSignal?.removeEventListener("abort", onParentAbort);
|
|
396
|
+
}, { once: true });
|
|
349
397
|
if (parentSignal) {
|
|
350
398
|
if (parentSignal.aborted) {
|
|
351
399
|
clearTimeout(timeout);
|
|
352
400
|
controller.abort(parentSignal.reason);
|
|
353
401
|
} else {
|
|
354
|
-
parentSignal.addEventListener("abort",
|
|
355
|
-
clearTimeout(timeout);
|
|
356
|
-
controller.abort(parentSignal.reason);
|
|
357
|
-
});
|
|
402
|
+
parentSignal.addEventListener("abort", onParentAbort, { once: true });
|
|
358
403
|
}
|
|
359
404
|
}
|
|
360
|
-
controller.signal.addEventListener("abort", () => {
|
|
361
|
-
clearTimeout(timeout);
|
|
362
|
-
});
|
|
363
405
|
return controller;
|
|
364
406
|
}
|
|
365
407
|
|
|
@@ -391,10 +433,10 @@ var ResilientNetworkLayer = class {
|
|
|
391
433
|
totalController.signal
|
|
392
434
|
);
|
|
393
435
|
try {
|
|
394
|
-
const response = await executeRequest(
|
|
395
|
-
...request,
|
|
396
|
-
|
|
397
|
-
|
|
436
|
+
const response = await executeRequest(
|
|
437
|
+
{ ...request, signal: attemptController.signal },
|
|
438
|
+
this.options.maxResponseBytes ?? 1e5
|
|
439
|
+
);
|
|
398
440
|
const elapsed = Date.now() - startTime;
|
|
399
441
|
this.logger?.debug(`Request to ${calendarUrl} succeeded in ${elapsed}ms`);
|
|
400
442
|
if (!response.ok) {
|
|
@@ -413,8 +455,7 @@ var ResilientNetworkLayer = class {
|
|
|
413
455
|
}
|
|
414
456
|
return response;
|
|
415
457
|
} finally {
|
|
416
|
-
attemptController.
|
|
417
|
-
});
|
|
458
|
+
attemptController.abort(new Error("Attempt complete"));
|
|
418
459
|
}
|
|
419
460
|
},
|
|
420
461
|
this.options.retries,
|
|
@@ -427,8 +468,7 @@ var ResilientNetworkLayer = class {
|
|
|
427
468
|
this.logger?.error(`Request to ${calendarUrl} failed after ${elapsed}ms`, error);
|
|
428
469
|
throw error;
|
|
429
470
|
} finally {
|
|
430
|
-
totalController.
|
|
431
|
-
});
|
|
471
|
+
totalController.abort(new Error("Request complete"));
|
|
432
472
|
}
|
|
433
473
|
}
|
|
434
474
|
/** Get circuit breaker state for a calendar */
|
|
@@ -445,1453 +485,11 @@ var ResilientNetworkLayer = class {
|
|
|
445
485
|
}
|
|
446
486
|
};
|
|
447
487
|
|
|
448
|
-
//
|
|
449
|
-
var
|
|
450
|
-
constructor(message) {
|
|
451
|
-
super(message);
|
|
452
|
-
this.name = new.target.name;
|
|
453
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
454
|
-
}
|
|
455
|
-
};
|
|
456
|
-
var BadMagicError = class extends DeserializationError {
|
|
457
|
-
};
|
|
458
|
-
var TruncatedStreamError = class extends DeserializationError {
|
|
459
|
-
};
|
|
460
|
-
var OversizedDataError = class extends DeserializationError {
|
|
461
|
-
};
|
|
462
|
-
var VaruintOverflowError = class extends DeserializationError {
|
|
463
|
-
};
|
|
464
|
-
var TrailingGarbageError = class extends DeserializationError {
|
|
465
|
-
};
|
|
466
|
-
var UnknownOperationError = class extends DeserializationError {
|
|
467
|
-
};
|
|
468
|
-
var OpExecutionError = class extends Error {
|
|
469
|
-
constructor(message) {
|
|
470
|
-
super(message);
|
|
471
|
-
this.name = new.target.name;
|
|
472
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
473
|
-
}
|
|
474
|
-
};
|
|
475
|
-
var MessageTooLongError = class extends OpExecutionError {
|
|
476
|
-
};
|
|
477
|
-
var ResultTooLongError = class extends OpExecutionError {
|
|
478
|
-
};
|
|
479
|
-
var InvalidUriError = class extends DeserializationError {
|
|
480
|
-
};
|
|
481
|
-
var VerificationError = class extends Error {
|
|
482
|
-
constructor(message) {
|
|
483
|
-
super(message);
|
|
484
|
-
this.name = new.target.name;
|
|
485
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
486
|
-
}
|
|
487
|
-
};
|
|
488
|
-
var EmptyTimestampError = class extends Error {
|
|
489
|
-
constructor(message) {
|
|
490
|
-
super(message);
|
|
491
|
-
this.name = new.target.name;
|
|
492
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
493
|
-
}
|
|
494
|
-
};
|
|
495
|
-
var MergeError = class extends Error {
|
|
496
|
-
constructor(message) {
|
|
497
|
-
super(message);
|
|
498
|
-
this.name = new.target.name;
|
|
499
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
500
|
-
}
|
|
501
|
-
};
|
|
502
|
-
var EmptyMerkleTreeError = class extends Error {
|
|
503
|
-
constructor(message) {
|
|
504
|
-
super(message);
|
|
505
|
-
this.name = new.target.name;
|
|
506
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
507
|
-
}
|
|
508
|
-
};
|
|
509
|
-
var UnsupportedVersionError = class extends DeserializationError {
|
|
510
|
-
};
|
|
511
|
-
var HEX_RE = /^[0-9a-fA-F]*$/;
|
|
512
|
-
function hexToBytes(hex) {
|
|
513
|
-
if (hex.length % 2 !== 0) {
|
|
514
|
-
throw new Error(`hex string must have even length; got ${hex.length}`);
|
|
515
|
-
}
|
|
516
|
-
if (!HEX_RE.test(hex)) {
|
|
517
|
-
throw new Error("hex string contains non-hex characters");
|
|
518
|
-
}
|
|
519
|
-
const out = new Uint8Array(hex.length / 2);
|
|
520
|
-
for (let i = 0; i < out.length; i++) {
|
|
521
|
-
out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
522
|
-
}
|
|
523
|
-
return out;
|
|
524
|
-
}
|
|
525
|
-
var HEX_TABLE = Array.from({ length: 256 }, (_, b) => b.toString(16).padStart(2, "0"));
|
|
526
|
-
function bytesToHex(bytes) {
|
|
527
|
-
let s = "";
|
|
528
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
529
|
-
s += HEX_TABLE[bytes[i]];
|
|
530
|
-
}
|
|
531
|
-
return s;
|
|
532
|
-
}
|
|
533
|
-
var encoder = new TextEncoder();
|
|
534
|
-
var decoder = new TextDecoder("utf-8", { fatal: true });
|
|
535
|
-
function textToBytes(text) {
|
|
536
|
-
return encoder.encode(text);
|
|
537
|
-
}
|
|
538
|
-
function bytesEqual(a, b) {
|
|
539
|
-
if (a.length !== b.length) return false;
|
|
540
|
-
for (let i = 0; i < a.length; i++) {
|
|
541
|
-
if (a[i] !== b[i]) return false;
|
|
542
|
-
}
|
|
543
|
-
return true;
|
|
544
|
-
}
|
|
545
|
-
function compareBytes(a, b) {
|
|
546
|
-
const min = Math.min(a.length, b.length);
|
|
547
|
-
for (let i = 0; i < min; i++) {
|
|
548
|
-
const d = a[i] - b[i];
|
|
549
|
-
if (d !== 0) return d;
|
|
550
|
-
}
|
|
551
|
-
return a.length - b.length;
|
|
552
|
-
}
|
|
553
|
-
var StreamDeserializationContext = class {
|
|
554
|
-
#buffer;
|
|
555
|
-
#counter = 0;
|
|
556
|
-
constructor(stream) {
|
|
557
|
-
if (!(stream instanceof Uint8Array)) {
|
|
558
|
-
throw new TypeError("StreamDeserializationContext expects a Uint8Array");
|
|
559
|
-
}
|
|
560
|
-
this.#buffer = stream;
|
|
561
|
-
}
|
|
562
|
-
get counter() {
|
|
563
|
-
return this.#counter;
|
|
564
|
-
}
|
|
565
|
-
/** Lee `length` bytes. Lanza si el stream no tiene suficientes. */
|
|
566
|
-
read(length) {
|
|
567
|
-
if (length < 0) throw new RangeError("read length must be >= 0");
|
|
568
|
-
if (this.#counter + length > this.#buffer.length) {
|
|
569
|
-
throw new TruncatedStreamError(
|
|
570
|
-
`attempted to read ${length} bytes at offset ${this.#counter}, only ${this.#buffer.length - this.#counter} available`
|
|
571
|
-
);
|
|
572
|
-
}
|
|
573
|
-
const slice = this.#buffer.subarray(this.#counter, this.#counter + length);
|
|
574
|
-
this.#counter += length;
|
|
575
|
-
return slice;
|
|
576
|
-
}
|
|
577
|
-
/** Lee un único byte. */
|
|
578
|
-
readByte() {
|
|
579
|
-
return this.read(1)[0];
|
|
580
|
-
}
|
|
581
|
-
/** Varuint LEB128: 7 bits por byte, bit 7 = continuación. */
|
|
582
|
-
readVaruint() {
|
|
583
|
-
let value = 0;
|
|
584
|
-
let shift = 0;
|
|
585
|
-
let byte;
|
|
586
|
-
do {
|
|
587
|
-
if (shift > 56) {
|
|
588
|
-
throw new VaruintOverflowError("varuint exceeds Number.MAX_SAFE_INTEGER");
|
|
589
|
-
}
|
|
590
|
-
byte = this.readByte();
|
|
591
|
-
value += (byte & 127) * 2 ** shift;
|
|
592
|
-
if (!Number.isSafeInteger(value)) {
|
|
593
|
-
throw new VaruintOverflowError("varuint exceeds Number.MAX_SAFE_INTEGER");
|
|
594
|
-
}
|
|
595
|
-
shift += 7;
|
|
596
|
-
} while (byte & 128);
|
|
597
|
-
return value;
|
|
598
|
-
}
|
|
599
|
-
/** Lee un bloque varbytes. `maxLen` es obligatorio (defensa DoS). */
|
|
600
|
-
readVarbytes(maxLen, minLen = 0) {
|
|
601
|
-
const length = this.readVaruint();
|
|
602
|
-
if (length > maxLen) {
|
|
603
|
-
throw new OversizedDataError(`varbytes length ${length} exceeds maxLen ${maxLen}`);
|
|
604
|
-
}
|
|
605
|
-
if (length < minLen) {
|
|
606
|
-
throw new OversizedDataError(`varbytes length ${length} below minLen ${minLen}`);
|
|
607
|
-
}
|
|
608
|
-
return this.read(length);
|
|
609
|
-
}
|
|
610
|
-
/** Verifica el número mágico de cabecera. */
|
|
611
|
-
assertMagic(expectedMagic) {
|
|
612
|
-
const actual = this.read(expectedMagic.length);
|
|
613
|
-
if (!bytesEqual(expectedMagic, actual)) {
|
|
614
|
-
throw new BadMagicError("header magic mismatch");
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
/** Exige que no queden bytes sin consumir. */
|
|
618
|
-
assertEof() {
|
|
619
|
-
if (this.#counter < this.#buffer.length) {
|
|
620
|
-
throw new TrailingGarbageError("trailing garbage after end of deserialized data");
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
};
|
|
624
|
-
var StreamSerializationContext = class {
|
|
625
|
-
#buffer = new Uint8Array(4096);
|
|
626
|
-
#length = 0;
|
|
627
|
-
get length() {
|
|
628
|
-
return this.#length;
|
|
629
|
-
}
|
|
630
|
-
getOutput() {
|
|
631
|
-
return this.#buffer.slice(0, this.#length);
|
|
632
|
-
}
|
|
633
|
-
writeByte(value) {
|
|
634
|
-
if (!Number.isInteger(value) || value < 0 || value > 255) {
|
|
635
|
-
throw new RangeError(`writeByte expects a byte 0..255; got ${value}`);
|
|
636
|
-
}
|
|
637
|
-
if (this.#length >= this.#buffer.length) {
|
|
638
|
-
const grown = new Uint8Array(this.#buffer.length * 2);
|
|
639
|
-
grown.set(this.#buffer, 0);
|
|
640
|
-
this.#buffer = grown;
|
|
641
|
-
}
|
|
642
|
-
this.#buffer[this.#length] = value;
|
|
643
|
-
this.#length += 1;
|
|
644
|
-
}
|
|
645
|
-
writeBytes(value) {
|
|
646
|
-
for (let i = 0; i < value.length; i++) {
|
|
647
|
-
this.writeByte(value[i]);
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
/** Codifica un varuint LEB128. */
|
|
651
|
-
writeVaruint(value) {
|
|
652
|
-
if (!Number.isSafeInteger(value) || value < 0) {
|
|
653
|
-
throw new RangeError(`writeVaruint expects a safe non-negative integer; got ${value}`);
|
|
654
|
-
}
|
|
655
|
-
do {
|
|
656
|
-
let byte = value % 128;
|
|
657
|
-
value = Math.floor(value / 128);
|
|
658
|
-
if (value > 0) byte |= 128;
|
|
659
|
-
this.writeByte(byte);
|
|
660
|
-
} while (value > 0);
|
|
661
|
-
}
|
|
662
|
-
writeVarbytes(value) {
|
|
663
|
-
this.writeVaruint(value.length);
|
|
664
|
-
this.writeBytes(value);
|
|
665
|
-
}
|
|
666
|
-
};
|
|
667
|
-
var rotl = (x, n) => x << n | x >>> 32 - n;
|
|
668
|
-
function sha1(msg) {
|
|
669
|
-
let h0 = 1732584193, h1 = 4023233417, h2 = 2562383102, h3 = 271733878, h4 = 3285377520;
|
|
670
|
-
const bitLen = msg.length * 8;
|
|
671
|
-
const withOne = msg.length + 1;
|
|
672
|
-
const padded = new Uint8Array(Math.ceil((withOne + 8) / 64) * 64);
|
|
673
|
-
padded.set(msg, 0);
|
|
674
|
-
padded[msg.length] = 128;
|
|
675
|
-
const dv = new DataView(padded.buffer);
|
|
676
|
-
dv.setUint32(padded.length - 4, bitLen >>> 0, false);
|
|
677
|
-
dv.setUint32(padded.length - 8, Math.floor(bitLen / 2 ** 32), false);
|
|
678
|
-
const w = new Uint32Array(80);
|
|
679
|
-
for (let off = 0; off < padded.length; off += 64) {
|
|
680
|
-
for (let i = 0; i < 16; i++) w[i] = dv.getUint32(off + i * 4, false);
|
|
681
|
-
for (let i = 16; i < 80; i++) w[i] = rotl(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1);
|
|
682
|
-
let a = h0, b = h1, c = h2, d = h3, e = h4;
|
|
683
|
-
for (let i = 0; i < 80; i++) {
|
|
684
|
-
let f2, k;
|
|
685
|
-
if (i < 20) {
|
|
686
|
-
f2 = b & c | ~b & d;
|
|
687
|
-
k = 1518500249;
|
|
688
|
-
} else if (i < 40) {
|
|
689
|
-
f2 = b ^ c ^ d;
|
|
690
|
-
k = 1859775393;
|
|
691
|
-
} else if (i < 60) {
|
|
692
|
-
f2 = b & c | b & d | c & d;
|
|
693
|
-
k = 2400959708;
|
|
694
|
-
} else {
|
|
695
|
-
f2 = b ^ c ^ d;
|
|
696
|
-
k = 3395469782;
|
|
697
|
-
}
|
|
698
|
-
const t = rotl(a, 5) + f2 + e + k + w[i] | 0;
|
|
699
|
-
e = d;
|
|
700
|
-
d = c;
|
|
701
|
-
c = rotl(b, 30);
|
|
702
|
-
b = a;
|
|
703
|
-
a = t;
|
|
704
|
-
}
|
|
705
|
-
h0 = h0 + a | 0;
|
|
706
|
-
h1 = h1 + b | 0;
|
|
707
|
-
h2 = h2 + c | 0;
|
|
708
|
-
h3 = h3 + d | 0;
|
|
709
|
-
h4 = h4 + e | 0;
|
|
710
|
-
}
|
|
711
|
-
const out = new Uint8Array(20);
|
|
712
|
-
const odv = new DataView(out.buffer);
|
|
713
|
-
odv.setUint32(0, h0, false);
|
|
714
|
-
odv.setUint32(4, h1, false);
|
|
715
|
-
odv.setUint32(8, h2, false);
|
|
716
|
-
odv.setUint32(12, h3, false);
|
|
717
|
-
odv.setUint32(16, h4, false);
|
|
718
|
-
return out;
|
|
719
|
-
}
|
|
720
|
-
var K = new Uint32Array([
|
|
721
|
-
1116352408,
|
|
722
|
-
1899447441,
|
|
723
|
-
3049323471,
|
|
724
|
-
3921009573,
|
|
725
|
-
961987163,
|
|
726
|
-
1508970993,
|
|
727
|
-
2453635748,
|
|
728
|
-
2870763221,
|
|
729
|
-
3624381080,
|
|
730
|
-
310598401,
|
|
731
|
-
607225278,
|
|
732
|
-
1426881987,
|
|
733
|
-
1925078388,
|
|
734
|
-
2162078206,
|
|
735
|
-
2614888103,
|
|
736
|
-
3248222580,
|
|
737
|
-
3835390401,
|
|
738
|
-
4022224774,
|
|
739
|
-
264347078,
|
|
740
|
-
604807628,
|
|
741
|
-
770255983,
|
|
742
|
-
1249150122,
|
|
743
|
-
1555081692,
|
|
744
|
-
1996064986,
|
|
745
|
-
2554220882,
|
|
746
|
-
2821834349,
|
|
747
|
-
2952996808,
|
|
748
|
-
3210313671,
|
|
749
|
-
3336571891,
|
|
750
|
-
3584528711,
|
|
751
|
-
113926993,
|
|
752
|
-
338241895,
|
|
753
|
-
666307205,
|
|
754
|
-
773529912,
|
|
755
|
-
1294757372,
|
|
756
|
-
1396182291,
|
|
757
|
-
1695183700,
|
|
758
|
-
1986661051,
|
|
759
|
-
2177026350,
|
|
760
|
-
2456956037,
|
|
761
|
-
2730485921,
|
|
762
|
-
2820302411,
|
|
763
|
-
3259730800,
|
|
764
|
-
3345764771,
|
|
765
|
-
3516065817,
|
|
766
|
-
3600352804,
|
|
767
|
-
4094571909,
|
|
768
|
-
275423344,
|
|
769
|
-
430227734,
|
|
770
|
-
506948616,
|
|
771
|
-
659060556,
|
|
772
|
-
883997877,
|
|
773
|
-
958139571,
|
|
774
|
-
1322822218,
|
|
775
|
-
1537002063,
|
|
776
|
-
1747873779,
|
|
777
|
-
1955562222,
|
|
778
|
-
2024104815,
|
|
779
|
-
2227730452,
|
|
780
|
-
2361852424,
|
|
781
|
-
2428436474,
|
|
782
|
-
2756734187,
|
|
783
|
-
3204031479,
|
|
784
|
-
3329325298
|
|
785
|
-
]);
|
|
786
|
-
var rotr = (x, n) => x >>> n | x << 32 - n;
|
|
787
|
-
function sha256(msg) {
|
|
788
|
-
const h = new Uint32Array([
|
|
789
|
-
1779033703,
|
|
790
|
-
3144134277,
|
|
791
|
-
1013904242,
|
|
792
|
-
2773480762,
|
|
793
|
-
1359893119,
|
|
794
|
-
2600822924,
|
|
795
|
-
528734635,
|
|
796
|
-
1541459225
|
|
797
|
-
]);
|
|
798
|
-
const bitLen = msg.length * 8;
|
|
799
|
-
const withOne = msg.length + 1;
|
|
800
|
-
const padded = new Uint8Array(Math.ceil((withOne + 8) / 64) * 64);
|
|
801
|
-
padded.set(msg, 0);
|
|
802
|
-
padded[msg.length] = 128;
|
|
803
|
-
const dv = new DataView(padded.buffer);
|
|
804
|
-
dv.setUint32(padded.length - 4, bitLen >>> 0, false);
|
|
805
|
-
dv.setUint32(padded.length - 8, Math.floor(bitLen / 2 ** 32), false);
|
|
806
|
-
const w = new Uint32Array(64);
|
|
807
|
-
for (let off = 0; off < padded.length; off += 64) {
|
|
808
|
-
for (let i = 0; i < 16; i++) w[i] = dv.getUint32(off + i * 4, false);
|
|
809
|
-
for (let i = 16; i < 64; i++) {
|
|
810
|
-
const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ w[i - 15] >>> 3;
|
|
811
|
-
const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ w[i - 2] >>> 10;
|
|
812
|
-
w[i] = w[i - 16] + s0 + w[i - 7] + s1 | 0;
|
|
813
|
-
}
|
|
814
|
-
let [a, b, c, d, e, f2, g, hh] = h;
|
|
815
|
-
for (let i = 0; i < 64; i++) {
|
|
816
|
-
const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
817
|
-
const ch = e & f2 ^ ~e & g;
|
|
818
|
-
const t1 = hh + S1 + ch + K[i] + w[i] | 0;
|
|
819
|
-
const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
|
|
820
|
-
const maj = a & b ^ a & c ^ b & c;
|
|
821
|
-
const t2 = S0 + maj | 0;
|
|
822
|
-
hh = g;
|
|
823
|
-
g = f2;
|
|
824
|
-
f2 = e;
|
|
825
|
-
e = d + t1 | 0;
|
|
826
|
-
d = c;
|
|
827
|
-
c = b;
|
|
828
|
-
b = a;
|
|
829
|
-
a = t1 + t2 | 0;
|
|
830
|
-
}
|
|
831
|
-
h[0] = h[0] + a | 0;
|
|
832
|
-
h[1] = h[1] + b | 0;
|
|
833
|
-
h[2] = h[2] + c | 0;
|
|
834
|
-
h[3] = h[3] + d | 0;
|
|
835
|
-
h[4] = h[4] + e | 0;
|
|
836
|
-
h[5] = h[5] + f2 | 0;
|
|
837
|
-
h[6] = h[6] + g | 0;
|
|
838
|
-
h[7] = h[7] + hh | 0;
|
|
839
|
-
}
|
|
840
|
-
const out = new Uint8Array(32);
|
|
841
|
-
const odv = new DataView(out.buffer);
|
|
842
|
-
for (let i = 0; i < 8; i++) odv.setUint32(i * 4, h[i], false);
|
|
843
|
-
return out;
|
|
844
|
-
}
|
|
845
|
-
var rol = (x, n) => x << n | x >>> 32 - n;
|
|
846
|
-
var ZL = [
|
|
847
|
-
0,
|
|
848
|
-
1,
|
|
849
|
-
2,
|
|
850
|
-
3,
|
|
851
|
-
4,
|
|
852
|
-
5,
|
|
853
|
-
6,
|
|
854
|
-
7,
|
|
855
|
-
8,
|
|
856
|
-
9,
|
|
857
|
-
10,
|
|
858
|
-
11,
|
|
859
|
-
12,
|
|
860
|
-
13,
|
|
861
|
-
14,
|
|
862
|
-
15,
|
|
863
|
-
7,
|
|
864
|
-
4,
|
|
865
|
-
13,
|
|
866
|
-
1,
|
|
867
|
-
10,
|
|
868
|
-
6,
|
|
869
|
-
15,
|
|
870
|
-
3,
|
|
871
|
-
12,
|
|
872
|
-
0,
|
|
873
|
-
9,
|
|
874
|
-
5,
|
|
875
|
-
2,
|
|
876
|
-
14,
|
|
877
|
-
11,
|
|
878
|
-
8,
|
|
879
|
-
3,
|
|
880
|
-
10,
|
|
881
|
-
14,
|
|
882
|
-
4,
|
|
883
|
-
9,
|
|
884
|
-
15,
|
|
885
|
-
8,
|
|
886
|
-
1,
|
|
887
|
-
2,
|
|
888
|
-
7,
|
|
889
|
-
0,
|
|
890
|
-
6,
|
|
891
|
-
13,
|
|
892
|
-
11,
|
|
893
|
-
5,
|
|
894
|
-
12,
|
|
895
|
-
1,
|
|
896
|
-
9,
|
|
897
|
-
11,
|
|
898
|
-
10,
|
|
899
|
-
0,
|
|
900
|
-
8,
|
|
901
|
-
12,
|
|
902
|
-
4,
|
|
903
|
-
13,
|
|
904
|
-
3,
|
|
905
|
-
7,
|
|
906
|
-
15,
|
|
907
|
-
14,
|
|
908
|
-
5,
|
|
909
|
-
6,
|
|
910
|
-
2,
|
|
911
|
-
4,
|
|
912
|
-
0,
|
|
913
|
-
5,
|
|
914
|
-
9,
|
|
915
|
-
7,
|
|
916
|
-
12,
|
|
917
|
-
2,
|
|
918
|
-
10,
|
|
919
|
-
14,
|
|
920
|
-
1,
|
|
921
|
-
3,
|
|
922
|
-
8,
|
|
923
|
-
11,
|
|
924
|
-
6,
|
|
925
|
-
15,
|
|
926
|
-
13
|
|
927
|
-
];
|
|
928
|
-
var ZR = [
|
|
929
|
-
5,
|
|
930
|
-
14,
|
|
931
|
-
7,
|
|
932
|
-
0,
|
|
933
|
-
9,
|
|
934
|
-
2,
|
|
935
|
-
11,
|
|
936
|
-
4,
|
|
937
|
-
13,
|
|
938
|
-
6,
|
|
939
|
-
15,
|
|
940
|
-
8,
|
|
941
|
-
1,
|
|
942
|
-
10,
|
|
943
|
-
3,
|
|
944
|
-
12,
|
|
945
|
-
6,
|
|
946
|
-
11,
|
|
947
|
-
3,
|
|
948
|
-
7,
|
|
949
|
-
0,
|
|
950
|
-
13,
|
|
951
|
-
5,
|
|
952
|
-
10,
|
|
953
|
-
14,
|
|
954
|
-
15,
|
|
955
|
-
8,
|
|
956
|
-
12,
|
|
957
|
-
4,
|
|
958
|
-
9,
|
|
959
|
-
1,
|
|
960
|
-
2,
|
|
961
|
-
15,
|
|
962
|
-
5,
|
|
963
|
-
1,
|
|
964
|
-
3,
|
|
965
|
-
7,
|
|
966
|
-
14,
|
|
967
|
-
6,
|
|
968
|
-
9,
|
|
969
|
-
11,
|
|
970
|
-
8,
|
|
971
|
-
12,
|
|
972
|
-
2,
|
|
973
|
-
10,
|
|
974
|
-
0,
|
|
975
|
-
4,
|
|
976
|
-
13,
|
|
977
|
-
8,
|
|
978
|
-
6,
|
|
979
|
-
4,
|
|
980
|
-
1,
|
|
981
|
-
3,
|
|
982
|
-
11,
|
|
983
|
-
15,
|
|
984
|
-
0,
|
|
985
|
-
5,
|
|
986
|
-
12,
|
|
987
|
-
2,
|
|
988
|
-
13,
|
|
989
|
-
9,
|
|
990
|
-
7,
|
|
991
|
-
10,
|
|
992
|
-
14,
|
|
993
|
-
12,
|
|
994
|
-
15,
|
|
995
|
-
10,
|
|
996
|
-
4,
|
|
997
|
-
1,
|
|
998
|
-
5,
|
|
999
|
-
8,
|
|
1000
|
-
7,
|
|
1001
|
-
6,
|
|
1002
|
-
2,
|
|
1003
|
-
13,
|
|
1004
|
-
14,
|
|
1005
|
-
0,
|
|
1006
|
-
3,
|
|
1007
|
-
9,
|
|
1008
|
-
11
|
|
1009
|
-
];
|
|
1010
|
-
var SL = [
|
|
1011
|
-
11,
|
|
1012
|
-
14,
|
|
1013
|
-
15,
|
|
1014
|
-
12,
|
|
1015
|
-
5,
|
|
1016
|
-
8,
|
|
1017
|
-
7,
|
|
1018
|
-
9,
|
|
1019
|
-
11,
|
|
1020
|
-
13,
|
|
1021
|
-
14,
|
|
1022
|
-
15,
|
|
1023
|
-
6,
|
|
1024
|
-
7,
|
|
1025
|
-
9,
|
|
1026
|
-
8,
|
|
1027
|
-
7,
|
|
1028
|
-
6,
|
|
1029
|
-
8,
|
|
1030
|
-
13,
|
|
1031
|
-
11,
|
|
1032
|
-
9,
|
|
1033
|
-
7,
|
|
1034
|
-
15,
|
|
1035
|
-
7,
|
|
1036
|
-
12,
|
|
1037
|
-
15,
|
|
1038
|
-
9,
|
|
1039
|
-
11,
|
|
1040
|
-
7,
|
|
1041
|
-
13,
|
|
1042
|
-
12,
|
|
1043
|
-
11,
|
|
1044
|
-
13,
|
|
1045
|
-
6,
|
|
1046
|
-
7,
|
|
1047
|
-
14,
|
|
1048
|
-
9,
|
|
1049
|
-
13,
|
|
1050
|
-
15,
|
|
1051
|
-
14,
|
|
1052
|
-
8,
|
|
1053
|
-
13,
|
|
1054
|
-
6,
|
|
1055
|
-
5,
|
|
1056
|
-
12,
|
|
1057
|
-
7,
|
|
1058
|
-
5,
|
|
1059
|
-
11,
|
|
1060
|
-
12,
|
|
1061
|
-
14,
|
|
1062
|
-
15,
|
|
1063
|
-
14,
|
|
1064
|
-
15,
|
|
1065
|
-
9,
|
|
1066
|
-
8,
|
|
1067
|
-
9,
|
|
1068
|
-
14,
|
|
1069
|
-
5,
|
|
1070
|
-
6,
|
|
1071
|
-
8,
|
|
1072
|
-
6,
|
|
1073
|
-
5,
|
|
1074
|
-
12,
|
|
1075
|
-
9,
|
|
1076
|
-
15,
|
|
1077
|
-
5,
|
|
1078
|
-
11,
|
|
1079
|
-
6,
|
|
1080
|
-
8,
|
|
1081
|
-
13,
|
|
1082
|
-
12,
|
|
1083
|
-
5,
|
|
1084
|
-
12,
|
|
1085
|
-
13,
|
|
1086
|
-
14,
|
|
1087
|
-
11,
|
|
1088
|
-
8,
|
|
1089
|
-
5,
|
|
1090
|
-
6
|
|
1091
|
-
];
|
|
1092
|
-
var SR = [
|
|
1093
|
-
8,
|
|
1094
|
-
9,
|
|
1095
|
-
9,
|
|
1096
|
-
11,
|
|
1097
|
-
13,
|
|
1098
|
-
15,
|
|
1099
|
-
15,
|
|
1100
|
-
5,
|
|
1101
|
-
7,
|
|
1102
|
-
7,
|
|
1103
|
-
8,
|
|
1104
|
-
11,
|
|
1105
|
-
14,
|
|
1106
|
-
14,
|
|
1107
|
-
12,
|
|
1108
|
-
6,
|
|
1109
|
-
9,
|
|
1110
|
-
13,
|
|
1111
|
-
15,
|
|
1112
|
-
7,
|
|
1113
|
-
12,
|
|
1114
|
-
8,
|
|
1115
|
-
9,
|
|
1116
|
-
11,
|
|
1117
|
-
7,
|
|
1118
|
-
7,
|
|
1119
|
-
12,
|
|
1120
|
-
7,
|
|
1121
|
-
6,
|
|
1122
|
-
15,
|
|
1123
|
-
13,
|
|
1124
|
-
11,
|
|
1125
|
-
9,
|
|
1126
|
-
7,
|
|
1127
|
-
15,
|
|
1128
|
-
11,
|
|
1129
|
-
8,
|
|
1130
|
-
6,
|
|
1131
|
-
6,
|
|
1132
|
-
14,
|
|
1133
|
-
12,
|
|
1134
|
-
13,
|
|
1135
|
-
5,
|
|
1136
|
-
14,
|
|
1137
|
-
13,
|
|
1138
|
-
13,
|
|
1139
|
-
7,
|
|
1140
|
-
5,
|
|
1141
|
-
15,
|
|
1142
|
-
5,
|
|
1143
|
-
8,
|
|
1144
|
-
11,
|
|
1145
|
-
14,
|
|
1146
|
-
14,
|
|
1147
|
-
6,
|
|
1148
|
-
14,
|
|
1149
|
-
6,
|
|
1150
|
-
9,
|
|
1151
|
-
12,
|
|
1152
|
-
9,
|
|
1153
|
-
12,
|
|
1154
|
-
5,
|
|
1155
|
-
15,
|
|
1156
|
-
8,
|
|
1157
|
-
8,
|
|
1158
|
-
5,
|
|
1159
|
-
12,
|
|
1160
|
-
9,
|
|
1161
|
-
12,
|
|
1162
|
-
5,
|
|
1163
|
-
14,
|
|
1164
|
-
6,
|
|
1165
|
-
8,
|
|
1166
|
-
13,
|
|
1167
|
-
6,
|
|
1168
|
-
5,
|
|
1169
|
-
15,
|
|
1170
|
-
13,
|
|
1171
|
-
11,
|
|
1172
|
-
11
|
|
1173
|
-
];
|
|
1174
|
-
var KL = [0, 1518500249, 1859775393, 2400959708, 2840853838];
|
|
1175
|
-
var KR = [1352829926, 1548603684, 1836072691, 2053994217, 0];
|
|
1176
|
-
var f = (j, x, y, z) => {
|
|
1177
|
-
if (j < 16) return x ^ y ^ z;
|
|
1178
|
-
if (j < 32) return x & y | ~x & z;
|
|
1179
|
-
if (j < 48) return (x | ~y) ^ z;
|
|
1180
|
-
if (j < 64) return x & z | y & ~z;
|
|
1181
|
-
return x ^ (y | ~z);
|
|
1182
|
-
};
|
|
1183
|
-
function ripemd160(msg) {
|
|
1184
|
-
let h0 = 1732584193, h1 = 4023233417, h2 = 2562383102, h3 = 271733878, h4 = 3285377520;
|
|
1185
|
-
const bitLen = msg.length * 8;
|
|
1186
|
-
const withOne = msg.length + 1;
|
|
1187
|
-
const padded = new Uint8Array(Math.ceil((withOne + 8) / 64) * 64);
|
|
1188
|
-
padded.set(msg, 0);
|
|
1189
|
-
padded[msg.length] = 128;
|
|
1190
|
-
const dv = new DataView(padded.buffer);
|
|
1191
|
-
dv.setUint32(padded.length - 8, bitLen >>> 0, true);
|
|
1192
|
-
dv.setUint32(padded.length - 4, Math.floor(bitLen / 2 ** 32), true);
|
|
1193
|
-
const x = new Uint32Array(16);
|
|
1194
|
-
for (let off = 0; off < padded.length; off += 64) {
|
|
1195
|
-
for (let i = 0; i < 16; i++) x[i] = dv.getUint32(off + i * 4, true);
|
|
1196
|
-
let al = h0, bl = h1, cl = h2, dl = h3, el = h4;
|
|
1197
|
-
let ar = h0, br = h1, cr = h2, dr = h3, er = h4;
|
|
1198
|
-
for (let j = 0; j < 80; j++) {
|
|
1199
|
-
const round = Math.floor(j / 16);
|
|
1200
|
-
let t2 = al + f(j, bl, cl, dl) + x[ZL[j]] + KL[round] | 0;
|
|
1201
|
-
t2 = rol(t2, SL[j]) + el | 0;
|
|
1202
|
-
al = el;
|
|
1203
|
-
el = dl;
|
|
1204
|
-
dl = rol(cl, 10);
|
|
1205
|
-
cl = bl;
|
|
1206
|
-
bl = t2;
|
|
1207
|
-
t2 = ar + f(79 - j, br, cr, dr) + x[ZR[j]] + KR[round] | 0;
|
|
1208
|
-
t2 = rol(t2, SR[j]) + er | 0;
|
|
1209
|
-
ar = er;
|
|
1210
|
-
er = dr;
|
|
1211
|
-
dr = rol(cr, 10);
|
|
1212
|
-
cr = br;
|
|
1213
|
-
br = t2;
|
|
1214
|
-
}
|
|
1215
|
-
const t = h1 + cl + dr | 0;
|
|
1216
|
-
h1 = h2 + dl + er | 0;
|
|
1217
|
-
h2 = h3 + el + ar | 0;
|
|
1218
|
-
h3 = h4 + al + br | 0;
|
|
1219
|
-
h4 = h0 + bl + cr | 0;
|
|
1220
|
-
h0 = t;
|
|
1221
|
-
}
|
|
1222
|
-
const out = new Uint8Array(20);
|
|
1223
|
-
const odv = new DataView(out.buffer);
|
|
1224
|
-
odv.setUint32(0, h0, true);
|
|
1225
|
-
odv.setUint32(4, h1, true);
|
|
1226
|
-
odv.setUint32(8, h2, true);
|
|
1227
|
-
odv.setUint32(12, h3, true);
|
|
1228
|
-
odv.setUint32(16, h4, true);
|
|
1229
|
-
return out;
|
|
1230
|
-
}
|
|
1231
|
-
var MAX_RESULT_LENGTH = 4096;
|
|
1232
|
-
var MAX_MSG_LENGTH = 4096;
|
|
1233
|
-
function concatBytes(a, b) {
|
|
1234
|
-
const out = new Uint8Array(a.length + b.length);
|
|
1235
|
-
out.set(a, 0);
|
|
1236
|
-
out.set(b, a.length);
|
|
1237
|
-
return out;
|
|
1238
|
-
}
|
|
1239
|
-
var Op = class _Op {
|
|
1240
|
-
static MAX_RESULT_LENGTH = MAX_RESULT_LENGTH;
|
|
1241
|
-
static MAX_MSG_LENGTH = MAX_MSG_LENGTH;
|
|
1242
|
-
/** Deserializa una operación leyendo su tag y despachando a la factoría correcta. */
|
|
1243
|
-
static deserialize(ctx) {
|
|
1244
|
-
return _Op.deserializeFromTag(ctx, ctx.readByte());
|
|
1245
|
-
}
|
|
1246
|
-
/** Igual que `deserialize`, pero con el tag ya leído del stream (lo usa el árbol Timestamp). */
|
|
1247
|
-
static deserializeFromTag(ctx, tag) {
|
|
1248
|
-
const factory = OP_BY_TAG.get(tag);
|
|
1249
|
-
if (factory === void 0) {
|
|
1250
|
-
throw new UnknownOperationError(`unknown operation tag 0x${tag.toString(16).padStart(2, "0")}`);
|
|
1251
|
-
}
|
|
1252
|
-
return factory(ctx);
|
|
1253
|
-
}
|
|
1254
|
-
get maxMsgLength() {
|
|
1255
|
-
return MAX_MSG_LENGTH;
|
|
1256
|
-
}
|
|
1257
|
-
checkMsg(msg) {
|
|
1258
|
-
if (msg.length > this.maxMsgLength) {
|
|
1259
|
-
throw new MessageTooLongError(`message length ${msg.length} exceeds ${this.maxMsgLength}`);
|
|
1260
|
-
}
|
|
1261
|
-
}
|
|
1262
|
-
checkResult(result) {
|
|
1263
|
-
if (result.length > MAX_RESULT_LENGTH) {
|
|
1264
|
-
throw new ResultTooLongError(`result length ${result.length} exceeds ${MAX_RESULT_LENGTH}`);
|
|
1265
|
-
}
|
|
1266
|
-
return result;
|
|
1267
|
-
}
|
|
1268
|
-
};
|
|
1269
|
-
var OpBinary = class extends Op {
|
|
1270
|
-
arg;
|
|
1271
|
-
constructor(arg) {
|
|
1272
|
-
super();
|
|
1273
|
-
if (!(arg instanceof Uint8Array)) {
|
|
1274
|
-
throw new TypeError("OpBinary arg must be a Uint8Array");
|
|
1275
|
-
}
|
|
1276
|
-
this.arg = arg.slice();
|
|
1277
|
-
}
|
|
1278
|
-
serialize(ctx) {
|
|
1279
|
-
ctx.writeByte(this.tag);
|
|
1280
|
-
ctx.writeVarbytes(this.arg);
|
|
1281
|
-
}
|
|
1282
|
-
};
|
|
1283
|
-
var OpAppend = class _OpAppend extends OpBinary {
|
|
1284
|
-
static TAG = 240;
|
|
1285
|
-
tag = _OpAppend.TAG;
|
|
1286
|
-
tagName = "append";
|
|
1287
|
-
call(msg) {
|
|
1288
|
-
this.checkMsg(msg);
|
|
1289
|
-
return this.checkResult(concatBytes(msg, this.arg));
|
|
1290
|
-
}
|
|
1291
|
-
equals(other) {
|
|
1292
|
-
return other instanceof _OpAppend && bytesEqual(this.arg, other.arg);
|
|
1293
|
-
}
|
|
1294
|
-
};
|
|
1295
|
-
var OpPrepend = class _OpPrepend extends OpBinary {
|
|
1296
|
-
static TAG = 241;
|
|
1297
|
-
tag = _OpPrepend.TAG;
|
|
1298
|
-
tagName = "prepend";
|
|
1299
|
-
call(msg) {
|
|
1300
|
-
this.checkMsg(msg);
|
|
1301
|
-
return this.checkResult(concatBytes(this.arg, msg));
|
|
1302
|
-
}
|
|
1303
|
-
equals(other) {
|
|
1304
|
-
return other instanceof _OpPrepend && bytesEqual(this.arg, other.arg);
|
|
1305
|
-
}
|
|
1306
|
-
};
|
|
1307
|
-
var OpUnary = class extends Op {
|
|
1308
|
-
serialize(ctx) {
|
|
1309
|
-
ctx.writeByte(this.tag);
|
|
1310
|
-
}
|
|
1311
|
-
};
|
|
1312
|
-
var OpReverse = class _OpReverse extends OpUnary {
|
|
1313
|
-
static TAG = 242;
|
|
1314
|
-
tag = _OpReverse.TAG;
|
|
1315
|
-
tagName = "reverse";
|
|
1316
|
-
call(msg) {
|
|
1317
|
-
this.checkMsg(msg);
|
|
1318
|
-
const r = new Uint8Array(msg.length);
|
|
1319
|
-
for (let i = 0; i < msg.length; i++) r[i] = msg[msg.length - 1 - i];
|
|
1320
|
-
return this.checkResult(r);
|
|
1321
|
-
}
|
|
1322
|
-
equals(other) {
|
|
1323
|
-
return other instanceof _OpReverse;
|
|
1324
|
-
}
|
|
1325
|
-
};
|
|
1326
|
-
var OpHexlify = class _OpHexlify extends OpUnary {
|
|
1327
|
-
static TAG = 243;
|
|
1328
|
-
tag = _OpHexlify.TAG;
|
|
1329
|
-
tagName = "hexlify";
|
|
1330
|
-
// El resultado mide el doble que el mensaje; el límite de mensaje es la mitad.
|
|
1331
|
-
get maxMsgLength() {
|
|
1332
|
-
return MAX_RESULT_LENGTH / 2;
|
|
1333
|
-
}
|
|
1334
|
-
call(msg) {
|
|
1335
|
-
this.checkMsg(msg);
|
|
1336
|
-
return this.checkResult(textToBytes(bytesToHex(msg)));
|
|
1337
|
-
}
|
|
1338
|
-
equals(other) {
|
|
1339
|
-
return other instanceof _OpHexlify;
|
|
1340
|
-
}
|
|
1341
|
-
};
|
|
1342
|
-
var CryptOp = class extends OpUnary {
|
|
1343
|
-
call(msg) {
|
|
1344
|
-
this.checkMsg(msg);
|
|
1345
|
-
return this.hash(msg);
|
|
1346
|
-
}
|
|
1347
|
-
/**
|
|
1348
|
-
* Hashea el contenido COMPLETO de un fichero (longitud arbitraria) con el algoritmo
|
|
1349
|
-
* de esta operación. A diferencia de `call`, NO aplica el límite `MAX_MSG_LENGTH`:
|
|
1350
|
-
* `call` transforma digests dentro del árbol de prueba (≤ 4096 bytes), mientras que
|
|
1351
|
-
* `hashFile` recibe el contenido íntegro del fichero a sellar, que puede ser de
|
|
1352
|
-
* cualquier tamaño. Lo usa `DetachedTimestampFile.fromBytes`.
|
|
1353
|
-
*/
|
|
1354
|
-
hashFile(data) {
|
|
1355
|
-
if (!(data instanceof Uint8Array)) {
|
|
1356
|
-
throw new TypeError("hashFile expects a Uint8Array");
|
|
1357
|
-
}
|
|
1358
|
-
return this.hash(data);
|
|
1359
|
-
}
|
|
1360
|
-
};
|
|
1361
|
-
var OpSHA1 = class _OpSHA1 extends CryptOp {
|
|
1362
|
-
static TAG = 2;
|
|
1363
|
-
tag = _OpSHA1.TAG;
|
|
1364
|
-
tagName = "sha1";
|
|
1365
|
-
digestLength = 20;
|
|
1366
|
-
hash(msg) {
|
|
1367
|
-
return sha1(msg);
|
|
1368
|
-
}
|
|
1369
|
-
equals(other) {
|
|
1370
|
-
return other instanceof _OpSHA1;
|
|
1371
|
-
}
|
|
1372
|
-
};
|
|
1373
|
-
var OpRIPEMD160 = class _OpRIPEMD160 extends CryptOp {
|
|
1374
|
-
static TAG = 3;
|
|
1375
|
-
tag = _OpRIPEMD160.TAG;
|
|
1376
|
-
tagName = "ripemd160";
|
|
1377
|
-
digestLength = 20;
|
|
1378
|
-
hash(msg) {
|
|
1379
|
-
return ripemd160(msg);
|
|
1380
|
-
}
|
|
1381
|
-
equals(other) {
|
|
1382
|
-
return other instanceof _OpRIPEMD160;
|
|
1383
|
-
}
|
|
1384
|
-
};
|
|
1385
|
-
var OpSHA256 = class _OpSHA256 extends CryptOp {
|
|
1386
|
-
static TAG = 8;
|
|
1387
|
-
tag = _OpSHA256.TAG;
|
|
1388
|
-
tagName = "sha256";
|
|
1389
|
-
digestLength = 32;
|
|
1390
|
-
hash(msg) {
|
|
1391
|
-
return sha256(msg);
|
|
1392
|
-
}
|
|
1393
|
-
equals(other) {
|
|
1394
|
-
return other instanceof _OpSHA256;
|
|
1395
|
-
}
|
|
1396
|
-
};
|
|
1397
|
-
var unary = (ctor) => () => new ctor();
|
|
1398
|
-
var binary = (ctor) => (ctx) => new ctor(ctx.readVarbytes(MAX_RESULT_LENGTH, 1));
|
|
1399
|
-
function buildTagTable(entries) {
|
|
1400
|
-
const map = /* @__PURE__ */ new Map();
|
|
1401
|
-
for (const [tag, factory] of entries) {
|
|
1402
|
-
if (map.has(tag)) {
|
|
1403
|
-
throw new Error(`duplicate operation tag 0x${tag.toString(16)}`);
|
|
1404
|
-
}
|
|
1405
|
-
map.set(tag, factory);
|
|
1406
|
-
}
|
|
1407
|
-
return map;
|
|
1408
|
-
}
|
|
1409
|
-
var OP_BY_TAG = buildTagTable([
|
|
1410
|
-
[OpAppend.TAG, binary(OpAppend)],
|
|
1411
|
-
[OpPrepend.TAG, binary(OpPrepend)],
|
|
1412
|
-
[OpReverse.TAG, unary(OpReverse)],
|
|
1413
|
-
[OpHexlify.TAG, unary(OpHexlify)],
|
|
1414
|
-
[OpSHA1.TAG, unary(OpSHA1)],
|
|
1415
|
-
[OpRIPEMD160.TAG, unary(OpRIPEMD160)],
|
|
1416
|
-
[OpSHA256.TAG, unary(OpSHA256)]
|
|
1417
|
-
]);
|
|
1418
|
-
var TAG_SIZE = 8;
|
|
1419
|
-
var MAX_PAYLOAD_SIZE = 8192;
|
|
1420
|
-
var MAX_URI_LENGTH = 1e3;
|
|
1421
|
-
var ALLOWED_URI_CHARS = new Set(
|
|
1422
|
-
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._/:"
|
|
1423
|
-
);
|
|
1424
|
-
var PENDING_TAG = new Uint8Array([131, 223, 227, 13, 46, 249, 12, 142]);
|
|
1425
|
-
var BITCOIN_TAG = new Uint8Array([5, 136, 150, 13, 115, 215, 25, 1]);
|
|
1426
|
-
var LITECOIN_TAG = new Uint8Array([6, 134, 154, 13, 115, 215, 27, 69]);
|
|
1427
|
-
function decodeAndValidateUri(bytes) {
|
|
1428
|
-
if (bytes.length === 0) {
|
|
1429
|
-
throw new InvalidUriError("pending attestation URI is empty");
|
|
1430
|
-
}
|
|
1431
|
-
if (bytes.length > MAX_URI_LENGTH) {
|
|
1432
|
-
throw new InvalidUriError(`pending attestation URI exceeds ${MAX_URI_LENGTH} bytes`);
|
|
1433
|
-
}
|
|
1434
|
-
let uri = "";
|
|
1435
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
1436
|
-
const byte = bytes[i];
|
|
1437
|
-
const char = String.fromCharCode(byte);
|
|
1438
|
-
if (!ALLOWED_URI_CHARS.has(char)) {
|
|
1439
|
-
throw new InvalidUriError(
|
|
1440
|
-
`pending attestation URI contains invalid byte 0x${byte.toString(16).padStart(2, "0")}`
|
|
1441
|
-
);
|
|
1442
|
-
}
|
|
1443
|
-
uri += char;
|
|
1444
|
-
}
|
|
1445
|
-
return uri;
|
|
1446
|
-
}
|
|
1447
|
-
function deserializeAttestation(ctx) {
|
|
1448
|
-
const tag = ctx.read(TAG_SIZE).slice();
|
|
1449
|
-
const payload = ctx.readVarbytes(MAX_PAYLOAD_SIZE);
|
|
1450
|
-
const payloadCtx = new StreamDeserializationContext(payload);
|
|
1451
|
-
let attestation;
|
|
1452
|
-
if (bytesEqual(tag, PENDING_TAG)) {
|
|
1453
|
-
const uriBytes = payloadCtx.readVarbytes(MAX_URI_LENGTH).slice();
|
|
1454
|
-
attestation = { kind: "pending", tag, uri: decodeAndValidateUri(uriBytes), uriBytes };
|
|
1455
|
-
} else if (bytesEqual(tag, BITCOIN_TAG)) {
|
|
1456
|
-
attestation = { kind: "bitcoin", tag, height: payloadCtx.readVaruint() };
|
|
1457
|
-
} else if (bytesEqual(tag, LITECOIN_TAG)) {
|
|
1458
|
-
attestation = { kind: "litecoin", tag, height: payloadCtx.readVaruint() };
|
|
1459
|
-
} else {
|
|
1460
|
-
return { kind: "unknown", tag, payload: payload.slice() };
|
|
1461
|
-
}
|
|
1462
|
-
payloadCtx.assertEof();
|
|
1463
|
-
return attestation;
|
|
1464
|
-
}
|
|
1465
|
-
function serializePayload(ctx, att) {
|
|
1466
|
-
switch (att.kind) {
|
|
1467
|
-
case "pending":
|
|
1468
|
-
ctx.writeVarbytes(att.uriBytes);
|
|
1469
|
-
return;
|
|
1470
|
-
case "bitcoin":
|
|
1471
|
-
case "litecoin":
|
|
1472
|
-
ctx.writeVaruint(att.height);
|
|
1473
|
-
return;
|
|
1474
|
-
case "unknown":
|
|
1475
|
-
ctx.writeBytes(att.payload);
|
|
1476
|
-
return;
|
|
1477
|
-
}
|
|
1478
|
-
}
|
|
1479
|
-
function serializeAttestation(ctx, att) {
|
|
1480
|
-
ctx.writeBytes(att.tag);
|
|
1481
|
-
const payloadCtx = new StreamSerializationContext();
|
|
1482
|
-
serializePayload(payloadCtx, att);
|
|
1483
|
-
ctx.writeVarbytes(payloadCtx.getOutput());
|
|
1484
|
-
}
|
|
1485
|
-
function compareAttestations(a, b) {
|
|
1486
|
-
const deltaTag = compareBytes(a.tag, b.tag);
|
|
1487
|
-
if (deltaTag !== 0) {
|
|
1488
|
-
return deltaTag;
|
|
1489
|
-
}
|
|
1490
|
-
switch (a.kind) {
|
|
1491
|
-
case "pending":
|
|
1492
|
-
return compareBytes(a.uriBytes, b.uriBytes);
|
|
1493
|
-
case "bitcoin":
|
|
1494
|
-
case "litecoin":
|
|
1495
|
-
return a.height - b.height;
|
|
1496
|
-
case "unknown":
|
|
1497
|
-
return compareBytes(a.payload, b.payload);
|
|
1498
|
-
}
|
|
1499
|
-
}
|
|
1500
|
-
function attestationsEqual(a, b) {
|
|
1501
|
-
if (a.kind !== b.kind || !bytesEqual(a.tag, b.tag)) {
|
|
1502
|
-
return false;
|
|
1503
|
-
}
|
|
1504
|
-
switch (a.kind) {
|
|
1505
|
-
case "pending":
|
|
1506
|
-
return bytesEqual(a.uriBytes, b.uriBytes);
|
|
1507
|
-
case "bitcoin":
|
|
1508
|
-
case "litecoin":
|
|
1509
|
-
return a.height === b.height;
|
|
1510
|
-
case "unknown":
|
|
1511
|
-
return bytesEqual(a.payload, b.payload);
|
|
1512
|
-
}
|
|
1513
|
-
}
|
|
1514
|
-
var MERKLEROOT_RE = /^[0-9a-fA-F]{64}$/;
|
|
1515
|
-
function verifyAgainstBlockheader(digest, block) {
|
|
1516
|
-
if (digest.length !== 32) {
|
|
1517
|
-
throw new VerificationError(`expected digest of 32 bytes; got ${digest.length}`);
|
|
1518
|
-
}
|
|
1519
|
-
if (typeof block.merkleroot !== "string" || !MERKLEROOT_RE.test(block.merkleroot)) {
|
|
1520
|
-
throw new VerificationError("block merkleroot is not a 64-char hex string");
|
|
1521
|
-
}
|
|
1522
|
-
if (!Number.isInteger(block.time) || block.time <= 0) {
|
|
1523
|
-
throw new VerificationError("block time is not a positive integer");
|
|
1524
|
-
}
|
|
1525
|
-
if (!bytesEqual(digest, hexToBytes(block.merkleroot))) {
|
|
1526
|
-
throw new VerificationError("digest does not match block merkleroot");
|
|
1527
|
-
}
|
|
1528
|
-
return block.time;
|
|
1529
|
-
}
|
|
1530
|
-
var MAX_TREE_DEPTH = 256;
|
|
1531
|
-
function opToBytes(op) {
|
|
1532
|
-
const ctx = new StreamSerializationContext();
|
|
1533
|
-
op.serialize(ctx);
|
|
1534
|
-
return ctx.getOutput();
|
|
1535
|
-
}
|
|
1536
|
-
function opKey(op) {
|
|
1537
|
-
return bytesToHex(opToBytes(op));
|
|
1538
|
-
}
|
|
1539
|
-
var Timestamp = class _Timestamp {
|
|
1540
|
-
/** Digest de este nodo (copia defensiva, no comparte memoria con la entrada). */
|
|
1541
|
-
msg;
|
|
1542
|
-
/** Sellos directos sobre `msg`. El cliente puede añadir con `.push()`. */
|
|
1543
|
-
attestations = [];
|
|
1544
|
-
/** Ramas indexadas por la serialización canónica (hex) de su op. */
|
|
1545
|
-
#ops = /* @__PURE__ */ new Map();
|
|
1546
|
-
constructor(msg) {
|
|
1547
|
-
if (!(msg instanceof Uint8Array)) {
|
|
1548
|
-
throw new TypeError("Timestamp msg must be a Uint8Array");
|
|
1549
|
-
}
|
|
1550
|
-
if (msg.length > Op.MAX_MSG_LENGTH) {
|
|
1551
|
-
throw new TypeError(`Timestamp msg length ${msg.length} exceeds ${Op.MAX_MSG_LENGTH}`);
|
|
1552
|
-
}
|
|
1553
|
-
this.msg = msg.slice();
|
|
1554
|
-
}
|
|
1555
|
-
/** El digest de este nodo (copia: mutar el resultado no afecta al árbol). */
|
|
1556
|
-
getDigest() {
|
|
1557
|
-
return this.msg.slice();
|
|
1558
|
-
}
|
|
1559
|
-
/** Las ramas (op + sub-timestamp) de este nodo, como array de solo lectura. */
|
|
1560
|
-
get branches() {
|
|
1561
|
-
return [...this.#ops.values()];
|
|
1562
|
-
}
|
|
1563
|
-
/**
|
|
1564
|
-
* Deserializa un Timestamp. El formato no incluye el mensaje sobre el que opera,
|
|
1565
|
-
* así que hay que aportarlo (`initialMsg`) para recalcular los resultados de las ops.
|
|
1566
|
-
* @param depth profundidad actual; protege contra árboles maliciosamente profundos.
|
|
1567
|
-
*/
|
|
1568
|
-
static deserialize(ctx, initialMsg, depth = 0) {
|
|
1569
|
-
if (depth > MAX_TREE_DEPTH) {
|
|
1570
|
-
throw new OversizedDataError(`timestamp tree exceeds max depth ${MAX_TREE_DEPTH}`);
|
|
1571
|
-
}
|
|
1572
|
-
const self = new _Timestamp(initialMsg);
|
|
1573
|
-
let tag = ctx.readByte();
|
|
1574
|
-
while (tag === 255) {
|
|
1575
|
-
self.#deserializeElement(ctx, ctx.readByte(), depth);
|
|
1576
|
-
tag = ctx.readByte();
|
|
1577
|
-
}
|
|
1578
|
-
self.#deserializeElement(ctx, tag, depth);
|
|
1579
|
-
return self;
|
|
1580
|
-
}
|
|
1581
|
-
#deserializeElement(ctx, tag, depth) {
|
|
1582
|
-
if (tag === 0) {
|
|
1583
|
-
this.attestations.push(deserializeAttestation(ctx));
|
|
1584
|
-
return;
|
|
1585
|
-
}
|
|
1586
|
-
const op = Op.deserializeFromTag(ctx, tag);
|
|
1587
|
-
let result;
|
|
1588
|
-
try {
|
|
1589
|
-
result = op.call(this.msg);
|
|
1590
|
-
} catch (err) {
|
|
1591
|
-
throw new DeserializationError(
|
|
1592
|
-
`operation failed during deserialization: ${err.message}`
|
|
1593
|
-
);
|
|
1594
|
-
}
|
|
1595
|
-
const stamp = _Timestamp.deserialize(ctx, result, depth + 1);
|
|
1596
|
-
this.#ops.set(opKey(op), { op, stamp });
|
|
1597
|
-
}
|
|
1598
|
-
/** Serializa este nodo en orden canónico (determinista byte-a-byte). */
|
|
1599
|
-
serialize(ctx) {
|
|
1600
|
-
const attestations = [...this.attestations].sort(compareAttestations);
|
|
1601
|
-
const branches = [...this.#ops.values()].sort(
|
|
1602
|
-
(a, b) => compareBytes(opToBytes(a.op), opToBytes(b.op))
|
|
1603
|
-
);
|
|
1604
|
-
const total = attestations.length + branches.length;
|
|
1605
|
-
if (total === 0) {
|
|
1606
|
-
throw new EmptyTimestampError("an empty timestamp cannot be serialized");
|
|
1607
|
-
}
|
|
1608
|
-
let index = 0;
|
|
1609
|
-
for (const attestation of attestations) {
|
|
1610
|
-
if (index < total - 1) ctx.writeByte(255);
|
|
1611
|
-
ctx.writeByte(0);
|
|
1612
|
-
serializeAttestation(ctx, attestation);
|
|
1613
|
-
index++;
|
|
1614
|
-
}
|
|
1615
|
-
for (const { op, stamp } of branches) {
|
|
1616
|
-
if (index < total - 1) ctx.writeByte(255);
|
|
1617
|
-
op.serialize(ctx);
|
|
1618
|
-
stamp.serialize(ctx);
|
|
1619
|
-
index++;
|
|
1620
|
-
}
|
|
1621
|
-
}
|
|
1622
|
-
/**
|
|
1623
|
-
* Añade una op a este nodo y devuelve el sub-timestamp de su resultado.
|
|
1624
|
-
* Si la op (por contenido) ya existe, devuelve la rama existente.
|
|
1625
|
-
*/
|
|
1626
|
-
add(op) {
|
|
1627
|
-
const key = opKey(op);
|
|
1628
|
-
const existing = this.#ops.get(key);
|
|
1629
|
-
if (existing !== void 0) {
|
|
1630
|
-
return existing.stamp;
|
|
1631
|
-
}
|
|
1632
|
-
const stamp = new _Timestamp(op.call(this.msg));
|
|
1633
|
-
this.#ops.set(key, { op, stamp });
|
|
1634
|
-
return stamp;
|
|
1635
|
-
}
|
|
1636
|
-
/**
|
|
1637
|
-
* Vincula `op` a un sub-timestamp YA EXISTENTE, compartiendo el objeto (no crea uno nuevo).
|
|
1638
|
-
* A diferencia de `add`, hace que esta rama apunte al mismo `Timestamp` que otra rama
|
|
1639
|
-
* (de otro nodo) ya construyó, de modo que las attestations añadidas más arriba sean
|
|
1640
|
-
* alcanzables desde ambos caminos. Lo usa el árbol Merkle para el cross-link izquierda/derecha.
|
|
1641
|
-
* Falla (fail-closed) si `stamp` no es un Timestamp o si `op.call(this.msg)` no coincide con `stamp.msg`.
|
|
1642
|
-
*/
|
|
1643
|
-
addExisting(op, stamp) {
|
|
1644
|
-
if (!(stamp instanceof _Timestamp)) {
|
|
1645
|
-
throw new TypeError("addExisting requires a Timestamp");
|
|
1646
|
-
}
|
|
1647
|
-
if (!bytesEqual(op.call(this.msg), stamp.msg)) {
|
|
1648
|
-
throw new MergeError("operation result does not match the existing timestamp message");
|
|
1649
|
-
}
|
|
1650
|
-
this.#ops.set(opKey(op), { op, stamp });
|
|
1651
|
-
return stamp;
|
|
1652
|
-
}
|
|
1653
|
-
/** Incorpora las attestations y ramas de `other` (mismo `msg`) en este timestamp. */
|
|
1654
|
-
merge(other) {
|
|
1655
|
-
if (!(other instanceof _Timestamp)) {
|
|
1656
|
-
throw new MergeError("can only merge Timestamps together");
|
|
1657
|
-
}
|
|
1658
|
-
if (!bytesEqual(this.msg, other.msg)) {
|
|
1659
|
-
throw new MergeError("cannot merge timestamps for different messages");
|
|
1660
|
-
}
|
|
1661
|
-
for (const attestation of other.attestations) {
|
|
1662
|
-
if (!this.attestations.some((existing) => attestationsEqual(existing, attestation))) {
|
|
1663
|
-
this.attestations.push(attestation);
|
|
1664
|
-
}
|
|
1665
|
-
}
|
|
1666
|
-
for (const { op, stamp } of other.#ops.values()) {
|
|
1667
|
-
const key = opKey(op);
|
|
1668
|
-
let branch = this.#ops.get(key);
|
|
1669
|
-
if (branch === void 0) {
|
|
1670
|
-
branch = { op, stamp: new _Timestamp(op.call(this.msg)) };
|
|
1671
|
-
this.#ops.set(key, branch);
|
|
1672
|
-
}
|
|
1673
|
-
branch.stamp.merge(stamp);
|
|
1674
|
-
}
|
|
1675
|
-
}
|
|
1676
|
-
/** Todas las attestations del árbol con el msg de su nodo (sin pérdida de datos). */
|
|
1677
|
-
allAttestations() {
|
|
1678
|
-
const result = [];
|
|
1679
|
-
for (const attestation of this.attestations) {
|
|
1680
|
-
result.push({ msg: this.msg.slice(), attestation });
|
|
1681
|
-
}
|
|
1682
|
-
for (const { stamp } of this.#ops.values()) {
|
|
1683
|
-
result.push(...stamp.allAttestations());
|
|
1684
|
-
}
|
|
1685
|
-
return result;
|
|
1686
|
-
}
|
|
1687
|
-
/** Todas las attestations del árbol (sin el msg asociado). */
|
|
1688
|
-
getAttestations() {
|
|
1689
|
-
return this.allAttestations().map((entry) => entry.attestation);
|
|
1690
|
-
}
|
|
1691
|
-
/** Verdadero si el árbol contiene una attestation verificable localmente (Bitcoin/Litecoin). */
|
|
1692
|
-
isTimestampComplete() {
|
|
1693
|
-
return this.allAttestations().some(
|
|
1694
|
-
({ attestation }) => attestation.kind === "bitcoin" || attestation.kind === "litecoin"
|
|
1695
|
-
);
|
|
1696
|
-
}
|
|
1697
|
-
/** Sub-timestamps que tienen attestations directas. */
|
|
1698
|
-
directlyVerified() {
|
|
1699
|
-
if (this.attestations.length > 0) {
|
|
1700
|
-
return [this];
|
|
1701
|
-
}
|
|
1702
|
-
const result = [];
|
|
1703
|
-
for (const { stamp } of this.#ops.values()) {
|
|
1704
|
-
result.push(...stamp.directlyVerified());
|
|
1705
|
-
}
|
|
1706
|
-
return result;
|
|
1707
|
-
}
|
|
1708
|
-
/** Los mensajes de las hojas del árbol (nodos sin ops). */
|
|
1709
|
-
allTips() {
|
|
1710
|
-
if (this.#ops.size === 0) {
|
|
1711
|
-
return [this.msg.slice()];
|
|
1712
|
-
}
|
|
1713
|
-
const result = [];
|
|
1714
|
-
for (const { stamp } of this.#ops.values()) {
|
|
1715
|
-
result.push(...stamp.allTips());
|
|
1716
|
-
}
|
|
1717
|
-
return result;
|
|
1718
|
-
}
|
|
1719
|
-
/** Igualdad estructural recursiva con otro timestamp. */
|
|
1720
|
-
equals(other) {
|
|
1721
|
-
if (!(other instanceof _Timestamp)) {
|
|
1722
|
-
return false;
|
|
1723
|
-
}
|
|
1724
|
-
if (!bytesEqual(this.msg, other.msg)) {
|
|
1725
|
-
return false;
|
|
1726
|
-
}
|
|
1727
|
-
if (this.attestations.length !== other.attestations.length) {
|
|
1728
|
-
return false;
|
|
1729
|
-
}
|
|
1730
|
-
const ours = [...this.attestations].sort(compareAttestations);
|
|
1731
|
-
const theirs = [...other.attestations].sort(compareAttestations);
|
|
1732
|
-
for (let i = 0; i < ours.length; i++) {
|
|
1733
|
-
if (!attestationsEqual(ours[i], theirs[i])) {
|
|
1734
|
-
return false;
|
|
1735
|
-
}
|
|
1736
|
-
}
|
|
1737
|
-
if (this.#ops.size !== other.#ops.size) {
|
|
1738
|
-
return false;
|
|
1739
|
-
}
|
|
1740
|
-
for (const [key, branch] of this.#ops) {
|
|
1741
|
-
const otherBranch = other.#ops.get(key);
|
|
1742
|
-
if (otherBranch === void 0) {
|
|
1743
|
-
return false;
|
|
1744
|
-
}
|
|
1745
|
-
if (!branch.stamp.equals(otherBranch.stamp)) {
|
|
1746
|
-
return false;
|
|
1747
|
-
}
|
|
1748
|
-
}
|
|
1749
|
-
return true;
|
|
1750
|
-
}
|
|
1751
|
-
};
|
|
1752
|
-
function catSha256(left, right) {
|
|
1753
|
-
if (!(left instanceof Timestamp) || !(right instanceof Timestamp)) {
|
|
1754
|
-
throw new TypeError("catSha256 requires two Timestamps");
|
|
1755
|
-
}
|
|
1756
|
-
const concat = right.add(new OpPrepend(left.msg));
|
|
1757
|
-
left.addExisting(new OpAppend(right.msg), concat);
|
|
1758
|
-
return concat.add(new OpSHA256());
|
|
1759
|
-
}
|
|
1760
|
-
function makeMerkleTree(timestamps) {
|
|
1761
|
-
if (timestamps.length === 0) {
|
|
1762
|
-
throw new EmptyMerkleTreeError("makeMerkleTree requires at least one timestamp");
|
|
1763
|
-
}
|
|
1764
|
-
for (const stamp of timestamps) {
|
|
1765
|
-
if (!(stamp instanceof Timestamp)) {
|
|
1766
|
-
throw new TypeError("makeMerkleTree requires an array of Timestamps");
|
|
1767
|
-
}
|
|
1768
|
-
}
|
|
1769
|
-
let round = [...timestamps];
|
|
1770
|
-
while (round.length > 1) {
|
|
1771
|
-
const next = [];
|
|
1772
|
-
for (let i = 0; i < round.length; i += 2) {
|
|
1773
|
-
if (i + 1 < round.length) {
|
|
1774
|
-
next.push(catSha256(round[i], round[i + 1]));
|
|
1775
|
-
} else {
|
|
1776
|
-
next.push(round[i]);
|
|
1777
|
-
}
|
|
1778
|
-
}
|
|
1779
|
-
round = next;
|
|
1780
|
-
}
|
|
1781
|
-
return round[0];
|
|
1782
|
-
}
|
|
1783
|
-
var HEADER_MAGIC = new Uint8Array([
|
|
1784
|
-
0,
|
|
1785
|
-
79,
|
|
1786
|
-
112,
|
|
1787
|
-
101,
|
|
1788
|
-
110,
|
|
1789
|
-
84,
|
|
1790
|
-
105,
|
|
1791
|
-
109,
|
|
1792
|
-
101,
|
|
1793
|
-
115,
|
|
1794
|
-
116,
|
|
1795
|
-
97,
|
|
1796
|
-
109,
|
|
1797
|
-
112,
|
|
1798
|
-
115,
|
|
1799
|
-
0,
|
|
1800
|
-
0,
|
|
1801
|
-
80,
|
|
1802
|
-
114,
|
|
1803
|
-
111,
|
|
1804
|
-
111,
|
|
1805
|
-
102,
|
|
1806
|
-
0,
|
|
1807
|
-
191,
|
|
1808
|
-
137,
|
|
1809
|
-
226,
|
|
1810
|
-
232,
|
|
1811
|
-
132,
|
|
1812
|
-
232,
|
|
1813
|
-
146,
|
|
1814
|
-
148
|
|
1815
|
-
]);
|
|
1816
|
-
var MAJOR_VERSION = 1;
|
|
1817
|
-
var DetachedTimestampFile = class _DetachedTimestampFile {
|
|
1818
|
-
fileHashOp;
|
|
1819
|
-
timestamp;
|
|
1820
|
-
constructor(fileHashOp, timestamp) {
|
|
1821
|
-
if (!(fileHashOp instanceof CryptOp)) {
|
|
1822
|
-
throw new TypeError("DetachedTimestampFile: fileHashOp must be a CryptOp");
|
|
1823
|
-
}
|
|
1824
|
-
if (!(timestamp instanceof Timestamp)) {
|
|
1825
|
-
throw new TypeError("DetachedTimestampFile: timestamp must be a Timestamp");
|
|
1826
|
-
}
|
|
1827
|
-
if (timestamp.msg.length !== fileHashOp.digestLength) {
|
|
1828
|
-
throw new TypeError(
|
|
1829
|
-
`DetachedTimestampFile: timestamp message length ${timestamp.msg.length} does not match ${fileHashOp.tagName} digest length ${fileHashOp.digestLength}`
|
|
1830
|
-
);
|
|
1831
|
-
}
|
|
1832
|
-
this.fileHashOp = fileHashOp;
|
|
1833
|
-
this.timestamp = timestamp;
|
|
1834
|
-
}
|
|
1835
|
-
/** Digest del fichero sellado (copia defensiva: mutarla no afecta al objeto). */
|
|
1836
|
-
fileDigest() {
|
|
1837
|
-
return this.timestamp.getDigest();
|
|
1838
|
-
}
|
|
1839
|
-
/** Escribe el fichero `.ots` en el contexto: magic → versión → op → digest → árbol. */
|
|
1840
|
-
serialize(ctx) {
|
|
1841
|
-
ctx.writeBytes(HEADER_MAGIC);
|
|
1842
|
-
ctx.writeVaruint(MAJOR_VERSION);
|
|
1843
|
-
this.fileHashOp.serialize(ctx);
|
|
1844
|
-
ctx.writeBytes(this.timestamp.msg);
|
|
1845
|
-
this.timestamp.serialize(ctx);
|
|
1846
|
-
}
|
|
1847
|
-
/** Serializa el fichero `.ots` completo a bytes. */
|
|
1848
|
-
serializeToBytes() {
|
|
1849
|
-
const ctx = new StreamSerializationContext();
|
|
1850
|
-
this.serialize(ctx);
|
|
1851
|
-
return ctx.getOutput();
|
|
1852
|
-
}
|
|
1853
|
-
/**
|
|
1854
|
-
* Lee un fichero `.ots` desde bytes. Único tipo de entrada: `Uint8Array` (fail-closed;
|
|
1855
|
-
* elimina los 4 tipos del original y el bug `Array.from(ArrayBuffer) → []`).
|
|
1856
|
-
*/
|
|
1857
|
-
static deserialize(input) {
|
|
1858
|
-
if (!(input instanceof Uint8Array)) {
|
|
1859
|
-
throw new TypeError("DetachedTimestampFile.deserialize expects a Uint8Array");
|
|
1860
|
-
}
|
|
1861
|
-
const ctx = new StreamDeserializationContext(input);
|
|
1862
|
-
ctx.assertMagic(HEADER_MAGIC);
|
|
1863
|
-
const major = ctx.readVaruint();
|
|
1864
|
-
if (major !== MAJOR_VERSION) {
|
|
1865
|
-
throw new UnsupportedVersionError(`unsupported .ots major version ${major}`);
|
|
1866
|
-
}
|
|
1867
|
-
const op = Op.deserialize(ctx);
|
|
1868
|
-
if (!(op instanceof CryptOp)) {
|
|
1869
|
-
throw new DeserializationError("file hash operation must be a cryptographic hash");
|
|
1870
|
-
}
|
|
1871
|
-
const fileHash = ctx.read(op.digestLength);
|
|
1872
|
-
const timestamp = Timestamp.deserialize(ctx, fileHash);
|
|
1873
|
-
ctx.assertEof();
|
|
1874
|
-
return new _DetachedTimestampFile(op, timestamp);
|
|
1875
|
-
}
|
|
1876
|
-
/** Crea un `.ots` nuevo hasheando el contenido completo de un fichero. */
|
|
1877
|
-
static fromBytes(fileHashOp, fileContent) {
|
|
1878
|
-
if (!(fileHashOp instanceof CryptOp)) {
|
|
1879
|
-
throw new TypeError("DetachedTimestampFile.fromBytes: fileHashOp must be a CryptOp");
|
|
1880
|
-
}
|
|
1881
|
-
const digest = fileHashOp.hashFile(fileContent);
|
|
1882
|
-
return new _DetachedTimestampFile(fileHashOp, new Timestamp(digest));
|
|
1883
|
-
}
|
|
1884
|
-
/** Crea un `.ots` nuevo a partir de un digest ya calculado del fichero. */
|
|
1885
|
-
static fromHash(fileHashOp, fileDigest) {
|
|
1886
|
-
return new _DetachedTimestampFile(fileHashOp, new Timestamp(fileDigest));
|
|
1887
|
-
}
|
|
1888
|
-
/** Igualdad estructural con otro fichero `.ots`. */
|
|
1889
|
-
equals(other) {
|
|
1890
|
-
return other instanceof _DetachedTimestampFile && this.fileHashOp.equals(other.fileHashOp) && this.timestamp.equals(other.timestamp);
|
|
1891
|
-
}
|
|
1892
|
-
};
|
|
488
|
+
// src/core/orchestration.ts
|
|
489
|
+
var import_core4 = require("@otskit/core");
|
|
1893
490
|
|
|
1894
491
|
// src/network/calendar.ts
|
|
492
|
+
var import_core2 = require("@otskit/core");
|
|
1895
493
|
var MAX_CALENDAR_RESPONSE_SIZE = 1e4;
|
|
1896
494
|
function assertCommitment(bytes) {
|
|
1897
495
|
if (!(bytes instanceof Uint8Array)) {
|
|
@@ -1917,7 +515,7 @@ var CalendarClient = class {
|
|
|
1917
515
|
url;
|
|
1918
516
|
networkLayer;
|
|
1919
517
|
logger;
|
|
1920
|
-
/**
|
|
518
|
+
/** Envia un digest al calendario y devuelve el Timestamp que lo commit-ea. */
|
|
1921
519
|
async submit(digest, signal) {
|
|
1922
520
|
assertCommitment(digest);
|
|
1923
521
|
this.logger?.debug(`Submitting digest to ${this.url}/digest`);
|
|
@@ -1928,10 +526,10 @@ var CalendarClient = class {
|
|
|
1928
526
|
);
|
|
1929
527
|
return this.#parseTimestamp(response.data, digest);
|
|
1930
528
|
}
|
|
1931
|
-
/** Pregunta al calendario si tiene un Timestamp
|
|
529
|
+
/** Pregunta al calendario si tiene un Timestamp mas completo para `commitment` (upgrade). */
|
|
1932
530
|
async getTimestamp(commitment, signal) {
|
|
1933
531
|
assertCommitment(commitment);
|
|
1934
|
-
const path = `/timestamp/${bytesToHex(commitment)}`;
|
|
532
|
+
const path = `/timestamp/${(0, import_core2.bytesToHex)(commitment)}`;
|
|
1935
533
|
this.logger?.debug(`Querying ${this.url}${path}`);
|
|
1936
534
|
let response;
|
|
1937
535
|
try {
|
|
@@ -1957,62 +555,85 @@ var CalendarClient = class {
|
|
|
1957
555
|
`calendar response of ${data.length} bytes exceeds limit ${MAX_CALENDAR_RESPONSE_SIZE}`
|
|
1958
556
|
);
|
|
1959
557
|
}
|
|
1960
|
-
const ctx = new StreamDeserializationContext(data);
|
|
1961
|
-
const timestamp = Timestamp.deserialize(ctx, commitment);
|
|
558
|
+
const ctx = new import_core2.StreamDeserializationContext(data);
|
|
559
|
+
const timestamp = import_core2.Timestamp.deserialize(ctx, commitment);
|
|
1962
560
|
ctx.assertEof();
|
|
1963
561
|
return timestamp;
|
|
1964
562
|
}
|
|
1965
563
|
};
|
|
1966
|
-
function
|
|
1967
|
-
|
|
1968
|
-
|
|
564
|
+
function parseWhitelistPattern(raw) {
|
|
565
|
+
let parsed;
|
|
566
|
+
try {
|
|
567
|
+
parsed = new URL(raw);
|
|
568
|
+
} catch {
|
|
569
|
+
return void 0;
|
|
570
|
+
}
|
|
571
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return void 0;
|
|
572
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
573
|
+
const wildcardSuffix = hostname.startsWith("*.") ? hostname.slice(2) : void 0;
|
|
574
|
+
if (hostname.includes("*") && wildcardSuffix === void 0) return void 0;
|
|
575
|
+
if (wildcardSuffix !== void 0 && (wildcardSuffix.length === 0 || wildcardSuffix.includes("*"))) return void 0;
|
|
576
|
+
return {
|
|
577
|
+
protocol: parsed.protocol,
|
|
578
|
+
hostname,
|
|
579
|
+
port: parsed.port,
|
|
580
|
+
pathname: parsed.pathname,
|
|
581
|
+
wildcardSuffix
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
function hostnameMatchesPattern(hostname, pattern) {
|
|
585
|
+
if (pattern.wildcardSuffix === void 0) return hostname === pattern.hostname;
|
|
586
|
+
if (!hostname.endsWith("." + pattern.wildcardSuffix)) return false;
|
|
587
|
+
const label = hostname.slice(0, -pattern.wildcardSuffix.length - 1);
|
|
588
|
+
return label.length > 0 && !label.includes(".");
|
|
1969
589
|
}
|
|
1970
590
|
var UrlWhitelist = class {
|
|
1971
|
-
#patterns = /* @__PURE__ */ new
|
|
591
|
+
#patterns = /* @__PURE__ */ new Map();
|
|
1972
592
|
constructor(urls) {
|
|
1973
593
|
if (urls) {
|
|
1974
594
|
for (const u of urls) this.add(u);
|
|
1975
595
|
}
|
|
1976
596
|
}
|
|
1977
|
-
/**
|
|
597
|
+
/** Anade un patron; si no trae esquema, se anaden las variantes http y https. */
|
|
1978
598
|
add(url) {
|
|
1979
599
|
if (typeof url !== "string") {
|
|
1980
600
|
throw new TypeError("UrlWhitelist: URL must be a string");
|
|
1981
601
|
}
|
|
1982
602
|
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
1983
|
-
|
|
603
|
+
const pattern = parseWhitelistPattern(url);
|
|
604
|
+
if (pattern !== void 0) this.#patterns.set(url, pattern);
|
|
1984
605
|
} else {
|
|
1985
|
-
this
|
|
1986
|
-
this
|
|
606
|
+
this.add("http://" + url);
|
|
607
|
+
this.add("https://" + url);
|
|
1987
608
|
}
|
|
1988
609
|
}
|
|
1989
|
-
/** Verdadero si `url` casa con
|
|
610
|
+
/** Verdadero si `url` casa con algun patron de la whitelist. */
|
|
1990
611
|
contains(url) {
|
|
1991
|
-
|
|
1992
|
-
|
|
612
|
+
let parsed;
|
|
613
|
+
try {
|
|
614
|
+
parsed = new URL(url);
|
|
615
|
+
} catch {
|
|
616
|
+
return false;
|
|
617
|
+
}
|
|
618
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
|
|
619
|
+
if (parsed.search !== "" || parsed.hash !== "") return false;
|
|
620
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
621
|
+
for (const pattern of this.#patterns.values()) {
|
|
622
|
+
if (parsed.protocol !== pattern.protocol || parsed.port !== pattern.port) continue;
|
|
623
|
+
if (parsed.pathname !== pattern.pathname) continue;
|
|
624
|
+
if (hostnameMatchesPattern(hostname, pattern)) return true;
|
|
1993
625
|
}
|
|
1994
626
|
return false;
|
|
1995
627
|
}
|
|
1996
628
|
toString() {
|
|
1997
|
-
return `UrlWhitelist([${[...this.#patterns].join(", ")}])`;
|
|
629
|
+
return `UrlWhitelist([${[...this.#patterns.keys()].join(", ")}])`;
|
|
1998
630
|
}
|
|
1999
631
|
};
|
|
2000
|
-
var DEFAULT_CALENDAR_WHITELIST = new UrlWhitelist([
|
|
2001
|
-
|
|
2002
|
-
// Peter Todd
|
|
2003
|
-
"https://*.calendar.eternitywall.com",
|
|
2004
|
-
// Eternity Wall
|
|
2005
|
-
"https://*.calendar.catallaxy.com"
|
|
2006
|
-
// Catallaxy
|
|
2007
|
-
]);
|
|
2008
|
-
var DEFAULT_AGGREGATORS = [
|
|
2009
|
-
"https://a.pool.opentimestamps.org",
|
|
2010
|
-
"https://b.pool.opentimestamps.org",
|
|
2011
|
-
"https://a.pool.eternitywall.com",
|
|
2012
|
-
"https://ots.btc.catallaxy.com"
|
|
2013
|
-
];
|
|
632
|
+
var DEFAULT_CALENDAR_WHITELIST = new UrlWhitelist([...import_core2.TRUSTED_CALENDAR_WHITELIST_PATTERNS]);
|
|
633
|
+
var DEFAULT_AGGREGATORS = [...import_core2.DEFAULT_AGGREGATOR_URLS];
|
|
2014
634
|
|
|
2015
635
|
// src/network/esplora.ts
|
|
636
|
+
var import_core3 = require("@otskit/core");
|
|
2016
637
|
var PUBLIC_ESPLORA_URL = "https://blockstream.info/api";
|
|
2017
638
|
var MAX_ESPLORA_RESPONSE_SIZE = 1e5;
|
|
2018
639
|
var HEX64_RE = /^[0-9a-f]{64}$/i;
|
|
@@ -2092,19 +713,125 @@ var EsploraClient = class {
|
|
|
2092
713
|
`esplora response of ${data.length} bytes exceeds limit ${MAX_ESPLORA_RESPONSE_SIZE}`
|
|
2093
714
|
);
|
|
2094
715
|
}
|
|
2095
|
-
|
|
716
|
+
try {
|
|
717
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(data);
|
|
718
|
+
} catch (cause) {
|
|
719
|
+
throw new EsploraResponseError("esplora response contains invalid UTF-8 bytes", {
|
|
720
|
+
cause: cause instanceof Error ? cause : void 0
|
|
721
|
+
});
|
|
722
|
+
}
|
|
2096
723
|
}
|
|
2097
724
|
};
|
|
2098
725
|
async function verifyTimestampAttestation(digest, attestation, explorer, signal) {
|
|
2099
726
|
if (attestation.kind !== "bitcoin" && attestation.kind !== "litecoin") {
|
|
2100
|
-
throw new VerificationError(`cannot verify a '${attestation.kind}' attestation against the chain`);
|
|
727
|
+
throw new import_core3.VerificationError(`cannot verify a '${attestation.kind}' attestation against the chain`);
|
|
2101
728
|
}
|
|
2102
729
|
const hash = await explorer.blockHash(attestation.height, signal);
|
|
2103
730
|
const header = await explorer.block(hash, signal);
|
|
2104
|
-
return verifyAgainstBlockheader(digest, header);
|
|
731
|
+
return (0, import_core3.verifyAgainstBlockheader)(digest, header);
|
|
2105
732
|
}
|
|
2106
733
|
|
|
2107
734
|
// src/core/orchestration.ts
|
|
735
|
+
var import_node_crypto = require("crypto");
|
|
736
|
+
|
|
737
|
+
// src/security/ssrf.ts
|
|
738
|
+
var import_promises = require("dns/promises");
|
|
739
|
+
var import_node_net = require("net");
|
|
740
|
+
var BLOCKED_CIDRS_V4 = [
|
|
741
|
+
{ network: 0, mask: 4278190080, label: "0.0.0.0/8 (This Network)" },
|
|
742
|
+
{ network: 167772160, mask: 4278190080, label: "10.0.0.0/8 (RFC 1918)" },
|
|
743
|
+
{ network: 2130706432, mask: 4278190080, label: "127.0.0.0/8 (Loopback)" },
|
|
744
|
+
{ network: 2851995648, mask: 4294901760, label: "169.254.0.0/16 (Link-local/IMDS)" },
|
|
745
|
+
{ network: 2886729728, mask: 4293918720, label: "172.16.0.0/12 (RFC 1918)" },
|
|
746
|
+
{ network: 3232235520, mask: 4294901760, label: "192.168.0.0/16 (RFC 1918)" },
|
|
747
|
+
{ network: 3323068416, mask: 4294836224, label: "198.18.0.0/15 (Benchmarking)" },
|
|
748
|
+
{ network: 3758096384, mask: 4026531840, label: "224.0.0.0/4 (Multicast)" },
|
|
749
|
+
{ network: 4026531840, mask: 4026531840, label: "240.0.0.0/4 (Reserved)" },
|
|
750
|
+
{ network: 4294967295, mask: 4294967295, label: "255.255.255.255 (Broadcast)" }
|
|
751
|
+
];
|
|
752
|
+
function ipv4ToUint32(ip) {
|
|
753
|
+
const parts = ip.split(".");
|
|
754
|
+
return (parseInt(parts[0], 10) << 24 | parseInt(parts[1], 10) << 16 | parseInt(parts[2], 10) << 8 | parseInt(parts[3], 10)) >>> 0;
|
|
755
|
+
}
|
|
756
|
+
function assertNotPrivateIPv4(ip, calendarUrl) {
|
|
757
|
+
const n = ipv4ToUint32(ip);
|
|
758
|
+
for (const cidr of BLOCKED_CIDRS_V4) {
|
|
759
|
+
if ((n & cidr.mask) >>> 0 === cidr.network) {
|
|
760
|
+
throw new ValidationError(
|
|
761
|
+
`Calendar URL "${calendarUrl}" resolves to a private/reserved IPv4 address (${ip} \u2014 ${cidr.label}). Set allowPrivateCalendars: true to override.`
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
var BLOCKED_IPV6_PREFIXES = [
|
|
767
|
+
{ prefix: "::1", label: "Loopback" },
|
|
768
|
+
{ prefix: "::", label: "Unspecified" },
|
|
769
|
+
{ prefix: "fc", label: "fc00::/7 (Unique Local)" },
|
|
770
|
+
{ prefix: "fd", label: "fd00::/8 (Unique Local)" },
|
|
771
|
+
{ prefix: "fe8", label: "fe80::/10 (Link-local)" },
|
|
772
|
+
{ prefix: "fe9", label: "fe80::/10 (Link-local)" },
|
|
773
|
+
{ prefix: "fea", label: "fe80::/10 (Link-local)" },
|
|
774
|
+
{ prefix: "feb", label: "fe80::/10 (Link-local)" },
|
|
775
|
+
{ prefix: "ff", label: "ff00::/8 (Multicast)" },
|
|
776
|
+
{ prefix: "::ffff:", label: "IPv4-mapped IPv6" },
|
|
777
|
+
{ prefix: "64:ff9b:", label: "64:ff9b::/96 (NAT64)" },
|
|
778
|
+
{ prefix: "2001:db8", label: "2001:db8::/32 (Documentation)" }
|
|
779
|
+
];
|
|
780
|
+
function assertNotPrivateIPv6(ip, calendarUrl) {
|
|
781
|
+
const lower = ip.toLowerCase();
|
|
782
|
+
for (const { prefix, label } of BLOCKED_IPV6_PREFIXES) {
|
|
783
|
+
if (lower === prefix || lower.startsWith(prefix)) {
|
|
784
|
+
throw new ValidationError(
|
|
785
|
+
`Calendar URL "${calendarUrl}" resolves to a private/reserved IPv6 address (${ip} \u2014 ${label}). Set allowPrivateCalendars: true to override.`
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
async function assertSafeCalendarUrl(url, options) {
|
|
791
|
+
let parsed;
|
|
792
|
+
try {
|
|
793
|
+
parsed = new URL(url);
|
|
794
|
+
} catch {
|
|
795
|
+
throw new ValidationError(`Calendar URL is not valid: "${url}"`);
|
|
796
|
+
}
|
|
797
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
798
|
+
throw new ValidationError(`Calendar URL must use http or https: "${url}"`);
|
|
799
|
+
}
|
|
800
|
+
if (parsed.username || parsed.password) {
|
|
801
|
+
throw new ValidationError(`Calendar URL must not contain embedded credentials: "${url}"`);
|
|
802
|
+
}
|
|
803
|
+
if (options.allowPrivate) return;
|
|
804
|
+
const hostname = parsed.hostname;
|
|
805
|
+
const host = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
|
|
806
|
+
const ipVersion = (0, import_node_net.isIP)(host);
|
|
807
|
+
if (ipVersion === 4) {
|
|
808
|
+
assertNotPrivateIPv4(host, url);
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
if (ipVersion === 6) {
|
|
812
|
+
assertNotPrivateIPv6(host, url);
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
let addresses;
|
|
816
|
+
try {
|
|
817
|
+
addresses = await (0, import_promises.lookup)(hostname, { all: true });
|
|
818
|
+
} catch (err) {
|
|
819
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
820
|
+
throw new ValidationError(
|
|
821
|
+
`Calendar URL hostname "${hostname}" could not be resolved: ${message}`
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
if (addresses.length === 0) {
|
|
825
|
+
throw new ValidationError(`Calendar URL hostname "${hostname}" resolved to no addresses`);
|
|
826
|
+
}
|
|
827
|
+
for (const { address, family } of addresses) {
|
|
828
|
+
if (family === 4) assertNotPrivateIPv4(address, url);
|
|
829
|
+
else if (family === 6) assertNotPrivateIPv6(address, url);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// src/core/orchestration.ts
|
|
834
|
+
var MAX_BITCOIN_ATTESTATIONS = 10;
|
|
2108
835
|
function validateHash(hash) {
|
|
2109
836
|
if (typeof hash === "string") {
|
|
2110
837
|
const hex = hash.trim().toLowerCase();
|
|
@@ -2126,19 +853,12 @@ function secureNonce(n) {
|
|
|
2126
853
|
globalThis.crypto.getRandomValues(bytes);
|
|
2127
854
|
return bytes;
|
|
2128
855
|
}
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
try {
|
|
2133
|
-
parsed = new URL(url);
|
|
2134
|
-
} catch {
|
|
2135
|
-
throw new ValidationError(`${label} is not a valid URL: ${url}`);
|
|
2136
|
-
}
|
|
2137
|
-
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
2138
|
-
throw new ValidationError(`${label} must use http(s): ${url}`);
|
|
2139
|
-
}
|
|
856
|
+
function timingSafeEq(a, b) {
|
|
857
|
+
if (a.length !== b.length) return false;
|
|
858
|
+
return (0, import_node_crypto.timingSafeEqual)(a, b);
|
|
2140
859
|
}
|
|
2141
|
-
|
|
860
|
+
var bytesEqFast = (a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b)) === 0;
|
|
861
|
+
async function orchestrateStamp(hash, calendars, networkLayer, logger, signal, minimumSuccessfulSubmissions = 2, allowPrivateCalendars = false) {
|
|
2142
862
|
if (calendars.length === 0) {
|
|
2143
863
|
throw new ValidationError("at least one calendar is required to stamp");
|
|
2144
864
|
}
|
|
@@ -2150,13 +870,15 @@ async function orchestrateStamp(hash, calendars, networkLayer, logger, signal, m
|
|
|
2150
870
|
`minimumSuccessfulSubmissions (${minimumSuccessfulSubmissions}) cannot exceed the number of calendars (${calendars.length})`
|
|
2151
871
|
);
|
|
2152
872
|
}
|
|
2153
|
-
|
|
873
|
+
await Promise.all(
|
|
874
|
+
calendars.map((url) => assertSafeCalendarUrl(url, { allowPrivate: allowPrivateCalendars }))
|
|
875
|
+
);
|
|
2154
876
|
const digest = validateHash(hash);
|
|
2155
877
|
logger?.info(`Starting stamp for ${Buffer.from(digest).toString("hex")}`);
|
|
2156
|
-
const detached = DetachedTimestampFile.fromHash(new OpSHA256(), digest);
|
|
2157
|
-
const nonceAppended = detached.timestamp.add(new OpAppend(secureNonce(16)));
|
|
2158
|
-
const merkleRoot = nonceAppended.add(new OpSHA256());
|
|
2159
|
-
const merkleTip = makeMerkleTree([merkleRoot]);
|
|
878
|
+
const detached = import_core4.DetachedTimestampFile.fromHash(new import_core4.OpSHA256(), digest);
|
|
879
|
+
const nonceAppended = detached.timestamp.add(new import_core4.OpAppend(secureNonce(16)));
|
|
880
|
+
const merkleRoot = nonceAppended.add(new import_core4.OpSHA256());
|
|
881
|
+
const merkleTip = (0, import_core4.makeMerkleTree)([merkleRoot]);
|
|
2160
882
|
const results = await Promise.allSettled(
|
|
2161
883
|
calendars.map((url) => new CalendarClient(url, networkLayer, logger).submit(merkleTip.getDigest(), signal))
|
|
2162
884
|
);
|
|
@@ -2186,7 +908,7 @@ async function orchestrateStamp(hash, calendars, networkLayer, logger, signal, m
|
|
|
2186
908
|
async function orchestrateUpgrade(incompleteProof, _calendars, networkLayer, logger, signal) {
|
|
2187
909
|
let detached;
|
|
2188
910
|
try {
|
|
2189
|
-
detached = DetachedTimestampFile.deserialize(new Uint8Array(incompleteProof));
|
|
911
|
+
detached = import_core4.DetachedTimestampFile.deserialize(new Uint8Array(incompleteProof));
|
|
2190
912
|
} catch (error) {
|
|
2191
913
|
throw new ValidationError("Invalid .ots proof format", {
|
|
2192
914
|
/* v8 ignore next */
|
|
@@ -2222,7 +944,7 @@ async function orchestrateUpgrade(incompleteProof, _calendars, networkLayer, log
|
|
|
2222
944
|
}
|
|
2223
945
|
}
|
|
2224
946
|
const after = detached.serializeToBytes();
|
|
2225
|
-
if (
|
|
947
|
+
if (bytesEqFast(before, after)) {
|
|
2226
948
|
throw new UpgradeError("No calendar has confirmed the timestamp yet (Bitcoin not yet mined)");
|
|
2227
949
|
}
|
|
2228
950
|
return Buffer.from(after);
|
|
@@ -2230,43 +952,82 @@ async function orchestrateUpgrade(incompleteProof, _calendars, networkLayer, log
|
|
|
2230
952
|
async function orchestrateVerify(proof, networkLayer, originalDataHash, logger, signal) {
|
|
2231
953
|
let detached;
|
|
2232
954
|
try {
|
|
2233
|
-
detached = DetachedTimestampFile.deserialize(new Uint8Array(proof));
|
|
2234
|
-
} catch {
|
|
2235
|
-
|
|
955
|
+
detached = import_core4.DetachedTimestampFile.deserialize(new Uint8Array(proof));
|
|
956
|
+
} catch (cause) {
|
|
957
|
+
throw new ValidationError("Invalid .ots proof format", {
|
|
958
|
+
cause: cause instanceof Error ? cause : void 0
|
|
959
|
+
});
|
|
2236
960
|
}
|
|
2237
961
|
if (originalDataHash !== void 0) {
|
|
2238
962
|
let expected;
|
|
2239
963
|
try {
|
|
2240
964
|
expected = validateHash(originalDataHash);
|
|
2241
965
|
} catch (err) {
|
|
2242
|
-
|
|
966
|
+
throw new ValidationError(
|
|
967
|
+
err instanceof Error ? err.message : "Invalid hash format",
|
|
968
|
+
{ cause: err instanceof Error ? err : void 0 }
|
|
969
|
+
);
|
|
2243
970
|
}
|
|
2244
|
-
if (!
|
|
2245
|
-
return {
|
|
971
|
+
if (!timingSafeEq(expected, detached.fileDigest())) {
|
|
972
|
+
return { status: "invalid", reason: "File hash does not match proof \u2014 file may have been modified" };
|
|
2246
973
|
}
|
|
2247
974
|
}
|
|
2248
|
-
const
|
|
975
|
+
const allBitcoin = detached.timestamp.allAttestations().filter(({ attestation }) => attestation.kind === "bitcoin");
|
|
976
|
+
const seenHeights = /* @__PURE__ */ new Set();
|
|
977
|
+
const deduped = allBitcoin.filter(({ attestation }) => {
|
|
978
|
+
if (attestation.kind !== "bitcoin") return false;
|
|
979
|
+
if (seenHeights.has(attestation.height)) {
|
|
980
|
+
logger?.debug(`Skipping duplicate Bitcoin attestation at height ${attestation.height}`);
|
|
981
|
+
return false;
|
|
982
|
+
}
|
|
983
|
+
seenHeights.add(attestation.height);
|
|
984
|
+
return true;
|
|
985
|
+
});
|
|
986
|
+
const bitcoinAtts = deduped.slice(0, MAX_BITCOIN_ATTESTATIONS);
|
|
987
|
+
if (deduped.length > MAX_BITCOIN_ATTESTATIONS) {
|
|
988
|
+
logger?.warn(
|
|
989
|
+
`Proof has ${deduped.length} unique Bitcoin attestations; verifying only the first ${MAX_BITCOIN_ATTESTATIONS}`
|
|
990
|
+
);
|
|
991
|
+
}
|
|
2249
992
|
if (bitcoinAtts.length === 0) {
|
|
2250
993
|
const hasLitecoin = detached.timestamp.allAttestations().some(({ attestation }) => attestation.kind === "litecoin");
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
994
|
+
return {
|
|
995
|
+
status: "pending",
|
|
996
|
+
reason: hasLitecoin ? "Litecoin-only attestation is not supported by this client" : "No Bitcoin attestation found \u2014 timestamp not yet confirmed"
|
|
997
|
+
};
|
|
2255
998
|
}
|
|
2256
999
|
const explorer = new EsploraClient(networkLayer);
|
|
2257
|
-
let
|
|
1000
|
+
let lastNetworkError;
|
|
1001
|
+
let lastCryptoError;
|
|
2258
1002
|
for (const { msg, attestation } of bitcoinAtts) {
|
|
2259
1003
|
if (attestation.kind !== "bitcoin") continue;
|
|
2260
1004
|
try {
|
|
2261
|
-
const
|
|
1005
|
+
const blockTime = await verifyTimestampAttestation(
|
|
1006
|
+
Uint8Array.from(msg).reverse(),
|
|
1007
|
+
attestation,
|
|
1008
|
+
explorer,
|
|
1009
|
+
signal
|
|
1010
|
+
);
|
|
2262
1011
|
logger?.info(`Verified against Bitcoin block ${attestation.height}`);
|
|
2263
|
-
return {
|
|
1012
|
+
return { status: "verified", blockHeight: attestation.height, blockTime };
|
|
2264
1013
|
} catch (err) {
|
|
2265
|
-
|
|
2266
|
-
|
|
1014
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1015
|
+
if (err instanceof NetworkError || err instanceof EsploraResponseError) {
|
|
1016
|
+
lastNetworkError = message;
|
|
1017
|
+
logger?.warn(`Network error at block ${attestation.height}: ${message}`);
|
|
1018
|
+
} else {
|
|
1019
|
+
lastCryptoError = message;
|
|
1020
|
+
logger?.warn(`Crypto verification failed at block ${attestation.height}: ${message}`);
|
|
1021
|
+
}
|
|
2267
1022
|
}
|
|
2268
1023
|
}
|
|
2269
|
-
|
|
1024
|
+
if (lastCryptoError !== void 0) {
|
|
1025
|
+
return { status: "invalid", reason: `Cryptographic verification failed: ${lastCryptoError}` };
|
|
1026
|
+
}
|
|
1027
|
+
return {
|
|
1028
|
+
status: "network_error",
|
|
1029
|
+
reason: `Could not reach Bitcoin blockchain: ${lastNetworkError ?? "unknown error"}`
|
|
1030
|
+
};
|
|
2270
1031
|
}
|
|
2271
1032
|
|
|
2272
1033
|
// src/client.ts
|
|
@@ -2276,6 +1037,7 @@ var OpenTimestampsClient = class {
|
|
|
2276
1037
|
logger;
|
|
2277
1038
|
globalSignal;
|
|
2278
1039
|
minimumSuccessfulSubmissions;
|
|
1040
|
+
allowPrivateCalendars;
|
|
2279
1041
|
/**
|
|
2280
1042
|
* Create a new OpenTimestamps client
|
|
2281
1043
|
*
|
|
@@ -2289,6 +1051,7 @@ var OpenTimestampsClient = class {
|
|
|
2289
1051
|
this.calendars = options.calendars;
|
|
2290
1052
|
}
|
|
2291
1053
|
this.minimumSuccessfulSubmissions = options.minimumSuccessfulSubmissions ?? 2;
|
|
1054
|
+
this.allowPrivateCalendars = options.allowPrivateCalendars ?? false;
|
|
2292
1055
|
const resilienceConfig = {
|
|
2293
1056
|
...DEFAULT_RESILIENCE,
|
|
2294
1057
|
...options.resilience,
|
|
@@ -2307,7 +1070,8 @@ var OpenTimestampsClient = class {
|
|
|
2307
1070
|
};
|
|
2308
1071
|
this.logger = options.logger;
|
|
2309
1072
|
this.globalSignal = options.signal;
|
|
2310
|
-
|
|
1073
|
+
const internalOptions = options;
|
|
1074
|
+
this.networkLayer = internalOptions._networkLayer ?? new ResilientNetworkLayer(resilienceConfig, this.logger);
|
|
2311
1075
|
this.logger?.info(`OpenTimestamps client initialized with ${this.calendars.length} calendars`);
|
|
2312
1076
|
}
|
|
2313
1077
|
/**
|
|
@@ -2336,7 +1100,8 @@ var OpenTimestampsClient = class {
|
|
|
2336
1100
|
this.networkLayer,
|
|
2337
1101
|
this.logger,
|
|
2338
1102
|
signal,
|
|
2339
|
-
this.minimumSuccessfulSubmissions
|
|
1103
|
+
this.minimumSuccessfulSubmissions,
|
|
1104
|
+
this.allowPrivateCalendars
|
|
2340
1105
|
);
|
|
2341
1106
|
}
|
|
2342
1107
|
/**
|
|
@@ -2421,6 +1186,10 @@ var OpenTimestampsClient = class {
|
|
|
2421
1186
|
}
|
|
2422
1187
|
};
|
|
2423
1188
|
|
|
1189
|
+
// src/index.ts
|
|
1190
|
+
var import_core5 = require("@otskit/core");
|
|
1191
|
+
var import_core6 = require("@otskit/core");
|
|
1192
|
+
|
|
2424
1193
|
// src/utils/hash.ts
|
|
2425
1194
|
var import_crypto = require("crypto");
|
|
2426
1195
|
var import_fs = require("fs");
|
|
@@ -2454,13 +1223,16 @@ function hashFile(path) {
|
|
|
2454
1223
|
OpenTimestampsClientError,
|
|
2455
1224
|
PUBLIC_ESPLORA_URL,
|
|
2456
1225
|
ResilientNetworkLayer,
|
|
1226
|
+
SizeLimitExceededError,
|
|
2457
1227
|
StampError,
|
|
2458
1228
|
Timestamp,
|
|
2459
1229
|
UpgradeError,
|
|
2460
1230
|
UrlWhitelist,
|
|
2461
1231
|
ValidationError,
|
|
1232
|
+
assertSafeCalendarUrl,
|
|
2462
1233
|
hashBuffer,
|
|
2463
1234
|
hashFile,
|
|
1235
|
+
isVerified,
|
|
2464
1236
|
verifyAgainstBlockheader,
|
|
2465
1237
|
verifyTimestampAttestation
|
|
2466
1238
|
});
|