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 worker test setup #98

Merged
merged 9 commits into from
Dec 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
"version": "0.1.0",
"private": true,
"devDependencies": {
"chai": "^4.3.4",
"prettier": "^2.7.1",
"ts-loader": "^9.2.4",
"tsc-alias": "^1.7.0",
Expand All @@ -13,10 +12,11 @@
"workspaces": [
"zp-memo-parser",
"zp-relayer",
"test-e2e"
"test-e2e",
"test-flow-generator"
],
"scripts": {
"initialize": "yarn install --unsafe-perm --frozen-lockfile",
"initialize": "yarn install --frozen-lockfile && yarn build:memo",
"build:relayer": "yarn workspace zp-relayer run build",
"build:memo": "yarn workspace zp-memo-parser run build",
"prettier": "npx prettier --write ."
Expand Down
1 change: 1 addition & 0 deletions test-flow-generator/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
flows
7 changes: 7 additions & 0 deletions test-flow-generator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Test flow generation tool

This tool allows you to describe a test flow via json:
1. Install dependencies. Run `yarn`
2. Describe a `Flow` object (structure can be found in `src/types.ts`) in a json and place it into `test-flows` folder
3. Run `./scripts/generate.sh`
4. Find your flow in `flows` directory
13 changes: 13 additions & 0 deletions test-flow-generator/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE >
<html>
<head>
<title>Test</title>
<meta charset="utf-8" />
</head>

<body>
<div id="mocha"></div>

<script defer src="http://localhost:8080/dist/main.js"></script>
</body>
</html>
21 changes: 21 additions & 0 deletions test-flow-generator/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "test-flow-generator",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"build:dev": "webpack --mode=development"
},
"dependencies": {
"@metamask/eth-sig-util": "^4.0.1",
"http-server": "14.1.1",
"libzkbob-rs-node": "0.1.27",
"libzkbob-rs-wasm-web": "^0.7.0",
"node-polyfill-webpack-plugin": "^1.1.4",
"puppeteer": "^19.2.0",
"web3-utils": "1.8.0",
"webpack": "^5.46.0",
"webpack-cli": "^4.10.0",
"zp-memo-parser": "link:../zp-memo-parser"
}
}
2 changes: 2 additions & 0 deletions test-flow-generator/scripts/generate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
yarn build:dev
node src/index.js
60 changes: 60 additions & 0 deletions test-flow-generator/src/EIP712.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { signTypedData, SignTypedDataVersion } from '@metamask/eth-sig-util'
import { config } from './config'

interface EIP712Domain {
name: string
version: string
chainId: number
verifyingContract: string
}

const domain: EIP712Domain = {
name: 'BOB',
version: '1',
chainId: config.chainId,
verifyingContract: config.tokenAddress,
}

const PERMIT: 'Permit' = 'Permit'

const types = {
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
],
[PERMIT]: [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
{ name: 'salt', type: 'bytes32' },
],
}

interface SaltedPermitMessage {
owner: string
spender: string
value: string
nonce: string
deadline: string
salt: string
}

export function createSignature(message: SaltedPermitMessage, privateKey: string) {
const data = {
types,
primaryType: PERMIT,
domain,
message: message as Record<string, any>,
}
const signature = signTypedData({
data,
version: SignTypedDataVersion.V4,
privateKey: Buffer.from(privateKey.slice(2), 'hex'),
})

return signature
}
5 changes: 5 additions & 0 deletions test-flow-generator/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const config = {
tokenAddress: '0xe78A0F7E598Cc8b0Bb87894B0F60dD2a88d6a8Ab',
poolAddress: '0xe982E462b094850F12AF94d21D470e21bE9D0E9C',
chainId: 31337,
}
117 changes: 117 additions & 0 deletions test-flow-generator/src/generate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import init, { TransactionData, UserAccount, UserState } from 'libzkbob-rs-wasm-web'
import { toChecksumAddress } from 'web3-utils'
import type { BaseOutputItem, Flow, FlowOutput } from './types'
import { ethAddrToBuf, packSignature, toTwosComplementHex } from './helpers'
import { createSignature } from './EIP712'
import { config } from './config'
import { TxType } from 'zp-memo-parser'

const TEN_YEARS = 315569520
const denominator = 1000000000n

export async function newAccount() {
const sk = Array.from({ length: 10 }, () => Math.floor(Math.random() * 100))
const stateId = sk.toString()
const state = await UserState.init(stateId)
const zkAccount = UserAccount.fromSeed(Uint8Array.from(sk), state)
return zkAccount
}

async function createDepositPermittable(
acc: UserAccount,
amount: string,
from: string
): Promise<[TransactionData, string]> {
const deadline = String(Math.floor(Date.now() / 1000) + TEN_YEARS)
const tx = await acc.createDepositPermittable({
fee: '0',
amount,
deadline,
holder: ethAddrToBuf(from),
})

return [tx, deadline]
}

async function createTransfer(acc: UserAccount, amount: string, zkAddress: string) {
const tx = await acc.createTransfer({
fee: '0',
outputs: [{ amount, to: zkAddress }],
})

return tx
}

async function createWithdraw(acc: UserAccount, amount: string, to: string) {
const tx = await acc.createWithdraw({
fee: '0',
amount,
to: ethAddrToBuf(to),
native_amount: '0',
energy_amount: '0',
})

return tx
}

async function createFlow({ independent, accounts, flow }: Flow): Promise<FlowOutput> {
const flowOutput: FlowOutput = []
const nonces: Record<string, number> = {}
let acc = await newAccount()
for (let [i, item] of flow.entries()) {
let tx
let txType: TxType
let txSpecificOutput: any = { depositSignature: null }
if ('from' in item) {
txType = TxType.PERMITTABLE_DEPOSIT
const [depositTx, deadline] = await createDepositPermittable(acc, item.amount, item.from)
const nonce = nonces[item.from] || 0
const salt = '0x' + toTwosComplementHex(BigInt(depositTx.public.nullifier), 32)
const depositSignature = packSignature(
createSignature(
{
owner: toChecksumAddress(item.from),
spender: toChecksumAddress(config.poolAddress),
value: (BigInt(parseInt(item.amount)) * denominator).toString(10),
nonce: nonce.toString(),
deadline,
salt,
},
accounts[item.from]
)
)
nonces[item.from] = nonce + 1
tx = depositTx
txSpecificOutput = { depositSignature, deadline }
} else if ('to' in item) {
txType = TxType.WITHDRAWAL
tx = await createWithdraw(acc, item.amount, item.to)
} else if ('zkAddress' in item) {
txType = TxType.TRANSFER
tx = await createTransfer(acc, item.amount, item.zkAddress)
} else {
throw Error('Unknown flow item')
}

if (independent) {
acc = await newAccount()
} else {
acc.addAccount(BigInt(i) * 128n, tx.out_hashes, tx.secret.tx.output[0], [])
}

// @ts-ignore
flowOutput.push({
txType,
txTypeData: item,
transactionData: tx,
...txSpecificOutput,
})
}
return flowOutput
}

Object.assign(global, {
init,
newAccount,
createFlow,
})
66 changes: 66 additions & 0 deletions test-flow-generator/src/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
function padLeft(string: string, chars: number, sign = '0') {
const hasPrefix = /^0x/i.test(string) || typeof string === 'number'
string = string.replace(/^0x/i, '')

const padding = chars - string.length + 1 >= 0 ? chars - string.length + 1 : 0

return (hasPrefix ? '0x' : '') + new Array(padding).join(sign ? sign : '0') + string
}

export function ethAddrToBuf(address: string): Uint8Array {
return hexToBuf(address, 20)
}

export function hexToBuf(hex: string, bytesCnt: number = 0): Uint8Array {
if (hex.length % 2 !== 0) {
throw new Error('Invalid hex string')
}

if (hex.startsWith('0x')) {
hex = hex.slice(2)
}

if (bytesCnt > 0) {
const digitsNum = bytesCnt * 2
hex = hex.slice(-digitsNum).padStart(digitsNum, '0')
}

const buffer = new Uint8Array(hex.length / 2)

for (let i = 0; i < hex.length; i = i + 2) {
buffer[i / 2] = parseInt(hex.slice(i, i + 2), 16)
}

return buffer
}

export function toTwosComplementHex(num: bigint, numBytes: number): string {
let hex
if (num < 0) {
let val = BigInt(2) ** BigInt(numBytes * 8) + num
hex = val.toString(16)
} else {
hex = num.toString(16)
}

return padLeft(hex, numBytes * 2)
}

export function packSignature(signature: string): string {
signature = signature.slice(2)

if (signature.length > 128) {
let v = signature.substr(128, 2)
if (v == '1c') {
return `${signature.slice(0, 64)}${(parseInt(signature[64], 16) | 8).toString(16)}${signature.slice(65, 128)}`
} else if (v != '1b') {
throw 'invalid signature: v should be 27 or 28'
}

return signature.slice(0, 128)
} else if (signature.length < 128) {
throw 'invalid signature: it should consist at least 64 bytes (128 chars)'
}

return signature
}
69 changes: 69 additions & 0 deletions test-flow-generator/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
const fs = require('fs')
const path = require('path')
const puppeteer = require('puppeteer')
const { createServer } = require('http-server')
const { Proof, Params } = require('libzkbob-rs-node')

const OUT_FLOWS_DIR = './flows/'
const TEST_FLOWS_DIR = './test-flows/'
const PARAMS = Params.fromFile(process.env.PARAMS_PATH || '../zp-relayer/params/transfer_params.bin')

if (!fs.existsSync(OUT_FLOWS_DIR)) {
fs.mkdirSync(OUT_FLOWS_DIR, { recursive: true })
}

function proveTx(pub, sec) {
return Proof.tx(PARAMS, pub, sec)
}

async function generateFlow() {
const browser = await puppeteer.launch()
try {
const page = await browser.newPage()
page.on('console', async msg => {
const msgArgs = msg.args()
for (let i = 0; i < msgArgs.length; ++i) {
console.log(msgArgs[i].toString())
}
})
const flowFiles = fs.readdirSync(TEST_FLOWS_DIR)
console.log(flowFiles)
for (let file of flowFiles) {
const fullPath = path.join(TEST_FLOWS_DIR, file)
const flow = require('../' + fullPath)
await page.goto('http://127.0.0.1:8080/')
const res = await page.evaluate(async flow => {
await init()
const flowOutput = await createFlow(flow)
return flowOutput
}, flow)

for (let tx of res) {
const proof = proveTx(tx.transactionData.public, tx.transactionData.secret)
tx.proof = proof
}

fs.writeFileSync(`${OUT_FLOWS_DIR}/flow_${path.parse(file).name}.json`, JSON.stringify(res, null))
}
} finally {
await browser.close()
}
}

async function main() {
const server = createServer({
root: '.',
cors: true,
})
server.listen(8080, async () => {
try {
await generateFlow()
} catch (err) {
console.log(err.toString())
} finally {
server.close()
}
})
}

main()
Loading