比特币底层技术探秘

比特币真的很酷。当然,关于这项技术目前还存在着不少的争议,包括:它是否是一项有用的技术,加密数字货币是否

存在着泡沫,目前面临的管理问题是否能够得到解决。但是从纯技术层面来说,神秘的Satoshi Nakamoto创造了这个引人注

目的技术。

不幸的是,尽管可以找到很多资源站在较高的层 次解释了比特币的工作原理,但却没有有关底层的资料。在我看来,如果你

从一万英尺的高度看的话,你只能够凭感觉来意会了。

对于这么一个新兴的领域,我发现我自己非常渴望去了解比特币的工作机制。幸运的是,因为比特币本质上是分散的,并且

是对等的,所以任何人都能够开发出一款符合协议标准的客户端。为了能够更好地了解比特币的工作原理,我决定开发一款

属于我自己的比特币客户端,可以向比特币区块链发布交易。

这篇文章介绍了开发一个最小而又可用的比特币客户端的过程,它可以创建一笔交易并将其提交到比特币对等网络上,以便

让它包含在区块链中。如果你只是想读一下原始代码,可以随时查看我的Github代码库。

要成为比特币网络的一部分,必须要有一个可以发送和接收资金的地址。比特币使用了公钥加密技术,而地址是从私钥派生

出来的公钥的散列版本。令人吃惊的是,与大多数的公共密钥加密技术不同,它的公共密钥会一直保密存放,直到资金从这

个地址发送出去。

比特币对其地址使用了椭圆曲线公钥密码技术。椭圆曲线加密技术与RSA一样,用于从私钥生成公钥,但其占用的空间更

小。如果你有兴趣了解一下这种加密技术背后的数学知识的话,那么Cloudflare上的一篇入门文章值得一读。

从256位的私钥开始,生成比特币地址的过程如下图所示:

在Python中,我使用ecsda库来实现椭圆曲线加密。以下代码片段展示了从一个相当重要(也相当不安全)的私钥

0xFEEDB0BDEADBEEF(前面补零以达到64或者256个十六进制字符串)来获取公钥的过程。如果你想在地址中存储任何实际的

值,那么需要一种更安全的私钥生成方法!

from ecdsa import SECP256k1, SigningKeydef get_private_key(hex_string):

运行代码,获取到私钥(十六进制):

0000000000000000000000000000000000000000000000000feedb0bdeadbeef

获取到的公钥(十六进制):

04d077e18fd45c031e0d256d75dfa8c3c21c589a861c4c33b99e64cf613113fcff9fc9d90a9d81346bcac64d3c01e6e0ef0828543edad73c0e257b845

以0x04开头的公钥表明这是一个没有经过压缩的公钥,这意味着ECDSA(椭圆曲线数字签名算法)中x和y轴坐标简单的关联

在一起。根据ECSDA的原理,如果你知道x值,那么y值只能取两个值,一个偶数和一个奇数。基于这个信息,可以仅使用x中

的一个值和y的极性来表达一个公钥。这使得公钥的大小从65位减少到33位,这个过程(和后续计算的地址)称之为压缩。

对于压缩后的公钥,根据y的极性,将以0x02或0x03开头。未压缩的公钥常用于比特币,这也是我在这里所使用的。

要从公钥生成比特币地址,公钥先要计算sha256散列,然后再计算ripemd160散列。这种双重散列提供了额外的安全层,

ripemd160散列提供了sha256的256位散列之后的160位散列,这样缩短了地址的长度。一个有趣的结果是,两个不同的公钥

可以哈希生成一个相同的地址!然而,对于2的160次方个不同的地址,这不太可能在短时间内发生。

import hashlibdef get_public_address(public_key):

这将生成c8db639c24f6dc026378225e40459ba8a9e54d1a这个公共地址,这有时会被称为哈希160地址。

如前所述,有一点比较有意思,从私钥到公钥的转换以及从公钥到公共地址的转换都是单向转换。如果你有一个地址,那么

找到关联公钥的唯一办法就是解决SHA256哈希问题。这与大多数的公钥加密技术不同,那些机密技术中公钥是公开的,而私

钥会隐藏起来。而在当前这个情况下,公钥和私钥都会隐藏起来,而只公布地址(哈希过的公钥)。

表示一个比特币地址的标准方式是使用Base58Check进行编码。该编码只是地址的一种表示形式(因此可以被解码/反推)。

它生成类似于1661HxZpSy5jhcJ2k6av2dxuspa8aafDac这种形式的地址。 Base58Check编码提供了一个较短的地址表示方法,

并且还内置校验和,可以检测出错误的地址。几乎在每个比特币客户端中,你看到的地址都是Base58Check编码后的地址。

Base58Check还包含一个版本号,在下面的代码中我把它设置为0,这表示该地址是一个pubkey散列。

# 58 character alphabet usedBASE58_ALPHABET =

'123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'def base58_encode(version, public_address):

以上所有的代码展示了我从私钥FEEDB0BDEADBEEF(前面需要填充零)到到比特币地址KK2xni6gmTtdnSGRiuAf94jciFgRjDj7W

的整个过程!

通过这个地址,我现在就可以来获得比特币了!为了把比特币放入我的地址,我用澳元从btcmarkets购买了0.0045比特币

(在撰写本文时约为11美元)。 使用btcmarket的交易门户,我将其转移到上面的地址,在此过程中会损失0.0005比特币的

交易费用。你可以在交易*95855ba9f46c6936d7b5ee6733c81e715ac92199938ce30ac3e1214b8c2cd8d7*中的区块链上看到这笔

交易。

现在,我有了一个地址,而且上面还有一些比特币,事情变得更有趣了。如果我想将比特币发送到别的地方,那么就必须连

接到比特币的P2P网络上。

当我第一次学习比特币的时候,我发现了一个关键的问题:由于网络的分散性,网络上的节点是如何找到其他节点的?没有

中央控制点,比特币客户端是如何知道如何引导并与网络的其他节点进行交互的?

理论服从实践,在最初的节点发现过程中是存在着极少数的集中控制器。一个新的节点寻找其他节点的方法在原理上就是通

过DNS去查找Bitcoin社区成员维护的“DNS种子”服务器。

事实证明,DNS非常适合于引导客户端,因为DNS协议基于UDP,轻量级,不太容易受到DDoS攻击。IRC以前曾被用作引导的方

法,但是因为容易受DDoS攻击这个弱点而停止使用了。

种子DNS被硬编码到Bitcoin的核心源代码中,并由核心开发人员负责修改。

下面的Python代码首先连接到一个DNS种子,然后打印出一个可以连接的节点列表。使用socket库,它基本上执行的是一个

nslookup操作,然后返回从seed.bitcoin.sipa.be查询得到IPv4地址结果中的第一个。

import socket# 向bitcoin DNS服务器发送DNS请求来查找节点nodes = socket.getaddrinfo('seed.bitcoin.sipa.be',

None)# 选择第一个节点node = nodes[0][4][0]

查到的地址是208.67.251.126,这是一个友好的对端节点,我可以去连接这个地址了!

各个节点之间是通过TCP来建立连接的。连接对端节点时,比特币协议最开始的握手消息是一个版本消息。在节点交换版本

消息之后,才会接受其他消息。

比特币协议消息在“Bitcoin开发人员参考手册”中有详细的记录。使用开发人员参考手册作为指南,可以在Python中创建

version消息,如下面的代码片段所示。 其中大多数的代码都是用于打开与对端节点的连接。如果你对细节感兴趣的话,可

以查看开发者参考手册。

version = 70014services = 1 # not a full node, cant provide any datatimestamp = int(time.time())

使用Python的struct库,版本有效载荷数据可以打包成正确的格式,请特别注意一下数据的字节顺序和字节宽度。将数据打

包成正确的格式很重要,不然对端节点将无法理解收到的原始数据。

payload = struct.pack('

再说一遍,可以在开发人员参考手册中找到这些数据的说明。最后,在比特币网络上传输的每个有效载荷都需要加上一个包

头,其中包含了有效载荷的长度、校验和以及消息类型。包头还包含了魔术常数0xF9BEB4D9,它在所有主要的比特币消息中

都有。以下函数返回一个带有包头的比特币消息。

def get_bitcoin_message(message_type, payload):

将数据打包成正确的格式,并添加包头,然后发送给对等节点!

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

比特币协议要求在接收到版本消息后,返回一个Verack确认消息。 因为我正在构建的是一个微型的“为了兴趣而做”的客

户端,并且因为即使我不按照协议这么做的话,其他节点也不会认为我这个客户端有什么不同,所以我忽略了他们的版本信

息,并且没有发送响应消息。在连接时发送版本消息足以让我在后面能够发送更加有意义的消息。

运行上面的代码会打印出以下内容。结果看起来很有希望,“Satoshi”和“Verack”是在消息转储中看到的最好的单词!

因为如果我的版本消息格式错误的话,对端根本就不会做出回应。

b'\xf9\xbe\xb4\xd9version\x00\x00\x00\x00\x00f\x00\x00\x00\xf8\xdd\x9aL\x7f\x11\x01\x00\r\x00\x00\x00\x00\x00\x00\x00\xdd

要转移比特币,必须向比特币网络广播这笔交易。

有一个很重要的概念需要知道,那就是比特币地址的余额仅由该地址可以支出的“未花费的交易输出”(UTXO)的数量组

成。当鲍勃向爱丽丝发送比特币时,他只是创建了一个UTXO,而Alice(而且只有Alice)可以以此来创建另一个UTXO,并发

送比特币。因此,比特币地址的余额是由可以转移到另一个地址的比特币的数量决定,而不是直接由比特币的数量决定。

要强调的是,当有人说他们拥有X数量的比特币时,他们的意思是说所有可以用来支付的UTXO的总和是价值X比特币。区别很

小,但是很重要,比特币地址的余额不直接记录在某个地方,而是可以通过对可以支付的所有的UTXO进行求和来得到的。当

我意识到这个的时候,我大大惊叹了一句:“哦,原来它是这样工作的!”。

这样做的一个副作用就是交易输出可以是未花费的(UTXO),也可以是已花费的。不可能只花费某人为你花费的数量的一

半,然后在以后花费剩余的数量。对于收到的输出,如果你只想要花费其中一小部分,那你可以发送这一小部分给其他人,

而将其余部分发送给自己。其简化版本如下图所示。

在创建交易输出的时候,将同时创建一个锁定条件,这将允许将来的某人通过所谓的交易脚本来花费它。最常见的锁定条件

是:“要花费这个输出,你需要证明你拥有与特定公共地址对应的私钥”。这被称为“支付公钥哈希”脚本。然而,通过比

特币脚本创建其他类型的条件也是可以的。例如,创建可以由任何一个拥有某个哈希的人花费的交易输出,或者创建任何人

都可以花费的交易输出。

通过脚本,可以创建简单的基于合同的交易。脚本是一种基本的基于栈的语言,它包含了大量的操作,以此来检查哈希是否

相等以及验证签名。脚本并不是完整的图灵机,它不支持任何循环功能。与之有竞争关系的加密数字货币以太坊

(Ethereum)就是建立在这一点上,它拥有“智能合同”,并具有图灵机的完整语言。关于在加密货币中包含图灵机完整语

言的实用性、必要性和安全性方面有很多的争论,但我还是把争论留给其他人吧!

在标准术语中,比特币交易由输入和输出组成。输入是一个UTXO(当前正在花费的),输出是一个新的UTXO。单个输入可以

有多个输出,但输入需要在交易中完全消耗。输入剩余物的任何一部分都是矿工的采矿费。

对于我这个客户端,我希望能够将以前从交易所转移到的比特币发送到我的FEEDB0BDEADBEEF地址。使用与之前相同的过

程,我使用私钥BADCAFEFABC0FFEE生成了另外一个地址1QGNXLzGXhWTKF3HTSjuBMpQyUYFkWfgVC。

要创建一笔交易,首先是对“原始交易”进行打包,然后对原始交易进行签名。开发人员参考手册中详细说明了交易的内

容。下文将讲述交易的构成元素,但这里先说几个注意事项:

比特币中常见的术语包括签名脚本和pubkey脚本,我发现这有点混乱。签名脚本用于满足我们要在交易中使用的UTXO的条

件,而pubkey脚本用于设置条件以满足我们正在创建的UTXO的花费。签名脚本的另一个更好的名称是解锁脚本,而pubkey脚

本的另一个更好名称是锁定脚本。

比特币交易值在Satoshis中指定。Satoshi代表比特币可分割的最小部分,是一个比特币的十亿分之一。

为了简单起见,下面显示的是一个输出和一个输入的交易。可以以相同的方式来创建具有多个输入和输出的更复杂的交易。

忽略签名脚本和pubkey脚本,我们可以很容易地看到原始交易中的其他字段应该怎么设置。要将我的FEEDB0BDEADBEEF地址

中的资金发送到我的BADCAFEFABC0FFEE地址,我们来看看交易所创建的这笔交易:

交易ID为95855ba9f46c6936d7b5ee6733c81e715ac92199938ce30ac3e1214b8c2cd8d7。

发送到我的地址的输出是第二个输出,输出1(输出编号从0开始)。

输出的数量为1,因为我想将FEEDB0BDEADBEEF中的所有内容发送到BADCAFEFABC0FFEE。

值最大可以达到40万的Satoshis。为了留出一些费用来,一定要将值设置地小于这个最大值。我允许有2万的Satoshi作为费

用,所以将值设定为38万。

锁定时间将被设置为0,这样可以在任何时候或区块中包含交易。

对于我们的交易的Pubkey脚本,我们使用了“支付Pubkey哈希”(或p2pk)脚本。该脚本确保只有拥有公钥的人才能够使用

所提供的比特币地址来支付所创建的输出,并且所提供的签名已经由保存相应私钥的人来生成公钥。

要解锁已由p2pk脚本锁定的交易,用户需要提供公钥和原始交易的哈希签名。根据公钥计算出散列值,并与脚本创建的地址

进行比较,并对所提供的公钥进行签名验证。如果公钥的散列值和地址相等,并且签名通过验证,则可以花费输出了。

在比特币脚本的运算对象中,p2pk脚本如下所示:OP_DUP

将运算对象转换为值(可以在wiki上找到)并输入公共地址(在Base58Check编码之前)可以得到如下十六进制形式的脚

本:0x76

p2pk交易中的签名脚本有两个单独但关联的用途:

通过提供公钥散列到UTXO已发送的地址,脚本对我们正在尝试花费的UTXO进行校验(解锁)。

脚本还会给我们正在提交到网络的交易进行签名,这样就没有人能够在不使签名失效的情况下修改交易了。

但是,原始交易包含了一个签名脚本,而这个签名脚本又应该包含原始交易!要解决这个鸡和鸡蛋的问题,需要在对交易签

名之前把我们在签名脚本中使用的UTXO的Pubkey脚本放进去。据我所知,使用Pubkey作为占位符似乎并没有什么原因,占位

符可以是任意数据。

在原始交易被哈希之前,它还需要附加一个Hashtype值。最常见的Hashtype值是SIGHASH_ALL,它标识整个结构,使得输入

或输出都不能被修改。这个Wiki页面列出了其他哈希类型,这些类型允许在交易签名后对输入和输出的组合进行修改。

下面这个函数将原始交易的值放在一起,返回一个python字典。

def get_p2pkh_script(pub_key):

使用以下值调用代码能创建出我所感兴趣的原始交易。

private_key = address_utils.get_private_key('FEEDB0BDEADBEEF')

在上面的代码中我使用了私钥来生成to_address,这个看起来可能会让人感到困惑。其实这只是为了方便,并能展示出如何

找到to_address。在你和别人交易的时候,你需要问他们要公共地址,而不需要知道他们的私钥。

为了能够进行签名,并最终将交易发布到网上去,原始交易需要采用适当的手段进行打包。这个过程是在

get_packed_transaction函数中实现的,我不会把代码复制到这里,因为它本质上只是一些结构打包代码。 如果你感兴趣

的话,可以在我的Github代码库的bitcoin_transaction_utils.py文件中找到它。

我定义了一个生成签名脚本的函数。生成签名脚本后,应该替换掉占位符签名脚本。

def get_transaction_signature(transaction, private_key):

从本质上讲,签名脚本的提供是为了证明我可以把输出当做输入来花费,这个签名脚本是我之前交易的pubkey脚本的输入。

这个工作机制如下所示,这是从比特币wiki上获取的。从表格的第一行到下面的最后一行,每行都是脚本的一个迭代。 这

是用于支付pubkey散列pubkey脚本,上文提到过这是一个最常见的脚本。 它也是我正在创建的交易和我要履行的交易的脚

本。

如果提供的公钥散列不是脚本中的公钥散列,或者提供的签名与提供的公钥不匹配,那么这个脚本就会执行失败。这是为了

确保只有拥有pubkey脚本中地址的私钥的人才能够花费输出。

你可以看到,这是我第一次提供公钥。到目前为止,只有公共地址被公布出来。在这里提供公钥是为了能够验证交易的签

名。

为了能在网络上进行传输,我们可以使用get_transaction_signature函数对交易进行签名和打包了!这涉及到使用真实签

名脚本替换占位符签名脚本,并从交易中移除hash_code_type,如下所示。

signature = get_transaction_signature(raw, private_key )

随着交易打包和签名的完成,下一步就是网络的事情了。通过使用本文之前在bitcoin_p2p_message_utils.py中定义的一些

函数,下面的代码片段将消息头添加到待发送的数据上,并将其发送给对端节点。如前所述,首先需要发送一个版本消息,

以便能够接受后续的消息。

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

发送交易是最烦人的一部分。如果我发送了一个结构或签名错误的交易,则对端节点通常会删除连接,或者更好一点,回复

一个包含错误信息的消息。类似于这样的错误消息(非常得烦人):“S值不需要这么高”,这是由于使用sigencode_der的

ECSDA编码方法对交易散列进行签名导致的。尽管签名是有效的,但实际上比特币矿工并不喜欢以允许网络垃圾邮件形式格

式化的ECSDA签名。这个问题的解决方案就是使用sigencode_der_canonize函数,该函数用于将签名格式化为其他的格式。

这是一个简单但非常难调试的问题!

不管怎么样,我终于让程序运行起来了,看到我的交易进入了区块链,我非常得兴奋!当获知我的这个小巧简洁并且是纯手

工打造的交易将永远成为比特币账户的一部分的时候,心中的成就感油然而生。

当我提交交易的时候,我的交易费用相对于中位数来说相当得低(我通过比特币费用网站查到的),因此这花了矿工大约5

个小时的时间来决定将其包含在一个区块中。我通过查看交易的确认次数来检查这一点,这是对交易所涉及的区块数量的度

量。在写这篇文章的时候,有190个确认。这意味着在我的交易的区块之后,还有190个区块。这可以相当安全地得到确认,

因为需要对网络进行猛烈的攻击才能重写190个块来删除我的交易。

我希望你能通过阅读本文来对比特币的工作原理有所了解。虽然这里提供的大部分信息并不是很实用,并且你通常只会使用

某个客户端来完成所有的操作,但是我认为更好地理解工作原理能够让你更好的了解客户端内部发生的事情,并让你对这项

技术更有信心。

如果你想阅读更详细的代码,或者深入地研究这个示例,请查看我的Github代码库。在比特币世界里还有很多的探索空间,

我只是提供了一个非常常见的比特币的例子。那里肯定还有更多更酷的功能,而不仅仅是在两个地址之间转移价值!我也没

有研究挖掘比特币以及向区块链添加交易的过程。

如果你看到这里,你可能已经意识到,我转移到1QGNXLzGXhWTKF3HTSjuBMpQyUYFkWfgVC的380000的Satoshi(或0.0038比特

币)能被任何人取走,因为本文中有该地址的私钥。我非常感兴趣地想知道多久之后这些比特币会被转移走,我希望大家能

够采用我这里介绍的一些技巧来做到这一点。如果你刚刚将私钥加载到钱包应用程序中,那么我会鄙视你,但我不会阻止

你!在撰写本文时,这些比特币价值约为10美元,但如果把比特币“拿到月球去”,谁知道它值多少呢!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335

推荐阅读更多精彩内容