@verify-network/protocol
v0.2.0
Published
The Verify Network VRF protocol off-chain: ECVRF prove and verify, request ids, fulfillment typed data, router events and fees, the router watch and its request tracker, and the direct-request client.
Readme
@verify-network/protocol
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.
Install
npm install @verify-network/protocol viem effectviem (^2.56.3) and effect (^4.0.0) are peer dependencies. Node 22 or later.
Entry points
| Import | Contents |
| -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| @verify-network/protocol | Everything below, plus the direct-request client (DirectClient), the router quote (Quote), the router watch and its request tracker, and the schemas |
| @verify-network/protocol/ecvrf | ECVRF-SECP256K1-SHA256-TAI (RFC 9381): prove, verify, deriveRandomness, proofCommitment, isSentinel, isFulfillable |
| @verify-network/protocol/event | The router's RandomnessRequested and RandomnessFulfilled events and their decoders |
| @verify-network/protocol/fee | The reservation, the charge, and the settlement, as the router computes them |
| @verify-network/protocol/fulfillment | The EIP-712 typed data an enclave signs, and its digest |
| @verify-network/protocol/request | requestId, the stale delay, and the callback data limit |
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 its request tracker, and the direct-request client.
Verify a fulfillment
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.
In a browser, import the same names from /ecvrf, /event, and /fulfillment instead of the root.
import {
decodeRandomnessFulfilled,
decodeRandomnessRequested,
deriveRandomness,
fulfillmentDigest,
isFulfillable,
isSentinel,
proofCommitment,
randomnessFulfilledEvent,
randomnessRequestedEvent,
verify,
} from "@verify-network/protocol";
import {
isAddressEqual,
parseAbi,
recoverAddress,
type Address,
type Hex,
type PublicClient,
} from "viem";
const nodes = parseAbi(["function publicKeyOf(address node) view returns (uint256[2])"]);
export const checkFulfillment = async ({
publicClient,
router,
requestId,
fromBlock,
}: {
publicClient: PublicClient;
router: Address;
requestId: Hex;
fromBlock: bigint;
}) => {
const [requested] = await publicClient.getLogs({
address: router,
event: randomnessRequestedEvent,
args: { requestId },
fromBlock,
});
const [fulfilled] = await publicClient.getLogs({
address: router,
event: randomnessFulfilledEvent,
args: { requestId },
fromBlock,
});
if (requested === undefined || fulfilled === undefined) throw new Error("not fulfilled yet");
const { request } = decodeRandomnessRequested(requested);
const { proof, randomness, enclaveSignature } = decodeRandomnessFulfilled(fulfilled);
const chainId = await publicClient.getChainId();
const publicKey = await publicClient.readContract({
address: router,
abi: nodes,
functionName: "publicKeyOf",
args: [request.fulfiller],
});
const signer = await recoverAddress({
hash: fulfillmentDigest({
chainId,
router,
requestId,
proofCommitment: proofCommitment(proof),
}),
signature: enclaveSignature,
});
const proven = isSentinel(proof)
? !isFulfillable(publicKey, requestId)
: verify({ publicKey, proof, alpha: requestId });
return (
isAddressEqual(signer, request.fulfiller) && proven && deriveRandomness(proof) === randomness
);
};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.
Send a direct request
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.
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.
import { DirectClient, run } from "@verify-network/protocol";
import { createPublicClient, http, toHex, type Chain } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { base } from "viem/chains";
// Widened to `Chain`: `DirectClient` takes a `PublicClient` of any chain.
const publicClient = createPublicClient({ chain: base as Chain, transport: http() });
const client = await run(
DirectClient.connect({
publicClient,
chainId: base.id,
router: "0x…",
account: privateKeyToAccount("0x…"), // a session key or a backend key, funded on the router
}),
);
const result = await run(client.request({ consumer: "0x…", callbackData: toHex("round-1") }));
if (result._tag === "Landed") {
console.log(result.requestId, result.randomness, result.hash);
} else {
// NotLanded: the deadline passed on the chain's clock and nothing landed. Fall back to an on-chain request.
}The consumer contract implements IDirectVrfReceiver from @verify-network/contracts. The signer is passed to its callback, so the consumer decides which signers it accepts.
To sign an authorization yourself, use signDirectRequest and directRequestId. validateEndpoint checks an endpoint read from the chain before you connect to it.
License
MIT
