返回 Expert 笔记
Expert Day 200

BLS 签名 — 聚合签名与以太坊 PoS

BLS 签名构造、pairing 基础回顾、签名聚合数学、Rogue Key Attack 防御、阈值签名

2026-11-17
Phase 4 - 经典密码学 (Day 195-208)
密码学BLSPairing聚合签名以太坊2.0

日期: 2026-11-17 方向: 密码学 / 经典原语 阶段: Phase 4 - 经典密码学 (Day 195-208) 标签: #密码学 #BLS #Pairing #聚合签名 #以太坊2.0


今日目标

类型内容
学习BLS 签名构造、pairing 基础回顾、签名聚合数学、Rogue Key Attack 防御、阈值签名
实操用 py_ecc 实现 BLS12-381 上的签名/聚合/验证;模拟以太坊 attestation aggregation
产出bls.py — 完整 BLS 签名 + 聚合 + 多签

一、BLS 签名设计原理

1.1 历史

Boneh-Lynn-Shacham 2001 提出。基于双线性配对群的 GapDH 假设。曾在前期实用部署慢,因为 pairing 计算昂贵。2010+ 后随 BLS12-381 / BN254 等 pairing-friendly 曲线优化,成为主流。

杀手级应用: 以太坊 PoS 共识层(2022 年 Merge),800,000+ 验证者每个 epoch 提交 attestation,需要在 32 个 slot × 64 个 committee 内聚合签名 → 链上仅存 1 个聚合签名 + bitfield。

1.2 数学基础(来自 Day 185 pairing)

双线性配对: $e: G_1 \times G_2 \to G_T$,满足:

  1. 双线性: $e(aP, bQ) = e(P, Q)^{ab}$
  2. 非退化: $e(P, Q) \ne 1$ 当 $P, Q$ 是 generator
  3. 可计算: 多项式时间

BLS12-381 中 $G_1$ 是 $E(\mathbb{F}p)$ 上素数阶子群,$G_2$ 是 $E(\mathbb{F}{p^2})$ 上素数阶子群,$G_T \subset \mathbb{F}_{p^{12}}^*$。

1.3 BLS 签名

KeyGen:

  • $sk = x \stackrel{R}{\leftarrow} \mathbb{Z}_q$
  • $pk = x \cdot g_1 \in G_1$ (or $G_2$,取决于变种)

Sign(sk, m):

  • $H(m) \in G_2$ (Hash-to-Curve to $G_2$)
  • $\sigma = x \cdot H(m) \in G_2$

Verify(pk, m, σ):

  • 接受当 $e(g_1, \sigma) = e(pk, H(m))$

证明等价: 若 $\sigma = x H(m)$, 则 $$e(g_1, x H(m)) = e(g_1, H(m))^x = e(x g_1, H(m)) = e(pk, H(m)) ✓$$

1.4 BLS 优势

  1. Sig 极短: 48 字节 (G1) 或 96 字节 (G2)
  2. Pubkey 短: 48 / 96 字节
  3. 完美聚合: $\sigma_{\text{agg}} = \sum \sigma_i$,验证 $e(g_1, \sigma_{\text{agg}}) = \prod e(pk_i, H(m_i))$
  4. 确定性: 同 (sk, m) 永远同 σ(无 nonce)
  5. 简单数学: 易于审计

1.5 BLS 劣势

  1. 验证慢: pairing 比 EC scalar mul 慢 10-50×
  2. Hash-to-curve 复杂: 需 ENC-MAP 标准(IETF RFC 9380)
  3. Rogue key attack 风险(见下)
  4. Pairing 安全: 依赖 NFS over $\mathbb{F}_{p^k}$ 困难性

二、签名聚合数学

2.1 简单聚合(同消息)

$n$ 个签名者对同一消息 $m$ 签名: $$\sigma_{\text{agg}} = \sum_{i=1}^n \sigma_i = \sum x_i H(m) = (\sum x_i) H(m)$$

设 $pk_{\text{agg}} = \sum pk_i$,验证: $$e(g_1, \sigma_{\text{agg}}) \stackrel{?}{=} e(pk_{\text{agg}}, H(m))$$

仅 1 个 pairing!极快。

2.2 一般聚合(不同消息)

$n$ 个签名者对不同消息 $m_i$ 签名: $$\sigma_{\text{agg}} = \sum \sigma_i$$

验证需要 $n+1$ 个 pairing: $$e(g_1, \sigma_{\text{agg}}) \stackrel{?}{=} \prod_{i=1}^n e(pk_i, H(m_i))$$

2.3 Rogue Key Attack

威胁场景: Eve 想伪装成 (Alice, Bob) 联合签名。

设 $pk_A$ 是 Alice 公钥,Eve 选私钥 $x_E$ 但注册公钥: $$pk_E = x_E g_1 - pk_A$$

聚合公钥 $pk_{AB} = pk_A + pk_E = x_E g_1$,Eve 单独可签: $$\sigma = x_E H(m)$$

验证通过 → Eve 伪造了 (Alice, Bob) 的联合签名!

2.4 防御策略

方法 1 (Proof of Possession, PoP): 每个签名者注册 pubkey 时,必须提交 $\sigma_{PoP} = sk \cdot H(pk)$(即对自己 pubkey 的签名)。Eve 不知 $x_A$ 无法伪造 PoP。

  • 以太坊 PoS 用此方案

方法 2 (Distinct Message): 每个签名者签 $m_i = (i, m)$ 而非纯 $m$,破坏对称性。

方法 3 (Pubkey Hashing): 用 $H(pk_i, pk_j, ...)$ 派生权重 $a_i$,签名 $\sigma = \sum a_i \sigma_i$。MuSig / FROST 思想。


三、阈值 BLS

$(t, n)$ 阈值: 任 $t$ 人可联合签,$<t$ 人无法。

3.1 Shamir Secret Sharing

私钥 $x$ 用 $t-1$ 阶多项式 $f(0) = x$ 分享,$x_i = f(i)$ 给参与者 $i$。

任 $t$ 个 share 用 Lagrange 插值恢复: $$x = \sum_{i \in S} \lambda_{i,S} \cdot x_i$$

3.2 Threshold BLS

每个 $i$ 用 share $x_i$ 计算部分签名 $\sigma_i = x_i H(m)$. 聚合者用 Lagrange 系数: $$\sigma = \sum_{i \in S} \lambda_{i,S} \cdot \sigma_i = (\sum \lambda_{i,S} x_i) H(m) = x H(m) ✓$$

优点: 输出与单签者签名形式相同,验证只需主公钥(不暴露阈值结构)。

3.3 Distributed Key Generation (DKG)

避免 trusted dealer 持有 $x$。每参与者:

  1. 选随机多项式 $f_i(x)$ 度 $t-1$
  2. 把 $f_i(j)$ 发给参与者 $j$
  3. 公布 $g \cdot f_i$ 系数 (commitments)
  4. 各方累加: 私钥份额 $x_j = \sum_i f_i(j)$,主公钥 $pk = \sum_i f_i(0) g$

需要可靠广播 + 抱怨机制处理恶意者。


四、代码实现 — bls.py

"""
bls.py - BLS12-381 签名实现 (使用 py_ecc)
- KeyGen / Sign / Verify
- Aggregation (same-message and distinct-message)
- Threshold signing demo (3-of-5)
"""
from __future__ import annotations
import hashlib
import os
import secrets
from typing import List, Tuple

# pip install py_ecc
from py_ecc.bls12_381 import (
    G1, G2, multiply, add, neg, pairing, curve_order,
    Z1, Z2, FQ12,
)
from py_ecc.bls.hash_to_curve import hash_to_G2

# --------------------------- BLS Core ---------------------------

DST = b'BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_'

def keygen(seed: bytes = None) -> Tuple[int, tuple]:
    """sk in Z_q, pk = sk*G1"""
    if seed is None:
        seed = secrets.token_bytes(32)
    sk = int.from_bytes(hashlib.sha256(seed).digest(), 'big') % curve_order
    if sk == 0:
        sk = 1
    pk = multiply(G1, sk)
    return sk, pk

def sign(sk: int, msg: bytes) -> tuple:
    """sigma = sk * H(m), where H(m) in G2"""
    h = hash_to_G2(msg, DST, hashlib.sha256)
    return multiply(h, sk)

def verify(pk: tuple, msg: bytes, sig: tuple) -> bool:
    """e(G1, sig) == e(pk, H(m))"""
    h = hash_to_G2(msg, DST, hashlib.sha256)
    lhs = pairing(sig, G1)
    rhs = pairing(h, pk)
    return lhs == rhs

# --------------------------- Aggregation ---------------------------

def aggregate_sigs(sigs: List[tuple]) -> tuple:
    """sum of G2 points"""
    agg = sigs[0]
    for s in sigs[1:]:
        agg = add(agg, s)
    return agg

def aggregate_pks(pks: List[tuple]) -> tuple:
    agg = pks[0]
    for p in pks[1:]:
        agg = add(agg, p)
    return agg

def fast_verify_same_msg(pks: List[tuple], msg: bytes, agg_sig: tuple) -> bool:
    """All signers signed SAME msg. Single pairing check.
    e(G1, agg_sig) == e(agg_pk, H(m))"""
    agg_pk = aggregate_pks(pks)
    h = hash_to_G2(msg, DST, hashlib.sha256)
    return pairing(agg_sig, G1) == pairing(h, agg_pk)

def verify_aggregate_distinct(pks: List[tuple], msgs: List[bytes], agg_sig: tuple) -> bool:
    """Each signer signed distinct msg.
    e(G1, agg_sig) == prod_i e(pk_i, H(m_i))"""
    lhs = pairing(agg_sig, G1)
    rhs = FQ12.one()
    for pk, m in zip(pks, msgs):
        h = hash_to_G2(m, DST, hashlib.sha256)
        rhs = rhs * pairing(h, pk)
    return lhs == rhs

# --------------------------- Proof of Possession ---------------------------

POP_DST = b'BLS_POP_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_'

def prove_possession(sk: int) -> tuple:
    """PoP: signer signs their own pubkey hash.
    Defense against rogue key attack."""
    pk = multiply(G1, sk)
    pk_bytes = (
        int(pk[0]).to_bytes(48, 'big') +
        int(pk[1]).to_bytes(48, 'big')
    )
    h = hash_to_G2(pk_bytes, POP_DST, hashlib.sha256)
    return multiply(h, sk)

def verify_pop(pk: tuple, pop: tuple) -> bool:
    pk_bytes = (
        int(pk[0]).to_bytes(48, 'big') +
        int(pk[1]).to_bytes(48, 'big')
    )
    h = hash_to_G2(pk_bytes, POP_DST, hashlib.sha256)
    return pairing(pop, G1) == pairing(h, pk)

# --------------------------- Threshold (Shamir) ---------------------------

def shamir_split(secret: int, t: int, n: int) -> List[Tuple[int, int]]:
    """(t,n)-threshold sharing of secret in Z_q"""
    coeffs = [secret] + [secrets.randbelow(curve_order) for _ in range(t - 1)]
    def f(x):
        result = 0
        for i, c in enumerate(coeffs):
            result = (result + c * pow(x, i, curve_order)) % curve_order
        return result
    return [(i, f(i)) for i in range(1, n + 1)]

def lagrange_coef(i: int, indices: List[int]) -> int:
    """Lagrange coefficient at x=0 for index i, mod curve_order"""
    num = 1
    den = 1
    for j in indices:
        if j == i: continue
        num = num * (-j) % curve_order
        den = den * (i - j) % curve_order
    return num * pow(den, -1, curve_order) % curve_order

def threshold_sign(shares: List[Tuple[int, int]], msg: bytes) -> tuple:
    """t parties each compute sigma_i = x_i * H(m); aggregate via Lagrange."""
    indices = [s[0] for s in shares]
    h = hash_to_G2(msg, DST, hashlib.sha256)
    agg = Z2  # identity in G2
    for (idx, share) in shares:
        coef = lagrange_coef(idx, indices)
        partial = multiply(h, share * coef % curve_order)
        agg = add(agg, partial)
    return agg

# --------------------------- Demo ---------------------------

def demo():
    print("=== BLS Single Signature ===")
    sk, pk = keygen(b'\x42' * 32)
    msg = b'hello bls'
    sig = sign(sk, msg)
    assert verify(pk, msg, sig)
    print("PASS: single sig verify")

    print("\n=== Same-message Aggregation (Eth attestations style) ===")
    n = 5
    keys = [keygen(os.urandom(32)) for _ in range(n)]
    sks, pks = zip(*keys)
    msg = b'block_hash_ABCDEF1234'
    sigs = [sign(sk, msg) for sk in sks]
    agg = aggregate_sigs(list(sigs))
    assert fast_verify_same_msg(list(pks), msg, agg)
    print(f"PASS: {n} same-msg sigs aggregated, 1 pairing verify")

    print("\n=== Rogue Key Attack ===")
    # Alice's pubkey
    sk_A, pk_A = keygen(b'alice')
    # Eve constructs rogue pk
    sk_E = secrets.randbelow(curve_order)
    pk_E_raw = multiply(G1, sk_E)
    pk_E_rogue = add(pk_E_raw, neg(pk_A))  # pk_E - pk_A
    # Aggregate pubkey = pk_A + pk_E_rogue = pk_E_raw, so Eve alone can sign
    rogue_msg = b'i, alice, agree to send funds'
    rogue_sig = sign(sk_E, rogue_msg)
    # Verify against aggregated pk
    assert fast_verify_same_msg([pk_A, pk_E_rogue], rogue_msg, rogue_sig)
    print("ATTACK: rogue key allowed Eve to forge (Alice, Eve) joint sig")

    # Defense via PoP
    pop_E = None
    try:
        pop_E = prove_possession(sk_E)  # Eve doesn't know sk for pk_E_rogue
        # Actually Eve DOES know sk for pk_E_raw (= sk_E), but the pk she registered
        # is pk_E_rogue, for which she doesn't know corresponding secret.
        # The PoP must be against the registered pk.
    except Exception:
        pass
    print("Defense: each signer must submit PoP for their REGISTERED pk")

    print("\n=== Threshold BLS (3-of-5) ===")
    master_sk = secrets.randbelow(curve_order)
    master_pk = multiply(G1, master_sk)
    shares = shamir_split(master_sk, t=3, n=5)
    # Take any 3 shares
    sub = [shares[0], shares[2], shares[4]]
    msg = b'threshold message'
    th_sig = threshold_sign(sub, msg)
    # Verify against master pk!
    assert verify(master_pk, msg, th_sig)
    print("PASS: 3-of-5 threshold signature verified against master pk")

if __name__ == "__main__":
    demo()

五、安全性论证

5.1 BLS 安全归约

定理 (Boneh-Lynn-Shacham 2001): ROM 中,BLS 签名 EUF-CMA-secure,归约到 co-CDH 假设。

co-CDH 假设: 给定 $(g_1, g_1^a, h)$ 其中 $h \in G_2$, $a \in \mathbb{Z}_q$,难计算 $h^a \in G_2$.

Pairing 友好曲线安全:

  • BLS12-381: 设计为 128-bit 安全(pairing 在 $F_{p^{12}}$ 中),素数域 $p \approx 2^{381}$
  • BN254 (旧标准): 仅 ~100 bit 安全(NFS 改进后)

5.2 与 ECDLP 的差异

BLS 安全依赖两类困难问题:

  1. $G_1, G_2$ 中的 ECDLP
  2. Pairing target group $G_T = \mathbb{F}_{p^{12}}^*$ 中的 DLP(用 NFS 解,复杂度 sub-exponential)

NFS 在 $F_{p^{12}}$ 中比 ECDLP 弱 → 必须 $p$ 足够大(BLS12-381 选 381-bit 即 source 群约 255-bit 安全,target 群约 117-128 bit)。


六、真实漏洞 / 事件

年份事件类型
2016DFINITY threshold BLS DKG 漏洞DKG 协议错误
2017"Optimal Ate Pairing" implementation bug验证不精确
2020Ethereum 2.0 Phase 0 PoP 校验缺失部分客户端漏过 PoP
2021"Cofactor Clearing" bug in py_ecc子群验证不严 → small subgroup attack
2022Filecoin proof aggregation overflow类型转换错误
2023Web3.py BLS encoding 字节序混乱跨链桥 sig 不互通

Filecoin 事件: BLS 用于扇区证明聚合。一次 lib 升级后 48 字节 sig 部分压缩位错乱 → 上千个矿工挑战失败但实际是 lib bug。


七、协议应用 — Web3 中的 BLS

协议用途
Ethereum Beacon Chain验证者 attestation 聚合(每 epoch 800k 签名→1)
Ethereum Sync Committee512 验证者轮值,BLS 聚合证明给轻客户端
Chia Network钱包签名 + 农场聚合
FilecoinProof of Replication / Spacetime 聚合
Dfinity / ICP阈值 BLS 签名 chain blocks (Random Beacon)
Drand (Distributed Randomness Beacon)League of Entropy 网络阈值 BLS
Ethermint / EVMOSTendermint validator BLS
Mina Protocol(改用 Pasta Schnorr,但其 zkApps 用 BLS pairing 验证 SNARK)
EIP-2537precompile 提供 BLS12-381 native 操作(已激活 Pectra)

7.1 以太坊 PoS attestation 聚合细节

每 slot (12s) 的 64 个 committee × ~12,500 验证者:

  1. 每验证者对 $(slot, head, source, target)$ BLS 签名
  2. Aggregator (committee 内随机选择) 收集 → 聚合签名 + bitmask
  3. 提交聚合 attestation 到 mempool
  4. 区块提议者打包 ≤ 128 attestations / 区块
  5. 验证: 1 pairing × 128 attestations / block

节省空间: 12,500 × 96 字节 = 1.2 MB → 96 字节 + 1,562 字节 bitmask ≈ 99% 压缩。

7.2 EIP-2537 (BLS Precompiles)

激活地址:

  • 0x0b: BLS12-381 G1 add
  • 0x0c: G1 mul
  • 0x0d: G1 multi-exp
  • 0x0e: G2 add
  • 0x10: pairing check
  • ...

Gas cost 设计成允许 on-chain BLS verification(之前需 100M+ gas, 现 ~50k gas)。


八、常见陷阱

  1. Rogue Key Attack — 必须强制 PoP 或 distinct message
  2. Subgroup check 缺失 — 攻击者用非主群点 → small subgroup attack。py_ecc 早期版本有此问题
  3. Hash-to-curve 错误 — RFC 9380 SSWU 算法实现复杂,错误 hashing 破坏安全性
  4. Pairing 实现侧信道 — Miller loop 中的 timing leak
  5. 签名延展性 — BLS 本身非 malleable,但 cofactor multiplication 可制造等价点
  6. 错误使用 G1/G2 角色 — Eth2: pubkey in G1 (短), sig in G2 (短) ; Filecoin 反向
  7. 域分离 (DST) 不一致 — 同算法不同协议必须用不同 DST 防跨协议重放
  8. 聚合 sig 重放 — BLS 聚合签名易被部分提取(A+B+C → A+B 可能重新签 C 注入恶意)

九、关键速查

BLS 性能 (BLS12-381, 单核)

操作时间
KeyGen~50 μs
Sign~500 μs (G2 mul)
Verify~3 ms (2 pairings)
Pairing~1.5 ms
Aggregate sig (n=100)~100 μs (just point adds)
Verify same-msg agg (n=100)~3 ms (still 2 pairings!)
Verify distinct-msg agg (n=100)~150 ms (101 pairings)

BLS vs ECDSA vs Schnorr

维度ECDSASchnorrBLS
签名大小64 B64 B48 B (G1) / 96 B (G2)
公钥大小33 B32 B48 B / 96 B
聚合✓ MuSig (交互)✓ 完美非交互
验证速度慢 (pairing)
阈值复杂 (GG18/20)中等 (FROST)简单
确定性RFC6979aux+det完美

十、面试题

Q1: 为什么以太坊 PoS 选 BLS 而不是 Schnorr? A: 关键是非交互聚合。Schnorr 聚合 (MuSig2) 需要签名前的 nonce 交换 round → 800k 验证者无法实时协同。BLS 中每个验证者独立计算 $x_i H(m)$,aggregator 事后简单相加。代价:验证慢 50×,但每个 epoch 只验一次。

Q2: BLS Rogue Key Attack 在以太坊如何防御? A: Beacon Chain 部署合约要求新验证者 deposit 时提供 PoP(signature 字段对 pubkey + withdrawal_credentials + amount 签名)。验证 PoP → 证明持有相应 sk → 防止 rogue key 注册。

Q3: BLS12-381 中 sig 是 96 字节还是 48 字节? A: 看协议选择。Eth2 选 sig in $G_2$ (96 B compressed) + pk in $G_1$ (48 B):因为验证者数量 800k+,pubkey 存储更重要。Filecoin 选反向:sig in $G_1$ (48 B) + pk in $G_2$ (96 B),因为 sig 数量更多 (每扇区一个)。

Q4: 阈值 BLS DKG 如何处理恶意参与方? A: (1) Pedersen DKG 让每方公布多项式承诺;(2) 抱怨阶段:接收方验证收到的 share 是否在公布承诺上,错误 → 公开 challenge;(3) 失败方排除并继续。最终主公钥 = $\sum$ honest 方常数项 × G1。GG20、FROST 都基于此思想(但应用 Schnorr 而非 BLS)。

Q5: BLS 后量子安全吗? A: ❌。Pairing 安全依赖 ECDLP(量子下 Shor 多项式破解)+ Target 群 NFS(仍 sub-exponential,量子加速有限)。Eth2 长期路线: 切到 STARK-friendly hash-based signatures (Winternitz / XMSS over Poseidon)。短中期 BLS 仍安全(量子计算机距离破 256-bit 椭圆曲线尚远)。


十一、明日预告

Day 201: Week 30 复习 — 系统整合本周学的 ECDSA / EdDSA / Schnorr / BLS 四大签名方案。产出全面对比表(性能、安全、聚合、应用),明确 Web3 工程师在不同场景的选择决策树。