窥探比特币核心如何运转

比特币真的很酷。当然,有人在想它是否是一种有用的技术,无论我们目前是否处于加密货币泡沫中,或者它目前面临的治理问题是否会得到解决……但在纯粹的技术层面上,神秘的Satoshi Nakamoto创造了令人印象深刻的技术。

不幸的是,虽然有很多资源可以对比特币的工作原理给出高级解释,我强烈推荐的一个这样的视频资源Anders Brownworth的blockchain visual 101,但是底层信息比较少。在我看来,如果你查看10000英尺的视图,那么你可以正确地理解这一点。

作为一个新手来说,我发现自己渴望了解比特币如何运作的机制。幸运的是,由于比特币本质上是去中心化的并且是对等的,所以任何人都能够开发符合协议的客户端。为了更好地了解比特币的运作方式,我决定编写自己的小玩具比特币客户端,该客户端能够向比特币区块链发布交易。

这篇文章介绍了创建一个最低限度可行的比特币客户端的过程,该客户端可以创建一个交易并将其提交给比特币点对点网络,以便它包含在区块链中。如果你只是阅读原始代码,请随时查看我的Github repo

地址生成

要成为比特币网络的一部分,必须有一个地址,你可以从中发送和接收资金。比特币使用公钥加密,并且地址基本上是从公钥私钥派生的公钥的哈希版本。令人惊讶的是,与大多数公钥加密不同,公钥在保存之前也会加密,直到资金从地址发送——但稍后会有更多的不同和惊讶。

快速了解术语:在比特币中,客户使用术语钱包wallet来表示地址集合。在协议级别没有钱包的概念,只有地址。

比特币使用椭圆曲线公钥加密技术作为其地址。在超高级别,椭圆曲线加密用于从私钥生成公钥,与RSA相同,但占用空间较小。如果你有兴趣学习一些关于它如何工作的数学知识,那么Cloudflare的入门书是一个很棒的资源。

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

比特币地址生成

在Python中,我使用ecsda库来完成椭圆曲线加密的繁重工作。下面的代码片段获取了一个公钥通过高度令人难忘的(并且非常不安全)私钥0xFEEDB0BDEADBEEF(前面填充了足够的零,使其成为64个十六进制字符长,或256位)。如果你想在地址中存储任何实际值,你需要一种更安全的生成私钥的方法!

作为一个有趣的测试,我最初使用私钥0xFACEBEEF创建了一个地址并发送了它0.0005BTC,1个月后,有人偷了我的0.0005BTC!我想人们必须偶尔会使用简单的公共私钥去搜索地址。你真的必须使用正确的密钥推导技术!

1
2
3
4
5
6
7
8
9
10
11
12
from ecdsa import SECP256k1, SigningKey

def get_private_key(hex_string):
return bytes.fromhex(hex_string.zfill(64)) # pad the hex string to the required 64 characters

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)

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

1
0000000000000000000000000000000000000000000000000feedb0bdeadbeef

和公钥(十六进制)

1
04d077e18fd45c031e0d256d75dfa8c3c21c589a861c4c33b99e64cf613113fcff9fc9d90a9d81346bcac64d3c01e6e0ef0828543edad73c0e257b845812cc8d28

预先设置公钥的0x04表示这是一个未压缩的公钥,这意味着ECDSA的xy坐标只是连接在一起。由于ECSDA的工作方式,如果你知道x值,y值只能取两个值,一个偶数,一个奇数。使用该信息,可以使用xy的极性来表达公钥。这将公钥大小从65位减少到33位,并且密钥(和随后的计算地址)被称为压缩。对于压缩公钥,前置值将为0x020x03,具体取决于y的极性。未压缩的公钥最常用于比特币,所以这也是我在这里使用的。

从这里开始,要从公钥生成比特币地址,公钥是sha256哈希,然后是cookedmd160哈希。这种双重哈希提供了额外的安全层,而ripemd160哈希提供了sha256的256位哈希的160位哈希,缩短了地址的长度。一个有趣的结果是,两个不同的公钥可以哈希到同一个地址!但是,对于2^160个不同的地址,这不太可能很快发生。

1
2
3
4
5
6
7
8
9
10
11
12
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编码。该编码仅是地址的表示(因此可以被解码/反转)。Base58Check生成1661HxZpSy5jhcJ2k6av2dxuspa8aafDac格式的地址。Base58Check编码提供了一个较短的地址来表达,并且还有一个内置的校验和,允许检测错误的地址。在几乎每个比特币客户端中,你的地址的Base58Check编码就是你将看到的地址。Base58Check还包含一个版本号,我在下面的代码中将其设置为0——这表示该地址是一个pubkey哈希。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 58 character alphabet used
BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'

def base58_encode(version, public_address):
"""
Gets a Base58Check string
See 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)

# count the leading 0s
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开始(前面填充零),我的比特币地址为1KK2xni6gmTtdnSGRiuAf94jciFgRjDj7W!

有了地址,现在可以获得一些比特币!为了让比特币进入我的地址,我从btcmarkets以澳元购买了0.0045BTC(在撰写本文时,大约11美元)。使用btcmarket的交易门户网站,我将其转移到上述地址,在此过程中损失了0.0005BTC到交易费用。你可以在交易95855ba9f46c6936d7b5ee6733c81e715ac92199938ce30ac3e1214b8c2cd8d7中的区块链上看到此交易。

连接到p2p网络

现在我有一个地址包含一些比特币,事情变得更有趣。如果我想将比特币发送到其他地方,则需要连接到比特币节点网络。

导言

我在第一次了解比特币时遇到的一个难点是,考虑到网络的分散性,网络中的同行如何找到其他同行?如果没有集中的权限,比特币客户端如何知道如何引导并开始与网络的其他部分进行通信?

事实证明,理想主义提出了实用性,并且在最初的同伴发现过程中有最少量的集中化。新客户端找到要连接的节点的主要方式是使用DNS查找到由比特币社区成员维护的任意数量的DNS种子DNS seed服务器。

事实证明,DNS非常适合引导客户端的目的,因为DNS协议运行在UDP上并且很轻,很难用于DDoS。IRC被用作以前的引导方法,但由于其对DDoS攻击的弱点而被终止。

种子被硬编码为比特币核心的源代码,但核心开发人员可能会对其进行更改。

下面的Python代码连接到DNS种子DNS seed并随意选择要连接的第一个节点。使用套接字库,代码基本上执行nslookup,并在针对种子节点seed.bitcoin.sipa.be运行查询时返回第一个结果的ipv4地址。

1
2
3
4
5
6
7
import socket

# use a dns request to a seed bitcoin DNS server to find a node
nodes = socket.getaddrinfo("seed.bitcoin.sipa.be", None)

# arbitrarily choose the first node
node = nodes[0][4][0]

运行之后,返回的地址是208.67.251.126,这是一个友好的节点,我可以连接到!

和新节点打个招呼

节点之间的比特币连接是通过TCP进行的。在连接到节点时,比特币协议的开始握手消息是版本消息。在节点交换版本消息之前,不会接受其他消息。

比特币协议消息在比特币开发人员参考中有详细记录。使用开发人员参考作为指南,可以在Python中构建版本消息,如下面的代码段所示。大多数数据都是相当无趣的,用于建立节点连接的管理数据。如果你对所附详细信息的详细信息感兴趣,请阅读开发人员参考。

1
2
3
4
5
6
7
8
9
10
11
12
13
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 library,版本有效负载数据被打包成正确的格式,特别注意数据的字节顺序和字节宽度。将数据打包成正确的格式很重要,否则接收节点将无法理解它接收的原始字节。

1
2
3
4
5
6
7
8
9
10
11
12
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)

同样,开发人员参考中提供了应该打包这些数据的方式。最后,在比特币网络上传输的每个有效载荷需要加上一个header,header包含有效载荷的长度,校验和以及有效载荷的消息类型。header还包含为所有主要比特币消息设置的magic常量0xF9BEB4D9。以下函数获取带有标头的比特币消息。

1
2
3
4
5
6
7
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

将数据打包成正确的格式,并附上header,它可以发送给我们的同行!

1
2
3
4
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确认消息。因为我正在构建一个小小的for fun的客户端,并且因为如果不这样做,同行不会对我不同,我将忽略他们的版本消息而不向他们发送确认。在我连接时发送版本消息足以让我以后发送更有意义的消息。

运行上面的代码段打印出以下内容。它当然看起来很有希望——SatoshiVerack在转储中看到的好词!如果我的版本消息格式错误,则节点根本不会响应。

1
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'

比特币交易

要进行比特币转账,必须向比特币网络广播交易。

关键的是,需要理解的最重要的想法是比特币地址的余额仅由地址可以花费的”未花费的交易输出”(Unspent Transaction Outputs 简称UTXO)的数量组成。当Bob向Alice发送比特币时,他真的只是在创建一个UTXO,Alice(以及只有Alice)以后可以用来创建另一个UTXO并发送比特币。因此,比特币地址的余额由它能够转移到另一个地址的比特币数量来定义,而不是它所拥有的比特币数量。

要强调的是,当有人说他们有X比特币时,他们真的说他们可以花费的所有UTXO总和为X比特币的价值。区别是微妙但重要的,比特币地址的平衡不是直接记录在任何地方,而是可以通过对它可以花费的UTXO求和来找到。当我意识到这绝对是“哦,这就是它的运作方式!”时刻。

这样做的副作用是交易输出可以是未花费(UTXO)或完全花费。不可能只花掉某人已经花费给你的一半的输出,然后在以后花掉其余的时间。如果你确实想要花掉你收到的输出的一小部分,你可以发送你要发送的分数,同时将其余部分发回给自己。简化版本如下图所示。

比特币交易

创建交易输出时,将使用锁定条件创建交易输出,以便将来某人通过所谓的交易脚本来使用它。最常见的是,这种锁定条件是:“要花费此输出,你需要证明你拥有与特定公共地址相对应的私钥”。这称为Pay-to-Public-Key-Hash脚本。但是,通过比特币脚本,其他类型的条件也是可能的。例如,可以创建可以由任何可以解决某个哈希的任何人花费的输出,或者可以创建任何人都可以花费的交易。

通过Script,可以创建简单的基于合同的交易。脚本是一种基于堆栈的基本语言,其中的操作数量集中在检查哈希值的相等性和验证签名。脚本不是图灵完整的,也没有能力进行任何循环。以此为基础的竞争加密货币以太坊能够拥有智能合约,它具有图灵完整语言。关于在加密货币中加入图灵完整语言的效用,必要性和安全性存在很多争议,但我会将这种争论留给其他人!

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

对于我的这个用来玩儿的客户端,我希望能够将先前从交易所转移的比特币发送到我的FEEDB0BDEADBEEF地址。使用与以前相同的过程,我使用私钥(在填充之前)生成另一个地址BADCAFEFABC0FFEE。这产生了地址1QGNXLzGXhWTKF3HTSjuBMQQyUYFkWfgVC

创建原生交易

创建交易是首先打包“原生交易”然后签署原生交易。同样,开发人员参考文献描述了进入交易的内容。构成交易的内容如下所示,但首先是几个注释:

  • 常见的比特币说法使用术语签名脚本和pubkey脚本,我觉得有点混乱。签名脚本用于满足我们想要在交易中使用的UTXO的条件,pubkey脚本用于设置花费我们正在创建的UTXO所需的条件。签名脚本的更好名称可能是解锁脚本,而pubkey脚本的更好名称可能是锁定脚本。
  • 比特币交易价值在Satoshis中指定。Satoshi代表比特币中最小的可分割部分,代表比特币的百万分之一。

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

暂时忽略签名脚本和pubkey脚本,很容易看到原生交易的其他字段应该包含哪些内容。要将我的FEEDB0BDEADBEEF地址中的资金发送到我的BADCAFEFABC0FFEE地址,我会查看交易所创建的交易。这给了我:

  • 交易ID为95855ba9f46c6936d7b5ee6733c81e715ac92199938ce30ac3e1214b8c2cd8d7
  • 发送到我的地址的输出是第二个输出,输出1(输出数字为0索引)。
  • 输出数量为1,因为我想将FEEDB0BDEADBEEF中的所有内容发送到BADCAFEFABC0FFEE
  • 价值最多可达400,000Satoshis。允许一些费用必须低于此值。我将允许20,000 Satoshi作为费用,因此将设定值为380,000。
  • 锁定时间将设置为0,这允许在任何时间或块中包含交易。

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

要解锁已被p2pk脚本锁定的交易,用户将提供其公钥和原生交易的哈希签名。对公钥进行哈希处理,并与创建脚本的地址进行比较,并对提供的公钥进行签名验证。如果公钥的哈希值和地址相等,并且验证了签名,则可以使用输出。

在比特币脚本操作数中,p2pk脚本如下所示。

1
2
3
4
5
6
OP_DUP
OP_HASH160
<Length of address in bytes>
<Bitcoin address>
OP_EQUALVERIFY
OP_CHECKSIG

将操作数转换为它们的值(可以在wiki上找到)并输入公共地址(在Base58Check编码之前)以十六进制形式给出以下脚本:

1
2
3
4
5
6
0x76
0xA9
0x14
0xFF33195EC053D6E58D5FD3CC67747D3E1C71B280
0x88
0xAC

对于我们发送给的私钥0xBADCAFEFABC0FFEE,使用前面显示的代码从私钥导出地址找到该地址。

签署交易

(p2pk)交易中签名脚本有两个独立但有些相关的用法:

  • 该脚本通过提供哈希到已发送UTXO的地址的哈希来验证(解锁)我们正在尝试花费的UTXO。
  • 该脚本还对我们提交给网络的交易进行签名,这样任何人都无法在不使签名失效的情况下修改交易。

但是,原生交易包含签名脚本,该脚本应包含原生交易的签名!在签署原生交易之前,通过将我们正在使用的UTXO的Pubkey脚本放在签名脚本槽中来解决这个鸡和蛋的问题。据我所知,使用Pubkey作为占位符似乎没有任何合理的理由,它实际上可能是任意数据。

在对原生交易进行哈希处理之前,还需要附加Hashtype value。最常见的hashtype值是SIGHASH_ALL,它对整个结构进行签名,以便不能修改输入或输出。链接的Wiki页面列出了其他哈希类型,这些哈希类型可以允许在签名交易之后修改输入和输出的组合。

以下函数汇总了原生交易值的python字典。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
def get_p2pkh_script(pub_key):
"""
This is the standard 'pay to pubkey hash' script
"""
# OP_DUP then OP_HASH160 then 20 bytes (pub address length)
script = bytes.fromhex("76a914")

# The address to pay to
script += pub_key

# OP_EQUALVERIFY then OP_CHECKSIG
script += bytes.fromhex("88ac")

return script

def get_raw_transaction(from_addr, to_addr, transaction_hash, output_index, satoshis_spend):
"""
Gets a raw transaction for a one input to one output transaction
"""
transaction = {}
transaction["version"] = 1
transaction["num_inputs"] = 1

# transaction byte order should be reversed:
# https://bitcoin.org/en/developer-reference#hash-byte-order
transaction["transaction_hash"] = bytes.fromhex(transaction_hash)[::-1]
transaction["output_index"] = output_index

# temporarily make the signature script the old pubkey script
# this will later be replaced. I'm assuming here that the previous
# pubkey script was a p2pkh script here
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

使用以下值调用代码会创建我有兴趣制作的原生交易。

1
2
3
4
5
6
7
8
9
10
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函数中实现的,我不会在这里复制,因为它实际上只是更多的struct打包代码。如果你有兴趣,可以在我的Github仓库中的bitcoin_transaction_utils.pyPython文件中找到它。

这允许我定义一个将产生签名脚本的函数。生成签名脚本后,它应替换占位符签名脚本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def get_transaction_signature(transaction, private_key):
"""
Gets the sigscript of a raw transaction
private_key should be in bytes form
"""
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脚本的输入提供的,这样我就可以证明我可以将我现在用作输入的输出用作输入。下面显示了这种工作原理的机制,它来自比特币维基。从上到下,每行都是脚本的一次迭代。这是一个a pay to pubkey hash pubkey,如前所述,这是最常见的脚本。它也是我正在创建的交易和我正在兑换的交易使用的脚本。

如果提供的公钥未哈希到脚本中的公钥哈希,或者提供的签名与提供的公钥不匹配,则此脚本将失败。这确保只有持有pubkey脚本中的地址私钥的人才能够使用输出。

你可以看到这是我第一次需要在任何地方提供我的公钥。到目前为止,只有我的公共地址已经发布。这里必须提供公钥,因为它允许验证交易已签名的签名。

使用get_transaction_signature函数,我们现在可以签署并打包我们的交易准备好传输!这涉及使用真实签名脚本替换占位符签名脚本并从交易中删除hash_code_type,如下所示。

1
2
3
4
5
6
7
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中定义的一些函数,下面的代码将比特币消息头放在传输上并将其传输给节点。如前所述,首先需要向节点发送版本消息,以便它接受后续消息。

1
2
3
4
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值不必要地高S value is unnecessarily high,这是由使用sigencode_der的ECSDA编码方法对交易哈希进行签名引起的。尽管签名有效,但显然比特币矿工不喜欢以允许网络中的垃圾邮件格式化的ECSDA签名。解决方案是使用sigencode_der_canonize函数,该函数负责格式化其他格式的签名。一个简单但非常难以调试的问题!

无论如何,当我看到我的交易进入区块链时,我最终得到了它并且非常兴奋!知道我的小巧,手工制作的交易现在将永远成为比特币分类账的一部分,这是一种很棒的成就感。

当我提交交易时,我的交易费实际上与中位数相比非常低(我使用比特币费用网站]()进行检查),因此矿工大约花了5个小时决定将其包含在一个区块内。我通过查看交易确认的数量来检查这一点—— 这是对交易块的深度块数的衡量。在撰写本文时,这是190个确认…意味着在块之后我的交易在,还有另外190个区块。这可以非常安全地被认为已经确认,因为它会在网络上进行一次令人印象深刻的攻击,重写190个块来删除我的交易。

结论

我希望你通过阅读这篇文章对比特币的工作方式有所了解,我知道在我花了几个月把这一切放在一起的时候我确实做到了!虽然这里提供的大部分信息都不太适用——你通常只是使用一个能为你完成所有工作的客户——我认为对事情的运作方式有了更深入的了解,可以让你更好地了解幕后发生的事情。并且让你更加自信地使用该技术。

如果你想仔细阅读代码,或进一步了解玩具示例,请查看我的相关Github repo。在比特币世界中有很多可以进一步探索的空间,我只是真正关注比特币的一个非常常见的用例。除了在两个地址之间传递价值之外,还有空间可以做更酷的功绩!我也没有触及如何挖掘,向区块链添加交易的过程,工作..这是另一个兔子洞一起。

如果你已经读过这篇文章,你可能已经意识到我转移到1QGNXLzGXhWTKF3HTSjuBMpQyUYFkWfgVC的380000Satoshi(或0.0038BTC)可以有足够的智慧,任何人都可以使用..因为本文中存在该地址的私钥。我很想知道需要多长时间才能被转移,并希望无论是谁拿走它都可以使用我在这里详述的一些技术来实现这一目标!如果你只是将私钥加载到钱包应用程序中,它会非常蹩脚,但我想我无法阻止你!在撰写本文时,这笔金额价值约10美元,但如果比特币“登月”,谁知道它可能值多少钱! (编辑:它现在已经消失了!在发表这篇文章几个小时之后,由1KgoPFVDNcx7H2VY9bB2dxxP9yNM2Nar1N拿走了。好好玩!)

如果你正在寻找一个地址来发送比特币,当你正在玩这些东西,或者如果你认为这篇文章有价值足以保证提示——我的地址18uKa5c9S84tkN1ktuG568CR23vmeU7F5H很乐意接受任何小额捐款!或者,如果你想对我说错误,我很乐意听到它。

更多资源

如果你发现这篇文章很有趣,可以查看一些其他资源:

  • 掌握比特币这本书解释了比特币的技术细节。我没有完全阅读过这篇文章,但是在略读时看起来它是一个很好的信息。
  • Ken Sheriff的博客文章是一个很好的信息来源,涵盖了很多与本文相同的主题,但遗憾的是,当我写完这篇文章时,我才发现这一点。如果你对本文中的任何内容一无所知,那么阅读Ken的优秀帖子将是一个很好的起点。
  • 前面提到过,但Anders Brownworth的 blockchain visual 101 video是区块链技术如何运作的绝佳顶级视图。
  • 除非你是痛苦的受虐狂,否则我也建议你不要从头开始做任何事情,除非你有兴趣为学习目的这样做。pycoin库是一个Python比特币库,可以为你节省一些麻烦。
  • 为了节省自己的痛苦,建议使用比特币测试网络,而不是像我一样使用主网。也就是说,当代码错误的风险正在失去真正的金钱时,它会更有趣!
  • 最后,可能值得重复的是,本文附带的代码可以在我的Github repo中找到。

======================================================================

分享一些比特币、以太坊、EOS等区块链相关的交互式在线编程实战教程:

  • java比特币开发教程,本课程面向初学者,内容即涵盖比特币的核心概念,例如区块链存储、去中心化共识机制、密钥与脚本、交易与UTXO等,同时也详细讲解如何在Java代码中集成比特币支持功能,例如创建地址、管理钱包、构造裸交易等,是Java工程师不可多得的比特币开发学习课程。
  • php比特币开发教程,本课程面向初学者,内容即涵盖比特币的核心概念,例如区块链存储、去中心化共识机制、密钥与脚本、交易与UTXO等,同时也详细讲解如何在Php代码中集成比特币支持功能,例如创建地址、管理钱包、构造裸交易等,是Php工程师不可多得的比特币开发学习课程。
  • EOS教程,本课程帮助你快速入门EOS区块链去中心化应用的开发,内容涵盖EOS工具链、账户与钱包、发行代币、智能合约开发与部署、使用代码与智能合约交互等核心知识点,最后综合运用各知识点完成一个便签DApp的开发。
  • java以太坊开发教程,主要是针对java和android程序员进行区块链以太坊开发的web3j详解。
  • python以太坊,主要是针对python工程师使用web3.py进行区块链以太坊开发的详解。
  • php以太坊,主要是介绍使用php进行智能合约开发交互,进行账号创建、交易、转账、代币开发以及过滤器和交易等内容。
  • 以太坊入门教程,主要介绍智能合约与dapp应用开发,适合入门。
  • 以太坊开发进阶教程,主要是介绍使用node.js、mongodb、区块链、ipfs实现去中心化电商DApp实战,适合进阶。
  • ERC721以太坊通证实战,课程以一个数字艺术品创作与分享DApp的实战开发为主线,深入讲解以太坊非同质化通证的概念、标准与开发方案。内容包含ERC-721标准的自主实现,讲解OpenZeppelin合约代码库二次开发,实战项目采用Truffle,IPFS,实现了通证以及去中心化的通证交易所。
  • C#以太坊,主要讲解如何使用C#开发基于.Net的以太坊应用,包括账户管理、状态与交易、智能合约开发与交互、过滤器和交易等。
  • tendermint区块链开发详解,本课程适合希望使用tendermint进行区块链开发的工程师,课程内容即包括tendermint应用开发模型中的核心概念,例如ABCI接口、默克尔树、多版本状态库等,也包括代币发行等丰富的实操代码,是go语言工程师快速入门区块链开发的最佳选择。

汇智网原创翻译,转载请标明出处。这里是原文窥探比特币核心如何运转