py-evm系列译文,有不当之处请联系改正,原文如下:https://py-evm.readthedocs.io/en/latest/guides/understanding_the_mining_process.html

py-evm系列译文之2-Guides翻译3:理解挖矿过程,主要是介绍了创建了一个非常非常低难度的链,然后模拟挖掘了一个空块以及一个包含0值交易的块的挖掘

注:挖矿挖掘在这篇文章里是一个意思

理解挖矿过程

Cookbook我们已经可以学习如何使用Chain该类创建单个区块链,作为不同块的不同虚拟机的组合。

在本指南中,我们希望建立在这些知识上并研究实际的挖掘过程。

!注意

挖矿是一个过载的术语,事实上,所提到的API的名称可能会发生变化。

挖矿

根据我们的观点,挖矿这个术语可以指不同的东西。大多数时候,当我们阅读有关挖矿的内容时,我们会认为这是一个多个方竞争成为第一个创建新的有效块并将其传递到网络的过程。

在本指南中,当我们讨论 mine_block()API时,我们只是指创建,验证和设置块作为链的新规范头部的部分,但它不一定是所提到的竞争为第一的一部分。实际上,当我们导入其他人创建的现有块时,内部也会调用mine_block()API。

挖掘空块

通常当我们考虑创建块时,我们自然会考虑首先向块添加事务,因为,毕竟,以太坊区块链的一个主要用例是处理以块为单位的 事务

但是为了简单起见,我们将挖掘一个空块作为第一个例子(意味着该块不包含任何事务)

作为回顾,下面演示我们如何创建一个链,正如cookbook 使用链对象 中所示

1
2
3
4
5
6
7
8
from eth.db.atomic import AtomicDB
from eth.chains.mainnet import MAINNET_GENESIS_HEADER

# 提高gas限制
genesis_header = MAINNET_GENESIS_HEADER.copy(gas_limit=3141592)

# 初始化一条新链
chain = chain_class.from_genesis_header(AtomicDB(), genesis_header)

由于我们决定不向我们的块添加任何事务,所以我们只需调用 mine_block()并查看会发生什么。

1
2
3
4
# 初始化一条新链
chain = chain_class.from_genesis_header(AtomicDB(), genesis_header)

chain.mine_block()

我们遇到了例外check_pow()。显然,我们正在尝试向不符合工作量证明(PoW)规则的链添加块。该错误准确地告诉我们mix_hash我们的块与预期值不匹配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Traceback (most recent call last):
File "scripts/benchmark/run.py", line 111, in <module>
run()
File "scripts/benchmark/run.py", line 52, in run
block = chain.mine_block() #**pow_args
File "/py-evm/eth/chains/base.py", line 545, in mine_block
self.validate_block(mined_block)
File "/py-evm/eth/chains/base.py", line 585, in validate_block
self.validate_seal(block.header)
File "/py-evm/eth/chains/base.py", line 622, in validate_seal
header.mix_hash, header.nonce, header.difficulty)
File "/py-evm/eth/consensus/pow.py", line 70, in check_pow
encode_hex(mining_output[b'mix digest']), encode_hex(mix_hash)))

eth.exceptions.ValidationError: mix hash mismatch;
0x7a76bbf0c8d0e683fafa2d7cab27f601e19f35e7ecad7e1abb064b6f8f08fe21 !=
0x0000000000000000000000000000000000000000000000000000000000000000

让我们来看看如何check_pow()实现。

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
def check_pow(block_number: int,
mining_hash: Hash32,
mix_hash: Hash32,
nonce: bytes,
difficulty: int) -> None:
validate_length(mix_hash, 32, title="Mix Hash")
validate_length(mining_hash, 32, title="Mining Hash")
validate_length(nonce, 8, title="POW Nonce")
cache = get_cache(block_number)
mining_output = hashimoto_light(
block_number, cache, mining_hash, big_endian_to_int(nonce))
if mining_output[b'mix digest'] != mix_hash:
raise ValidationError(
"mix hash mismatch; expected: {} != actual: {}. "
"Mix hash calculated from block #{}, mine hash {}, nonce {}, difficulty {}, "
"cache hash {}".format(
encode_hex(mining_output[b'mix digest']),
encode_hex(mix_hash),
block_number,
encode_hex(mining_hash),
encode_hex(nonce),
difficulty,
encode_hex(keccak(cache)),
)
)
result = big_endian_to_int(mining_output[b'result'])
validate_lte(result, 2**256 // difficulty, title="POW Difficulty")

只需查看该函数的签名,我们就可以看到验证PoW基于以下参数:

  • block_number - 给定块的编号
  • difficulty - PoW算法的难度
  • mining_hash - 挖掘头的哈希值
  • mix_hash - 与nonce表格一起提供实际证明
  • nonce - 与mix_hash表格一起提供实际证明

PoW算法检查所有这些参数是否正确匹配,确保只能将有效块添加到链中。

为了产生有效的块,我们要在头部设置正确的 mix_hashnonce 。当我们调用时 mine_block(),我们可以将这些作为键值对传递给该函数,如下所示。

1
chain.mine_block(nonce=valid_nonce, mix_hash=valid_mix_hash)

假设我们传递正确的noncemix_hash并且对应于被挖掘的块,此调用将正常工作。

检索有效的随机数并混合哈希

现在我们知道我们可以调用mine_block() 正确的参数来成功地向我们的链添加一个块,让我们简单地回顾一个示例,演示如何检索匹配 noncemix_hash

!注意

Py-EVM目前没有为实际的PoW挖掘提供稳定的API。以下代码仅用于演示目的。

在主链矿业是许多矿工同时进行竞争,因此 开采难度是相当高的,这意味着,在商用硬件上它需要很长的时间才能找到合适的 noncemix_hash。为了让我们能够在常规笔记本电脑上修改一些东西(也就是挖矿),我们将构建一个难度值(difficulty)设置为1的测试链。

让我们从定义GENESIS_PARAMS开始吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from eth import constants

GENESIS_PARAMS = {
'parent_hash': constants.GENESIS_PARENT_HASH,
'uncles_hash': constants.EMPTY_UNCLE_HASH,
'coinbase': constants.ZERO_ADDRESS,
'transaction_root': constants.BLANK_ROOT_HASH,
'receipt_root': constants.BLANK_ROOT_HASH,
'difficulty': 1,
'block_number': constants.GENESIS_BLOCK_NUMBER,
'gas_limit': 3141592,
'timestamp': 1514764800,
'extra_data': constants.GENESIS_EXTRA_DATA,
'nonce': constants.GENESIS_NONCE
}

接下来,我们将使用已定义的GENESIS_PARAMS和最新的 ByzantiumVM

1
2
3
4
5
6
7
8
9
10
11
from eth import MiningChain
from eth.vm.forks.byzantium import ByzantiumVM
from eth.db.backends.memory import AtomicDB


klass = MiningChain.configure(
__name__='TestChain',
vm_configuration=(
(constants.GENESIS_BLOCK_NUMBER, ByzantiumVM),
))
chain = klass.from_genesis(AtomicDB(), GENESIS_PARAMS)

现在我们已经有了可用的构建块,让我们把它们放在一起并挖掘一个合适的块!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from eth.consensus.pow import mine_pow_nonce


# 我们必须首先完成(finalize)区块以便可以阅读
# 对PoW算法来说重要的一些属性
block = chain.get_vm().finalize_block(chain.get_block())

# 基于mining_hash, 区块号 以及难度值 我们可以完成
# 实际的工作量证明(PoW) 机制来挖取正确的
# 区块的nonce和mix_hash
nonce, mix_hash = mine_pow_nonce(
block.number,
block.header.mining_hash,
block.header.difficulty)

block = chain.mine_block(mix_hash=mix_hash, nonce=nonce)
>>> print(block)
Block #1

我们花一点时间来完全理解这段代码的作用。

1.我们调用finalize_block()底层VM以检索我们计算noncemix_hash所需要的信息。

2.我们然后调用mine_pow_nonce()来检索我们需要挖掘块的正确的noncemix_hash,满足了验证。

3.最后我们调用mine_block()并传递noncemix_hash

!注意

上面的代码基本上会执行finalize_block两次。请记住,此代码仅用于演示目的,并且Py-EVM将在未来提供可插拔系统以允许PoW挖掘等。

挖掘包含事务的块

现在我们已经了解了挖掘过程的基本知识,让我们重新审视我们的示例,并在挖掘另一个块之前添加一个事务。为了实现这一目标,我们需要深入了解一些概念。

每笔交易都是从发件人Address到收件人 Address。每个事务都需要一些计算能力才能得以执行,该能力由单位gas测量。

在实践中,我们必须支付矿工将我们的交易放在一个区块中。然而, 没有任何技术原因表明我们需要为计算能力付费,而仅仅是经济原因,即实际上我们通常很难找到一个愿意包含不支付其计算成本的交易的矿工。

然而,在这个例子中,我们是矿工,这意味着我们可以自由地包括我们喜欢的任何交易。根据本指南的精神,让我们从简单开始,创建一个将以太网从一个地址发送到另一个地址的事务。请记住,即使传输的值为零,处理的成本仍然是计算成本,但由于我们是矿工,即使没有人愿意为此付费,我们也会挖掘它!

我们首先设置发送方和接收方。

1
2
3
4
5
6
7
8
9
10
11
from eth_keys import keys
from eth_utils import decode_hex
from eth_typing import Address

SENDER_PRIVATE_KEY = keys.PrivateKey(
decode_hex('0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8')
)

SENDER = Address(SENDER_PRIVATE_KEY.public_key.to_canonical_address())

RECEIVER = Address(b'\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x02')

在这里发现的一件事是我们只需要接收人的普通地址,而对于发送者我们正在获得从该地址派生的地址SENDER_PRIVATE_KEY。那是因为我们显然无法从我们没有私钥的地址用私钥签名交易并发送。

准备好发送方和接收方后,让我们创建实际的交易。

1
2
3
4
5
6
7
8
9
10
11
vm = chain.get_vm()
nonce = vm.state.get_nonce(SENDER)

tx = vm.create_unsigned_transaction(
nonce=nonce,
gas_price=0,
gas=100000,
to=RECEIVER,
value=0,
data=b'',
)

每个事务都需要nonce,不要与nonce我们之前作为PoW算法的一部分开采的事务相混淆。该交易的随机数(nonce)作为一个计数器,以确保从一个地址发出的所有交易可以按序执行。当前nonce 可以通过调用检索get_nonce(sender)()

一旦我们有了nonce我们就可以调用create_unsigned_transaction()并将nonce以及其余的事务属性作为键值对传递。

  • nonce - 发件人发送的交易数量
  • gas_price- Wei作为单位的天然气的支付数量
  • gas- 在拒绝之前允许消耗的最大交易gas
  • to - 交易收件人的地址
  • value- 要转移给收件人的Wei

在我们将交易加入到区块前,我们需要做的最后一步就是使用私钥参数用SENDER_PRIVATE_KEY调用as_signed_transaction()签名这个交易。

1
signed_tx = tx.as_signed_transaction(SENDER_PRIVATE_KEY)

最后,我们可以调用apply_transaction()和传递 signed_tx

1
chain.apply_transaction(signed_tx)

以下是完整的脚本,演示了如何使用挖掘包含一个简单的零值传输事务的单个块。

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
>>> from eth_keys import keys
>>> from eth_utils import decode_hex
>>> from eth_typing import Address
>>> from eth import constants
>>> from eth.chains.base import MiningChain
>>> from eth.consensus.pow import mine_pow_nonce
>>> from eth.vm.forks.byzantium import ByzantiumVM
>>> from eth.db.atomic import AtomicDB


>>> GENESIS_PARAMS = {
... 'parent_hash': constants.GENESIS_PARENT_HASH,
... 'uncles_hash': constants.EMPTY_UNCLE_HASH,
... 'coinbase': constants.ZERO_ADDRESS,
... 'transaction_root': constants.BLANK_ROOT_HASH,
... 'receipt_root': constants.BLANK_ROOT_HASH,
... 'difficulty': 1,
... 'block_number': constants.GENESIS_BLOCK_NUMBER,
... 'gas_limit': 3141592,
... 'timestamp': 1514764800,
... 'extra_data': constants.GENESIS_EXTRA_DATA,
... 'nonce': constants.GENESIS_NONCE
... }

>>> SENDER_PRIVATE_KEY = keys.PrivateKey(
... decode_hex('0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8')
... )

>>> SENDER = Address(SENDER_PRIVATE_KEY.public_key.to_canonical_address())

>>> RECEIVER = Address(b'\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x02')

>>> klass = MiningChain.configure(
... __name__='TestChain',
... vm_configuration=(
... (constants.GENESIS_BLOCK_NUMBER, ByzantiumVM),
... ))

>>> chain = klass.from_genesis(AtomicDB(), GENESIS_PARAMS)
>>> vm = chain.get_vm()

>>> nonce = vm.state.get_nonce(SENDER)

>>> tx = vm.create_unsigned_transaction(
... nonce=nonce,
... gas_price=0,
... gas=100000,
... to=RECEIVER,
... value=0,
... data=b'',
... )

>>> signed_tx = tx.as_signed_transaction(SENDER_PRIVATE_KEY)

>>> chain.apply_transaction(signed_tx)
(<ByzantiumBlock(#Block #1...)
>>> # 我们必须首先完成(finalize)区块以便可以阅读
>>> # 对PoW算法来说重要的一些属性
>>> block = chain.get_vm().finalize_block(chain.get_block())

>>> # 基于mining_hash, 区块号 以及难度值 我们可以完成
>>> # 实际的工作量证明(PoW) 机制来挖取正确的
>>> # 区块的nonce和mix_hash
>>> nonce, mix_hash = mine_pow_nonce(
... block.number,
... block.header.mining_hash,
... block.header.difficulty
... )

>>> chain.mine_block(mix_hash=mix_hash, nonce=nonce)
<ByzantiumBlock(#Block #1)>