ZKSAFE Password 技术对接
准备工作
Node.js 建议 v16,安装 snarkjs,你可以不会snarkjs,照着代码写也行
npm install -g snarkjs
安装 ethers,你必须会ethers,所有代码示例都假设你会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.wasm
、circuit_final.zkey
、verification_key.json
是固定值,可以在ZK源码找到
getProof()
即图中的ZK Circuit
getProof()
有4个参数,分别是:
nonce:从ZKPass合约获取的你的nonce值,string类型
datahash:你想要对什么数据进行签名,这个数据的hash值,string类型
返回所有ZK算法有关的数据:
proof:ZK-SNARK的proof,由8个uint256组成的数组
pwdhash:ZKPass合约需要用到的pwdhash,uint256类型
address:参数里的address,string类型
expiration:签名过期时间,默认10分钟,int类型
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即可,或者把新密码的proof2
当proof1
传也行(示例就是这么干的)
成功后,调用者的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个参数,分别是
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
必须一致