首页
/ Bitcoin Core 内置 libsecp256k1 的 MuSig2 多签模块 API 深入解析:密钥聚合、Taproot 调整与防误用设计

Bitcoin Core 内置 libsecp256k1 的 MuSig2 多签模块 API 深入解析:密钥聚合、Taproot 调整与防误用设计

2026-09-07 21:44:58作者:邵娇湘

MuSig2 是基于 BIP 327 的、面向 BIP 340 Schnorr 签名的多方聚合签名协议,本仓库内嵌的 libsecp256k1(src/secp256k1)提供了其完整实现,且该模块同时支持 BIP 341 Taproot 公钥调整。本文以 src/secp256k1/doc/musig.md 为骨架,结合 secp256k1_musig.hmusig.c 示例与底层 keyagg/session 实现代码,系统讲解公钥聚合、两种 tweak 方式、两轮交互签名全流程与"防误用"设计红线。读完本文,你将能够正确调用这套 API 完成 n-of-n 多签签名,理解何种操作会泄漏私钥,并掌握仅验证不签名(旁观者)流程与 Bitcoin Core 侧钱包/PSBT 对 MuSig2 的封装用法。

一、模块定位与配套阅读资料

doc/musig.md 是 libsecp256k1 MuSig 模块 API(定义于 secp256k1_musig.h)的补充说明文档。根据头文件顶部注释:

  • 该模块实现 BIP 327「MuSig2 for BIP340-compatible Multi-Signatures」v1.0.0
  • 同时支持 BIP 341(Taproot)公钥调整
  • 由于第一版 MuSig 基本已被 MuSig2 取代,头文件与文档中 MuSig、musig 与 MuSig2 同义使用;
  • 文档明确建议使用者"认真通读头文件中的说明",本笔记(doc/musig.md)只收录头文件之外的补充注意事项;
  • 一个可直接运行的完整用法示例位于 examples/musig.c(演示 3-of-3 多签),另见 examples/CMakeLists.txt

从构建配置看,该模块是默认开启的可选模块:CMakeLists.txtoption(SECP256K1_ENABLE_MODULE_MUSIG "Enable musig module." ON),且模块实现会随 secp256k1.cmain_impl.h 编入(见 secp256k1.c#include "modules/musig/main_impl.h")。

不透明数据结构速查

MuSig API 大量使用"不透明结构体",其内部表示是实现定义的、不保证跨平台/跨版本可移植。除 secp256k1_musig_secnonce 外,其余结构可安全拷贝/移动;若需存储、传输或比较,必须使用配套的序列化/解析函数。各结构体尺寸与序列化长度如下表(尺寸来自 secp256k1_musig.h):

结构体 内存尺寸 序列化长度 序列化/解析函数 能否拷贝/传输
secp256k1_musig_keyagg_cache 197 字节 —(暂无序列化函数) 可拷贝(内部已带 magic 检测)
secp256k1_musig_secnonce 132 字节 禁止序列化 绝对不能拷贝
secp256k1_musig_pubnonce 132 字节 66 字节 musig_pubnonce_serialize/parse 可(发送给其他签名者)
secp256k1_musig_aggnonce 132 字节 66 字节 musig_aggnonce_serialize/parse
secp256k1_musig_session 133 字节 —(暂无序列化函数) 可拷贝(无需保密)
secp256k1_musig_partial_sig 36 字节 32 字节 musig_partial_sig_serialize/parse

二、API 误用防护:三条"红线"必须遵守

MuSig 的 API 设计以抗误用为核心目标。但与单方 Schnorr 签名相比,由于 MuSig 协议具有交互性,存在普通签名流程中不存在的额外失败模式。一旦触发,后果可能是灾难性的(例如泄露私钥),而 MuSig 实现本身无法阻止所有此类误用。因此模块使用者必须对以下三点格外小心(原文三点逐条展开如下,并辅以源码佐证):

红线一:每次签名会话必须生成唯一 nonce

secp256k1_musig_nonce_gen 中的 session_secrand32 必须为每次调用唯一,并且是 32 字节均匀随机数;一旦调用成功,该缓冲区会被置零作废,以防复用。若没有好的随机源但有不会重复的计数器,可使用 secp256k1_musig_nonce_gen_counter(以 uint64_t 计数器代替随机数,前提是必须提供 keypair,且同一个 keypair 与同一个计数器值绝不能重复使用两次——这意味着若同一 keypair 在多台设备上使用,各设备的计数器不能冲突)。

从实现看,nonce_gen 内部保存的 secnonce 带有固定的 4 字节 magic 0x22,0x0e,0xdc,0xf1,并保存两个标量 k0/k1 与对应公钥(见 session_impl.hsecp256k1_musig_secnonce_save)。头文件与 musig.c 都以醒目的注释强调:如果复用 session_secrand32(等同于复用 nonce),攻击者可以轻松提取私钥!

/* 创建随机 session ID。对于每次 secp256k1_musig_nonce_gen 调用,
 * 该 ID 必须是唯一的。否则攻击者可轻易提取私钥! */
unsigned char session_secrand[32];
if (!fill_random(session_secrand, sizeof(session_secrand))) return 0;
/* 可选地提前绑定:seckey、pubkey、msg32、keyagg_cache、extra_input32 */
if (!secp256k1_musig_nonce_gen(ctx, &secrets[i].secnonce,
        &signers[i].pubnonce, session_secrand, seckey, &signers[i].pubkey,
        msg32, /*keyagg_cache=*/NULL, /*extra_input32=*/NULL)) {
    return 0;
}

注意:nonce_gen 要求传入的是普通 context 而非 secp256k1_context_staticseckeymsg32keyagg_cacheextra_input32 在 nonce 生成时若已知均可选填,用于派生 nonce 并提高抗误用性

红线二:secp256k1_musig_secnonce 绝不拷贝、绝不序列化

非交互签名失败模式的一个典型来源就是 nonce 重用。库内最坏情况下的防误用机制是:secp256k1_musig_partial_sign 在成功签名后将传入的 secnonce 全部清零,并在收到一个全零 secnonce 时直接中止(ARG_CHECK),见 session_impl.hsecp256k1_musig_secnonce_invalidatepartial_sign 实现。但正如头文件警告:一旦 secnonce 被拷贝或序列化,这个清零保护就被轻易绕过(另一份拷贝仍可再次签名),进而泄漏私钥。所以正确做法是:

  • 签名者应全程在线,把 secnonce 只保留在内存中,只通过本 API 提供的函数读写;
  • Bitcoin Core 侧在 src/musig.h 中为此专门定义了不可拷贝类 MuSig2SecNonce:其拷贝构造/赋值被 = delete,用 std::unique_ptr 持有底层 secp256k1_musig_secnonce,只以指针 Get() 向外暴露引用,从语言层面杜绝拷贝导致 nonce 复用——这正是对"红线二"的一种工程化落地。

红线三:不透明结构体严禁直接读写

只能通过官方提供的访问函数操作不透明结构体。例如 pubnonce/aggnonce 需要序列化传输时必须使用 musig_pubnonce_serializemusig_pubnonce_parsemusig_aggnonce_serializemusig_aggnonce_parse;partial signature 使用 musig_partial_sig_serialize/parse。直接读写内部字节既不保证跨版本兼容,也容易踩坏 magic/长度校验导致难以排查的错误。

三、密钥聚合与(Taproot)公钥调整

3.1 计算聚合公钥

给定一组参与方公钥,调用 secp256k1_musig_pubkey_agg(ctx, agg_pk, keyagg_cache, pubkeys, n_pubkeys) 计算聚合公钥,其中:

  • agg_pk:输出聚合后的 x-only 公钥,不需要时可以传 NULL
  • keyagg_cache:若为非 NULL,则初始化一个聚合缓存,签名(或旁观验证部分签名)必须用到它;只聚合不签名时可不提供缓存;
  • pubkeys:公钥数组。顺序非常重要——不同顺序会得到不同的聚合公钥!为了对同一公钥集合得到确定结果,可先用 secp256k1_ec_pubkey_sort 排序再聚合。

从底层实现 keyagg_impl.h 可以看到 keyagg_cache 的 197 字节内部布局:4 字节初始化 magic 0xf4,0xad,0xbb,0xdf + 64 字节聚合(可能已 tweak)公钥 + 64 字节 "second" 公钥 + 32 字节公钥集合哈希 pks_hash + 1 字节内部公钥奇偶位 + 32 字节 tweak。聚合算法先以固定 midstate(对应 SHA256("KeyAgg list")||SHA256("KeyAgg list"))算出所有公钥的 pks_hash,再为每个公钥计算 KeyAgg 系数 keyaggcoef:除 "second" 公钥的系数固定为 1 外,其余均为 tagged_hash(pks_hash, pk)(固定 midstate 对应 SHA256("KeyAgg coefficient")||...),最终聚合公钥通过批量 EC 多点乘 keyaggcoef_0·P_0 + keyaggcoef_1·P_1 + ... 计算得到。这种哈希系数机制(区别于简单地把公钥直接相加)是 MuSig2 抵御 Rogue Key 攻击的核心。

聚合后如需要完整(非 x-only)公钥,可用 secp256k1_musig_pubkey_get(ctx, agg_pk, keyagg_cache) 从缓存取回,它主要用于普通(非 x-only)tweak 或(未来可能的)多密钥聚合批量验证场景。

3.2 两种 tweak 方式及其语义差异

文档指出,聚合公钥可再叠加 tweak,且两种 tweak 可以任意组合、并可多次调用(若应用确实需要):

  • 普通(plain)tweak:语义上等于对公钥加上"生成元 × tweak32",即 secp256k1_ec_pubkey_tweak_add 的行为,常用于按 BIP 32 从聚合公钥派生子密钥,此时 tweak32 取 BIP 32 定义的哈希;
  • Taproot(x-only)tweak:语义上等于 secp256k1_xonly_pubkey_tweak_add 的行为,用于构造 Taproot 输出,此时 tweak32 取 BIP 341 定义的 TapTweak 哈希。

这里存在一个容易踩坑的细节(头文件对此有等价性证明伪代码):如果只是计算公钥而不打算用它签名,直接用通用函数 secp256k1_ec_pubkey_tweak_add / secp256k1_xonly_pubkey_tweak_add 即可;但如果要为 tweak 后的聚合密钥签名,就必须使用模块内感知缓存的版本:

int secp256k1_musig_pubkey_ec_tweak_add(
    const secp256k1_context *ctx,
    secp256k1_pubkey *output_pubkey,      /* 不需要可传 NULL */
    secp256k1_musig_keyagg_cache *keyagg_cache,  /* 原地更新缓存 */
    const unsigned char *tweak32);

int secp256k1_musig_pubkey_xonly_tweak_add(
    const secp256k1_context *ctx,
    secp256k1_pubkey *output_pubkey,
    secp256k1_musig_keyagg_cache *keyagg_cache,
    const unsigned char *tweak32);

两者会把 tweak 写回 keyagg_cache(即更新缓存内"聚合公钥"与"tweak"字段),使后续 partial_sign 能够针对"已被 tweak 的聚合密钥"签名。tweak32 的合法性条件是能通过 secp256k1_ec_seckey_verify 且不等于缓存对应私钥或其负元;对均匀随机的 32 字节数组,非法概率可忽略(约 1/2^128)。调用方有责任以不削弱 MuSig 安全性的方式派生 tweak(例如遵循 BIP 32 / BIP 341)。

文档中提示的另一条路径是:不需要签名时,可先 secp256k1_musig_pubkey_get 拿全量公钥,再走 secp256k1_ec_pubkey_tweak_add / secp256k1_xonly_pubkey_tweak_add(x-only 场景可能需先用 secp256k1_xonly_pubkey_from_pubkey 转换),两者最终得到的公钥与缓存感知版本一致。

四、签名全流程:从密钥生成到聚合签名验证

签名流程由 musig.c 完整覆盖,本质上按以下 9 步推进(这是 doc/musig.md 的核心脉络,必须逐条遵循):

  1. secp256k1_keypair_create 生成密钥对,并用 secp256k1_keypair_pub 取得公钥;
  2. 用所有参与方的公钥调用 secp256k1_musig_pubkey_agg
  3. 可选地用 secp256k1_musig_pubkey_xonly_tweak_add 添加 Taproot tweak、用 secp256k1_musig_pubkey_ec_tweak_add 添加普通 tweak;
  4. secp256k1_musig_nonce_gen 生成一对秘密/公开 nonce,并把公开 nonce 发送给其他签名者;
  5. 由某方(不一定是签名者本人)用 secp256k1_musig_nonce_agg 聚合各公开 nonce,再把聚合 nonce 发回各签名者;
  6. 各签名者用 secp256k1_musig_nonce_process 处理聚合 nonce(创建会话 session,绑定消息与聚合密钥缓存);
  7. secp256k1_musig_partial_sign 生成部分签名;
  8. secp256k1_musig_partial_sig_verify 验证部分签名(某些场景下可选);
  9. 由某方(不一定是签名者)收集全部部分签名,用 secp256k1_musig_partial_sig_agg 聚合成最终 Schnorr 签名。

最终聚合签名可交给 secp256k1_schnorrsig_verify 用(tweak 后的)x-only 聚合公钥验证。示例中主流程概要如下(完整可编译代码见 musig.c):

#define N_SIGNERS 3
struct signer_secrets { secp256k1_keypair keypair; secp256k1_musig_secnonce secnonce; };
struct signer { secp256k1_pubkey pubkey; secp256k1_musig_pubnonce pubnonce; secp256k1_musig_partial_sig partial_sig; };

/* 1) 生成密钥对并取公钥 */
secp256k1_keypair_create(ctx, &secrets[i].keypair, seckey);
secp256k1_keypair_pub(ctx, &signers[i].pubkey, &secrets[i].keypair);

/* 2) 聚合前先排序,使聚合公钥与签名者顺序无关 */
secp256k1_ec_pubkey_sort(ctx, pubkeys_ptr, N_SIGNERS);
/* 只聚合不签名时可把 cache 传 NULL;要签名则必须保留 cache */
secp256k1_musig_pubkey_agg(ctx, NULL, &cache, pubkeys_ptr, N_SIGNERS);

/* 3) 可选 tweak:普通 tweak + x-only tweak,最终转出 xonly 公钥供验签 */
secp256k1_musig_pubkey_ec_tweak_add(ctx, NULL, &cache, plain_tweak);
secp256k1_musig_pubkey_xonly_tweak_add(ctx, &output_pk, &cache, xonly_tweak);
secp256k1_xonly_pubkey_from_pubkey(ctx, &agg_pk, NULL, &output_pk);

/* 4) 每个签名者生成 nonce,公开 nonce 进 pubnonces[i](见红线一代码) */

/* 轮次 1:协调者收集全部 pubnonce 后聚合 */
secp256k1_musig_nonce_agg(ctx, &agg_pubnonce, pubnonces, N_SIGNERS);

/* 5-7) 每个签名者用同一聚合 nonce 创建 session 并部分签名 */
secp256k1_musig_nonce_process(ctx, &session, &agg_pubnonce, msg32, &cache);
secp256k1_musig_partial_sign(ctx, &signers[i].partial_sig,
        &secrets[i].secnonce, &secrets[i].keypair, &cache, &session);

/* 8) 轮次 2:协调者逐人验证部分签名 */
secp256k1_musig_partial_sig_verify(ctx, &signers[i].partial_sig,
        &signers[i].pubnonce, &signers[i].pubkey, &cache, &session);

/* 9) 聚合部分签名为最终 64 字节 Schnorr 签名,并用聚合公钥验证 */
secp256k1_musig_partial_sig_agg(ctx, sig, &session, partial_sigs, N_SIGNERS);
secp256k1_schnorrsig_verify(ctx, sig, msg, 32, &agg_pk);

4.1 流程关键语义与安全提示

  • 第 1~5 步既可在消息已知前进行,也可在其后进行。文档强烈建议:只要可能,就在消息已知后再生成 nonce。这提供了额外的纵深防御,能抵御特定场景下潜在的 API 误用;代价是签名过程需要两轮通信。相反,在消息未知的预处理阶段提前生成 nonce,会失去这些保护,但可实现非交互式签名(例如把 nonce 预先分发给各签名者)。
  • API 还支持另一种协议流:先交换 nonce(第 4~5 步)再生成聚合密钥(第 1~3 步)
  • nonce_agg 可由不受信任的第三方执行:它专门用于减少签名者间的通信量——不必人人互发 nonce,而是由一方收集、聚合后只回传聚合 nonce。若聚合者算错,最终签名必然无效(协议自行发现,不构成安全问题)。
  • partial_sign 有两条重要实现事实(见 secp256k1_musig.h):其一,传入的 secnonce 必须是由与该 keypair 对应公钥调用 nonce_gen 产生的,否则触发 illegal callback;其二,partial_sign 不会自行验证输出(这点与 BIP 327 规范建议不同),因此文档与头文件都推荐调用后用 partial_sig_verify 校验输出,以拦截随机性或对抗性诱发的计算错误。
  • 同一个密钥对参与多个不同 MuSig 会话是安全的;真正危险的是同一次会话中 nonce 复用。

4.2 序列化与解析函数用法

需要网络传输的三类对象各有配套函数,均要求传 66/32 字节缓冲区:

int secp256k1_musig_pubnonce_serialize(ctx, unsigned char *out66, const secp256k1_musig_pubnonce *nonce);
int secp256k1_musig_pubnonce_parse  (ctx, secp256k1_musig_pubnonce *nonce, const unsigned char *in66);
int secp256k1_musig_aggnonce_serialize(ctx, unsigned char *out66, const secp256k1_musig_aggnonce *nonce);
int secp256k1_musig_aggnonce_parse  (ctx, secp256k1_musig_aggnonce *nonce, const unsigned char *in66);
int secp256k1_musig_partial_sig_serialize(ctx, unsigned char *out32, const secp256k1_musig_partial_sig *sig);
int secp256k1_musig_partial_sig_parse  (ctx, secp256k1_musig_partial_sig *sig, const unsigned char *in32);

序列化函数恒返回 1;解析函数解析失败返回 0(带 SECP256K1_WARN_UNUSED_RESULT,必须检查返回值)。

五、部分签名验证:只验证、不签名的参与者

希望验证各签名者的部分签名、但自己并不参与签名的"旁观者",可按上面同一套说明操作,区别仅在:旁观者跳过第 1、4、7 步(即不生成自己的密钥对、不生成 nonce、不产生部分签名)。旁观者同样需要拿到 keyagg_cache、聚合 nonce 处理出的 session、以及每个签名者当时发送的 pubnoncepubkey

partial_sig_verify 的调用需要满足三条一致性约束(见 secp256k1_musig.h):

  1. 传入的 keyagg_cache 必须与当初用 musig_nonce_process 创建 session 时所用的完全一致;
  2. pubkey 必须与该签名者在 musig_pubkey_agg 聚合成 keyagg_cache 前发送的一致;
  3. pubnonce 必须与该签名者在 musig_nonce_agg 前发送的一致。

需要强调的是:常规 MuSig 会话中并不强制调用此函数——因为只要任一签名者的部分签名无效,最终聚合签名也必然无法通过 schnorrsig_verify,问题终会被发现。调用 partial_sig_verify 的价值在于可以精确定位到底是哪一份部分签名无效(从而判断是哪一位签名者/哪一次传输导致协议失败)。也可以先验证聚合签名、失败后再逐人验证以缩小排查范围。

六、实现级细节印证与测试保障

  • secnonce 结构与作废机制:secnonce 内存布局为 4 字节 magic 0x22,0x0e,0xdc,0xf1 + 两个 32 字节 nonce 标量 k0/k1 + 64 字节对应公钥(session_impl.h)。partial_sign 成功后调用常数时间的 secp256k1_memczero 把整块内存清零(flag 为 1),并以 ARG_CHECK 拒绝再次使用已清零的 nonce。库还会对 session_secrand32 输入缓冲原位清零,从源头阻止同一随机数再次喂给 nonce_gen
  • 抗 Rogue Key 的 KeyAgg 系数:见 keyagg_impl.h,使用带固定 SHA256 midstate 的 tagged hash 分别计算公钥集合哈希("KeyAgg list")与逐公钥系数("KeyAgg coefficient"),"second" 公钥系数恒为 1,聚合通过批量标量乘实现。
  • 测试向量与一致性:模块自带 BIP MuSig2 测试向量(vectors.h),由 tools/test_vectors_musig2_generate.pykey_agg_vectors.jsonnonce_gen_vectors.jsonnonce_agg_vectors.jsonsign_verify_vectors.json 等官方向量转成 C 结构供测试框架断言;行为测试见 tests_impl.h,另有常数时间(ctime)相关检查(ctime_tests.c 亦引用该模块)。

七、Bitcoin Core 对 MuSig2 的工程化封装

在 Bitcoin Core 这一侧,本仓库并不只是"含一份第三方源码",而是已经围绕 MuSig2 建立了自己的封装层,读者可顺着这些文件继续深入:

  • src/musig.hsrc/musig.cpp:提供 MuSig2AggregatePubkeys(按当前顺序聚合公钥并校验聚合结果是否匹配期望值)、CreateMuSig2NonceCreateMuSig2PartialSigCreateMuSig2AggregateSigMuSig2SessionID(以 SHA256 绑定聚合公钥/参与方公钥/sighash/pubnonce 生成会话标识)等高层封装;
  • 不可拷贝的 MuSig2SecNoncesrc/musig.h):如前所述,通过删除拷贝构造/赋值并把底层对象放在 unique_ptr 中,把"secnonce 永不复制"从文档告诫落实为编译器强制约束,并提供 Invalidate()/IsValid() 管理其生命周期;
  • 该封装被脚本/描述符签名链路使用,涉及 src/script/descriptor.cppsrc/script/sign.cppsrc/script/signingprovider.cppsrc/psbt.cpp 与钱包侧 src/wallet/scriptpubkeyman.cpp,并有专门测试 src/test/bip328_tests.cpp 覆盖——从仓库结构可以看出,MuSig2 已被整合进描述符、PSBT 与钱包的多签工作流。

结语:把"防误用"刻进代码习惯

MuSig2 的公开 API 数量不多,但真正危险的地方不在密码学本身,而在交互协议引入的唯一 nonce 约束。回归 doc/musig.md 的核心提醒:每次会话生成唯一 nonce(红线一)、secnonce 永不拷贝/序列化(红线二)、不透明结构只用访问函数(红线三);能推迟到消息已知再生成 nonce 就尽量推迟;部分签名无论是否在常规流程中验证,都应保证最终聚合签名经受住 secp256k1_schnorrsig_verify 的检验。把这三条红线与本文给出的流程、缓存与序列化细节落实到位,即可安全地在基于 BIP 340/BIP 341 的系统上构建 n-of-n 聚合签名应用。

延伸阅读:头文件全文 secp256k1_musig.h、完整示例 musig.c、模块实现 keyagg_impl.hsession_impl.h,以及 Bitcoin Core 封装 src/musig.h 与测试 src/test/bip328_tests.cpp

登录后查看全文
热门项目推荐
相关项目推荐