You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
DateKeys-App/src/lib/dkc/releaseobject.ts

765 lines
25 KiB

// The release object of spec v0.15, §47.1, as provider/release.go,
// provider/drandjson.go and provider/archive.go of the Go reference: the
// release of a round as data that is kept: an entry of a release cache or
// archive and the answer of a release cache or of the Release API; drand's
// JSON, which a reader accepts too as the input of the caller, read strictly
// since spec v0.16; the sources of a release in the caller's hand
// (provider.Supplier); and a local release archive, the informative format
// of §50. The texts of the errors are those of the reference.
//
// No noble here: the verification of a release is release.ts. This module
// only decodes, so that the page can read what the person gives before
// loading the code that opens a capsule.
import {
decodeUtf8,
equalBytes,
fromHex,
goQuote,
sha256,
toHex,
utf8Length,
} from "./bytes.ts";
import {
checkSchema,
Decoder,
Encoder,
MAX_SAFE_UINT,
peek,
unmarshal,
} from "./cbor.ts";
import { DateKeysError, withContext } from "./errors.ts";
import { QUICKNET_SCHEME, type Profile } from "./profile.ts";
/** The type tag of the release object (key 0). */
export const RELEASE_TYPE_TAG = "datekeys-release";
/** The schema version of the release object (key 1). */
export const RELEASE_SCHEMA_VERSION = 1;
/**
* The largest release object a reader decodes (spec v0.15, §47.1): the
* object has no frame, and an input of more bytes, or of none, is rejected
* before it is decoded, with ERR_NON_CANONICAL_CBOR. No valid encoding comes
* close to it.
*/
export const MAX_RELEASE_OBJECT_SIZE = 1024;
/** The longest signature of the schema: a compressed point of G2 of BLS12-381. */
export const MAX_SIGNATURE_LEN = 96;
/** The largest drand JSON read as a release, the bound of a relay response. */
export const MAX_RELEASE_JSON_SIZE = 8 << 10;
/** The type tag of the header of a release archive (spec v0.15, §50). */
export const ARCHIVE_TYPE_TAG = "datekeys-release-archive";
/** The schema version of the header of a release archive. */
export const ARCHIVE_SCHEMA_VERSION = 1;
const RELEASE_KEYS = 5;
const ARCHIVE_KEYS = 5;
// Its five keys take at most 1 + 26 + 2 + 35 + 9 + 9 bytes.
const MAX_ARCHIVE_HEADER = 128;
// The length of a signature of Quicknet, a compressed point of G1.
const QUICKNET_SIGNATURE_LEN = 48;
/** The material that satisfies a round: for drand, the BLS signature of the round. */
export interface Release {
readonly round: number;
readonly signature: Uint8Array;
/**
* The chain the release names, key 2 of a release object (spec v0.15,
* §47.1), or undefined when it names none, as the answer of a relay and
* drand's JSON. verifyRelease compares it with the pinned profile.
*/
readonly chainHash?: Uint8Array;
}
/**
* A release as the caller gives it, before step 10. Since spec v0.16 the
* round of drand's JSON is at most 2^53 - 1, as that of a release object
* (§47.1), so it is a Release.
*/
export type ParsedRelease = Release;
// ---------------------------------------------------------------------------
// The release object
interface ReleaseWire {
chainHash: Uint8Array;
round: number;
signature: Uint8Array;
}
function encodeWire(e: Encoder, w: ReleaseWire): void {
e.map(RELEASE_KEYS);
e.uint(0);
e.text(RELEASE_TYPE_TAG);
e.uint(1);
e.uint(RELEASE_SCHEMA_VERSION);
e.uint(2);
e.bstr(w.chainHash);
e.uint(3);
e.uint(w.round);
e.uint(4);
e.bstr(w.signature);
}
// Reads the map with every CDDL rule of the release object, all of them
// ERR_NON_CANONICAL_CBOR: what each field means against the pinned profile
// and the DateKey is checked by verifyRelease, at step 10.
function decodeWire(d: Decoder): ReleaseWire {
const w: ReleaseWire = {
chainHash: new Uint8Array(0),
round: 0,
signature: new Uint8Array(0),
};
const pairs = d.map(RELEASE_KEYS);
if (pairs !== RELEASE_KEYS)
throw new DateKeysError(
"ERR_NON_CANONICAL_CBOR",
`${pairs} keys, want all ${RELEASE_KEYS}`,
);
for (let want = 0; want < RELEASE_KEYS; want++) {
const k = d.key();
if (k !== want)
throw new DateKeysError(
"ERR_NON_CANONICAL_CBOR",
`key ${k} where key ${want} was expected`,
);
withContext(`key ${k}`, () => {
switch (want) {
case 0:
d.text(utf8Length(RELEASE_TYPE_TAG));
break;
case 1:
d.uint(RELEASE_SCHEMA_VERSION);
break;
case 2:
w.chainHash = d.bstr(32, 32);
break;
case 3:
w.round = d.uint(MAX_SAFE_UINT);
if (w.round === 0)
throw new DateKeysError("ERR_NON_CANONICAL_CBOR", "round 0");
break;
default:
w.signature = d.bstr(1, MAX_SIGNATURE_LEN);
}
});
}
d.endMap();
return w;
}
/**
* The release object of `r`, as a release cache or archive keeps it (spec v0.15,
* §47.1): its chain hash, its round and its signature, as
* provider.EncodeRelease. It does not verify the release: verifyRelease
* does, against the pinned profile.
*/
export function encodeRelease(r: Release): Uint8Array {
const chainHash = r.chainHash ?? new Uint8Array(0);
if (chainHash.length !== 32) {
throw new DateKeysError(
"ERR_NON_CANONICAL_CBOR",
`provider: release object: chain hash of ${chainHash.length} bytes, want 32`,
);
}
if (!Number.isSafeInteger(r.round) || r.round < 1) {
throw new DateKeysError(
"ERR_NON_CANONICAL_CBOR",
`provider: release object: round ${r.round} outside 1..${MAX_SAFE_UINT}`,
);
}
if (r.signature.length === 0 || r.signature.length > MAX_SIGNATURE_LEN) {
throw new DateKeysError(
"ERR_NON_CANONICAL_CBOR",
`provider: release object: signature of ${r.signature.length} bytes outside 1..${MAX_SIGNATURE_LEN}`,
);
}
const e = new Encoder();
encodeWire(e, { chainHash, round: r.round, signature: r.signature });
return e.out();
}
/**
* Decodes a release object (spec v0.15, §47.1) with the layers of spec §69.1
* that it has, as provider.DecodeRelease: its size, 1 to
* MAX_RELEASE_OBJECT_SIZE bytes; its type and schema version
* (ERR_NON_CANONICAL_CBOR, then ERR_UNSUPPORTED_VERSION); its encoding and
* schema (ERR_NON_CANONICAL_CBOR). The release names its chain, which
* verifyRelease checks against the pinned profile at step 10 of spec §63.
*/
export function decodeRelease(b: Uint8Array): Release {
if (b.length === 0 || b.length > MAX_RELEASE_OBJECT_SIZE) {
throw new DateKeysError(
"ERR_NON_CANONICAL_CBOR",
`provider: release object of ${b.length} bytes, outside 1..${MAX_RELEASE_OBJECT_SIZE}`,
);
}
const w = withContext("provider: release object", () => {
checkSchema(b, RELEASE_TYPE_TAG, RELEASE_SCHEMA_VERSION);
return unmarshal(b, decodeWire, encodeWire);
});
return { round: w.round, signature: w.signature, chainHash: w.chainHash };
}
/**
* Reports whether `b` is read as drand's JSON rather than as a release
* object: its first byte other than a JSON space is "{" (spec v0.15, §47.1).
*/
export function isDrandJSON(b: Uint8Array): boolean {
for (const c of b) {
if (c === 0x20 || c === 0x09 || c === 0x0a || c === 0x0d) continue;
return c === 0x7b;
}
return false;
}
/**
* Reads a release that the caller supplies, as provider.ParseRelease:
* drand's JSON when isDrandJSON, read strictly by parseDrandJSON, or else a
* release object, with decodeRelease. drand's JSON is the answer of a relay,
* {"round": …, "signature": "…"}, with an optional "randomness" that must be
* SHA-256 of the signature; it does not name its chain, so the release has no
* chain hash, and any failure to read it is ERR_RELEASE_INVALID. It is
* accepted as input, never written.
*/
export async function parseRelease(b: Uint8Array): Promise<ParsedRelease> {
return isDrandJSON(b) ? parseDrandJSON(b) : decodeRelease(b);
}
// ---------------------------------------------------------------------------
// drand's JSON, read strictly
//
// The strict reading of drand's JSON (spec v0.16, §47.1), as
// provider/drandjson.go of the reference: JSON of RFC 8259, in UTF-8, whose
// value is an object; no object of the JSON repeats a name, and names are
// compared exactly, code point by code point, once their escapes are
// decoded, so that "\u0072ound" is round and Round is another name; and an
// escape of a surrogate that does not pair with the next one makes the JSON
// malformed. A common JSON reader keeps the last of two repeated names, or
// does not tell upper from lower case in them, and two readers would see two
// rounds in the same input.
/**
* A member of the outer object of drand's JSON: its name, decoded; the kind
* of its value, its first character ('"', '{', '[', 't', 'f' or 'n'), or '0'
* for a number; the text of the value as written; and for a string, the
* string decoded.
*/
export interface JSONMember {
readonly name: string;
readonly kind: string;
readonly raw: string;
readonly str: string;
}
/**
* Reads `b` as a JSON object with the strict rules of spec v0.16, §47.1, and
* returns the members of the outer object in their order, or undefined when
* `b` breaks one: it is not UTF-8, its value is not an object, an object
* repeats a name, or a string escapes a surrogate without its pair.
*/
export function strictJSON(b: Uint8Array): JSONMember[] | undefined {
const text = decodeUtf8(b);
if (text === undefined) return undefined;
const r = new JSONReader(text);
r.space();
if (!r.at("{")) return undefined;
const members = r.object(true);
r.space();
return members !== undefined && r.i === text.length ? members : undefined;
}
// Reads JSON from the text s at i. The text is decoded UTF-8, so it holds no
// lone surrogate, and a byte order mark at its start stays, as a character
// that is not a space of JSON.
class JSONReader {
i = 0;
private readonly s: string;
constructor(s: string) {
this.s = s;
}
at(c: string): boolean {
return this.s[this.i] === c;
}
// Skips the four spaces of JSON.
space(): void {
while (this.i < this.s.length && " \t\n\r".includes(this.s[this.i]!))
this.i++;
}
// One value of any kind, undefined when it breaks the grammar.
value(): Omit<JSONMember, "name"> | undefined {
const start = this.i;
const c = this.s[this.i];
let kind = c;
let str = "";
let ok = false;
if (c === "{") ok = this.object(false) !== undefined;
else if (c === "[") ok = this.array();
else if (c === '"') {
const v = this.string();
ok = v !== undefined;
str = v ?? "";
} else if (c === "t") ok = this.literal("true");
else if (c === "f") ok = this.literal("false");
else if (c === "n") ok = this.literal("null");
else if (c === "-" || (c !== undefined && c >= "0" && c <= "9")) {
kind = "0";
ok = this.number();
}
return ok
? { kind: kind!, raw: this.s.slice(start, this.i), str }
: undefined;
}
// An object, with no name twice; its members when keep is set.
object(keep: boolean): JSONMember[] | undefined {
this.i++; // {
this.space();
const out: JSONMember[] = [];
if (this.at("}")) {
this.i++;
return out;
}
const seen = new Set<string>();
for (;;) {
this.space();
if (!this.at('"')) return undefined;
const name = this.string();
if (name === undefined || seen.has(name)) return undefined;
seen.add(name);
this.space();
if (!this.at(":")) return undefined;
this.i++;
this.space();
const m = this.value();
if (m === undefined) return undefined;
if (keep) out.push({ name, ...m });
this.space();
if (this.at(",")) this.i++;
else if (this.at("}")) {
this.i++;
return out;
} else return undefined;
}
}
array(): boolean {
this.i++; // [
this.space();
if (this.at("]")) {
this.i++;
return true;
}
for (;;) {
this.space();
if (this.value() === undefined) return false;
this.space();
if (this.at(",")) this.i++;
else if (this.at("]")) {
this.i++;
return true;
} else return false;
}
}
// A string, with its escapes decoded; a surrogate escaped alone, without
// its pair, breaks it.
string(): string | undefined {
this.i++; // "
let out = "";
while (this.i < this.s.length) {
const c = this.s[this.i]!;
if (c === '"') {
this.i++;
return out;
}
if (c < " ") return undefined;
if (c !== "\\") {
out += c;
this.i++;
continue;
}
const e = this.s[this.i + 1];
this.i += 2;
const k = e === undefined ? -1 : '"\\/bfnrt'.indexOf(e);
if (k >= 0) {
out += '"\\/\b\f\n\r\t'[k];
continue;
}
if (e !== "u") return undefined;
let u = this.hex4();
if (u === undefined || (u >= 0xdc00 && u <= 0xdfff)) return undefined;
if (u >= 0xd800 && u <= 0xdbff) {
if (!this.s.startsWith("\\u", this.i)) return undefined;
this.i += 2;
const low = this.hex4();
if (low === undefined || low < 0xdc00 || low > 0xdfff)
return undefined;
u = 0x10000 + ((u - 0xd800) << 10) + (low - 0xdc00);
}
out += String.fromCodePoint(u);
}
return undefined;
}
// The four hexadecimal digits of an escape \u.
hex4(): number | undefined {
const h = this.s.slice(this.i, this.i + 4);
if (!/^[0-9a-fA-F]{4}$/.test(h)) return undefined;
this.i += 4;
return parseInt(h, 16);
}
literal(word: string): boolean {
if (!this.s.startsWith(word, this.i)) return false;
this.i += word.length;
return true;
}
// A number of the grammar of RFC 8259: a minus, an integer part without
// leading zeros, and an optional fraction and exponent.
number(): boolean {
const digits = (): number => {
const from = this.i;
while (this.i < this.s.length && /[0-9]/.test(this.s[this.i]!))
this.i++;
return this.i - from;
};
if (this.at("-")) this.i++;
if (this.at("0")) this.i++;
else if (digits() === 0) return false;
if (this.at(".")) {
this.i++;
if (digits() === 0) return false;
}
if (this.at("e") || this.at("E")) {
this.i++;
if (this.at("+") || this.at("-")) this.i++;
if (digits() === 0) return false;
}
return true;
}
}
/**
* The round of drand's JSON (spec v0.16, §47.1): a number without sign,
* fraction or exponent, from 1 to 2^53 - 1, as in the release object;
* undefined for any other member.
*/
export function jsonRound(m: JSONMember): number | undefined {
if (m.kind !== "0" || !/^[1-9][0-9]{0,15}$/.test(m.raw)) return undefined;
const n = Number(m.raw);
return n <= MAX_SAFE_UINT ? n : undefined;
}
/**
* Reads the JSON of a drand relay with the strict rules of spec v0.16, §47.1,
* as provider.ParseDrandJSON: at most MAX_RELEASE_JSON_SIZE bytes of JSON
* whose value is an object, with no repeated name and names compared exactly
* once their escapes are decoded; "round" a number without sign, fraction or
* exponent, from 1 to 2^53 - 1; "signature" a string of hexadecimal, in lower
* or upper case; and "randomness", when present, a string with SHA-256 of the
* signature in hexadecimal. Other members are ignored. Any failure is
* ERR_RELEASE_INVALID, with the texts of the reference. The release names no
* chain. The page reads the answers of the relays of drand with it too.
*/
export async function parseDrandJSON(b: Uint8Array): Promise<Release> {
if (b.length > MAX_RELEASE_JSON_SIZE) {
throw new DateKeysError(
"ERR_RELEASE_INVALID",
`provider: drand JSON of ${b.length} bytes, larger than ${MAX_RELEASE_JSON_SIZE}`,
);
}
const malformed = (): DateKeysError =>
new DateKeysError(
"ERR_RELEASE_INVALID",
"provider: drand JSON: malformed, or without round or signature",
);
const members = strictJSON(b);
if (members === undefined) throw malformed();
let round: number | undefined;
let signature: string | undefined;
let randomness: string | undefined;
for (const m of members) {
if (m.name === "round") {
round = jsonRound(m);
if (round === undefined) throw malformed();
} else if (m.name === "signature" || m.name === "randomness") {
if (m.kind !== '"') throw malformed();
if (m.name === "signature") signature = m.str;
else randomness = m.str;
}
}
if (round === undefined || signature === undefined) throw malformed();
if (!/^([0-9a-fA-F]{2})*$/.test(signature)) {
throw new DateKeysError(
"ERR_RELEASE_INVALID",
"provider: drand JSON: signature is not hex",
);
}
const sig = fromHex(signature);
if (
randomness !== undefined &&
randomness.toLowerCase() !== toHex(await sha256(sig))
) {
throw new DateKeysError(
"ERR_RELEASE_INVALID",
"provider: drand JSON: randomness does not match the signature",
);
}
return { round, signature: sig };
}
/**
* The release object of a release of the profile `p`, with the chain hash
* of `p`, as provider.NewReleaseObject: what a reader would keep after
* verifying the release (spec v0.15, §47.1).
*/
export function newReleaseObject(p: Profile, r: Release): Uint8Array {
return encodeRelease({
round: r.round,
signature: r.signature,
chainHash: p.chainHash,
});
}
// ---------------------------------------------------------------------------
// A release in the caller's hand
/**
* A release that the caller has in hand (spec v0.15, §49, §63 step 9.c), as
* provider.Supplier: a release object read from a file, drand's JSON that
* the person saved, or an entry of a local archive. It makes no network
* request, so open asks it for the release without comparing its clock with
* the round time: a valid signature proves that the round was published.
*
* `supply` returns the encoding of the release of `round`, as it is: a
* release object or drand's JSON, which open decodes and verifies at step
* 10 with the codes of that step. Without a release for `round` it throws
* ERR_RELEASE_UNAVAILABLE, the code of step 9.
*/
export interface ReleaseSupplier {
supply(p: Profile, round: number): Promise<Uint8Array>;
}
/**
* A release in hand, already read, as provider.Encoded: the bytes of a
* release object or of drand's JSON. It supplies itself whatever the round;
* step 10 compares its round with the DateKey.
*/
export function encodedRelease(b: Uint8Array): ReleaseSupplier {
return { supply: () => Promise.resolve(b) };
}
// ---------------------------------------------------------------------------
// A local release archive (informative, spec v0.15, §50)
// The text of what a read threw, for an error of the archive.
const errText = (err: unknown): string =>
err instanceof Error ? err.message : String(err);
interface ArchiveHeader {
chainHash: Uint8Array;
first: number;
count: number;
}
function encodeArchiveWire(e: Encoder, h: ArchiveHeader): void {
e.map(ARCHIVE_KEYS);
e.uint(0);
e.text(ARCHIVE_TYPE_TAG);
e.uint(1);
e.uint(ARCHIVE_SCHEMA_VERSION);
e.uint(2);
e.bstr(h.chainHash);
e.uint(3);
e.uint(h.first);
e.uint(4);
e.uint(h.count);
}
// The header decoder of the reference, whose own failures carry no code: the
// format is informative, and every failure is ERR_RELEASE_UNAVAILABLE.
function decodeArchiveWire(d: Decoder): ArchiveHeader {
const h: ArchiveHeader = { chainHash: new Uint8Array(0), first: 0, count: 0 };
const pairs = d.map(ARCHIVE_KEYS);
if (pairs !== ARCHIVE_KEYS)
throw new Error(`${pairs} keys, want all ${ARCHIVE_KEYS}`);
for (let want = 0; want < ARCHIVE_KEYS; want++) {
const k = d.key();
if (k !== want) throw new Error(`key ${k} where key ${want} was expected`);
try {
switch (want) {
case 0:
d.text(utf8Length(ARCHIVE_TYPE_TAG));
break;
case 1:
d.uint(ARCHIVE_SCHEMA_VERSION);
break;
case 2:
h.chainHash = d.bstr(32, 32);
break;
case 3:
h.first = d.uint(MAX_SAFE_UINT);
break;
default:
h.count = d.uint(MAX_SAFE_UINT);
}
} catch (err) {
/* v8 ignore next -- @preserve: the decoder throws only DateKeysError */
throw new Error(`key ${k}: ${errText(err)}`);
}
}
d.endMap();
return h;
}
/**
* The header of an archive of `count` rounds of the chain `chainHash` from
* the round `first` (spec v0.15, §50), as provider.EncodeArchiveHeader. The
* signatures follow it, one after another, each with the length of a
* signature of the chain, and a round the archive lacks is written as zeros.
*/
export function encodeArchiveHeader(
chainHash: Uint8Array,
first: number,
count: number,
): Uint8Array {
if (
chainHash.length !== 32 ||
!Number.isSafeInteger(first) ||
!Number.isSafeInteger(count) ||
first < 1 ||
count < 1 ||
first > MAX_SAFE_UINT - count + 1
) {
throw new Error(
`provider: archive header: chain hash of ${chainHash.length} bytes, rounds ${first} to ${first} + ${count} - 1`,
);
}
const e = new Encoder();
encodeArchiveWire(e, { chainHash, first, count });
return e.out();
}
/** Reports whether `b`, the start of a file, is the start of a release archive. */
export function isReleaseArchive(b: Uint8Array): boolean {
try {
return peek(b).typeTag === ARCHIVE_TYPE_TAG;
} catch {
return false;
}
}
/**
* A local release archive, the informative format of spec v0.15, §50, as
* provider.Archive: a header in deterministic CBOR, {0:
* "datekeys-release-archive", 1: 1, 2: chain_hash, 3: first round, 4: number
* of rounds}, followed by the signatures, so that the one of round r starts
* at the end of the header plus (r - first)·n, with n the length of a
* signature of the chain, 48 bytes in Quicknet. A round written as zeros is
* missing.
*
* A local archive is a release in hand: its entry is decoded and verified at
* step 10 like a release object. A round it lacks, a header it cannot read,
* an archive of another chain or of another length are failures to supply a
* release, ERR_RELEASE_UNAVAILABLE at step 9: the format has no codes of its
* own. Of a Blob only the header and one signature are read.
*/
export class ReleaseArchive implements ReleaseSupplier {
readonly #data: Uint8Array | Blob;
constructor(data: Uint8Array | Blob) {
this.#data = data;
}
async #read(start: number, length: number): Promise<Uint8Array> {
const d = this.#data;
if (d instanceof Uint8Array) return d.subarray(start, start + length);
return new Uint8Array(await d.slice(start, start + length).arrayBuffer());
}
async supply(p: Profile, round: number): Promise<Uint8Array> {
const unavailable = (detail: string): DateKeysError =>
new DateKeysError(
"ERR_RELEASE_UNAVAILABLE",
`provider: release archive: ${detail}`,
);
const size =
this.#data instanceof Uint8Array ? this.#data.length : this.#data.size;
let head: Uint8Array;
try {
head = await this.#read(0, Math.min(size, MAX_ARCHIVE_HEADER));
} catch (err) {
throw unavailable(errText(err));
}
try {
checkSchema(head, ARCHIVE_TYPE_TAG, ARCHIVE_SCHEMA_VERSION);
} catch {
throw unavailable(`not an archive of version ${ARCHIVE_SCHEMA_VERSION}`);
}
let h: ArchiveHeader;
try {
h = decodeArchiveWire(new Decoder(head));
} catch (err) {
throw unavailable(`its header does not decode: ${errText(err)}`);
}
// The header is the deterministic encoding of what it says: its length
// is that of the encoding, and the signatures follow it. The strict
// decoder already refuses any other encoding; the check is the one of
// the reference.
const e = new Encoder();
encodeArchiveWire(e, h);
const enc = e.out();
/* v8 ignore next 3 -- @preserve: what the strict decoder read encodes back to the same bytes */
if (!equalBytes(enc, head.subarray(0, Math.min(enc.length, head.length)))) {
throw unavailable(
"its header is not the deterministic encoding of its value",
);
}
if (!equalBytes(h.chainHash, p.chainHash)) {
throw unavailable(
`archive of chain ${toHex(h.chainHash)}, the pinned profile ${p.id} is chain ${toHex(p.chainHash)}`,
);
}
if (
h.first === 0 ||
h.count === 0 ||
round < h.first ||
round - h.first >= h.count
) {
throw unavailable(
`round ${round} is not in the archive, which holds ${h.count} rounds from ${h.first}`,
);
}
if (p.provider !== "drand")
throw unavailable(
`profile ${p.id}: provider ${goQuote(p.provider)} is not drand: ERR_UNKNOWN_PROFILE`,
);
if (p.scheme !== QUICKNET_SCHEME)
throw unavailable(
`profile ${p.id}: ${goQuote(p.scheme)} is not a drand scheme: ERR_UNKNOWN_PROFILE`,
);
const n = QUICKNET_SIGNATURE_LEN;
const want = BigInt(enc.length) + BigInt(h.count) * BigInt(n);
if (BigInt(h.count) > (1n << 62n) / BigInt(n) || BigInt(size) !== want) {
throw unavailable(
`${size} bytes, its header announces ${h.count} rounds of ${n} bytes`,
);
}
let sig: Uint8Array;
try {
sig = await this.#read(enc.length + (round - h.first) * n, n);
} catch (err) {
throw unavailable(errText(err));
}
if (sig.every((x) => x === 0))
throw unavailable(`round ${round} is missing: its entry is zeros`);
return encodeRelease({ chainHash: h.chainHash, round, signature: sig });
}
}

Powered by TurnKey Linux.