跳至主要內容
高級

Schnorr Signatures

BIP-340 Schnorr 簽名:更高效的數位簽名與金鑰聚合

20 分鐘

概述

Schnorr 簽名是由 Claus Schnorr 在 1989 年提出的數位簽名方案。 由於專利限制(2008 年到期),比特幣最初採用了 ECDSA。 BIP-340 在 2021 年 Taproot 升級中將 Schnorr 簽名引入比特幣, 帶來更高效率、更好的隱私性和原生多簽支援。

啟用時間: Schnorr 簽名在區塊高度 709,632(2021 年 11 月 14 日)隨 Taproot 啟用。 它是所有 Taproot (P2TR) 交易的基礎。

數學基礎

橢圓曲線

secp256k1 曲線:

方程式: y² = x³ + 7 (mod p)

參數:
p = 2²⁵⁶ - 2³² - 977 (有限域)
n = 曲線階數 (點的數量)
G = 基點 (生成元)

私鑰: d ∈ [1, n-1]
公鑰: P = d × G (橢圓曲線點乘法)

重要性質:
- 已知 P 和 G,無法計算 d (離散對數難題)
- 點加法和標量乘法運算封閉

Schnorr vs ECDSA

ECDSA 簽名:
1. 選擇隨機數 k
2. R = k × G
3. r = R.x mod n
4. s = k⁻¹ × (z + r × d) mod n
5. 簽名 = (r, s)

ECDSA 問題:
- 簽名可塑性: (r, s) 和 (r, -s) 都有效
- 無法線性聚合多個簽名
- 驗證需要計算 k⁻¹ (較慢)

Schnorr 簽名:
1. 選擇隨機數 k
2. R = k × G
3. e = H(R || P || m)
4. s = k + e × d mod n
5. 簽名 = (R, s)

Schnorr 優勢:
- 無簽名可塑性 (R 是固定的)
- 線性特性允許金鑰聚合
- 驗證只需乘法 (更快)
- 可進行批量驗證

BIP-340 規範

公鑰格式

BIP-340 公鑰 (x-only):

傳統壓縮公鑰: 33 bytes
┌──────────────────────────────────────┐
│ 0x02/0x03 │ 32-byte x coordinate     │
└──────────────────────────────────────┘
            ↑
      表示 y 是偶數或奇數

BIP-340 公鑰: 32 bytes
┌──────────────────────────────────────┐
│ 32-byte x coordinate                 │
└──────────────────────────────────────┘

規則:
- 只使用 x 座標
- 隱式假設 y 是偶數 (even y)
- 如果計算出的 y 是奇數,則取反私鑰

優點:
- 節省 1 byte
- 簡化驗證邏輯

簽名格式

BIP-340 簽名: 64 bytes

┌──────────────────────────────────────┐
│ R.x (32 bytes)                       │
├──────────────────────────────────────┤
│ s (32 bytes)                         │
└──────────────────────────────────────┘

vs ECDSA DER 編碼:
- ECDSA: 70-72 bytes (可變長度)
- Schnorr: 64 bytes (固定長度)

R 的 y 座標:
- 同樣隱式假設偶數
- 簽名時若 R.y 是奇數,取反 k

Tagged Hash

// BIP-340 使用 tagged hash 防止跨協議攻擊
function taggedHash(tag: string, data: Uint8Array): Uint8Array {
  // 計算 tag 的雜湊
  const tagHash = sha256(new TextEncoder().encode(tag));

  // tagged_hash = SHA256(SHA256(tag) || SHA256(tag) || data)
  return sha256(
    concatBytes([tagHash, tagHash, data])
  );
}

// BIP-340 定義的標籤
const TAGS = {
  BIP0340_challenge: 'BIP0340/challenge',
  BIP0340_aux: 'BIP0340/aux',
  BIP0340_nonce: 'BIP0340/nonce',
};

// 計算挑戰 e
function computeChallenge(
  rx: Uint8Array,
  px: Uint8Array,
  message: Uint8Array
): bigint {
  const hash = taggedHash(
    'BIP0340/challenge',
    concatBytes([rx, px, message])
  );
  return bytesToBigInt(hash) % SECP256K1_ORDER;
}

TypeScript 實作

簽名算法

import { secp256k1 } from '@noble/curves/secp256k1';

const CURVE_ORDER = secp256k1.CURVE.n;

interface SchnorrSignature {
  rx: Uint8Array; // 32 bytes
  s: Uint8Array;  // 32 bytes
}

function schnorrSign(
  message: Uint8Array,
  privateKey: Uint8Array,
  auxRand: Uint8Array = crypto.getRandomValues(new Uint8Array(32))
): SchnorrSignature {
  // 1. 確保私鑰對應的公鑰 y 是偶數
  let d = bytesToBigInt(privateKey);
  const P = secp256k1.ProjectivePoint.fromPrivateKey(privateKey);
  if (!hasEvenY(P)) {
    d = CURVE_ORDER - d;
  }
  const dBytes = bigIntToBytes(d, 32);

  // 2. 計算公鑰 x 座標
  const px = bigIntToBytes(P.x, 32);

  // 3. 生成隨機數 k
  // t = d XOR tagged_hash("BIP0340/aux", auxRand)
  const t = xor(dBytes, taggedHash('BIP0340/aux', auxRand));

  // k = tagged_hash("BIP0340/nonce", t || px || message)
  const kHash = taggedHash(
    'BIP0340/nonce',
    concatBytes([t, px, message])
  );
  let k = bytesToBigInt(kHash) % CURVE_ORDER;
  if (k === 0n) throw new Error('Invalid k');

  // 4. R = k × G
  const R = secp256k1.ProjectivePoint.BASE.multiply(k);
  if (!hasEvenY(R)) {
    k = CURVE_ORDER - k;
  }
  const rx = bigIntToBytes(R.x, 32);

  // 5. 計算挑戰 e
  const e = computeChallenge(rx, px, message);

  // 6. s = k + e × d mod n
  const s = (k + e * d) % CURVE_ORDER;

  return {
    rx,
    s: bigIntToBytes(s, 32)
  };
}

function hasEvenY(point: secp256k1.ProjectivePoint): boolean {
  const affine = point.toAffine();
  return affine.y % 2n === 0n;
}

驗證算法

function schnorrVerify(
  message: Uint8Array,
  signature: SchnorrSignature,
  publicKey: Uint8Array // 32-byte x-only
): boolean {
  const { rx, s } = signature;

  // 1. 解析公鑰
  const P = liftX(bytesToBigInt(publicKey));
  if (!P) return false;

  // 2. 解析 r 和 s
  const r = bytesToBigInt(rx);
  const sVal = bytesToBigInt(s);

  // 驗證範圍
  if (r >= secp256k1.CURVE.p || sVal >= CURVE_ORDER) {
    return false;
  }

  // 3. 計算挑戰 e
  const e = computeChallenge(rx, publicKey, message);

  // 4. 計算 R' = s × G - e × P
  const sG = secp256k1.ProjectivePoint.BASE.multiply(sVal);
  const eP = P.multiply(e);
  const R = sG.subtract(eP);

  // 5. 驗證 R' == R
  if (R.equals(secp256k1.ProjectivePoint.ZERO)) {
    return false;
  }

  const RAffine = R.toAffine();

  // R 的 y 必須是偶數
  if (RAffine.y % 2n !== 0n) {
    return false;
  }

  // R 的 x 必須等於 r
  return RAffine.x === r;
}

// 從 x 座標恢復點 (假設 y 是偶數)
function liftX(x: bigint): secp256k1.ProjectivePoint | null {
  const p = secp256k1.CURVE.p;

  if (x >= p) return null;

  // y² = x³ + 7 mod p
  const ySq = (x ** 3n + 7n) % p;

  // 計算平方根
  const y = modPow(ySq, (p + 1n) / 4n, p);

  if ((y * y) % p !== ySq) return null;

  // 選擇偶數 y
  const yFinal = y % 2n === 0n ? y : p - y;

  return secp256k1.ProjectivePoint.fromAffine({
    x,
    y: yFinal
  });
}

批量驗證

// Schnorr 簽名支持高效批量驗證
function schnorrBatchVerify(
  items: Array<{
    message: Uint8Array;
    signature: SchnorrSignature;
    publicKey: Uint8Array;
  }>
): boolean {
  if (items.length === 0) return true;
  if (items.length === 1) {
    return schnorrVerify(
      items[0].message,
      items[0].signature,
      items[0].publicKey
    );
  }

  // 隨機權重 (防止攻擊者找到抵消的簽名)
  const weights: bigint[] = items.map((_, i) =>
    i === 0 ? 1n : bytesToBigInt(crypto.getRandomValues(new Uint8Array(16)))
  );

  // 計算:
  // Σ(aᵢ × sᵢ) × G = Σ(aᵢ × Rᵢ) + Σ(aᵢ × eᵢ × Pᵢ)
  let leftSum = 0n;
  let rightPoint = secp256k1.ProjectivePoint.ZERO;

  for (let i = 0; i < items.length; i++) {
    const { message, signature, publicKey } = items[i];
    const { rx, s } = signature;
    const a = weights[i];

    // 左邊: Σ(aᵢ × sᵢ)
    leftSum = (leftSum + a * bytesToBigInt(s)) % CURVE_ORDER;

    // 右邊: aᵢ × Rᵢ
    const R = liftX(bytesToBigInt(rx));
    if (!R) return false;
    rightPoint = rightPoint.add(R.multiply(a));

    // 右邊: aᵢ × eᵢ × Pᵢ
    const P = liftX(bytesToBigInt(publicKey));
    if (!P) return false;
    const e = computeChallenge(rx, publicKey, message);
    rightPoint = rightPoint.add(P.multiply((a * e) % CURVE_ORDER));
  }

  // 驗證: leftSum × G == rightPoint
  const leftPoint = secp256k1.ProjectivePoint.BASE.multiply(leftSum);
  return leftPoint.equals(rightPoint);
}

// 批量驗證速度:
// - 單個驗證: 2 次標量乘法
// - 批量 n 個: (n+1) 次標量乘法 + n 次點加法
// - 速度提升: 約 n/(n+1) ≈ 50% (對於大批量)

金鑰聚合

MuSig2 概述

Schnorr 的線性特性允許金鑰聚合:

基本概念:
- 多個參與者的公鑰可以聚合成一個
- 多個簽名可以聚合成一個
- 外部觀察者無法區分單簽和多簽

2-of-2 範例:
Alice: 私鑰 dₐ, 公鑰 Pₐ = dₐ × G
Bob:   私鑰 d_b, 公鑰 P_b = d_b × G

聚合公鑰:
P = Pₐ + P_b = (dₐ + d_b) × G

聯合簽名:
sₐ = kₐ + e × dₐ
s_b = k_b + e × d_b
s = sₐ + s_b = (kₐ + k_b) + e × (dₐ + d_b)

結果: 單一 64 byte 簽名,驗證如同單簽

MuSig2 協議

// MuSig2: 兩輪多簽協議

interface MuSig2Context {
  pubkeys: Uint8Array[];
  aggregatedPubkey: Uint8Array;
  keyAggCoefficients: bigint[];
}

// 步驟 1: 金鑰聚合
function keyAgg(pubkeys: Uint8Array[]): MuSig2Context {
  // 排序公鑰以確保確定性
  const sorted = [...pubkeys].sort((a, b) =>
    bytesToBigInt(a) < bytesToBigInt(b) ? -1 : 1
  );

  // 計算聚合係數 (防止 rogue key 攻擊)
  const L = taggedHash(
    'KeyAgg list',
    concatBytes(sorted)
  );

  const coefficients: bigint[] = [];
  let aggPoint = secp256k1.ProjectivePoint.ZERO;

  for (const pk of sorted) {
    // aᵢ = H(L || Pᵢ)
    const aHash = taggedHash(
      'KeyAgg coefficient',
      concatBytes([L, pk])
    );
    const a = bytesToBigInt(aHash) % CURVE_ORDER;
    coefficients.push(a);

    // P = Σ(aᵢ × Pᵢ)
    const P = liftX(bytesToBigInt(pk))!;
    aggPoint = aggPoint.add(P.multiply(a));
  }

  return {
    pubkeys: sorted,
    aggregatedPubkey: bigIntToBytes(aggPoint.toAffine().x, 32),
    keyAggCoefficients: coefficients
  };
}

// 步驟 2: 第一輪 - 承諾 nonces
interface NonceCommitment {
  R1: secp256k1.ProjectivePoint;
  R2: secp256k1.ProjectivePoint;
}

function generateNonce(): {
  secret: [bigint, bigint];
  public: NonceCommitment;
} {
  const k1 = bytesToBigInt(crypto.getRandomValues(new Uint8Array(32))) % CURVE_ORDER;
  const k2 = bytesToBigInt(crypto.getRandomValues(new Uint8Array(32))) % CURVE_ORDER;

  return {
    secret: [k1, k2],
    public: {
      R1: secp256k1.ProjectivePoint.BASE.multiply(k1),
      R2: secp256k1.ProjectivePoint.BASE.multiply(k2)
    }
  };
}

// 步驟 3: 聚合 nonces
function aggregateNonces(
  nonces: NonceCommitment[],
  message: Uint8Array,
  aggPubkey: Uint8Array
): { R: secp256k1.ProjectivePoint; b: bigint } {
  // 聚合 R1 和 R2
  let aggR1 = secp256k1.ProjectivePoint.ZERO;
  let aggR2 = secp256k1.ProjectivePoint.ZERO;

  for (const nonce of nonces) {
    aggR1 = aggR1.add(nonce.R1);
    aggR2 = aggR2.add(nonce.R2);
  }

  // b = H(aggR1 || aggR2 || aggPubkey || message)
  const bHash = taggedHash(
    'MuSig/noncecoef',
    concatBytes([
      pointToBytes(aggR1),
      pointToBytes(aggR2),
      aggPubkey,
      message
    ])
  );
  const b = bytesToBigInt(bHash) % CURVE_ORDER;

  // R = aggR1 + b × aggR2
  const R = aggR1.add(aggR2.multiply(b));

  return { R, b };
}

// 步驟 4: 創建部分簽名
function partialSign(
  ctx: MuSig2Context,
  myIndex: number,
  myPrivateKey: bigint,
  myNonce: [bigint, bigint],
  R: secp256k1.ProjectivePoint,
  b: bigint,
  message: Uint8Array
): bigint {
  // e = H(R.x || aggPubkey || message)
  const rx = bigIntToBytes(R.toAffine().x, 32);
  const e = computeChallenge(rx, ctx.aggregatedPubkey, message);

  // 調整私鑰
  let d = myPrivateKey * ctx.keyAggCoefficients[myIndex];
  d = d % CURVE_ORDER;

  // k = k1 + b × k2
  const [k1, k2] = myNonce;
  let k = (k1 + b * k2) % CURVE_ORDER;

  // 調整 k (如果 R.y 是奇數)
  if (!hasEvenY(R)) {
    k = CURVE_ORDER - k;
  }

  // s = k + e × d
  return (k + e * d) % CURVE_ORDER;
}

// 步驟 5: 聚合簽名
function aggregateSignatures(
  partialSigs: bigint[],
  R: secp256k1.ProjectivePoint
): SchnorrSignature {
  let s = 0n;
  for (const sig of partialSigs) {
    s = (s + sig) % CURVE_ORDER;
  }

  return {
    rx: bigIntToBytes(R.toAffine().x, 32),
    s: bigIntToBytes(s, 32)
  };
}

適配器簽名

Adaptor Signatures (適配器簽名):

用途:
- 原子交換 (Atomic Swaps)
- 支付通道
- 無腳本腳本 (Scriptless Scripts)

概念:
Alice 創建一個「不完整」的簽名 s'
Bob 擁有秘密 t
完整簽名 s = s' + t

流程:
1. Alice 知道 T = t × G (但不知道 t)
2. Alice 創建 R' = R + T
3. Alice 的適配器簽名: s' = k + e × d
4. Bob 驗證 s' × G = R' + e × P - T
5. Bob 發布完整簽名: s = s' + t
6. Alice 從 s - s' = t 學到秘密

應用: 閃電網路 PTLC (Point Time Locked Contracts)

Taproot 整合

Schnorr 在 Taproot 中的角色:

1. Key Path 花費
   - 直接使用 Schnorr 簽名
   - 外部看來如同單簽交易
   - 最高效的花費方式

2. Script Path 花費
   - Tapscript 中的 OP_CHECKSIG 使用 Schnorr
   - 比 ECDSA 更高效

3. 隱私提升
   - 多簽看起來像單簽
   - 沒有使用的腳本路徑完全隱藏
   - 所有 Taproot 輸出看起來相同

4. 效率提升
   - 64 byte 簽名 vs 70-72 byte ECDSA
   - 32 byte 公鑰 vs 33 byte 壓縮公鑰
   - 批量驗證加速

安全考量

安全注意事項:

1. 隨機數生成
   - k 必須是安全的隨機數
   - 重複使用 k 會洩露私鑰!
   - BIP-340 使用 aux_rand 增加熵

2. Rogue Key 攻擊
   - 多簽時攻擊者可能選擇特殊公鑰
   - MuSig2 使用係數防止此攻擊

3. 側信道攻擊
   - 確保恆定時間實現
   - 避免基於秘密的分支

4. 驗證要點
   - 必須驗證 R 的 y 座標是偶數
   - 必須驗證 r 和 s 在有效範圍內
   - 必須驗證公鑰在曲線上

與 ECDSA 比較

特性 ECDSA Schnorr
簽名大小 70-72 bytes 64 bytes
公鑰大小 33 bytes 32 bytes
簽名可塑性 存在
金鑰聚合 不支援 原生支援
批量驗證 不支援 支援
安全證明 複雜 簡單

相關資源

已複製連結
已複製到剪貼簿