#
Engine
#
Installation
To install EZKL engine, simply use your favorite package manager:
# npm
npm install @ezkljs/engine
# yarn
yarn add @ezkljs/engine
# pnpm
pnpm add @ezkljs/engine
If you want to get your hands dirty with the Engine bindings, check out this github codespace:
It contains descriptive Jest tests that show you how to use the various bindings available in
@ezkljs/engine/nodejs
package as well as an example
Next.js application that demos the @ezkljs/engine/web
web bundle. Simply run pnpm run test
to execute all of the tests in the codespace. Everything should work out of the box, no setup commands necessary.
To view each test, open the tests
directory and click on the test you want to view. All of them are written in typescript and are well documented.
To spin up the example web app locally run pnpm run dev
We have broken down the tests for the bindings into 4 different files:
The engine exposes JS bindings to the main ezkl repo that make hashing, encrypting, decrypting, proving and verifying in the browser/nodejs context seamless:
genWitness : Generate a witness from a given input.elgamalGenRandom : Generate an ElGamal keypair from a random seed.elgamalEncrypt : Encypt an arbitrary message using the ElGamal public key and randomness.elgamalDecrypt : Decrypt a cipher text using the ElGamal secret keyprove : Generate a proofverify : Verify a given proofposeidonHash : Hash a given message using the Poseidon hash function
As well as some helper functions that make formatting data to and from field elements (the way numbers are represented in ZK):
vecU64ToFelt : Converts 4 u64s to a field element (hex string)vecU64ToInt : Converts 4 u64s representing a field element directly to an integervecU64ToFloat : Converts 4 u64s representing a field element directly to a (rescaled from fixed point scaling) floating pointfloatToVecU64 : Converts a floating point element to 4 u64s representing a fixed point field elementbufferToVecOfVecU64 : Converts a buffer to vector of vector of 4 u64s
The engine methods use web assembly in the backend to support running rust code in a browser/JS context. Non primitive data types passed to and returned from WASM bindings must come in a serialized (buffer) format. These serialize and deserialize methods make it easier to convert JS objects to and from a format that can be accepted by the engine methods.
serialize : Convert the JS object representation of a given artifact into a format that can be accepted by the engine methods.deserialize : Convert the serialized representation of a given artifact into a JS object.
#
Nodejs vs Web targets
The engine has two targets: nodejs
and web
. The nodejs
target is used for nodejs applications (e.i. nodejs serverside data processing) and the web
target is used for browser applications (e.i. React front ends). From a peformance standpoint, the only
difference between the two is that the web bundle supports multithreading via a web worker instance and nodejs does not.
This readme will focus on documenting the web target. If you are interested in using the nodejs target, please refer to the unit tests we wrote for them.
If you just want to jump right into viewing an example application that demonstrates how to use all of the core engine bindings check out this app. The code for which can be found here
#
Cross Origin Isolation [VERY IMPORTANT].
In order to use "SharedArrayBuffer" feature in all browsers, you need to ensure the global crossOriginIsolated
is set to true
. Otherwise, the ezkljs engine bindings might not work across all browsers, as the WebAssembly memory is shared between the main thread and the web worker. Follow this guide by Google to ensure your web app is cross origin isolated. In the example app we built using next js, we enabled cross origin isolation by adding the following to our next.config.js file:
const nextConfig = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Cross-Origin-Embedder-Policy',
value: 'require-corp',
},
{
key: 'Cross-Origin-Opener-Policy',
value: 'same-origin',
},
],
},
];
},
};
#
Engine Debugging
To make the errors returned by the engine comprehenadble, we recommend calling the init_panic_hook
method before calling any of the other engine methods.
This will ensure that the engine errors are logged to the console in a readable format, otherwise if an error does throw it will be logged to the console as RuntimeError: unreachable
If you also want to print debug statments from the engine, you can call the init_logger
method. This will print all debug statments to the console.
#
Instantiating a new web assembly instance.
If you are using the web bundle, you will first need to initialize a web assembly module before you can use any of the methods exposed by the engine. To do this import the default export from @ezkljs/engine/web/ezkl.js
and then call it. If you want to overide the default WASM memory size, you can pass in a WebAssembly.Memory object as the second argument. We highly recommend doing this if you want your application to be compatible with mobile browsers, as the default memory allocation is too large for iOS browsers. From our experimentation, we have found that the maximum memory allocation that iOS browsers can handle is 4096 mb (default is 65536 mb).
In the example code below, we call each of these potential "setup" methods in the useEffect hook of the home component of a Next.js application::
import init, { init_panic_hook, init_logger } from '@ezkljs/engine/web/ezkl.js'
export default function Home() {
useEffect(() => {
async function run() {
// Initialize the WASM module. Here we are overiding the default memory allocation with the recommend allocation to be compatible across all
// browsers, including mobile browsers (the most limiting in terms of memory allocation).
await init(undefined, new WebAssembly.Memory({initial:20,maximum:4096,shared:true}))
// Initialize the panic hook and logger
init_panic_hook()
init_logger()
}
run()
})
}
#
Generating a witness
Check out the generate witness form in the example app here. To generate a witness from a given input, you can use the genWitness
method. You can think of the witness as the input output pair that gets generated by quantizing the input, running it through the quantized model and performing any hashing or encryption. This witness file contains the input and output pair needed to prove statements such as "I ran my neural network on this data and it produced this output". Link to the method used here in the example app: handleGenWitnessButton.
import { genWitness, deserialize } from '@ezkljs/engine/web'
// This is the code for the button that triggers the
// witness generation that we used in the example app.
export async function handleGenWitnessButton<T extends FileMapping>(
files: T,
): Promise<Uint8ArrayResult> {
const result = await convertFilesToFilesSer(files)
const start = performance.now(); // Start the timer
let output = genWitness(
result['compiled_model'],
result['input']
)
let witness = deserialize(output)
console.log(JSON.stringify(witness, null, 2))
const end = performance.now(); // End the timer
return {
output: output,
executionTime: end - start
}
}
Output:
{
"inputs": [
[
[
"14385415396251402209",
"2429374486035521128",
"12558163205804149944",
"2583518171365219058"
],
[
"6425625360762666998",
"7924344314350639699",
"14762033076929465436",
"2023505479389396574"
],
[
"1949230679015292902",
"16913946402569752895",
"5177146667339417225",
"1571765431670520771"
]
]
],
"outputs": [
[
[
"415066004289224689",
"11886516471525959549",
"3696305541684646538",
"3035258219084094862"
],
[
"956231351009279921",
"10951436676983309100",
"2250248050743556928",
"1228298028208591648"
],
[
0,
0,
0,
0
],
[
0,
0,
0,
0
]
]
],
"processed_inputs": null,
"processed_params": null,
"processed_outputs": null,
"max_lookup_inputs": 22
}
#
Elgamal Variables
Check out the encryption form in the example app here. You can generate a random ElGamal keypair from a random seed to use for encryption and decryption by using the elgamalGenRandom
method. Link to the methods used here in the example app: handleGenREVButton
import { elgamalGenRandom, deserialize } from '@ezkljs/engine/web'
// Function to generate a 256 bit seed in the browser
// using a cryptographically secure source of randomness.
// DO NOT USE MATH.RANDOM AS ITS NOT A SECURE SOURCE OF RANDOMNESS
function generate256BitSeed(): Uint8ClampedArray {
const uuid = self.crypto.randomUUID();
const buffer = new TextEncoder().encode(uuid)
let seed = self.crypto.getRandomValues(buffer);
seed = seed.slice(0, 32);
return new Uint8ClampedArray(seed.buffer);
}
// Function to convert the return buffer elgamalGenRandom into a JSON object
function uint8ArrayToJsonObject(uint8Array) {
let string = new TextDecoder().decode(uint8Array);
let jsonObject = JSONBig.parse(string);
return jsonObject;
}
const buffer = elgamalGenRandom(generate256BitSeed())
// We can take a look at the serialized contents of the
// buffer returned by elgamalGenRandom by converting
// the Uint8Array to a JSON object
let elgamalVariables = deserialize(buffer)
console.log(JSON.stringify(elgamalVariables, null, 2))
Output:
{
"r": [
"3454421873345507054",
"16808310348879090885",
"8288793628600014913",
"1828575853528138165"
],
"sk": [
"8833745371820985590",
"8074024378078747364",
"11905923764904461178",
"851912620759564510"
],
"pk": [
[
"10721601427614006364",
"165460855961439422",
"2494139285445986520",
"993922445972607230"
],
[
"11418900806032782576",
"15880182954156597531",
"16112055391177898581",
"485390167428225782"
]
],
"aux_generator": [
[
"13503889609873471352",
"8660427972151475473",
"16373159693836788928",
"3475191407935738629"
],
[
"9899281824843853575",
"11168630486339379471",
"550474616581266472",
"3277527638473654200"
]
],
"window_size": 4
}
#
Elgamal Encrypt
Check out the encryption form in the example app here. Using the public key (pk) and randomness (r) from the previous step, you can encrypt an arbitrary message using the elgamalEncrypt
method.
In the example app we use this ElgamalZipFileDownload method to download the r, sk and pk fields of the elgamalVariables as JSON files.
Once you have downloaded your Elgamal variables by clicking the "Generate" button at the top of the page, you can unzip the "elgamal_var" file in your download folder.
After which you need to create a text file that contains a message you wish to encrypt.
Since elgamal encrypt is a ZKP operation, we need to convert the message into
serialized field elements represented as u254s. We serialize the field elements
by breaking up each u254 field element into 4 u64s.
For reference, check out the example message file here
import { elgamalEncrypt, deserialize } from '@ezkljs/engine/web'
// This is the code for the button that triggers the
// generation of the elgamal encyrption ciphertext.
export async function handleGenElgamalEncryptionButton<T extends FileMapping>(
files: T,
): Promise<Uint8ArrayResult> {
const result = await convertFilesToFilesSer(files)
const start = performance.now(); // Start the timer
let output = elgamalEncrypt(
result['pk'],
result['message'],
result['r']
)
const end = performance.now(); // End the timer
let cipherText = deserialize(output)
console.log(JSON.stringify(cipherText, null, 2))
return {
output: output,
executionTime: end - start
}
}
Output:
[
[
[
"10209188590651781239",
"1606867371818388001",
"2009969314029440862",
"68539226482053667"
],
[
"11542493346099842483",
"15506753538305362096",
"11085744708984070406",
"1449434265777497591"
],
[
"14888253900846995994",
"17596893823803099477",
"7958618303962015576",
"30430041561409514"
]
],
[
[
"10873062863189913544",
"1639997932972316199",
"7128784801669346971",
"2001604198948701760"
],
[
"10873062863189913545",
"1639997932972316199",
"7128784801669346971",
"2001604198948701760"
]
]
]
#
Elgamal Decrypt
Check out the encryption form in the example app here. Using the secret key (sk) and ciphertext from the previous step, you can decrypt the ciphertext using the elgamalDecrypt
method.
Method used in the example app: handleGenElgamalDecryptionButton
import { elgamalDecrypt, deserialize } from '@ezkljs/engine/web'
// This is the code for the button that triggers the
// generation of the elgamal decryption of the ciphertext.
export async function handleGenElgamalDecryptionButton<T extends FileMapping>(
files: T,
): Promise<Uint8ArrayResult> {
const result = await convertFilesToFilesSer(files)
const start = performance.now(); // Start the timer
let output = elgamalDecrypt(
result['cipher'],
result['sk']
)
const end = performance.now(); // End the timer
let message = deserialize(output)
console.log(JSON.stringify(message, null, 2))
return {
output: output,
executionTime: end - start
}
}
Output:
[
[
0,
0,
0,
0
],
[
1,
0,
0,
0
]
]
And as you can see, the output matches the original message inside of message.txt that we encrypted :-)
#
Prove
Check out the proving form in the example app here. Once you have all the necessary artifacts needed to prove (Witness, Proving Key, Compiled Onnx Model and SRS) you can generate a proof using the prove
method.
Method use in the example app handleGenProofButton
import { prove, deserialize } from '@ezkljs/engine/web'
// This is the code for the button that triggers the
// generation of a proof.
export async function handleGenProofButton<T extends FileMapping>(
files: T,
): Promise<Uint8ArrayResult> {
const result = await convertFilesToFilesSer(files)
const start = performance.now(); // Start the timer
let output = prove(
result['data'],
result['pk'],
result['model'],
result['srs'],
)
const end = performance.now(); // End the timer
let proof = deserialize(output)
console.log(JSON.stringify(proof.proof, null, 2))
console.log(JSON.stringify(proof.instances, null, 2))
return {
output: output,
executionTime: end - start
}
}
Output:
"061557cfc629ce604da894831cd029d0444acf4f7e384d980e1ba7a5204f71a42a4c51e0a713df141416fd197367b56cfb3a56b1a5c7bd137f25046aa051665612f99bb5511db4b3d582f052ec82ed49356370e792ac669a8e940d3568285bbc10c78295369f844ada048e5efa897a227f05f68cc62706b145f88e3f943e559021dcd03884e21668db5cd925ce6807d6549e851709a7a3a8ac7b9666d1b2c0100d8a2682aa56d96a2ce8d0ae671e0c35c9c43e380308d246e7613122beca4f0828cdf8c9eebb0d9ee5a82a99a16c83ee5af9830a532a37c20e9f007d3f0312c200bd527998d6bbf40fab9e56bebd691ccc8131800a9b52851aea5019d06b736c1c44165d3c1992de958365a24f151606a4a50ffd121765d5283f7f809fa684432504487d8c48f12aeaf9caf30133f9ab1ec820e18db051b2ea31f2a10174514c2fe7a9f069c9913c3ee92a5deb9ab2fe9960d9a538ca00c3e0fb2cc457b899702aa74659de954943d3a5b589f82502693997178fa55a4f8677cd908e8ddcd9801cb371b84319454d1e8e1028f61bd355ae90689f6d3793444cb343cfe4ea637f2171d720686e0de1415d608c66ea9780c1abc0c884520e81a79ff8c11f5c339c2fb76fc9740cadb6eacee21f5c4adda72dce152d513c6562f3a7d23acfa83eff124945177df5433f7929730caae3622748a1ca86a0f73d838fcdb35f2a5686f02cba638cb1e5dff403519bf4813c60782f79cf08f2e583c490670e126a21213704fde2b3a4f9a8c418b3278bfe2f55ff47c6aa2015ded6752829b3cee375941315c98e5e31d0f4ec9b894ec51fee3941c438c974c51c41316de87b09be96f5f00fcfd4a5f556069e5798f49de750328088864927d4e4fe981550e5db1b981cff01ec4b2d03f63d3b4af7dd228f44c0b9b0ad4b72caf9bf7d4cd8c5b0ca51a9cd13fcda2a4cac59a8dd387ee6f503bbfd463ee379fb89aaa0cd6cffcbd95bb7ac1a1fa808ce43262d64c2fe355fdeae74ed366d27840dda8d0fe55ba6ffcef20e1be8475b82348ec92e6025c2496e11448d8b33377a360fca6d2918898a686c982db58e3bd42491992af5b4aea77e6991ae1f039f87bd3ed19b9ec74423956a3708d42d087723886ddd1c5486ed16610a123250c3c5e6014689d2767b4b4e22ef1e3af64dee3c29529c19be5081fdcb0a40171965ad5985dd942a836617f9dbbe012f27810b056ecc44636fadbc71943de4a20ffd3dc97238b0dbca16be8c10180d116ba8cc95eeb91d10d5e19f59303e984a845437a2c850021b2aa3c2141b3e020d1469095d76fb8298a870761fd2fcc2fc8057bf030f62ac3144a5770c60b0000000000000000000000000000000000000000000000000000000000000000025865fabaaa498f6b2042243bea2276cc7cdcf25e091f96a43eddaefd2a6a935013a3354557867640b7e902a005357c86c983aaee40bccb10eb8d6db6415915e1b477e066537576f70026752c4a27da71d491787297c18ba10f2a42db41b13df2d4096ced97df14e8c89a27bbc0e26b828dad7f5005e98e201992d3849611f520a0ed6030f402c363e526bf3607cced57998af94af4b6800ba87a685edbf742b202130ece182d26daefcfbe7412ff629407701d9adeff18446edaefd9abf6c98000000000000000000000000000000000000000000000000000000000000000000bda3a6a626469e211146d2f95c86d27f2a5b9d6d68f0a9670cee1243955c0f0ae946af8d44a1c2d448c9e024d2bbe96e5d2fea5d7915ca8eb481149bfadef20ecfee1f2b7d4a2ab4df06e00486bc1bd1c617f4d59a63fc2ec237594364f7510230555057f2f18e41e3d497f29bc9a30e2cbb17896f3e7f47ae0c010264b74501a6e3c81f4a937b66fc4baa9aa1cd062cdffdfecfef96f25b52f904c8b9d5b00fdd517551727930dd77dd45abee71f7aa50966d6e97aa4ba24831bacdd6354a18930a821ae32be9c13e66e4cb2a82c4d9e9ffd500c812c9a9a731a288d9c2361888a4e1b1a43df2fe0eca25a20025b1fccef7a4da7f808e59d050b652551fa100eeac3b40d1280d81aba6a3bfb433421427713de0d5d0b0a3639f1c7a4be64b2949b981ef4878eece723328d64def0ad4df93a2774d6a882ff0b6cd039239d627305afa05611ce6705dedef6c8cb221a1f251df9b3f1a04bb66ddc64a3feac51c01e60a44f75a12585a003ae3ddfdefa7568d8b30a31e2e5e8252aa9f34e476253ae4d74edd37762da29a7287184a2b7052d3b133721407aa36b5c9996e68c70b754ea01d61e33600675eb551a78f9fbb5b2abaf9a6b4c06b6cb2046b1eba8b2691a9fb51778a8bb703a618ff4406336431f38dd6927b254bc13428883ae6520e7139fcf91c3ec9c1e2cffbd588759f55308bdde05292d97988056e23a5814c1725005ba922bcadd7efbe9144f55628330b6d6d7e75d634b917e69b648ef2fc267660bd34097a148cbce8d2e9bfb96c57c8de474bd0e8de959f76f7eea00cfe05de16ea880ec32cf78d401a6210bec843dbdcc3b55df477887fce3b921ec76a019098130b9a6bcde1ea663d118e28e6b1878a5826679483ab408ed1a128561d0b62d261638e6f75f70b0406102cf15270c719be349cadec69638972ffc3dfed2afa94ed6db88dd30fc3c370115a1b317c6994f1afb1b74e1bf123b176f5665e2f419728174208207da99c5a6fbf8eaadecfbbd7d028866dcc737b8bff64b8c40bb3ca88b64c55a6b6ecafc5336c130eafc53d44a40e26a3b0bd8270bfa32c2e"
[
[
[
0,
0,
0,
0
],
[
0,
0,
0,
0
],
[
0,
0,
0,
0
],
[
0,
0,
0,
0
]
]
]
#
Verify
Check out the verifying form in the example app here. When the proof has been generated, you can verify it using the verify
method. In addition to the proof, you will need to pass the file contents of the
verification key, circuit settings and srs files. If all the artifacts are correct, the verify method will return true.
The method used in the example app: handleVerifyButton
import { verify } from '@ezkljs/engine/web'
// This is the code for the button that triggers the
// verification of a proof.
export async function handleVerifyButton<T extends FileMapping>(
files: T,
): Promise<VerifyResult> {
const result = await convertFilesToFilesSer(files)
const start = performance.now(); // Start the timer
let output = verify(
result['proof'],
result['vk'],
result['circuitSettings'],
result['srs'],
)
const end = performance.now(); // End the timer
console.log(output)
return {
output: output,
executionTime: end - start
}
}
Output:
true
#
Hash
Check out the hashing form in the example app here. We can also use the engine to hash a given message using the Poseidon hash function. Like with the elgamal encryption and decryption, we need to convert the message into serialized field elements.
import { hash, deserialize } from '@ezkljs/engine/web'
// This is the code for the button that triggers the
// posiedon hashing of a message.
export async function handleGenHashButton(message: File): Promise<Uint8Array> {
const message_hash = await readUploadedFileAsBuffer(message)
let output = poseidonHash(message_hash)
let hash = deserialize(output)
console.log(JSON.stringify(hash, null, 2))
return output
}
If you use the same message.txt file from the elgamal encryption example, you should get the following output:
[
[
[
"11803729404268464273",
"978666863701269549",
"3979600028898666103",
"2559194768255192844"
]
]
]