主页 > token.im钱包下载 > 探索比特币底层技术

探索比特币底层技术

token.im钱包下载 2023-01-17 14:05:50

以太坊地址比特币地址_比特币分叉影响比特币总量_比特币地址格式

比特币分叉影响比特币总量_比特币地址格式_以太坊地址比特币地址

译者注:本文介绍了比特币的一些底层技术,包括地址、P2P网络、创建和发布交易等。 文章还给出了关键的 Python 代码片段,通过这些代码片段可以构建一个最小和最基本的比特币客户端程序。 下面是翻译。

原文:比特币引擎盖下的一瞥作者:Sam Lewis

比特币真的很酷。 当然,关于这项技术还有很多争议,包括:是否是一项有用的技术,加密数字货币是否存在泡沫,能否解决目前的管理问题等。 但在纯技术层面上,神秘的中本聪创造了这项非凡的技术。

不幸的是,虽然有很多资源可以解释比特币在高层次上是如何工作的,但在低层次上却没有太多的资料。 在我看来,如果从10,000英尺的高度看,只能凭感觉得到。

对于这样一个新兴领域,我发现自己很想了解比特币是如何运作的。 幸运的是,由于比特币本质上是去中心化和点对点的,任何人都可以开发符合协议标准的客户端。 为了更好地理解比特币是如何工作的,我决定开发自己的比特币客户端,可以将交易发布到比特币区块链。

这篇文章介绍了开发一个最小但可用的比特币客户端的过程,该客户端可以创建交易并将其提交到比特币对等网络以包含在区块链中。 如果您只想阅读原始代码,请随时查看我的 Github 存储库。

地址生成

要成为比特币网络的一部分,您必须拥有一个可以发送和接收资金的地址。 比特币使用公钥加密,地址是从私钥派生的公钥的散列版本。 令人惊讶的是,与大多数公钥加密不同,公钥一直保密,直到从该地址发送资金。

术语解释:在比特币中,客户使用的术语“钱包”是指地址的集合。 从协议层面来说,没有钱包的概念,只有地址。

比特币对其地址使用椭圆曲线公钥密码术。 椭圆曲线密码术,如 RSA,用于从私钥生成公钥,但占用空间较小。 如果您有兴趣了解这种加密技术背后的数学原理,Cloudflare 上的一篇介绍性文章值得一读。

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

比特币地址格式_以太坊地址比特币地址_比特币分叉影响比特币总量

在 Python 中,我使用 ecsda 库来实现椭圆曲线加密。 以下代码片段显示了从相当重要(且相当不安全)的私钥 0xFEEDB0BDEADBEEF(零填充以达到 64 或 256 个十六进制字符串)获取公钥的过程。 如果你想在地址中存储任何实际值,那么你需要一种更安全的私钥生成方法!

有趣的事实:我最初使用密钥 0xFACEBEEF 创建了一个地址,并向它发送了 0.0005 个比特币。 一个月后,有人偷走了我的 0.0005 个比特币! 我猜有人必须偶尔使用一些简单或通用的私钥访问地址。 你真的应该使用一些更合适的密钥派生技术!

从 ecdsa 导入 SECP256k1, SigningKeydef get_private_key(hex_string):

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

0000000000000000000000000000000000000000000000000feedb0bdeadbeef

比特币地址格式_比特币分叉影响比特币总量_以太坊地址比特币地址

获得的公钥(十六进制):

04d077e18fd45c031e0d256d75dfa8c3c21c589a861c4c33b99e64cf613113fcff9fc9d90a9d81346bcac64d3c01e6e0ef0828543edad73c0e257b845812cc8d28

以0x04开头的公钥表示这是一个未压缩的公钥,也就是说ECDSA(Elliptic Curve Digital Signature Algorithm)中的x轴和y轴坐标是简单相关的。 根据ECSDA的原则,如果知道x值,那么y值只能取两个值,一偶一奇。 基于此信息,可以仅使用 x 中的值和 y 的极性来表示公钥。 这将公钥的大小从 65 位减少到 33 位,这个过程(以及随后计算的地址)称为压缩。 对于压缩公钥,根据 y 的极性,它将以 0x02 或 0x03 开头。 未压缩的公钥通常用于比特币,这就是我在这里使用的。

要从公钥生成比特币地址,公钥首先用 sha256 散列,然后用 ripemd160 散列。 这种双重散列提供了额外的安全层,ripemd160 散列在 sha256 的 256 位散列之后提供了 160 位散列,从而减少了地址的长度。 一个有趣的结果是两个不同的公钥可以哈希生成相同的地址! 但是,对于 2160 个不同的地址比特币地址格式,这在短时间内不太可能发生。

导入 hashlibdef get_public_address(public_key):

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

如前所述,有趣的是,从私钥到公钥的转换以及从公钥到公共地址的转换是一种单向转换。 如果您有地址,找到关联公钥的唯一方法是求解 SHA256 哈希。 这与大多数公钥加密技术不同,公钥是公开的而私钥是隐藏的。 在目前的情况下,公钥和私钥都是隐藏的,只有地址(哈希公钥)被公开。

公钥被隐藏是有原因的。 虽然从公钥计算出对应的私钥通常是不可行的,但是如果私钥的生成方法被破解了,那么私钥就可以很容易的从公钥中推导出来。 2013 年,这种情况发生在安卓比特币钱包上。 Android 在随机数生成方面存在一个严重缺陷,它打开了一个向量,攻击者可以通过该向量从公钥中找到私钥。 这就是不鼓励地址重用的原因,因为要签署交易,您必须公开公钥。 如果您在向某个地址发送交易后不重复使用该地址,则无需担心该地址的私钥被暴露。

表示比特币地址的标准方法是使用 Base58Check 对其进行编码。 这种编码只是地址的表示(因此可以解码/反转)。 它生成 1661HxZpSy5jhcJ2k6av2dxuspa8aafDac 形式的地址。 Base58Check 编码提供了一种更短的地址表示方法,并且还具有内置的校验和,可以检测出错误的地址。 在几乎每个比特币客户端中,你看到的地址都是 Base58Check 编码的地址。 Base58Check 还包括一个版本号,我在下面的代码中将其设置为 0,表示该地址是一个公钥哈希。

# 使用 58 个字符的字母表 BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' def base58_encode(version, public_address):

以上所有代码都给我展示了从私钥FEEDB0BDEADBEEF(需要补0)到比特币地址KK2xni6gmTtdnSGRiuAf94jciFgRjDj7W的整个过程!

有了这个地址,我现在就可以来拿比特币了! 为了将比特币存入我的地址,我用澳元从 btcmarkets 购买了 0.0045 个比特币(在撰写本文时约为 11 美元)。 使用btcmarket的交易入口,我将其转入上述地址,在此过程中损失了0.0005个比特币的交易手续费。 你可以在区块链交易*95855ba9f46c6936d7b5ee6733c81e715ac92199938ce30ac3e1214b8c2cd8d7*中看到这笔交易。

连接到 P2P(点对点)网络

现在我有了地址和一些比特币,事情变得更有趣了。 如果我想将比特币发送到其他地方,我必须连接到比特币 P2P 网络。

指导

当我第一次了解比特币时,我发现了一个关键问题:由于网络的去中心化特性,网络上的节点如何找到其他节点? 没有中央控制点,比特币客户端如何知道如何导航并与网络的其余部分交互?

理论遵循实践,在最初的节点发现过程中,中心化控制器很少。 新节点寻找其他节点的方式原则上是通过DNS寻找比特币社区成员维护的“DNS种子”服务器。

比特币分叉影响比特币总量_以太坊地址比特币地址_比特币地址格式

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

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

以下 Python 代码首先连接到一个 DNS 种子,然后打印出可以连接的节点列表。 使用套接字库,它基本上执行 nslookup 操作并返回 seed.bitcoin.sipa.be 上查询的第一个 IPv4 地址结果。

import socket#向比特币DNS服务器发送DNS请求寻找节点 nodes = socket.getaddrinfo('seed.bitcoin.sipa.be', None)#选择第一个节点 node = nodes[0][4][0]

查到的地址是208.67.251.126,是友好的peer节点,我可以连接到这个地址!

向对等节点问好

每个节点之间的连接是通过TCP建立的。 当连接到对等节点时,比特币协议的初始握手消息是版本消息。 在节点交换版本消息之前,不会接受其他消息。

比特币协议消息在“比特币开发者参考”中有详细记录。 使用开发人员参考手册作为指南,可以在 Python 中创建版本消息,如下面的代码片段所示。 大部分代码用于打开与对等节点的连接。 如果您对详细信息感兴趣,请查看开发者参考。

version = 70014services = 1 # 不是完整节点,不能提供任何数据timestamp = int(time.time())

使用Python的struct库,可以将version payload数据打包成正确的格式,请特别注意数据的字节顺序和字节宽度。 将数据打包成正确的格式很重要,否则同行将无法理解收到的原始数据。

有效载荷 = 结构。 盒('

同样,可以在开发人员参考手册中找到对这些数据的描述。 最后,比特币网络上传输的每一个有效负载都需要添加一个头部,其中包含有效负载长度、校验和和消息类型。 标头还包含魔法常量 0xF9BEB4D9,它存在于所有主要的比特币消息中。 以下函数返回带有标头的比特币消息。

def get_bitcoin_message(message_type, payload):

将数据打包成正确的格式,添加标头,然后发送给同行!

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

比特币协议要求在收到版本信息后返回一个Verack确认信息。 因为我正在构建一个微型的“为了好玩”的客户端,并且因为如果我不按照协议进行操作,其他节点不会将我视为不同的客户端,所以我忽略了它们的版本信息,并且没有发送响应消息. 在连接上发送版本消息足以让我稍后发送更有意义的消息。

运行上面的代码打印出以下内容。 结果看起来很有希望,“Satoshi”和“Verack”是消息转储中看到的最好的词! 因为如果我的版本消息格式不正确,对端根本不会响应。

比特币地址格式_比特币分叉影响比特币总量_以太坊地址比特币地址

b'xf9xbexb4xd9versionx00x00x00x00x00fx00x00x00xf8xddx9aLx7fx11x01x00rx00x00x00x00x00x00x00xddR1Yx00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00xffxffxcbxcex1dxfcxe9jrx00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x06xb8>*x88@Ix8ex10/Satoshi:0.14.0/t)x07x00x01xf9xbexb4xd9verackx00x00x00x00x00x00x00x00x00x00]xf6xe0xe2'

比特币交易

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

需要知道的一个重要概念是,比特币地址的余额仅包含该地址可以花费的“未花费交易输出”(UTXO)的数量。 当 Bob 将比特币发送给 Alice 时,他只是创建了一个 UTXO,Alice(并且只有 Alice)可以使用该 UTXO 创建另一个 UTXO 并发送比特币。 因此,一个比特币地址的余额是由可以转移到另一个地址的比特币数量决定的,而不是直接由比特币数量决定的。

需要强调的是,当有人说他们拥有 X 数量的比特币时,他们的意思是所有可用于支付的 UTXO 的总和价值 X 比特币。 差别很小,但很重要,一个比特币地址的余额并没有直接记录在某个地方,而是可以通过将所有可以支付的 UTXO 相加得到。 当我意识到这一点时,我大吃一惊:“哦,原来是这样的!”。

这样做的一个副作用是交易输出可以是未花费的(UTXO)或已花费的。 不可能只花别人为你花的钱的一半,然后再花剩下的钱。 如果您只想花费收到的输出的一小部分,您可以将该部分发送给其他人,其余部分发送给您自己。 它的简化版本如下图所示。

以太坊地址比特币地址_比特币分叉影响比特币总量_比特币地址格式

当创建交易输出时,同时创建锁定条件,这将允许将来有人通过所谓的交易脚本花费它。 最常见的锁定条件是:“要花费这个输出,你需要证明你拥有特定公共地址对应的私钥”。 这被称为“支付公钥哈希”脚本。 但是,也可以通过比特币脚本创建其他类型的条件。 例如,创建任何人都可以使用特定哈希的交易输出,或者创建任何人都可以使用的交易输出。

通过脚本,可以创建简单的基于合约的交易。 脚本是一种基于堆栈的基本语言,其中包含大量用于检查哈希相等性和验证签名的操作。 Script 不是一个完整的图灵机,它不支持任何循环函数。 竞争对手的加密货币以太坊(Ethereum)就是建立在这一点上的,它有“智能合约”,有完整的图灵机语言。 关于在加密货币中包含图灵完备语言的实用性、必要性和安全性存在很多争论,但我将把争论留给其他人!

在标准术语中,比特币交易由输入和输出组成。 输入是一个 UTXO(当前被花费),输出是一个新的 UTXO。 单个输入可以有多个输出,但输入需要在交易中被完全消耗掉。 输入剩余的任何部分都是矿工的挖矿费。

对于我的客户,我希望能够将之前从交易所转移的比特币发送到我的 FEEDB0BDEADBEEF 地址。 使用与之前相同的过程,我使用私钥 BADCAFEFABC0FFEE 生成了另一个地址 1QGNXLzGXhWTKF3HTSjuBMpQyUYFkWfgVC。

创建原始交易

要创建交易,首先要打包“原始交易”,然后对原始交易进行签名。 交易的内容在开发者参考手册中有详细说明。 构成交易的元素如下所述,但这里有一些注意事项:

比特币中的常用术语包括签名脚本和公钥脚本,我觉得这有点令人困惑。 签名脚本用于满足我们要在交易中使用的UTXO的条件,pubkey脚本用于设置条件以满足我们正在创建的UTXO的花费。 签名脚本的另一个更好的名称是解锁脚本,公钥脚本的另一个更好的名称是锁定脚本。

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

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

比特币地址格式_以太坊地址比特币地址_比特币分叉影响比特币总量

比特币分叉影响比特币总量_比特币地址格式_以太坊地址比特币地址

忽略签名脚本和公钥脚本,我们很容易看出原始交易中的其他字段应该如何设置。 要从我的 FEEDB0BDEADBEEF 地址向我的 BADCAFEFABC0FFEE 地址发送资金,让我们看一下交易所创建的这个交易:

交易 ID 为 95855ba9f46c6936d7b5ee6733c81e715ac92199938ce30ac3e1214b8c2cd8d7。

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

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

最大值可达40万聪。 请务必将值设置为低于此最大值,以便收取一些费用。 我允许 20,000 聪作为费用,因此将值设置为 380,000。

锁定时间将设置为 0,因此可以在任何时间或块中包含交易。

对于我们交易的 Pubkey 脚本,我们使用了“Pay Pubkey Hash”(或 p2pk)脚本。 该脚本确保只有拥有公钥的人才能使用提供的比特币地址花费创建的输出,并且所提供的签名是由持有相应私钥的人生成的。

要解锁已被 p2pk 脚本锁定的交易,用户需要提供原始交易的公钥和哈希签名。 根据公钥计算哈希值,与脚本创建的地址进行比较,对提供的公钥进行签名验证。 如果公钥和地址的哈希值相等,并且签名通过验证,则可以使用输出。

在比特币脚本的操作对象中,p2pk脚本如下: OP_DUP

将操作数转换为值(在 wiki 上找到)并输入公共地址(在 Base58Check 编码之前)会产生一个十六进制形式的脚本,如下所示:0x76

签署交易

p2pk 交易中的签名脚本服务于两个独立但相关的目的:

该脚本通过向 UTXO 发送到的地址提供公钥哈希来验证(解锁)我们尝试花费的 UTXO。

该脚本还将签署我们提交给网络的交易,这样就没有人可以在不使签名无效的情况下修改交易。

然而,原始交易包含一个签名脚本,而签名脚本又应该包含原始交易! 要解决这个先有鸡还是先有蛋的问题,我们需要在签署交易之前,将我们使用的UTXO Pubkey脚本放到签名脚本中。 据我所知,似乎没有理由使用 Pubkey 作为占位符,它可以是任意数据。

在对原始交易进行哈希处理之前,它还需要附加一个 Hashtype 值。 最常见的 Hashtype 值是 SIGHASH_ALL,它标识整个结构,使得输入和输出都不能被修改。 此 wiki 页面列出了其他哈希类型,这些哈希类型允许在交易签署后修改输入和输出的组合。

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

以太坊地址比特币地址_比特币地址格式_比特币分叉影响比特币总量

def get_p2pkh_(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(交易,私钥):

本质上,提供签名脚本是为了证明我可以将输出作为输入。 这个签名脚本是我之前交易的pubkey脚本的输入。 工作机制如下所示,摘自比特币维基。 每行都是脚本的迭代,从表的第一行到下面的最后一行。 这是用于支付公钥哈希的公钥脚本,如上所述,这是最常见的脚本之一。 它也是我正在创建的交易和我正在尝试完成的交易的脚本。

以太坊地址比特币地址_比特币地址格式_比特币分叉影响比特币总量

如果提供的公钥哈希不是脚本中的公钥哈希,或者如果提供的签名与提供的公钥不匹配比特币地址格式,脚本将失败。 这是为了确保只有拥有公钥脚本中地址私钥的人才能使用输出。

如您所见,这是我第一次提供公钥。 到目前为止,只发布了公共地址。 这里提供公钥是为了能够验证交易的签名。

为了能够在网络上传输,我们可以使用get_transaction_signature函数对交易进行签名和打包! 这涉及用真实签名脚本替换占位符签名脚本,并从交易中删除 hash_code_type,如下所示。

signature = get_transaction_signature(原始,私钥)

交易后

打包和签署交易后,下一步就是网络。 使用本文前面 bitcoin_p2p_message_utils.py 中定义的一些函数,下面的代码片段将消息头添加到要发送的数据并将其发送给对等方。 如前所述,首先需要发送版本消息,以便接受后续消息。

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

发送交易是最烦人的部分。 如果我发送一个结构或签名错误的交易,对等方通常会断开连接,或者更好的是,回复一条包含错误信息的消息。 像这样的错误信息(很烦人):“S value doesn't needed to be this high”是使用sigencode_der的ECSDA编码方式签署交易哈希导致的。 尽管签名有效,但实际上比特币矿工不喜欢以允许网络垃圾邮件的方式格式化的 ECSDA 签名。 这个问题的解决方案是使用 sigencode_der_canonize 函数,该函数用于将签名格式化为其他格式。 这是一个简单但很难调试的问题!

无论如何,我终于让程序运行起来了,看到我的交易进入区块链非常兴奋! 当我得知我的这笔小而简单的手工交易将永远是比特币账户的一部分时,我的内心充满了成就感。

当我提交交易时,我的交易费用相对于中位数来说相当低(我通过比特币费用网站查看),所以矿工花了大约 5 个小时才决定将其包含在一个区块中。 我通过查看交易的确认数量来检查这一点,这是衡量交易中涉及的区块数量的指标。 在撰写本文时,已有 190 个确认。 这意味着在我的交易区块之后,还剩下 190 个区块。 这可以相当安全地确认,因为需要对网络进行暴力攻击才能重写 190 个区块才能删除我的交易。返回搜狐查看更多