
科普向介绍请参考 tutorial。技术开发教程请参考本文。本系列文章更偏向技术入门。参考网上资料根据个人理解整理而成。本文主要参考廖雪峰博客。
1 区块链1.0时代:Bitcoin 比特币中本聪2008年发布《Bitcoin: A Peer-to-Peer Electronic Cash System》宣布了Bitcoin的诞生。人类经济生活最早期是物物交换,随着经济和贸易的发展,迫切需要一种“一般等价物”来作为商品交换的“中介”,这种一般等价物就是货币。货币由贝壳变成了金属再变成了纸币。而20-21世纪信息革命造成社会巨变,货币也进一步进化成了数字货币/加密货币。比特币和网银/支付宝/微信不同的是,比特币不需要一个类似银行的中央信任机构,就可以通过全球P2P网络进行发行和流通。比特币通过技术手段解决了现金电子化以后交易的清结算问题。传统的基于银行等金融机构进行交易,本质上是通过中央数据库,确保两个交易用户的余额一增一减。这些交易高度依赖专业的开发和运维人员,以及完善的风控机制。比特币则是通过区块链技术,把整个账本全部公开,人手一份,全网相同,因此,修改账本不会被其他人承认。比特币的区块链就是一种存储了全部账本的链式数据库,通过一系列密码学理论进行防篡改,防双花。
从现金和存款的角度看,现金是M0,而银行存款是M1和M2。银行存款本质上已经不是现金,而是用户的资产,对应着银行的负债。因为银行只记录用户在银行的资产余额,因此,用户A通过银行把100元转账给用户B的时候,用户A的资产减少100元,相应的,用户B的资产增加100元,银行对用户A和用户B的总负债不变。换句话说,存款是用户的“提款期权”。比特币由用户自己保存,不需要经过银行。比特币解决的是现金电子化后无需中央信任机构的交易问题,即M0如何通过网络进行价值传输。比特币背后对应的技术就是区块链。
比特币使用两种哈希算法,一种是对数据进行两次SHA-256计算,这种算法被称为hash256或者dhash。另一种是先计算SHA-256,再计算RipeMD160,这种算法被称为hash160。
每个区块都有一个唯一的哈希标识,被称为区块哈希,指向上一个区块。每一个区块还有一个Merkle哈希用来确保该区块的所有交易记录无法被篡改。区块链中的主要数据就是一系列交易,第一条交易通常是Coinbase交易,也就是矿工的挖矿奖励,后续交易都是用户的交易。
const
bitcoin = require('bitcoinjs-lib'),
createHash = require('create-hash');
function standardHash(name, data) {
let h = createHash(name);
return h.update(data).digest();
}
function hash160(data) {
let h1 = standardHash('sha256', data);
let h2 = standardHash('ripemd160', h1);
return h2;
}
function hash256(data) {
let h1 = standardHash('sha256', data);
let h2 = standardHash('sha256', h1);
return h2;
}
console.log('ripemd160 = ' + standardHash('ripemd160', s).toString('hex'));
console.log(' hash160 = ' + hash160(s).toString('hex'));
console.log(' sha256 = ' + standardHash('sha256', s).toString('hex'));
console.log(' hash256 = ' + hash256(s).toString('hex'));
Merkle Hash
在区块的头部,有一个Merkle Hash字段,它记录了本区块所有交易的Merkle Hash:
计算方法:先对每笔交易计算hash,然后对hash值自底向上2个一组拼接然后hash,不够2个一组的复制自身拼接然后hash。从Merkle Hash的计算方法可以得出结论:修改任意一个交易哪怕一个字节,或者交换两个交易的顺序,都会导致Merkle Hash验证失败,也就会导致这个区块本身是无效的,所以,Merkle Hash记录在区块头部,它的作用就是保证交易记录永远无法修改。
区块本身用Block Hash——也就是区块哈希来标识。但是,一个区块自己的区块哈希并没有记录在区块头部,而是通过计算区块头部的哈希得到的。区块链的第一个区块(又称创世区块)并没有上一个区块,因此,它的Prev Hash被设置为00000000…000。
Note:每个新矿机加入挖矿需要硬盘存储过去的账本,目前是几百个G的账本。矿池只需要计算,有专门负责存储账本的设备。
1.2 P2P交易原理 签名比特币采用的签名算法是椭圆曲线签名算法:ECDSA,使用的椭圆曲线是一个已经定义好的标准曲线secp256k1:
y
2
=
x
3
+
7
y^2=x^3+7
y2=x3+7。比特币的私钥是一个随机的非常大的256位整数。比特币账本是全网公开的,任何人都可以根据公钥查询余额,只有持有私钥可以控制对应账户。
比特币钱包实际上就是帮助用户管理私钥的软件。本地钱包、手机钱包、在线钱包(第三方在线服务商保存私钥)、硬件钱包。
交易每个区块都记录了至少一笔交易,一笔交易就是把一定金额的比特币从一个输入转移到一个输出:
比特币协议规定一个输出必须一次性花完,一个输入可以对应多个输出:
不能直接凭空产生数或求和,输入需要关联到交易,一个交易对应多输入1输出:
多输入多输出:
在实际的交易中,输入比输出要稍微大一点点,这个差额就是隐含的交易费用(gas),交易费用会算入当前区块的矿工收入中作为矿工奖励的一部分:
未花费的输出被称为UTXO(Unspent Transaction Ouptut)。
当我们要简单验证某个交易的时候,例如,对于交易f36abd,它记录的输入是3f96ab,索引号是1(索引号从0开始,0表示第一个输出,1表示第二个输出,以此类推),我们就根据3f96ab找到前面已发生的交易,再根据索引号找到对应的输出是0.5个比特币,所以,这笔交易的输入总计是0.5个比特币,输出分别是0.4个比特币和0.09个比特币,隐含的交易费用是0.01个比特币:
const bitcoin = require('bitcoinjs-lib');
let keyPair = bitcoin.ECPair.makeRandom();
console.log('private key = ' + keyPair.d);
私钥编码格式/钱包导入格式:WIF(Wallet Import Format),带校验的Base58编码
在32字节的私钥前添加一个0x80字节前缀,对其计算4字节的校验码(两次SHA256,取开头4字节),附加到最后,一共得到37字节的数据:
进行Base58编码,得到总是以5开头的字符串编码:
const wif = require('wif');
// 十六进制表示的私钥:
let privateKey = '0c28fca386c7a227600b2fe50b7cae11ec86d3bf1fbe471be89827e19d72aa1d';
// 对私钥编码:
let encoded = wif.encode(
0x80, // 0x80前缀
Buffer.from(privateKey, 'hex'), // 转换为字节
false // 非压缩格式
);
console.log(encoded);
压缩的私钥格式会在32字节的私钥前后各添加一个0x80字节前缀和0x01字节后缀,共34字节的数据,对其计算4字节的校验码,附加到最后,一共得到38字节的数据:
得到总是以K或L开头的字符串编码。非压缩的格式几乎已经不使用了。
私钥k根据ECDSA计算出2个256位整数(x,y),即为非压缩格式公钥。根据ECC曲线特点,由x也可推算出y,但需要知道y的奇偶性。因此,可以根据(x, y)推算出x’,作为压缩格式的公钥。压缩格式的公钥实际上只保存x这一个256位整数,但需要根据y的奇偶性在x前面添加02或03前缀,y为偶数时添加02,否则添加03。
let
wif = 'KwdMAjGmerYanjeui5SHS7JkmpZvVipYvB2LJGU1ZxJwYvP98617',
ecPair = bitcoin.ECPair.fromWIF(wif); // 导入私钥
// 计算公钥:
let pubKey = ecPair.getPublicKeyBuffer(); // 返回Buffer对象
console.log(pubKey.toString('hex')); // 02或03开头的压缩公钥
要特别注意,比特币的地址并不是公钥,而是公钥的哈希,即从公钥能推导出地址,但从地址不能反推公钥,因为哈希函数是单向函数。
以压缩格式的公钥为例,从公钥计算地址的方法是:
let
publicKey = '02d0de0aaeaefad02b8bdc8a01a1b8b11c696bd3d66a2c5f10780d95b7df42645c',
ecPair = bitcoin.ECPair.fromPublicKeyBuffer(Buffer.from(publicKey, 'hex')); // 导入公钥
// 计算地址:
let address = ecPair.getAddress();
console.log(address); // 1开头的地址
签名
签名算法是使用私钥签名,公钥验证的方法,对一个消息的真伪进行确认。如果一个人持有私钥,他就可以使用私钥对任意的消息进行签名,即通过私钥sk对消息message进行签名,得到signature:
let
message = 'a secret message!', // 原始消息
hash = bitcoin.crypto.sha256(message), // 消息哈希
wif = 'KwdMAjGmerYanjeui5SHS7JkmpZvVipYvB2LJGU1ZxJwYvP98617',
keyPair = bitcoin.ECPair.fromWIF(wif);
// 用私钥签名:
let signature = keyPair.sign(hash).toDER(); // ECSignature对象
// 打印签名:
console.log('signature = ' + signature.toString('hex'));
// 打印公钥以便验证签名:
console.log('public key = ' + keyPair.getPublicKeyBuffer().toString('hex'));
let signAsStr = '304402205d0b6e817e01e22ba6ab19c0'
+ 'ab9cdbb2dbcd0612c5b8f990431dd063'
+ '4f5a96530220188b989017ee7e830de5'
+ '81d4e0d46aa36bbe79537774d56cbe41'
+ '993b3fd66686'
let
signAsBuffer = Buffer.from(signAsStr, 'hex'),
signature = bitcoin.ECSignature.fromDER(signAsBuffer), // ECSignature对象
message = 'a secret message!', // 原始消息
hash = bitcoin.crypto.sha256(message), // 消息哈希
pubKeyAsStr = '02d0de0aaeaefad02b8bdc8a01a1b8b11c696bd3d66a2c5f10780d95b7df42645c',
pubKeyAsBuffer = Buffer.from(pubKeyAsStr, 'hex'),
pubKeyOnly = bitcoin.ECPair.fromPublicKeyBuffer(pubKeyAsBuffer); // 从public key构造ECPair
// 验证签名:
let result = pubKeyOnly.verify(hash, signature);
console.log('Verify result: ' + result);
1.3 挖矿原理
比特币的挖矿原理是工作量证明机制POW,Proof of Work
矿工:不停地计算,试图把新的交易打包成新的区块并附加到区块链上
挖矿:每打包一个新的区块,打包该区块的矿工就可以获得一笔比特币作为奖励。
工作量证明:计算非常复杂,需要消耗一定的时间,但验证过程相对简单的运算(实际上哈希满足这种要求)。
通过改变区块头部的一个nonce字段的值,计算机可以计算出不同的区块哈希值:
直到计算出某个特定的哈希值的时候,计算结束。这个哈希和其他的哈希相比,它的特点是前面有好几个0:
hash256(block data, nonce=0) = 291656f37cdcf493c4bb7b926e46fee5c14f9b76aff28f9d00f5cca0e54f376f hash256(block data, nonce=1) = f7b2c15c4de7f482edee9e8db7287a6c5def1c99354108ef33947f34d891ea8d hash256(block data, nonce=2) = b6eebc5faa4c44d9f5232631f39ddf4211443d819208da110229b644d2a99e12 hash256(block data, nonce=3) = 00aeaaf01166a93a2217fe01021395b066dd3a81daffcd16626c308c644c5246 hash256(block data, nonce=4) = 26d33671119c9180594a91a2f1f0eb08bdd0b595e3724050acb68703dc99f9b5 hash256(block data, nonce=5) = 4e8a3dcab619a7ce5c68e8f4abdc49f98de1a71e58f0ce9a0d95e024cce7c81a hash256(block data, nonce=6) = 185f634d50b17eba93b260a911ba6dbe9427b72f74f8248774930c0d8588c193 hash256(block data, nonce=7) = 09b19f3d32e3e5771bddc5f0e1ee3c1bac1ba4a85e7b2cc30833a120e41272ed ... hash256(block data, nonce=124709132) = 00000000fba7277ef31c8ecd1f3fef071cf993485fe5eab08e4f7647f47be95c
比特币挖矿的工作量证明原理就是,不断尝试计算区块的哈希,直到计算出一个特定的哈希值,它比难度值要小。
对于给定难度的SHA-256:假设我们用难度N表示必须算出首位N个0,那么,每增加一个难度,计算量将增加16倍。
对于比特币挖矿来说,就是先给定一个难度值,然后不断变换nonce,计算Block Hash,直到找到一个比给定难度值低的Block Hash,就算成功挖矿。
关于16倍的原因:这里的一位0是16进制数。如果选择的hash算法足够优秀,理论上遍历相邻16个数得到的hash值会遍历0-F。这里是近似计算。【在不同情形下难度也可能指bit位,就是2倍而不是16倍】
矿工先确定Prev Hash,Merkle Hash,Timestamp,bits然后,不断变化nonce来计算哈希,直到找出连续n个0开头的哈希值。
实际的难度是根据bits由一个公式计算出来,比特币协议要求计算出的区块的哈希值比难度值要小,这个区块才算有效:
Difficulty = 402937298
= 0x18 0455d2
= 0x0455d2 * 28 * (0x18 - 3)
= 106299667504289830835845558415962632664710558339861315584
= 0x00000000000000000455d2000000000000000000000000000000000000000000
注意,难度值越小,说明哈希值前面的0越多,计算难度越大。(难度值不等于计算难度)。
比特币网络的难度是不断变化的,它的难度保证大约每10分钟产生一个区块,而难度值在每2015个区块调整一次:如果区块平均生成时间小于10分钟,说明全网算力增加,难度也会增加,如果区块平均生成时间大于10分钟,说明全网算力减少,难度也会减少。因此,难度随着全网算力的增减会动态调整。
比特币设计时本来打算每2016个区块调整一次难度,也就是两周一次,但是由于第一版代码的一个bug,实际调整周期是2015个区块。根据比特币每个区块的难度值和产出时间,就可以推算出整个比特币网络的全网算力。目前全网算力超过了100EH/s,而单机CPU算力不过几M,GPU算力也不过1G,所以,单机挖矿的成功率几乎等于0。比特币挖矿已经从早期的CPU、GPU发展到专用的ASIC芯片构建的矿池挖矿。
当某个矿工成功找到特定哈希的新区块后,他会立刻向全网广播该区块。其他矿工在收到新区块后,会对新区块进行验证,如果有效,就把它添加到区块链的尾部。同时说明,在本轮工作量证明的竞争中,这个矿工胜出,而其他矿工都失败了。失败的矿工会抛弃自己当前正在计算还没有算完的区块,转而开始计算下一个区块,进行下一轮工作量证明的竞争。比特币总量被限制为约2100万个比特币,初始挖矿奖励为每个区块50个比特币,以后每4年减半。
共识算法如果两个矿工在同一时间各自找到了有效区块,注意,这两个区块是不同的,因为coinbase交易不同,所以Merkle Hash不同,区块哈希也不同。但它们只要符合难度值,就都是有效的。这个时候,网络上的其他矿工应该接收哪个区块并添加到区块链的末尾呢?答案是,都有可能。通常,矿工接收先收到的有效区块,由于P2P网络广播的顺序是不确定的,不同的矿工先收到的区块是有可能的不同的。这个时候,我们说区块发生了分叉:
在分叉的情况下,有的矿工在绿色的分叉上继续挖矿,有的矿工在蓝色的分叉上继续挖矿:
总有一个分叉首先挖到后续区块,这个时候,由于比特币网络采用最长分叉的共识算法,绿色分叉胜出,蓝色分叉被废弃,整个网络上的所有矿工又会继续在最长的链上继续挖矿。
由于区块链虽然最终会保持数据一致,但是,一个交易可能被打包到一个后续被孤立的区块中。所以,要确认一个交易被永久记录到区块链中,需要对交易进行确认。如果后续的区块被追加到区块链上,实际上就会对原有的交易进行确认,因为链越长,修改的难度越大。一般来说,经过6个区块确认的交易几乎是不可能被修改的。
1.4 可编程支付原理比特币的支付实际上并不是直接支付到对方的地址,而是一个脚本,这个脚本的意思是:谁能够提供另外一个脚本,让这两个脚本能顺利执行通过,谁就能花掉这笔钱。比特币交易的输出是一个锁定脚本,而下一个交易的输入是一个解锁脚本。必须提供一个解锁脚本,让锁定脚本正确运行,那么该输入有效,就可以花
FROM: UTXO Hash#index AMOUNT: 0.5 btc TO: OP_DUP OP_HASH160 OP_EQUALVERIFY OP_CHECKSIG
以真实的比特币交易为例,某个交易的某个输出脚本是76a914dc…489c88ac这样的数据,而花费该输出的某个交易的输入脚本是48304502…14cf740f:
先看锁定脚本,锁定脚本的第一个字节76翻译成比特币脚本的字节码就是OP_DUP,a9翻译成比特币脚本的字节码就是OP_HASH160。14表示这是一个20字节的数据,于是我们得到20字节的数据。最后两个字节,88表示字节码OP_EQUALVERIFY,ac表示字节码OP_CHECKSIG,所以整个锁定脚本是:
OP_DUP 76
OP_HASH160 a9
DATA 14 (dc5dc65c...fe9f489c)
OP_EQUALVERIFY 88
OP_CHECKSIG ac
解锁脚本。解锁脚本的第一个字节48表示一个72字节长度的数据。接下来的字节21表示一个33字节长度的数据。因此,该解锁脚本实际上只有两个数据。
DATA 48 (30450221...68fa9b01) DATA 21 (03dd8763...14cf740f)
接下来,要验证这个交易是否有效。要验证这个交易,首先,我们要把解锁脚本和锁定脚本拼到一块,然后,开始执行这个脚本:
DATA 48 (30450221...68fa9b01)
DATA 21 (03dd8763...14cf740f)
OP_DUP 76
OP_HASH160 a9
DATA 14 (dc5dc65c...fe9f489c)
OP_EQUALVERIFY 88
OP_CHECKSIG ac
比特币脚本是一种基于栈结构的编程语言,先准备一个空栈,用来执行比特币脚本。然后执行第一行代码,由于前俩行代码是数据,所以直接把数据压栈:
OP_DUP指令,这条指令会把栈顶的元素复制一份。执行OP_HASH160指令,这条指令会计算栈顶数据的hash160,也就是先计算SHA-256,再计算RipeMD160。对十六进制数据03dd8763f8c3db6b77bee743ddafd33c969a99cde9278deb441b09ad7c14cf740f计算hash160后得到结果dc5dc65c7e6cc3c404c6dd79d83b22b2fe9f489c,然后用结果替换栈顶数据:
接下来的指令是一条数据,所以直接压栈:
执行OP_EQUALVERIFY指令,它比较栈顶两份数据是否相同(比较地址是否正确),如果相同,则验证通过,脚本将继续执行,如果不同,则验证失败,整个脚本就执行失败了。在这个脚本中,栈顶的两个元素是相同的,所以验证通过,脚本将继续执行(栈顶两份数据移除):
最后,执行OP_CHECKSIG指令,它使用栈顶的两份数据,第一份数据被看作公钥,第二份数据被看作签名,这条指令就是用公钥来验证签名是否有效。根据验证结果,成功存入1,失败存入0。最后,当整个脚本执行结束后,检查栈顶元素是否为0,如果不为0,那么整个脚本就执行成功,这笔交易就被验证为有效的。
因为比特币的脚本不含条件判断、循环等复杂结构。上述脚本就是对输入的两个数据视作签名和公钥,然后先验证公钥哈希是否与地址相同,再根据公钥验证签名,这种标准脚本称之为P2PKH(Pay to Public Key Hash)脚本。
当小明给小红支付一笔比特币时,实际上小明创建了一个锁定脚本,该锁定脚本中引入了小红的地址。要想通过解锁脚本花费该输出,只有持有对应私钥的小红才能创建正确的解锁脚本(因为解锁脚本包含的签名只有小红的私钥才能创建),因此,小红事实上拥有了花费该输出的权利。
使用钱包软件创建的交易都是标准的支付脚本,但是,比特币的交易本质是成功执行解锁脚本和锁定脚本,所以,可以编写各种符合条件的脚本。比如,有人创建了一个交易,它的锁定脚本像这样:
OP_HASH256
DATA 6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000
OP_EQUAL
谁能够提供一个数据,它的hash256等于6fe28c0a…,谁就可以花费这笔输出。所以,解锁脚本实际上只需要提供一个正确的数据,就可以花费这笔输出。
比特币的脚本通过不同的指令还可以实现更灵活的功能。例如,多重签名可以让一笔交易只有在多数人同意的情况下才能够进行。最常见的多重签名脚本可以提供3个签名,只要任意两个签名被验证成功,这笔交易就可以成功。
FROM: UTXO Hash#index AMOUNT: 10.5 btc TO: P2SH: OP_2 pk1 pk2 pk3 OP_3 OP_CHECKMULTISIG
从比特币支付的脚本可以看出,比特币支付的本质是由程序触发的数字资产转移。这种支付方式无需信任中介的参与,可以在零信任的基础上完成数字资产的交易,这也是为什么数字货币又被称为可编程的货币。
由此催生出了智能合约:当一个预先编好的条件被触发时,智能合约可以自动执行相应的程序,自动完成数字资产的转移。保险、贷款等金融活动在将来都可以以智能合约的形式执行。智能合约以程序来替代传统的纸质文件条款,并由计算机强制执行,将具有更高的更低的信任成本和运营成本。
1.5 UTXO模型UTXO:Unspent TX Output
给定任何一个区块,计算当前所有的UXTO金额之和,等同于自创世区块到给定区块的挖矿奖励之和。想要确定某个人拥有的比特币,并无法通过某个账户查到,必须知道此人控制的所有UTXO金额之和。
在钱包程序中,钱包管理的是一组私钥,对应的是一组公钥和地址。钱包程序必须从创世区块开始扫描每一笔交易,如果:
如果刚装了一个新钱包,导入了一组私钥,在钱包扫描完整个比特币区块之前,是无法得知当前管理的地址余额的。怎么加速计算?
对区块链进行查询之前,先扫描整个区块链,重建一个类似关系数据库的地址-余额映射表。一开始,这是一个空表。每当扫描一个区块的所有交易后,某些地址的余额增加,另一些地址的余额减少,两者之差恰好为区块奖励:
扫描完所有区块后,我们就得到了整个区块链所有地址的完整余额记录,查询的时候,并不是从区块链查询,而是从本地数据库查询。大多数钱包程序使用LevelDB来存储这些信息,手机钱包程序则是请求服务器,由服务器查询数据库后返回结果。
可见,比特币的区块链记录的是修改日志,而不是当前状态。
Segwit地址(隔离见证地址)。在比特币区块链上,经常可以看到类似bc1qmy63mjadtw8nhzl69ukdepwzsyvv4yex5qlmkd这样的以bc开头的地址,这种地址就是隔离见证地址。
Segwit地址有好几种,一种是以3开头的隔离见证兼容地址(Nested Segwit Address),从该地址上无法区分到底是多签地址还是隔离见证兼容地址,好处是钱包程序不用修改,可直接付款到该地址。
另一种是原生隔离见证地址(Native Segwit Address),即以bc开头的地址,它本质上就是一种新的编码方式。
bc地址使用的不是Base58编码,而是Bech32编码,它的算法是:
Bech32编码由两部分组成:一部分是bc这样的前缀,被称为HRP(Human Readable Part,用户可读部分),另一部分是特殊的Base32编码,使用字母表qpzry9x8gf2tvdw0s3jn54khce6mua7l,中间用1连接。对一个公钥进行Bech32编码的代码如下:
const
bitcoin = require('bitcoinjs-lib'),
bech32 = require('bech32'),
createHash = require('create-hash');
// 压缩的公钥:
let publicKey = '02d0de0aaeaefad02b8bdc8a01a1b8b11c696bd3d66a2c5f10780d95b7df42645c';
// 计算hash160:
let
sha256 = createHash('sha256'),
ripemd160 = createHash('ripemd160'),
hash256 = sha256.update(Buffer.from(publicKey, 'hex')).digest(),
hash160 = ripemd160.update(hash256).digest();
// 计算bech32编码:
let words = bech32.toWords(hash160);
// 头部添加版本号0x00:
words.unshift(0);
// 对地址编码:
let address = bech32.encode('bc', words);
console.log(address); // bc1qmy63mjadtw8nhzl69ukdepwzsyvv4yex5qlmkd
和Base58地址相比,Bech32地址的优点有:
它的缺点是:
引入Segwit地址目的是为了解决比特币交易的延展性(Transaction Malleability)攻击。
tx = ... input#index ... signature ... output-script ... tx-hash = dhash(tx)
出于保护隐私的目的,同一用户如果控制的UTXO其地址都是不同的,那么很难从地址获知某个用户的比特币持币总额。但是,管理一组成千上万的地址,意味着管理成千上万的私钥,管理起来非常麻烦。
能不能只用一个私钥管理成千上万个地址?实际上是可以的。虽然椭圆曲线算法决定了一个私钥只能对应一个公钥,但是,可以通过某种确定性算法,先确定一个私钥k1,然后计算出k2、k3、k4……等其他私钥,就相当于只需要管理一个私钥,剩下的私钥可以按需计算出来。根据某种确定性算法,只需要管理一个根私钥,即可实时计算所有“子私钥”的管理方式,称为HD钱包。
HD,Hierarchical Deterministic意思是分层确定性。先确定根私钥root,然后根据索引计算每一层的子私钥:
这种计算被称为衍生(Derivation)。
HD钱包采用的计算子私钥的算法是一个扩展的512位私钥,记作xprv,它通过SHA-512算法配合ECC计算出子扩展私钥,仍然是512位。通过扩展私钥可计算出用于签名的私钥以及公钥。
简单来说,只要给定一个根扩展私钥(随机512位整数),即可计算其任意索引的子扩展私钥。扩展私钥总是能计算出扩展公钥,记作xpub:
从xprv及其对应的xpub可计算出真正用于签名的私钥和公钥。之所以要设计这种算法,是因为扩展公钥xpub也有一个特点,那就是可以直接计算其子层级的扩展公钥:
xpub只包含公钥,不包含私钥,因此,可以安全地把xpub交给第三方(例如,一个观察钱包),它可以根据xpub计算子层级的所有地址,然后在比特币的链上监控这些地址的余额,但因为没有私钥,所以只能看,不能花。
HD钱包通过分层确定性算法,实现了以下功能: