本地搭建和测试zkLogin零知识证明服务 @SUI Move开发必知必会

  • rzexin
  • 更新于 2024-06-14 22:05
  • 阅读 333

本地搭建和测试zkLogin零知识证明服务

本地搭建和测试zkLogin零知识证明服务 @SUI Move开发必知必会

1 前言

zkLoginSui的一种原生功能,它允许人们只使用来自如GoogleMeta等的现有Web2网络凭证来创建Sui地址并签名交易。为确保隐私,集成zkLogin的应用必须创建零知识证明ZKP (Zero Knowledge proofs),以使凭证对应用实现隐藏,交易历史对网络服务实现隐藏。

由于生成ZKP可能需要大量资源,在客户端本地生成可能会较慢,建议使用专用于ZKP生成的后端服务来进行生成。

本文将在本地搭建zkLogin证明服务端,并进行合约接口调用测试验证。

2 本地搭建

2.1 安装Git LSF

在下载 zkey 之前安装 Git Large File Storage(用于大文件版本控制的开源 Git 扩展)

https://git-lfs.com/

apt/deb: sudo apt-get install git-lfs
yum/rpm: sudo yum install git-lfs

2.2 下载证明密钥文件

下载Groth16证明密钥zkey文件,稍后将其用作运行prover的参数

  • 主网、测试网
wget -O - https://raw.githubusercontent.com/sui-foundation/zklogin-ceremony-contributions/main/download-main-zkey.sh | bash
  • 开发网
wget -O - https://raw.githubusercontent.com/sui-foundation/zklogin-ceremony-contributions/main/download-test-zkey.sh | bash
  • 文件校验

要验证下载的 zkey 文件是否正确,可以使用如下命令校验文件Blake2b哈希值:

  $ b2sum ${file_name}.zkey
Network zkey file name Hash
Mainnet<br /> Testnet zkLogin-main.zkey 060beb961802568ac9ac7f14de0fbcd55e373e8f5ec7cc32189e26fb65700aa4e36f5604f868022c765e634d14ea1cd58bd4d79cef8f3cf9693510696bcbcbce
Devnet zkLogin-test.zkey 686e2f5fd969897b1c034d7654799ee2c3952489814e4eaaf3d7e1bb539841047ae8ee5fdcdaca5f4ddd76abb5a8e8eb77b44b693a2ba9d4be57e94292b26ce2

2.3 镜像下载和启动

(1)准备docker-compose文件

Docker仓库:https://hub.docker.com/repository/docker/mysten/zklogin/general, 下载镜像,也可以使用docker-compose文件来比较方便的下载和启动。

  • prover-fe-stable

  • prover-stable

services:
  backend:
    image: mysten/zklogin:prover-stable
    volumes:
      # The ZKEY environment variable must be set to the path of the zkey file.
      - ${ZKEY}:/app/binaries/zkLogin.zkey
    environment:
      - ZKEY=/app/binaries/zkLogin.zkey
      - WITNESS_BINARIES=/app/binaries

  frontend:
    image: mysten/zklogin:prover-fe-stable
    command: '8080'
    ports:
      # The PROVER_PORT environment variable must be set to the desired port.
      - '${PROVER_PORT}:8080'
    environment:
      - PROVER_URI=http://backend:8080/input
      - NODE_ENV=production
      - DEBUG=zkLogin:info,jwks
      # The default timeout is 15 seconds. Uncomment the following line to change it.
      # - PROVER_TIMEOUT=30

(2)设置环境变量

根据实际情况填写

export ZKEY=/root/zklogin-ceremony-contributions/zkLogin-main.zkey
export PROVER_PORT=7788

(3)启动命令

  • 执行该命令将会拉取镜像后,启动镜像:
$ docker-compose up
  • 成功启动,将会输出以下日志:
zkLogin:info Server is listening on port 8080

2.4 功能测试

服务启动后,将会对外暴露两个接口。

(1)/ping:心跳测试

访问ping返回pong

$ curl http://localhost:7788/ping
pong

(2)/v1:获取零知识证明

请求与响应,与官方维护的版本相同,如:https://prover-dev.mystenlabs.com/v1

  • 请求
curl -X POST 'http://localhost:7788/v1' -H 'Content-Type: application/json' \
-d '{
    "jwt": "eyJhbGciOiJSUz~~~XAiOiJKV1QifQ.eyJpc3MiO~~~~~~2EwIn0.pbZ2Z0VZD~~~Y1LzyS6v8XxIVa6ocww",
    "extendedEphemeralPublicKey": "ACG7j6wBvLWx~~~~PKwwPGEr",
    "maxEpoch": "401",
    "jwtRandomness": "115679807962189344077088833730703216993",
    "salt": "1001",
    "keyClaimName": "sub"
}'
  • 响应
{
    "proofPoints": {
        "a": ["18183913133488170987244886660719107007331181614040444504674056517093026894391", "16842299504470545911087571663739457925754422998499354640417518206207142769772", "1"],
        "b": [
            ["7555024475646145341689699627504855210714456812562052703065054137626135385647", "858749325816392910726049922089768075436189230488439583553391676001732141378"],
            ["6496804230514851815321010008902850198445392090959512529353333447840300687819", "7260574299287359028780871778232557568068946816911691368034443648636199939995"],
            ["1", "0"]
        ],
        "c": ["12717252660902811179218400063519536450898576609509570808290830037323247140386", "18393778769176542175036000358619938954524364490058277374962691740025730049657", "1"]
    },
    "issBase64Details": {
        "value": "yJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLC",
        "indexMod4": 1
    },
    "headerBase64": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImMzYWJlNDEzYjIyNjhhZTk3NjQ1OGM4MmMxNTE3OTU0N2U5NzUyN2UiLCJ0eXAiOiJKV1QifQ"
}

3 使用实践

3.1 准备环境变量

# 谷歌OAuth认证URL
OPENID_AUTHORIZATION_ENDPOINT=https://accounts.google.com/o/oauth2/v2/auth

# 注册的谷歌OAuth客户端ID
CLIENT_ID=xxx.apps.googleusercontent.com

# 使用SUI测试网
FULLNODE_URL=https://fullnode.testnet.sui.io:443

# 随便填一个地址,用于获得登录授权后,回调的JWT
REDIRECT_URL=http://localhost:8080

# 为我们自建的证明服务器地址,而非官方提供的证明服务
PROVER_URL=http://localhost:7788/v1

3.2 获取OAuth URL

执行以下代码,将会生成OAuth URL,并将生成临时私钥、JWT随机数、临时密钥过期时间记录到环境变量.env中,用于后续证明文件生成、签名验签等。

import dotenv from "dotenv";
dotenv.config();
import { generateNonce, generateRandomness } from "@mysten/zklogin";
import { Ed25519Keypair, Ed25519PublicKey } from "@mysten/sui/keypairs/ed25519";
import { SuiClient } from "@mysten/sui.js/client";
import { decodeSuiPrivateKey } from "@mysten/sui/cryptography";
import * as fs from "fs";

const OPENID_AUTHORIZATION_ENDPOINT =
  process.env.OPENID_AUTHORIZATION_ENDPOINT!;
const CLIENT_ID = process.env.CLIENT_ID!;
const FULLNODE_URL = process.env.FULLNODE_URL!;
const REDIRECT_URL = process.env.REDIRECT_URL!;

export const SUI_CLIENT = new SuiClient({ url: FULLNODE_URL });

// 创建临时密钥对,并将私钥记录到.env中
const ephemeralKeyPair = createEphemeralKeyPair("EPHEMERAL_PRIVATE_KEY");

// 本地生成随机数,并记录到.env中
const jwtRandomness = createJWTRandomness("JWT_RANDOMNESS");

function createJWTRandomness(tag: string): string {
  const jwtRandomness = generateRandomness();

  fs.appendFile(".env", `\n${tag}=${jwtRandomness}`, (err) => {
    if (err) {
      console.error(`append ${tag} jwtRandomness to .env failed, ${err}`);
      return;
    }
  });

  return jwtRandomness;
}

function createEphemeralKeyPair(tag: string): Ed25519Keypair {
  const ekp = new Ed25519Keypair();

  const skBytes = decodeSuiPrivateKey(ekp.getSecretKey()).secretKey.toString();

  fs.appendFile(".env", `\n${tag}=${skBytes}`, (err) => {
    if (err) {
      console.error(`append ${tag} sk to .env failed, ${err}`);
      return;
    }
  });

  return ekp;
}

// 创建OAuth URL
async function getOAuthURL(
  epk: Ed25519PublicKey,
  jwtRandomness: string
): Promise&lt;string> {
  const { epoch } = await SUI_CLIENT.getLatestSuiSystemState();

  // 设置临时密钥对的过期时间(2天后过期),并添加到环境变量
  const maxEpoch = Number(epoch) + 2;
  fs.appendFile(".env", `\nMAX_EPOCH=${maxEpoch}`, (err) => {
    if (err) {
      console.error(`append MAX_EPOCH to .env failed, ${err}`);
      return;
    }
  });

  const nonce = generateNonce(epk, maxEpoch, jwtRandomness);

  const params = new URLSearchParams({
    client_id: CLIENT_ID,
    redirect_uri: REDIRECT_URL,
    response_type: "id_token",
    scope: "openid email",
    nonce: nonce,
  });

  return `${OPENID_AUTHORIZATION_ENDPOINT}?${params.toString()}`;
}

async function main() {
  const oAuthUrl = await getOAuthURL(
    ephemeralKeyPair.getPublicKey(),
    jwtRandomness
  );
  console.log(oAuthUrl);
}

main();
  • 输出的OAuth URL内容如下
https://accounts.google.com/o/oauth2/v2/auth?client_id=xxx.apps.googleusercontent.com&redirect_uri=http%3A%2F%2Flocalhost%3A8080&response_type=id_token&scope=openid+email&nonce=PRKKJyW9IU44lRMHaeoSduewgZ4

3.3 获取JWT

将上面生成的OAuth URL拷贝到浏览器执行,完整谷歌账号的登录与授权,便能从回调地址中得到JWT,例如回调地址如下,id_token的内容即为JWT

http://localhost:8080/#id_token=eyJhbGciOiJSUzI1NiIs~~~~UiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJo~~~~~NlZWFkMmI4In0.HvPYeIlTcxcaraVuhbQYIiTdCxMVB2oE9CJvhIT3bxFGEjmfG35BIuPIfPdM5ibx-hAM1MD6vRkl2RTXsBX8gGTAlhFJPbIIhPDDTr4ua-WIB39eVwjnUayF~~~~Nt0qCQ&authuser=0&prompt=none
  • JWT信息添加到环境变量.env文件中
JWT=eyJhbGciOiJSUzI1NiIs~~~~UiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJo~~~~~NlZWFkMmI4In0.HvPYeIlTcxcaraVuhbQYIiTdCxMVB2oE9CJvhIT3bxFGEjmfG35BIuPIfPdM5ibx-hAM1MD6vRkl2RTXsBX8gGTAlhFJPbIIhPDDTr4ua-WIB39eVwjnUayF~~~~Nt0qCQ

3.4 获取钱包地址

为了生成zkLogin钱包地址,除了JWT外,还需要盐值,作为测试,我们固定盐值,将盐值添加到环境变量.env文件中

USER_SALT=1000
  • 代码实现
const JWT = process.env.JWT!;
const USER_SALT = process.env.USER_SALT!;

function getWalletAddress() {
  return jwtToAddress(JWT, USER_SALT);
}

console.log("zkLogin Wallet address:", getWalletAddress());
  • 得到zklogin钱包地址

    为该地址充值,便于后续发送交易

zkLogin Wallet address: 0xb4629c0f44......45f7c4d99e9a

3.5 获取zkLogin证明

  • 代码实现
// ========== 获取证明文件 ==========
const PROVER_URL = process.env.PROVER_URL!;

// 从环境变量中获得私钥,并解析得到临时Keypair对象
function createEphemeralKeyPairFromEnv(): Ed25519Keypair {
  const sk = process.env.EPHEMERAL_PRIVATE_KEY as string;
  const skArray = sk.split(",").map(Number);
  return Ed25519Keypair.fromSecretKey(new Uint8Array(skArray));
}
const ephemeralKeyPair = createEphemeralKeyPairFromEnv();

// 从环境变量获得随机数
const jwtRandomness = process.env.JWT_RANDOMNESS as string;

async function getPartialZkLoginSignature(
  keyPair: Ed25519Keypair,
  jwt: string,
  jwtRandomness: string,
  userSalt: string
): Promise&lt;any> {
  const extendedEphemeralPublicKey = getExtendedEphemeralPublicKey(
    keyPair.getPublicKey()
  );
  const verificationPayload = {
    jwt: jwt,
    extendedEphemeralPublicKey,
    maxEpoch: process.env.MAX_EPOCH as string,
    jwtRandomness: jwtRandomness,
    salt: userSalt,
    keyClaimName: "sub",
  };

  return await verifyPartialZkLoginSignature(verificationPayload);
}

export type PartialZkLoginSignature = Omit&lt;
  Parameters&lt;typeof getZkLoginSignature>["0"]["inputs"],
  "addressSeed"
>;

async function verifyPartialZkLoginSignature(zkpRequestPayload: any) {
  try {
    const proofResponse = await axios.post(PROVER_URL, zkpRequestPayload, {
      headers: {
        "content-type": "application/json",
      },
    });
    const partialZkLoginSignature =
      proofResponse.data as PartialZkLoginSignature;
    return partialZkLoginSignature;
  } catch (error) {
    console.log("failed to reqeust the partial sig: ", error);
    return {};
  }
}

async function main() {
  const partialZkLoginSignature = await getPartialZkLoginSignature(
    ephemeralKeyPair,
    JWT,
    jwtRandomness,
    USER_SALT
  );

  console.log("partialZkLoginSignature:", partialZkLoginSignature);
}

main();
  • 执行输出
partialZkLoginSignature: {
  proofPoints: {
    a: [
      '15495134036289421734756486075638497259476885063665323148887219006441184413426',
      '5496814077189949990762154445157348379519913307613637699373164633041138209869',
      '1'
    ],
    b: [ [Array], [Array], [Array] ],
    c: [
      '18788824701150535476050426460267418374653743176995002590195890205726508614553',
      '11951306859484876841425870106123409206262537628752160521522546504400831519926',
      '1'
    ]
  },
  issBase64Details: {
    value: 'yJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLC',
    indexMod4: 1
  },
  headerBase64: 'eyJhbGciOiJSUzI1NiIsImtpZCI6ImMzYWJlNDEzYjIyNjhhZTk3NjQ1OGM4MmMxNTE3OTU0N2U5NzUyN2UiLCJ0eXAiOiJKV1QifQ'
}
  • 证明服务日志输出

    在我们搭建的证明服务器上也可以看到相关日志。

frontend_1  | UID: 241dc3dfa7fad24befd15d7261d5597b078fa33e7a682c034d8014740d22ca38
frontend_1  | The issuer https://accounts.google.com is supported
frontend_1  | Skipping aud whitelist check
frontend_1  | verifySignature: 0.963ms
frontend_1  | Input validation passed!
frontend_1  | validateInput: 3.273ms
frontend_1  | genZKLoginInputs: 36.841ms
backend_1   | SingleProver::startProve begin
backend_1   | /app/binaries/zkLogin /tmp/zklogin_9M54c8 /tmp/zklogin_hDGjvW
backend_1   | Witness generation finished in 519ms
backend_1   | SingleProver::prove begin
backend_1   | Proof generation finished in 1967ms
frontend_1  | Call rapidsnark: 2.497s
frontend_1  | Request took 2537ms
frontend_1  | Sending response

3.6 发送交易

  • 代码实现

这里调用的合约接口,还是使用的《如何构建一个基于zkLogin的SUI Move dApp?》一文中的笔记合约(notes新增笔记(create_note 接口。

interface JwtPayload {
  iss: string;
  sub: string;
  aud: string[] | string;
  exp?: number;
  nbf?: number;
  iat?: number;
  jti?: string;
}

async function generateZkLoginSignature(
  userSalt: string,
  jwt: string,
  userSignature: string
): Promise&lt;string> {
  const partialZkLoginSignature = await getPartialZkLoginSignature(
    ephemeralKeyPair,
    JWT,
    jwtRandomness,
    USER_SALT
  );

  console.log("partialZkLoginSignature:", partialZkLoginSignature);

  const decodedJWT = jwtDecode(jwt) as JwtPayload;
  const addressSeed = genAddressSeed(
    BigInt(userSalt),
    "sub",
    decodedJWT.sub,
    decodedJWT.aud.toString()
  ).toString();

  return getZkLoginSignature({
    inputs: {
      ...partialZkLoginSignature,
      addressSeed,
    },
    maxEpoch: MAX_EPOCH,
    userSignature,
  });
}

function getZkLoginPk(jwt: string, userSalt: string): ZkLoginPublicIdentifier {
  const decodedJWT = jwtDecode(jwt) as JwtPayload;
  const addressSeed = genAddressSeed(
    userSalt,
    "sub",
    decodedJWT.sub,
    decodedJWT.aud.toString()
  );

  return toZkLoginPublicIdentifier(addressSeed, decodedJWT.iss);
}

async function addNote(title: string, body: string) {
  const txb = new Transaction();
  const txData = {
    target: `${PACKAGE_ID}::notes::create_note`,
    arguments: [txb.pure.string(title), txb.pure.string(body)],
  };
  return makeMoveCall(txData, txb);
}

async function makeMoveCall(txData: any, txb: Transaction) {
  const sender = getWalletAddress();
  txb.setSender(sender);
  txb.moveCall(txData);

  const { bytes, signature: userSignature } = await txb.sign({
    client: SUI_CLIENT,
    signer: ephemeralKeyPair,
  });

  const zkLoginSignature = await generateZkLoginSignature(
    USER_SALT,
    JWT,
    userSignature
  );

  return SUI_CLIENT.executeTransactionBlock({
    transactionBlock: bytes,
    signature: zkLoginSignature,
  });
}

async function main() {
  await addNote("欢迎关注微信公众号:Move中文", "开启你的Sui Move之旅!");
}

main();
  • 成功执行后,在浏览器上可查看到该笔zkLogin签名交易

image.png

4 参考资料

https://docs.sui.io/guides/developer/cryptography/zklogin-integration#run-the-proving-service-in-your-backend

https://medium.com/sui-network-cn/%E6%96%B0%E6%89%8B%E6%95%99%E7%A8%8B-%E6%90%AD%E5%BB%BAzklogin%E7%9A%84%E8%AF%81%E6%98%8E%E6%9C%8D%E5%8A%A1%E7%AB%AF-1e2cfa3b08ee

5 更多

欢迎关注微信公众号:Move中文,开启你的 Sui Move 之旅!

image.png

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
rzexin
rzexin
0x6Fa5...8165
江湖只有他的大名,没有他的介绍。