比特币
密码学基础
现代密码学中,我们通常要求一个加密的系统满足一下几点要求(CIA):
- Confidentiality,保密性,及信息不能泄露给不应该知道这个信息的人。
- Integrity,完整性,及数据在传输的过程中不能被篡改,如果被篡改,通信双方应该立马能发现。
- Accessiblity,可通达性,及系统应该免于DDos等攻击,通信双方可以保持通信畅通。 (也有说Authority的,权威性,及保证信息来源真实可信)
现代密码学和密码加密技术通常被分为两类:对称加密和非对称加密。对称加密是指用于加密和解密的密钥是一样的,对称加密中加密和解密通常是“对称”的操作。非对称加密中,加密和解密的密钥是不一样的。非对称加密中,存在一对密钥,其中一个用于加密,另一个用于解密。用其中一个密钥加密的信息,只能用另一个密钥解密,而反过来用另一个密钥加密的信息,也只能由当前密钥解密。
对称加密中又通常分为流加密(stream cipher)和块加密(block cipher),常见的流加密算法有chacha20。chacha20 中,通过一段固定的伪随机 2 进制流生成算法,用密钥作为伪随机生成器的种子,生成一段由01组成的2进制比特流。加密者将要加密的明文跟刚生成的 2 进制比特流做 XOR 操作,就能得到加密后的密文。而解密者只需要拿到密钥,用相同的伪随机2进制生成算法和密钥(也是伪随机生成算法的种子),就能生成相同的比特流。解密者此时只需将比特流跟密文做 XOR 操作,就能得到加密前的明文。
XOR(通常表示为⊕)操作具有对称性,例如我们的明文是010010,我们的生成的比特流是011010,那么加密后,我们能得到001000。而解密时,我们只需要将密文同相同的比特流做XOR操作,就能恢复明文,即 001000 ⊕ 011010 = 010010。
常见的块加密算法有 DES 和 AES 算法,以 DES 算法举例,需要加密的明文被分为多个一定长度的小块(DES中,是64 bit,如果长度不足,用0填充),每一块经过16轮的一定规则的替换和重新排列,被加密成密文,而置换和重新排列的规则,是由密钥决定的。解密时,解密者通过密钥得到相同的替换和重新排列规则,解密者通过这些相同的规则,能将密文还原成明文。
非对称加密中,常见的算法有RSA加密算法和ECC椭圆圆锥曲线加密算法。非对称加密比起对称加密,不仅可以用于数据加加密,还可以用于数字签名,用于保证数据的完整性和权威性。非对称加密,也是区块链中,最常用的加密方式。
RSA加密
RSA算法的数学理论支持来源于欧拉定理,欧拉定理描述如下:
其中:
根据这个定理,我们可以得出RSA算法,RSA密钥生成算法具体步骤如下:
- 选取两个较大的质数p和q
- 计算p和q的乘积,n = pq
- 计算 φ(n) = (p-1)(q-1)
- 随机选取一个正整数 e (与 φ(n) 互质且大于1,小于φ(n)),计算d,使得:ed ≡ 1 (mod φ(n))
- 选取 ( e , n ) 作为公钥, ( d , n ) 作为私钥
用公钥加密的信息只能用私钥解密,而反过来私钥加密的信息只能通过公钥解密。密钥的生成者通常会将公钥公开,而将私钥秘密保存。当互联网上有人想要给密钥的生成者发送加密信息时,通常会先找密钥的生成者拿取其公钥,然后发送者会用公钥加密加密想要发送的信息 M,此时生成的密文 C 只能用私钥还原。这样就避免的信息M被互联网上其他的人知道。
如果有攻击者想要通过公钥( e , n )计算出私钥,那么他首先要计算出φ(n)。 而想要计算出φ(n),攻击者只能通过暴力破解穷举 n 所有的质数因数。实践中 n 通常是一个十分巨大的数字(例如现在HTTPS证书上常见的公钥 n为一个2048位的二进制数字),暴力破解需要耗费攻击者大量时间和算力。因此密钥拥有者只需要定期更换密钥,就能保证其信息的安全。
当需要得到密文 C 时,加密者只需要计算明文 M 的 e 次方模 n:
而当解密者需要还原被加密的信息 M 时,他只需要计算 C 的 d 次方模 n:
其原理为:
其中:
ECC椭圆曲线加密
ECC 椭圆曲线加密也是非对称加密的一种方式,也是比特币中用的最多的加密方式。椭圆曲线在相同密钥长度的情况下,能做到比RSA更高的安全等级。
椭圆曲线 E 在数学上通常表示为:
其常见的例子以及函数图像有:

而在密码学中,常用的椭圆曲线表达式一般限定为(即二次项系数为 0):
在这样的椭圆曲线上,我们定义一套特殊的运算规则。我们定义在椭圆曲线上,有P和Q两点,其 P + Q 的结果表示为PQ直线与椭圆曲线相交的第三个点 R’ 的关于x轴的对称点R。其几何表示为:

我们还定义,一个点的关于x轴的对称点为其负数,即 R’= - R。
如果P和Q是曲线上重合的两点,那么我们则计算椭圆曲线上p点的切线与曲线其他部位的交点 R’,然后计算R’关于x轴的对称点R。 我们记作 P + Q = P + P = 2P = R。如果计算3P,则是先计算 2P,然后计算2P + P = 3P。这样定于出来的加法和乘法符合交换律和结合律,即 P+Q = Q+P,(ab)P = a(bP)。在数学上,我们称具有这种性质的集合及其运算规则为阿贝尔群。
连续的椭圆曲线比较容易被找到规律,并不适合用于加密。为了实现离散的结果,我们取椭圆曲线内一些离散的点作为集合。为此,我们把椭圆曲线定义在离散的域{0,1,2,…,p-1}内,其中p为一个较大的质数,其通常表达为:
并将其操作定义如下:
- 其加法规则如下:
例如:
补充:
- 其取反规则如下:
例如:
- 其乘法规则如下:
例如:
- 其除法规则如下:
下图是上述计算的计算结果的图形表示,这样定义出的集合与运算规则也符合阿贝尔群,而且比起连续的椭圆函数,更不可预测。

在这样定义出来的阿贝尔群有个特殊的性质,群内有点存在最小正整数n,使得nP = P,即一个点的n倍等于其自己。例如上图中的 27P = (3,13)=-P,28P = (3,10) = P,这样我们则称 28 为P的“阶”。我们也把P ~ 28P这些点叫做循环阿贝尔群,同时也写作:
如果定义域内的点不存在一个n,能使得,则我们称P是无限阶的。
椭圆曲线加密算法所有使用的就是循环阿贝尔群。在椭圆曲线加密算法中,我们选取一个有阶的点G,即;我们在选取一个小于n的数字k,并且计算出
。这样的计算满足one-way trap-door函数的性质:
- 根据加法法则,如果我们拥有k和G,则计算出K相对简单。
- 但是如果我们只知道G和K,而求解出k则困难很多
这里我们称G点为基点(base point),k为私钥(private key),而K为公钥(public key)。
ECC椭圆曲线加密算法具体如下:
- Alice选取一条指定椭圆曲线E,以及椭圆曲线上一点G,使其满足
- Alice随机选择一个私钥k(k<n),并根据私钥生成公钥
- Bob想要同Alice通信,他请求Alice的公钥(椭圆曲线E,公钥K,基点G)
- Bob将想要发送的信息编码到椭圆曲线上的一点M,并生成随机数r (r<n)
- Bob计算密文
,
, 并且将C1,C2传输给Alice
- Alice只需计算
,就能得到加密前的明文 M
Alice能还原出M的原理如下:
其中:
比特币使用secp256k1作为其圆锥曲线加密的方案,在这个具体实现中,其选取的椭圆曲线为:
其选择了一个很大数质数作为p,其值为:
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F 即:
在secp256k1中,其基点G定义如下:
其中:
Gx = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
Gy = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8
而G的阶n为:
n=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
签名原理
数字签名也被广泛用于比特币系统中。数字签名不仅可以保证数据的权威性,确保数据的来源可信,还能保证数据的完整性,防止签名的数据被篡改。在非对称加密中,我们使用公钥加密的信息,只能被私钥解密。而在签名算法中,我们使用私钥加密文件的哈希值,这样,只有拥有私钥的人可以签名文件,而任何人都可以同过使用公钥解密签名,以此验证签名的有效性。
在RSA非对称加密算法中,我们通过算法生成了一对公钥(e,n)和私钥(d,n),使用一个钥匙加密的信息,只能用另一个钥匙解密。而RSA签名算法中,我们使用私钥加密需要签名文件的哈希值。任何想要验证签名的人,都可以使用公钥解密签名,验证者只需比对解密后的信息与文件哈希值,就能验证签名的有效性。
签名者生成文件签名的流程如下:
- 签名者通过RSA算法生成一对用于签名的公钥
(e,n)和私钥(d,n) - 签名者计算需要签名文件的哈希值
- 签名者生成签名 S:
- 当有人需要使用文件时,签名者需将文件本身 File,文件签名S,以及验证签名的公钥
(e,n)一并传送给使用人。
当任何人想要验证签名时,他需要计算:
- 计算文件的哈希值
- 使用签名者的公钥解密签名,得到H2
- 验证者通过比较
和
,就能知道该签名的有效性:如果H1同H2一样,那么签名是有效的;如果
同
不一样,那么签名无效。
其具体流程,可以概括为下图:

在签名过程中,使用哈希函数对需要签名的文件生成摘要是非常有必要。如果直接对原文件签名,将会引起诸多问题:第一个是如果文件过大,那么加密和解密的过程将浪费大量计算资源;第二个是攻击者可以使用存在性伪造攻击(Existential Forgery Attack)来迷惑验证者,让验证者误以为攻击者拥有私钥。攻击者虽然无法指定信息的内容,但是仍可以生成有效签名。存在性伪造攻击过程如下:
- 攻击者可以随机生成一条假签名 S’,并计算其对应的假消息M’:
- 攻击者可以通过暴力破解,穷举S’,使其找到一条M’与其对应,并且M’具有实际意义
椭圆曲线同样可以用于签名算法,并且使用椭圆曲线的椭圆曲线签名算法(Elliptic Curve Digital Signature Algorithm,ECDSA)比RSA签名算法更高效,更安全。生成椭圆曲线签名的步骤如下:
- 通过椭圆曲线加密算法生成一对公钥(公钥K ,基点G,椭圆函数E)和私钥k
- 求需要签名的消息M的哈希值
- 选择一个随机的整数 d(大于零,小于n)
- 计算点
,其中
- 计算r,如果r=0,则重新选则d并重新计算
- 计算s,如果s=0,则重新选择d并重新计算
- 将消息M,签名
(r,s)以及公钥(K,G,E)一并发送给验证者。
而验证签名,只需要计算:
- 消息的哈希值
- 计算 w:
- 计算u1和u2:
- 计算点R:
- 计算v:
- 比较v和r,如果v和r一样,则签名有效;如果不一样,则签名无效
比特币的账户设计
比特币中,账户通常就指的是一对公钥和私钥。这对公钥和私钥,既可以用来签名,也可以用来加密。比特币中,我们用公钥标识一个钱包。在不同的转账模式之下,我们用不同的算法对公钥进行一些运算,并将运算结果叫做钱包的地址(一个钱包可以有多个地址,一个转账模式对应一个地址),例如在P2PKH(Pay-to-Public-Key-Hash)转账模式之下,我们把公钥的哈希值作为钱包的地址;在P2PK(Pay-to-Public-Key)转账模式之下,我们直接使用公钥作为地址。以下是在P2PKH转账模式之下,地址的具体生成流程:
- 对公钥进行SHA256哈希运算, 再进行RIPEMD-160哈希,得到公钥哈希值
- 在公钥哈希前添加一个字节的版本前缀,例如(0x00表示比特币主网),得到地址
- 再对公钥哈希进行两次SHA256哈希运算,其前四个字节作为校验和,添加到地址结尾
- 对地址进行Base58Check编码,等到最终地址
交易
比特币作为一种电子货币,本质上是一个公共的去中心化的分布式账本。在这个网络中,每个人都拥有一个账本。当交易发生时,交易双方向网络中广播自己交易。所有收到广播的人,都会将交易记录在自己的账本上。比特币运用密码学和各种算法,确保了分布式账本内容的公平和一致。与一般记账账本不同的是,比特币账本,并不直接记录每个参与者的余额,而是记录网络中发生的每一笔交易,例如“Alice给Bob转账了300元”。当有参与者想要知道自己账户余额时,他需要找当账本内每一本与自己有关的交易,然后挨个计算,最后才能得到自己账户的余额。账本本身是一个由首尾相连的区块所组成的区块链。每个区块,都相当于账本内的一页账目,记录了比特币网络中发生的每一笔合法的交易。而交易,则像是每页账目中的条目,具体记录了每一笔钱从谁转账给了谁。
交易结构和UTXO
在比特币中,每一个交易被具体分为了交易的输入CTxIn和交易的输出CTxOut,即类似 “Alice(交易的输入) 给 Bob(交易的输出) 转账了 300元”的格式。其中CTxIn包含了转账人的地址和转账数额(例如上例中的300元),CTxOut包含了收账人的地址以及收账数额(例如300元)。在这里,转账的数额与收账的数额不一定相等,例如:Alice可能转账了300元,而Bob只收帐了290元——这其中10元的差额,会被作为转账的手续费,交给矿工,即记账者(这部分后面会讲到)。当然,转账的数额不可能比收账的数额小,这样的交易是不会被矿工承认的。
以下是比特币源码中,关于交易部分的代码:(以下所有代码所使用的比特币项目的源码链接:https://github.com/bitcoin/bitcoin.git,Accessed On: 2024/6/27)
1 | /** The basic transaction that is broadcasted on the network and contained in |
从以上代码可以看出,每笔交易中包含了多个交易的输入vector<CTxIn> vin,多个交易的输出 std::vector<CTxOut> vout,交易版本信息 version,以及一个用于额外添加限制条件的时间戳 nLockTime。交易版本信息是用去区分不同版本的交易规则的,比特币网络中的协议并非一成不变,而是随着时间演变的。当然网络内协议的改变有可能导致更新前后不兼容,从而导致链分叉、社区分裂等问题(这部分问题会在后面讲到),所以比特币社区对与协议的更改是比较保守的。nLockTime是用于控制交易被加入到区块中的时间的,例如交易双方想延迟交易,就可以设置这个字段,使交易只有在一定事件后才被打包入账本。当nLockTime为零时,代表交易者希望当前交易被尽快打包入区块内。而当其是一个小于500 000 000的值时,这个字段意味着这个交易在整个区块链达到这个长度前,都不希望被打包入区块;当这个字段高于500 000 000时,这个字段则被解释为Unix时间戳(自1970年1月1日后流逝的秒数),意味着在当前时间早于这个时间前,交易双方都不希望这笔交易被打包入区块。交易结构体中多个交易输入输出意味着可以出现以下情形:Alice转入了200元,Bob转也转入了200元,支付给Charly 300 元,同时也支付给Daren 95元,以及其中的差额 5元。其中的差额5元会被自动奖励给打包这笔交易的人。
比特币使用未花费交易输出模型(Unspent Transaction Output, UTXO)作为其交易模型。在这个模型中,用户想要使用比特币,只能以UTXO作为单位。UTXO,未花费交易输出,对应着区块链中每一笔交易中的每一个输出。例如上例中支付给Charly的300元和支付给Daren的95元,都是一个UTXO。我们把支付给Charly的300元,叫做属于Charly的一个UTXO,其面值为300元;支付给Daren的95元,属于Daren的一个UTXO,其面值为95元。当Charly想要花费这个属于他的UTXO时,他需要将整个UTXO作为交易的输入,即一次性将者300元转出去。这样的设计虽然没有直接转账一定数额的灵活和精确,但是可以保证每笔钱来源都可以被追踪。当然,这种交易模式也不一定要求Charly只能一次性使用300元,当交易所需的金额小于300元时,Charly可以创建一笔这样的的交易:Charly转入300元,转给Bob 200元,转给Charly自己95元(同样,多余的5元会被当做手续费转给矿工)。这样,Charly可以通过创建交易拆分或者聚合UTXO,将一个面值大的UTXO拆分成几个面值更小的,或者将几个面值更小的UTXO聚合成一个面值更大的UTXO。
需要注意的是,只有被打包好上链的交易的输出,到才能被称作是UTXO。这意味着,还没有被打包上链的交易中的输出,是不能被立马使用的。而在实践中,即便是以及被打包好的交易,也是可能被推翻的(链有可能会分叉,被消灭的分链上的交易会被要求重新打包)。因此,在实践中为了确保交易的安全(即当前交易一定生效),社区要求UTXO的拥有者只有在打包当前交易的区块后面,又有人逐个添加了6个区块后,才能花费其中的输出。
以下是比特币具体实现中,每一个输出CTxOut的具体的内容:
1 | /** An output of a transaction. It contains the public key that the next input |
在比特币的具体实现中,每一个输出包括了一个转账金额CAmount nValue以及一个脚本CScript scriptPubKey。其中CAmount是一个有符号的64位整数,有符号意味着其可以为负数。但比特币网络中的转账金额是不能为负数的。这里的负数是为了比特币客户端内计算方便所设计的。同时,转账金额的单位为”聪“(satoshis)。这里是人为规定的,100 000 000聪为一个比特币。计算机中的小数是不精确的,如果直接用“0.001 比特币”这样的计数方式表示网络中的比特币,会导致出现精度不够,从而引发错误。 这样用更小单位分割每一个比特币的方法方便我们对网络内的比特币进行细小精确的分割。以下是关于CAmount具体的代码。
1 | /** Amount in satoshis (Can be negative) */ |
在比特币网络的输出中,为了确保转账交易的匿名性,我们并不直接注明收款人的姓名或者是地址。比起直接注明交易人的姓名地址,我们给每个输出加上一个锁,任何想要花费这输出的人,需要提供这个锁相应的钥匙才能花费这个UTXO。这里的锁,就是CTxOut中的CScript scriptPubKey。我们称这里的scriptPubKey为加密脚本(locking script)。而想要花费这个UTXO的人,需要提供相应的解锁脚本(unlocking script)。在密码学中,我们称类似的操作为Challenge-Response,即挑战和回应。这类操作通常包括一个人发起挑战,这个挑战一般只有特定的人能应答。然后一个人通过正确地给出答案,证明自己的身份。例如Bob提出问题,”Alice早上吃了什么“这类只有Alice能回答的问题。如果对面的人能正确回答问题,那么就能证明其Alice的身份。在计算机中,我们能够通过现代密码学中的非对称加密做到类似操作,例如Bob想要确定以自己通话的人的身份,那么它可以发起一个Challenge-Response:Bob用Alice的公钥加密一个随机数字,如果对面能还原这个随机数字,那么我们就能确定其Alice的身份(假设Alice的私钥只有她自己知道)。
在比特币的具体实现中,每一个交易的输入CTxIn都包含了一个指针COutPoint prevout,指向之前一笔交易其中的一个输出,验证脚本CScript scriptSig(即解锁脚本,unlocking script),用于限制交易的uint32_t nSequence,以及用于隔离见证的脚本CScriptWitness scriptWitness。以下是CTxIn的源代码:
1 | /** An input of a transaction. It contains the location of the previous |
其中COutPoint代码如下:
1 | /** An outpoint - a combination of a transaction hash and an index n into its vout */ |
在COutPoint中,我们保存了前一个交易的哈希值。通过这个哈希Txid hash,我们就能找到包含这个输出的交易。然后保存了一个索引uint32_t n。这个索引可以帮助我们找到当前UTXO所使用的是上一笔交易输出中的第几个输出。
交易的验证 比特币脚本
比特币中的锁定脚本的生成与验证的方法并不是固定的,比特币为脚本的验证提供一个简单的基于栈的虚拟机。这个虚拟机能够执行一些简单的命令。因此比特币能够执行相对复杂多样的脚本验证,让比特币中的脚本类型多种多样。其中最流行最广泛应用的比特币脚本机制叫做P2PKH和P2SH。
在P2PKH(Pay to Public Key Hash)中,锁定脚本收账人公钥的哈希值,解锁脚本是该UTXO拥有者的签名以及其公钥。例如,Alice想要给Bob转账200个比特币,Alice为了创建这笔交易,她需要先知道Bob的公钥的哈希值。在转账时,Alice需要附上Bob公钥的哈希值以表示这笔钱是转给Bob的。当这比交易被打包上链后,此时Bob就拥有了一个面值200的UTXO。当下次Bob想要花费这个UXTO的时候,他需要附上自己对使用UTXO这笔交易的签名,以及自己的公钥。当网络内有人想要验证Bob这笔交易时,他则可以计算Bob公钥的哈希,同时使用Bob的公钥验证Bob对这笔交易的签名。如果Bob公钥的哈希跟UTXO中的对的上,则证明该UTXO确实属于该公钥所属的账户。如果交易的签名有效,则证明Bob确实是这个账户的拥有者。在实际过程中,如果Alice想要在比特币网络中给Bob转账,那么她需要先一步向Bob请求一些信息。例如在P2PKH中,Alice要先知道Bob的公钥哈希才能给Bob转账。
在执行验证脚本的过程中,验证者会用到验证虚拟机。Alice在转账给Bob时,会在锁定脚本locking script中指定验证脚本的方式。Alice会在锁定脚本中加入验证虚拟机的指令,最后产生的锁定脚本如下所示:
1 | OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG···、、······· |
其中OP_DUP OP_HASH160…则是虚拟机内置的指令,而
1 | <signature> <public key> |
验证虚拟机在执行验证时,首先会初始化一个栈,然后分别执行解锁脚本和锁定脚本。其执行过程如下图所式:

其详细执行过程解释如下:
- 先执行解锁脚本,虚拟机将
<signature><public key>分别压入栈, 此时栈内有(左边为栈底,右边为栈口):
[<signature> <public key>
- 然后执行锁定脚本,执行锁定脚本中的第一个指令:
OP_DUP,这个指令将复制栈顶的元素,此时,栈内有:
[<signature> <public key> <public key>
- 虚拟机再执行
OP_HASH160,这个操作会对弹出栈顶的第一个元素,对其做哈希运算(先算SHA256,再算RIPEMD-160),最后将运算结果压入栈中。此时栈内:
[<signature> <public key> <pubKeyHash>
- 将锁定脚本中的
<pubKeyHash>压入栈中,此时栈内:
[<signature> <public key> <pubKeyHash><pubKeyHash>
- 执行
OP_EQUALVERIFY,这个操作会弹出栈顶两个元素,然后比较他们。如果比较结果是相等,则继续执行。如果不相等,在返回验证失败。此时栈内:
[<signature> <public key>
- 最后执行
OP_CHECKSIG,这个操作会依次弹出栈顶的两个元素,弹出的第一个元素作为公钥,第二个元素作为签名,然后验证签名。执行后的结果会被压入栈中,如果为true则代表验证成功。
[true
另一个流行的脚本为P2SH(Pay-to-Script-Hash),在这个脚本中,收账方需要提供一段脚本的哈希作为锁定脚本。在使用UTXO时则需要提供该脚本与运行该脚本所需的额外数据。具体格式如下:
- 锁定脚本:
OP_HASH160 <redeemScriptHash> OP_EQUAL - 解锁脚本:
<unlocking data> <redeemScript>
这里所说的<redeemScript>指的也是比特币脚本,<unlocking data>则指的是签名,公钥等运行脚本所需的数据。当验证者需要验证P2SH脚本时,其不仅要验证脚本本身,还要验证脚本内的<redeemScript>脚本。
除了这些流行的脚本,还有一些比较有意思的脚本,例如多重签名脚本(MultiSig)。要解锁这个脚本,需要收集多方签名。其脚本内容具体如下:
- 解锁脚本:
<OP_0> <signature1> <signature2> ... - 锁定脚本:
OP_2 <pubKey1> <pubKey2> <pubKey3> OP_3 OP_CHECKMULTISIG
例如某些比特币是几个人的共有财产,则可以使用这个脚本。只有当三个人同时允许交易时,交易才能有效。
区块
比特币中的区块是为是为了打包交易而存在的。比特币中的区块可以保证区块链中已经发生的交易不可篡改,不可撤销。这样就能保证发生过的交易的有效性。当交易双发达成一致后,就会向网络内广播自己的交易,每笔交易内通常有一定数额用于支付给矿工的手续费(即交易输入和输出之间的差距)。网络内的矿工会收集网络内的所有交易(矿工在接收到新的交易后,会将交易储存在本地数据库LevelDB中),并在构造新的区块的时候将其打包至新区块内。这样,矿工就能在网络内协议的允许下,将所有自己所打包的交易的手续费转账给自己。矿工会创建一个新的交易,这个交易没有输入,只有输出。并且,交易的输出数额,是新区块的奖励(例如25个比特币)以及所有所打包的手续费(例如30.124个比特币)之和,输出内的地址是矿工自己的钱包地址(其实是个只有矿工自己能解锁的锁定脚本)。我们把这样的交易叫做Coinbase交易,并且规定Coinbase交易总放在区块链所有所打包的交易的前面,即整个区块的第一个交易。比特币网络中并没有规定区块一定要打包交易,也就是说矿工可以构造一个不携带任何其他交易的新区块。因此我们引入交易手续费作为网络内矿工打包交易的激励之一,这样矿工才更倾向于打包网络内的交易而不是构建空的区块。以下是比特币中,关于区块的源码:
1 | class CBlock : public CBlockHeader |
这个CBlock类是继承至CBlockHeader,这意味着CBlock同样也包含CBlockHeader内的属性。CblockHeader的代码如下(这部分代码违反了“组合优于继承的原则”,不是很好读):
1 | /** Nodes collect new transactions into a block, hash them into a hash tree, |
总结一下以上代码,一个区块内包含的信息如下:
| 区块信息 | 解释 | |
|---|---|---|
| 区块头 | int32_t nVersion |
版本信息,比特币协议可能会随着时间升级 |
uint256 hashPrevBlock |
前一个区块的哈希值 | |
uint256 hashMerkleRoot |
一个哈希值,叫默克尔树树根,从区块体内所有交易的哈希值而来,确保了区块内交易的有效 | |
uint32_t nTime |
一个Unix时间戳,代表这个区块在挖出来时的时间 | |
uint32_t nBits |
代表了矿工在挖当前区块时网络中的挖矿难度 | |
uint32_t nNonce |
一个用于挖矿的随机数字 | |
| 区块体 | std::vector<CTransactionRef> vtx |
一个保存了所有交易的数组 |
比特币区块的版本和交易的版本一样,是随着时间动态升级的。同时版本的改变,也会带来链分叉、社区分裂等诸多问题。比特币中,每一个区块内的都保存了前一个区块的哈希值。一旦之前有一个区块的哈希值改变了,其后面所有的区块哈希值都会改变。这样当我们拥有了一个区块,并且确认这个去区块是可信的之后,我们就能挨个往前检查所有区块,并且不用担心区块是被伪造的。这样就形成了一个由区块组成的链条,我们把这种数据结构叫做区块链。我们可以随着这个链往前一直追溯到整个链开始的地方,网络中的用户只要拥有一个可信的区块,就能通过密码学原理保证这个区块前所有区块的可信性。这个机制大大减少了网络内信任的成本。整个比特币网络中的区块链开始的区块是由中本聪(Satoshi Nakamoto)在 2009年1月3日创立的,我们把这个区块称作创世区块(Genesis Block)。如果有人想要伪造另一条完整且有效的链,他需要足够的计算资源完成自2009年1月3日以来,所有矿工完成的工作量——这在现有的物理条件下是几乎不可能的,只有未来当量子计算机大规模普及后才有可能对使用PoW算法(即我们常说的挖矿算法)的区块链造成毁坏。并且,如果真的有机构或者个人拥有这么庞大的计算资源,那么其加入比特币网络会比破坏比特币网络拥有更多收益(这个以下会讨论)。
默克尔树 Merkle Tree
uint256 hashMerkleRoot这个字段是一个哈希值,其代表了一个数据结构默克尔树(Merkle Tree)的根节点。默克尔树是一个由哈希值组成的类似完全二叉树的数据结构。默克尔树可以保证区块内所有所打包交易的完整性(Integrity),同时默克尔树这样的数据结构还能帮助验证者快速验证任何一个交易的完整性(Integrity)。

默克尔根的计算过程如下:
- 例如我们现在区块内有
四个交易
- 首先我们的分别计算四个交易的哈希值
- 接着,我们计算
等两两拼接起来的哈希值;如果这一步只有奇数个哈希值,那么,两两配对的时候会多出一个哈希值来,此时我们计算其和自己拼接起来的哈希值,即
。其中
||符号代表了简单的拼接操作,例如, - 我们可以重复以上步骤,直到最后所有的哈希值都被聚合称一个哈希值(在上例中,即为
),我们称最后的哈希值为默克尔树的根。而我们刚刚构建出来的树状数据结构为默克尔树。
当区块内的任何一个交易被改变,都会引起默克尔树根的改变。因此,只要保证了默克尔树根的有效,我们就能保证整个区块内的交易数据不被篡改。当我们想校验其中一个交易时,我们只需要重新计算
这三步操作,减少的校验的步骤。
以下是比特币中,关于计算默克尔树的源码:
1 | uint256 ComputeMerkleRoot(std::vector<uint256> hashes, bool* mutated) { |
比特币中的源码提及,这样的算法可能会导致一个致命的漏洞:当区块中的交易数量为奇数时,打包交易的矿工会将其中的一个分支复制一次后计算哈希值。例如以下例子中,当区块内打包的交易为[1,2,3,4,5,6]时,其生成的默克尔根和打包交易为[1,2,3,4,5,6,5,6]没有区别。但是当交易为 [1,2,3,4,5,6,5,6] 的时候,其他的矿工不会承认这个新的区块,因为区内有重复的交易,这样会产生两个相同UTXO。攻击者可以通过向网络内广播[1,2,3,4,5,6,5,6]这个区块,导致这个区块被其他矿工拒收。之后当矿工接受到 [1,2,3,4,5,6]这个诚信的区块时,矿工通过比较区块头发现这个区块和之前拒收的区块区块头一样,是同一个区块。这时矿工就会拒收这个区块,导致网络内诚实的区块不被承认。
1 | A A |
PoW 算法
以上区块字段中,uint32_t nBits和uint32_t nNonce都是跟PoW算法相关的字段,第一个字段nBits决定了挖矿的难度;而另一个字段nNounce,是一个随机数。矿工需要通过不断调整这个字段来得到一个新的哈希值,直到找到一个哈希值满足挖矿难度。PoW,Proof of Work,工作量证明,同时也是我们通常说的挖矿,是一个求解困难,但是验证容易的算法。在比特币网络中,矿工们需要通过不断调整nNounce字段,计算区块头的哈希值(在实际代码中,是对区块头进行两次SHA256哈希运算。即对原先的区块头进行SHA256运算,得到一个256位的哈希值,再对256位的哈希值再进行一个SHA256运算),以期待找到一个哈希值可以比目标挖矿难度更小的哈希值。有的时候,即使遍历完所有可能的nNounce也找不到一个有效的哈希值,这时矿工需要调整区块内其他数据来继续挖矿,例如重新选择要打包的交易,或者更新时间戳。但验证者只需要将整个区块头哈希一次,就能验证区块的有效性。
目标难度的哈希值,是通过nBits字段计算得到到。nBits是一个32位的二进制无符号整型,其是目标难度的压缩表示。我们通过以下步骤计算出目标难度:
- 我们把这个32位无符号整型分为四个字节
,
,
,
,例如
nBits = 0x1b0404cb,那么 :
=0x1b,
=0x04,
=0x04,
=0xcb
- 计算目标难度,即:
在这个例子中,即:
比特币中,压缩表示nBits与target difficulty互相转换的代码如下:
1 | // 从压缩表示转换称目标难度 |
矿工在挖矿时,必须找到一个哈希值,使其小于目标目标难度,即这里的。以下是关于检查区块中PoW是否有效的算法:
1 | bool CheckProofOfWork(uint256 hash, unsigned int nBits, const Consensus::Params& params) |
在比特币中,挖矿的难度是动态调整的,每隔2016个区块调整一次。比特币网络,通过动态调整挖矿难度,将每一个区块的生成时间控制在10分钟左右。这样就可以确保比特币网络中的交易以一个固定的速率打包。比特币将每个区块最最大大小限制在1 Mb,也就是说一个区块最多能打包 2000 到 3000 笔交易。我们可以通过这个数据估算出,比特币网络处理交易的速度大概是每秒处理3.34 到 5笔交易,即 3.34 Tps ~ 5 Tps(Transaction per second)。这样的交易处理速度是远远赶不上如今主流网购网站的交易速度(例如每年双11每秒交易成交量可达60万笔,即 600000Tps)。因此,比特币远远称不上是一个实用的电子支付系统。比特币网络中条件挖矿难度的算法如下:
即网络中算力越多,挖矿难度越大;网络中算力越少,挖矿难度越低。网络中每个人实际挖到区块的可能性等于其算力在网络中的占比。
coinbase交易
在比特币协议里,我们约定俗成每个区块的第一个交易为coinbase交易,用来奖励矿工付出的工作量。这个交易没有输入,只有输出。用来奖励矿工的奖励币分为两种,分别是所有交易的手续费,以及补贴。交易手续费为所有打包交易的输入和输出之间的差额;补贴即为新发行的比特币,比特币通过这种方式向网络中发行新的货币。交易的补贴会随着区块的整张逐渐减少到0,在挖矿补贴减少到0后,矿工打包新区块的收益就只剩下交易手续费。
在比特币中,如果当前新挖出来的区块被网络内的成员普遍认可,那么在下一个区块的开采中,新的区块就会被添加到我们刚挖出来的这个区块的后面。我把这种情况叫做我们的区块收到了一个确认(confirmation)。比特币协议规定,所有coinbase交易产生的UTXO,必须收到100个确认后才能被花出。而社区内为了确保交易的安全,也规定所有常规的UTXO,即普通交易产生的UTXO,必须在收到6个确认后才能被花出。
区块的奖励每过 210000 个区块减半一次,并且中本聪在2009年开采第一个区块时,将奖励定在了50个比特币。于是比特币网络中所有比特币数量可以表示为:
这是一个收敛的几何无穷级数,其收敛于,即网络中比特币的总量为固定的 21 000 000 枚。这个性质赋予比特币类似黄金的金融属性——限量,稀有。以下是比特币中相关源码:
1 | CAmount GetBlockSubsidy(int nHeight, const Consensus::Params& consensusParams) |
其中,consensusParams.nSubsidyHalvingInterval定义在src/kernel/chainparams.cpp中:
1 | class CMainParams : public CChainParams { |
时间戳以及其验证
比特币协议中,验证者要求新产生的区块是时间戳必须大于前11个区块的时间戳中值,小于验证者本地网络时间加上2个小时。其代码如下:
1 | /** Context-dependent validity checks. |
当矿工想要开始挖矿时,他会从自己数据库中存储的未打包的交易(我们通常称为交易池)中选出一些交易——矿工为了利益最大化,通常会按一定规则选择交易,例如按照手续费的高低,或者每字节的手续费等评判标准——然后计算手续费之和,加上下一个区块的补贴,生成coinbase交易。之后矿工会计算所有交易的默克尔树,接着向网络中核对区块头内其他信息,例如版本,时间,和难度。最后,矿工就可以通过不断调整nNonce以期望找到一个符合目标难度的哈希值。如果该矿工找到了哈希值,那么他就可以向网络内广播自己新挖出的区块,当这个新区块被网络内的大多数节点验证接受后,这个矿工就挖矿成功了,这时,它可以获得这个新区块的所有收益。而当网络内有其他矿工先一步找到下一个区块时,这个矿工就必须放弃之前所有的工作,从交易池中删除已经被打包好的交易,重新选择一些交易。
在现实中,挖矿的收益是非常不确定的。一旦有人先一步找到了一个合法的区块,那么之前所有付出的努力都将白费。因此,在现实中,矿工们会自发的组成矿池,一但某个矿池发现了一个合法的区块,那么挖出该区块所得到的收益都将按照贡献的算力分配给矿池内的每一个矿工。加入矿池并不能提高挖矿收益的期望,但是可以显著降低收益的方差,让挖矿的收益更稳定。
共识算法
区块链网络是由矿工们组成的一个P2P(点对点)网络。在这个网络中,并没有一个处于中心位置的节点。每个网络中的参与者(我们称作节点)都在本地维护了一份公共账本的副本。为了维护一个公共的交易系统,我们需要所有节点的账本保持一致。为了保持网络中所有节点的账本的一致性,网络中的所有参与者需要达成共识。
最简单的共识算法就是一个泛洪算法(没有PoW),每个人在收到区块后向网络内广播自己新收到的区块。为了避免泛滥,每个节点在接受到已接收过的区块时,需要直接丢弃。但这样的网络时十分脆弱的,攻击者不需要任何成本就可以伪造新的区块,一旦有攻击者向网络内广播伪造的新区块,网络内就会达成错误的共识。
在这个算法改进上,我们可以让每个节点从多个来源接受区块,选择相信最多人认可的区块。我们把这样的共识协议称为拜占庭协议。这样的共识网络需要网络内至少的节点是诚实的。但这样的网络容易受到女巫攻击,即攻击者可以一台电脑同时维持多个节点,从而在数量上占优。
比特币在拜占庭协议上,添加了PoW算法,即攻击者的投票权是通过其在网络中提供的算力占比决定的。我们称这样的共识为中本聪协议。攻击者在产生新的区块时,需要耗费大量算力成本和时间成本,这让攻击者不能轻易伪造区块,同时,多开多个节点也没有意义,一台电脑所能提供的算力是恒定的。这样的共识系统,只用保证网络内51%的节点(其实是拥有该比例算力的节点)是诚实的,就能保证整个系统的安全。
激励相容机制:中本聪在PoW上,进一步提出用虚拟货币奖励提供算力的挖矿者。这时,即便有人能凭借一己之力拥有网络内一半以上的算力,其遵守网络内的挖矿规则比破坏网络能获得更大的收益,攻击者也没理由破坏整个比特币网络。
共识算法的分叉问题
比特币网络硬性规定,只有最长的点才是合法的链,当同时有两条合法的链存在时,比特币中所有节点都应该保存较长的那一条。当比特币网络中同时有人挖出区块时,网络中其他节点会同时保存两个分支,直到其中一个分支比另一个更长。此时,较短的那条支链会被丢弃,其上打包的所有交易,都应该重新打包。
比特币网络中,协议的改变或许会导致链的分叉和社区分裂。例如,当新的版本发布后,网络内只有一部分矿工升级了节点,那么有可能会导致升级后的节点不承认没有升级的节点挖出来的新区块,或者没有升级的节点不承认升级后的节点挖出来的新区块。这样,他们都会在原有的链上继续挖矿,导致链的分叉。
其中区块链的分链分为硬分叉和软分叉。硬分叉是指由于新旧版本不兼容,导致比特币社区永久分裂,旧版本的节点会在原链上继续挖矿,而新版本的节点则在另一条分支上挖矿。例如,比特币协议版本的升级导致旧版本的节点不认可新版本节点挖出来的区块,但是新版本的节点是兼容旧版本节点挖出来的区块的,这时,新旧版本的节点同时在原有的链尾继续添加区块。当新版本的节点挖出新区块时,旧版本的节点会认为新区块是非法,而旧版本的节点挖出来的区块会被新版本承认。此时新版本的链会更长,因此所有更新的协议的节点都会沿着这个更长的链挖矿,而所有没更新协议的节点,都会沿着另一条分支继续挖矿。
软分叉是指协议升级而导致链的暂时分叉。例如升级协议后的节点不承认旧协议节点挖出来的新区块,但是就旧协议节点承认新协议节点产生出来的新区块。这样新协议节点挖出来的区块会在网络内被认可,而旧协议节点挖出来的区块却一直被抛弃,直到旧协议节点升级其协议。
PoW的51%攻击
如果攻击者拥有了整个比特币网络内51% 的算力,那么他可以对整个网络发起51%攻击。攻击者可以通过利用比网络内所有其他人都多的算力,将自己违法的交易打包进区块中,并且在违法区块后不断添加新区块,达到比网络中“诚实链”更长的的长度。这时,网络内所有其他节点都不得不接受更长的“违法链”。通过这种方式,攻击者可以做到:
- 修改自己的交易记录,双花问题
- 阻止区块确认部分或者全部交易
- 阻止部分或者全部矿工开采任何有效区块
- 阻止某些交易进入区块链
攻击者不可以做到:
- 修改他人交易记录,把不属于自己的比特币发送给其他人
- 阻止他人发送交易
- 改变每个区块比特币产生的数量
- 凭空产生比特币
如果一个攻击者拥有了全网51% 的算例,如果他遵守网络规则,他的收益是:
- 剩下所有的coinbase coin
- 未来所有的交易费用
- 需要很长时间才能获取这些收益
如果他选择破坏系统,那么他能获取最大收益的方式为,在别人发现前,将自己所有的钱多次花出去(双花/多花攻击)。这样他的收益为:
- 当前手上所有比特币的n倍,保守估计,如果他在第一次实行双花攻击的时候就被人举报,导致网络内信任崩塌,那么他所能拥有的额外收益为当前上手所有比特币的价值。(他能兑现的收益为手上所有比特币和双花攻击给他带来的收益)
- 相对短时间内就能获取这些收益
实际上,当攻击者拥有较少的比特币,那么他破坏网络的收益远赶不上他遵守网络规则的收益;而如果他拥有较多的比特币,那么他一次性将这些比特币全部花出去是不现实的。因此奖励虚拟货币的激励机制进一步强化了比特币网络的健壮性。
比特币网络
加入网络
比特币中,新节点往往依赖于硬编码在软件内的IP地址加入网络。新节点在第一次启动的时候尝试与硬编码在源码内的地址沟通。在建立连接后,新节点会尝试通过该节点获取网络内其他节点的IP地址,同时向其他节点请求同步网络内区块链的最新状态,这一过程通常需要下载几个GB的数据,是个耗时的过程。同步过程是从最新的区块开始的,然后逐渐向前同步,直到和整个网络中的区块链的状态一致。储存整个区块链的信息开销是巨大的,这样就对成为节点的设备有了限制。中本聪在论问题提出一种新型的轻型节点——SPV——让手机等计算能力有限的设备成为比特币节点成为可能。
轻型节点
比特币网络中,所有节点可以分为两类:一种是全节点,全节点储存了网络内的所有区块和UTXO;另一种是轻型节点,也叫简单交易验证节点(Simple Payment Verification Node,SPV Node),其值储存区块的区块头和自己的钱包。矿工节点必须是全节点,因为矿工需要依赖存储网络内所有交易来验证打包交易。轻型节点的存在让用手机转账比特币成为可能。
简单交易验证节点SPV正在发起交易时,需要验证交易内作为输入的UTXO是否有效。此时,SPV需要验证作为UTXO来源的交易。简单交易验证节点本身是不储存任何交易和区块的,因此,他需要向任何其他全节点请求UTXO的来源交易。同时,为了验证交易是否真的属于某个区块,它需要向全节点请求该交易到区块的默克尔根的默克尔路径。默克尔路径指得是从交易哈希到默克尔根的的一系列哈希值,例如下图中,想要验证交易1,则需要知道哈希值2,哈希值E,哈希值C(哈希值A是默克尔根,包含在区块头中)。简单交易节点接着就可以通过默克尔证明,证明交易1的有效性。例如通过交易1和交易2的哈希值计算出哈希值D;用哈希值D和哈希值E计算出哈希值B;最后用哈希值B和哈希值C就能计算出哈希值A。(A是已知的默克尔根)
1 | A |
简单交易节点通过计算一系列哈希运算,就能验证交易的有效性。自此简单交易节点就能够完善自己交易的信息,向网络内广播交易。
比特币存储LevelDB
比特币实际使用LevelDB作为其存储方式。LevelDB是由谷歌开发的一款简单高效的键值对存储数据库。其提供简单操作和高性能的读取:LevelDB提供三种基础操作,Put(key,value)——将数据以键值对的方式存储,Get(key)——通过键去除与其关联的值,和Delete(key)——删除某个键与其相关联的值。这个数据库还提供压缩数据和查看数据库内容的功能。LevelDB在接受到存储请求后,会将数据先存储在内存中的MemTable。当内存中MemTable的数据增长到一定规模,内存中的数据会被打包成一个不可变的SSTable(Sorted String Table)放入磁盘当中。当用户想要修改某个键值对时,这个键值对会直接被写入MemTable。当收到存储请求的时候,LevelDB会从MemTable开始,从较新的SSTable查找到较老的SSTable,这样就能保证查到的值一定是最新的值。为了优化磁盘空间利用率,LevelDB会定期合并磁盘中的SSTable,同时合并的过程中会删去旧的失效键值对。以键值对方式存储的数据库通常不包含数据关系模型,LevelDB也一样,其同时还不提供多进程操作,网络通信等功能。
比特币客户端在收到交易、区块、地址、或者在计算出余额和UTXO后,都会存储在本地数据库当中。比特币不直接将交易储存在数据库中,而是将交易作为区块的一部分,同区块头一起存储在数据库中。存储的区块的时候,使用的键(key)是区块的哈希值,值(value)是区块索引信息,例如区块头,区块高度等信息。比特币会将区块内的UTXO单独提取出来储存,以便在验证UTXO时能快速找到。UTXO在存储时,比特币客户端使用该交易的哈希值作为键(key),交易内所有输出(即该交易包含的所有UTXO)作为值(value)。
补充内容
证书 (补充至第一章)
从网络上获取的公钥并不能保证其来源的真实性,攻击者可以通过中间人攻击(Man-In-The-Middle Attack,MITM)篡改公钥,从而窃取信息。例如,Alice想要和Bob在互联网中加密通信,于是他们准备互换公钥。这时,攻击者可以伪装成Bob,与Alice互换公钥,同时也伪装成Alice,与Bob互换公钥。Alice发给Bob的信息,这时被发给了攻击者;攻击者将信息解码后,用Bob的公钥加密,再发给Bob。Alice和Bob在这个情况下都可以正常通信,并且对中间的第三者毫不知情。

为了避免这种情况,在现代互联网中,我们引入证书机制,来确保从网络获取到的公钥的真实性。即保证从网络中获取到的Alice的公钥,确实属于Alice本人。当Alice向互联网上其他人传输自己的公钥时,Alice还需要传输一份与她公钥相关联的电子证书。接受者只需要验证证书的真伪,就能确定Alice公钥的真实性。公钥的证书内容通常包括Alice的公钥,Alice的身份信息,证书的有效期,和证书的签名。验证证书的过程,即为验证证书电子签名的过程。证书的签名是电子签名,通常是由权威机构(Certificate Authority,CA)签下的。在验证证书的签名时,验证者需要获取签名机构的公钥。权威机构的公钥通常属于网络内的共识,难以被伪冒。
单个签名机构很难满足整个互联网的证书需求。为了满足整个互联网的证书需求,签名机构被拓展为树状层级结构。在这个树结构中,每个叶节点(最底层的节点)都可以给其他机构颁发证书,而叶节点的证书,是由上层签名机构颁发的。

Alice在生成其公钥的时候,需要向在叶节点的证书权威机构CA申请其公钥的证书。在权威机构批准后,Alice就能获得与其公钥相关联的证书。当Bob向Alice请求Alice的公钥时,Alice就可以出示证书来证明其公钥的真实性。当Bob想要验证Alice的证书时,他首先需要验证Alice签名机构的证书;而验证Alice签名机构的证书,则要验证其签名机构的签名机构的证书…直到追踪到整个树的根签名机构。根签名机构(Root CA)的公钥则是网络内共识。

证书被广泛应用于现代互联网中,现在带几乎所有使用HTTPS链接的网站,都有证书。以下是我正在编辑这段文字时,所使用的语雀网站的证书:
