🤖技术对接

ZKSAFE Password 技术对接

准备工作

Node.js 建议 v16,安装 snarkjs,你可以不会snarkjs,照着代码写也行

npm install -g snarkjs

安装 ethers,你必须会ethers,所有代码示例都假设你会ethers

npm install ethers

合约源码

测试代码

注意:测试环境是hardhat,ethers的用法跟正式环境略有不同,以下代码都基于测试环境

不建议用户在ZKSAFE以外的地方输入密码,防止密码泄漏。所以ZKPass (ZKSAFE Password简称ZKPass) 的合约面向的是合作方合约,比如ZKSAFE

resetPassword() 设置密码

初始化密码和改密码都是这个接口,先说所有跟ZK相关的接口都要用到的工具方法getProof()

工具方法

//util
async function getProof(pwd, address, nonce, datahash) {
    let expiration = parseInt(Date.now() / 1000 + 600)
    let chainId = (await provider.getNetwork()).chainId
    let fullhash = utils.solidityKeccak256(['uint256','uint256','uint256','uint256'], [expiration, chainId, nonce, datahash])
    fullhash = s(b(fullhash).div(8)) //fullhash必须是254位, solidityKeccak256是256位,所以要转换

    let input = [stringToHex(pwd), address, fullhash]
    let data = await snarkjs.groth16.fullProve({in:input}, "./zk/v1/circuit_js/circuit.wasm", "./zk/v1/circuit_final.zkey")

    const vKey = JSON.parse(fs.readFileSync("./zk/v1/verification_key.json"))
    const res = await snarkjs.groth16.verify(vKey, data.publicSignals, data.proof)

    if (res === true) {
        console.log("Verification OK")

        let pwdhash = data.publicSignals[0]
        let fullhash = data.publicSignals[1]
        let allhash = data.publicSignals[2]

        let proof = [
            BigNumber.from(data.proof.pi_a[0]).toHexString(),
            BigNumber.from(data.proof.pi_a[1]).toHexString(),
            BigNumber.from(data.proof.pi_b[0][1]).toHexString(),
            BigNumber.from(data.proof.pi_b[0][0]).toHexString(),
            BigNumber.from(data.proof.pi_b[1][1]).toHexString(),
            BigNumber.from(data.proof.pi_b[1][0]).toHexString(),
            BigNumber.from(data.proof.pi_c[0]).toHexString(),
            BigNumber.from(data.proof.pi_c[1]).toHexString()
        ]

        return {proof, pwdhash, address, expiration, chainId, nonce, datahash, fullhash, allhash}

    } else {
        console.log("Invalid proof")
    }
}

为方便起见,我们写了一个工具方法getProof(),封装了所有用到的ZK算法,处理了ZK里面256位转254位的坑,需要注意的是circuit.wasmcircuit_final.zkeyverification_key.json是固定值,可以在ZK源码找到

getProof()即图中的ZK Circuit

getProof()有4个参数,分别是:

  • pwd:你的密码,string类型

  • address:你的钱包地址,string类型

  • nonce:从ZKPass合约获取的你的nonce值,string类型

  • datahash:你想要对什么数据进行签名,这个数据的hash值,string类型

返回所有ZK算法有关的数据:

  • proof:ZK-SNARK的proof,由8个uint256组成的数组

  • pwdhash:ZKPass合约需要用到的pwdhash,uint256类型

  • address:参数里的address,string类型

  • expiration:签名过期时间,默认10分钟,int类型

  • chainId:公链id,int类型

  • nonce:参数里的nonce,string类型

  • datahash:参数里的datahash,string类型

  • fullhash:这个不需要传入合约,254位,string类型

  • allhash:以上所有参数的hash,uint256类型

初始化密码

let pwd = 'abc123' //你的密码
let nonce = '1' //初始化密码,nonce就是1
let datahash = '0' //对于resetPassword接口,datahash固定是0
let p = await getProof(pwd, accounts[0].address, nonce, datahash)

//需要付一点手续费 :)
fee = await zkPass.fee()
console.log('zkPass fee(Ether)', utils.formatEther(fee))

let gasLimit = await zkPass.estimateGas.resetPassword(p.proof, 0, 0, p.proof, p.pwdhash, p.expiration, p.allhash, {value: fee})
await zkPass.resetPassword(p.proof, 0, 0, p.proof, p.pwdhash, p.expiration, p.allhash, {value: fee, gasLimit})
console.log('initPassword done')

resetPassword()有7个参数,分别是:

  • proof1:旧密码生成proof,由8个uint256组成的数组

  • expiration1:旧密码的过期时间,uint256类型

  • allhash1:旧密码生成allhash,uint256类型

  • proof2:新密码生成proof,由8个uint256组成的数组

  • pwdhash2:新密码的pwdhash,由ZK生成,uint256类型

  • expiration2:新密码的过期时间,uint256类型

  • allhash2:新密码生成allhash,uint256类型

因为初始化密码没有旧密码,所以前3个旧密码相关的参数在合约里是用不到的,但是必须得传,全部传0即可,或者把新密码的proof2proof1传也行(示例就是这么干的)

成功后,调用者的address(msg.sender)的密码就是pwd

修改密码

let oldpwd = 'abc123' //旧密码
let nonce = await zkPass.nonceOf(accounts[0].address) //当前的nonce
let datahash = '0' //对于resetPassword接口,datahash固定是0
let oldZkp = await getProof(oldpwd, accounts[0].address, s(nonce), datahash) //旧密码的proof

let newpwd = '123123' //新密码
let newZkp = await getProof(newpwd, accounts[0].address, s(nonce.add(1)/**新密码的nonce+1*/), datahash) //新密码的proof

fee = await zkPass.fee()
console.log('zkPass fee(Ether)', utils.formatEther(fee))

//need fee
await zkPass.resetPassword(oldZkp.proof, oldZkp.expiration, oldZkp.allhash, newZkp.proof, newZkp.pwdhash, newZkp.expiration, newZkp.allhash, {value: fee})
console.log('resetPassword done')

还是resetPassword()接口,修改密码需要用旧密码,所以要用旧密码生成前3个参数

成功后,调用者的address(msg.sender)的密码就是newpwd,旧密码oldpwd作废

verify() 校验密码

密码可以在链下校验,获取pwdhash在链下就可以校验;也可以上链校验,通常是配合合作方合约一起,由合作方合约调用ZKPass.verify(),密码错误就报错,不报错的话就是密码正确,且签名有效,合作方合约可以继续处理后续

不建议用户在ZKSAFE以外的地方输入密码,防止密码泄漏,所以链下校验只在ZKPass就行,合作方可以用链上校验的方式对接ZKPass

verify()有5个参数,分别是

  • user:哪个用户的签名,address类型

  • proof:密码在ZK生成的proof,由8个uint256组成的数组

  • datahash:用户对什么数据进行的签名,这个就是数据的hash,uint256类型

  • expiration:签名的过期时间,uint256类型

  • allhash:签名在ZK生成allhash,uint256类型

合约内会用user的pwdhash进行密码的校验,以及把datahash转成254位的fullhash。。。总之,getProof()工具会处理所有ZK校验相关的参数

ZKSAFE作为合作方的合约调用ZKPass

function withdrawERC20(
    uint[8] memory proof, //转给ZKPass的参数
    address tokenAddr, //提什么token
    uint amount, //提多少
    uint expiration, //转给ZKPass的参数
    uint allhash //转给ZKPass的参数
) external onlyOwner {
    uint datahash = uint(keccak256(abi.encodePacked(tokenAddr, amount))); //计算datahash
    eps.verify(owner(), proof, datahash, expiration, allhash); //密码和签名的校验

    IERC20(tokenAddr).safeTransfer(owner(), amount); //校验通过,干活!

    emit WithdrawERC20(tokenAddr, amount);
}

在这个示例中,用户想要把token从ZKSAFE提出来,所以需要对提什么token(tokenAddr)、提多少(amount)用密码进行签名

ZKSAFE的链下代码

let pwd = 'abc123' //用户的密码
let nonce = s(await eps.nonceOf(accounts[0].address)) //用户当前的nonce
let tokenAddr = usdt.address //提什么token
let amount = s(m(40, 18)) //提多少
let datahash = utils.solidityKeccak256(['address', 'uint256'], [tokenAddr, amount]) //计算datahash
datahash = s(b(datahash)) //转成string类型数字
let p = await getProof(pwd, accounts[0].address, nonce, datahash) //计算ZK Proof

await safebox.withdrawERC20(p.proof, tokenAddr, amount, p.expiration, p.allhash) //调用合约,提款
console.log('withdrawERC20 done')

await print()

datahash是合作方定义的,uint256类型,通常是hash值。也有例外的,比方说签名的是address,即uint160类型,直接放datahash也能装得下,可以不用hash

合作方链下计算的datahash,和合作方合约计算的datahash必须一致

最后更新于