@collectorcrypt/vrf-client 0.1.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/addresses.d.ts +34 -0
- package/dist/addresses.d.ts.map +1 -0
- package/dist/addresses.js +87 -0
- package/dist/addresses.js.map +1 -0
- package/dist/constants.d.ts +12 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +15 -0
- package/dist/constants.js.map +1 -0
- package/dist/idl/cc_vrf.json +1024 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +58 -0
- package/dist/index.js.map +1 -0
- package/dist/light.d.ts +98 -0
- package/dist/light.d.ts.map +1 -0
- package/dist/light.js +182 -0
- package/dist/light.js.map +1 -0
- package/dist/operations.d.ts +172 -0
- package/dist/operations.d.ts.map +1 -0
- package/dist/operations.js +417 -0
- package/dist/operations.js.map +1 -0
- package/dist/program.d.ts +9 -0
- package/dist/program.d.ts.map +1 -0
- package/dist/program.js +21 -0
- package/dist/program.js.map +1 -0
- package/dist/verifyEndToEnd.d.ts +124 -0
- package/dist/verifyEndToEnd.d.ts.map +1 -0
- package/dist/verifyEndToEnd.js +150 -0
- package/dist/verifyEndToEnd.js.map +1 -0
- package/package.json +49 -0
- package/src/addresses.ts +105 -0
- package/src/constants.ts +16 -0
- package/src/idl/cc_vrf.json +1024 -0
- package/src/index.ts +80 -0
- package/src/light.ts +259 -0
- package/src/operations.ts +655 -0
- package/src/program.ts +17 -0
- package/src/verifyEndToEnd.ts +305 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { verifyVRF, vrfProofToHash, bytesToHex } from "@collectorcrypt/ecvrf";
|
|
2
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
3
|
+
import { PublicKey } from "@solana/web3.js";
|
|
4
|
+
import { deriveAuthorityAddress } from "./addresses";
|
|
5
|
+
import { CC_VRF_PROGRAM_ID, SUITE_EDWARDS25519_SHA512_TAI } from "./constants";
|
|
6
|
+
|
|
7
|
+
export interface OnChainAuthority {
|
|
8
|
+
authorityAddress?: PublicKey;
|
|
9
|
+
owner: PublicKey;
|
|
10
|
+
pk: Uint8Array;
|
|
11
|
+
suite: number;
|
|
12
|
+
frozen: boolean;
|
|
13
|
+
revoked: boolean;
|
|
14
|
+
label: Uint8Array;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* On-chain commitment record fetched from a VrfProofCommit compressed PDA.
|
|
19
|
+
* Either the consumer fetches this themselves via fetchProofCommit() and
|
|
20
|
+
* passes it in, or they construct it from raw fields if validating offline.
|
|
21
|
+
*/
|
|
22
|
+
export interface OnChainCommit {
|
|
23
|
+
authority?: PublicKey;
|
|
24
|
+
memoHash: Uint8Array;
|
|
25
|
+
proofHash: Uint8Array;
|
|
26
|
+
alphaHash: Uint8Array;
|
|
27
|
+
committedSlot: bigint | number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface VerifyEndToEndInput {
|
|
31
|
+
/** The operator's published VRF public key (32 bytes Ed25519). */
|
|
32
|
+
pk: Uint8Array;
|
|
33
|
+
/** The exact alpha bytes the operator hashed and signed over. */
|
|
34
|
+
alpha: Uint8Array;
|
|
35
|
+
/** The 80-byte VRF proof the operator produced. */
|
|
36
|
+
proof: Uint8Array;
|
|
37
|
+
/** The on-chain commitment row, fetched from the program. */
|
|
38
|
+
onChainCommit: OnChainCommit;
|
|
39
|
+
/** Original memo string/bytes, used to verify the memo hash. */
|
|
40
|
+
memo: string | Uint8Array;
|
|
41
|
+
/** VRF suite. Omitted means RFC 9381 Ed25519-SHA512-TAI. */
|
|
42
|
+
suite?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface VerifyEndToEndResult {
|
|
46
|
+
/**
|
|
47
|
+
* True iff every check passed: ECVRF math, on-chain proof hash, on-chain
|
|
48
|
+
* alpha hash, on-chain memo hash. Any failure flips this false.
|
|
49
|
+
*/
|
|
50
|
+
valid: boolean;
|
|
51
|
+
ecvrfValid: boolean;
|
|
52
|
+
suiteSupported: boolean;
|
|
53
|
+
proofHashMatches: boolean;
|
|
54
|
+
alphaHashMatches: boolean;
|
|
55
|
+
memoHashMatches: boolean;
|
|
56
|
+
/** SHA-512 (64-byte) beta derived from the proof, present iff `ecvrfValid`. */
|
|
57
|
+
beta: Uint8Array | null;
|
|
58
|
+
reasons: string[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
|
|
62
|
+
if (a.length !== b.length) return false;
|
|
63
|
+
let diff = 0;
|
|
64
|
+
for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
|
|
65
|
+
return diff === 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Single-call full verification for a VRF outcome that's been committed
|
|
70
|
+
* on-chain. Validates:
|
|
71
|
+
*
|
|
72
|
+
* 1. ECVRF math: `verifyVRF(pk, alpha, proof)` → true
|
|
73
|
+
* 2. sha256(proof) == on-chain commit.proof_hash
|
|
74
|
+
* 3. sha256(alpha) == on-chain commit.alpha_hash
|
|
75
|
+
* 4. sha256(memo) == on-chain commit.memo_hash
|
|
76
|
+
*
|
|
77
|
+
* If all four hold, the operator cannot have substituted the proof, alpha,
|
|
78
|
+
* or memo after the fact — the result is provably tied to the on-chain
|
|
79
|
+
* commitment.
|
|
80
|
+
*/
|
|
81
|
+
/**
|
|
82
|
+
* Result of picking the canonical commit from a list of events for the same
|
|
83
|
+
* `(authority, memo)`. The "canonical" commit is the unique event whose
|
|
84
|
+
* `proof_hash` matches the SHA-256 of a proof that verifies under ECVRF.
|
|
85
|
+
*
|
|
86
|
+
* Because ECVRF proofs are deterministic for a given (pk, alpha), at most one
|
|
87
|
+
* candidate can have a valid `proof_hash`. Extra events (which the chain
|
|
88
|
+
* allows in event-mode) are simply garbage payloads — detectable, not a
|
|
89
|
+
* successful forgery.
|
|
90
|
+
*/
|
|
91
|
+
export interface PickCanonicalResult {
|
|
92
|
+
/** The single event whose committed hash matches the verifying proof, or null if none verify. */
|
|
93
|
+
canonical: OnChainCommit | null;
|
|
94
|
+
/** All candidates inspected, in the order supplied. */
|
|
95
|
+
candidates: OnChainCommit[];
|
|
96
|
+
/** Whether more than one event was found for the same memo. Informational. */
|
|
97
|
+
duplicateMemoEvents: boolean;
|
|
98
|
+
/** Whether more than one candidate's `proof_hash` matched a verifying proof (should be impossible if ECVRF is sound). */
|
|
99
|
+
multipleVerifying: boolean;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface VerifyAuthorityCommitEndToEndInput extends Omit<
|
|
103
|
+
VerifyEndToEndInput,
|
|
104
|
+
"pk" | "suite"
|
|
105
|
+
> {
|
|
106
|
+
/** Authority state fetched from the VrfAuthority compressed PDA. */
|
|
107
|
+
authority: OnChainAuthority;
|
|
108
|
+
/** Expected owner for the authority, usually the operator wallet. */
|
|
109
|
+
expectedOwner?: PublicKey;
|
|
110
|
+
/** Expected 32-byte authority label. */
|
|
111
|
+
expectedLabel?: Uint8Array;
|
|
112
|
+
/**
|
|
113
|
+
* Optional sanity check: redundant against the address rederived from
|
|
114
|
+
* `(owner, label)` inside the verifier. If provided, it must match the
|
|
115
|
+
* derived address or `valid` flips false.
|
|
116
|
+
*/
|
|
117
|
+
expectedAuthorityAddress?: PublicKey;
|
|
118
|
+
/**
|
|
119
|
+
* Optional program ID override. Defaults to the canonical cc-vrf program
|
|
120
|
+
* ID and only needs to be set for forked deployments.
|
|
121
|
+
*/
|
|
122
|
+
programId?: PublicKey;
|
|
123
|
+
/** 64-byte beta fetched from a VrfProofCommitWithBeta account. */
|
|
124
|
+
onChainBeta?: Uint8Array;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface VerifyAuthorityCommitEndToEndResult extends VerifyEndToEndResult {
|
|
128
|
+
authorityFrozen: boolean;
|
|
129
|
+
authorityNotRevoked: boolean;
|
|
130
|
+
authorityOwnerMatches: boolean;
|
|
131
|
+
authorityLabelMatches: boolean;
|
|
132
|
+
commitAuthorityMatches: boolean;
|
|
133
|
+
betaMatches: boolean | null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Resolve which committed event corresponds to a known-valid proof. Use this
|
|
138
|
+
* when fetching event-mode commits where the chain doesn't enforce
|
|
139
|
+
* one-commit-per-memo. Pass in all candidates from `fetchProofCommitEvents`
|
|
140
|
+
* plus the proof bytes the operator gave you; you get back the unique
|
|
141
|
+
* canonical row (or null if none match).
|
|
142
|
+
*/
|
|
143
|
+
export function pickCanonicalCommit(
|
|
144
|
+
candidates: OnChainCommit[],
|
|
145
|
+
proof: Uint8Array,
|
|
146
|
+
): PickCanonicalResult {
|
|
147
|
+
const proofHash = sha256(proof);
|
|
148
|
+
const matching = candidates.filter((c) => bytesEqual(proofHash, c.proofHash));
|
|
149
|
+
return {
|
|
150
|
+
canonical: matching[0] ?? null,
|
|
151
|
+
candidates,
|
|
152
|
+
duplicateMemoEvents: candidates.length > 1,
|
|
153
|
+
multipleVerifying: matching.length > 1,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function verifyEndToEnd(
|
|
158
|
+
input: VerifyEndToEndInput,
|
|
159
|
+
): VerifyEndToEndResult {
|
|
160
|
+
const reasons: string[] = [];
|
|
161
|
+
|
|
162
|
+
const suite = input.suite ?? SUITE_EDWARDS25519_SHA512_TAI;
|
|
163
|
+
const suiteSupported = suite === SUITE_EDWARDS25519_SHA512_TAI;
|
|
164
|
+
if (!suiteSupported) reasons.push(`unsupported-suite-${suite}`);
|
|
165
|
+
|
|
166
|
+
const ecvrfValid =
|
|
167
|
+
suiteSupported && verifyVRF(input.pk, input.alpha, input.proof);
|
|
168
|
+
if (!ecvrfValid) reasons.push("ecvrf-verify-failed");
|
|
169
|
+
|
|
170
|
+
const computedProofHash = sha256(input.proof);
|
|
171
|
+
const proofHashMatches = bytesEqual(
|
|
172
|
+
computedProofHash,
|
|
173
|
+
input.onChainCommit.proofHash,
|
|
174
|
+
);
|
|
175
|
+
if (!proofHashMatches) {
|
|
176
|
+
reasons.push(
|
|
177
|
+
`proof-hash-mismatch (computed=${bytesToHex(computedProofHash)} onchain=${bytesToHex(input.onChainCommit.proofHash)})`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const computedAlphaHash = sha256(input.alpha);
|
|
182
|
+
const alphaHashMatches = bytesEqual(
|
|
183
|
+
computedAlphaHash,
|
|
184
|
+
input.onChainCommit.alphaHash,
|
|
185
|
+
);
|
|
186
|
+
if (!alphaHashMatches) reasons.push("alpha-hash-mismatch");
|
|
187
|
+
|
|
188
|
+
const memoBytes =
|
|
189
|
+
typeof input.memo === "string"
|
|
190
|
+
? new TextEncoder().encode(input.memo)
|
|
191
|
+
: input.memo;
|
|
192
|
+
const computedMemoHash = sha256(memoBytes);
|
|
193
|
+
const memoHashMatches = bytesEqual(
|
|
194
|
+
computedMemoHash,
|
|
195
|
+
input.onChainCommit.memoHash,
|
|
196
|
+
);
|
|
197
|
+
if (!memoHashMatches) reasons.push("memo-hash-mismatch");
|
|
198
|
+
|
|
199
|
+
const valid =
|
|
200
|
+
suiteSupported &&
|
|
201
|
+
ecvrfValid &&
|
|
202
|
+
proofHashMatches &&
|
|
203
|
+
alphaHashMatches &&
|
|
204
|
+
memoHashMatches;
|
|
205
|
+
const beta = ecvrfValid ? vrfProofToHash(input.proof) : null;
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
valid,
|
|
209
|
+
ecvrfValid,
|
|
210
|
+
suiteSupported,
|
|
211
|
+
proofHashMatches,
|
|
212
|
+
alphaHashMatches,
|
|
213
|
+
memoHashMatches,
|
|
214
|
+
beta,
|
|
215
|
+
reasons,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function verifyAuthorityCommitEndToEnd(
|
|
220
|
+
input: VerifyAuthorityCommitEndToEndInput,
|
|
221
|
+
): VerifyAuthorityCommitEndToEndResult {
|
|
222
|
+
const base = verifyEndToEnd({
|
|
223
|
+
pk: input.authority.pk,
|
|
224
|
+
suite: input.authority.suite,
|
|
225
|
+
alpha: input.alpha,
|
|
226
|
+
proof: input.proof,
|
|
227
|
+
onChainCommit: input.onChainCommit,
|
|
228
|
+
memo: input.memo,
|
|
229
|
+
});
|
|
230
|
+
const reasons = [...base.reasons];
|
|
231
|
+
|
|
232
|
+
const authorityFrozen = input.authority.frozen;
|
|
233
|
+
if (!authorityFrozen) reasons.push("authority-not-frozen");
|
|
234
|
+
|
|
235
|
+
const authorityNotRevoked = !input.authority.revoked;
|
|
236
|
+
if (!authorityNotRevoked) reasons.push("authority-revoked");
|
|
237
|
+
|
|
238
|
+
const authorityOwnerMatches = input.expectedOwner
|
|
239
|
+
? input.authority.owner.equals(input.expectedOwner)
|
|
240
|
+
: true;
|
|
241
|
+
if (!authorityOwnerMatches) reasons.push("authority-owner-mismatch");
|
|
242
|
+
|
|
243
|
+
const authorityLabelMatches = input.expectedLabel
|
|
244
|
+
? bytesEqual(input.authority.label, input.expectedLabel)
|
|
245
|
+
: true;
|
|
246
|
+
if (!authorityLabelMatches) reasons.push("authority-label-mismatch");
|
|
247
|
+
|
|
248
|
+
// Always rederive the canonical authority address from (owner, label) and
|
|
249
|
+
// require the commit to point at it. This closes the prior footgun where a
|
|
250
|
+
// hand-built OnChainAuthority + missing expectedAuthorityAddress silently
|
|
251
|
+
// skipped the commit-to-authority binding.
|
|
252
|
+
const programId = input.programId ?? CC_VRF_PROGRAM_ID;
|
|
253
|
+
const derivedAuthorityAddress = deriveAuthorityAddress(
|
|
254
|
+
input.authority.owner,
|
|
255
|
+
input.authority.label,
|
|
256
|
+
programId,
|
|
257
|
+
);
|
|
258
|
+
const commitAuthority = input.onChainCommit.authority;
|
|
259
|
+
const commitAuthorityMatches =
|
|
260
|
+
commitAuthority?.equals(derivedAuthorityAddress) === true;
|
|
261
|
+
if (!commitAuthorityMatches) {
|
|
262
|
+
reasons.push(
|
|
263
|
+
commitAuthority
|
|
264
|
+
? "commit-authority-mismatch"
|
|
265
|
+
: "commit-authority-missing",
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// If the caller passed their own expectedAuthorityAddress, it must also
|
|
270
|
+
// match the derived one — otherwise the caller's expectation contradicts
|
|
271
|
+
// the (owner, label) they supplied.
|
|
272
|
+
const expectedAuthorityMatchesDerived = input.expectedAuthorityAddress
|
|
273
|
+
? input.expectedAuthorityAddress.equals(derivedAuthorityAddress)
|
|
274
|
+
: true;
|
|
275
|
+
if (!expectedAuthorityMatchesDerived) {
|
|
276
|
+
reasons.push("expected-authority-address-mismatch");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const betaMatches = input.onChainBeta
|
|
280
|
+
? base.beta !== null && bytesEqual(input.onChainBeta, base.beta)
|
|
281
|
+
: null;
|
|
282
|
+
if (betaMatches === false) reasons.push("beta-mismatch");
|
|
283
|
+
|
|
284
|
+
const valid =
|
|
285
|
+
base.valid &&
|
|
286
|
+
authorityFrozen &&
|
|
287
|
+
authorityNotRevoked &&
|
|
288
|
+
authorityOwnerMatches &&
|
|
289
|
+
authorityLabelMatches &&
|
|
290
|
+
commitAuthorityMatches &&
|
|
291
|
+
expectedAuthorityMatchesDerived &&
|
|
292
|
+
betaMatches !== false;
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
...base,
|
|
296
|
+
valid,
|
|
297
|
+
reasons,
|
|
298
|
+
authorityFrozen,
|
|
299
|
+
authorityNotRevoked,
|
|
300
|
+
authorityOwnerMatches,
|
|
301
|
+
authorityLabelMatches,
|
|
302
|
+
commitAuthorityMatches,
|
|
303
|
+
betaMatches,
|
|
304
|
+
};
|
|
305
|
+
}
|