三个参与方在浏览器里各自运行 CMP 协议:先各自生成私钥分片并组建同一个钱包(公钥 X = X₁ + X₂ + X₃),再通过 Paillier 加密的 MtA 生成随机数 R,最后各自算出签名份额 si,三份相加得到一个标准 ECDSA 签名。任何一方都不掌握完整私钥。所有计算都是真实运算:secp256k1、2048 位 Paillier、1024 位 Ring-Pedersen。同一次密钥生成还会产出一把三方共管的 Ed25519 密钥,「③ 链上转账」用这两把密钥在以太坊 Sepolia(EIP-1559)、Solana Devnet 和比特币 testnet4(P2WPKH)上发起真实转账。
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。
论文 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 承诺,只广播承诺。
论文 Fig.6 Round 2 · cmp_setup_service::store_setup_commitments
收齐三份承诺后,每方计算 ack = SHA256(Σ id ‖ commitment),然后发出解承诺:seed、Xi、Schnorr 的 R、Paillier 公钥、Ring-Pedersen 公钥,以及 ack。
论文 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。
论文 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。
论文 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。
论文 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。
论文 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 轮承诺一致。
论文 Fig.8 Output · store_presigning_data / get_si → calc_R
验证其他方的离散对数证明;δ = Σ δj = k·γ,检查 δ·G = Σ Δj;R = δ⁻¹·Γ = k⁻¹·G。每方保存 (ki, χi, R)。
论文 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) 恢复出公钥,和钱包公钥比对。
论文 Fig.10 Output · ecdsa_offline_signature / get_cmp_signature → make_sig_s_positive
另外演示两点:单独一份 si 不是有效签名;只拿两方份额相加也验不过,因为 CMP 是 3-of-3。
以太坊交易按 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。
先在「① 构建钱包」完成密钥生成,或在「④ 密钥备份与恢复」中恢复钱包,这里才会启用。
领取测试币:Google Cloud Sepolia 水龙头 · pk910 PoW 水龙头。把上面的地址粘贴过去即可。
比特币和以太坊用同一把 secp256k1 MPC 密钥,签名同样走 CMP。领取测试币:mempool.space 水龙头 · faucet.testnet4.dev · coinfaucet.eu。testnet4 大约 10 分钟出一个块。
在当前分片组三片私钥都在的前提下,再分出一组新的三片私钥,交给三个新成员。新组与旧组对应同一个钱包公钥,以太坊、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)。
cmp_setup_service::add_user_request · platform_service::encrypt_for_player
当前签名组的每一方取出自己的 xi,拆成三份加法分片:前两份随机,最后一份 = xi − 前两份。每份用对应新成员的公钥加密,连同钱包公钥一起作为 add_user_data 发出。secp256k1 和 Ed25519 两把密钥各拆一次。
cmp_setup_service::add_user → generate_setup_commitments
新成员核对三方发来的钱包公钥一致、都有给自己的那份,解密后相加得到自己的新分片 x′j。之后与密钥生成第 1 轮相同:生成自己的 Paillier 与 Ring-Pedersen 密钥,用新的 key_id 做承诺并广播。
cmp_setup_service::store_setup_commitments
新组三方之间的 setup 第 2 轮,与「① 构建钱包」第 2 轮完全相同,只是参与方换成了新成员。
cmp_setup_service::generate_setup_proofs
新组的 srid 由新成员的 seed 异或得到;Schnorr 证明的是新分片 x′j,Πprm 证明新成员自己的 Ring-Pedersen 参数。
cmp_setup_service::verify_setup_proofs
验证另外两方的证明。add_user 路径在 metadata 里事先存了原钱包公钥,源码要求 Σ X′j 与它完全一致,否则报 INVALID_PARAMETERS。通过后新分片组加入钱包,可以单独签名,也可以单独备份。
三方的私钥分片可以用 GPG 加密后下载:每方一个 JSON,三个文件打成 zip,再用你的 GPG 公钥加密。恢复时上传加密文件和 GPG 私钥,页面在本地解密,从三个 JSON 中恢复三把私钥分片。所有处理都在浏览器里完成。钱包有多个分片组时,每组单独备份成一个文件。
每方的分片单独存成一个 JSON(只含这一方的 xi、Paillier 私钥和自己那份预签名,加上所有人的公开参数),三个文件打成 zip,再用你的 GPG 公钥加密成 ASCII armor 文件下载。
上传加密的备份文件和对应的 GPG 私钥,页面在本地解密、解压,从三个 JSON 中恢复三把私钥分片,核对无误后放回钱包:与当前钱包是同一把钥匙时只替换或加入这一组,其余分片组保留;否则替换整个钱包。私钥和口令只在本页面内使用,不会发到任何地方。
paillier.c:p ≡ 3 mod 8、q ≡ 7 mod 8、g = N + 1),Ring-Pedersen 1024 位(ring_pedersen.c:安全素数,t = r²,s = tλ)。commitments.c、ack_message),aad = sid ‖ id ‖ srid(build_aad)。schnorr.c)、Ring-Pedersen 参数 Πprm 80 轮(ring_pedersen.c)、Diffie-Hellman 离散对数证明(diffie_hellman_log.c,挑战值用 drng.c 的 SHA-512 确定性随机数生成)。BETA_HIDING_FACTOR = 5),解密时把大于 N/2 的结果当作负数(decrypt_mta_response)。MPC_RAND_R_VERSION)与 low-s 规范化(make_sig_s_positive)。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 等于原钱包公钥。paillier_generate_paillier_blum_zkp)和 Paillier 大因子证明(Fig.26,range_proof_paillier_large_factors_zkp_generate)。range_proof_diffie_hellman_zkpok_generate)、γ 的指数证明(range_proof_paillier_exponent_zkpok_generate)、MtA 仿射运算证明 Πaff-g(Fig.25,mta_range_generate_zkp)。缺少它们时协议只对诚实参与方正确,不能抵御恶意参与方。encrypt_for_player 由平台实现,源码不规定算法;本演示用 secp256k1 ECIES(SHA-256 派生密钥流与 MAC)代替。新成员的加密密钥只在这一次流程里使用,没有平台身份登记。