返回 登录
6

比特币底层技术探秘

原文A peek under Bitcoin’s hood
作者:Sam Lewis
翻译:雁惊寒

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

比特币真的很酷。当然,关于这项技术目前还存在着不少的争议,包括:它是否是一项有用的技术,加密数字货币是否存在着泡沫,目前面临的管理问题是否能够得到解决。但是从纯技术层面来说,神秘的Satoshi Nakamoto创造了这个引人注目的技术。

不幸的是,尽管可以找到很多资源站在较高的层次解释了比特币的工作原理,但却没有有关底层的资料。在我看来,如果你从一万英尺的高度看的话,你只能够凭感觉来意会了。

对于这么一个新兴的领域,我发现我自己非常渴望去了解比特币的工作机制。幸运的是,因为比特币本质上是分散的,并且是对等的,所以任何人都能够开发出一款符合协议标准的客户端。为了能够更好地了解比特币的工作原理,我决定开发一款属于我自己的比特币客户端,可以向比特币区块链发布交易。

这篇文章介绍了开发一个最小而又可用的比特币客户端的过程,它可以创建一笔交易并将其提交到比特币对等网络上,以便让它包含在区块链中。如果你只是想读一下原始代码,可以随时查看我的Github代码库

地址的生成

要成为比特币网络的一部分,必须要有一个可以发送和接收资金的地址。比特币使用了公钥加密技术,而地址是从私钥派生出来的公钥的散列版本。令人吃惊的是,与大多数的公共密钥加密技术不同,它的公共密钥会一直保密存放,直到资金从这个地址发送出去。

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

比特币对其地址使用了椭圆曲线公钥密码技术。椭圆曲线加密技术与RSA一样,用于从私钥生成公钥,但其占用的空间更小。如果你有兴趣了解一下这种加密技术背后的数学知识的话,那么Cloudflare上的一篇入门文章值得一读。

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

bitcoin address generation

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

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

from ecdsa import SECP256k1, SigningKey

def get_private_key(hex_string):
    return bytes.fromhex(hex_string.zfill(64)) # 在十六进制字符串的前面补零以达到64个字符的长度

def get_public_key(private_key):
    # this returns the concatenated x and y coordinates for the supplied private address
    # the prepended 04 is used to signify that it's uncompressed
    return (bytes.fromhex("04") + SigningKey.from_string(private_key, curve=SECP256k1).verifying_key.to_string())

private_key = get_private_key("FEEDB0BDEADBEEF")
public_key = get_public_key(private_key)

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

0000000000000000000000000000000000000000000000000feedb0bdeadbeef

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

04d077e18fd45c031e0d256d75dfa8c3c21c589a861c4c33b99e64cf613113fcff9fc9d90a9d81346bcac64d3c01e6e0ef0828543edad73c0e257b845812cc8d28

以0x04开头的公钥表明这是一个没有经过压缩的公钥,这意味着ECDSA(椭圆曲线数字签名算法)中x和y轴坐标简单的关联在一起。根据ECSDA的原理,如果你知道x值,那么y值只能取两个值,一个偶数和一个奇数。基于这个信息,可以仅使用x中的一个值和y的极性来表达一个公钥。这使得公钥的大小从65位减少到33位,这个过程(和后续计算的地址)称之为压缩。对于压缩后的公钥,根据y的极性,将以0x02或0x03开头。未压缩的公钥常用于比特币,这也是我在这里所使用的。

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

import hashlib

def get_public_address(public_key):
    address = hashlib.sha256(public_key).digest()

    h = hashlib.new('ripemd160')
    h.update(address)
    address = h.digest()

    return address

public_address = get_public_address(public_key)

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

如前所述,有一点比较有意思,从私钥到公钥的转换以及从公钥到公共地址的转换都是单向转换。如果你有一个地址,那么找到关联公钥的唯一办法就是解决SHA256哈希问题。这与大多数的公钥加密技术不同,那些机密技术中公钥是公开的,而私钥会隐藏起来。而在当前这个情况下,公钥和私钥都会隐藏起来,而只公布地址(哈希过的公钥)。

隐藏公钥是有原因的。虽然从公钥计算得到相应的私钥通常是不可行的,但是如果生成私钥的方法已经被破解,那么就能很容易地通过公钥推断出私钥。在2013年,这种事情发生在了Android比特币钱包的身上。 Android在随机数的生成上有一个关键性的缺陷,它会打开一个向量,攻击者通过这个向量可以从公钥找到私钥。这也就是为什么不鼓励地址重用,因为要签署交易,你就得公开公钥。如果你在向某个地址发送交易后不重用该地址,那你就无需担心该地址的私钥会暴露。

表示一个比特币地址的标准方式是使用Base58Check进行编码。该编码只是地址的一种表示形式(因此可以被解码/反推)。它生成类似于1661HxZpSy5jhcJ2k6av2dxuspa8aafDac这种形式的地址。 Base58Check编码提供了一个较短的地址表示方法,并且还内置校验和,可以检测出错误的地址。几乎在每个比特币客户端中,你看到的地址都是Base58Check编码后的地址。 Base58Check还包含一个版本号,在下面的代码中我把它设置为0,这表示该地址是一个pubkey散列。

# 58 character alphabet used
BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'

def base58_encode(version, public_address):
    """
    获取Base58Check编码的字符串
    请参阅:https://en.bitcoin.it/wiki/base58Check_encoding
    """
    version = bytes.fromhex(version)
    checksum = hashlib.sha256(hashlib.sha256(version + public_address).digest()).digest()[:4]
    payload = version + public_address + checksum

    result = int.from_bytes(payload, byteorder="big")

    print(result)

    # 计算前面的0的数量
    padding = len(payload) - len(payload.lstrip(b'\0'))
    encoded = []

    while result != 0:
        result, remainder = divmod(result, 58)
        encoded.append(BASE58_ALPHABET[remainder])

    return padding*"1" + "".join(encoded)[::-1]

bitcoin_address = base58_encode("00", public_address)

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

通过这个地址,我现在就可以来获得比特币了!为了把比特币放入我的地址,我用澳元从btcmarkets购买了0.0045比特币(在撰写本文时约为11美元)。 使用btcmarket的交易门户,我将其转移到上面的地址,在此过程中会损失0.0005比特币的交易费用。你可以在交易* 95855ba9f46c6936d7b5ee6733c81e715ac92199938ce30ac3e1214b8c2cd8d7*中的区块链上看到这笔交易。

连接到P2P(点对点)网络

现在,我有了一个地址,而且上面还有一些比特币,事情变得更有趣了。如果我想将比特币发送到别的地方,那么就必须连接到比特币的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 = 70014
services = 1 # not a full node, cant provide any data
timestamp = int(time.time())
addr_recvservices = 1
addr_recvipaddress = socket.inet_pton(socket.AF_INET6, "::ffff:127.0.0.1") #ip address of receiving node in big endian
addr_recvport = 8333
addr_transservices = 1
addr_transipaddress = socket.inet_pton(socket.AF_INET6, "::ffff:127.0.0.1")
addr_transport = 8333
nonce = 0
user_agentbytes = 0
start_height = 329167
relay = 0

使用Python的struct库,版本有效载荷数据可以打包成正确的格式,请特别注意一下数据的字节顺序和字节宽度。将数据打包成正确的格式很重要,不然对端节点将无法理解收到的原始数据。

payload = struct.pack("<I", version)
payload += struct.pack("<Q", services)
payload += struct.pack("<Q", timestamp)
payload += struct.pack("<Q", addr_recvservices)
payload += struct.pack("16s", addr_recvipaddress)
payload += struct.pack(">H", addr_recvport)
payload += struct.pack("<Q", addr_transservices)
payload += struct.pack("16s", addr_transipaddress)
payload += struct.pack(">H", addr_transport)
payload += struct.pack("<Q", nonce)
payload += struct.pack("<H", user_agentbytes)
payload += struct.pack("<I", start_height)

再说一遍,可以在开发人员参考手册中找到这些数据的说明。最后,在比特币网络上传输的每个有效载荷都需要加上一个包头,其中包含了有效载荷的长度、校验和以及消息类型。包头还包含了魔术常数0xF9BEB4D9,它在所有主要的比特币消息中都有。以下函数返回一个带有包头的比特币消息。

def get_bitcoin_message(message_type, payload):
    header = struct.pack(">L", 0xF9BEB4D9)
    header += struct.pack("12s", bytes(message_type, 'utf-8'))
    header += struct.pack("<L", len(payload))
    header += hashlib.sha256(hashlib.sha256(payload).digest()).digest()[:4]

    return header + payload

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

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((node, 8333))
s.send(get_bitcoin_message("version", payload))
print(s.recv(1024))

比特币协议要求在接收到版本消息后,返回一个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\xddR1Y\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xcb\xce\x1d\xfc\xe9j\r\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\xb8>*\x88@I\x8e\x10/Satoshi:0.14.0/t)\x07\x00\x01\xf9\xbe\xb4\xd9verack\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00]\xf6\xe0\xe2'

比特币交易

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

有一个很重要的概念需要知道,那就是比特币地址的余额仅由该地址可以支出的“未花费的交易输出”(UTXO)的数量组成。当鲍勃向爱丽丝发送比特币时,他只是创建了一个UTXO,而Alice(而且只有Alice)可以以此来创建另一个UTXO,并发送比特币。因此,比特币地址的余额是由可以转移到另一个地址的比特币的数量决定,而不是直接由比特币的数量决定。

要强调的是,当有人说他们拥有X数量的比特币时,他们的意思是说所有可以用来支付的UTXO的总和是价值X比特币。区别很小,但是很重要,比特币地址的余额不直接记录在某个地方,而是可以通过对可以支付的所有的UTXO进行求和来得到的。当我意识到这个的时候,我大大惊叹了一句:“哦,原来它是这样工作的!”。

这样做的一个副作用就是交易输出可以是未花费的(UTXO),也可以是已花费的。不可能只花费某人为你花费的数量的一半,然后在以后花费剩余的数量。对于收到的输出,如果你只想要花费其中一小部分,那你可以发送这一小部分给其他人,而将其余部分发送给自己。其简化版本如下图所示。

bitcoin address generation

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

通过脚本,可以创建简单的基于合同的交易。脚本是一种基本的基于栈的语言,它包含了大量的操作,以此来检查哈希是否相等以及验证签名。脚本并不是完整的图灵机,它不支持任何循环功能。与之有竞争关系的加密数字货币以太坊(Ethereum)就是建立在这一点上,它拥有“智能合同”,并具有图灵机的完整语言。关于在加密货币中包含图灵机完整语言的实用性、必要性和安全性方面有很多的争论,但我还是把争论留给其他人吧!

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

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

创建原始交易

要创建一笔交易,首先是对“原始交易”进行打包,然后对原始交易进行签名。开发人员参考手册中详细说明了交易的内容。下文将讲述交易的构成元素,但这里先说几个注意事项:

  • 比特币中常见的术语包括签名脚本和pubkey脚本,我发现这有点混乱。签名脚本用于满足我们要在交易中使用的UTXO的条件,而pubkey脚本用于设置条件以满足我们正在创建的UTXO的花费。签名脚本的另一个更好的名称是解锁脚本,而pubkey脚本的另一个更好名称是锁定脚本。
  • 比特币交易值在Satoshis中指定。Satoshi代表比特币可分割的最小部分,是一个比特币的十亿分之一。

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

字段 描述
Version 交易的版本 (当前是1)
Number of inputs 需要花费的输入的数量
Transaction ID 需要花费地交易地源头
Output number 需要花费的交易的输出
Signature script length 签名脚本的长度(字节)
Signature script 签名脚本
Sequence number 除非你要使用一个锁定时间,否则总是0xffffffff
Number of outputs 需要创建的输出的数量
Value 需要花费的Satoshis的数量
Pubkey script length pubkey脚本的长度(字节)
Pubkey script pubkey脚本
Lock time 包含在区块中的交易的最早时间/区块号

忽略签名脚本和pubkey脚本,我们可以很容易地看到原始交易中的其他字段应该怎么设置。要将我的FEEDB0BDEADBEEF地址中的资金发送到我的BADCAFEFABC0FFEE地址,我们来看看交易所创建的这笔交易:

  • 交易ID为95855ba9f46c6936d7b5ee6733c81e715ac92199938ce30ac3e1214b8c2cd8d7
  • 发送到我的地址的输出是第二个输出,输出1(输出编号从0开始)。
  • 输出的数量为1,因为我想将FEEDB0BDEADBEEF中的所有内容发送到BADCAFEFABC0FFEE
  • 值最大可以达到40万的Satoshis。为了留出一些费用来,一定要将值设置地小于这个最大值。我允许有2万的Satoshi作为费用,所以将值设定为38万。
  • 锁定时间将被设置为0,这样可以在任何时候或区块中包含交易。

对于我们的交易的Pubkey脚本,我们使用了“支付Pubkey哈希”(或p2pk)脚本。该脚本确保只有拥有公钥的人才能够使用所提供的比特币地址来支付所创建的输出,并且所提供的签名已经由保存相应私钥的人来生成公钥。

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

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

OP_DUP
OP_HASH160
<Length of address in bytes>
<Bitcoin address>
OP_EQUALVERIFY
OP_CHECKSIG

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

0x76
0xA9
0x14
0xFF33195EC053D6E58D5FD3CC67747D3E1C71B280
0x88
0xAC

对交易进行签名

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

  • 通过提供公钥散列到UTXO已发送的地址,脚本对我们正在尝试花费的UTXO进行校验(解锁)。
  • 脚本还会给我们正在提交到网络的交易进行签名,这样就没有人能够在不使签名失效的情况下修改交易了。

但是,原始交易包含了一个签名脚本,而这个签名脚本又应该包含原始交易!要解决这个鸡和鸡蛋的问题,需要在对交易签名之前把我们在签名脚本中使用的UTXO的Pubkey脚本放进去。据我所知,使用Pubkey作为占位符似乎并没有什么原因,占位符可以是任意数据。

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

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

def get_p2pkh_script(pub_key):
    """
    这是一个标准的“支付pubkey散列”脚本
    """
    # 先是OP_DUP,然后是OP_HASH160,然后是20 bytes (pub地址的长度)
    script = bytes.fromhex("76a914")

    # 要支付的地址
    script += pub_key

    # OP_EQUALVERIFY,然后是OP_CHECKSIG
    script += bytes.fromhex("88ac")

    return script

def get_raw_transaction(from_addr, to_addr, transaction_hash, output_index, satoshis_spend):
    """
    获取一个输入对应一个输出的交易的原始交易
    """
    transaction = {}
    transaction["version"] = 1
    transaction["num_inputs"] = 1

    # 交易的字节序需要反过来:
    # https://bitcoin.org/en/developer-reference#hash-byte-order
    transaction["transaction_hash"] = bytes.fromhex(transaction_hash)[::-1]
    transaction["output_index"] = output_index

    # 临时让签名脚本成为老的pubkey脚本,这个脚本后面会被取代
    # 我假设之前的pubkey脚本就是这里的p2pkh脚本
    transaction["sig_script_length"] = 25
    transaction["sig_script"] = get_p2pkh_script(from_addr)

    transaction["sequence"] = 0xffffffff
    transaction["num_outputs"] = 1
    transaction["satoshis"] = satoshis_spend
    transaction["pubkey_length"] = 25
    transaction["pubkey_script"] = get_p2pkh_script(to_addr)
    transaction["lock_time"] = 0
    transaction["hash_code_type"] = 1

    return transaction

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

private_key = address_utils.get_private_key("FEEDB0BDEADBEEF")
public_key = address_utils.get_public_key(private_key)
from_address = address_utils.get_public_address(public_key)
to_address = address_utils.get_public_address(address_utils.get_public_key(address_utils.get_private_key("BADCAFEFABC0FFEE")))

transaction_id = "95855ba9f46c6936d7b5ee6733c81e715ac92199938ce30ac3e1214b8c2cd8d7"  
satoshis = 380000
output_index = 1

raw = get_raw_transaction(from_address, to_address, transaction_id, output_index, satoshis)

在上面的代码中我使用了私钥来生成to_address,这个看起来可能会让人感到困惑。其实这只是为了方便,并能展示出如何找到to_address。在你和别人交易的时候,你需要问他们要公共地址,而不需要知道他们的私钥。

为了能够进行签名,并最终将交易发布到网上去,原始交易需要采用适当的手段进行打包。这个过程是在get_packed_transaction函数中实现的,我不会把代码复制到这里,因为它本质上只是一些结构打包代码。 如果你感兴趣的话,可以在我的Github代码库的bitcoin_transaction_utils.py文件中找到它。

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

def get_transaction_signature(transaction, private_key):
    """
    获得原始交易的签名脚本
    """
    packed_raw_transaction = get_packed_transaction(transaction)
    hash = hashlib.sha256(hashlib.sha256(packed_raw_transaction).digest()).digest()
    public_key = address_utils.get_public_key(private_key)
    key = SigningKey.from_string(private_key, curve=SECP256k1)
    signature = key.sign_digest(hash, sigencode=util.sigencode_der)
    signature += bytes.fromhex("01") #hash code type

    sigscript = struct.pack("<B", len(signature))
    sigscript += signature
    sigscript += struct.pack("<B", len(public_key))
    sigscript += public_key

    return sigscript

从本质上讲,签名脚本的提供是为了证明我可以把输出当做输入来花费,这个签名脚本是我之前交易的pubkey脚本的输入。这个工作机制如下所示,这是从比特币wiki上获取的。从表格的第一行到下面的最后一行,每行都是脚本的一个迭代。 这是用于支付pubkey散列pubkey脚本,上文提到过这是一个最常见的脚本。 它也是我正在创建的交易和我要履行的交易的脚本。

脚本 描述
signature
publicKey
OP_DUP
OP_HASH160
pubKeyHash
OP_EQUALVERIFY
OP_CHECKSIG
签名脚本中的signaturepublicKey合并到pubkey脚本中。
signature
publicKey
OP_DUP
OP_HASH160
pubKeyHash
OP_EQUALVERIFY
OP_CHECKSIG
signaturepublicKey添加到栈中
signature
publicKey
publicKey
OP_HASH160
pubKeyHash
OP_EQUALVERIFY
OP_CHECKSIG
栈顶的元素(publicKey)被OP_DUP复制了一份
signature
publicKey
pubHashA
pubKeyHash
OP_EQUALVERIFY
OP_CHECKSIG
栈顶的元素(publicKey)被OP_HASH160计算哈希,并把pubHashA压入栈中。
signature
publicKey
pubHashA
pubKeyHash
OP_EQUALVERIFY
OP_CHECKSIG
pubKeyHash添加到栈中。
signature
publicKey
OP_CHECKSIG 检查pubHashApubKeyHash是否相等,如果不相等,则中断程序运行。
True - 根据提供的publicKey来检查signature是否是有效的交易签名散列。

如果提供的公钥散列不是脚本中的公钥散列,或者提供的签名与提供的公钥不匹配,那么这个脚本就会执行失败。这是为了确保只有拥有pubkey脚本中地址的私钥的人才能够花费输出。

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

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

signature = get_transaction_signature(raw, private_key )

raw["sig_script_length"] = len(signature)
raw["sig_script"] = signature
del raw["hash_code_type"]

transaction = get_packed_transaction(raw)

发布交易

随着交易打包和签名的完成,下一步就是网络的事情了。通过使用本文之前在bitcoin_p2p_message_utils.py中定义的一些函数,下面的代码片段将消息头添加到待发送的数据上,并将其发送给对端节点。如前所述,首先需要发送一个版本消息,以便能够接受后续的消息。

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((get_bitcoin_peer(), 8333))
s.send(get_bitcoin_message("version", get_version_payload())
s.send(get_bitcoin_message("tx", transaction)

发送交易是最烦人的一部分。如果我发送了一个结构或签名错误的交易,则对端节点通常会删除连接,或者更好一点,回复一个包含错误信息的消息。类似于这样的错误消息(非常得烦人):“S值不需要这么高”,这是由于使用sigencode_der的ECSDA编码方法对交易散列进行签名导致的。尽管签名是有效的,但实际上比特币矿工并不喜欢以允许网络垃圾邮件形式格式化的ECSDA签名。这个问题的解决方案就是使用sigencode_der_canonize函数,该函数用于将签名格式化为其他的格式。这是一个简单但非常难调试的问题!

不管怎么样,我终于让程序运行起来了,看到我的交易进入了区块链,我非常得兴奋!当获知我的这个小巧简洁并且是纯手工打造的交易将永远成为比特币账户的一部分的时候,心中的成就感油然而生。

transaction success

当我提交交易的时候,我的交易费用相对于中位数来说相当得低(我通过比特币费用网站查到的),因此这花了矿工大约5个小时的时间来决定将其包含在一个区块中。我通过查看交易的确认次数来检查这一点,这是对交易所涉及的区块数量的度量。在写这篇文章的时候,有190个确认。这意味着在我的交易的区块之后,还有190个区块。这可以相当安全地得到确认,因为需要对网络进行猛烈的攻击才能重写190个块来删除我的交易。

总结

我希望你能通过阅读本文来对比特币的工作原理有所了解。虽然这里提供的大部分信息并不是很实用,并且你通常只会使用某个客户端来完成所有的操作,但是我认为更好地理解工作原理能够让你更好的了解客户端内部发生的事情,并让你对这项技术更有信心。

如果你想阅读更详细的代码,或者深入地研究这个示例,请查看我的Github代码库。在比特币世界里还有很多的探索空间,我只是提供了一个非常常见的比特币的例子。那里肯定还有更多更酷的功能,而不仅仅是在两个地址之间转移价值!我也没有研究挖掘比特币以及向区块链添加交易的过程。

如果你看到这里,你可能已经意识到,我转移到1QGNXLzGXhWTKF3HTSjuBMpQyUYFkWfgVC的380000的Satoshi(或0.0038比特币)能被任何人取走,因为本文中有该地址的私钥。我非常感兴趣地想知道多久之后这些比特币会被转移走,我希望大家能够采用我这里介绍的一些技巧来做到这一点。如果你刚刚将私钥加载到钱包应用程序中,那么我会鄙视你,但我不会阻止你!在撰写本文时,这些比特币价值约为10美元,但如果把比特币“拿到月球去”,谁知道它值多少呢!

如果你正在尝试比特币这个玩意并在寻找一个地址来发送比特币,或者如果你认为这篇文章有价值,能给你一些启发的话,那么我的地址18uKa5c9S84tkN1ktuG568CR23vmeU7F5H将很高兴能收到任意数量的捐款!或者,如果你想告诉我某些地方有错误,我也很乐意能听到。

更多的资源

如果你发现这篇文章很有趣,那么可以查看以下更多的资源:

  • 掌握比特币是一本解释比特币技术细节的书。我没有完整地阅读这本书,但它包含了很多很有用的信息。
  • Ken Sheriff的博客是一个很好的信息来源,拥有很多与本文相同主题的文章。很不幸,我在写这篇文章的时候才发现这个博客。如果在这篇文章中有你不明白的地方,那么阅读他的帖子将是一个很好的开始。
  • 安德斯·布朗沃思(Anders Brownworth)的梦幻般的blockchain visual 101视频是学习区块技术工作原理的绝佳资料。
  • 除非你是一个疼痛的受虐狂,否则我建议你不要从零开始,除非你为了学习而特意希望这么做。pycoin库是一个Python比特币库,会让你少一些头痛。
  • 为了减少自己的痛苦,可以玩玩Bitcoin testnet,而不是像我一样使用主网络。
  • 最后,再说一遍,本文的相关代码可以在我的Github代码库中找到。
评论