@verify-network/protocol 0.1.0 → 0.1.1

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.
Files changed (3) hide show
  1. package/README.md +148 -0
  2. package/dist/index.d.ts +13 -13
  3. package/package.json +1 -1
package/README.md ADDED
@@ -0,0 +1,148 @@
1
+ # @verify-network/protocol
2
+
3
+ The off-chain half of the Verify Network VRF protocol. Use it to verify a fulfillment the router delivered, to sign and send a direct request to a node, and to compute request ids, fees, and the typed data the router checks. Every function mirrors the router byte for byte.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ npm install @verify-network/protocol viem effect
9
+ ```
10
+
11
+ `viem` (`^2.56.3`) and `effect` (`^4.0.0`) are peer dependencies. Node 22 or later.
12
+
13
+ ## Entry points
14
+
15
+ | Import | Contents |
16
+ | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
17
+ | `@verify-network/protocol` | Everything below, plus the direct-request client (`DirectClient`), the router quote (`Quote`), the router watch, and the schemas |
18
+ | `@verify-network/protocol/ecvrf` | ECVRF-SECP256K1-SHA256-TAI (RFC 9381): `prove`, `verify`, `deriveRandomness`, `proofCommitment`, `isSentinel`, `isFulfillable` |
19
+ | `@verify-network/protocol/event` | The router's `RandomnessRequested` and `RandomnessFulfilled` events and their decoders |
20
+ | `@verify-network/protocol/fee` | The reservation, the charge, and the settlement, as the router computes them |
21
+ | `@verify-network/protocol/fulfillment` | The EIP-712 typed data an enclave signs, and its digest |
22
+ | `@verify-network/protocol/request` | `requestId`, the stale delay, and the callback data limit |
23
+
24
+ The subpath entries do not load Effect. Import from them in a browser bundle. The root entry loads Effect for its schemas, the quote, the router watch, and the direct-request client.
25
+
26
+ ## Verify a fulfillment
27
+
28
+ A fulfillment is valid when three things hold. The enclave signature recovers to the request's fulfiller. The proof verifies under that node's key for the request id. The randomness is the one the proof derives.
29
+
30
+ In a browser, import the same names from `/ecvrf`, `/event`, and `/fulfillment` instead of the root.
31
+
32
+ ```ts
33
+ import {
34
+ decodeRandomnessFulfilled,
35
+ decodeRandomnessRequested,
36
+ deriveRandomness,
37
+ fulfillmentDigest,
38
+ isFulfillable,
39
+ isSentinel,
40
+ proofCommitment,
41
+ randomnessFulfilledEvent,
42
+ randomnessRequestedEvent,
43
+ verify,
44
+ } from "@verify-network/protocol";
45
+ import {
46
+ isAddressEqual,
47
+ parseAbi,
48
+ recoverAddress,
49
+ type Address,
50
+ type Hex,
51
+ type PublicClient,
52
+ } from "viem";
53
+
54
+ const nodes = parseAbi(["function publicKeyOf(address node) view returns (uint256[2])"]);
55
+
56
+ export const checkFulfillment = async ({
57
+ publicClient,
58
+ router,
59
+ requestId,
60
+ fromBlock,
61
+ }: {
62
+ publicClient: PublicClient;
63
+ router: Address;
64
+ requestId: Hex;
65
+ fromBlock: bigint;
66
+ }) => {
67
+ const [requested] = await publicClient.getLogs({
68
+ address: router,
69
+ event: randomnessRequestedEvent,
70
+ args: { requestId },
71
+ fromBlock,
72
+ });
73
+ const [fulfilled] = await publicClient.getLogs({
74
+ address: router,
75
+ event: randomnessFulfilledEvent,
76
+ args: { requestId },
77
+ fromBlock,
78
+ });
79
+ if (requested === undefined || fulfilled === undefined) throw new Error("not fulfilled yet");
80
+
81
+ const { request } = decodeRandomnessRequested(requested);
82
+ const { proof, randomness, enclaveSignature } = decodeRandomnessFulfilled(fulfilled);
83
+ const chainId = await publicClient.getChainId();
84
+ const publicKey = await publicClient.readContract({
85
+ address: router,
86
+ abi: nodes,
87
+ functionName: "publicKeyOf",
88
+ args: [request.fulfiller],
89
+ });
90
+
91
+ const signer = await recoverAddress({
92
+ hash: fulfillmentDigest({
93
+ chainId,
94
+ router,
95
+ requestId,
96
+ proofCommitment: proofCommitment(proof),
97
+ }),
98
+ signature: enclaveSignature,
99
+ });
100
+ const proven = isSentinel(proof)
101
+ ? !isFulfillable(publicKey, requestId)
102
+ : verify({ publicKey, proof, alpha: requestId });
103
+ return (
104
+ isAddressEqual(signer, request.fulfiller) && proven && deriveRandomness(proof) === randomness
105
+ );
106
+ };
107
+ ```
108
+
109
+ A fulfillment that fails this check is evidence for `challengeFulfillment(request, proof, enclaveSignature)` on the router. Anyone may send it while the node still holds stake.
110
+
111
+ ## Send a direct request
112
+
113
+ A direct request skips the consumer's own transaction. A signer signs an authorization, posts it to one node, and the node lands the request and its fulfillment in one transaction. The signer's balance on the router pays.
114
+
115
+ `DirectClient` finds the fastest serving node on the router, signs, posts, and waits until the fulfillment lands. Its methods return Effects. `run` turns one into a promise, so a caller needs no Effect code.
116
+
117
+ ```ts
118
+ import { DirectClient, run } from "@verify-network/protocol";
119
+ import { createPublicClient, http, toHex, type Chain } from "viem";
120
+ import { privateKeyToAccount } from "viem/accounts";
121
+ import { base } from "viem/chains";
122
+
123
+ // Widened to `Chain`: `DirectClient` takes a `PublicClient` of any chain.
124
+ const publicClient = createPublicClient({ chain: base as Chain, transport: http() });
125
+ const client = await run(
126
+ DirectClient.connect({
127
+ publicClient,
128
+ chainId: base.id,
129
+ router: "0x…",
130
+ account: privateKeyToAccount("0x…"), // a session key or a backend key, funded on the router
131
+ }),
132
+ );
133
+
134
+ const result = await run(client.request({ consumer: "0x…", callbackData: toHex("round-1") }));
135
+ if (result._tag === "Landed") {
136
+ console.log(result.requestId, result.randomness, result.hash);
137
+ } else {
138
+ // NotLanded: the deadline passed on the chain's clock and nothing landed. Fall back to an on-chain request.
139
+ }
140
+ ```
141
+
142
+ The consumer contract implements `IDirectVrfReceiver` from `@verify-network/contracts`. The signer is passed to its callback, so the consumer decides which signers it accepts.
143
+
144
+ To sign an authorization yourself, use `signDirectRequest` and `directRequestId`. `validateEndpoint` checks an endpoint read from the chain before you connect to it.
145
+
146
+ ## License
147
+
148
+ MIT
package/dist/index.d.ts CHANGED
@@ -76,7 +76,7 @@ export declare const NodeIdentity: Schema.Struct<{
76
76
  export type NodeIdentity = typeof NodeIdentity.Type;
77
77
  declare const DirectRequestRefused_base: Schema.Class<DirectRequestRefused, Schema.TaggedStruct<"DirectRequestRefused", {
78
78
  readonly reason: Schema.Literals<readonly ["too-long", "wrong-node", "deadline", "max-gas-price", "callback-gas-limit", "bad-signature", "unfulfillable", "insufficient-balance"]>;
79
- }>, import("effect/unstable/Cause").YieldableError>;
79
+ }>, import("effect/Cause").YieldableError>;
80
80
  /**
81
81
  * A 400: why `POST /direct` refused an authorization, cheapest check first (ADR 0026, 0028):
82
82
  * the callback data is over the router's cap; the named node is another; the deadline has
@@ -89,12 +89,12 @@ export declare class DirectRequestRefused extends DirectRequestRefused_base {}
89
89
  declare const NonceTaken_base: Schema.Class<NonceTaken, Schema.TaggedStruct<"NonceTaken", {
90
90
  readonly signer: Schema.String;
91
91
  readonly nonce: Schema.BigInt;
92
- }>, import("effect/unstable/Cause").YieldableError>;
92
+ }>, import("effect/Cause").YieldableError>;
93
93
  /** A 409: this node already took up, or saw land, an authorization under `signer` and `nonce`. */
94
94
  export declare class NonceTaken extends NonceTaken_base {}
95
95
  declare const TooManyRequests_base: Schema.Class<TooManyRequests, Schema.TaggedStruct<"TooManyRequests", {
96
96
  readonly reason: Schema.Literals<readonly ["in-flight", "signer", "ip", "edge"]>;
97
- }>, import("effect/unstable/Cause").YieldableError>;
97
+ }>, import("effect/Cause").YieldableError>;
98
98
  /**
99
99
  * A 429: the node is past a bound (as many direct requests in flight as it takes at once, as
100
100
  * many under this signer, or a block on the caller's address for the incorrect authorizations it
@@ -104,7 +104,7 @@ declare const TooManyRequests_base: Schema.Class<TooManyRequests, Schema.TaggedS
104
104
  export declare class TooManyRequests extends TooManyRequests_base {}
105
105
  declare const NotServing_base: Schema.Class<NotServing, Schema.TaggedStruct<"NotServing", {
106
106
  readonly reason: Schema.Literals<readonly ["not-set-up", "not-active", "disconnected", "chain-unreachable"]>;
107
- }>, import("effect/unstable/Cause").YieldableError>;
107
+ }>, import("effect/Cause").YieldableError>;
108
108
  /**
109
109
  * A 503: the node serves no direct request right now: it is not set up; it is not in the active
110
110
  * set (exiting, ejected, forgotten, or stopped); its request source reports no connection, so it
@@ -113,7 +113,7 @@ declare const NotServing_base: Schema.Class<NotServing, Schema.TaggedStruct<"Not
113
113
  export declare class NotServing extends NotServing_base {}
114
114
  declare const DeadlinePassed_base: Schema.Class<DeadlinePassed, Schema.TaggedStruct<"DeadlinePassed", {
115
115
  readonly deadline: Schema.BigInt;
116
- }>, import("effect/unstable/Cause").YieldableError>;
116
+ }>, import("effect/Cause").YieldableError>;
117
117
  /**
118
118
  * A 504: the authorization's deadline passed before the source saw the fulfillment land: it can
119
119
  * no longer land, so the signer may make an on-chain request instead (ADR 0026).
@@ -286,7 +286,7 @@ type Pricing = {
286
286
  declare const RouterUnreadable_base: Schema.Class<RouterUnreadable, Schema.TaggedStruct<"RouterUnreadable", {
287
287
  readonly operation: Schema.String;
288
288
  readonly cause: Schema.Defect;
289
- }>, import("effect/unstable/Cause").YieldableError>;
289
+ }>, import("effect/Cause").YieldableError>;
290
290
  /** The router could not be read; `operation` names the viem call. */
291
291
  export declare class RouterUnreadable extends RouterUnreadable_base {}
292
292
  /** The request being priced: its callback data's length, and its callback gas limit when not the router's default. */
@@ -370,7 +370,7 @@ type Fetch = (input: string, init?: RequestInit) => Promise<Response>;
370
370
  declare const NoNodeServing_base: Schema.Class<NoNodeServing, Schema.TaggedStruct<"NoNodeServing", {
371
371
  readonly active: Schema.Int;
372
372
  readonly published: Schema.Int;
373
- }>, import("effect/unstable/Cause").YieldableError>;
373
+ }>, import("effect/Cause").YieldableError>;
374
374
  /**
375
375
  * No node to post to: of `active` nodes, `published` had an endpoint the client trusts the shape
376
376
  * of, and none of those answered `GET /` as itself in time.
@@ -378,13 +378,13 @@ declare const NoNodeServing_base: Schema.Class<NoNodeServing, Schema.TaggedStruc
378
378
  export declare class NoNodeServing extends NoNodeServing_base {}
379
379
  declare const SigningFailed_base: Schema.Class<SigningFailed, Schema.TaggedStruct<"SigningFailed", {
380
380
  readonly cause: Schema.Defect;
381
- }>, import("effect/unstable/Cause").YieldableError>;
381
+ }>, import("effect/Cause").YieldableError>;
382
382
  /** The account did not sign the authorization: a wallet refused, or the key could not. */
383
383
  export declare class SigningFailed extends SigningFailed_base {}
384
384
  declare const UnexpectedResponse_base: Schema.Class<UnexpectedResponse, Schema.TaggedStruct<"UnexpectedResponse", {
385
385
  readonly status: Schema.Int;
386
386
  readonly body: Schema.String;
387
- }>, import("effect/unstable/Cause").YieldableError>;
387
+ }>, import("effect/Cause").YieldableError>;
388
388
  /**
389
389
  * The node answered something `POST /direct` does not: neither a landing nor a refusal it
390
390
  * spells, under a status that says the authorization was not taken up (a 4xx of the edge's, a
@@ -541,7 +541,7 @@ export declare const SubscriptionFrame: Schema.Union<readonly [Schema.Struct<{
541
541
  }>;
542
542
  }>]>;
543
543
  export type SubscriptionFrame = typeof SubscriptionFrame.Type;
544
- export declare const decodeSubscriptionFrame: (input: unknown, options?: import("effect/unstable/SchemaAST").ParseOptions) => Result.Result<{
544
+ export declare const decodeSubscriptionFrame: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => Result.Result<{
545
545
  readonly id: number;
546
546
  readonly result?: string;
547
547
  readonly error?: {
@@ -566,7 +566,7 @@ export declare const PendingLog: Schema.Struct<{
566
566
  readonly removed: Schema.optionalKey<Schema.Boolean>;
567
567
  }>;
568
568
  export type PendingLog = typeof PendingLog.Type;
569
- export declare const decodePendingLog: (input: unknown, options?: import("effect/unstable/SchemaAST").ParseOptions) => Result.Result<{
569
+ export declare const decodePendingLog: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => Result.Result<{
570
570
  readonly address: `0x${string}`;
571
571
  readonly topics: readonly `0x${string}`[];
572
572
  readonly data: `0x${string}`;
@@ -608,14 +608,14 @@ export declare const seenIn: (router: Address$1, result: unknown, at: number) =>
608
608
  declare const SubscriptionRefused_base: Schema.Class<SubscriptionRefused, Schema.TaggedStruct<"SubscriptionRefused", {
609
609
  readonly url: Schema.String;
610
610
  readonly reason: Schema.String;
611
- }>, import("effect/unstable/Cause").YieldableError>;
611
+ }>, import("effect/Cause").YieldableError>;
612
612
  /** The endpoint refused the router's log subscription, or never confirmed it. */
613
613
  export declare class SubscriptionRefused extends SubscriptionRefused_base {
614
614
  get message(): string;
615
615
  }
616
616
  declare const WatchEnded_base: Schema.Class<WatchEnded, Schema.TaggedStruct<"WatchEnded", {
617
617
  readonly url: Schema.String;
618
- }>, import("effect/unstable/Cause").YieldableError>;
618
+ }>, import("effect/Cause").YieldableError>;
619
619
  /** The socket carrying the router's logs ended while something was still waiting on it. */
620
620
  export declare class WatchEnded extends WatchEnded_base {
621
621
  get message(): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@verify-network/protocol",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "The Verify Network VRF protocol off-chain: ECVRF prove and verify, request ids, fulfillment typed data, router events and fees, and the router watch and direct-request client.",
5
5
  "license": "MIT",
6
6
  "files": [