欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

比特币学习

程序员文章站 2022-05-21 21:25:26
...

1

对于科技从业者而言,比特币则更多了一层含义:比特币对应着 一种划时代的数字加密货币系统,其内容包含通信协议、激励机制、实现代码 与承载网络等:

比特币学习

2

比特币采用一种特殊的数据结构区块链/Blockchain来保障交易的不可篡改性, 每一个包含一批交易数据的区块,同时也包含了前一个区块的指纹:

比特币学习

在比特币中,一个区块的指纹是使用密码学中常见的哈希函数来实现的。 哈希函数可以将大块数据压缩成精简的表示,而且可以保证如果精简 的表示不同,那么其对应的原始数据也不同。

例如,在上图中如果12#区块被攻击者篡改,那么它的哈希结果将不同于在 13#区块中保存的其原始指纹,这使得识别篡改的区块这一任务很容易,或者 说篡改的难度很大 —— 攻击者必须同时修改12#之后的所有区块才能保证 指纹校验成功。

另一方面,如果攻击者直接篡改14#区块(我们假设这是最后一个块),那么 显然是可行的,因为它缺乏之后更多区块的保护。这引入了在比特币中常用 的一个概念:交易的确认数/Confirms

交易一旦被确认打包到区块中,它的确认数就是1,之后每增长一个区块 则确认数加1。例如对于上图中的标注交易,当链增长到14#块时,该交易的 确认数就是3。

显然一个交易的确认数越多,意味着攻击者篡改交易的可能性越小。在比特币 中的应用当中,交易的接收者通常需要在六个确认之后,才可以将该笔 交易视为成功。

3

当前流通的任何法币都不同,比特币是去中心化的,没有一个*机构 来管理比特币的发行与流通,因此比特币网络是一个典型的P2P网络,在每个 (全)节点上都有完整的区块链数据:

比特币学习

在这样的分布式计算环境下,如何保证新的交易在各个节点区块链中得以 一致的更新,就是经典的分布式一致性问题了 —— 每个节点都有可能提交 新的交易,而不同节点提交的交易也可能不相同,到底以哪个节点为准?

解决这种问题的经典方法就是(动态)选举一个决策者,其他节点复制 决策者的行为即可避免节点之间的不一致了。比特币的解决思路也一样, 不过它采用了一种类似于抢答的机制来动态选择胜出的节点,由胜出的 节点负责出块并打包交易 —— 所有节点都同时求解同一个问题,最先得到结果 的节点获胜并获得出块权利,其他节点则转而求解下一次出块的问题:

比特币学习

比特币给出的问题不可以通过解析方法求解,节点必须在所有的可能 结果中暴力尝试求解,由于胜出的节点可以获得比特币奖励,使得 节点旳动机和行为颇为类似于淘金的西部牛仔,因此这一求解过程被 称为挖矿/Mining

理论上每个节点都有获胜的概率,但显然,在同样的时间内,计算力强大的 节点会比其他节点尝试的机会更多,因此获胜的概率也越大 —— 在这种抢答机制下, 算力代替了智力,而这种依赖于暴力求解问题从而达成节点一致性的共识算法 被称为工作量证明/Proof Of Work

4秘钥地址

比特币的身份识别体系是建立在非对称加密技术之上的去中心化系统, 每一个身份对应着一对**。

非对称加密采用一对**(私钥、公钥)进行数据的加密或解密: 用私钥加密,则需要用公钥解密;用公钥加密,则需要用私钥解密。 这一非对称特性使得其非常适合用于身份表示与验证 —— 公钥用于 身份的表示,而私钥则用于身份的验证:

比特币学习

在上图中,当tommy需要向jerry发送原始数据时,他首先使用自己的私钥对 原始数据进行签名,得到的签名数据附加在原始数据后面,一同发送给jerry。 jerry收到数据后,使用tommy的公钥就可以验证签名是否是由tommy的私钥签发的, 从而确认该数据确实来自于tommy。

虽然公钥可以唯一的标识一个身份,但在比特币中更多的使用地址来 标识身份,可以认为地址是一个或一组公钥的精简表示,因此同样可以 使用私钥进行验证:

比特币学习

5交易确认与激励

我们知道,比特币是通过挖矿这种机制来保证分布式环境下的节点一致性, 只有通过挖矿,交易才能在众多节点之间达成共识并最终打包到区块中上链。

在另一方面,挖矿也是比特币系统的造币机制,所有的比特币都是通过各个 节点的挖矿出块产生的,并且支付给矿工作为其付出的奖励 —— 这一奖励最初 是50个比特币,并且每出21万个区块之后减半,直至最终减少到0。目前阶段的挖矿 奖励是每区块12.5个比特币。

节点挖矿获得的奖励并不会立刻生效,而必须等待更多的区块生成之后才可用。 这是因为当比特币网络中出现分叉时,某些区块会变成孤儿,而这些区块包含的交易 将被重新打包入其他区块,同时这些孤儿区块的挖矿奖励将被回收:

比特币学习

因此按照约定,挖块奖励得到的比特币必须要等101个确认(Confirms)之后才能生效。

6交易

交易(Transaction)是比特币的核心,它不仅是比特币流转的记录,而且 比特币本身也隐含在交易当中。下图表示了两个交易之间比特币的流转过程:

比特币学习

通常一个交易总是包含输入(vin)和输出(vout)两个部分,其中的输入 用来引用其他交易的输出。但用来记录挖矿奖励的币基交易/coinbase transaction 是个例外:它只有输出部分。例如,上图中的交易1111是一个币基交易,其 输出部分记录了挖矿奖励的50个币转入了地址x。

交易2222则记录了地址x的持有者向地址y转40个币的事实。在这个交易 的输入部分,可以看到它引用了交易1111的第0个输出,就是说,将把交易 1111的第0个输出所表示的比特币转给地址y。

交易输出在没有被其他交易使用之前,被称为未消费交易输出/Unspent Transaction Output, 简称为UTXO。UTXO的一个重要特点是不可以拆分消费:要么不使用它,要么完全使用它。 这有点像现钞:你不能把一张50元的钞票撕成40元和10元。 因此,虽然只需要给地址y转40个比特币,但是交易1111中的这个面值50个币的UTXO 会完全消耗掉,不再可用。

交易的输入总额与输出总额的差值,就是支付给矿工的交易费/Transaction Fee。 因此对于上面的交易我们还需要构造第二个交易输出来处理多出来的10个比特币,只把 其中一小部分支付交易费,剩下的使用x持有人控制的另一个地址z来回收 —— 就 像找零钱一样,因此这个地址z通常被称为找零地址/change address。当然,也 可以将这部分比特币转给另一个外部地址。

当交易2222被确认后,交易1111的第0个输出就被消耗掉了,不再可用;产生 的两个新的UTXO,则分别属于地址y和地址z,而这一事实,被记录在 交易2222中写入区块链。

7转账交易

我们可以使用如下的命令来创建、确认一个转账交易:

~$ addr=`bitcoin-cli getnewaddress`
~$ txid=`bitcoin-cli sendtoaddress $addr 3.33`
~$ blks=`bitcoin-cli generate 6`

然后使用getrawtransaction调用来查看交易的详情:

~$ bitcoin-cli getrawtransaction $txid true

当指定参数true时,返回的结果是解码后的JSON格式的交易详情,其 vin字段中包含本次交易使用的所有前序UTXO,这些UTXO在交易确认后就 不再是UTXO了:

比特币学习

在vout字段中则包含本次交易生成的新的UTXO:

比特币学习

8 RPC概叙

JSON RPC采用JSON语法表示一个远程过程调用(Remote Procedure Call) 的请求与应答消息。例如对于getbalance调用,请求消息与应答消息的格式 示意如下:

比特币学习

在请求消息中使用method字段声明要调用的远程方法名,使用params字段 声明调用参数列表;消息中的jsonrpc字段声明所采用的JSON RPC版本号, 而可选的id字段则用于建立响应消息与请求消息之间的关联,以便客户端 在同时发送多个请求后能正确跟踪其响应。

响应消息中的result字段记录了远程调用的执行结果,而error字段 则记录了调用执行过程中出现的错误,id字段则对应于请求消息中的同名 字段值。

JSON RPC是传输协议无关的,但基于HTTP的广泛应用,节点通常都会提供基于 HTTP协议的实现,也就是说将JSON PRC消息作为HTTP报文的内容载荷进行传输:

比特币学习

 

身份验证逻辑

PaytoPubKeyHashAddress类除了getAddress()方法之外,还有一个方法 值得我们研究:

比特币学习

getScriptPubKey()方法用来获取地址对应的公钥脚本,调用它将会返回如下 的结果:

比特币学习

公钥脚本有什么作用?

让我们先考虑一个相关的问题:如果一个UTXO上标明了接收地址,那么接收地址 的持有人该如何向节点证明这个UTXO属于他?

P2PKH地址是由公钥推导出来的,我们知道公钥可以验证私钥的签名,那么 只要引用UTXO的交易,提供对交易的签名和公钥,节点就可以利用公钥, 来验证提交交易者,是不是该地址的持有人了:

比特币学习

在上图中,交易2222的提交者需要在交易的输入中,为引用的每个UTXO补充 自己的公钥以及交易签名,然后提交给节点。节点将按照如下逻辑验证提交者是否是地址 x的真正持有人:

  1. 验证公钥:利用公钥推算地址,检查是否与地址x一致,如果不一致则拒绝交易
  2. 验证私钥:利用交易和公钥,验证提交的签名是否匹配,如果不一致则拒绝交易
  3. 接受并广播交易

因此,当我们向目标地址发送比特币时,实际上相当于给这个转出的UTXO 加了一个目标地址提供的锁,而只有目标地址对应的私钥才可以解开这个锁。 回到前面的问题,getScriptPubKey()方法返回的公钥脚本,就对应于这个 提供给发送方的锁了 —— 给我发的UTXO,请用我提供的锁先锁上

 

P2PKH脚本执行原理

在前一节,我们理解了节点如何验证交易提交者对UTXO的所有权,那么接 下来就容易理解getScriptPubKey()方法获取的脚本到底是什么了。

简单地说,比特币实际上是将UTXO所有权的验证逻辑,从节点中剥离 到交易中实现的:在UTXO中定义一段脚本(公钥脚本),在引用UTXO时定义另一段脚本 (签名脚本),节点在验证UTXO所有权时,只需要拼接这两段脚本,并确定运行结果为 真,就表示交易提交者的确持有该UTXO:

比特币学习

比特币所采用的脚本采用自定义的简单语法,不支持循环,因此不是图灵 完备的语言,但也降低了安全风险。脚本使用预定义的指令编写,从左至右依次执行。

例如,对于P2PKH地址,其对应的采用助记符表示的两部分脚本如下:

scriptPubKey: OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG
scriptSig: <sig> <pubKey>

最终节点合并脚本时,总会将scriptPubKey放在后面,而scriptSig放在前面:

比特币学习

比特币脚本指令的运行需要一个栈,在上图中,列出了整个脚本的7个指令执行 过程中,每个指令执行后的栈的情况。接下来让我们单步跟踪指令的运行情况。

签名与公钥入栈

指令1和2首先将签名和公钥压入栈。 当执行第1个指令时,将向栈顶压入签名<sig>,当执行第2个指令时,将向栈顶压入 公钥<pubkey>

公钥验证

接下来指令3/4/5/6将验证公钥是否匹配scriptPubKey中预留的解锁公钥哈希。

首先,使用指令OP_DUP将栈顶成员复制一份再压入栈,因此该指令执行后,栈顶将有 两个公钥<pubkey>

接下来,使用指令OP_HASH160提取栈顶成员并进行两重哈希计算(SHA-256 -> RIMPEMD-160), 我们知道这就是公钥哈希的算法。该指令的结果将压入栈,以便和scriptPubKey中 的预留公钥哈希进行对比。

然后,脚本会将scriptPubKey中预留的解锁公钥哈希压入栈顶,这样栈顶就有两个公钥哈希了: 预留的解锁公钥哈希,以及根据解锁脚本提供的公钥重新生成的公钥哈希。

指令OP_EQUALVERIFY将提取栈顶的两个公钥哈希进行比较,如果不相等则直接标注交易无效, 退出脚本执行。如果成功的话,栈顶此时只有两个成员了:公钥和交易签名。

签名验证

指令OP_CHECKSIG负责提取栈顶的两个成员进行签名验证,如果验证成功,则将01压入栈, 栈顶的非零值意味着验证成功。否则将00压入栈,这意味着验证失败。

 

 

创建P2SH地址

基于前一节的学习,我们知道比特币的UTXO所有权的认证,是完全基于 交易中嵌入的两部分脚本来完成的,这种独立于节点旳脚本化验证机制为比特币 的支付提供了巨大的灵活性。

P2SH(Pay To Script Hash)地址就是为了充分利用比特币的脚本能力而提出的改进。 容易理解,这种地址是基于脚本的哈希来构造的 —— 该脚本被称为赎回(redeem)脚本:

比特币学习

P2SH地址的公钥脚本只是简单地验证UTXO的消费者所提交的序列化的赎回脚本 serializedRedeemScript是否匹配预留的脚本哈希scriptHash

比特币学习

如果上述验证通过,那么节点会将序列化的赎回脚本展开并与签名再次拼接。例如 下图展示了一个简单的赎回脚本展开后与签名拼接的完整脚本:

比特币学习

同样,P2SH地址前缀根据网络不同有所区别:

比特币学习

脚本

bitcoinj实现了完整的比特币脚本编写与执行功能,通常我们使用ScriptBuilder 类提供的方便函数来构造脚本对象:

比特币学习

基于P2SH地址的构造原理,我们可以创建任意一个脚本作为赎回脚本来创建一个P2SH 地址。例如,在下面的代码中首先生成前面描述的简单赎回脚本,然后创建该脚本的P2SH地址:

NetworkParameters params = RegTestParams.get();
ECKey key = new ECKey();
Script redeemScript = (new ScriptBuilder()).data(key.getPubKey()).op(OP_CHECKSIG).build();
byte[] hash = Utils.sha256hash160(redeemScript.getProgram());
Address addr = Address.fromP2SHHash(params,hash);
System.out.format("p2sh address => %s\n",addr.toString());

 

多重签名赎回脚本

P2SH地址应用最多的领域就是进行多重签名交易:一个UTXO的消费交易必须从n个 参与者中至少获得m个签名才能被确认,这被称为m-of-n签名。

例如,一个2-of-3的多重签名的赎回脚本如下:

比特币学习

多重签名的赎回脚本主要使用指令OP_CHECKMULTISIG完成,它执行时需要栈顶 的成员如下:

比特币学习

我们可以使用ScriptBuilder的方便函数创建多重签名赎回脚本,但更简单的是使用 其静态方法createMultiSigOutputScript()直接返回赎回脚本:

比特币学习

当获得赎回脚本后,继续使用ScriptBuilder的静态方法createP2SHOutputScript() 就可以获得对应的P2SH脚本,进而使用脚本的getToAddress()方法获得地址。

例如下面的代码构造一个2-of-2签名脚本,并创建其对应的P2SH地址:

NetworkParameters params = RegTestParams.get();
List<ECKey> keys = Arrays.asList(new ECKey(),new ECKey());
Script redeemScript = ScriptBuilder.createMultiSigOutputScript(2,keys);
Script p2sh = ScriptBuilder.createP2SHOutputScript(redeemScript);    
Address addr = outputScript.getToAddress(params);
System.out.format("p2sh msig address => %s\n",addr);