@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.
- package/README.md +148 -0
- package/dist/index.d.ts +13 -13
- 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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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.
|
|
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": [
|