@d20dao/vrf-sdk 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/epoch.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { AbiCoder, Contract, getAddress, hexlify, id, keccak256, getBytes, toUtf8Bytes, verifyMessage } from "ethers";
2
- import { canonicalApiRequest, attestationDigest, hashAttestation, validateApiSignatureEncoding } from "./sources.js";
2
+ import { canonicalApiRequest, canonicalPassthroughRequest, parsePassthroughRequest, attestationDigest, hashAttestation, validateApiSignatureEncoding } from "./sources.js";
3
3
  import { encodeDataTemplate, matchesDataTemplate, validateDataTemplate } from "./templates.js";
4
+ import { BEACON_TEMPLATE, beaconCanonicalRequest, beaconRoundTime, beaconSlotSigner, decodeBeaconRound, verifyBeaconRound } from "./beacon.js";
4
5
  import { deriveRequestSeed, hashPublicKey, hashProof, verifyVRFProof } from "./verification.js";
5
6
  import { hashMapping, mapRandomness } from "./mapping.js";
6
7
  const abi = AbiCoder.defaultAbiCoder();
@@ -40,19 +41,54 @@ export const BUILTIN_EPOCH_RECIPES = frozen([
40
41
  builtin(5, "drpc", "Base block hash", blockHash("base"), blockHashRecord),
41
42
  ]);
42
43
  export const INITIAL_EPOCH_RECIPES = frozen([0, 1, 2, 3]);
44
+ const jsonRpcCall = (network) => ({ method: "POST", path: "/ogrpc", query: { network },
45
+ body: JSON.stringify({ jsonrpc: "2.0", id: null, method: "eth_call", params: [{ to: MULTICALL3, data: GET_LAST_BLOCK_HASH }, "latest"] }) });
46
+ /// Each built-in recipe's listing reached through the gateway's passthrough (/api) instead of POST /: the same parameters
47
+ /// and projection, answered with the same signed record, so it keeps the built-in template. The gateway signs it under
48
+ /// another request hash, so it is a separate recipe the owner registers; EpochEntropy does not register it.
49
+ export const PASSTHROUGH_EPOCH_REQUESTS = frozen([
50
+ { method: "POST", path: "/info", body: '{"type":"metaAndAssetCtxs","dex":""}', projection: { symbol: "/0/universe/0/name", value: "/1/0/dayNtlVlm" } },
51
+ jsonRpcCall("ethereum"),
52
+ { method: "GET", path: "/crypto/trade/last/BTCUSD" },
53
+ { method: "GET", path: "/crypto/trade/last/ETHUSD" },
54
+ { method: "GET", path: "/feed/latest", query: { name: "ETH/USD" } },
55
+ jsonRpcCall("base"),
56
+ ]);
57
+ /// The passthrough recipe of a built-in recipe id: its canonical request is also its body.
58
+ export function passthroughEpochRecipe(builtinRecipe) {
59
+ const request = PASSTHROUGH_EPOCH_REQUESTS[builtinRecipe];
60
+ if (request === undefined)
61
+ throw new Error(`Built-in recipe ${builtinRecipe} has no passthrough form`);
62
+ const canonicalRequest = canonicalPassthroughRequest(request);
63
+ return { canonicalRequest, template: BUILTIN_EPOCH_RECIPES[builtinRecipe].template, body: canonicalRequest };
64
+ }
43
65
  const byteLength = (text) => toUtf8Bytes(text).length;
44
66
  /// Throws unless a recipe satisfies the registry's registration rules: bounded sizes and a well-formed template.
67
+ /// A beacon recipe must also be exactly what registerBeacon appends for its registration.
45
68
  export function validateEpochRecipe(recipe) {
46
69
  if (typeof recipe?.canonicalRequest !== "string" || byteLength(recipe.canonicalRequest) === 0 || byteLength(recipe.canonicalRequest) > MAX_RECIPE_REQUEST_BYTES)
47
70
  throw new Error(`A recipe canonical request must be 1 to ${MAX_RECIPE_REQUEST_BYTES} bytes`);
48
71
  if (typeof recipe.body !== "string" || byteLength(recipe.body) === 0 || byteLength(recipe.body) > MAX_RECIPE_BODY_BYTES)
49
72
  throw new Error(`A recipe body must be 1 to ${MAX_RECIPE_BODY_BYTES} bytes`);
50
73
  validateDataTemplate(recipe.template);
74
+ const { beacon } = recipe;
75
+ if (beacon === undefined)
76
+ return;
77
+ if (BigInt(beacon.verifier) === 0n || BigInt(beacon.chainHash) === 0n || beacon.genesis < 1n || beacon.period < 1n || getBytes(beacon.publicKey).length !== 128)
78
+ throw new Error("Invalid beacon registration");
79
+ const request = beaconCanonicalRequest(beacon.chainHash);
80
+ if (recipe.canonicalRequest !== request || recipe.body !== request || recipe.template !== BEACON_TEMPLATE)
81
+ throw new Error("A beacon recipe's canonical request and body name its chain hash, and its template is one round number");
51
82
  }
52
- /// The canonical request of a JSON gateway body: an object with a string operation, a parameters object and an
53
- /// optional responseProjection object. Keepers refuse a recipe whose body does not canonicalize to its request.
83
+ /// The canonical request of a recipe body. An object is a POST / gateway body with a string operation, a parameters
84
+ /// object and an optional responseProjection object; an array is a passthrough request, which is its own canonical
85
+ /// request. Keepers refuse a recipe whose body does not canonicalize to its request.
54
86
  export function canonicalRequestOfBody(body) {
55
87
  const parsed = JSON.parse(body);
88
+ if (Array.isArray(parsed)) {
89
+ parsePassthroughRequest(body);
90
+ return body;
91
+ }
56
92
  const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
57
93
  if (!isObject(parsed))
58
94
  throw new Error("A recipe body must be a JSON object");
@@ -61,15 +97,26 @@ export function canonicalRequestOfBody(body) {
61
97
  throw new Error("A recipe body needs a string operation, a parameters object and an optional responseProjection object");
62
98
  return canonicalApiRequest(request);
63
99
  }
64
- const RECIPE_VIEW = ["function getRecipe(uint8 recipe) view returns (bytes32 queryHash,string canonicalRequest,bytes template,string body)"];
100
+ const RECIPE_VIEW = ["function getRecipe(uint8 recipe) view returns (bytes32 queryHash,string canonicalRequest,bytes template,string body)",
101
+ "function beaconOf(uint8 recipe) view returns (tuple(address verifier,uint64 genesis,uint64 period,bytes32 chainHash,bytes publicKey))"];
65
102
  /// Read registered recipes from a registry. Each definition is checked against its query hash and the registration rules.
66
- export async function readEpochRecipes(runner, registry, ids) {
103
+ /// A recipe whose canonical request names drand is asked for its beacon registration, which a signed recipe lacks.
104
+ /// With a blockTag the recipes and registrations are read as of that block instead of the latest one. A registry that was rolled back to an
105
+ /// implementation without beaconOf no longer answers it, so a beacon epoch published earlier replays from a block of the implementation
106
+ /// that had it, for example the epoch's commit block; that needs an RPC that serves the state of past blocks.
107
+ export async function readEpochRecipes(runner, registry, ids, options = {}) {
67
108
  const view = new Contract(registry, RECIPE_VIEW, runner), book = {};
109
+ const overrides = options.blockTag === undefined ? {} : { blockTag: options.blockTag };
68
110
  for (const recipe of new Set(ids)) {
69
- const [queryHash, canonicalRequest, template, body] = await view.getRecipe(recipe);
111
+ const [queryHash, canonicalRequest, template, body] = await view.getRecipe(recipe, overrides);
70
112
  const entry = { canonicalRequest, template: hexlify(template), body };
71
113
  if (keccak256(toUtf8Bytes(canonicalRequest)) !== queryHash)
72
114
  throw new Error(`Recipe ${recipe} query hash mismatch`);
115
+ if (canonicalRequest.startsWith('["drand","')) {
116
+ const { verifier, genesis, period, chainHash, publicKey } = await view.beaconOf(recipe, overrides);
117
+ if (BigInt(verifier) !== 0n)
118
+ entry.beacon = { verifier, chainHash, publicKey: hexlify(publicKey), genesis, period };
119
+ }
73
120
  validateEpochRecipe(entry);
74
121
  book[recipe] = entry;
75
122
  }
@@ -131,14 +178,28 @@ export function selectEpoch(catalog, epochId, anchorHash, attempt = 0) {
131
178
  throw new Error("Invalid fallback attempt");
132
179
  const selector = keccak256(abi.encode(["bytes32", "bytes32", "uint64", "bytes32"], [id("D20_EPOCH_SELECT"), epochCatalogHash(catalog.signers, catalog.recipes), epochId, anchorHash]));
133
180
  const source = (Number(BigInt(selector) % BigInt(count)) + attempt) % count, recipe = recipes[source];
134
- const { canonicalRequest, template, body } = epochRecipe(catalog, recipe);
135
- return { source, recipe, attempt, airnode: catalog.signers[source], selector, canonicalRequest, queryHash: keccak256(toUtf8Bytes(canonicalRequest)), template, body };
181
+ const { canonicalRequest, template, body, beacon } = epochRecipe(catalog, recipe);
182
+ return { source, recipe, attempt, airnode: catalog.signers[source], selector, canonicalRequest, queryHash: keccak256(toUtf8Bytes(canonicalRequest)), template, body, ...(beacon && { beacon }) };
136
183
  }
137
184
  export function verifyEpochAttestation(selected, a, commitTimestamp) {
138
- validateApiSignatureEncoding(a.signature);
185
+ const { beacon } = selected;
186
+ if (beacon === undefined)
187
+ validateApiSignatureEncoding(a.signature);
139
188
  if (a.timestamp > commitTimestamp || commitTimestamp - a.timestamp > MAX_ATTESTATION_AGE)
140
189
  throw new Error("Invalid epoch attestation time");
141
190
  validateEpochData(selected.template, a.data);
191
+ if (beacon !== undefined) {
192
+ // A beacon round is scheduled, so its number fixes the timestamp; the verifier's check stands in for the signer's, and
193
+ // the catalog's signer must be the identity this registration derives.
194
+ const round = decodeBeaconRound(a.data);
195
+ if (a.timestamp !== beaconRoundTime(beacon, round))
196
+ throw new Error("Invalid epoch attestation time");
197
+ if (!verifyBeaconRound(beacon.publicKey, round, a.signature))
198
+ throw new Error("Invalid beacon signature");
199
+ if (selected.airnode.toLowerCase() !== beaconSlotSigner(beacon).toLowerCase())
200
+ throw new Error("Wrong epoch signer/query");
201
+ return { dataHash: keccak256(a.data), attestationHash: hashAttestation(selected.queryHash, a), signer: selected.airnode };
202
+ }
142
203
  const signer = verifyMessage(getBytes(attestationDigest(selected.queryHash, a)), a.signature);
143
204
  if (signer.toLowerCase() !== selected.airnode.toLowerCase())
144
205
  throw new Error("Wrong epoch signer/query");
package/dist/index.d.ts CHANGED
@@ -2,13 +2,15 @@ export { builtins, Operation, validateMapping, hashMapping, mapRandomness } from
2
2
  export type { MappingSpec } from "./mapping.js";
3
3
  export { deriveRequestSeed, hashPublicKey, hashProof, verifyVRFProof } from "./verification.js";
4
4
  export type { RequestContext, VRFProof, XY } from "./verification.js";
5
- export { canonicalApiRequest, attestationDigest, hashAttestation, validateApiSignatureEncoding } from "./sources.js";
6
- export type { ApiRequest, ApiAttestation } from "./sources.js";
5
+ export { canonicalApiRequest, PASSTHROUGH, canonicalPassthroughRequest, parsePassthroughRequest, passthroughUrl, attestationDigest, hashAttestation, validateApiSignatureEncoding } from "./sources.js";
6
+ export type { ApiRequest, ApiAttestation, PassthroughRequest } from "./sources.js";
7
7
  export { replayCoordinator, transcriptHash } from "./replay.js";
8
8
  export { encodeEvidencePacket, decodeEvidencePacket, EVIDENCE_PACKET_BYTES } from "./evidence.js";
9
9
  export { MAX_EPOCH_DATA_BYTES, MAX_DATA_TEMPLATE_BYTES, encodeDataTemplate, decodeDataTemplate, isValidDataTemplate, validateDataTemplate, matchesDataTemplate } from "./templates.js";
10
10
  export type { DataTemplateSegment } from "./templates.js";
11
- export { EPOCH_LENGTH, EPOCH_RECIPE_DOMAIN, BUILTIN_EPOCH_RECIPES, INITIAL_EPOCH_RECIPES, MAX_EPOCH_SOURCES, MAX_EPOCH_RECIPES, MAX_RECIPE_REQUEST_BYTES, MAX_RECIPE_BODY_BYTES, FALLBACK_DELAY_BLOCKS, validateEpochRecipe, canonicalRequestOfBody, readEpochRecipes, epochRecipe, epochCatalogHash, epochCatalogRecipes, resolveEpochCatalog, epochStart, epochForBlock, fallbackOpensAt, selectEpoch, validateEpochData, verifyEpochAttestation, epochCommitmentHash, encodeEpochEvidencePacket, decodeEpochEvidencePacket, replayEpochCommitment, epochProtocolConfigurationHash } from "./epoch.js";
11
+ export { BEACON_DST, BEACON_DOMAIN, BEACON_TEMPLATE, DRAND_EVMNET, beaconCanonicalRequest, beaconSlotSigner, beaconRoundTime, beaconRoundAt, encodeBeaconRound, decodeBeaconRound, beaconRoundMessage, verifyBeaconRound } from "./beacon.js";
12
+ export type { BeaconRegistration } from "./beacon.js";
13
+ export { EPOCH_LENGTH, EPOCH_RECIPE_DOMAIN, BUILTIN_EPOCH_RECIPES, INITIAL_EPOCH_RECIPES, PASSTHROUGH_EPOCH_REQUESTS, passthroughEpochRecipe, MAX_EPOCH_SOURCES, MAX_EPOCH_RECIPES, MAX_RECIPE_REQUEST_BYTES, MAX_RECIPE_BODY_BYTES, FALLBACK_DELAY_BLOCKS, validateEpochRecipe, canonicalRequestOfBody, readEpochRecipes, epochRecipe, epochCatalogHash, epochCatalogRecipes, resolveEpochCatalog, epochStart, epochForBlock, fallbackOpensAt, selectEpoch, validateEpochData, verifyEpochAttestation, epochCommitmentHash, encodeEpochEvidencePacket, decodeEpochEvidencePacket, replayEpochCommitment, epochProtocolConfigurationHash } from "./epoch.js";
12
14
  export type { EpochCatalog, EpochSigners, EpochRecord, EpochRecipe, EpochRecipeBook, BuiltinEpochRecipe, EpochProvider, EpochProtocolConfiguration } from "./epoch.js";
13
15
  export { quoteRequestFee, DEFAULT_FEE_BUFFER_BPS } from "./fees.js";
14
16
  export type { FeeQuote, FeeQuoteOptions, FeeQuoteProvider } from "./fees.js";
package/dist/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
  export { builtins, Operation, validateMapping, hashMapping, mapRandomness } from "./mapping.js";
2
2
  export { deriveRequestSeed, hashPublicKey, hashProof, verifyVRFProof } from "./verification.js";
3
- export { canonicalApiRequest, attestationDigest, hashAttestation, validateApiSignatureEncoding } from "./sources.js";
3
+ export { canonicalApiRequest, PASSTHROUGH, canonicalPassthroughRequest, parsePassthroughRequest, passthroughUrl, attestationDigest, hashAttestation, validateApiSignatureEncoding } from "./sources.js";
4
4
  export { replayCoordinator, transcriptHash } from "./replay.js";
5
5
  export { encodeEvidencePacket, decodeEvidencePacket, EVIDENCE_PACKET_BYTES } from "./evidence.js";
6
6
  export { MAX_EPOCH_DATA_BYTES, MAX_DATA_TEMPLATE_BYTES, encodeDataTemplate, decodeDataTemplate, isValidDataTemplate, validateDataTemplate, matchesDataTemplate } from "./templates.js";
7
- export { EPOCH_LENGTH, EPOCH_RECIPE_DOMAIN, BUILTIN_EPOCH_RECIPES, INITIAL_EPOCH_RECIPES, MAX_EPOCH_SOURCES, MAX_EPOCH_RECIPES, MAX_RECIPE_REQUEST_BYTES, MAX_RECIPE_BODY_BYTES, FALLBACK_DELAY_BLOCKS, validateEpochRecipe, canonicalRequestOfBody, readEpochRecipes, epochRecipe, epochCatalogHash, epochCatalogRecipes, resolveEpochCatalog, epochStart, epochForBlock, fallbackOpensAt, selectEpoch, validateEpochData, verifyEpochAttestation, epochCommitmentHash, encodeEpochEvidencePacket, decodeEpochEvidencePacket, replayEpochCommitment, epochProtocolConfigurationHash } from "./epoch.js";
7
+ export { BEACON_DST, BEACON_DOMAIN, BEACON_TEMPLATE, DRAND_EVMNET, beaconCanonicalRequest, beaconSlotSigner, beaconRoundTime, beaconRoundAt, encodeBeaconRound, decodeBeaconRound, beaconRoundMessage, verifyBeaconRound } from "./beacon.js";
8
+ export { EPOCH_LENGTH, EPOCH_RECIPE_DOMAIN, BUILTIN_EPOCH_RECIPES, INITIAL_EPOCH_RECIPES, PASSTHROUGH_EPOCH_REQUESTS, passthroughEpochRecipe, MAX_EPOCH_SOURCES, MAX_EPOCH_RECIPES, MAX_RECIPE_REQUEST_BYTES, MAX_RECIPE_BODY_BYTES, FALLBACK_DELAY_BLOCKS, validateEpochRecipe, canonicalRequestOfBody, readEpochRecipes, epochRecipe, epochCatalogHash, epochCatalogRecipes, resolveEpochCatalog, epochStart, epochForBlock, fallbackOpensAt, selectEpoch, validateEpochData, verifyEpochAttestation, epochCommitmentHash, encodeEpochEvidencePacket, decodeEpochEvidencePacket, replayEpochCommitment, epochProtocolConfigurationHash } from "./epoch.js";
8
9
  export { quoteRequestFee, DEFAULT_FEE_BUFFER_BPS } from "./fees.js";
package/dist/sources.d.ts CHANGED
@@ -9,6 +9,17 @@ export interface ApiAttestation {
9
9
  signature: string;
10
10
  }
11
11
  export declare function canonicalApiRequest(request: ApiRequest): string;
12
+ export interface PassthroughRequest {
13
+ method: "GET" | "POST";
14
+ path: string;
15
+ query?: Record<string, string>;
16
+ body?: string;
17
+ projection?: Record<string, string>;
18
+ }
19
+ export declare const PASSTHROUGH = "passthrough";
20
+ export declare function canonicalPassthroughRequest(request: PassthroughRequest): string;
21
+ export declare function parsePassthroughRequest(text: string): PassthroughRequest;
22
+ export declare function passthroughUrl(gateway: string, request: PassthroughRequest): string;
12
23
  export declare function attestationDigest(requestHash: string, a: ApiAttestation): string;
13
24
  export declare function hashAttestation(requestHash: string, a: ApiAttestation): string;
14
25
  export declare function validateApiSignatureEncoding(signature: string): void;
package/dist/sources.js CHANGED
@@ -15,6 +15,78 @@ export function canonicalApiRequest(request) {
15
15
  parts.push(canonical(request.responseProjection));
16
16
  return JSON.stringify(parts);
17
17
  }
18
+ export const PASSTHROUGH = "passthrough";
19
+ // Path segments, query names and projection aliases are unreserved characters, so they reach the gateway unambiguously.
20
+ const UNRESERVED = /^[A-Za-z0-9._~-]+$/;
21
+ const sortedEntries = (entries) => Object.entries(entries).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
22
+ export function canonicalPassthroughRequest(request) {
23
+ const parts = [PASSTHROUGH, request.method, request.path, sortedEntries(request.query ?? {}), request.body ?? ""];
24
+ if (request.projection !== undefined)
25
+ parts.push(sortedEntries(request.projection));
26
+ const text = JSON.stringify(parts);
27
+ parsePassthroughRequest(text);
28
+ return text;
29
+ }
30
+ function passthroughEntries(value, what) {
31
+ if (!Array.isArray(value))
32
+ throw new Error(`A passthrough ${what} is an array of [name, value] entries`);
33
+ return value.map((entry, index) => {
34
+ if (!Array.isArray(entry) || entry.length !== 2 || typeof entry[0] !== "string" || typeof entry[1] !== "string" || !UNRESERVED.test(entry[0]))
35
+ throw new Error(`Passthrough ${what} entry ${index} is not a [name, value] pair with an unreserved name`);
36
+ if (index > 0 && !(value[index - 1][0] < entry[0]))
37
+ throw new Error(`Passthrough ${what} entries must be sorted by name without repeats`);
38
+ return [entry[0], entry[1]];
39
+ });
40
+ }
41
+ /// The request a passthrough recipe's canonical request describes. Throws unless the text is exactly the compact JSON the
42
+ /// gateway hashes and names one request a keeper sends unambiguously: GET without a body or POST with a JSON body, a path
43
+ /// of unreserved segments, query names outside the gateway's own x-airnode- parameters, and projection JSON Pointers.
44
+ export function parsePassthroughRequest(text) {
45
+ const parts = JSON.parse(text);
46
+ if (!Array.isArray(parts) || (parts.length !== 5 && parts.length !== 6) || parts[0] !== PASSTHROUGH || JSON.stringify(parts) !== text)
47
+ throw new Error('A passthrough request is the compact JSON array ["passthrough", method, path, query, body(, projection)]');
48
+ const [, method, path, query, body, projection] = parts;
49
+ if (method !== "GET" && method !== "POST")
50
+ throw new Error("A passthrough method is GET or POST");
51
+ if (typeof path !== "string" || !path.startsWith("/") || path.slice(1).split("/").some(segment => !UNRESERVED.test(segment) || segment === "." || segment === ".."))
52
+ throw new Error("A passthrough path is / followed by segments of unreserved characters");
53
+ const queryEntries = passthroughEntries(query, "query");
54
+ if (queryEntries.some(([name]) => name.toLowerCase().startsWith("x-airnode-")))
55
+ throw new Error("x-airnode- query parameters belong to the gateway");
56
+ if (typeof body !== "string")
57
+ throw new Error("A passthrough body is a string");
58
+ if (method === "GET" && body !== "")
59
+ throw new Error("A passthrough GET has no body");
60
+ if (method === "POST") {
61
+ try {
62
+ JSON.parse(body);
63
+ }
64
+ catch {
65
+ throw new Error("A passthrough POST body is JSON text");
66
+ }
67
+ }
68
+ const request = { method, path };
69
+ if (queryEntries.length)
70
+ request.query = Object.fromEntries(queryEntries);
71
+ if (body !== "")
72
+ request.body = body;
73
+ if (parts.length === 6) {
74
+ const projectionEntries = passthroughEntries(projection, "projection");
75
+ if (!projectionEntries.length || projectionEntries.some(([, pointer]) => !/^\/(?:[^~]|~[01])*$/.test(pointer)))
76
+ throw new Error("A passthrough projection lists alias and JSON Pointer entries");
77
+ request.projection = Object.fromEntries(projectionEntries);
78
+ }
79
+ return request;
80
+ }
81
+ const percentEncode = (text) => Array.from(new TextEncoder().encode(text), byte => UNRESERVED.test(String.fromCharCode(byte)) ? String.fromCharCode(byte) : `%${byte.toString(16).toUpperCase().padStart(2, "0")}`).join("");
82
+ /// Where a keeper sends a passthrough request: the gateway's /api and the path, then the query entries and one
83
+ /// x-airnode-project parameter per projection entry, all in canonical order, with names and values percent-encoded except
84
+ /// unreserved characters. The gateway decodes them before hashing; projected fields come back in the order sent.
85
+ export function passthroughUrl(gateway, request) {
86
+ const params = [...sortedEntries(request.query ?? {}).map(([name, value]) => `${percentEncode(name)}=${percentEncode(value)}`),
87
+ ...sortedEntries(request.projection ?? {}).map(([alias, pointer]) => `x-airnode-project=${percentEncode(`${alias}:${pointer}`)}`)];
88
+ return `${gateway.replace(/\/+$/, "")}/api${request.path}${params.length ? `?${params.join("&")}` : ""}`;
89
+ }
18
90
  export function attestationDigest(requestHash, a) {
19
91
  return keccak256(solidityPacked(["bytes32", "uint256", "bytes"], [requestHash, a.timestamp, a.data]));
20
92
  }
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Kevin Charm
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -9,3 +9,15 @@
9
9
  - Modifications: none. `npm run vendor:check` verifies the original bytes.
10
10
 
11
11
  This is the Chainlink secp256k1/Keccak VRF construction described in that source, with its documented differences from the referenced IETF draft. It is not advertised as RFC 9381 wire-format interoperability. Reusing this file neither makes D20DAO a Chainlink service nor extends any upstream audit to our coordinator, mapping, consumer or keeper.
12
+
13
+ # Vendored BLS library on BN254
14
+
15
+ - Upstream: https://github.com/kevincharm/bls-bn254
16
+ - Commit: `9f70fb4dff2cd0921dc2144929dc0aff6f21a9b9` (release 2.0.0)
17
+ - Files, in `bls-bn254/`:
18
+ - `contracts/BLS.sol`, SHA-256 `887fd94553b9d6d0a247900bdb05523d3b9ef3b8ca049e2a0524a0b1a9d37e69`
19
+ - `contracts/ModExp.sol`, SHA-256 `fd91ae9291511668914b05fece03fb9bf6e2b57f7784d2edddc82456af12b495`
20
+ - License: MIT, preserved in `bls-bn254/LICENSE`, SHA-256 `af36460fa628a7aca8c5b1f1b6b3615376f00212284fa0fd61a013ee982a3666`.
21
+ - Modifications: none. `npm run vendor:check` verifies the original bytes, the license included.
22
+
23
+ `D20BeaconVerifier` uses this library's RFC 9380 hash-to-curve onto G1 (expand_message_xmd with keccak256 and the Shallue-van de Woestijne map) and its point checks. These are the operations of drand's `bls-bn254-unchained-on-g1` scheme. The verifier runs the pairing itself, with a fixed gas allowance, instead of the library's `verifySingle`, which forwards all remaining gas to the precompile. Reusing this file neither makes D20DAO a drand or League of Entropy service nor extends any upstream review to our registry or keeper.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@d20dao/vrf-sdk",
3
- "version": "0.4.0",
4
- "description": "D20DAO verifiable randomness: Solidity consumer helpers, contract ABIs and public proof replay",
3
+ "version": "0.5.0",
4
+ "description": "Verifiable randomness (VRF) for onchain apps: Solidity consumers, contract ABIs, USDC fee quotes and public proof replay. Live on Arc.",
5
5
  "publishConfig": {
6
6
  "access": "public",
7
7
  "registry": "https://registry.npmjs.org/"
@@ -28,6 +28,7 @@
28
28
  },
29
29
  "./abi/D20VRFCoordinator.json": "./abi/D20VRFCoordinator.json",
30
30
  "./abi/EpochEntropy.json": "./abi/EpochEntropy.json",
31
+ "./abi/D20BeaconVerifier.json": "./abi/D20BeaconVerifier.json",
31
32
  "./contracts/*": "./contracts/*",
32
33
  "./examples/*": "./examples/*",
33
34
  "./AGENTS.md": "./AGENTS.md",
@@ -47,7 +48,8 @@
47
48
  "BUILD-MANIFEST.json",
48
49
  "PROTOCOL-PROVENANCE.json",
49
50
  "abi/D20VRFCoordinator.json",
50
- "abi/EpochEntropy.json"
51
+ "abi/EpochEntropy.json",
52
+ "abi/D20BeaconVerifier.json"
51
53
  ],
52
54
  "scripts": {
53
55
  "build": "node scripts/build.mjs",
@@ -76,5 +78,29 @@
76
78
  "repository": {
77
79
  "type": "git",
78
80
  "url": "https://github.com/d20dao/d20-sdk.git"
79
- }
81
+ },
82
+ "homepage": "https://d20dao.org",
83
+ "bugs": {
84
+ "url": "https://github.com/d20dao/d20-sdk/issues"
85
+ },
86
+ "keywords": [
87
+ "vrf",
88
+ "verifiable-random-function",
89
+ "verifiable-randomness",
90
+ "randomness",
91
+ "random-number",
92
+ "rng",
93
+ "oracle",
94
+ "arc",
95
+ "arc-network",
96
+ "usdc",
97
+ "solidity",
98
+ "smart-contracts",
99
+ "evm",
100
+ "ethers",
101
+ "onchain",
102
+ "dice",
103
+ "raffle",
104
+ "d20dao"
105
+ ]
80
106
  }