第1章:私钥、公钥与地址编码#

参考: examples/ch01_keys_and_addresses.py, code/chapter01/
最后更新: 2025-12-05


要学 Taproot,先得把 Bitcoin 的密码学基础搞清楚。这一章讲私钥(private key)、公钥(public key)和地址生成——它们是所有交易的底层构件。

# 本章环境(一次性加载,后续代码块复用)
from btcaaron import Key, TapTree
from bitcoinutils.setup import setup
from bitcoinutils.keys import PrivateKey, P2shAddress, P2wpkhAddress
from bitcoinutils.script import Script

密码学层次结构#

Bitcoin 的安全性建立在一条单向链上:

Private Key (256-bit) → Public Key (ECDSA point) → Address (encoded hash)

每一层都有明确分工:

  • 私钥:证明资产归属,用于签名

  • 公钥:验证签名,授权支付

  • 地址:方便收款,同时隐藏公钥

私钥:所有权的基础#

Bitcoin 私钥就是一个 256 位的随机整数,用它来证明资产归属。2^256 有多大?比可观测宇宙里所有原子加起来还多。

生成私钥#

使用 btcaaron 简洁 API(本书示例库):

# 示例 1: 生成私钥(btcaaron)
# 参考: examples/ch01_keys_and_addresses.py

alice = Key.from_wif("cPeon9fBsW2BxwJTALj3hGzh9vm8C52Uqsce7MzXGS1iFJkPF4AT")
print(f"Private Key (WIF): {alice.wif}")
Private Key (WIF): cPeon9fBsW2BxwJTALj3hGzh9vm8C52Uqsce7MzXGS1iFJkPF4AT

示例输出:

Private Key (HEX): e9873d79c6d87dc0fb6a5778633389dfa5c32fa27f99b5199abf2f9848ee0289
Private Key (WIF): L1aW4aubDFB7yfras2S1mN3bqg9w3KmCPSM3Qh4rQG9E1e84n5Bd

十六进制表示包含 64 个字符(每个字符代表 4 位),总共 256 位或 32 字节。这种格式在数学上精确,但不适合存储或导入/导出操作。

钱包导入格式(WIF)#

钱包导入格式(Wallet Import Format,WIF)用 Base58Check 编码让私钥更好用:

  • 加入校验和,能检测输入错误

  • 去掉容易混淆的字符(0、O、I、l)

  • 统一了钱包之间导入导出的格式

WIF 编码过程如下:

  1. 添加版本前缀:主网使用 0x80,测试网使用 0xEF

  2. (可选)添加压缩标志:如果对应的公钥将被压缩,则在有效载荷后追加 0x01,此步骤会改变 WIF 的最终 Base58 前缀

  3. 计算校验和:应用 SHA256(SHA256(data)) 并取前 4 字节

  4. 应用 Base58 编码:转成易读的字符串

WIF 编码流程

图 1-1:WIF 编码将 32 字节私钥转换为 Base58Check 编码字符串

生成的 WIF 字符串具有独特的前缀:

  • LK:主网私钥

  • c:测试网私钥

公钥:密码学验证点#

公钥是 secp256k1 椭圆曲线上的一个点,由私钥通过椭圆曲线乘法算出。数学原理比较复杂,但代码写起来很简单。

ECDSA 与 secp256k1#

Bitcoin 使用 secp256k1 曲线实现椭圆曲线数字签名算法(ECDSA)。secp256k1 曲线由以下方程定义:

y² = x³ + 7

secp256k1 椭圆曲线

图 1-2:Bitcoin 使用的 secp256k1 椭圆曲线

不必深究数学,只需理解:

  • 每个私钥 k 在曲线上生成一个唯一点 (x, y)

  • 这种关系在计算上是不可逆的

  • 曲线的数学性质保证了安全性

压缩与未压缩公钥#

公钥可以用两种格式表示:

未压缩格式(65 字节):

04 + x-coordinate (32 bytes) + y-coordinate (32 bytes)

压缩格式(33 字节):

02/03 + x-coordinate (32 bytes)

为什么能压缩?椭圆曲线的性质决定了:只要有 x 坐标和 y 的奇偶性,就能算出完整的 y:

  • 02 前缀:y 坐标为偶数

  • 03 前缀:y 坐标为奇数

# 示例 2: 生成公钥 + X-only(btcaaron)
# 参考: examples/ch01_keys_and_addresses.py

# 压缩公钥(33 字节)与 x-only(32 字节)
print(f"Compressed (33 bytes): {alice.pubkey}")
print(f"X-only (32 bytes):     {alice.xonly}")
print(f"验证: x-only = 压缩公钥去掉 02/03 前缀 → {alice.pubkey[2:]} == {alice.xonly}")
Compressed (33 bytes): 02898711e6bf63f5cbe1b38c05e89d6c391c59e9f8f695da44bf3d20ca674c8519
X-only (32 bytes):     898711e6bf63f5cbe1b38c05e89d6c391c59e9f8f695da44bf3d20ca674c8519
验证: x-only = 压缩公钥去掉 02/03 前缀 → 898711e6bf63f5cbe1b38c05e89d6c391c59e9f8f695da44bf3d20ca674c8519 == 898711e6bf63f5cbe1b38c05e89d6c391c59e9f8f695da44bf3d20ca674c8519

示例输出:

Compressed:   0250be5fc44ec580c387bf45df275aaa8b27e2d7716af31f10eeed357d126bb4d3
Uncompressed: 0450be5fc44ec580c387bf45df275aaa8b27e2d7716af31f10eeed357d126bb4d3...

现在的 Bitcoin 软件都只用压缩公钥,因为更省空间,安全性一样。

X-Only 公钥:Taproot 的创新#

Taproot 更进一步,引入了 x-only 公钥——连 y 的奇偶性都不存了,只保留 x 坐标(32 字节)。好处是:

  • 交易更小

  • 验签更快

  • 方便做密钥聚合

# 示例 3: Taproot X-Only 公钥(复用示例 1/2 的 alice)
print(f"Taproot X-only 公钥 (32 bytes): {alice.xonly}")
Taproot X-only 公钥 (32 bytes): 898711e6bf63f5cbe1b38c05e89d6c391c59e9f8f695da44bf3d20ca674c8519

这是 Taproot 效率提升的关键,后面章节会详细讲。

地址生成:从公钥到支付目的地#

Bitcoin 地址不是公钥,而是公钥哈希后再编码的结果。多这一层有几个好处:

  • 隐私:花费之前不暴露公钥

  • 抗量子:哈希提供额外一层保护

  • 防错:编码自带校验和

地址生成过程#

生成地址的流程大同小异:

  1. 哈希公钥:先 SHA256,再 RIPEMD160(合称 Hash160)

  2. 加版本字节:标明地址类型

  3. 加校验和:用于检测输入错误

  4. 编码:转成 Base58Check 或 Bech32 字符串

传统 Bitcoin 地址流程

图 1-3:传统 Bitcoin 地址生成流程

# 示例 4: 生成不同类型的地址
# Legacy/SegWit/P2SH: bitcoinutils | Taproot: btcaaron

setup('mainnet')
priv = PrivateKey()
pub = priv.get_public_key()

# Legacy / SegWit / P2SH-P2WPKH(bitcoin-utils)
legacy_address = pub.get_address()
segwit_native = pub.get_segwit_address()
segwit_p2sh = P2shAddress.from_script(segwit_native.to_script_pub_key())

# Taproot:btcaaron 一行搞定
program = TapTree(internal_key=Key.from_wif(priv.to_wif())).build()

print(f"Legacy (P2PKH):     {legacy_address.to_string()}")
print(f"SegWit Native:      {segwit_native.to_string()}")
print(f"SegWit P2SH:        {segwit_p2sh.to_string()}")
print(f"Taproot (P2TR):     {program.address}")
Legacy (P2PKH):     1C45GpJKoBMpJfHdfVu1uL2zR7CSkjDFCQ
SegWit Native:      bc1q0ylr9w4nu0gth8uv7wd6l5mn22xcyda2l7pntm
SegWit P2SH:        3Ps5zqeRr6jojKxdESWP3FxA5sotUNd8fD
Taproot (P2TR):     bc1pf2jessjqdpzmjm2nkhxqksurq3uf7mst6pn53x0ndwnlwrg5jlcsyllyjw

示例输出:

Legacy (P2PKH):     1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa
SegWit Native:      bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kygt080
SegWit P2SH:        3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy
Taproot (P2TR):     bc1p53ncq9ytax924ps66z6al3wfhy6a29w8h6xfu27xem06t98zkmvsakd43h  (~62 chars)

地址类型与编码格式#

Base58Check 编码#

Base58Check 用于传统地址,去掉了容易看错的字符,还自带校验:

排除的字符: 0(零)、O(大写 o)、I(大写 i)、l(小写 L)

P2PKH(Pay-to-Public-Key-Hash):

  • 前缀:1

  • 格式:Base58Check 编码

  • 用途:原始 Bitcoin 地址格式

  • 示例:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa

P2SH(Pay-to-Script-Hash):

  • 前缀:3

  • 格式:Base58Check 编码

  • 用途:多重签名和包装的 SegWit 地址

  • 示例:3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy

Bech32 编码:SegWit 的创新#

Bech32 是 SegWit 引入的新编码,错误检测能力更强:

P2WPKH(Pay-to-Witness-Public-Key-Hash):

  • 前缀:bc1q

  • 格式:Bech32 编码

  • 优势:更低费用,更好的错误检测

  • 示例:bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kygt080

Bech32m 编码:Taproot 的增强#

Taproot 用 Bech32m,是 Bech32 的改进版:

P2TR(Pay-to-Taproot):

  • 前缀:bc1p

  • 格式:Bech32m 编码

  • 优势:增强隐私,脚本灵活性

  • 示例:bc1p53ncq9ytax924ps66z6al3wfhy6a29w8h6xfu27xem06t98zkmvsakd43h(约 62 字符)

地址格式对比#

Address Type

Encoding

Data Size

Address Length

Prefix

Primary Use Case

P2PKH

Base58Check

25 bytes

~34 chars

1...

Legacy payments

P2SH

Base58Check

25 bytes

~34 chars

3...

Multi-sig, wrapped SegWit

P2WPKH

Bech32

21 bytes

42-46 chars

bc1q...

SegWit payments

P2TR

Bech32m

33 bytes

58-62 chars

bc1p...

Taproot payments

# 示例 5: 验证地址格式(简要)
# 参考: code/chapter01/05_verify_addresses.py

setup('mainnet')
priv = PrivateKey()
pub = priv.get_public_key()
legacy = pub.get_address()
segwit = pub.get_segwit_address()
taproot = pub.get_taproot_address()
print(f"P2PKH:  {len(legacy.to_string())} chars | P2WPKH: {len(segwit.to_string())} chars | P2TR: {len(taproot.to_string())} chars")
print(f"P2TR ScriptPubKey: OP_1 + 32B x-only = {len(taproot.to_script_pub_key().to_hex())//2} bytes")
P2PKH:  34 chars | P2WPKH: 42 chars | P2TR: 62 chars
P2TR ScriptPubKey: OP_1 + 32B x-only = 34 bytes

地址编码的细节很多——版本字节、校验和、不同编码方案——但核心思路就一条:

👉 地址是给人看的。本质上只是锁定脚本(scriptPubKey)的友好表示,协议层面并不需要它。

看到前缀(1、3、bc1q、bc1p)就知道背后是什么脚本。节点眼里没有”地址”这个概念,只有脚本。

后面几章我们会聚焦真正重要的东西:各类地址对应的 scriptPubKey。逻辑都在那里——Bitcoin Script 和可编程性的起点也在那里。能推断地址背后的脚本,就能理解它怎么被花费。

派生模型#

理解从私钥到地址的完整派生链路很重要。下图展示了整个流程——从私钥一路到链上脚本。普通用户只看到地址,但开发者要能追踪每一步,才能理解 Bitcoin 怎么验证所有权、执行花费条件。

密钥-公钥-地址关系

图 1-4:从私钥到地址的完整派生模型

Private Key (k)
    ↓ ECDSA multiplication
Public Key (x, y)
    ↓ SHA256 + RIPEMD160
Public Key Hash (20 bytes)
    ↓ Version + Checksum + Encoding
Address (Base58/Bech32)
  ↓ Decoded by wallet/node
ScriptPubKey (locking script on-chain)

安全性保证:

  • 正向:每一步都能快速算出来

  • 逆向:每一步都无法反推

  • 抗碰撞:不同公钥撞出同一地址的概率可以忽略

对 Taproot 的实际意义#

后面会看到,Taproot 就是建立在这些基础上的:

  • X-only 公钥:省空间,还能做密钥聚合

  • Bech32m 编码:错误检测更强

  • 统一的地址外观:多签和单签看起来一样,提升隐私

搞懂这些编码和密钥格式,就为理解 Taproot 的高级特性打好了基础——多个花费条件可以藏在同一个地址里。

章节总结#

本章梳理了 Bitcoin 交易的密码学基础:

  • ✅ 私钥是 256 位随机数,用 WIF 编码方便导入导出

  • ✅ 公钥是椭圆曲线上的点,现在都用压缩格式

  • ✅ 地址是公钥哈希后编码的结果,不是公钥本身

  • ✅ 不同地址类型用不同编码:Base58Check、Bech32、Bech32m

  • ✅ Taproot 引入 x-only 公钥和 Bech32m,进一步提升效率

这些组件——密钥、哈希、编码——都是 Bitcoin Script 要处理和验证的对象。下一章我们看它们怎么跟 Bitcoin Script 配合——这门脚本语言定义了花费条件,也是 Taproot 高级功能的基础。