Skip to content

Commit 0e5f958

Browse files
committed
bindings: create napi srs bindings
1 parent 0b18661 commit 0e5f958

File tree

3 files changed

+350
-2
lines changed

3 files changed

+350
-2
lines changed

src/bindings/crypto/bindings.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@ import { PallasBindings, VestaBindings } from './bindings/curve.js';
1515
import { jsEnvironment } from './bindings/env.js';
1616
import { FpBindings, FqBindings } from './bindings/field.js';
1717
import { srs } from './bindings/srs.js';
18+
import { srs as napiSrs } from './napi-srs.js';
1819
import { FpVectorBindings, FqVectorBindings } from './bindings/vector.js';
1920
import { napiConversionCore } from './napi-conversion-core.js';
2021
import { napiProofConversion } from './napi-conversion-proof.js';
22+
import type * as napiNamespace from '../compiled/node_bindings/plonk_wasm.cjs';
2123

22-
export { RustConversion, Wasm, createNativeRustConversion, getRustConversion };
24+
export { RustConversion, Wasm, Napi, createNativeRustConversion, getRustConversion };
2325

2426
/* TODO: Uncomment in phase 2 of conversion layer
2527
import { conversionCore as conversionCoreNative } from './native/conversion-core.js';
@@ -49,13 +51,14 @@ const tsBindings = {
4951
return bundle.srsFactory(wasm, bundle.conversion);
5052
},*/
5153
srs: (wasm: Wasm) => srs(wasm, getRustConversion(wasm)),
52-
srsNative: (napi: Wasm) => srs(napi, createNativeRustConversion(napi) as any),
54+
srsNative: (napi: Napi) => napiSrs(napi, createNativeRustConversion(napi) as any),
5355
};
5456

5557
// this is put in a global variable so that mina/src/lib/crypto/kimchi_bindings/js/bindings.js finds it
5658
(globalThis as any).__snarkyTsBindings = tsBindings;
5759

5860
type Wasm = typeof wasmNamespace;
61+
type Napi = typeof napiNamespace;
5962

6063
type RustConversion = ReturnType<typeof buildWasmConversion>;
6164

src/bindings/crypto/napi-srs.ts

Lines changed: 343 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
1+
import { MlArray } from '../../lib/ml/base.js';
2+
import {
3+
readCache,
4+
withVersion,
5+
writeCache,
6+
type Cache,
7+
type CacheHeader,
8+
} from '../../lib/proof-system/cache.js';
9+
import { assert } from '../../lib/util/errors.js';
10+
import { type WasmFpSrs, type WasmFqSrs } from '../compiled/node_bindings/plonk_wasm.cjs';
11+
import type { RustConversion, Napi } from './bindings.js';
12+
import { OrInfinity, OrInfinityJson } from './bindings/curve.js';
13+
import { PolyComm } from './bindings/kimchi-types.js';
14+
15+
export { setSrsCache, srs, unsetSrsCache };
16+
17+
type NapiSrs = WasmFpSrs | WasmFqSrs;
18+
19+
type SrsStore = Record<number, NapiSrs>;
20+
21+
function empty(): SrsStore {
22+
return {};
23+
}
24+
25+
const srsStore = { fp: empty(), fq: empty() };
26+
27+
const CacheReadRegister = new Map<string, boolean>();
28+
29+
let cache: Cache | undefined;
30+
31+
function setSrsCache(c: Cache) {
32+
cache = c;
33+
}
34+
function unsetSrsCache() {
35+
cache = undefined;
36+
}
37+
38+
const srsVersion = 1;
39+
40+
function cacheHeaderLagrange(f: 'fp' | 'fq', domainSize: number): CacheHeader {
41+
let id = `lagrange-basis-${f}-${domainSize}`;
42+
return withVersion(
43+
{
44+
kind: 'lagrange-basis',
45+
persistentId: id,
46+
uniqueId: id,
47+
dataType: 'string',
48+
},
49+
srsVersion
50+
);
51+
}
52+
function cacheHeaderSrs(f: 'fp' | 'fq', domainSize: number): CacheHeader {
53+
let id = `srs-${f}-${domainSize}`;
54+
return withVersion(
55+
{
56+
kind: 'srs',
57+
persistentId: id,
58+
uniqueId: id,
59+
dataType: 'string',
60+
},
61+
srsVersion
62+
);
63+
}
64+
65+
function srs(napi: Napi , conversion: RustConversion) {
66+
return {
67+
fp: srsPerField('fp', napi, conversion),
68+
fq: srsPerField('fq', napi, conversion),
69+
};
70+
}
71+
72+
function srsPerField(f: 'fp' | 'fq', napi: Napi, conversion: RustConversion) {
73+
// note: these functions are properly typed, thanks to TS template literal types
74+
let createSrs = (size: number) => {
75+
try {
76+
console.log(0);
77+
return napi[`caml_${f}_srs_create_parallel`](size);
78+
} catch (error) {
79+
console.error(`Error in SRS get for field ${f}`);
80+
throw error;
81+
}
82+
};
83+
let getSrs = (srs: NapiSrs) => {
84+
try {
85+
console.log(1);
86+
let v = napi[`caml_${f}_srs_get`](srs);
87+
console.log(2);
88+
return v;
89+
} catch (error) {
90+
console.error(`Error in SRS get for field ${f}`);
91+
throw error;
92+
}
93+
};
94+
let setSrs = (bytes: any) => {
95+
try {
96+
console.log(2);
97+
return napi[`caml_${f}_srs_set`](bytes);
98+
} catch (error) {
99+
console.error(`Error in SRS set for field ${f} args ${bytes}`);
100+
throw error;
101+
}
102+
};
103+
104+
let maybeLagrangeCommitment = (srs: NapiSrs, domain_size: number, i: number) => {
105+
try {
106+
return napi[`caml_${f}_srs_maybe_lagrange_commitment`](srs, domain_size, i);
107+
} catch (error) {
108+
console.error(`Error in SRS maybe lagrange commitment for field ${f}`);
109+
throw error;
110+
}
111+
};
112+
let lagrangeCommitment = (srs: NapiSrs, domain_size: number, i: number) => {
113+
try {
114+
return napi[`caml_${f}_srs_lagrange_commitment`](srs, domain_size, i);
115+
} catch (error) {
116+
console.error(`Error in SRS lagrange commitment for field ${f}`);
117+
throw error;
118+
}
119+
};
120+
let setLagrangeBasis = (srs: NapiSrs, domain_size: number, input: any) => {
121+
try {
122+
console.log(6);
123+
return napi[`caml_${f}_srs_set_lagrange_basis`](srs, domain_size, input);
124+
} catch (error) {
125+
console.error(`Error in SRS set lagrange basis for field ${f}`);
126+
throw error;
127+
}
128+
};
129+
let getLagrangeBasis = (srs: NapiSrs, n: number) => {
130+
try {
131+
return napi[`caml_${f}_srs_get_lagrange_basis`](srs, n);
132+
} catch (error) {
133+
console.error(`Error in SRS get lagrange basis for field ${f}`);
134+
throw error;
135+
}
136+
};
137+
return {
138+
/**
139+
* returns existing stored SRS or falls back to creating a new one
140+
*/
141+
create(size: number): NapiSrs {
142+
let srs = srsStore[f][size] satisfies NapiSrs as NapiSrs | undefined;
143+
144+
if (srs === undefined) {
145+
if (cache === undefined) {
146+
// if there is no cache, create SRS in memory
147+
console.log('Creating SRS without cache');
148+
srs = createSrs(size);
149+
console.log('SRS created without cache:', srs);
150+
} else {
151+
let header = cacheHeaderSrs(f, size);
152+
153+
// try to read SRS from cache / recompute and write if not found
154+
console.log('Reading SRS from cache');
155+
srs = readCache(cache, header, (bytes) => {
156+
// TODO: this takes a bit too long, about 300ms for 2^16
157+
// `pointsToRust` is the clear bottleneck
158+
let jsonSrs: OrInfinityJson[] = JSON.parse(new TextDecoder().decode(bytes));
159+
let mlSrs = MlArray.mapTo(jsonSrs, OrInfinity.fromJSON);
160+
let wasmSrs = conversion[f].pointsToRust(mlSrs);
161+
return setSrs(wasmSrs);
162+
});
163+
console.log('SRS read from cache:', srs);
164+
if (srs === undefined) {
165+
// not in cache
166+
console.log(1);
167+
srs = createSrs(size);
168+
console.log('Writing SRS to cache', srs);
169+
170+
if (cache.canWrite) {
171+
console.log(2);
172+
let wasmSrs = getSrs(srs);
173+
console.log(3);
174+
let mlSrs = conversion[f].pointsFromRust(wasmSrs);
175+
let jsonSrs = MlArray.mapFrom(mlSrs, OrInfinity.toJSON);
176+
let bytes = new TextEncoder().encode(JSON.stringify(jsonSrs));
177+
178+
writeCache(cache, header, bytes);
179+
}
180+
}
181+
}
182+
console.log('Storing SRS in memory');
183+
srsStore[f][size] = srs;
184+
console.log('SRS stored in memory:', srs);
185+
}
186+
187+
// TODO should we call freeOnFinalize() and expose a function to clean the SRS cache?
188+
console.trace('Returning SRS:', srs);
189+
return srsStore[f][size];
190+
},
191+
192+
/**
193+
* returns ith Lagrange basis commitment for a given domain size
194+
*/
195+
lagrangeCommitment(srs: NapiSrs, domainSize: number, i: number): PolyComm {
196+
console.log('lagrangeCommitment');
197+
// happy, fast case: if basis is already stored on the srs, return the ith commitment
198+
let commitment = maybeLagrangeCommitment(srs, domainSize, i);
199+
200+
if (commitment === undefined) {
201+
if (cache === undefined) {
202+
// if there is no cache, recompute and store basis in memory
203+
commitment = lagrangeCommitment(srs, domainSize, i);
204+
} else {
205+
// try to read lagrange basis from cache / recompute and write if not found
206+
let header = cacheHeaderLagrange(f, domainSize);
207+
let didRead = readCacheLazy(
208+
cache,
209+
header,
210+
conversion,
211+
f,
212+
srs,
213+
domainSize,
214+
setLagrangeBasis
215+
);
216+
if (didRead !== true) {
217+
// not in cache
218+
if (cache.canWrite) {
219+
// TODO: this code path will throw on the web since `caml_${f}_srs_get_lagrange_basis` is not properly implemented
220+
// using a writable cache in the browser seems to be fairly uncommon though, so it's at least an 80/20 solution
221+
let napiComms = getLagrangeBasis(srs, domainSize);
222+
console.log('napiComms', napiComms);
223+
let mlComms = conversion[f].polyCommsFromRust(napiComms);
224+
console.log('mlComms', mlComms);
225+
let comms = polyCommsToJSON(mlComms);
226+
let bytes = new TextEncoder().encode(JSON.stringify(comms));
227+
writeCache(cache, header, bytes);
228+
} else {
229+
lagrangeCommitment(srs, domainSize, i);
230+
}
231+
}
232+
// here, basis is definitely stored on the srs
233+
let c = maybeLagrangeCommitment(srs, domainSize, i);
234+
assert(c !== undefined, 'commitment exists after setting');
235+
commitment = c;
236+
}
237+
}
238+
239+
// edge case for when we have a writeable cache and the basis was already stored on the srs
240+
// but we didn't store it in the cache separately yet
241+
if (commitment && cache && cache.canWrite) {
242+
let header = cacheHeaderLagrange(f, domainSize);
243+
let didRead = readCacheLazy(
244+
cache,
245+
header,
246+
conversion,
247+
f,
248+
srs,
249+
domainSize,
250+
setLagrangeBasis
251+
);
252+
// only proceed for entries we haven't written to the cache yet
253+
if (didRead !== true) {
254+
// same code as above - write the lagrange basis to the cache if it wasn't there already
255+
// currently we re-generate the basis via `getLagrangeBasis` - we could derive this from the
256+
// already existing `commitment` instead, but this is simpler and the performance impact is negligible
257+
let napiComms = getLagrangeBasis(srs, domainSize);
258+
let mlComms = conversion[f].polyCommsFromRust(napiComms);
259+
let comms = polyCommsToJSON(mlComms);
260+
let bytes = new TextEncoder().encode(JSON.stringify(comms));
261+
262+
writeCache(cache, header, bytes);
263+
}
264+
}
265+
return conversion[f].polyCommFromRust(commitment);
266+
},
267+
268+
/**
269+
* Returns the Lagrange basis commitments for the whole domain
270+
*/
271+
lagrangeCommitmentsWholeDomain(srs: NapiSrs, domainSize: number) {
272+
console.log('lagrangeCommitmentsWholeDomain');
273+
try {
274+
let napiComms = napi[`caml_${f}_srs_lagrange_commitments_whole_domain_ptr`](srs, domainSize);
275+
let mlComms = conversion[f].polyCommsFromRust(napiComms as any);
276+
return mlComms;
277+
} catch (error) {
278+
console.error(`Error in SRS lagrange commitments whole domain ptr for field ${f}`);
279+
throw error;
280+
}
281+
},
282+
283+
/**
284+
* adds Lagrange basis for a given domain size
285+
*/
286+
addLagrangeBasis(srs: NapiSrs, logSize: number) {
287+
console.log('addLagrangeBasis');
288+
// this ensures that basis is stored on the srs, no need to duplicate caching logic
289+
this.lagrangeCommitment(srs, 1 << logSize, 0);
290+
},
291+
};
292+
}
293+
294+
type PolyCommJson = {
295+
shifted: OrInfinityJson[];
296+
unshifted: OrInfinityJson | undefined;
297+
};
298+
299+
function polyCommsToJSON(comms: MlArray<PolyComm>): PolyCommJson[] {
300+
return MlArray.mapFrom(comms, ([, elems]) => {
301+
return {
302+
shifted: MlArray.mapFrom(elems, OrInfinity.toJSON),
303+
unshifted: undefined,
304+
};
305+
});
306+
}
307+
308+
function polyCommsFromJSON(json: PolyCommJson[]): MlArray<PolyComm> {
309+
return MlArray.mapTo(json, ({ shifted, unshifted }) => {
310+
return [0, MlArray.mapTo(shifted, OrInfinity.fromJSON)];
311+
});
312+
}
313+
314+
function readCacheLazy(
315+
cache: Cache,
316+
header: CacheHeader,
317+
conversion: RustConversion,
318+
f: 'fp' | 'fq',
319+
srs: NapiSrs,
320+
domainSize: number,
321+
setLagrangeBasis: (srs: NapiSrs, domainSize: number, comms: Uint32Array) => void
322+
) {
323+
if (CacheReadRegister.get(header.uniqueId) === true) return true;
324+
return readCache(cache, header, (bytes) => {
325+
let comms: PolyCommJson[] = JSON.parse(new TextDecoder().decode(bytes));
326+
let mlComms = polyCommsFromJSON(comms);
327+
let napiComms = conversion[f].polyCommsToRust(mlComms);
328+
329+
setLagrangeBasis(srs, domainSize, napiComms);
330+
CacheReadRegister.set(header.uniqueId, true);
331+
return true;
332+
});
333+
}
334+
function runInTryCatch<T extends (...args: any[]) => any>(fn: T): T {
335+
return function (...args: Parameters<T>): ReturnType<T> {
336+
try {
337+
return fn(...args);
338+
} catch (e) {
339+
console.error(`Error in SRS function ${fn.name} with args:`, args);
340+
throw e;
341+
}
342+
} as T;
343+
}

tests/native/native.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import assert from 'node:assert';
22
import native from '../../src/native/native';
33

4+
// run with `./run tests/native/native.ts --bundle`
5+
46
console.log(native);
57

68
assert(native.getNativeCalls() == 0n, 'native module starts with no calls');

0 commit comments

Comments
 (0)