跳至主要內容
進階

X-Only Public Keys

了解 Taproot 使用的 32 字節 X-Only 公鑰格式,節省空間並簡化協議。

10 分鐘

X-Only 公鑰是 BIP-340 Schnorr 簽名引入的 32 字節公鑰格式,只包含橢圓曲線點的 x 座標。 這種格式比傳統的 33 字節壓縮公鑰更緊湊,並在 Taproot 中被廣泛使用。

公鑰格式比較

橢圓曲線公鑰格式:

1. 非壓縮格式 (65 bytes):
┌────────┬─────────────────┬─────────────────┐
│ 0x04   │ x coordinate    │ y coordinate    │
│ 1 byte │ 32 bytes        │ 32 bytes        │
└────────┴─────────────────┴─────────────────┘
- 包含完整的 (x, y) 座標
- 最大但最直接

2. 壓縮格式 (33 bytes):
┌────────┬─────────────────┐
│ prefix │ x coordinate    │
│ 1 byte │ 32 bytes        │
└────────┴─────────────────┘
- prefix: 0x02 (y 是偶數) 或 0x03 (y 是奇數)
- y 可以從 x 計算得出

3. X-Only 格式 (32 bytes):
┌─────────────────┐
│ x coordinate    │
│ 32 bytes        │
└─────────────────┘
- 沒有前綴
- 假定 y 是偶數(如果不是則取反)
- Taproot 和 Schnorr 使用此格式

為什麼使用 X-Only?

X-Only 公鑰的優勢:

1. 節省空間
   - 每個公鑰節省 1 字節
   - 在區塊鏈上累積可觀
   - Taproot 輸出更小

2. 簡化協議
   - 無需處理前綴
   - 公鑰格式統一
   - 減少解析複雜度

3. 數學簡潔
   - secp256k1 曲線上每個 x 對應兩個 y
   - 選擇偶數 y 是確定性的
   - 沒有信息損失

橢圓曲線特性:
y² = x³ + 7 (mod p)

對於每個有效的 x:
- 存在 y 和 -y (mod p) 兩個解
- 其中一個是偶數,一個是奇數
- 我們約定使用偶數 y

// 這種約定使得公鑰可以唯一地從 x 恢復

Y 座標處理

確定 Y 座標的過程:

給定 x 座標:
1. 計算 y² = x³ + 7 (mod p)
2. 計算 y = sqrt(y²) (mod p)
3. 如果 y 是奇數,取 y = p - y (偶數)
4. 返回點 (x, y)

Python 實現:
def lift_x(x: int) -> tuple:
    """從 x 座標恢復完整公鑰點"""
    p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F

    # y² = x³ + 7
    y_sq = (pow(x, 3, p) + 7) % p

    # 計算 y = sqrt(y²) mod p
    # 對於 secp256k1: y = y_sq^((p+1)/4) mod p
    y = pow(y_sq, (p + 1) // 4, p)

    # 驗證
    if pow(y, 2, p) != y_sq:
        return None  # x 不在曲線上

    # 確保 y 是偶數
    if y % 2 != 0:
        y = p - y

    return (x, y)

# 使用
x = int.from_bytes(x_only_pubkey, 'big')
point = lift_x(x)

對簽名的影響

X-Only 對 Schnorr 簽名的影響:

簽名生成時:
1. 如果公鑰 P 的 y 是奇數
   - 必須使用 -d (取反的私鑰) 進行簽名
   - 這樣公開的 x-only P 對應偶數 y

2. R 點也使用 x-only
   - 同樣確保 R 的 y 是偶數
   - 如果不是,nonce 取反

簽名格式:
sig = (r, s) 其中:
- r: R 的 x 座標 (32 bytes)
- s: 簽名值 (32 bytes)
- 總共 64 bytes

驗證時:
1. 從 r 恢復 R 點 (假設偶數 y)
2. 從 P 的 x 座標恢復 P 點 (偶數 y)
3. 驗證 s·G = R + e·P

關鍵洞察:
如果 y 的奇偶性錯誤,簽名會驗證失敗
這保證了系統的一致性

在 Taproot 中的應用

Taproot 輸出使用 X-Only:

P2TR 輸出結構:
OP_1 <32-byte x-only pubkey>

相比 P2WPKH:
OP_0 <20-byte pubkey hash>

Taproot 更直接地使用公鑰:
- 不用 hash160
- 直接嵌入 x-only 公鑰
- 驗證時直接使用

公鑰調整 (Tweaking):
Q = P + t·G

其中:
- P: 內部公鑰 (x-only)
- t: 調整值 = tagged_hash("TapTweak", P || merkle_root)
- Q: 輸出公鑰 (x-only)

處理 y 座標:
1. 從 P 的 x-only 恢復點 (偶數 y)
2. 計算 Q = P + t·G
3. 如果 Q 的 y 是奇數:
   - 調整私鑰: d' = n - (d + t)
   - 其中 n 是曲線階數

實現細節

轉換函數:

# 壓縮公鑰 → X-Only
def to_x_only(compressed_pubkey: bytes) -> bytes:
    """將 33 字節壓縮公鑰轉為 32 字節 x-only"""
    assert len(compressed_pubkey) == 33
    # 直接取 x 座標部分
    return compressed_pubkey[1:]

# X-Only → 壓縮公鑰
def from_x_only(x_only: bytes, parity: int = 0) -> bytes:
    """將 x-only 轉為壓縮公鑰"""
    assert len(x_only) == 32
    # parity: 0 = 偶數 y (0x02), 1 = 奇數 y (0x03)
    prefix = bytes([0x02 + parity])
    return prefix + x_only

# 私鑰調整
def adjust_privkey_for_xonly(privkey: int, pubkey_point) -> int:
    """確保對應的公鑰有偶數 y"""
    n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141

    if pubkey_point.y % 2 != 0:
        # y 是奇數,取反私鑰
        return n - privkey
    return privkey

# 完整的簽名準備
def prepare_for_signing(privkey: bytes) -> tuple:
    """準備私鑰和公鑰用於 Schnorr 簽名"""
    d = int.from_bytes(privkey, 'big')

    # 計算公鑰點
    P = d * G  # 橢圓曲線乘法

    # 調整私鑰
    d = adjust_privkey_for_xonly(d, P)

    # 獲取 x-only 公鑰
    x_only_pubkey = P.x.to_bytes(32, 'big')

    return d, x_only_pubkey

控制塊中的奇偶性

Taproot Script Path 的控制塊:

控制塊結構:
┌─────────────────────────────────────────┐
│ leaf_version | parity (1 byte)          │
│ internal_pubkey (32 bytes)              │
│ merkle_path (32 bytes × n)              │
└─────────────────────────────────────────┘

第一個字節編碼:
- 位 0: 輸出公鑰 Q 的 y 奇偶性
- 位 1-7: 葉子版本 (0xc0 或 0xc1)

編碼方式:
control_byte = leaf_version | parity

範例:
- 0xc0: 葉子版本 0, Q 的 y 是偶數
- 0xc1: 葉子版本 0, Q 的 y 是奇數

為什麼需要奇偶性?
1. 控制塊包含內部公鑰 P (x-only)
2. 驗證時需要計算 Q = P + t·G
3. 但需要知道 Q 應該有什麼 y
4. 奇偶性位提供這個信息

驗證流程:
1. 解析控制塊獲取 parity
2. 恢復 P (假設偶數 y)
3. 計算 Q' = P + t·G
4. 如果 Q' 的 y 奇偶性與 parity 不匹配
   - 取 Q' 的 negation
5. 比較 Q' 與輸出公鑰

批量驗證優化

X-Only 對批量驗證的影響:

Schnorr 批量驗證:
∑(s_i · G) = ∑(R_i) + ∑(e_i · P_i)

使用 x-only 時:
1. 所有 R_i 和 P_i 都從 x 座標恢復
2. 假設所有 y 都是偶數
3. 如果原始 y 是奇數,簽名生成時已取反

批量驗證優勢:
- 統一的點恢復過程
- 無需處理不同的前綴
- 點運算可以並行化

性能考量:
- lift_x 需要模平方根
- 可以預計算或快取
- 批量操作時成本攤薄

// X-Only 格式使批量驗證更加整潔

兼容性考量

X-Only 與傳統系統的兼容:

ECDSA 簽名:
- 仍使用 33 字節壓縮公鑰
- X-Only 僅用於 Schnorr/Taproot

混合使用:
- P2WPKH: 使用壓縮公鑰
- P2TR: 使用 x-only 公鑰
- 同一私鑰可以導出兩種格式

錢包實現:
# BIP-86 路徑
m/86'/0'/0'/0/0  → Taproot (x-only)

# BIP-84 路徑
m/84'/0'/0'/0/0  → SegWit (壓縮)

轉換示例:
private_key = derive_key(path)
compressed_pubkey = private_key.get_public_key()  # 33 bytes
x_only_pubkey = compressed_pubkey[1:]  # 32 bytes

# 但注意私鑰可能需要調整
if needs_negation(compressed_pubkey):
    signing_key = negate(private_key)

安全注意事項

  • 私鑰調整:簽名前必須正確處理 y 座標奇偶性
  • 點恢復:確保 x 座標在曲線上有效
  • 一致性:公鑰導出和簽名必須使用相同的約定
  • 測試:使用官方測試向量驗證實現

相關概念

  • Tagged Hashes:標籤雜湊函數
  • Schnorr Signatures:Schnorr 簽名
  • Taproot:Taproot 升級
  • Key Path:密鑰路徑花費
  • Script Path:腳本路徑花費
已複製連結
已複製到剪貼簿