Bitcoin 支付方式及地址类型
info
由于 Bitcoin 中的支付类型众多,这也导致地址各式各样,在 btcd 中提供了不同的函数来为公钥生成不同的地址。
这一篇作为 btcd:Golang 下的交易构建 的补充,说明各种支付方式和地址类型。
另外,这篇博文的大部分内容对应 Learn me a bitcoin [1] 的 Script,所以这篇博文更像一个学习记录
Script
Script is a mini programming language used as a locking mechanism for outputs in bitcoin transactions.
脚本是一种迷你程序语言,用作比特币交易输出的锁定机制。
- 在每笔交易的输出上都会由一个锁定脚本,它对应了输出中的 $ScriptPubKey$ 字段;
- 每笔交易还需要提供对应每个输入的解锁脚本,用于在这笔交易中解锁前一笔交易的输出,这对应了输入中的 $ScriptSig$ 字段;
锁定脚本和解锁脚本在比特币中体现为二进制形式混合的操作码和数据,并且通过运行时栈来运行,如果最终栈内的唯一元素是OP_1
或更大的元素,那么脚本就是有效的;
基于此,一笔有效的交易需要所有输入的有效(每一笔输入的锁定脚本和在该笔交易下的输出脚本所构成的脚本有效)
脚本语言、EVM 两者存在很大的相似性,都是以字节码的形式在运行时栈中运行。它们最大的区别在于脚本语言只验证脚本的有效性用于实现支付,而不会保存程序的状态,也因此 Bitcoin 的脚本语言不能做到像 Ethereum 那样的 DApp。
支付脚本
P2PK:支付到公钥
支付到公钥涉及到两个操作码:
OP_PUSHBYTES
:向栈中压入指定字节长度的数据OP_CHECKSIG
:取出栈顶的两条数据,并进行签名校验,如果这两条数据分别是签名sig
以及对应的公钥pk
,签名正确则返回[3]
签名的格式[2]为
[DER signature][hashtype]
,即 DER 编码格式下的签名和哈希类型,DER 编码格式如下,(R,S) 是椭圆曲线下签名后得到的证明[4]
P2PK 的锁定脚本和解锁脚本的形式依次为:
- 锁定脚本:
OP_PUSHBYTES_65 [pk] OP_CHECKSIG
- 解锁脚本:
OP_PUSHBYTES_72 [sig]
- 完整脚本:
OP_PUSHBYTES_72 [sig] OP_PUSHBYTES_65 [pk] OP_CHECKSIG
其运行过程就是:先把签名和公钥压栈,然后依次取出后调用 OP_CHECKSIG
来验证签名,OP_CHECKSIG 使用公钥 pk
验证签名 sig
是否对应当前的交易
这里的交易被作为签名时需要的消息,通常签名需要制定消息输入,否则无法满足签名所需要的安全性(算是密码学常识)
于是 P2PK 交易的锁定脚本只需要公钥即可,锁定脚本中的公钥可以是压缩形式(33 bytes),也可以是未压缩形式(65 bytes)
公钥的压缩
在椭圆曲线密码中,私钥通常是一个大整数 $k$,而公钥则是一个点 $kO$,$O$ 是椭圆曲线上的一个基点
所以,这就导致公钥实际上是二维坐标系上的一个二维坐标 (x,y),这就使得正常存储公钥需要存放两个坐标值
但是根据具体使用的 secp256k1 曲线的公式有 $y^2=x^3 + 7$,只需要知道 $x$ 就可以计算得到 $y$,这就使得通过只存放 $x$ 来压缩公钥成为可能。
由于平方的存在这使得取值会有两个(在取模下是一个奇数和一个偶数),因此压缩的公钥中需要指定是奇数或是偶数,即前缀中记录0x02(偶数)或0x03(奇数)。
P2PKH:支付到公钥哈希
支付到公钥的方式需要存放的数据太长,它需要存放完整的公钥信息,于是支付到哈希值这一方法被提出,然而实际上公钥还是需要被包含到交易中,区别在于:P2PK 的公钥在锁定脚本(交易输出)中,P2PKH 的公钥在解锁脚本(交易输入)中。
支付到公钥哈希涉及的额外操作码:
OP_DUP
:取出栈顶的数据,复制一份然后将两份数据都压栈OP_HASH160
:取出栈顶的数据,通过 hash160 计算哈希值后放入栈中OP_EQUALVERIFY
:取出栈顶的两份数据,检查是否相等
支付到公钥哈希的验证过程和支付到公钥类似,它多出来的步骤是验证解锁脚本中公钥的哈希值和锁定脚本中的哈希值是否相等,然后再验证交易的签名
P2PKH 的锁定脚本和解锁脚本的形式依次为:
- 锁定脚本:
OP_DUP OP_HASH160 OP_PUSHBYTES_20 [pkh] OP_EQUALVERIFY OP_CHECKSIG
- 解锁脚本:
OP_PUSHBYTES_72 [sig] OP_PUSHBYTES_33 [pk]
- 完整脚本:
OP_PUSHBYTES_72 [sig] OP_PUSHBYTES_33 [pk] OP_DUP OP_HASH160 OP_PUSHBYTES_20 [pkh] OP_EQUALVERIFY OP_CHECKSIG
完整脚本的除去最后验证签名的部分,都是为了验证公钥的哈希值是否正确
这种签名方式将交易的手续费从发送者转移到了接收者,因为接收者相比 P2PK 方式需要填入更多的信息,但是减少了发送者的信息长度
attention
后续都会提到代价转移这一概念,实际上就是越大的交易所需要的手续费越高
因此,后面的支付方式都在通过降低锁定脚本的大小,然后不得已又使得解锁脚本变大,这个过程中支付的成本就从发送者转移到了接收者
P2SH:支付到脚本哈希
支付到脚本哈希要求解锁脚本经过哈希后为某个哈希值,它最初被用于多签交易,因为多签交易中需要包含多个钱包的公钥,并且使用 P2PK 方式使得需要存放大量冗余公钥,这使得发送者在锁定时需要消耗较多的手续费(于是同样地,这部分费用被转移到使用者)
P2SH 的锁定脚本和解锁脚本的形式依次为:
- 锁定脚本:
OP_HASH160 OP_PUSHBYTES_20 [hash] OP_EQUAL
- 解锁脚本:没有确定的形式,只需要保证它的哈希值为
hash
即可,这里使用script
表示 - 完整脚本:
[args] [script] OP_HASH160 OP_PUSHBYTES_20 [hash] OP_EQUAL
因此,实际上 script
中包含的是完整的脚本代码,同时还有脚本需要的参数 args
,在多签交易中,这些参数是签名,这种支付方式完全将交易的代价转移给了发送者
非多重签名中,args
是压入交易的签名 OP_PUSHBYTES_72 [sig] OP_PUSHBYTES_33 [pk]
,script
包含的则是 P2PKH 的后半部分 OP_DUP OP_HASH160 OP_PUSHBYTES_20 [pkh] OP_EQUALVERIFY OP_CHECKSIG
Witness:隔离见证
BIP 141 提案中提出了隔离见证(SegWit)这一技术,见证隔离其实是某种程度上对区块的扩容(原来一个区块大小 1M 到现在的一个区块大小 4M),也算是开发者社区中达到的一种奇怪的平衡
隔离见证的核心思想
其核心思想是每笔交易的解锁脚本不再直接存储到交易的 ScriptSig
字段中,而是额外用一个名为 witness
的数组来存放,它们一一对应输入的位置作为解锁脚本。
此外,bip-141 还为交易提供了虚拟大小(vSize)这一概念,在计算区块大小的时候按照所有交易的虚拟大小总和作为区块大小。而见证隔离交易的虚拟大小为它真实大小的四分之一。这样就间接使得每个区块的大小变为了原来的 4 倍,但是实际上区块的真实大小还是 1 MB(这不是就是在耍流氓吗)。
此外,节点还可以剔除隔离见证的数据来减小区块的实际数据,它们在存放时和基础的区块是分开的。
见证隔离的锁定脚本形式:
- 针对 P2PKH 交易(P2WPKH):
OP_0 OP_PUSHBYTES_20 [pkh]
- 针对 P2SH 交易(P2WSH):
OP_0 OP_PUSHBYTES_32 [hash]
可以看出,锁定脚本根据类型具有不同的长度,客户端也可以根据长度来区分它们是 P2WPKH 还是 P2WSH
同时,见证隔离的解锁脚本也跟真实的 P2PKH 和 P2SH 交易不同,它们使用紧凑的字节码来表示数据,不需要操作码。解锁脚本被存放在交易的 witness
字段,并保证 sigScript
字段为空,解锁脚本的格式如下
针对 P2PKH 交易:
[sig] [pk]
针对 P2SH 交易:
[args] [script]
在这里,args 是后续脚本 script 的参数,因为不能保证所有 script 的参数都是一样的
见证隔离中设定了对应的脚本对来运行脚本,以此来达到压缩交易大小的目的
并且,锁定脚本的 OP_0
操作码对于一个旧的客户端来说,它是一个任何人都可以花费的输出,这就可以很好地兼容新版本客户端的区块和交易
P2TR:支付到 Taproot
支付到 Taproot (主根)是版本为 1 的隔离见证,上述的两种隔离见证是版本为 0 的隔离见证
P2TR 将两种支付方式统一到了一起,通过输出的格式无法知道这个输出是通过签名锁定(Key Path)还是脚本锁定(Script Path)[5]
只有在输出被使用的时候,通过见证信息才知道其锁定形式是签名锁定还是脚本锁定
attention
Taproot 交易也是 Bitcoin 的一个重大升级,这个支付方式设计到 Schnorr 签名和默克尔抽象语法树(Merklized Abstract Syntax Trees, MAST),要讲明白又需要开新的一篇来写了 = =,这里就先放着了,后续开新坑写
这部分内容见 BIP-340/1:Schnorr 签名与 MAST
由于是版本为 1 的隔离见证,所以它的锁定脚本为 OP_1 OP_PUSHBYTES_32 [hash]
,这里由于统一了两种支付路径,所以哈希值都是 32 bytes
解锁脚本格式:
- 针对 P2PKH 交易:
[sig]
- 针对 P2SH 交易:
[args] [script]
于是,在 P2TR 交易中,只能通过 witness 来区分交易类型,如果只有一个元素,说明是 P2PKH 交易,否则是 P2SH 交易
地址类型
在 Bitcoin 中没有 EVM 类区块链的实际意义上的地址,但是是否在所有的 UTxO 类区块链中都是这样还有待考证
Bitcoin 中常见的转账地址信息实际上是根据脚本类型推算得到,在常规的思维中是先有地址,才可以进行转账。而这也是比较反直觉的一点:Bitcoin 地址用于给发送方构造正确的输出,而交易中的地址是通过输出中的信息来推算得到
Bitcoin 交易的输出(Output)结构如下
字段 | 描述 | 长度 |
---|---|---|
$Amount$ | 输出中包含的聪有多少 | 8 bytes |
$ScriptPubKey Size$ | 输出锁定脚本长度 | 可选的 2 bytes |
$ScriptPubKey$ | 输出中的锁定脚本 | 1 - 9 bytes |
在 Mempool 这样的 Bitcoin 浏览器中通常会见到如 bc1q、bc1p 开头的地址,这些地址所对应的是交易的不同支付类型,它们是根据交易输出中的 ScriptPubKey
字段推算得到
在转账之前,通过用户输入的地址可以判断需要的输出类型,基于此来构建交易中正确的 ScriptPubKey
综合前面几种不同的支付类型就对应了不同的地址类型,最为简单的是支付到公钥的交易,其地址就是对方的公钥
P2PKH:支付到公钥的地址通过对公钥进行两次哈希,然后添加校验值后进行 base58 编码的方式得到,这样的地址通常以 1 开头,其生成流程如下图所示
这个过程也可以描述为:
1 | sha = SHA-256(pk) |
P2SH:支付到脚本哈希的地址针对脚本哈希值来实现编码,对于单一的地址,ScriptHash 是对 OP_DUP OP_HASH160 OP_PUSHBYTES_20 [pkh] OP_EQUALVERIFY OP_CHECKSIG
进行哈希所得的结果。对它们以以下列的方式进行 base58 编码,通常会得到以 3 开头的地址。
SegWit(P2WPKH)与 Taproot(P2TR)地址生成
隔离见证开始的地址都是比较统一的编码格式,它们的格式为 [readable][separator][bech32]
,这类地址也统称为 Bech32 地址
- readable: 人类可读的部分,主网为
bc
,测试网为tb
- separator:固定为
1
的分隔符 - bech32:经过 bech32 编码对公钥哈希进行编码得到的结果,并且前面加上版本号
0x00
或0x01
不同的版本号会对应到不同的地址,
0x00
对应了bc1q
类型的地址,0x01
对应了bc1p
类型的地址
在 P2WPKH 中,最后的 bech32 是对公钥哈希的编码,这是由于此时还没有 Taproot 这样的支付方式来将两种支付路径统一起来
而在 P2TR 中,不是对公钥哈希的编码,而是将公钥和一些额外的数据进行哈希,得到一个 tweak(详见 BIP-0341),再将它添加到原始公钥上得到新的公钥
attention
P2TR 中使用的是改进后的 bech32m 编码,这是由于 bech32 编码存在的缺点:如果地址的最后一个字符是 p,那么在 p 之前的位置插入或删除任意数量的字符 q 都不会使校验和失效
地址通常在区块浏览器和钱包这两个场景出现
在区块浏览器中,地址通常是通过输出中的 ScriptPubKey 判断支付类型后,再按照对应的地址生成格式得到地址
而在钱包中,则是用户在输入接收方地址后,根据地址判断所需要使用的支付类型,再按照对应的方式构造交易