Bitcoin Keys and Addresses

Table of Contents

1. 比特币私钥

比特币采用椭圆曲线 Secp256k1“私钥”是 \(1\) 到 \(n-1\) 之间的随机数, \(n\) 是个很大的数(256 个比特位),其值如下:
\(n\) = 0xFFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFE BAAE DCE6 AF48 A03B BFD2 5E8C D036 4141

\(n\) 用科学计数法表示,约为 \(1.15792 \times 10^{77}\) ,这个范围是巨大的,你无法“猜测”出别人随机生成的私钥。

1.1. 私钥编码

前面介绍过,私钥就是一个随机大整数,可以用 256 比特位表示,这 256 比特位可以编码为:

  1. 16 进制形式;
  2. Wallet Import Format (WIF);
  3. WIF-compressed 形式。

使用 openssl rand -hex 32 可以生成一个随机的 32 字节(256 比特)数据,它可以作为 16 进制形式的比特币私钥,如:

$ openssl rand -hex 32              # 随机生成 32 字节随机数,以 16 进制形式显示
d9815f47582f890ef4f818fd5dec98a6419fda23e57f1fed62ee0b9f79b1a785
$ openssl rand -hex 32              # 再测试一次
3bd5af025525af11f4471f299d600af51a6d697374b39ad19224a91b748a17ec

假设,16 进制的私钥为 0c28fca386c7a227600b2fe50b7cae11ec86d3bf1fbe471be89827e19d72aa1d ,其对应的 WIF、WIF-compressed 形式如表 1 所示。

Table 1: Bitcoin 私钥编码实例
Format Private Key Note
Hex 0c28fca386c7a227600b2fe50b7cae11ec86d3bf1fbe471be89827e19d72aa1d  
WIF 5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ 以 5 开头
WIF-compressed KwdMAjGmerYanjeui5SHS7JkmpZvVipYvB2LJGU1ZxJwYvP98617 以 K 或 L 开头

注:“WIF-compressed”这个名称取得不好,实际上 WIF-compressed 形式的私钥比 WIF 形式的私钥更长。

1.1.1. WIF、WIF-compressed 编码

下面介绍 WIF、WIF-compressed 具体的编码过程:
1、在 16 进制的私钥前面增加 version 字段,Bitcoin 主网为 0x80 ,Bitcoin 测试网为 0xef对于 WIF-compressed 形式,则后面还要增加 0x01 这个结果称为 extended key,即:

800c28fca386c7a227600b2fe50b7cae11ec86d3bf1fbe471be89827e19d72aa1d    # for WIF
800c28fca386c7a227600b2fe50b7cae11ec86d3bf1fbe471be89827e19d72aa1d01  # for WIF-compressed

2、对 extended key 计算 SHA-256:

1ce0723b5128b03fcfd2484368f1bbc89dc7c6eddf56589c2eccb2bacbbcafc9      # for WIF
b504a81b66924482a289ce571a614c37b122f40cefb0abba0227e143154c2153      # for WIF-compressed

3、再次计算上面结果的 SHA-256:

507a5b8dfed0fc6fe8801743720cedec06aa5c6fca72b07c49964492fb98a714      # for WIF
a62019d20340a1de1b5f254f07f2f6c96ad5165218459ab4f3c8f5a7c0e12183      # for WIF-compressed

4、第二次 SHA256 结果的前 4 个字节作为 checksum:

507a5b8d                                                              # for WIF
a62019d2                                                              # for WIF-compressed

5、把 checksum 加到 extended key 的后面:

800c28fca386c7a227600b2fe50b7cae11ec86d3bf1fbe471be89827e19d72aa1d507a5b8d    # for WIF
800c28fca386c7a227600b2fe50b7cae11ec86d3bf1fbe471be89827e19d72aa1d01a62019d2  # for WIF-compressed

6、对上面结果进行 Base58Check 编码,即得到 WIF、WIF-compressed 形式:

5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ                   # WIF
KwdMAjGmerYanjeui5SHS7JkmpZvVipYvB2LJGU1ZxJwYvP98617                  # WIF-compressed

注:Base58Check 编码表只有 58 个有效字符,具体来说,就是 10 个数字加上 26 个小写字母加上 26 个大写字母,再减去 0OIl 这 4 个容易产生混淆的字符。

1.1.2. WIF、WIF-compressed 编码及解码(Python 实现)

WIF、WIF-compressed 的编码及解码过程,用 Python 实现如下:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import hashlib
import base58
from typing import Union


def scrub_input(hex_str_or_bytes: Union[str, bytes]) -> bytes:
    if isinstance(hex_str_or_bytes, str):
        hex_str_or_bytes = bytes.fromhex(hex_str_or_bytes)
    return hex_str_or_bytes


# wallet import format key - base58 encoded format
# https://bitcoin.stackexchange.com/questions/9244/private-key-to-wif
def gen_wif_key(private_key: Union[str, bytes], compressed_WIF: bool = False) -> bytes:
    private_key = scrub_input(private_key)

    # prepended mainnet version byte to private key
    mainnet_private_key = b'\x80' + private_key
    if compressed_WIF:
        mainnet_private_key = b'\x80' + private_key + b'\x01'
    # perform SHA-256 hash on the mainnet_private_key
    sha256 = hashlib.sha256()
    sha256.update(mainnet_private_key)
    hash = sha256.digest()

    # perform SHA-256 on the previous SHA-256 hash
    sha256 = hashlib.sha256()
    sha256.update(hash)
    hash = sha256.digest()

    # create a checksum using the first 4 bytes of the previous SHA-256 hash
    # append the 4 checksum bytes to the mainnet_private_key
    checksum = hash[:4]

    hash = mainnet_private_key + checksum

    # convert mainnet_private_key + checksum into base58 encoded string
    return base58.b58encode(hash)


def decode_wif(wif: str) -> bytes:
    compressed = False
    if wif.startswith('K') or wif.startswith('L'):
        compressed = True
    decoded = base58.b58decode(wif)
    if compressed:
        private_key = decoded[1:-5]  # [80 xxx 1 checksum]
    else:
        private_key = decoded[1:-4]  # [80 xxx checksum]
    return private_key


prikey = '0c28fca386c7a227600b2fe50b7cae11ec86d3bf1fbe471be89827e19d72aa1d'  # hex
wif = gen_wif_key(prikey)
print(wif)  # 5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ
wif_compressed = gen_wif_key(prikey, True)
print(wif_compressed)  # KwdMAjGmerYanjeui5SHS7JkmpZvVipYvB2LJGU1ZxJwYvP98617

prikey = decode_wif('5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ')
print(prikey.hex())  # 0c28fca386c7a227600b2fe50b7cae11ec86d3bf1fbe471be89827e19d72aa1d

1.1.3. 私钥加密编码(BIP38)

WIF、WIF-compressed 形式的私钥是没有加密的,可以解码得到原始的那个“随机大整数”。

BIP38 提议用 AES 算法对私钥进行加密,这种方案得到的私钥以字符 6P 开头,这种私钥必须输入密码才能导入到各种比特币钱包中。表 2 是 BIP38 的例子。

Table 2: BIP-38 实例
Item Example
Private Key (WIF) 5J3mBbAH58CpQ3Y5RNJpUKPE62SQ5tfcvU2JpbnkeyhfsYB1Jcn
Passphrase MyTestPassphrase
Encrypted Key (BIP38) 6PRTHL6mWa48xSopbU1cKrVjpKbBZxcLRRCdctLJ3z5yxE87MobKoXdTsJ

2. 比特币公钥

公钥 \(K\) 是椭圆曲线上的一个点,它可由私钥计算而来,其公式为:
\[K = k G\]
上式中, \(k\) 为私钥, \(G\) 为 Base Point,它是 secp256k1 的一个参数。

假设私钥 \(k\) 为 0x1e99423a4ed27608a15a2616a2b0e9e52ced330ac530edcc32c8ffc6a526aedd ,那么由椭圆曲线上的计算公式 \(k G\) (具体过程略),可以得到 \(K\) 的两个坐标分别为:

\begin{aligned} x &= \texttt{f028892bad7ed57d2fb57bf33081d5cfcf6f9ed3d3d7f159c2e2fff579dc341a} \\ y &= \texttt{07cf33da18bd734c600b96a72bbc4749d5141c90ec8ac328ae52ddfe2e505bdb} \\ \end{aligned}

2.1. 公钥两种表达形式

公钥是一个坐标,如何用一个数来表达两个坐标呢?有两种方案:“Uncompressed format”和“Compressed format”,如图 1 所示。

bitcoin_pubkey.gif

Figure 1: 公钥两种表达形式

Uncompressed 形式,就是把两个坐标 \(x,y\) 直接连接在一起,再在前面加个 0x04 前缀即可,如前面公钥例子的 Uncompressed 形式为:
04f028892bad7ed57d2fb57bf33081d5cfcf6f9ed3d3d7f159c2e2fff579dc341a07cf33da18bd734c600b96a72bbc4749d5141c90ec8ac328ae52ddfe2e505bdb

Compressed 形式,就是当 \(y\) 为偶数时,编码为 \(02 x\) ,当 \(y\) 为奇数时,编码为 \(03 x\) 。如前面公钥例子中,由于 \(y\) 是奇数,所以公钥的 Compressed 形式为:
03f028892bad7ed57d2fb57bf33081d5cfcf6f9ed3d3d7f159c2e2fff579dc341a

为什么在 Compressed 形式中,我们可以不编码 \(y\) 值呢,因为通过椭圆曲线 Secp256k1 的公式 \(y^2 = x^3 + 7\) 可以重新计算出 \(y\) 。

2.1.1. 从私钥计算公钥(Python 实现)

下面 Python 代码可以实现从私钥计算 Uncompressed/Compressed 公钥:

import ecdsa
from ecdsa.ellipticcurve import PointJacobi


def derive_public_key(private_key: bytes, compressed: bool = False) -> bytes:
    Q: PointJacobi = int.from_bytes(private_key, byteorder='big') * ecdsa.curves.SECP256k1.generator
    xstr: bytes = Q.x().to_bytes(32, byteorder='big')
    ystr: bytes = Q.y().to_bytes(32, byteorder='big')
    if compressed:
        parity: int = Q.y() & 1
        return (2 + parity).to_bytes(1, byteorder='big') + xstr
    else:
        return b'\04' + xstr + ystr


prikey = bytearray.fromhex('1e99423a4ed27608a15a2616a2b0e9e52ced330ac530edcc32c8ffc6a526aedd')
uncompressed_pubkey = derive_public_key(prikey, False)
print("uncompressed public key =", uncompressed_pubkey.hex())
compressed_pubkey = derive_public_key(prikey, True)
print("compressed public key =", compressed_pubkey.hex())

3. 比特币地址

比特币各种类型的地址如图 2 所示(摘自 https://en.bitcoin.it/wiki/Invoice_address )。

bitcoin_address.jpg

Figure 2: Bitcoin Address

3.1. P2PKH(1 开头的地址)

P2PKH(Pay-to-Public-Key-Hash)是最原始的地址,由公钥通过 Hash 计算后得到,它们都以 1 开头。

我们以前面介绍过的 Compressed 公钥(03f028892bad7ed57d2fb57bf33081d5cfcf6f9ed3d3d7f159c2e2fff579dc341a)为例,介绍一下从公钥转换得到地址的步骤:
1、公钥为:

03f028892bad7ed57d2fb57bf33081d5cfcf6f9ed3d3d7f159c2e2fff579dc341a

2、计算其 SHA256,得到:

448997ae8759eb23d6c6b67de6fc3419006cbf061617c3e07323aaf5bcd53476

3、对上面结果计算 RIPEMD-160 哈希,得到:

bbc1e42a39d05a4cc61752d6963b7f69d09bb27b

4、计算 checksum,规则是对上面结果前面加上版本号(主网为 0x00,测试网为 0x6f),然后计算两次 SHA256:

bbc1e42a39d05a4cc61752d6963b7f69d09bb27b
             | 最前面加上版本号(主网为 00,测试网为 6f)
             v
00bbc1e42a39d05a4cc61752d6963b7f69d09bb27b
             | 第 1 次 sha256
             v
d094b259d52d874cf084aaece4251e5d3b731fe66fe2dc21564106330d25f278
             | 第 2 次 sha256
             v
37fefcd01823fdbce992747702d2420b11a5d266b92d516a93aa83f336ce8241
    | 取前 4 字节
    v
37fefcd0

5、用格式 [version][ripemd160_hash][checksum] 构造出下面结果:

00bbc1e42a39d05a4cc61752d6963b7f69d09bb27b37fefcd0

6、对上面结果进行 Base58Check 编码,得到:

1J7mdg5rbQyUHENYdx39WVWK7fsLpEoXZy

这个地址是用 Compressed 公钥计算出来的,被称为“Compressed 地址”;如果用 Uncompressed 公钥重复进行上面过程,则会得到另外一个地址,称为“Uncompressed 地址”。在这个例子中,用 Uncompressed 公钥 04f028892bad7ed57d2fb57bf33081d5cfcf6f9ed3d3d7f159c2e2fff579dc341a07cf33da18bd734c600b96a72bbc4749d5141c90ec8ac328ae52ddfe2e505bdb 进行计算会得到地址 1424C2F4bC9JidNjjTUZCbUxv6Sa1Mt62x ,这也是一个合法的比特币地址,称为 Uncompressed 地址。

一个比特币私钥对应两个地址(Compressed/Uncompressed 地址),它们都是合法的。

参考:https://en.bitcoin.it/wiki/Technical_background_of_version_1_Bitcoin_addresses

3.1.1. 从公钥计算 P2PKH 地址(Python 实现)

下面 Python 代码可以实现从公钥计算 P2PKH 地址:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import hashlib
import base58


def sha256(inputs: bytes) -> bytes:
    """ Computes sha256 """
    sha = hashlib.sha256()
    sha.update(inputs)
    return sha.digest()


def ripemd160(inputs: bytes) -> bytes:
    """ Computes ripemd160 """
    rip = hashlib.new('ripemd160')
    rip.update(inputs)
    return rip.digest()


def base58_cksum(inputs: bytes) -> bytes:
    """ Computes base 58 four bytes check sum """
    s1 = sha256(inputs)
    s2 = sha256(s1)
    checksum = s2[0:4]
    return checksum


def pubkey_compressed_to_uncompressed(compressed_pubkey: bytes) -> bytes:
    """ Converts compressed pubkey to uncompressed format """
    assert len(compressed_pubkey) == 33
    # modulo p which is defined by secp256k1's spec
    p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
    x = int.from_bytes(compressed_pubkey[1:33], byteorder='big')
    y_sq = (pow(x, 3, p) + 7) % p
    y = pow(y_sq, (p + 1) // 4, p)
    if compressed_pubkey[0] % 2 != y % 2:
        y = p - y
    y_bytes = y.to_bytes(32, byteorder='big')
    return b'\04' + compressed_pubkey[1:33] + y_bytes  # x + y


def pubkey_to_p2pkh_addr(pubkey: bytes, version: bytes) -> bytes:
    """ Derives legacy (p2pkh) address from pubkey """
    out1 = sha256(pubkey)
    out2 = ripemd160(out1)
    # Base-58 encoding with a checksum
    checksum = base58_cksum(version + out2)
    address = base58.b58encode(version + out2 + checksum)
    return address


pubkey = '03f028892bad7ed57d2fb57bf33081d5cfcf6f9ed3d3d7f159c2e2fff579dc341a'

pubkey_uncompressed = b''
pubkey_compressed = b''

if pubkey.startswith('04'):  # uncompressed
    pubkey_uncompressed = bytes.fromhex(pubkey)
    if ord(bytearray.fromhex(pubkey[-2:])) % 2 == 0:
        pubkey_compressed_hex_str = '02' + pubkey[2:66]
    else:
        pubkey_compressed_hex_str = '03' + pubkey[2:66]
    pubkey_compressed = bytes.fromhex(pubkey_compressed_hex_str)
else:  # compressed
    pubkey_uncompressed = pubkey_compressed_to_uncompressed(bytes.fromhex(pubkey))
    pubkey_compressed = bytes.fromhex(pubkey)

print("compressed public key =", pubkey_compressed.hex())
print("uncompressed public key =", pubkey_uncompressed.hex())
version = b'\x00'  # 0x00 for mainnet, 0x6f for testnet
addr_compressed = pubkey_to_p2pkh_addr(pubkey_compressed, version)
addr_uncompressed = pubkey_to_p2pkh_addr(pubkey_uncompressed, version)
print("address (uncompressed) = ", addr_uncompressed)  # 1424C2F4bC9JidNjjTUZCbUxv6Sa1Mt62x
print("address (compressed) = ", addr_compressed)  # 1J7mdg5rbQyUHENYdx39WVWK7fsLpEoXZy

3.2. P2SH(3 开头的地址)

Pay-to-Script Hash (P2SH) 地址由 Script Hash 的 Base58Check 编码得到。由于进行 Base58Check 编码时,对 P2SH 地址指定的版本前缀为 5,这导致这类地址以“3”开头。

3.2.1. P2SH-P2WPKH(隔离见证兼容地址,3 开头)

在原生的隔离见证地址(参考节 3.3)提出之前,生成隔离见证地址的方式是在 P2SH 中嵌入 Pay-to-Witness-Public-Key-Hash(P2WPKH),细节可参考:https://bitcointalk.org/index.php?topic=5229211.0

3.2.2. 从公钥计算 P2SH-P2WPKH 地址(Python 实现)

下面 Python 代码可以实现从公钥计算 P2SH-P2WPKH 地址:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import hashlib
import base58


def sha256(inputs: bytes) -> bytes:
    """ Computes sha256 """
    sha = hashlib.sha256()
    sha.update(inputs)
    return sha.digest()


def ripemd160(inputs: bytes) -> bytes:
    """ Computes ripemd160 """
    rip = hashlib.new('ripemd160')
    rip.update(inputs)
    return rip.digest()


def base58_cksum(inputs: bytes) -> bytes:
    """ Computes base 58 four bytes check sum """
    s1 = sha256(inputs)
    s2 = sha256(s1)
    checksum = s2[0:4]
    return checksum


def pubkey_to_p2sh_p2wpkh_addr(pubkey_compressed: bytes) -> bytes:
    """ Derives p2sh-segwit (p2sh p2wpkh) address from pubkey """
    pubkey_hash = sha256(pubkey_compressed)
    rip = ripemd160(pubkey_hash)
    redeem_script = b'\x00\x14' + rip  # 0x00: OP_0, 0x14: PushData
    redeem_hash = sha256(redeem_script)
    redeem_rip = ripemd160(redeem_hash)
    # Base-58 encoding with a checksum
    version = b'\x05'  # 0x05 for mainnet, 0xc4 for testnet
    checksum = base58_cksum(version + redeem_rip)
    address = base58.b58encode(version + redeem_rip + checksum)
    return address


pubkey = '03f028892bad7ed57d2fb57bf33081d5cfcf6f9ed3d3d7f159c2e2fff579dc341a'
addr_p2sh_segwit = pubkey_to_p2sh_p2wpkh_addr(bytes.fromhex(pubkey))
print("p2sh-segwit address", addr_p2sh_segwit)  # 3FyC6EYuxW22uj4CaEGjNCjxeg7gHyFeVv

3.3. Bech32(bc1q 开头的地址)

BIP0173 中提出了 bc1 开头的地址,它们是原生的隔离见证地址(注:对于版本 0 的隔离见证地址,它们总是以 bc1q 开头)。

Bech32 编码由 3 部分组成:

  1. human-readable part,比特币主网固定为 bc;
  2. separator,固定为 1;
  3. data part,由数字和小写字母组成,但排除这 4 个: 1, b, i, o (注:10 个数字加上 26 个小写字母,再减去这 4 个排除的字符,可得 32 个字符)。

后文仅介绍 Pay-to-Witness-Public-Key-Hash(P2WPKH)地址的生成, P2WPKH 地址长度固定为 42 字符。 这里不介绍 Pay-to-Witness-Script-Hash(P2WSH)地址, P2WSH 地址长度固定为 62 字符。

3.3.1. 从公钥计算 P2WPKH 地址(Python 实现)

下面 Python 代码可以实现从公钥计算 P2WPKH 地址:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import hashlib
from typing import Optional


def sha256(inputs: bytes) -> bytes:
    """ Computes sha256 """
    sha = hashlib.sha256()
    sha.update(inputs)
    return sha.digest()


def ripemd160(inputs: bytes) -> bytes:
    """ Computes ripemd160 """
    rip = hashlib.new('ripemd160')
    rip.update(inputs)
    return rip.digest()


# From https://github.com/sipa/bech32/blob/master/ref/python/segwit_addr.py
"""Reference implementation for Bech32 and segwit addresses."""
CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"

def bech32_polymod(values):
    """Internal function that computes the Bech32 checksum."""
    generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]
    chk = 1
    for value in values:
        top = chk >> 25
        chk = (chk & 0x1ffffff) << 5 ^ value
        for i in range(5):
            chk ^= generator[i] if ((top >> i) & 1) else 0
    return chk


def bech32_hrp_expand(hrp):
    """Expand the HRP into values for checksum computation."""
    return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp]


def bech32_verify_checksum(hrp, data):
    """Verify a checksum given HRP and converted data characters."""
    return bech32_polymod(bech32_hrp_expand(hrp) + data) == 1


def bech32_create_checksum(hrp, data):
    """Compute the checksum values given HRP and data."""
    values = bech32_hrp_expand(hrp) + data
    polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ 1
    return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)]


def bech32_encode(hrp, data):
    """Compute a Bech32 string given HRP and data values."""
    combined = data + bech32_create_checksum(hrp, data)
    return hrp + '1' + ''.join([CHARSET[d] for d in combined])


def bech32_decode(bech):
    """Validate a Bech32 string, and determine HRP and data."""
    if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or
            (bech.lower() != bech and bech.upper() != bech)):
        return (None, None)
    bech = bech.lower()
    pos = bech.rfind('1')
    if pos < 1 or pos + 7 > len(bech) or len(bech) > 90:
        return (None, None)
    if not all(x in CHARSET for x in bech[pos+1:]):
        return (None, None)
    hrp = bech[:pos]
    data = [CHARSET.find(x) for x in bech[pos+1:]]
    if not bech32_verify_checksum(hrp, data):
        return (None, None)
    return (hrp, data[:-6])


def convertbits(data, frombits, tobits, pad=True):
    """General power-of-2 base conversion."""
    acc = 0
    bits = 0
    ret = []
    maxv = (1 << tobits) - 1
    max_acc = (1 << (frombits + tobits - 1)) - 1
    for value in data:
        if value < 0 or (value >> frombits):
            return None
        acc = ((acc << frombits) | value) & max_acc
        bits += frombits
        while bits >= tobits:
            bits -= tobits
            ret.append((acc >> bits) & maxv)
    if pad:
        if bits:
            ret.append((acc << (tobits - bits)) & maxv)
    elif bits >= frombits or ((acc << (tobits - bits)) & maxv):
        return None
    return ret


def decode(hrp, addr):
    """Decode a segwit address."""
    hrpgot, data = bech32_decode(addr)
    if hrpgot != hrp:
        return (None, None)
    decoded = convertbits(data[1:], 5, 8, False)
    if decoded is None or len(decoded) < 2 or len(decoded) > 40:
        return (None, None)
    if data[0] > 16:
        return (None, None)
    if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32:
        return (None, None)
    return (data[0], decoded)


def encode(hrp: str, witver: int, witprog: bytes) -> Optional[str]:
    """Encode a segwit address."""
    ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5))
    if decode(hrp, ret) == (None, None):
        return None
    return ret

def pubkey_to_segwit_addr(pubkey: bytes) -> Optional[str]:
    hrp = "bc"               # "bc" for mainnet, "tb" for testnet
    witver = 0
    witprog = ripemd160(sha256(pubkey))
    addr = encode(hrp, witver, witprog)
    return addr

pubkey_hex = '0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798'
addr = pubkey_to_segwit_addr(bytearray.fromhex(pubkey_hex))
print(addr)         # bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4

3.4. Bech32m(bc1p 开头的地址)

Bech32 有个缺点:如果地址的最后一个字符是 p,则在紧接着 p 之前的位置插入或者删除任意数量的字符 q 都不会使其 checksum 失效。

为了缓解 Bech32 的上述缺点,在 BIP0350 中提出了 Bech32m 地址:

  • 对于版本为 0 的原生隔离见证地址,使用以前的 Bech32;
  • 对于版本为 1(或者更高)的原生隔离见证地址,则使用新的 Bech32m。

对于 Bech32m 地址,当版本为 1 时,它们总是以 bc1p 开头(即 Taproot 地址)。

3.4.1. 计算 P2TR 地址

P2TR 是 Pay-to-Taproot 的缩写,这种 Output 有两种花费路径:Key Path 和 Script Path。我们这里只考虑生成 Key Path 花费路径的 P2TR 地址。

怎么从公钥推导出 P2TR 地址呢?

P2PKH(公钥的 Hash 的编码):
public key -> ripemd160(sha256(public key)) ---base58---> 1...


P2WPKH(公钥的 Hash 的编码):
public key -> ripemd160(sha256(public key)) ---bech32---> bc1q...


P2TR(不再是公钥的 Hash 的编码了,而是对 tweaked public key 的直接编码):
public key ->      tweaked public key       ---bech32m---> bc1p...

关于怎样从 Public Key 得到 Tweaked Public Key,这里不介绍,可以参考 bip86 或者 bip341

3.5. 不同地址对应的不同 scriptPubKey

3 总结了不同地址对应的不同 scriptPubKey 等信息。

Table 3: 不同地址对应的 scriptPubKey 等信息
  scriptPubKey (Locking Script) scriptSig (Unlocking Script) Witness
P2PK <pubKey> OP_CHECKSIG <sig>  
P2PKH OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG <sig> <pubKey>  
P2MS OP_<M> <pubKey>...<pubKey> OP_<N> OP_CHECKMULTISIG OP_0 <signature>...  
P2SH OP_HASH160 <scriptHash> OP_EQUAL <scriptParam>... <redeemScript>  
P2SH (2-of-3 signatures) OP_HASH160 <multisig_scriptHash> OP_EQUAL 0 <sig1> <sig2> <OP_3 <pubKey1> <pubKey2> <pubKey3> OP_2 OP_CHECKMULTISIG>  
P2SH-P2WPKH OP_HASH160 <scriptHash> OP_EQUAL 0 <pubKeyHash> <sig> <pubKey>
P2SH-P2WSH OP_HASH160 <scriptHash> OP_EQUAL 0 <redeemScriptHash> <scriptParam>... <redeemScript>
P2SH-P2WSH (2-of-3 signatures) OP_HASH160 <scriptHash> OP_EQUAL 0 <multisig_scriptHash> 0 <sig1> <sig2> <OP_3 <pubKey1> <pubKey2> <pubKey3> OP_2 OP_CHECKMULTISIG>
Native P2WPKH OP_0 <pubKeyHash> -empty- <sig> <pubKey>
Native P2WSH OP_0 <scriptHash> -empty- <scriptParam>... <redeemScript>
Native P2WSH (2-of-3 signatures) OP_0 <multisig_scriptHash> -empty- 0 <sig1> <sig2> <OP_3 <pubKey1> <pubKey2> <pubKey3> OP_2 OP_CHECKMULTISIG>
P2TR (Public Key Path Spending) OP_1 <pubKey> -empty- <schnorr-sig>
P2TR (Script Path Spending) OP_1 <pubKey> -empty- <script> <controlBlock>

3.5.1. 从锁定脚本(scriptPubKey)可推测出相应地址类型

4 介绍了不同地址的 scriptPubKey 的具体实例。这样,我们从 scriptPubKey 可推测出相应地址类型。

Table 4: scriptPubKey 具体实例
  scriptPubKey (Locking Script) scriptPubKey 长度 说明
P2PK (Uncompressed) 0x41{65-byte pubkey}ac 67 bytes 地址以 1 开头
P2PK (Compressed) 0x21{33-byte pubkey}ac 35 bytes 地址以 1 开头
P2PKH 0x76a914{20-byte keyhash}88ac 25 bytes 地址以 1 开头,keyhash=RIPEMD160(SHA256(pubKey)),pubKey 可以是 Uncompressed 或者 Compressed
P2MS (2-of-3 signatures) 0x5241{65-byte pubkey}41{65-byte pubkey}41{65-byte pubkey}53ae 201 bytes P2MS 的 scriptPubKey 长度不固定,和签名方案有关。无地址
P2SH 0xa914{20-byte scripthash}87 23 bytes 地址以 3 开头,通过地址不可区分 P2SH-P2WPKH/P2SH-P2WSH 等。scripthash=RIPEMD160(redeemScript)
Native P2WPKH 0x0014{20-byte keyhash} 22 bytes 地址以 bc1q 开头,比 Native P2WSH 地址要短。keyhash=RIPEMD160(SHA256(compressedPubKey))
Native P2WSH 0x0020{32-byte scripthash} 34 bytes 地址以 bc1q 开头,比 Native P2WPKH 地址要长。scripthash=SHA256(witnessScript)
P2TR (Public Key Path Spending) 0x5120{32-byte-tweaked-public-key} 34 bytes 地址以 bc1p 开头,通过地址不可区分 Key Path/Script Path
P2TR (Script Path Spending) 0x5120{32-byte-tweaked-public-key} 34 bytes 地址以 bc1p 开头,通过地址不可区分 Key Path/Script Path

尽管 P2PK 的 scriptPubKey 和 P2PKH 的 scriptPubKey 形式不一样,但它们计算地址的方式是一样的,都是公钥进行哈希后编码得到地址(节 3.1 介绍了具体的推导步骤)。换句话说,如果公钥相同,则由 P2PK 的 scriptPubKey 推导出来的地址和由 P2PKH 的 scriptPubKey 推导出来的地址是相同的。比如,交易 0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9 的 Output 的 scriptPubKey(410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3ac)是 P2PK 类型,而交易 ad38beebfb3f9ecc759787a69ada97c561a11e1a22355a052e5ab750774e5f8b 的 Output 的 scriptPubKe(76a91411b366edfc0a8b66feebae5c2e25a7b6a5d1cf3188ac)是 P2PKH 类型,由它们推导出来的地址是相同,都是 12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S。

参考:
P2PK (Uncompressed) 交易实例:
https://www.blockchain.com/btc/tx/f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16
P2PK (Compressed) 交易实例:
https://www.blockchain.com/btc/tx/aa2cb91301d684c443f81db2a623baed5ea657ea33c5fc4d8f995033292ebbd6
P2MS 1-of-2 交易实例:
https://www.blockchain.com/btc/tx/78b28d3c2324da8c2f01840021addbcabb68f7ce1d4da870cabe5e9df6afe63d
P2MS 2-of-3 交易实例:
https://www.blockchain.com/btc/tx/581d30e2a73a2db683ac2f15d53590bd0cd72de52555c2722d9d6a78e9fea510

3.6. 地址安全性

比特币地址(这里是指 P2PKH 地址)是公钥的 Hash,如图 3 所示。

bitcoin_tx_address.png

Figure 3: Private key, public key, and bitcoin address

假设某一天椭圆公钥密码体系被攻破(即由公钥可以计算出私钥),你的比特币就马上不安全了吗?不一定,因为钱包地址是公钥的 Hash, 如果某个钱包地址只收比特币,而不转出比特币,那么公钥是不会暴露的 ,只有当你转出比特币时,你才要需要提供签名及公钥。如果你确实需要转出比特币,那么你可以在转出比特币的同时把余额全部转入到另外一个全新的地址(这个地址还没有过转出记录)。

4. 参考

Mastering Bitcoin, 2nd Edition

Author: cig01

Created: <2019-03-01 Fri>

Last updated: <2021-01-24 Sun>

Creator: Emacs 27.1 (Org mode 9.4)