以太坊区块的生成

从名称上来看,区块(Block)也是区块链系统中的核心概念,区块链简单来说就是将区块联结成链,区块中保存的是打包成的各种区块信息。在以太坊中,区块中保存的是各种交易信息。一个区块中可以包含若干个交易,也可以不包含任何交易。这篇文章主要会阐释以下问题:

  • 区块是什么?包含了哪些信息?
  • 区块是如何被打包的,写入到区块链的?
  • 一个区块的大小是多少?可以包含多少交易?
  • 多长时间可以产生一个区块?
  • 为什么有的区块中没有交易?

以太坊区块

以太坊的区块中保存了许多信息,最主要的有该区块的区块信息以及该区块中所包含的交易的信息。
我们可以在go-ethereum中的源码看到区块的结构体定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Block represents an entire block in the Ethereum blockchain.
type Block struct {
header *Header
uncles []*Header
transactions Transactions

// caches
hash atomic.Value
size atomic.Value

// Td is used by package core to store the total difficulty
// of the chain up to and including the block.
td *big.Int

// These fields are used by package eth to track
// inter-peer block relay.
ReceivedAt time.Time
ReceivedFrom interface{}
}

其中有下列主要属性:

  • header:存储的是该区块的信息(结构体为Header)
  • uncles:存储的是该区块所包含的叔块(uncle block)的信息,关于叔块的相关内容将会在之后的文章中进行讨论

其中Header结构体中是一个区块中所包含的信息,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Header represents a block header in the Ethereum blockchain.
type Header struct {
ParentHash common.Hash `json:"parentHash" gencodec:"required"`
UncleHash common.Hash `json:"sha3Uncles" gencodec:"required"`
Coinbase common.Address `json:"miner" gencodec:"required"`
Root common.Hash `json:"stateRoot" gencodec:"required"`
TxHash common.Hash `json:"transactionsRoot" gencodec:"required"`
ReceiptHash common.Hash `json:"receiptsRoot" gencodec:"required"`
Bloom Bloom `json:"logsBloom" gencodec:"required"`
Difficulty *big.Int `json:"difficulty" gencodec:"required"`
Number *big.Int `json:"number" gencodec:"required"`
GasLimit uint64 `json:"gasLimit" gencodec:"required"`
GasUsed uint64 `json:"gasUsed" gencodec:"required"`
Time *big.Int `json:"timestamp" gencodec:"required"`
Extra []byte `json:"extraData" gencodec:"required"`
MixDigest common.Hash `json:"mixHash" gencodec:"required"`
Nonce BlockNonce `json:"nonce" gencodec:"required"`
}

从代码中我们可以看到一个区块中包含了如下信息:

  • ParentHash:该区块的父区块的Hash值
  • UncleHash:该区块的叔区块的Hash值
  • Coinbase:打包该区块矿工的地址,矿工费和发现区块的奖励会被发送到该地址
  • Root:Merkle树根节点的Hash,以太坊中的交易状态信息是以Merkle状态树的形式进行存储的,Root是该状态树的根节点的Hash值
  • TxHash:保存该区块中交易Merkle树的根节点的Hash值
  • ReceiptHash:一个区块中所包含的交易中的接收者也是以Merkle树的形式进行存储的,该值是该Merkle树根节点的Hash值
  • Bloom:用于索引与搜索的结构(详见Tips)
  • Difficult:该区块的难度
  • Number:所有祖先区块的数量(也就是区块高度)
  • GasLimit:该区块的gas上限
  • GasUsed:该区块使用的gas
  • Time:区块开始打包的时间
  • Extra:区块相关的附加信息
  • MixDigest:该哈希值与Nonce值一起能够证明在该区块上已经进行了足够的计算(用于验证该区块挖矿成功与否的Hash值)
  • Nonce:该哈希值与MixDigest值一起能够证明在该区块上已经进行了足够的计算(用于验证该区块挖矿成功与否的Hash值)

Tips:
Bloom的作用
以太坊在设计的时候希望能够对Event能够进行快速的检索,并且在区块链上进行存储的成本是很高的,我们还希望不要有太多的重复数据(交易列表、交易产生的记录等)。Bloom就是用来解决这个问题的。在一个区块生成的时候,该区块中包含的所有的合约相关的地址以及所有交易产生的记录的索引都会被记录至Bloom中,以便之后的查找与索引。这些信息会被保存在Header结构中(Bloom字段),不保存在区块的数据段中,这样可以节省空间。当一个应用层的应用程序想要对某个合约中的数据进行检索时,它只需要在区块中的Header信息中进行查找,查看该区块中是否包含该合约相关的记录。

MixDigest和Nonce是如何进行验证的
我们知道以太坊目前采用的挖矿算法是PoW(Proof of Work),简单的来说,这种方法就像是猜数字,比如说在100000个数字中有5个可行解,猜到的就算是挖到了矿,这种算法要求猜到解的难度是可以调整的,一般难度会很大,不那么容易被猜到,但是验证这个解是否正确是很容易的。Nonce值其实就是矿工猜到的解,验证过程如下:经过预处理的Header与Nonce值做一个类似SHA3的运算产生一个128B的Mix(Mix0),这个值用于计算从DAG(以太坊用于挖矿算法的伪随机数据集,目前大小大约为2GB以上)中取出哪一页的数据(大小为128B),取出的DAG的页与Mix会进行一个以太坊特定的mixing方法,会生成下一个Mix(Mix1),这个过程会重复64次,直到得到Mix64。Mix64会被处理成为32B的数据,它被称为Mix Digest。Mix Digest会与一个叫做Target Threshold的值(相当于解集)进行比较,如果Mix Digest的值小于Threshold就认为挖矿成功,如果大于,表示失败。

区块的打包过程

交易在生成之后会被以太坊节点广播至网络,交易会被放到交易池(txPool)中,由矿工对交易进行验证然后放到正在打包的区块中,当选择好了区块中所要包含的交易之后,矿工就开始了挖矿过程(PoW),当矿工在挖矿竞争中取得胜利之后,该矿工的区块数据就可以被写入到区块链中。

交易池中有许多交易存在,矿工是如何从交易池中选择交易的呢?其实交易池会对各个交易进行排序,提供的矿工费(gas)高的交易会排在前面。因此,矿工会优先选择奖励高的交易打包至区块。这也就是为什么gas值高的交易会被处理的较快的原因。

区块的容量

以太坊的区块大小不同于比特币的区块大小,目前比特币的区块大小是1MB。因此,比特币一个区块中能够包含多少交易是取决于区块的大小以及每个交易的大小,一个区块中所有交易的总和不能超过区块的大小。但是,以太坊并没有固定的区块大小的限制,但是这样的话是如何确定一个区块中能够包含多少交易的呢?

以太坊的区块中有一个gasLimit,它表示的是该区块中所能包含的交易的gas值的上限。以太坊上的每一笔交易都会消耗gas值,一个区块中所包含的所有交易的gas总和不能超过区块的gasLimit。因此,通过这种方式我们就能够控制一个区块中的交易数量。

如果交易池中没有待处理的交易,那么矿工会直接进入挖矿过程,依旧会得到挖矿奖励(5 Ether),区块依旧会被打包和广播,只不过该区块中不会包含交易。

区块时间

在比特币中,大约10分钟会产生一个区块。根据以太坊白皮书所写,以太坊大约12秒回产生一个区块。这个时间就是区块时间。区块时间是和挖矿难度相关的,比特币的难度调整是有对应算法的,算法会把区块时间维持在10分钟左右。以太坊也有对应的难度调整算法。

我们可以在以太坊的源码中找到计算难度的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// CalcDifficulty is the difficulty adjustment algorithm. It returns
// the difficulty that a new block should have when created at time
// given the parent block's time and difficulty.
func CalcDifficulty(config *params.ChainConfig, time uint64, parent *types.Header) *big.Int {
next := new(big.Int).Add(parent.Number, big1)
switch {
case config.IsByzantium(next):
return calcDifficultyByzantium(time, parent)
case config.IsHomestead(next):
return calcDifficultyHomestead(time, parent)
default:
return calcDifficultyFrontier(time, parent)
}
}

我们可以看到有三个版本的代码,分别应用于不同版本的以太坊,我们先来看一下Homestead版本中的代码:
我们可以看到有三个版本的代码,分别应用于不同版本的以太坊,我们先来看一下Homestead版本中的代码:

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
func calcDifficultyHomestead(time uint64, parent *types.Header) *big.Int {
bigTime := new(big.Int).SetUint64(time)
bigParentTime := new(big.Int).Set(parent.Time)

x := new(big.Int)
y := new(big.Int)

x.Sub(bigTime, bigParentTime)
x.Div(x, big10)
x.Sub(big1, x)

if x.Cmp(bigMinus99) < 0 {
x.Set(bigMinus99)
}
y.Div(parent.Difficulty, params.DifficultyBoundDivisor)
x.Mul(y, x)
x.Add(parent.Difficulty, x)

if x.Cmp(params.MinimumDifficulty) < 0 {
x.Set(params.MinimumDifficulty)
}
periodCount := new(big.Int).Add(parent.Number, big1)
periodCount.Div(periodCount, expDiffPeriod)

if periodCount.Cmp(big1) > 0 {
y.Sub(periodCount, big2)
y.Exp(big2, y, nil)
x.Add(x, y)
}
return x
}

从代码中我们可以看到,以太坊的难度值是基于当前区块的出块时间,对之后的难度值进行调整的。

具体公式如下:

1
diff = (parent_diff +(parent_diff / 2048 * max(1 - (block_timestamp - parent_timestamp) // 10, -99))) + 2^(periodCount - 2)

其中2^(periodCount - 2)又称”难度炸弹“,计算公式如下:

1
2^(periodCount - 2) = 2**((block_number // expDiffPeriod) - 2)

目前最新的难度调整算法为Byzantium算法:

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
func calcDifficultyByzantium(time uint64, parent *types.Header) *big.Int {
bigTime := new(big.Int).SetUint64(time)
bigParentTime := new(big.Int).Set(parent.Time)

x := new(big.Int)
y := new(big.Int)

x.Sub(bigTime, bigParentTime)
x.Div(x, big9)
if parent.UncleHash == types.EmptyUncleHash {
x.Sub(big1, x)
} else {
x.Sub(big2, x)
}
if x.Cmp(bigMinus99) < 0 {
x.Set(bigMinus99)
}
y.Div(parent.Difficulty, params.DifficultyBoundDivisor)
x.Mul(y, x)
x.Add(parent.Difficulty, x)

if x.Cmp(params.MinimumDifficulty) < 0 {
x.Set(params.MinimumDifficulty)
}
fakeBlockNumber := new(big.Int)
if parent.Number.Cmp(big2999999) >= 0 {
fakeBlockNumber = fakeBlockNumber.Sub(parent.Number, big2999999) // Note, parent is 1 less than the actual block number
}
periodCount := fakeBlockNumber
periodCount.Div(periodCount, expDiffPeriod)

if periodCount.Cmp(big1) > 0 {
y.Sub(periodCount, big2)
y.Exp(big2, y, nil)
x.Add(x, y)
}
return x
}

算法如下:

1
diff = (parent_diff + (parent_diff / 2048 * max((2 if len(parent.uncles) else 1) - ((timestamp - parent.timestamp) // 9), -99))) + 2^(periodCount - 2)

以太坊中定义了一个难度的最小值MinimumDifficulty,定义于protocol_params.go源文件中,值为131072,这个值是以太坊中难度的最小值,也是创世块的难度值。


本文的版权归作者 罗远航 所有,采用 Attribution-NonCommercial 3.0 License。任何人可以进行转载、分享,但不可在未经允许的情况下用于商业用途;转载请注明出处。感谢配合!