# 技术对接

## ZKSAFE Password 技术对接

### 准备工作

Node.js 建议 v16，安装 [snarkjs](https://github.com/iden3/snarkjs)，你可以不会snarkjs，照着代码写也行

```javascript
npm install -g snarkjs
```

安装 [ethers](https://docs.ethers.io/v5/getting-started/)，你必须会ethers，所有代码示例都假设你会ethers

```javascript
npm install ethers
```

[合约源码](https://github.com/ZKSAFE/all-contracts/tree/main/contracts/zkpass)

[测试代码](https://github.com/ZKSAFE/all-contracts/blob/main/test/ZKPass-test.js)

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

不建议用户在ZKSAFE以外的地方输入密码，防止密码泄漏。所以ZKPass *（ZKSAFE Password简称ZKPass）* 的合约面向的是合作方合约，比如ZKSAFE<br>

### resetPassword() 设置密码

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

#### 工具方法

```javascript
//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源码](https://github.com/ZKSAFE/all-contracts/tree/main/zk)找到

`getProof()`即图中的ZK Circuit<br>

![](/files/tRODDYY6fTeNPcwIHoNo)

<br>

`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类型<br>

#### 初始化密码

```javascript
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`<br>

#### 修改密码

```javascript
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`作废<br>

### 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

```javascript
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的链下代码

```javascript
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`必须一致


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.zksafe.pro/zh/zkpass/build.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
