From 1f63d62c25585e2109febac7441572f8b5e14ac8 Mon Sep 17 00:00:00 2001 From: John Doe <anonymous@example.com> Date: Mon, 18 Sep 2023 17:10:44 -0400 Subject: [PATCH] Sepolia Testnet Launch --- .gitignore | 5 + Nargo.toml | 7 + README.md | 2 - controllers/services.mjs | 54 +++++ deps/TechnicalPreview.json | 435 ++++++++++++++++++++++++++++++++++++ deps/UltraVerifier.json | 85 +++++++ main.js | 41 ++++ package.json | 30 +++ script/utils/MerkleTree.mjs | 180 +++++++++++++++ script/utils/helpers.js | 318 ++++++++++++++++++++++++++ script/zk.js | 375 +++++++++++++++++++++++++++++++ src/main.nr | 99 ++++++++ 12 files changed, 1629 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 Nargo.toml delete mode 100644 README.md create mode 100644 controllers/services.mjs create mode 100644 deps/TechnicalPreview.json create mode 100644 deps/UltraVerifier.json create mode 100644 main.js create mode 100644 package.json create mode 100644 script/utils/MerkleTree.mjs create mode 100644 script/utils/helpers.js create mode 100644 script/zk.js create mode 100644 src/main.nr diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..251aebd --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules +notes.sqlite +package-lock.json +txs.db +.env \ No newline at end of file diff --git a/Nargo.toml b/Nargo.toml new file mode 100644 index 0000000..d2c6b3a --- /dev/null +++ b/Nargo.toml @@ -0,0 +1,7 @@ +[package] +name = "main" +type = "bin" +authors = [""] +compiler_version = "0.10.3" + +[dependencies] \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index a680c82..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# Momiji Sepolia 1 - diff --git a/controllers/services.mjs b/controllers/services.mjs new file mode 100644 index 0000000..a11e184 --- /dev/null +++ b/controllers/services.mjs @@ -0,0 +1,54 @@ +import inquirer from 'inquirer'; +import { deposit, transact, balance } from '../script/zk.js' +import { ethers } from 'ethers'; + +let trees; +let data; + +export const serviceController = { + error: "", + balance: balance, + deposit: async () => { + + let amountPrompt = await inquirer.prompt({ + type: "input", + name: "amount", + message: "Enter the amount of XFT to deposit: " + }) + + if (isNaN(amountPrompt.amount)) { + let errorMsg = "No amount specified"; + serviceController.error = errorMsg; + console.log(errorMsg); + return; + } + + const arg = { amount: ethers.parseEther(amountPrompt.amount) }; + [trees, data] = await deposit(arg); + }, + withdraw: async () => { + + let amountPrompt = await inquirer.prompt({ + type: "input", + name: "amount", + message: "Enter the amount of XFT to withdraw: " + }) + + if (isNaN(amountPrompt.amount)) { + let errorMsg = "No amount specified"; + serviceController.error = errorMsg; + console.log(errorMsg); + return; + } + const args = { amount: ethers.parseEther(amountPrompt.amount), trees: trees, data: data }; + + await transact(args); + } +} + +export const commandController = { + 'd': serviceController.deposit, + 'w': serviceController.withdraw, + 'r': () => true, + 'exit': process.exit +} \ No newline at end of file diff --git a/deps/TechnicalPreview.json b/deps/TechnicalPreview.json new file mode 100644 index 0000000..6f4e33f --- /dev/null +++ b/deps/TechnicalPreview.json @@ -0,0 +1,435 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "TechnicalPreview", + "sourceName": "contracts/TechnicalPreview.sol", + "abi": [ + { + "inputs": [ + { + "internalType": "contract IVerifier", + "name": "_verifier", + "type": "address" + }, + { + "internalType": "address", + "name": "_token", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "MAX_FIELD_SIZE", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "commitments", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "bytes32[16]", + "name": "tx_in", + "type": "bytes32[16]" + }, + { + "internalType": "bytes32", + "name": "current_root", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "amount_public_in", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "amount_public_out", + "type": "bytes32" + }, + { + "internalType": "bytes32[16]", + "name": "commitment_out", + "type": "bytes32[16]" + }, + { + "internalType": "bytes32", + "name": "recipient", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "oracle", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "old_root", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "new_root", + "type": "bytes32" + }, + { + "internalType": "bytes32[16]", + "name": "nullifier_hashes", + "type": "bytes32[16]" + } + ], + "internalType": "struct BatchPublicInputs", + "name": "input", + "type": "tuple" + } + ], + "name": "flattenBatchPublicInputs", + "outputs": [ + { + "internalType": "bytes32[]", + "name": "", + "type": "bytes32[]" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_commitment", + "type": "bytes32" + } + ], + "name": "getCommitment", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_utxo", + "type": "bytes32" + } + ], + "name": "getRootFromUtxo", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32[]", + "name": "_nullifierHashes", + "type": "bytes32[]" + } + ], + "name": "getSpentNullifiers", + "outputs": [ + { + "internalType": "bool[]", + "name": "spent", + "type": "bool[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_root", + "type": "bytes32" + } + ], + "name": "getUtxoFromRoot", + "outputs": [ + { + "internalType": "bytes32[]", + "name": "", + "type": "bytes32[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getValidRoots", + "outputs": [ + { + "internalType": "bytes32[]", + "name": "", + "type": "bytes32[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_nullifierHash", + "type": "bytes32" + } + ], + "name": "isSpent", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "merkleRoot", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "nullifierHashes", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "_proof", + "type": "bytes" + }, + { + "components": [ + { + "internalType": "bytes32[16]", + "name": "tx_in", + "type": "bytes32[16]" + }, + { + "internalType": "bytes32", + "name": "current_root", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "amount_public_in", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "amount_public_out", + "type": "bytes32" + }, + { + "internalType": "bytes32[16]", + "name": "commitment_out", + "type": "bytes32[16]" + }, + { + "internalType": "bytes32", + "name": "recipient", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "oracle", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "old_root", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "new_root", + "type": "bytes32" + }, + { + "internalType": "bytes32[16]", + "name": "nullifier_hashes", + "type": "bytes32[16]" + } + ], + "internalType": "struct BatchPublicInputs", + "name": "_batch", + "type": "tuple" + } + ], + "name": "publish", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "utxo", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "utxoPrevRoots", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "validRoots", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "verifier", + "outputs": [ + { + "internalType": "contract IVerifier", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "_proof", + "type": "bytes" + }, + { + "internalType": "bytes32[]", + "name": "_publicInputs", + "type": "bytes32[]" + } + ], + "name": "verifyProof", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + } + ] +} \ No newline at end of file diff --git a/deps/UltraVerifier.json b/deps/UltraVerifier.json new file mode 100644 index 0000000..c83c8a0 --- /dev/null +++ b/deps/UltraVerifier.json @@ -0,0 +1,85 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "UltraVerifier", + "sourceName": "contracts/plonk_vk.sol", + "abi": [ + { + "inputs": [], + "name": "EC_SCALAR_MUL_FAILURE", + "type": "error" + }, + { + "inputs": [], + "name": "MOD_EXP_FAILURE", + "type": "error" + }, + { + "inputs": [], + "name": "PROOF_FAILURE", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "expected", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "actual", + "type": "uint256" + } + ], + "name": "PUBLIC_INPUT_COUNT_INVALID", + "type": "error" + }, + { + "inputs": [], + "name": "PUBLIC_INPUT_GE_P", + "type": "error" + }, + { + "inputs": [], + "name": "PUBLIC_INPUT_INVALID_BN128_G1_POINT", + "type": "error" + }, + { + "inputs": [], + "name": "getVerificationKeyHash", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "_proof", + "type": "bytes" + }, + { + "internalType": "bytes32[]", + "name": "_publicInputs", + "type": "bytes32[]" + } + ], + "name": "verify", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + } + ] +} diff --git a/main.js b/main.js new file mode 100644 index 0000000..8f61a1c --- /dev/null +++ b/main.js @@ -0,0 +1,41 @@ +import inquirer from 'inquirer'; +import { serviceController, commandController } from './controllers/services.mjs' + +const mainMenu = async () => { + + let answers = await inquirer.prompt([{ + type: "list", + name: "menu", + message: ` + + Momiji CLI (PureL1Rollup Sepolia Technical Preview #1) + + Balance: ${await serviceController.balance()} + + \x1b[31m${serviceController.error}\x1b[0m + + Select an option:\n`, + choices: [ + { + value: 'd', + name: "\tGENERATE ZKXFT", + short: "\n\tShifting to zkXFT" + }, + { + value: 'w', + name: "\tREDEEM ZKXFT FOR XFT", + short: "\n\tShifting back to XFT" + }, + { + value: 'exit', + name: "\tEXIT\n", + } + ] + }]) + + serviceController.error = ""; + await commandController[answers.menu](); + mainMenu(); +} + +mainMenu(); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..941a351 --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "momiji-cli", + "type": "module", + "version": "1.0.0", + "description": "", + "main": "index.js", + "directories": { + "lib": "lib", + "test": "test" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "@aztec/bb.js": "0.3.6", + "@ethersproject/abi": "^5.6.4", + "@ethersproject/providers": "^5.6.8", + "@noir-lang/noir-source-resolver": "1.1.1", + "bigint-buffer": "^1.1.5", + "circomlibjs": "^0.1.7", + "dotenv": "^16.3.1", + "ethers": "^6.6.7", + "inquirer": "^9.2.10", + "keccak256": "^1.0.6", + "sequelize": "^6.32.0", + "sqlite3": "^5.1.6" + } +} diff --git a/script/utils/MerkleTree.mjs b/script/utils/MerkleTree.mjs new file mode 100644 index 0000000..318b2b1 --- /dev/null +++ b/script/utils/MerkleTree.mjs @@ -0,0 +1,180 @@ +// @ts-ignore -- no types +import { Fr } from '@aztec/bb.js/dest/node/types/index.js'; +// @ts-ignore -- no types + +export function pedersenLeftRight( + barretenberg, + left, + right) { + + let leftBuffer = Fr.fromBufferReduce(Buffer.from(left.slice(2), 'hex')); + let rightBuffer = Fr.fromBufferReduce(Buffer.from(right.slice(2), 'hex')); + let hashRes = barretenberg.pedersenPlookupCompressFields(leftBuffer, rightBuffer); + return hashRes.toString('hex') +} + +export class MerkleTree { + zeroValue = "0xf35fcb490b7ea67c3ac26ed530fa5d8dfe8be344e7177ebb63fe02723fb6f725"; // sha256("Momiji") + levels; + + hashLeftRight; + storage; + zeros; + totalLeaves; + barretenberg; + + constructor( + levels, + barretenberg, + defaultLeaves = [], + hashLeftRight = pedersenLeftRight) { + this.levels = levels; + this.hashLeftRight = hashLeftRight; + this.storage = new Map(); + this.zeros = []; + this.totalLeaves = 0; + this.barretenberg = barretenberg; + + // build zeros depends on tree levels + let currentZero = this.zeroValue; + this.zeros.push(currentZero); + for (let i = 0; i < levels; i++) { + currentZero = this.hashLeftRight(barretenberg, currentZero, currentZero); + this.zeros.push(currentZero); + } + + if (defaultLeaves.length > 0) { + this.totalLeaves = defaultLeaves.length; + + // store leaves with key value pair + let level = 0; + defaultLeaves.forEach((leaf, index) => { + this.storage.set(MerkleTree.indexToKey(level, index), leaf); + }); + + // build tree with initial leaves + level++; + let numberOfNodesInLevel = Math.ceil(this.totalLeaves / 2); + for (level; level <= this.levels; level++) { + for (let i = 0; i < numberOfNodesInLevel; i++) { + const leftKey = MerkleTree.indexToKey(level - 1, 2 * i); + const rightKey = MerkleTree.indexToKey(level - 1, 2 * i + 1); + + const left = this.storage.get(leftKey); + const right = this.storage.get(rightKey) || this.zeros[level - 1]; + if (!left) throw new Error("leftKey not found"); + + const node = this.hashLeftRight(barretenberg, left, right); + this.storage.set(MerkleTree.indexToKey(level, i), node); + } + numberOfNodesInLevel = Math.ceil(numberOfNodesInLevel / 2); + } + } + } + + static indexToKey(level, index) { + return `${level}-${index}`; + } + + getIndex(leaf) { + for (const [key, value] of this.storage) { + if (value === leaf) { + return Number(key.split("-")[1]); + } + } + return -1; + } + + root() { + return this.storage.get(MerkleTree.indexToKey(this.levels, 0)) || this.zeros[this.levels]; + } + + proof(indexOfLeaf) { + let pathElements = []; + let pathIndices = []; + + const leaf = this.storage.get(MerkleTree.indexToKey(0, indexOfLeaf)); + if (!leaf) throw new Error("leaf not found"); + + // store sibling into pathElements and target's indices into pathIndices + const handleIndex = (level, currentIndex, siblingIndex) => { + const siblingValue = this.storage.get(MerkleTree.indexToKey(level, siblingIndex)) || this.zeros[level]; + pathElements.push(siblingValue); + pathIndices.push(currentIndex % 2); + }; + + this.traverse(indexOfLeaf, handleIndex); + + return { + root: this.root(), + pathElements, + pathIndices, + leaf: leaf, + }; + } + + insert(leaf) { + const index = this.totalLeaves; + this.update(index, leaf, true); + this.totalLeaves++; + } + + update(index, newLeaf, isInsert = false) { + if (!isInsert && index >= this.totalLeaves) { + throw Error("Use insert method for new elements."); + } else if (isInsert && index < this.totalLeaves) { + throw Error("Use update method for existing elements."); + } + + let keyValueToStore = []; + let currentElement = newLeaf; + + const handleIndex = (level, currentIndex, siblingIndex) => { + const siblingElement = this.storage.get(MerkleTree.indexToKey(level, siblingIndex)) || this.zeros[level]; + + let left; + let right; + if (currentIndex % 2 === 0) { + left = currentElement; + right = siblingElement; + } else { + left = siblingElement; + right = currentElement; + } + + keyValueToStore.push({ + key: MerkleTree.indexToKey(level, currentIndex), + value: currentElement, + }); + currentElement = this.hashLeftRight(this.barretenberg, left, right); + }; + + this.traverse(index, handleIndex); + + // push root to the end + keyValueToStore.push({ + key: MerkleTree.indexToKey(this.levels, 0), + value: currentElement, + }); + + keyValueToStore.forEach(o => { + this.storage.set(o.key, o.value); + }); + } + + // traverse from leaf to root with handler for target node and sibling node + traverse(indexOfLeaf, handler) { + let currentIndex = indexOfLeaf; + for (let i = 0; i < this.levels; i++) { + let siblingIndex; + if (currentIndex % 2 === 0) { + siblingIndex = currentIndex + 1; + } else { + siblingIndex = currentIndex - 1; + } + + handler(i, currentIndex, siblingIndex); + currentIndex = Math.floor(currentIndex / 2); + } + } +} \ No newline at end of file diff --git a/script/utils/helpers.js b/script/utils/helpers.js new file mode 100644 index 0000000..844c20b --- /dev/null +++ b/script/utils/helpers.js @@ -0,0 +1,318 @@ +import { randomBytes } from 'crypto' +import { readFileSync } from 'fs'; +import { Fr } from '@aztec/bb.js/dest/node/types/index.js'; +import { MerkleTree } from './MerkleTree.mjs'; +import { keccak256 } from "@ethersproject/keccak256/lib/index.js"; +import fs from 'fs' +const ZERO_VALUE = "0xf35fcb490b7ea67c3ac26ed530fa5d8dfe8be344e7177ebb63fe02723fb6f725"; +const MAX_VALUE = Fr.MAX_VALUE; +export const treeConfig = { + utxoDepth: 4, + txDepth: 4, + stateDepth: 9 +} + +export const circuitConfig = { + main: "./src/main.nr" +} +const treeSum = [treeConfig.utxoDepth, treeConfig.txDepth, treeConfig.stateDepth].reduce((a, b) => a + b) +function evalWithScopePrefix(js) { + let public_inputs = {}; + js.split('\n').forEach(line => { + const trimmedLine = line.trim(); + if (trimmedLine.length > 0) { + eval(`public_inputs.` + trimmedLine); + } + }); + return public_inputs; +} +export function getInputsObject() { + + + + + let regex = /(.*)\: (pub )?(\[.*\; (.*)?\],)?.*/; + let circuit = fs.readFileSync(circuitConfig.main).toString(); + let inputs = {} + let input_ordering = [] + + let structLine = circuit.split('\n').find(line => line.includes('fn main')); + let circuitSplit = circuit.split('\n'); + + let structLines = circuitSplit.slice(circuitSplit.indexOf(structLine) + 1, circuitSplit.indexOf(circuitSplit.find(line => line.includes(') {')))); + let line_maps = structLines.map(line => line.match(regex)).filter(x => x !== null); + line_maps.forEach(line_map => { + inputs[line_map[1].trim()] = [(line_map[2] ? true : false), (line_map[4] ? line_map[4] : 1)]; + input_ordering.push(line_map[1].trim()); + }); + return [inputs, input_ordering]; +} +export function getInputs() { + let inputs = getInputsObject()[0]; + return inputs; +} +export function getInputsOrdered(pub) { + let inputsObj = getInputsObject(); + let inputs = inputsObj[1]; + let input_ordering = inputs; + if (pub) input_ordering = input_ordering.map(input => inputsObj[0][input][0] ? input : null).filter(x => x !== null); + return input_ordering; +} +export function getPublicInputsOrdered() { + return getInputsOrdered(true); +} +export function readToml(path) { + let public_inputs = getPublicInputsOrdered(); + let unordered_inputs = evalWithScopePrefix(fs.readFileSync(path).toString()); + let ordered_inputs = public_inputs.map(input => unordered_inputs[input]); + return ordered_inputs; +} +export function randomBytesFr(numBytes) { + const bytes = randomBytes(numBytes) + const bytesFr = Fr.fromBufferReduce(bytes) + return bytesFr +} +export const format = (data) => { + if (typeof data === "string") return `"${data}"`; + if (data.length === 0) return "[]"; + return `[\n "${data.join('",\n "')}"\n]`; +} +export const dumpToml = (data) => { + let public_inputs = getInputsOrdered(); + let toml = []; + public_inputs.forEach((input) => { + toml.push(` ${input} = ${format(data[input])}`); + }); + + toml = toml.join('\n'); + fs.writeFileSync('./Prover.toml', toml); +} +export const dumpSolidity = () => { + + let contract = fs.readFileSync("./contracts/TechnicalPreview.sol").toString(); + + let structLine = contract.split('\n').find(line => line.includes('BatchPublicInputs')); + + let public_inputs = getInputsOrdered(true); + + let public_inputs_sizes = public_inputs.map(input => getInputs()[input][1]); + + + let new_struct = `struct BatchPublicInputs {`; + public_inputs.forEach((input, index) => { + new_struct += `\n bytes32${public_inputs_sizes[index] !== 1 ? `[${public_inputs_sizes[index]}]` : ''} ${input};`; + }); + + contract = contract.replace(structLine, new_struct); + + + let flattenLine = contract.split('\n').find(line => line.includes('function flattenBatchPublicInputs(BatchPublicInputs memory input) public pure returns (bytes32[] memory) {')); + let flattenLineIndex = contract.split('\n').indexOf(flattenLine); + let flatArrayLine = ` bytes32[] memory flatArray = new bytes32[](${public_inputs_sizes.reduce((a, b) => parseInt(a) + parseInt(b))});`; + contract = contract.split('\n').slice(0, flattenLineIndex + 1).join('\n') + '\n' + flatArrayLine + '\n' + contract.split('\n').slice(flattenLineIndex + 1).join('\n'); + + let idxLine = ` uint256 idx = 0;\n`; + contract = contract.split('\n').slice(0, flattenLineIndex + 2).join('\n') + '\n' + idxLine + '\n' + contract.split('\n').slice(flattenLineIndex + 2).join('\n'); + + let forLoopLines = []; + public_inputs.forEach((input, index) => { + if (public_inputs_sizes[index] === 1) { + forLoopLines.push(` flatArray[idx++] = input.${input};`); + } else { + forLoopLines.push(` for (uint i = 0; i < ${public_inputs_sizes[index]}; i++) flatArray[idx++] = input.${input}[i];`); + } + }); + contract = contract.split('\n').slice(0, flattenLineIndex + 3).join('\n') + '\n' + forLoopLines.join('\n') + '\n' + contract.split('\n').slice(flattenLineIndex + 3).join('\n'); + + fs.writeFileSync('./contracts/TechnicalPreview.sol', contract); +} +export function path_to_uint8array(path) { + let buffer = readFileSync(path); + return new Uint8Array(buffer); +} +const toFixedHex = (number, pad0x, length = 32) => { + let hexString = number.toString(16).padStart(length * 2, '0'); + return (pad0x ? `0x` + hexString : hexString); +} +export function getSolidityHash(asset) { + return keccak256(asset); +} +export function generateHashPathInput(hash_path) { + let hash_path_input = []; + for (var i = 0; i < hash_path.length; i++) { + hash_path_input.push(`0x` + hash_path[i]); + } + return hash_path_input; +} +export function generateUTXO(batchSize, amounts, _secrets, BarretenbergApi) { + let utxos = [] + for (let i = 0; i < batchSize; i++) { + let amountBN = amounts[i] + let utxo = { + secret: _secrets[i], + owner: BarretenbergApi.pedersenPlookupCompress([_secrets[i]]), + amountBN: amountBN, + amount: Fr.fromString(toFixedHex(Number(amountBN.toString()), true)), + assetType: Fr.fromBufferReduce(Buffer.from(getSolidityHash(0), 'hex')), + } + utxos.push(utxo) + } + return utxos +} +export async function generateTreeProof(utxoIn, BarretenbergApi, contract) { + let trees = { + utxo_tree: new MerkleTree(treeConfig.utxoDepth, BarretenbergApi), + tx_tree: new MerkleTree(treeConfig.txDepth, BarretenbergApi), + historic_tree: new MerkleTree(treeConfig.stateDepth, BarretenbergApi), + } + let commitment = BarretenbergApi.pedersenPlookupCompress( + [utxoIn.owner, utxoIn.amount, utxoIn.assetType] + ).toString() + let old_root = await contract.getRootFromUtxo(commitment) + let utxo_list = await contract.getUtxoFromRoot(old_root) + let historic_roots = await contract.getValidRoots() + let oracle = BarretenbergApi.pedersenPlookupCompress([new Fr(0n)]) + + for (let i = 0; i < utxo_list.length; i++) { + trees.utxo_tree.insert(utxo_list[i]); + } + + + let utxo_root = trees.utxo_tree.root() + trees.tx_tree.insert(utxo_root); + let tx_root_Fr = Fr.fromString(trees.tx_tree.root()) + let batch = BarretenbergApi.pedersenPlookupCompress([tx_root_Fr, oracle]) + let new_root = BarretenbergApi.pedersenPlookupCompress([batch, Fr.fromString(old_root)]).toString() + + for (let i = 0; i < historic_roots.length; i++) { + trees.historic_tree.insert(historic_roots[i]); + } + let proofs = { + utxo: { + leaf: commitment, + index: trees.utxo_tree.getIndex(commitment), + root: utxo_root, + hash_path: trees.utxo_tree.proof( + trees.utxo_tree.getIndex(commitment) + ).pathElements + }, + tx: { + leaf: utxo_root, + index: trees.tx_tree.getIndex(utxo_root), + root: trees.tx_tree.root(), + hash_path: trees.tx_tree.proof( + trees.tx_tree.getIndex(utxo_root) + ).pathElements + }, + historic: { + leaf: new_root, + index: trees.historic_tree.getIndex(new_root), + root: trees.historic_tree.root(), + hash_path: trees.historic_tree.proof( + trees.historic_tree.getIndex(new_root) + ).pathElements + } + } + return proofs +} +export function generateDataToml(oldRoot, newRoot, trees, api) { + let zeroHash = api.pedersenPlookupCompress([Fr.fromString(toFixedHex(0, true))]) + const data = { + tx_in: new Array(16).fill(ZERO_VALUE), + secrets: new Array(16).fill('0'), + utxo_in: new Array(48).fill('0'), + utxo_out: new Array(48).fill('0'), + oracle: zeroHash.toString(), + old_root_proof: new Array(16).fill('0'), + old_root: ZERO_VALUE, + new_root: ZERO_VALUE, + current_root: trees.historic_tree.root(), + indexes: new Array(48).fill('0'), + hash_path: new Array(16 * treeSum).fill('0'), + commitment_out: new Array(16).fill(ZERO_VALUE), + amount_public_in: "0", + amount_public_out: "0", + nullifier_hashes: new Array(16).fill('0'), + recipient: Fr.fromString(toFixedHex(0, true)).toString() + } + if (oldRoot !== "0") data.old_root = oldRoot; + if (newRoot !== "0") data.new_root = newRoot; + return data +} +export async function generateTestTransaction(utxoIn, utxoOut, trees, treeProof, amountPublic, data, recipient, BarretenbergApi, contract) { + let utxoInLen = utxoIn.length + let utxoOutLen = utxoOut.length + + for (let i = 0; i < utxoInLen; i++) { + let ownerHex = utxoIn[i].owner.toString(); + let amountHex = utxoIn[i].amount.toString(); + let assetTypeHex = utxoIn[i].assetType.toString(); + let note_commitment = BarretenbergApi.pedersenPlookupCompress([utxoIn[i].owner, utxoIn[i].amount, utxoIn[i].assetType]); + let utxoLeaf = note_commitment.toString() + data.secrets[i] = utxoIn[i].secret + data.nullifier_hashes[i] = BarretenbergApi.pedersenPlookupCompressFields(utxoIn[i].secret, utxoIn[i].secret) + data.old_root_proof[i] = await contract.getRootFromUtxo(utxoLeaf) + data.utxo_in[i * 3 + 0] = ownerHex + data.utxo_in[i * 3 + 1] = amountHex + data.utxo_in[i * 3 + 2] = assetTypeHex + data.indexes[i * 4 + 0] = treeProof[i].utxo.index + data.indexes[i * 4 + 1] = treeProof[i].tx.index + data.indexes[i * 4 + 2] = treeProof[i].historic.index + let utxoPath = treeProof[i].utxo.hash_path + let txPath = treeProof[i].tx.hash_path + let historicPath = treeProof[i].historic.hash_path + for (let j = 0; j < utxoPath.length; j++) { + data.hash_path[i * treeSum + 0 + j] = utxoPath[j] + data.hash_path[i * treeSum + treeConfig.utxoDepth + j] = txPath[j] + } + for (let k = 0; k < historicPath.length; k++) { + data.hash_path[i * treeSum + treeConfig.utxoDepth + treeConfig.txDepth + k] = historicPath[k] + } + } + + for (let i = 0; i < utxoOutLen; i++) { + let ownerHex = utxoOut[i].owner.toString(); + let amountHex = utxoOut[i].amount.toString(); + let assetTypeHex = utxoOut[i].assetType.toString(); + let note_commitment = BarretenbergApi.pedersenPlookupCompress([utxoOut[i].owner, utxoOut[i].amount, utxoOut[i].assetType]); + let utxoLeaf = note_commitment.toString() + trees.utxo_tree.insert(utxoLeaf) + data.utxo_out[i * 3 + 0] = ownerHex + data.utxo_out[i * 3 + 1] = amountHex + data.utxo_out[i * 3 + 2] = assetTypeHex + data.commitment_out[i] = utxoLeaf + } + data.tx_in[0] = trees.utxo_tree.root() + data.amount_public_in = toFixedHex(Number(amountPublic.amountIn.toString()), true) + data.amount_public_out = toFixedHex(Number(amountPublic.amountOut.toString()), true) + data.recipient = recipient + data.current_root = trees.historic_tree.root() + dumpToml(data) +} +export function generateTestPublish(trees, data, api) { + + let utxoTree = trees.utxo_tree + let txTree = trees.tx_tree + let historicTree = trees.historic_tree + let utxoRoot = utxoTree.root() + txTree.insert(utxoRoot) + let txRoot = txTree.root() + let txRootFr = Fr.fromBufferReduce(Buffer.from(txRoot.slice(2), 'hex')) + let oracleFr = Fr.fromBufferReduce(toFixedHex(0, true)) + let oracleHash = api.pedersenPlookupCompress([oracleFr]) + let batch = api.pedersenPlookupCompressFields(txRootFr, oracleHash) + let oldHistoricRoot = Fr.fromBufferReduce(Buffer.from(data.new_root.slice(2), 'hex')) + let newHistoricRoot = api.pedersenPlookupCompress([batch, oldHistoricRoot]) + let newHistoricRootHex = newHistoricRoot.toString() + historicTree.insert(newHistoricRootHex) + data.old_root = oldHistoricRoot.toString() + data.new_root = newHistoricRootHex + + + trees.utxoTreeOld = trees.utxo_tree + trees.txTreeOld = trees.tx_tree + trees.newHistoricRoot = newHistoricRootHex + trees.utxo_tree = new MerkleTree(4, api) + trees.tx_tree = new MerkleTree(4, api) + dumpToml(data) +} \ No newline at end of file diff --git a/script/zk.js b/script/zk.js new file mode 100644 index 0000000..9021214 --- /dev/null +++ b/script/zk.js @@ -0,0 +1,375 @@ +import { exec, execSync } from "child_process"; +import { newBarretenbergApiSync } from "@aztec/bb.js/dest/node/index.js"; +import { Fr } from "@aztec/bb.js/dest/node/types/index.js"; +import inquirer from "inquirer"; + +import { + generateUTXO, + generateDataToml, + generateTestTransaction, + generateTreeProof, + generateTestPublish, + randomBytesFr, + readToml, + getSolidityHash, + treeConfig, + getInputs, + getInputsOrdered, + getPublicInputsOrdered, +} from "./utils/helpers.js"; +import { MerkleTree } from "./utils/MerkleTree.mjs"; +import fs from "fs"; +import { ethers, JsonRpcProvider, ContractFactory } from "ethers"; +import PrivateTransferData from "../deps/TechnicalPreview.json" assert { type: "json" }; +import { Sequelize, DataTypes, Model } from "sequelize"; +import "dotenv/config"; + +class UTXOdb extends Model { + secret; + amount; + assetType; + spent; +} + +let sequelize = new Sequelize({ + dialect: "sqlite", + storage: "txs.db", + logging: false, +}); + +UTXOdb.init( + { + secret: { + type: DataTypes.STRING, + allowNull: false, + }, + amount: { + type: DataTypes.STRING, + allowNull: false, + }, + assetType: { + type: DataTypes.INTEGER, + }, + spent: { + type: DataTypes.BOOLEAN, + }, + id: { + type: DataTypes.STRING, + allowNull: false, + primaryKey: true, + }, + }, + { + sequelize: sequelize, + tableName: "UTXOs", + } +); + + +await sequelize.sync(); + + +const fetchUTXOs = async () => + await UTXOdb.findAll({ where: { spent: false } }); +const fetchSpentUTXOs = async () => + await UTXOdb.findAll({ where: { spent: true } }); +const spendUTXO = async (UTXO) => + await UTXOdb.update({ spent: true }, { where: { id: UTXO } }); +const saveUTXO = async (UTXO) => { + await UTXOdb.upsert({ + secret: UTXO.secret.toString(), + amount: UTXO.amount.toString(), + assetType: 0, + id: UTXO.id.toString(), + spent: false, + }); +}; + +const provider = new JsonRpcProvider(process.env.PROVIDER); + +const privateKey = process.env.PRIVATE_KEY; +const wallet = new ethers.Wallet(privateKey, provider); + +const technicalPreviewContract = new ethers.Contract( + "0x8162d56A21D0ee799Eef055C3acC2b4F776f693a", + PrivateTransferData.abi, + wallet +); + +export async function balance() { + const api = await newBarretenbergApiSync(); + let utxos = await fetchUTXOs(); + + + let nullifierArray = []; + for (let i = 0; i < utxos.length; i++) { + nullifierArray.push(api.pedersenPlookupCompress([ + Fr.fromString(utxos[i].dataValues.secret), + Fr.fromString(utxos[i].dataValues.secret) + ]).toString()); + } + let spentArray = await technicalPreviewContract.getSpentNullifiers(nullifierArray); + + for (let i = 0; i < spentArray.length; i++) { + if (spentArray[i] === true) { + await spendUTXO(utxos[i].dataValues.id); + utxos.splice(i, 1); + } + } + + if (utxos.length === 0) return ethers.formatEther(BigInt(0)); + else { + let utxosBigInt = utxos.map((res) => BigInt(res.dataValues.amount)); + return ethers.formatEther(utxosBigInt.reduce((a, b) => a + b)); + } +} +export const deposit = async (args) => { + console.log(`** Compiling Transaction Circuit **`); + + execSync(`nargo check`); + execSync(`nargo compile`); + let recipient, recipient_new; + const api = await newBarretenbergApiSync(); + + let trees = { + utxo_tree: new MerkleTree(4, api), + tx_tree: new MerkleTree(4, api), + historic_tree: new MerkleTree(9, api), + utxoTreeOld: new MerkleTree(4, api), + txTreeOld: new MerkleTree(4, api), + newHistoricRoot: "", + }; + let utxoIn = []; + let utxoOut = []; + let treeProof = []; + let validRoots = await technicalPreviewContract.getValidRoots(); + + + let oldRoot, newRoot; + if (validRoots.length === 1) { + oldRoot = validRoots[0]; + newRoot = validRoots[0]; + } else { + oldRoot = validRoots[validRoots.length - 2]; + newRoot = validRoots[validRoots.length - 1]; + } + let data = generateDataToml(oldRoot, newRoot, trees, api); + let amountPublic = { + amountIn: BigInt(0), + amountOut: BigInt(0), + }; + + recipient = `0x` + `dEaD`.padStart(64, "0"); + + console.log("** Populating UTXO tree.. **"); + + console.log("** Generating a batch of zkXFT UTXOs... **"); + + let batchSize0 = 1; + let secret0 = []; + for (let s = 0; s < batchSize0; s++) { + secret0.push(randomBytesFr(32)); + } + let amountsOutUTXO = new Array(batchSize0).fill(BigInt(0)); + amountsOutUTXO[0] = BigInt(args.amount); + utxoOut = generateUTXO(batchSize0, amountsOutUTXO, secret0, api); + + amountPublic.amountIn = BigInt(args.amount); + + generateTestTransaction( + utxoIn, + utxoOut, + trees, + treeProof, + amountPublic, + data, + recipient, + api, + technicalPreviewContract + ); + console.log("** Creating Deposit and Publisher Proof... **"); + generateTestPublish(trees, data, api); + + execSync(`nargo prove`); + + let proof = fs.readFileSync("./proofs/main.proof").toString(); + proof = `0x` + proof; + + let public_inputs = readToml("./Verifier.toml"); + let tx = await technicalPreviewContract.publish(proof, public_inputs, { + value: 0, + }); + utxoIn = utxoOut; + + let txReceipt = await provider.getTransactionReceipt(tx.hash); + while (txReceipt === null) { + txReceipt = await provider.getTransactionReceipt(tx.hash); + } + + let utxoId = api.pedersenPlookupCompress([ + utxoIn[0].owner, + utxoIn[0].amount, + utxoIn[0].assetType, + ]); + + utxoOut[0].id = utxoId; + + await saveUTXO(utxoOut[0]); + return [trees, data]; +}; + +export const transact = async (args) => { + const api = await newBarretenbergApiSync(); + + let amount = BigInt(args.amount); + let trees = { + utxo_tree: new MerkleTree(4, api), + tx_tree: new MerkleTree(4, api), + historic_tree: new MerkleTree(9, api), + utxoTreeOld: new MerkleTree(4, api), + txTreeOld: new MerkleTree(4, api), + newHistoricRoot: "", + }; + + console.log("** Populating Historic tree.. **"); + + let validRoots = await technicalPreviewContract.getValidRoots(); + for (let i = 0; i < validRoots.length; i++) { + trees.historic_tree.insert(validRoots[i]); + } + + let spending = []; + let spendingSum = BigInt(0); + + let utxo_leaves = (await fetchUTXOs()).map((utxo) => { + let utxo_out = { + secret: Fr.fromString(utxo.dataValues.secret), + owner: api.pedersenPlookupCompress([ + Fr.fromString(utxo.dataValues.secret), + ]), + amount: Fr.fromString(utxo.dataValues.amount), + assetType: Fr.fromBufferReduce(Buffer.from(getSolidityHash(0), "hex")), + spent: utxo.dataValues.spent, + id: utxo.dataValues.id, + }; + return utxo_out; + }); + + + let utxoOut = []; + let changeUTXO = { + secret: Fr.fromString("0x00"), + owner: api.pedersenPlookupCompress([Fr.fromString("0x00")]), + amount: Fr.fromString("0x00"), + assetType: Fr.fromBufferReduce(Buffer.from(getSolidityHash(0), "hex")), + }; + for (let i = 0; i < utxo_leaves.length; i++) { + if (spendingSum < amount) { + spending.push(utxo_leaves[i]); + spendingSum += BigInt(utxo_leaves[i].amount.toString()); + } + + if (spendingSum > amount) { + let change = spendingSum - amount; + let secret = randomBytesFr(32); + changeUTXO = { + secret: secret, + owner: api.pedersenPlookupCompress([secret]), + amount: new Fr(change), + assetType: Fr.fromBufferReduce(Buffer.from(getSolidityHash(0), "hex")), + }; + changeUTXO.id = api.pedersenPlookupCompress([ + changeUTXO.owner, + changeUTXO.amount, + changeUTXO.assetType, + ]); + utxoOut.push(changeUTXO); + break; + } + } + + + let treeProofs = []; + for (let i = 0; i < spending.length; i++) { + let proofForUtxo = await generateTreeProof( + spending[i], + api, + technicalPreviewContract + ); + treeProofs.push(proofForUtxo); + } + + let batchSize1 = 1; + + + + let oldRoot, newRoot; + if (validRoots.length === 1) { + oldRoot = validRoots[0]; + newRoot = validRoots[0]; + } else { + oldRoot = validRoots[validRoots.length - 2]; + newRoot = validRoots[validRoots.length - 1]; + } + + let data = generateDataToml(oldRoot, newRoot, trees, api); + + let amountPublic = { + amountIn: BigInt(0), + amountOut: BigInt(args.amount), + }; + + + let recipientPrompt = await inquirer.prompt({ + type: "input", + name: "recipient", + message: "Enter the destination address: ", + }); + + let recipient_new = + `0x` + `${recipientPrompt.recipient.replace("0x", "")}`.padStart(64, "0"); + + await generateTestTransaction( + spending, + utxoOut, + trees, + treeProofs, + amountPublic, + data, + recipient_new, + api, + technicalPreviewContract + ); + + generateTestPublish(trees, data, api); + + console.log(`** Compiling Transaction Circuit **`); + + execSync(`nargo check`); + execSync(`nargo compile`); + console.log("** Generating transaction Parameters... **"); + + execSync(`nargo prove`); + + let proof = fs.readFileSync("./proofs/main.proof").toString(); + proof = `0x` + proof; + + let public_inputs = readToml("./Verifier.toml"); + + let tx = await technicalPreviewContract.publish(proof, public_inputs, { + value: 0, + }); + + + let txReceipt = await provider.getTransactionReceipt(tx.hash); + while (txReceipt === null) { + console.log("Waiting for tx confirmation...") + txReceipt = await provider.getTransactionReceipt(tx.hash); + } + + for (let i = 0; i < spending.length; i++) { + spendUTXO(spending[i].id); + } + + await saveUTXO(changeUTXO); +}; diff --git a/src/main.nr b/src/main.nr new file mode 100644 index 0000000..81af6cc --- /dev/null +++ b/src/main.nr @@ -0,0 +1,99 @@ +use dep::std; +global state_depth = 9; +global utxo_depth = 4; +global batch_depth = 4; +fn main( + tx_in: pub [Field; 16], + current_root: pub Field, + amount_public_in: pub Field, + amount_public_out: pub Field, + commitment_out: pub [Field; 16], + recipient: pub Field, + oracle: pub Field, + old_root_proof: [Field; 16], + old_root: pub Field, + new_root: pub Field, + nullifier_hashes: pub [Field; 16], + secrets: [Field; 16], + utxo_in: [Field; 48], + utxo_out: [Field; 48], + indexes: [Field; 48], + hash_path: [Field; 272], +) { + let trees: Field = 4; + let mut sum_in: Field = amount_public_in; + let mut sum_out: Field = amount_public_out; + for i in 0..16 { + if (utxo_in[i*3 + 1] != 0) { + let owner = std::hash::pedersen([secrets[i]]); + assert(owner[0] == utxo_in[i*3 + 0]); + assert(nullifier_hashes[i] == std::hash::pedersen([secrets[i], secrets[i]])[0]); + let commitment_in = std::hash::pedersen([utxo_in[i*3 + 0], utxo_in[i*3 + 1], utxo_in[i*3 + 2]])[0]; + let mut hash_path_utxo: [Field; utxo_depth] = [0; utxo_depth]; + let mut hash_path_tx: [Field; batch_depth] = [0; batch_depth]; + let mut hash_path_historic: [Field; state_depth] = [0; state_depth]; + for j in 0..4 { + hash_path_utxo[j] = hash_path[(state_depth + utxo_depth + batch_depth) * i + j]; + hash_path_tx[j] = hash_path[(state_depth + utxo_depth + batch_depth) * i + utxo_depth + j]; + } + for l in 0..state_depth { + hash_path_historic[l] = hash_path[(state_depth + utxo_depth + batch_depth) * i + utxo_depth + batch_depth + l]; + } + let index_utxo = indexes[trees * i + 0]; + let index_tx = indexes[trees * i + 1]; + let index_historic = indexes[trees * i + 2]; + let utxo_root = std::merkle::compute_merkle_root( + commitment_in, + index_utxo, + hash_path_utxo + ); + let tx_root = std::merkle::compute_merkle_root( + utxo_root, + index_tx, + hash_path_tx + ); + let leaf_batch = std::hash::pedersen([tx_root, oracle])[0]; + let leaf_historic = std::hash::pedersen([leaf_batch, old_root_proof[i]])[0]; + let historic_root = std::merkle::compute_merkle_root( + leaf_historic, + index_historic, + hash_path_historic + ); + assert(historic_root == current_root); + sum_in += utxo_in[i*3 + 1]; + } + } + for k in 0..16 { + if (utxo_out[k*3 + 1] != 0) { + let commitment_out_calc = std::hash::pedersen([utxo_out[k*3 + 0], utxo_out[k*3 + 1], utxo_out[k*3 + 2]]); + assert(commitment_out_calc[0] == commitment_out[k]); + sum_out += utxo_out[k*3 + 1]; + } + else { + let zero_hash = 0xf35fcb490b7ea67c3ac26ed530fa5d8dfe8be344e7177ebb63fe02723fb6f725 as Field; + assert(commitment_out[k] == zero_hash); + } + } + let utxo_root_calc: Field = pedersen_tree_four(commitment_out); + assert(tx_in[0] == utxo_root_calc); + let tx_root_calc: Field = pedersen_tree_four(tx_in); + assert(oracle == std::hash::pedersen([0])[0]); + let batch_root_calc: Field = std::hash::pedersen([tx_root_calc, oracle])[0]; + let new_root_calc: Field = std::hash::pedersen([batch_root_calc, old_root])[0]; + assert(new_root == new_root_calc); + assert(sum_in == sum_out); + assert(recipient == recipient); +} +fn pedersen_tree_four(leaves: [Field; 16]) -> Field { + let mut tx_tree: [Field; 16] = leaves; + for l in 0..8 { + tx_tree[l] = std::hash::pedersen([tx_tree[2*l], tx_tree[2*l + 1]])[0]; + } + for l in 0..4 { + tx_tree[l] = std::hash::pedersen([tx_tree[2*l], tx_tree[2*l + 1]])[0]; + } + for l in 0..2 { + tx_tree[l] = std::hash::pedersen([tx_tree[2*l], tx_tree[2*l + 1]])[0]; + } + std::hash::pedersen([tx_tree[0], tx_tree[1]])[0] +} \ No newline at end of file -- GitLab