CMP 三方 MPC 钱包

CMP 三方 MPC 钱包

三个参与方在浏览器里各自运行 CMP 协议:先各自生成私钥分片并组建同一个钱包(公钥 X = X₁ + X₂ + X₃),再通过 Paillier 加密的 MtA 生成随机数 R,最后各自算出签名份额 si,三份相加得到一个标准 ECDSA 签名。任何一方都不掌握完整私钥。所有计算都是真实运算:secp256k1、2048 位 Paillier、1024 位 Ring-Pedersen。同一次密钥生成还会产出一把三方共管的 Ed25519 密钥,「③ 链上转账」用这两把密钥在以太坊 Sepolia(EIP-1559)、Solana Devnet 和比特币 testnet4(P2WPKH)上发起真实转账。

论文 Canetti 等,UC Non-Interactive, Proactive, Distributed ECDSA with Identifiable Aborts(ePrint 2021/060,2024-10-21 版) 源码 fireblocks/mpc-lib @ 00ae08b

① · 生成私钥分片,组建钱包

对应论文 Figure 6(密钥生成)与 Figure 7(辅助信息)。mpc-lib 把这两部分合并成一次 setup:第 1 轮就同时生成私钥分片、Paillier 密钥和 Ring-Pedersen 参数,并一起做承诺。CMP 只支持 n-of-n,源码里 t ≠ n 会直接报错。

每一轮同时跑两个 key_id:secp256k1 密钥(以太坊用)和 Ed25519 密钥(Solana 用)。mpc-lib 的 cmp_setup_service 对两种曲线走同一套流程,只是换了 algebra。

  1. 第 1 轮

    生成分片与辅助密钥,广播承诺

    论文 Fig.6 Round 1 · cmp_setup_service::generate_setup_commitments

    每方随机取私钥分片 xi,算出 Xi = xi·G;生成 Schnorr 随机数 k、32 字节 seed(ρi)、Paillier 密钥(p ≡ 3, q ≡ 7 mod 8)和 Ring-Pedersen 参数(安全素数)。对这些公开值做带盐 SHA-256 承诺,只广播承诺。

  2. 第 2 轮

    打开承诺,附上 ack

    论文 Fig.6 Round 2 · cmp_setup_service::store_setup_commitments

    收齐三份承诺后,每方计算 ack = SHA256(Σ id ‖ commitment),然后发出解承诺:seed、Xi、Schnorr 的 R、Paillier 公钥、Ring-Pedersen 公钥,以及 ack。

  3. 第 3 轮

    校验承诺,计算 srid,发送零知识证明

    论文 Fig.6 Round 3 · cmp_setup_service::generate_setup_proofs

    核对每份解承诺与 ack;srid = seed₁ ⊕ seed₂ ⊕ seed₃,aad = key_id ‖ id ‖ srid。用第 1 轮承诺过的 k 生成 Schnorr 证明(s = k − c·xi),再生成 Ring-Pedersen 参数证明 Πprm。

  4. 第 4 轮

    验证证明,得到钱包公钥

    论文 Fig.6 Output · cmp_setup_service::verify_setup_proofs / create_secret

    验证其他两方的 Schnorr 证明和 Πprm,全部通过后公钥 X = X₁ + X₂ + X₃。每方只保存自己的 xi、Paillier 私钥,以及所有人的公开参数。

② · 预签名与签名

论文给出两种用法:在线签名(Figure 4,先确定消息,再跑完整的预签名和签名)和非交互签名(Figure 5,先离线批量生成预签名,消息到来时只需一轮)。mpc-lib 分别实现为 cmp_ecdsa_online_signing_service 和 cmp_ecdsa_offline_signing_service。

摘要 m
预签名池(每份只能使用一次,用后删除)
暂无预签名。
  1. 第 1 轮

    MtA 请求:加密 ki 与 γi

    论文 Fig.8 Round 1 · start_ecdsa_signature_preprocessing / start_signing → create_mta_request

    每方随机取 ki、γi、ai、bi;广播 Ki = enci(ki)、Gi = enci(γi),以及对 ki 的 El-Gamal 式承诺 A = a·G、B = b·G、Z = (ab + k)·G。初值 δi = kiγi,χi = 0。

  2. 第 2 轮

    MtA 应答:在密文上乘以 γi 和 xi

    论文 Fig.8 Round 2 · offline_mta_response / mta_response → mta::answer_mta_request

    对每个 j ≠ i:D = Kjγi · encj(β) = encj(kjγi + β),β 是 160 字节随机数;F = enci(β)。同样对 xi 算 D̂、F̂。本方 δi −= β,χi −= β̂。同时发出 Γi = γi·G。

  3. 第 3 轮

    解密得到 α,广播 δi 与 Δi

    论文 Fig.8 Round 3 · offline_mta_verify / mta_verify → verify_block_and_get_delta

    用自己的 Paillier 私钥解密 α = deci(D)(超过 N/2 时按负数处理),δi += α,χi += α̂。Γ = Σ Γj,Δi = ki·Γ,并附 Diffie-Hellman 离散对数证明,说明 Δi 里的 ki 与第 1 轮承诺一致。

  4. 输出

    计算 R,保存预签名

    论文 Fig.8 Output · store_presigning_data / get_si → calc_R

    验证其他方的离散对数证明;δ = Σ δj = k·γ,检查 δ·G = Σ Δj;R = δ⁻¹·Γ = k⁻¹·G。每方保存 (ki, χi, R)。

  5. 签名

    各方计算签名份额 si

    论文 Fig.10 Round 1 · cmp_ecdsa_offline_signing_service::ecdsa_sign / cmp_ecdsa_online_signing_service::get_si

    r = R.x mod n,si = ki·m + r·(ki·xi + χi)。三方的 Σ(kixi + χi) = k·x,所以 Σ si = k(m + r·x),正是以 k⁻¹ 为随机数的 ECDSA 签名。

② · 合成签名并恢复公钥

收齐三份签名份额后相加得到 s,按源码 make_sig_s_positive 规范成 low-s 并同步翻转 v,再用标准 ECDSA 验签,并从 (r, s, v) 恢复出公钥,和钱包公钥比对。

  1. 合成

    Σ si → (r, s, v) → 验签与 ecrecover

    论文 Fig.10 Output · ecdsa_offline_signature / get_cmp_signature → make_sig_s_positive

    另外演示两点:单独一份 si 不是有效签名;只拿两方份额相加也验不过,因为 CMP 是 3-of-3。

③ · 链上转账:Ethereum Sepolia、Solana Devnet 与 Bitcoin testnet4

以太坊交易按 EIP-1559(交易类型 2)构建,用第二部分的 CMP 协议签名,签名的消息是 Keccak256(0x02 ‖ rlp(…))。Solana 用 Ed25519,CMP 不适用,这里按 mpc-lib 的 eddsa_online_signing_service 做三方 EdDSA 签名,签名的消息是整条交易消息。比特币 testnet4 用同一把 secp256k1 密钥的 P2WPKH 地址,每个输入按 BIP143 算出签名哈希,各做一次 CMP 签名。

只用于测试网。钱包的三份分片都保存在这一个浏览器页面里,不提供真实 MPC 的安全性;导出的 JSON 等于完整私钥。在 claude.ai 在线版里,页面不能访问外部网络:可以手动填写参数并复制原始交易自行广播,也可以把 HTML 下载到本地,用浏览器直接打开,就能连接 RPC。

先在「① 构建钱包」完成密钥生成,或在「④ 密钥备份与恢复」中恢复钱包,这里才会启用。

发送方地址 = Keccak256(X)[12:],EIP-55 校验和格式
余额
—

领取测试币:Google Cloud Sepolia 水龙头 · pk910 PoW 水龙头。把上面的地址粘贴过去即可。

④ · 新增分片组(add user)

在当前分片组三片私钥都在的前提下,再分出一组新的三片私钥,交给三个新成员。新组与旧组对应同一个钱包公钥,以太坊、Solana、比特币地址都不变;两组各自独立,任何一组的三方都能单独签名,但两组的分片不能混用。对应 mpc-lib 的 cmp_setup_service::add_user_request(旧成员)与 add_user(新成员):旧成员把自己的分片拆开加密发给新成员,新成员求和后照常跑一遍 setup。

新组同样覆盖 secp256k1(以太坊、比特币)和 Ed25519(Solana)两把密钥,使用新的 key_id。CMP 只支持 n-of-n,所以新组也是 3-of-3。本演示最多 3 组(A、B、C)。

从当前签名使用的分片组拆分。完成后可以在页面上方切换签名使用的分片组,也可以在「⑤ 密钥备份与恢复」里单独备份新组。
钱包里的分片组
  1. 第 1 步

    旧组拆分分片,加密发给新成员

    cmp_setup_service::add_user_request · platform_service::encrypt_for_player

    当前签名组的每一方取出自己的 xi,拆成三份加法分片:前两份随机,最后一份 = xi − 前两份。每份用对应新成员的公钥加密,连同钱包公钥一起作为 add_user_data 发出。secp256k1 和 Ed25519 两把密钥各拆一次。

  2. 第 2 步

    新成员解密求和,生成辅助密钥并承诺

    cmp_setup_service::add_user → generate_setup_commitments

    新成员核对三方发来的钱包公钥一致、都有给自己的那份,解密后相加得到自己的新分片 x′j。之后与密钥生成第 1 轮相同:生成自己的 Paillier 与 Ring-Pedersen 密钥,用新的 key_id 做承诺并广播。

  3. 第 3 步

    打开承诺,附上 ack

    cmp_setup_service::store_setup_commitments

    新组三方之间的 setup 第 2 轮,与「① 构建钱包」第 2 轮完全相同,只是参与方换成了新成员。

  4. 第 4 步

    校验承诺,计算 srid,发送零知识证明

    cmp_setup_service::generate_setup_proofs

    新组的 srid 由新成员的 seed 异或得到;Schnorr 证明的是新分片 x′j,Πprm 证明新成员自己的 Ring-Pedersen 参数。

  5. 第 5 步

    验证证明,核对公钥不变

    cmp_setup_service::verify_setup_proofs

    验证另外两方的证明。add_user 路径在 metadata 里事先存了原钱包公钥,源码要求 Σ X′j 与它完全一致,否则报 INVALID_PARAMETERS。通过后新分片组加入钱包,可以单独签名,也可以单独备份。

⑤ · 密钥备份与恢复

三方的私钥分片可以用 GPG 加密后下载:每方一个 JSON,三个文件打成 zip,再用你的 GPG 公钥加密。恢复时上传加密文件和 GPG 私钥,页面在本地解密,从三个 JSON 中恢复三把私钥分片。所有处理都在浏览器里完成。钱包有多个分片组时,每组单独备份成一个文件。

GPG 加密导出

每方的分片单独存成一个 JSON(只含这一方的 xi、Paillier 私钥和自己那份预签名,加上所有人的公开参数),三个文件打成 zip,再用你的 GPG 公钥加密成 ASCII armor 文件下载。

GPG 解密恢复

上传加密的备份文件和对应的 GPG 私钥,页面在本地解密、解压,从三个 JSON 中恢复三把私钥分片,核对无误后放回钱包:与当前钱包是同一把钥匙时只替换或加入这一组,其余分片组保留;否则替换整个钱包。私钥和口令只在本页面内使用,不会发到任何地方。

本地保存与 JSON 备份

本演示实现了什么

  • secp256k1 椭圆曲线运算、SHA-256 / SHA-512 / Keccak-256,全部用 JavaScript BigInt 手写,不依赖外部库。
  • Paillier 2048 位(paillier.c:p ≡ 3 mod 8、q ≡ 7 mod 8、g = N + 1),Ring-Pedersen 1024 位(ring_pedersen.c:安全素数,t = r²,s = tλ)。
  • 带盐承诺与 ack(commitments.c、ack_message),aad = sid ‖ id ‖ srid(build_aad)。
  • 零知识证明:Schnorr Πsch(schnorr.c)、Ring-Pedersen 参数 Πprm 80 轮(ring_pedersen.c)、Diffie-Hellman 离散对数证明(diffie_hellman_log.c,挑战值用 drng.c 的 SHA-512 确定性随机数生成)。
  • MtA:β 取 32×5 字节(BETA_HIDING_FACTOR = 5),解密时把大于 N/2 的结果当作负数(decrypt_mta_response)。
  • 非交互签名的 R 再随机化(MPC_RAND_R_VERSION)与 low-s 规范化(make_sig_s_positive)。
  • Ed25519 曲线运算(ed25519_algebra.c:33 字节点格式、ed25519_calc_hram)与三方 EdDSA 签名(eddsa_online_signing_service.cpp:承诺 Ri → 公开 Ri → si = c·xi + ki → 合成并验签)。
  • 新增分片组(add_user_request / add_user):旧组每方把 xi 拆成三份加法分片,用 ECIES 加密给新成员;新成员解密求和后跑完整 setup,第 4 轮核对 Σ X′j 等于原钱包公钥。
  • GPG 加密备份:每方一个 JSON,打成 zip 后用 OpenPGP.js 加密(SEIPD + MDC,GnuPG 可直接解密);恢复时解密、解压,逐项核对三份分片后拼回钱包。
  • 比特币 testnet4:RIPEMD-160、bech32 / bech32m(BIP173 / BIP350)、base58check,P2WPKH 地址,BIP143 签名哈希,隔离见证交易序列化(BIP141)、严格 DER(BIP66)与 low-s,选币、找零与粉尘阈值,以及 mempool.space 的 Esplora 接口。
  • EIP-1559 交易的 RLP 编码与签名,Solana legacy 交易消息与 System Program 转账指令,base58 / base64,以及 JSON-RPC 查询、广播和确认。

为保持最小化而省略的部分

  • Paillier-Blum 模数证明 Πmod(论文 Fig.12,paillier_generate_paillier_blum_zkp)和 Paillier 大因子证明(Fig.26,range_proof_paillier_large_factors_zkp_generate)。
  • 预签名中依赖 Paillier 与 Ring-Pedersen 的范围证明:k 的加密范围证明(Fig.24,range_proof_diffie_hellman_zkpok_generate)、γ 的指数证明(range_proof_paillier_exponent_zkpok_generate)、MtA 仿射运算证明 Πaff-g(Fig.25,mta_range_generate_zkp)。缺少它们时协议只对诚实参与方正确,不能抵御恶意参与方。
  • HD 派生(本演示签名路径为空,派生增量为 0)、POSITIVE_R 标志、密钥刷新(Fig.7 的 refresh 部分)、备份、租户与持久化。
  • 三方运行在同一个页面里,消息直接在内存中传递,没有网络与身份认证。
  • 新增分片组时的 encrypt_for_player 由平台实现,源码不规定算法;本演示用 secp256k1 ECIES(SHA-256 派生密钥流与 MAC)代替。新成员的加密密钥只在这一次流程里使用,没有平台身份登记。
  • Ed25519 密钥的 setup 复用了 secp256k1 那组 Paillier 与 Ring-Pedersen 参数(源码对每个 key_id 都重新生成;EdDSA 签名本身不用它们),Πprm 仍按新的 aad 重新生成和验证。
  • 没有实现 mpc-lib 的 asymmetric EdDSA(离线)变体,Solana 只用在线三轮签名。

论文与源码的写法差异

  • 论文 R = Γ,签名用 k̃ = k/δ;源码 R = δ⁻¹·Γ,签名直接用 ki。两者等价。
  • 论文 D = enc(γk − β)、δ += β;源码 D = enc(γk + β)、δ −= β。符号相反,结果相同。
  • 论文 Fig.10 用 Δ̃j、S̃j 逐个检查 σj;源码不计算 Sj,在线模式只检查最终签名。本演示两种模式都会验最终签名。