玩转Sui多签钱包2:zkLogin公钥多签钱包 @SUI Move开发必知必会

  • rzexin
  • 更新于 2024-06-29 00:06
  • 阅读 1298

玩转Sui多签钱包2:zkLogin公钥多签钱包 @SUI Move开发必知必会

玩转Sui多签钱包2:zkLogin公钥多签钱包 @SUI Move开发必知必会

1 前言

本系列文章将会分为3部分内容,包括:

本文是该系列的第二篇,在正式介绍之前,我们先了解一些zkLogin的知识。更详细的可参考:

1.1 zkLogin是什么?

zkLoginSui的原语(primitive),它使你能够使用OAuth凭证从Sui地址发送交易,而无需公开将两者关联起来。

1.2 zkLogin设计目标

  • 简化入门流程(Streamlined onboarding): zkLogin使你能够使用熟悉的OAuth登录流程在Sui上进行交易,避免了记住私钥或助记词的烦恼
  • 自我托管(Self-custody): zkLogin交易需要用户通过标准的OAuth登录流程进行批准,OAuth提供商不能代表用户进行交易
  • 安全性(Security): zkLogin是一种双因子认证方案;发送交易需要一个最近的 OAuth 登录凭证和一个不由 OAuth 提供商管理的。攻击者即使攻破了 OAuth 账户,也无法从用户相应的 Sui 地址进行交易,除非他们单独攻破盐
  • 隐私(Privacy): 零知识证明阻止第三方将Sui地址与其相应的OAuth标识符关联起来
  • 可选的已验证身份(Optional verified identity): 用户可以选择验证用于派生特定Sui地址的OAuth标识符。这为可验证的链上身份层奠定了基础
  • 可访问性(Accessibility): zkLogin 是多种原生 Sui 签名方案之一,这得益于 Sui 的密码学灵活性。它与其他 Sui 原语(如赞助交易sponsored transactions和多重签名multisig)集成在一起
  • 严谨性(Rigorousness)zkLogin 的代码已由两家专门从事零知识的公司独立审计。创建公共参考字符串的公共 zkLogin 仪式吸引了来自 100 多名参与者的贡献

1.3 集成指南

  • 在应用中创建临时密钥对
  • 应用提示用户使用与临时公钥对应的随机数完成 OAuth 登录流程
  • 应用从回调中得到JWT
  • 应用根据JWT获取唯一的用户盐值,并计算zkLogin Sui地址
  • 获取零知识证明
  • 应用使用临时私钥签名交易
  • 应用提交带有临时签名和零知识证明的交易到节点

1.4 已支持的OpenId提供商

Provider Can support? Devnet Testnet Mainnet
Facebook Yes Yes Yes Yes
Google Yes Yes Yes Yes
Twitch Yes Yes Yes Yes
Slack Yes Yes No No
Kakao Yes Yes No No
Apple Yes Yes Yes Yes

1.5 完整流程

image.png

(1)用户本地生成临时密钥对(eph_sk, eph_pk),并制定max_epoch

(2)将eph_pk、到期时间(max_epoch)和随机数 (jwt_randomness)拼接后计算其哈希作为nonce值后,发起登录操作

(3)用户完成 OAuth 登录流程后,可以在应用程序的重定向 URL 中得到 JWT

(4)应用前端发送JWT到盐值服务上获取盐值

(5)盐值服务在校验JWT通过后,会基于 iss, aud, sub 创建唯一的用户盐值 user_salt ,并返回

(6)用户向零知识证明服务发送JWT、用户盐值、临时公钥、JWT随机数、密钥声明名称(即:sub)。证明服务生成一个零知识证明,将这些作为私有输入,并执行以下操作:

  • 检查随机数是否按照定义正确派生
  • 检查密钥声明值是否与 JWT 中的相应字段匹配
  • 验证来自 JWTOPRSA 签名
  • 地址与密钥声明值和用户盐值是否一致

(7)返回零知识证明信息

(8)应用前端根据issaudsub计算用户地址。只要应用程序具有有效的 JWT,就可以独立完成此步骤

(9)用户创建交易,使用临时私钥对交易进行签名,以生成临时签名

(10)用户将交易连通临时签名、临时公钥、零知识证明等信息提交给Sui节点。Sui节点会根据存储的 JWK 验证零知识证明以及临时签名是否正确

2 基于zkLogin公钥的多签钱包创建及交易执行

2.1 获取JWT

我们创建3个谷歌的OAuth认证URL,并分别在浏览器访问并登录,获得对应JWT。其他OpenId提供厂商的方法类似。

(1)代码实现

运行以下代码,将会创建3个谷歌OpenId OAuth认证URL,并将临时密钥、JWT随机数记录到.evn文件中,便于后续使用。

import dotenv from "dotenv";
dotenv.config();
import { generateNonce, generateRandomness } from "@mysten/zklogin";
import { Ed25519Keypair, Ed25519PublicKey } from "@mysten/sui/keypairs/ed25519";
import { SUI_CLIENT } from "./suiClient";
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 REDIRECT_URL = process.env.REDIRECT_URL!;

// 创建临时密钥对,记录到.env中
const ekp1 = createEphemeralKeyPair("SK1");
const ekp2 = createEphemeralKeyPair("SK2");
const ekp3 = createEphemeralKeyPair("SK3");

// 本地生成随机数,记录到.env中
const jwtRandomness1 = createJWTRandomness("JWT_RANDOMNESS1");
const jwtRandomness2 = createJWTRandomness("JWT_RANDOMNESS2");
const jwtRandomness3 = createJWTRandomness("JWT_RANDOMNESS3");

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;
}

async function getOAuthURL(
  epk: Ed25519PublicKey,
  jwtRandomness: string
): Promise<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 oAuthUrl1 = await getOAuthURL(ekp1.getPublicKey(), jwtRandomness1);
  const oAuthUrl2 = await getOAuthURL(ekp2.getPublicKey(), jwtRandomness2);
  const oAuthUrl3 = await getOAuthURL(ekp3.getPublicKey(), jwtRandomness3);

  console.log(oAuthUrl1);
  console.log(oAuthUrl2);
  console.log(oAuthUrl3);
}

main();

(2)获得OAuth认证URL

  • 用户1
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=lNj6vKFVTZOSMKczQlpspRL6isQ
  • 用户2
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=TPquW9sogteqoIFoj8f0qfFbNfI
  • 用户3
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=uaAkFzy7xd9g19u8zqiZVQbnTQg

(3)分别访问并登录授权获得JWT

将得到的JWT添加到.env环境变量文件中,方便后续使用

  • JWT1
JWT1=eyJhbGciOiJSUzI1NiIsImtpZCI6IjJhZjkwZTg3YmUxNDBjMjAwMzg4OThhNmVmYTExMjgzZGFiNjAzMWQiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2Fj~~~~~~wMTBhIn0.LU6JY3apvKpMa3HTqxTtf2kgMCiAVDHxE124yvUqoCQfrGFr1-_00hcAKi4sWbsiM2K7GzZrKAAuiEab9VA3KfkSw5Qw0W62ZH-bUT-vNkXECSfTWBXPnrf_w-cgWqRgIyzgPpmZM1p2W9A3swP9JwnJXjYetK63QtVTTm7xHBnW6PVMNTLechX1K3A6HsmNFYGsLK044PTeeP_tzdn6-9FilcxeD6fbQZlexlEyP41G4aVttI1oZHuudeifD5jC0gU5UP5H19Q1gr7QW8oZ4jMLVbPAlEfchr2KqGK0_w6AgCek5hYUEEjI16MVIdD16kQg42QIICre1GUk0aP6hA
  • JWT2
JWT2=eyJhbGciOiJSUzI1NiIsImtpZCI6IjJhZjkwZTg3YmUxNDBjMjAwMzg4OThhNmVmYTExMjgzZGFiNjAzMWQiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291b~~~~~~ZThmOTRiNmY2MzIifQ.eRzpNjgwM8aw43_QcThS9gyWpgxT7jsbYeCtKpBakCHnaq9Bt3LS2m6enFfSmtrPy3SBRY7M3Lq8hCwtTfHsHoJz9Qyrj6ekXyWYj_EUHzkGKaA49fOzfNw1zWMryJ9YYkRt2W4b2ia1pQJZNfnbwEON8S1Mq9nmlXRaY2EvBPpQSwOEA-Y7E2Njtxpgcrz-OF87ms2ge3T1AU4N658Uz8bczsSIQLV6syfSWYFUcPuAKxmi-D-SZB2Bjq8bVL-nLDzgjf2nKkRBWUUDDRpcW9t7E7R9pQUcgUwnE_C23wev-73k_OMjJeN440TYxwHWzdIctHpUOae1gEjiCdA0cw
  • JWT3
JWT3=eyJhbGciOiJSUzI1NiIsImtpZCI6IjJhZjkwZTg3YmUxNDBjMjAwMzg4OThhNmVmYTExMjgzZGFiNjAzMWQiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczov~~~~~~ZWY5OTE2ZWUifQ.DsNPdHATvhKw4qWCwuDnkePZIwBZHiR9CphyY8b8-yn9Q-L7ZZ_uYj15l0kGw-YUWXo6GkHBz90gNZ2aXvg09N6Zeq5COUz1KM40n6mmDVi1O1zoB57c28AkX0IriNiRY86p_TxCJ_f6CGW0u73kxINXiRzJ0_mSHGCf-iDTLJ63ncLoHjgL__YrJ-6Cdd9kGRMCQpIhL8KCJQza3JjXf1lHgtC53PamIWM5k5vNbkFJM7c1K8-Shr6qmfIFh7kpkWD49jun9bf9AseNBiQcR2bAdqFNqp2aEjlm1bZIzRR6iAHzgco1ShYtPp0GezxkjK2QvYUN7R_WRQI7QsorlQ

2.2 创建基于zkLogin公钥的多签钱包

(1)功能说明

以下代码同样将创建三个zkLogin公钥的多签钱包,其中公钥1和公钥2的权重为1,公钥3的权重为2,阈值为2。即:发起一笔交易,要么私钥1和私钥2进行zkLogin多签、要么私钥3进行zkLogin单签才能成功。

(2)代码实现

import dotenv from "dotenv";
dotenv.config();
import { genAddressSeed } from "@mysten/zklogin";
import { MultiSigPublicKey } from "@mysten/sui/multisig";
import {
  ZkLoginPublicIdentifier,
  toZkLoginPublicIdentifier,
} from "@mysten/sui/zklogin";
import { jwtDecode } from "jwt-decode";

// 从环境变量中获得的JWT
const JWT1 = process.env.JWT1 as string;
const JWT2 = process.env.JWT2 as string;
const JWT3 = process.env.JWT3 as string;

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

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);
}

function createMultisigAddressWithZKLogin(): MultiSigPublicKey {
  const multiSigPublicKey = MultiSigPublicKey.fromPublicKeys({
    threshold: 2,
    publicKeys: [
      {
        publicKey: getZkLoginPk(JWT1, "1001"),
        weight: 1,
      },
      {
        publicKey: getZkLoginPk(JWT2, "1002"),
        weight: 1,
      },
      {
        publicKey: getZkLoginPk(JWT3, "1003"),
        weight: 2,
      },
    ],
  });

  return multiSigPublicKey;
}

const multiSigPublicKey = createMultisigAddressWithZKLogin();
console.log(multiSigPublicKey.toSuiAddress());

(3)多签钱包地址充值

为得到的多签钱包地址,进行充值,以便后续发送交易支付手续费。

2.3 获取zkLogin零知识证明

(1)功能说明

运行以下代码,将会生成三个zkLogin地址的零知识证明,将会用于生成最终的zkLogin签名。

(2)代码实现

import dotenv from "dotenv";
dotenv.config();
import { getExtendedEphemeralPublicKey } from "@mysten/zklogin";
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
import { getZkLoginSignature } from "@mysten/sui/zklogin";
import axios from "axios";

const PROVER_URL = process.env.PROVER_URL!;

function createEphemeralKeyPairFromEnv(tag: string): Ed25519Keypair {
  let sk = process.env.SK1 as string;
  if (tag == "SK2") {
    sk = process.env.SK2 as string;
  } else if (tag == "SK3") {
    sk = process.env.SK3 as string;
  }

  const skArray = sk.split(",").map(Number);
  return Ed25519Keypair.fromSecretKey(new Uint8Array(skArray));
}

// 从环境变量创建临时密钥对
const ekp1 = createEphemeralKeyPairFromEnv("SK1");
const ekp2 = createEphemeralKeyPairFromEnv("SK2");
const ekp3 = createEphemeralKeyPairFromEnv("SK3");

// 从环境变量获得随机数
const jwtRandomness1 = process.env.JWT_RANDOMNESS1 as string;
const jwtRandomness2 = process.env.JWT_RANDOMNESS2 as string;
const jwtRandomness3 = process.env.JWT_RANDOMNESS3 as string;

// 从环境变量中获得的JWT
const JWT1 = process.env.JWT1 as string;
const JWT2 = process.env.JWT2 as string;
const JWT3 = process.env.JWT3 as string;

async function getPartialZkLoginSignature(
  keyPair: Ed25519Keypair,
  jwt: string,
  jwtRandomness: string,
  userSalt: string
): Promise<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<
  Parameters<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 partialZkLoginSignature1 = await getPartialZkLoginSignature(
    ekp1,
    JWT1,
    jwtRandomness1,
    "1001"
  );
  console.log(
    "partialZkLoginSignature1: ",
    JSON.stringify(partialZkLoginSignature1)
  );

  const partialZkLoginSignature2 = await getPartialZkLoginSignature(
    ekp2,
    JWT2,
    jwtRandomness2,
    "1002"
  );
  console.log(
    "partialZkLoginSignature2: ",
    JSON.stringify(partialZkLoginSignature2)
  );

  const partialZkLoginSignature3 = await getPartialZkLoginSignature(
    ekp3,
    JWT3,
    jwtRandomness3,
    "1003"
  );
  console.log(
    "partialZkLoginSignature3: ",
    JSON.stringify(partialZkLoginSignature3)
  );
}

main();

(3)获取到的zkLogin零知识证明

  • 用户1
partialZkLoginSignature1:  {"proofPoints":{"a":["14832536503620236750207539878309821304798845102886328200500002064261861024596","6098458781316679357431194461835045775756847359647150465987882945922602097684","1"],"b":[["8526710911098951542164729912098087801943066291645009567420356653275589013339","12191499139322446608204189789140884435811809808585535318587642087068460430650"],["15157126251392138080206919153527847755110528209517287687180723390366575294684","18435843692752302986435461580737708077786738415333681364131368985540376512131"],["1","0"]],"c":["9915061636399225528628085890086021346545174008425467121122025394538813828858","4234817782411751794097725246812494469567246413958518347403982701380724289799","1"]},"issBase64Details":{"value":"yJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLC","indexMod4":1},"headerBase64":"eyJhbGciOiJSUzI1NiIsImtpZCI6IjJhZjkwZTg3YmUxNDBjMjAwMzg4OThhNmVmYTExMjgzZGFiNjAzMWQiLCJ0eXAiOiJKV1QifQ"}
  • 用户2
partialZkLoginSignature2:  {"proofPoints":{"a":["18433919245915507148114493793411264105374103943871562990223521930266836271983","18939976090943115419839770923844241604814364406395842871698008772206087436878","1"],"b":[["11944915040664030630589443644466915837266322696718148988840305616669987862064","7700565435072459510735266829091297288471998405565419014882510487612437556381"],["15360253758166950243890078560825748794737606051799398911095255605626063841827","21330394914721293545198183304724898397231260614444624014943854049670139090199"],["1","0"]],"c":["11441443481802854899856472972901858079195788325079011273495325063938702195236","11852191467263750050627671811533142604227756537216910900889732585930256826158","1"]},"issBase64Details":{"value":"yJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLC","indexMod4":1},"headerBase64":"eyJhbGciOiJSUzI1NiIsImtpZCI6IjJhZjkwZTg3YmUxNDBjMjAwMzg4OThhNmVmYTExMjgzZGFiNjAzMWQiLCJ0eXAiOiJKV1QifQ"}
  • 用户3
partialZkLoginSignature3:  {"proofPoints":{"a":["4740731145292925652215230270851732536502954743960527232699771677642762080376","3047046322893722166104080614047142015224179441494873249302137668013525987808","1"],"b":[["19889686587291816871051044555672650377990875240267055150536208167747114908713","2676334022512023736718118063511178797092366686119862357043349874865738569290"],["9132265628905091888922925613035178934071346230407134478683778927653892962291","13794186112643208431435349588679025537267304717941663769161943409075541251871"],["1","0"]],"c":["18109617987645336494129514748177200680204598861839472849880675856081829298271","7177148781916594569772897974686503609852049585577400096543979831550750303275","1"]},"issBase64Details":{"value":"yJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLC","indexMod4":1},"headerBase64":"eyJhbGciOiJSUzI1NiIsImtpZCI6IjJhZjkwZTg3YmUxNDBjMjAwMzg4OThhNmVmYTExMjgzZGFiNjAzMWQiLCJ0eXAiOiJKV1QifQ"}

2.4 发送zkLogin多签交易

这里发送的交易还是采用我们简单的笔记合约交易,具体请参看:

2.4.1 场景一:用户1单签名,未达阀值,交易失败

(1)说明

由于用户1权重为1,多签钱包阈值为2,所以用户1单签名交易,交易验签会不通过,将会报错。

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

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();
  console.log("sender: ", sender);
  txb.setSender(sender);
  txb.moveCall(txData);

  // 写法1
  const txBytes = await txb.build({ client: SUI_CLIENT });
  const userSignature1 = (await ekp1.signTransaction(txBytes)).signature;

  // 写法2
  // const { bytes, signature: userSignature1 } = await txb.sign({
  //   client: SUI_CLIENT,
  //   signer: ekp1,
  // });

  const partialZkLoginSignature1 = await getPartialZkLoginSignature(
    ekp1,
    JWT1,
    jwtRandomness1,
    "1001"
  );

  const zkLoginSignature1 = await generateZkLoginSignature(
    partialZkLoginSignature1,
    "1001",
    JWT1,
    userSignature1
  );

  const combinedSignature = multiSigPublicKey.combinePartialSignatures([
    zkLoginSignature1,
  ]);

  return SUI_CLIENT.executeTransactionBlock({
    transactionBlock: txBytes,
    signature: zkLoginSignature1,
  });
}
(3)执行结果

报错信息为:Signature is not valid: Insufficient weight=1 threshold=2,符合预期。

/xxx/node_modules/@mysten/sui/src/client/http-transport.ts:119
                        throw new JsonRpcError(data.error.message, data.error.code);
         ^
JsonRpcError: Invalid user signature: Signature is not valid: Insufficient weight=1 threshold=2
    at SuiHTTPTransport.request (/xxx/node_modules/@mysten/sui/src/client/http-transport.ts:119:10)
    at processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async SuiClient.executeTransactionBlock (/xxx/node_modules/@mysten/sui/src/client/client.ts:414:10) {
  code: -32002,
  type: 'TransactionExecutionClientError'
}

注:上面代码实现,即便是单签,也必须使用combinePartialSignatures,否则会报类似如下错:

JsonRpcError: Invalid user signature: Required Signature from 0x06ebec847702fbc9f6de8487cf8cffcdd3739c0a957b08bda9ccc09ba8f6d7a8 is absent ["0x32b21e34a7ca68f14312f81304c29e87f385337ffeeadda927a06468926dc5f1"]

2.4.2 场景2:用户1和用户2执行进行多签,达到阀值,交易成功

(1)说明

由于用户1和用户2的权重均为1,多签钱包阈值为2,当用户1和用户2进行多签时,达到阀值2,交易可以执行成功。

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

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();
  console.log("sender: ", sender);
  txb.setSender(sender);
  txb.moveCall(txData);

  const txBytes = await txb.build({ client: SUI_CLIENT });
  const userSignature1 = (await ekp1.signTransaction(txBytes)).signature;
  const userSignature2 = (await ekp2.signTransaction(txBytes)).signature;

  const partialZkLoginSignature1 = await getPartialZkLoginSignature(
    ekp1,
    JWT1,
    jwtRandomness1,
    "1001"
  );

  const partialZkLoginSignature2 = await getPartialZkLoginSignature(
    ekp2,
    JWT2,
    jwtRandomness2,
    "1002"
  );

  const zkLoginSignature1 = await generateZkLoginSignature(
    partialZkLoginSignature1,
    "1001",
    JWT1,
    userSignature1
  );

  const zkLoginSignature2 = await generateZkLoginSignature(
    partialZkLoginSignature2,
    "1002",
    JWT2,
    userSignature2
  );

  const combinedSignature = multiSigPublicKey.combinePartialSignatures([
    zkLoginSignature1,
    zkLoginSignature2,
  ]);

  return SUI_CLIENT.executeTransactionBlock({
    transactionBlock: txBytes,
    signature: combinedSignature,
  });
}
(3)执行结果

查询浏览器,从交易签名中,我们可以看到是用户1和用户2的zkLogin地址进行了多签,交易成功执行。

https://testnet.suivision.xyz/txblock/w5g51eZ9WqvKQkAJd17VMRTvGPaPz8DxG1LGmc6iCbh?tab=User+Signatures

image.png

2.4.3 场景三:用户3单签名,达到阀值,交易成功

(1)说明

由于用户3权重为2,多签钱包阈值为2,估只需要用户3单签名就能达到阀值2,交易便可以执行成功。

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

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();
  console.log("sender: ", sender);
  txb.setSender(sender);
  txb.moveCall(txData);

  const txBytes = await txb.build({ client: SUI_CLIENT });
  const userSignature3 = (await ekp3.signTransaction(txBytes)).signature;

  const partialZkLoginSignature3 = await getPartialZkLoginSignature(
    ekp3,
    JWT3,
    jwtRandomness3,
    "1003"
  );

  const zkLoginSignature3 = await generateZkLoginSignature(
    partialZkLoginSignature3,
    "1003",
    JWT3,
    userSignature3
  );

  const combinedSignature = multiSigPublicKey.combinePartialSignatures([
    zkLoginSignature3,
  ]);

  return SUI_CLIENT.executeTransactionBlock({
    transactionBlock: txBytes,
    signature: combinedSignature,
  });
(3)执行结果

查询浏览器,从交易签名中,我们可以看到只有用户3的zkLogin地址进行了单签,交易成功。

https://testnet.suivision.xyz/txblock/F9SW6UFQDBKgwi7gtxg38VXJxGJs1Qe8fnNxLDZskoFm?tab=User+Signatures

image.png

3 参考资料

https://sdk.mystenlabs.com/typescript/cryptography/multisig

4 更多

本文示例中创建多签钱包使用的三个公钥均为zkLogin公钥,也可以混合使用普通公钥和zkLogin公钥生成多签钱包地址,以满足不同场景下的需求。

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

image.png

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

0 条评论

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