Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support to deploy smart contracts that verify ZkProgram proofs #547

Merged
merged 53 commits into from
Mar 8, 2024
Merged
Changes from 49 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
d6ef56c
feat(deploy.js): add function findZkPrograms to find zkprograms in a …
ymekuria Dec 13, 2023
63b9240
feat(deploy.js): add function to find a zkprogram file
ymekuria Dec 13, 2023
d26b6c8
feat(deploy.js): add support for finding zkPrograms in build directory
ymekuria Dec 15, 2023
7e15879
feat(deploy.js): add function findZkProgramFile to find the file name…
ymekuria Dec 15, 2023
894c9e1
feat(deploy.js): add function to check if smart contract verifys a zk…
ymekuria Dec 21, 2023
51a00c0
feat(deploy.js): add logic to check if zkprogram exists before findin…
ymekuria Dec 21, 2023
9cf223a
feat(deploy.js): handle error when compiling zkApp that verifies unco…
ymekuria Dec 21, 2023
963f357
refactor(deploy.js): add getZkProgramName function to extract zkProgr…
ymekuria Dec 22, 2023
416707f
feat(deploy.js): add conditional check to import and compile ZkProgra…
ymekuria Dec 22, 2023
2f1c625
feat(deploy.js): update findZkProgramFile function to return the vari…
ymekuria Dec 22, 2023
a3a7295
feat(deploy.js): return zkProgramFile and zkProgramVarName in findZkP…
ymekuria Dec 22, 2023
5990029
feat(deploy.js): construct zkprogram import path
ymekuria Dec 22, 2023
83a6864
feat(deploy.js): update the hasZkProgram function to check if a smart…
ymekuria Dec 22, 2023
85deb56
feat(deploy.js): import zkprogram that the smart contract verifies
ymekuria Dec 22, 2023
7e744ff
refactor(deploy.js): remove unused code
ymekuria Dec 22, 2023
8ad4af2
feat(deploy.js): add support for compiling zkProgram before deploying…
ymekuria Dec 22, 2023
69bf5b4
refactor(deploy.js): remove logs
ymekuria Dec 22, 2023
f43217b
refactor(deploy.js): remove unused hasZkProgram function
ymekuria Dec 22, 2023
7c636c2
refactor(deploy.js): remove unnecessary logs
ymekuria Dec 22, 2023
f5a1c3d
feat(deploy.js): improve doc comments
ymekuria Dec 22, 2023
a729426
refactor(deploy.js): remove unused code and add comments
ymekuria Dec 22, 2023
0084407
fix(deploy.js): change variable and function names from zkProgramName…
ymekuria Dec 22, 2023
6683280
fix(deploy.js): handle case when ZkProgram name argument contains hyp…
ymekuria Dec 24, 2023
1e0cfc0
fix(deploy.js): handle case when ZkProgram name argument contains hyp…
ymekuria Dec 24, 2023
9144dbb
feat(deploy.js): add zkProgram field to cache[contractName] object to…
ymekuria Dec 25, 2023
22fe704
feat(deploy.js): add support for storing zkProgramNameArg in cache fo…
ymekuria Dec 25, 2023
d067fc6
feat(deploy.js): add caching of ZkProgram verification key and name t…
ymekuria Dec 25, 2023
12c7950
feat(deploy.js): add ZkProgram digest to cache
ymekuria Dec 25, 2023
1dde941
feat(deploy.js): initialize ZkProgram cache
ymekuria Dec 28, 2023
d7118ee
feat(deploy.js): add support for checking if the zk program has chang…
ymekuria Dec 28, 2023
02651c2
feat(deploy.js): compile ZkProgram if digest changes
ymekuria Dec 28, 2023
19441b0
refactor(deploy.js): remove unused variables and logs
ymekuria Jan 4, 2024
1366cfa
Merge branch 'main' into feature/deploy-zkprogram
ymekuria Feb 20, 2024
b64578f
refactor(deploy.js): update file paths to use projectRoot instead of DIR
ymekuria Feb 21, 2024
8f2abb7
refactor(deploy.js): generate zkApp verification key in seperate func…
ymekuria Feb 21, 2024
259ece9
feat(deploy.js): update generateVerificationKey function to include n…
ymekuria Feb 21, 2024
e8f63be
refactor(deploy.js): update network selection user prompt response va…
ymekuria Feb 22, 2024
8e8a782
refactor(deploy.js): rename contract name selection user prompt respo…
ymekuria Feb 22, 2024
3df6bae
refactor(deploy.js): move code to get contract name to deploy into a …
ymekuria Feb 22, 2024
aae2f68
feat(deploy.js): add vk cache comments
ymekuria Feb 22, 2024
d37d0cd
docs(deploy.js): update documentation for `findZkProgram` function
ymekuria Feb 22, 2024
9f7eca3
feat(deploy.js): update doc comment for `getZkProgramNameArg` function
ymekuria Feb 22, 2024
4cba5f7
feat(deploy.js): add getZkProgram function to retrieve zk program fro…
ymekuria Feb 23, 2024
1da209c
feat(deploy.js): find ZKprogram and import it in a seperate function
ymekuria Feb 23, 2024
cede5eb
feat(deploy.js): add doc comments for the getZkProgram function
ymekuria Feb 23, 2024
7009d76
refactor(deploy.js): update verification key generation to use getZkP…
ymekuria Feb 23, 2024
5dd16b2
feat(deploy.js): improve error handeling when ZkProgram compilation o…
ymekuria Feb 23, 2024
8364ec8
refactor(deploy.js): remove logs
ymekuria Feb 23, 2024
6890a88
refactor(deploy.js): remove unused findZkPrograms function
ymekuria Feb 23, 2024
77532b0
refactor(deploy.js): update comments
ymekuria Mar 5, 2024
872f77a
Merge branch 'main' into feature/deploy-zkprogram
ymekuria Mar 7, 2024
080fd9f
chore(CHANGELOG.md): add entry for adding support to deploy smart con…
ymekuria Mar 7, 2024
b62a5df
fix(deploy.js): remove extra space in file path to fix import issue o…
ymekuria Mar 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
305 changes: 230 additions & 75 deletions src/lib/deploy.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export async function deploy({ alias, yes }) {
process.exit(1);
}

const res = await enquirer.prompt({
const deployAliasResponse = await enquirer.prompt({
type: 'select',
name: 'network',
choices: aliases,
Expand All @@ -84,7 +84,7 @@ export async function deploy({ alias, yes }) {
: chalk.red(state.symbols.cross);
},
});
alias = res.network;
alias = deployAliasResponse.network;
}

alias = alias.toLowerCase();
Expand Down Expand Up @@ -139,50 +139,7 @@ export async function deploy({ alias, yes }) {
return { smartContracts };
});

// Identify which smart contract to be deployed for this deploy alias.
let contractName = chooseSmartContract(config, build, alias);

// If no smart contract is specified for this deploy alias in config.json &
// 2+ smart contracts exist in build.json, ask which they want to use.
if (!contractName) {
const res = await enquirer.prompt({
type: 'select',
name: 'contractName',
choices: build.smartContracts,
message: (state) => {
// Makes the step text green upon success, else uses reset.
const style =
state.submitted && !state.cancelled
? state.styles.success
: chalk.reset;
return style('Choose smart contract to deploy');
},
prefix: (state) => {
// Shows a cyan question mark when not submitted.
// Shows a green check mark if submitted.
// Shows a red "x" if ctrl+C is pressed (default is a magenta).
if (!state.submitted) return state.symbols.question;
return !state.cancelled
? state.symbols.check
: chalk.red(state.symbols.cross);
},
});
contractName = res.contractName;
} else {
// Can't include the log message inside this callback b/c it will mess up
// the step formatting.
await step('Choose smart contract', async () => {});

if (config.deployAliases[alias]?.smartContract) {
log(
` The '${config.deployAliases[alias]?.smartContract}' smart contract will be used\n for this deploy alias as specified in config.json.`
);
} else {
log(
` Only one smart contract exists in the project: ${build.smartContracts[0]}`
);
}
}
const contractName = await getContractName(config, build, alias);

// Set the default smartContract name for this deploy alias in config.json.
// Occurs when this is the first time we're deploying to a given deploy alias.
Expand Down Expand Up @@ -286,6 +243,7 @@ export async function deploy({ alias, yes }) {

process.exit(1);
}

const zkApp = smartContractImports[contractName]; // The specified zkApp class to deploy
const zkAppPrivateKey = PrivateKey.fromBase58(zkAppPrivateKeyBase58); // The private key of the zkApp
const zkAppAddress = zkAppPrivateKey.toPublicKey(); // The public key of the zkApp
Expand All @@ -308,34 +266,15 @@ export async function deploy({ alias, yes }) {

const { verificationKey, isCached } = await step(
'Generate verification key (takes 10-30 sec)',
async () => {
let cache = fs.readJsonSync(`${projectRoot}/build/cache.json`);
// compute a hash of the contract's circuit to determine if 'zkapp.compile' should re-run or cached verfification key can be used
let currentDigest = await zkApp.digest(zkAppAddress);

// initialize cache if 'zk deploy' is run the first time on the contract
if (!cache[contractName]) {
cache[contractName] = { digest: '', verificationKey: '' };
}

if (!isInitMethod && cache[contractName]?.digest === currentDigest) {
return {
verificationKey: cache[contractName].verificationKey,
isCached: true,
};
} else {
const { verificationKey } = await zkApp.compile(zkAppAddress);
// update cache with new verification key and currrentDigest
cache[contractName].verificationKey = verificationKey;
cache[contractName].digest = currentDigest;

fs.writeJSONSync(`${projectRoot}/build/cache.json`, cache, {
spaces: 2,
});

return { verificationKey, isCached: false };
}
}
async () =>
await generateVerificationKey(
projectRoot,
contractName,
zkApp,
zkAppAddress,
isInitMethod
)
);

// Can't include the log message inside the callback b/c it will break
Expand Down Expand Up @@ -491,6 +430,148 @@ export async function deploy({ alias, yes }) {
process.exit(0);
}

async function getContractName(config, build, alias) {
// Identify which smart contract to be deployed for this deploy alias.
let contractName = chooseSmartContract(config, build, alias);

// If no smart contract is specified for this deploy alias in config.json &
// 2+ smart contracts exist in build.json, ask which they want to use.
if (!contractName) {
const contractNameResponse = await enquirer.prompt({
type: 'select',
name: 'contractName',
choices: build.smartContracts,
message: (state) => {
// Makes the step text green upon success, else uses reset.
const style =
state.submitted && !state.cancelled
? state.styles.success
: chalk.reset;
return style('Choose smart contract to deploy');
},
prefix: (state) => {
// Shows a cyan question mark when not submitted.
// Shows a green check mark if submitted.
// Shows a red "x" if ctrl+C is pressed (default is a magenta).
if (!state.submitted) return state.symbols.question;
return !state.cancelled
? state.symbols.check
: chalk.red(state.symbols.cross);
},
});
contractName = contractNameResponse.contractName;
} else {
// Can't include the log message inside this callback b/c it will mess up
// the step formatting.
await step('Choose smart contract', async () => {});

if (config.deployAliases[alias]?.smartContract) {
log(
` The '${config.deployAliases[alias]?.smartContract}' smart contract will be used\n for this deploy alias as specified in config.json.`
);
} else {
log(
` Only one smart contract exists in the project: ${build.smartContracts[0]}`
);
}
}
return contractName;
}

async function generateVerificationKey(
projectRoot,
contractName,
zkApp,
zkAppAddress,
isInitMethod
) {
let cache = fs.readJsonSync(`${projectRoot}/build/cache.json`);
// compute a hash of the contract's circuit to determine if 'zkapp.compile' should re-run or cached verfification key can be used
let currentDigest = await zkApp.digest(zkAppAddress);

// initialize cache if 'zk deploy' is run the first time on the contract
cache[contractName] = cache[contractName] ?? {};

let zkProgram, currentZkProgramDigest, zkProgramNameArg;

// if zk program name is in the cache, import it to compute the digest to determine if it has changed
if (cache[contractName]?.zkProgram) {
zkProgramNameArg = cache[contractName]?.zkProgram;
zkProgram = await getZkProgram(projectRoot, zkProgramNameArg);
currentZkProgramDigest = await zkProgram.digest();
}

// If smart contract doesn't change and there is no zkprogram return contract cached vk
if (!isInitMethod && cache[contractName]?.digest === currentDigest) {
let isCached = true;

if (
cache[contractName]?.zkProgram &&
currentZkProgramDigest !== cache[zkProgramNameArg]?.digest
) {
const zkProgramDigest = await zkProgram.digest();
await zkProgram.compile();

// update cache with zkprogram digest. VK is not necessary because not depoying the zkprogram
cache[zkProgramNameArg].digest = zkProgramDigest;

fs.writeJSONSync(`${projectRoot}/build/cache.json`, cache, {
spaces: 2,
});
isCached = false;
}
// return vk and isCached flag if only a smart contract that is unchanged or both zkprogram and smart contract unchanged
return {
verificationKey: cache[contractName].verificationKey,
isCached,
};
} else {
// case when deploy is run for the first time or smart contract has changed or has an init method
let verificationKey;
try {
// attempt to compile the zkApp
const result = await zkApp.compile(zkAppAddress);

verificationKey = result.verificationKey;
} catch (error) {
// if the zkApp compilation fails because the ZkProgram compilation output that the smart contract verifies is not found,
// the error message is parsed to get the ZkProgram name argument.
if (error.message.includes(`but we cannot find compilation output for`)) {
zkProgramNameArg = getZkProgramNameArg(error.message);
} else {
console.error(error);
process.exit(1);
}
}
// import and compile ZkProgram if smart contract to deploy verifies it
if (zkProgramNameArg) {
zkProgram = await getZkProgram(projectRoot, zkProgramNameArg);
const currentZkProgramDigest = await zkProgram.digest();
await zkProgram.compile();

//
const result = await zkApp.compile(zkAppAddress);
verificationKey = result.verificationKey;

// Add ZkProgram name to cache of the smart contract that verifies it
cache[contractName].zkProgram = zkProgramNameArg;
// Initialize zkprogram cache if not defined
cache[zkProgramNameArg] = cache[zkProgramNameArg] ?? {};
cache[zkProgramNameArg].digest = currentZkProgramDigest;
}

// update cache with new smart contract verification key and currrentDigest
cache[contractName].verificationKey = verificationKey;
cache[contractName].digest = currentDigest;

fs.writeJSONSync(`${projectRoot}/build/cache.json`, cache, {
spaces: 2,
});

return { verificationKey, isCached: false };
}
}

// Get the specified blockchain explorer url with txn hash
function getTxnUrl(graphQlUrl, txn) {
const txnBroadcastServiceName = new URL(graphQlUrl).hostname
Expand Down Expand Up @@ -569,21 +650,21 @@ function hasBreakingChanges(installedVersion, latestVersion) {
* @param {string} path The glob pattern--e.g. `build/**\/*.js`
* @returns {Promise<array>} The user-specified class names--e.g. ['Foo', 'Bar']
*/

export async function findSmartContracts(path) {
if (process.platform === 'win32') {
path = path.replaceAll('\\', '/');
}
const files = await glob(path);

let smartContracts = [];

for (const file of files) {
const str = fs.readFileSync(file, 'utf-8');
let results = str.matchAll(/class (\w*) extends SmartContract/gi);
results = Array.from(results) ?? []; // prevent error if no results
results = results.map((result) => result[1]); // only keep capture groups
smartContracts.push(...results);
}

return smartContracts;
}

Expand All @@ -610,6 +691,80 @@ export function chooseSmartContract(config, deploy, deployAliasName) {
return '';
}

// Finds the the user defined name argument of the ZkProgram that is verified in a smart contract
// https://github.com/o1-labs/o1js/blob/7f1745a48567bdd824d4ca08c483b4f91e0e3786/src/examples/zkprogram/program.ts#L16.
function getZkProgramNameArg(message) {
let zkProgramNameArg = null;

// Regex to parse the ZkProgram name argment that is specified in the given message
const regex =
/depends on ([\w-]+), but we cannot find compilation output for ([\w-]+)/;
const match = message.match(regex);
if (match && match[1] === match[2]) {
zkProgramNameArg = match[1];
return zkProgramNameArg;
}
return zkProgramNameArg;
}

/**
* Find the file and variable name of the ZkProgram.
* @param {string} buildPath The glob pattern--e.g. `build/**\/*.js`
* @param {string} zkProgramNameArg The user-specified ZkProgram name argument https://github.com/o1-labs/o1js/blob/7f1745a48567bdd824d4ca08c483b4f91e0e3786/src/examples/zkprogram/program.ts#L16.
* @returns {Promise<{zkProgramVarName: string, zkProgramFile: string}>}
* An object containing the variable name (`zkProgramVarName`)
* of the ZkProgram and the file name (`zkProgramFile`) in which the specified ZkProgram is found.
* Returns null if the ZkProgram is not found.
*/
async function findZkProgramFile(buildPath, zkProgramNameArg) {
if (process.platform === 'win32') {
buildPath = buildPath.replaceAll('\\', '/');
}
const files = await glob(buildPath);

for (const file of files) {
const zkProgram = fs.readFileSync(file, 'utf-8');

// Regex is used to find and extract the variable name of the ZkProgram
// that has a matching name argument that is verified in the smart contract
// to be deployed.
const regex =
/(\w+)\s*=\s*ZkProgram\(\{[\s\S]*?name:\s*'([\w-]+)'[\s\S]*?\}\);/g;
let match;

while ((match = regex.exec(zkProgram)) !== null) {
const [_, zkProgramVarName, nameArg] = match;

if (nameArg === zkProgramNameArg) {
return { zkProgramVarName, zkProgramFile: path.basename(file) };
}
}
}
}

/**
* Find and import the ZkProgram.
* @param {string} projectRoot The root directory path of the smart contract
* @param {string} zkProgramNameArg The user-specified ZkProgram name argument https://github.com/o1-labs/o1js/blob/7f1745a48567bdd824d4ca08c483b4f91e0e3786/src/examples/zkprogram/program.ts#L16.
* @returns {Promise<object>} The ZkProgram.
*
*/
async function getZkProgram(projectRoot, zkProgramNameArg) {
let { zkProgramFile, zkProgramVarName } = await findZkProgramFile(
shimkiv marked this conversation as resolved.
Show resolved Hide resolved
`${projectRoot}/build/**/*.js`,
zkProgramNameArg
);

const zkProgramImportPath =
process.platform === 'win32'
? `file:// ${projectRoot}/build/src/${zkProgramFile}`
ymekuria marked this conversation as resolved.
Show resolved Hide resolved
: `${projectRoot}/build/src/${zkProgramFile}`;

const zkProgramImports = await import(zkProgramImportPath);
const zkProgram = zkProgramImports[zkProgramVarName];

return zkProgram;
}
/**
* Find the file name of the smart contract to be deployed.
* @param {string} buildPath The glob pattern--e.g. `build/**\/*.js`
Expand Down
Loading