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.
614 lines
21 KiB
614 lines
21 KiB
// The release object of spec v0.15, §47.1, as provider/release.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; 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 { equalBytes, 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. Its round is a bigint
|
|
* only when drand's JSON names a round above 2^53-1, which no DateKey has:
|
|
* step 10 then reports ERR_ROUND_MISMATCH with its exact digits, as Go's
|
|
* uint64 does.
|
|
*/
|
|
export interface ParsedRelease {
|
|
readonly round: number | bigint;
|
|
readonly signature: Uint8Array;
|
|
readonly chainHash?: Uint8Array;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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, 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);
|
|
}
|
|
|
|
// The JSON of a drand relay, read as Go's encoding/json reads it into
|
|
// {Round *uint64; Signature *string; Randomness string}: the members of the
|
|
// object in order, each matched to a field by its name without case, the
|
|
// last one winning; null leaves a field as it was for Randomness and unset
|
|
// for the other two; a value of another type, or a round that is not an
|
|
// integer of 0 to 2^64-1, fails the whole input.
|
|
async function parseDrandJSON(b: Uint8Array): Promise<ParsedRelease> {
|
|
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 text = new TextDecoder().decode(b);
|
|
const members = jsonMembers(text);
|
|
if (members === undefined) throw malformed();
|
|
let round: bigint | undefined;
|
|
let signature: string | undefined;
|
|
let randomness = "";
|
|
let typeError = false;
|
|
for (const [key, raw] of members) {
|
|
const name = foldName(key);
|
|
if (name === "round") {
|
|
if (raw === "null") round = undefined;
|
|
else if (/^(0|[1-9][0-9]*)$/.test(raw) && BigInt(raw) < 2n ** 64n)
|
|
round = BigInt(raw);
|
|
else typeError = true;
|
|
} else if (name === "signature" || name === "randomness") {
|
|
if (raw === "null") {
|
|
if (name === "signature") signature = undefined;
|
|
} else if (raw.startsWith('"')) {
|
|
const v = JSON.parse(raw) as string;
|
|
if (name === "signature") signature = v;
|
|
else randomness = v;
|
|
} else {
|
|
typeError = true;
|
|
}
|
|
}
|
|
}
|
|
if (typeError || 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 = Uint8Array.from(signature.match(/../g) ?? [], (h) =>
|
|
parseInt(h, 16),
|
|
);
|
|
if (
|
|
randomness !== "" &&
|
|
randomness.toLowerCase() !== toHex(await sha256(sig))
|
|
) {
|
|
throw new DateKeysError(
|
|
"ERR_RELEASE_INVALID",
|
|
"provider: drand JSON: randomness does not match the signature",
|
|
);
|
|
}
|
|
return {
|
|
round: round <= BigInt(MAX_SAFE_UINT) ? Number(round) : round,
|
|
signature: sig,
|
|
};
|
|
}
|
|
|
|
// Go's encoding/json matches a key to a field name without case: ASCII
|
|
// letters in lower case, and any other character as unicode.ToLower of
|
|
// unicode.ToUpper, so that U+017F (long s) is an s and U+212A (Kelvin) a k.
|
|
function foldName(s: string): string {
|
|
let out = "";
|
|
for (const c of s)
|
|
out +=
|
|
c.charCodeAt(0) < 0x80 ? c.toLowerCase() : c.toUpperCase().toLowerCase();
|
|
return out;
|
|
}
|
|
|
|
// The members of the object that `text` is, as [key, raw value text] in
|
|
// their order, or undefined when `text` is not one JSON object. JSON.parse
|
|
// checks the grammar, the same as Go's; the scan then only splits valid text.
|
|
function jsonMembers(text: string): [string, string][] | undefined {
|
|
// Its first byte other than a space is "{": valid JSON is an object.
|
|
try {
|
|
JSON.parse(text);
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
const out: [string, string][] = [];
|
|
let i = 0;
|
|
const ws = (): void => {
|
|
while (i < text.length && " \t\n\r".includes(text[i]!)) i++;
|
|
};
|
|
const skipString = (): void => {
|
|
i++;
|
|
while (text[i] !== '"') i += text[i] === "\\" ? 2 : 1;
|
|
i++;
|
|
};
|
|
const skipValue = (): void => {
|
|
if (text[i] === '"') return skipString();
|
|
if (text[i] === "{" || text[i] === "[") {
|
|
let depth = 0;
|
|
do {
|
|
const c = text[i]!;
|
|
if (c === '"') {
|
|
skipString();
|
|
continue;
|
|
}
|
|
if (c === "{" || c === "[") depth++;
|
|
else if (c === "}" || c === "]") depth--;
|
|
i++;
|
|
} while (depth > 0);
|
|
return;
|
|
}
|
|
while (i < text.length && !",}] \t\n\r".includes(text[i]!)) i++;
|
|
};
|
|
ws();
|
|
i++; // {
|
|
ws();
|
|
while (text[i] !== "}") {
|
|
const k0 = i;
|
|
skipString();
|
|
const key = JSON.parse(text.slice(k0, i)) as string;
|
|
ws();
|
|
i++; // :
|
|
ws();
|
|
const v0 = i;
|
|
skipValue();
|
|
out.push([key, text.slice(v0, i)]);
|
|
ws();
|
|
if (text[i] === ",") {
|
|
i++;
|
|
ws();
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* 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 });
|
|
}
|
|
}
|