BLS 签名 — 聚合签名与以太坊 PoS
BLS 签名构造、pairing 基础回顾、签名聚合数学、Rogue Key Attack 防御、阈值签名
日期: 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$,满足:
- 双线性: $e(aP, bQ) = e(P, Q)^{ab}$
- 非退化: $e(P, Q) \ne 1$ 当 $P, Q$ 是 generator
- 可计算: 多项式时间
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 优势
- Sig 极短: 48 字节 (G1) 或 96 字节 (G2)
- Pubkey 短: 48 / 96 字节
- 完美聚合: $\sigma_{\text{agg}} = \sum \sigma_i$,验证 $e(g_1, \sigma_{\text{agg}}) = \prod e(pk_i, H(m_i))$
- 确定性: 同 (sk, m) 永远同 σ(无 nonce)
- 简单数学: 易于审计
1.5 BLS 劣势
- 验证慢: pairing 比 EC scalar mul 慢 10-50×
- Hash-to-curve 复杂: 需 ENC-MAP 标准(IETF RFC 9380)
- Rogue key attack 风险(见下)
- 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$。每参与者:
- 选随机多项式 $f_i(x)$ 度 $t-1$
- 把 $f_i(j)$ 发给参与者 $j$
- 公布 $g \cdot f_i$ 系数 (commitments)
- 各方累加: 私钥份额 $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 安全依赖两类困难问题:
- $G_1, G_2$ 中的 ECDLP
- 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)。
六、真实漏洞 / 事件
| 年份 | 事件 | 类型 |
|---|---|---|
| 2016 | DFINITY threshold BLS DKG 漏洞 | DKG 协议错误 |
| 2017 | "Optimal Ate Pairing" implementation bug | 验证不精确 |
| 2020 | Ethereum 2.0 Phase 0 PoP 校验缺失 | 部分客户端漏过 PoP |
| 2021 | "Cofactor Clearing" bug in py_ecc | 子群验证不严 → small subgroup attack |
| 2022 | Filecoin proof aggregation overflow | 类型转换错误 |
| 2023 | Web3.py BLS encoding 字节序混乱 | 跨链桥 sig 不互通 |
Filecoin 事件: BLS 用于扇区证明聚合。一次 lib 升级后 48 字节 sig 部分压缩位错乱 → 上千个矿工挑战失败但实际是 lib bug。
七、协议应用 — Web3 中的 BLS
| 协议 | 用途 |
|---|---|
| Ethereum Beacon Chain | 验证者 attestation 聚合(每 epoch 800k 签名→1) |
| Ethereum Sync Committee | 512 验证者轮值,BLS 聚合证明给轻客户端 |
| Chia Network | 钱包签名 + 农场聚合 |
| Filecoin | Proof of Replication / Spacetime 聚合 |
| Dfinity / ICP | 阈值 BLS 签名 chain blocks (Random Beacon) |
| Drand (Distributed Randomness Beacon) | League of Entropy 网络阈值 BLS |
| Ethermint / EVMOS | Tendermint validator BLS |
| Mina Protocol | (改用 Pasta Schnorr,但其 zkApps 用 BLS pairing 验证 SNARK) |
| EIP-2537 | precompile 提供 BLS12-381 native 操作(已激活 Pectra) |
7.1 以太坊 PoS attestation 聚合细节
每 slot (12s) 的 64 个 committee × ~12,500 验证者:
- 每验证者对 $(slot, head, source, target)$ BLS 签名
- Aggregator (committee 内随机选择) 收集 → 聚合签名 + bitmask
- 提交聚合 attestation 到 mempool
- 区块提议者打包 ≤ 128 attestations / 区块
- 验证: 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 add0x0c: G1 mul0x0d: G1 multi-exp0x0e: G2 add0x10: pairing check- ...
Gas cost 设计成允许 on-chain BLS verification(之前需 100M+ gas, 现 ~50k gas)。
八、常见陷阱
- Rogue Key Attack — 必须强制 PoP 或 distinct message
- Subgroup check 缺失 — 攻击者用非主群点 → small subgroup attack。py_ecc 早期版本有此问题
- Hash-to-curve 错误 — RFC 9380 SSWU 算法实现复杂,错误 hashing 破坏安全性
- Pairing 实现侧信道 — Miller loop 中的 timing leak
- 签名延展性 — BLS 本身非 malleable,但 cofactor multiplication 可制造等价点
- 错误使用 G1/G2 角色 — Eth2: pubkey in G1 (短), sig in G2 (短) ; Filecoin 反向
- 域分离 (DST) 不一致 — 同算法不同协议必须用不同 DST 防跨协议重放
- 聚合 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
| 维度 | ECDSA | Schnorr | BLS |
|---|---|---|---|
| 签名大小 | 64 B | 64 B | 48 B (G1) / 96 B (G2) |
| 公钥大小 | 33 B | 32 B | 48 B / 96 B |
| 聚合 | ✗ | ✓ MuSig (交互) | ✓ 完美非交互 |
| 验证速度 | 快 | 快 | 慢 (pairing) |
| 阈值 | 复杂 (GG18/20) | 中等 (FROST) | 简单 |
| 确定性 | RFC6979 | aux+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 工程师在不同场景的选择决策树。