@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/AGENTS.md +11 -11
- package/API.md +411 -193
- package/BUILD-MANIFEST.json +36 -24
- package/CHANGELOG.md +53 -0
- package/PROTOCOL-PROVENANCE.json +15 -9
- package/README.md +66 -26
- package/THIRD_PARTY_NOTICES.md +3 -1
- package/abi/D20BeaconVerifier.json +165 -0
- package/abi/EpochEntropy.json +233 -0
- package/dist/abi.d.ts +303 -0
- package/dist/abi.js +2 -1
- package/dist/beacon.d.ts +28 -0
- package/dist/beacon.js +133 -0
- package/dist/epoch.d.ts +11 -3
- package/dist/epoch.js +70 -9
- package/dist/index.d.ts +5 -3
- package/dist/index.js +3 -2
- package/dist/sources.d.ts +11 -0
- package/dist/sources.js +72 -0
- package/notices/BLS-BN254-LICENSE +21 -0
- package/notices/PROVENANCE.md +12 -0
- package/package.json +30 -4
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
|
|
53
|
-
/// optional responseProjection object
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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.
|
package/notices/PROVENANCE.md
CHANGED
|
@@ -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
|
-
"description": "
|
|
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
|
}
|