0%

《7天以太坊源码解读》 — 4、挖矿、共识算法介绍

>>> 挖矿介绍

前面文章介绍到,节点启动时,如果启用了挖矿,就会开启挖矿。

挖矿启动入口位于 miner/miner.go 69行

1
2
3
4
5
6
7
8
9
10
11
12
13
func New(eth Backend, config *Config, chainConfig *params.ChainConfig, mux *event.TypeMux, engine consensus.Engine, isLocalBlock func(block *types.Block) bool) *Miner {
miner := &Miner{
eth: eth,
mux: mux,
engine: engine,
exitCh: make(chan struct{}),
worker: newWorker(config, chainConfig, engine, eth, mux, isLocalBlock, true),
canStart: 1,
}
go miner.update()

return miner
}
  1. 新建一个miner
  2. 新建一个worker
    1. worker订阅交易池中出现新交易的事件
    2. worker订阅出现新块的事件
    3. go worker.mainLoop() 设置各种通道的处理(来了新work就开始构建新块并提交计算任务、来了新交易就执行这些交易)
    4. go worker.newWorkLoop(recommit) 设置各种通道的处理。其中使用了一个定时器,最少每隔1s进行一次work的提交,但目前定时器是关着的状态,后面才会开启。
    5. go worker.resultLoop() 设置接收到挖矿结果的处理。将区块写入链(会做计算量等等检查,遇到分叉会按照规则进行分叉),且更新state数据库,并且发出矿工出新块事件。
    6. go worker.taskLoop() 设置收到计算任务的处理。收到计算任务后就调用 sealHash := w.engine.SealHash(task.block.Header()) 开始计算
  3. 跟踪下载器(就是同步器)的相关事件。如果同步刚开始而挖矿正在工作的话,就停止挖矿。如果同步完成就调用Start函数开始挖矿

下面看 Start 函数

1
2
3
4
5
6
7
8
9
10
func (miner *Miner) Start(coinbase common.Address) {
atomic.StoreInt32(&miner.shouldStart, 1)
miner.SetEtherbase(coinbase)

if atomic.LoadInt32(&miner.canStart) == 0 {
log.Info("Network syncing, will start miner afterwards")
return
}
miner.worker.start()
}
  1. 首先设置了coinbase地址(接收收益的地址)
  2. 判断是否可以开始挖矿了,不可以的话直接返回
  3. 开始挖矿工作(触发上面提到的定时器)

>>> 共识算法介绍

看 consensus 文件夹,以太坊具有多种共识算法,ethash、clique

>>>> ethash 共识算法

ethash 共识算法与pow算法类似,都是通过计算一个满足要求的nonce值来进行挖矿,不同的是 ethash 通过 Dagger Hashimoto算法 做到了抵制矿机的功效。

看 consensus/ethash/ethash.go 文件 51 行,这里就是开始挖矿计算

大致就是开启多个线程进行挖矿计算工作。计算细节在 99行 ethash.mine(block, id, nonce, abort, locals)

进到这个方法

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
func (ethash *Ethash) mine(block *types.Block, id int, seed uint64, abort chan struct{}, found chan *types.Block) {
// Extract some data from the header
var (
header = block.Header()
hash = ethash.SealHash(header).Bytes()
target = new(big.Int).Div(two256, header.Difficulty)
number = header.Number.Uint64()
dataset = ethash.dataset(number, false)
)
// Start generating random nonces until we abort or find a good one
var (
attempts = int64(0)
nonce = seed
)
logger := ethash.config.Log.New("miner", id)
logger.Trace("Started ethash search for new nonces", "seed", seed)
search:
for {
select {
case <-abort:
// Mining terminated, update stats and abort
logger.Trace("Ethash nonce search aborted", "attempts", nonce-seed)
ethash.hashrate.Mark(attempts)
break search

default:
// We don't have to update hash rate on every nonce, so update after after 2^X nonces
attempts++
if (attempts % (1 << 15)) == 0 {
ethash.hashrate.Mark(attempts)
attempts = 0
}
// Compute the PoW value of this nonce
digest, result := hashimotoFull(dataset.dataset, hash, nonce)
if new(big.Int).SetBytes(result).Cmp(target) <= 0 {
// Correct nonce found, create a new header with it
header = types.CopyHeader(header)
header.Nonce = types.EncodeNonce(nonce)
header.MixDigest = common.BytesToHash(digest)

// Seal and return a block (if still needed)
select {
case found <- block.WithSeal(header):
logger.Trace("Ethash nonce found and reported", "attempts", nonce-seed, "nonce", nonce)
case <-abort:
logger.Trace("Ethash nonce found but discarded", "attempts", nonce-seed, "nonce", nonce)
}
break search
}
nonce++
}
}
// Datasets are unmapped in a finalizer. Ensure that the dataset stays live
// during sealing so it's not unmapped while being read.
runtime.KeepAlive(dataset)
}

nonce初始为seed,seed 是尝试计算开始的位置,是前面随机出来的一个值,每次计算都会nonce+1,直到找到满足要求的nonce。

target就是目标值。nonce满足的条件就是 计算出来的result < target。

result是通过 hashimotoFull(dataset.dataset, hash, nonce) 算出来的

下面看 hashimotoFull 函数(传递的参数分别是 这个块高度对应的DAG数据集、头部hash、nonce)

1
2
3
4
5
6
7
func hashimotoFull(dataset []uint32, hash []byte, nonce uint64) ([]byte, []byte) {
lookup := func(index uint32) []uint32 {
offset := index * hashWords
return dataset[offset : offset+hashWords]
}
return hashimoto(hash, nonce, uint64(len(dataset))*4, lookup)
}

调用了 hashimoto 函数,接着看

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
func hashimoto(hash []byte, nonce uint64, size uint64, lookup func(index uint32) []uint32) ([]byte, []byte) {
// Calculate the number of theoretical rows (we use one buffer nonetheless)
rows := uint32(size / mixBytes) // 计算总行数

// Combine header+nonce into a 64 byte seed
// 将头hash和nonce放到一个字节数组中
seed := make([]byte, 40)
copy(seed, hash)
binary.LittleEndian.PutUint64(seed[32:], nonce)

seed = crypto.Keccak512(seed) // 进行 Keccak512 加密
seedHead := binary.LittleEndian.Uint32(seed) // 取出前面的uint32数值

// Start the mix with replicated seed
mix := make([]uint32, mixBytes/4) // 开辟32个uint32的数组
for i := 0; i < len(mix); i++ {
mix[i] = binary.LittleEndian.Uint32(seed[i%16*4:]) // 填充数组
}
// Mix in random dataset nodes
temp := make([]uint32, len(mix))

for i := 0; i < loopAccesses; i++ { // 循环64次
parent := fnv(uint32(i)^seedHead, mix[i%len(mix)]) % rows
for j := uint32(0); j < mixBytes/hashBytes; j++ {
copy(temp[j*hashWords:], lookup(2*parent+j)) // 从DAG数据集中找出数据来进行mix
}
fnvHash(mix, temp) // 使用temp来mix
}
// Compress mix
for i := 0; i < len(mix); i += 4 {
mix[i/4] = fnv(fnv(fnv(mix[i], mix[i+1]), mix[i+2]), mix[i+3])
}
mix = mix[:len(mix)/4]

digest := make([]byte, common.HashLength)
for i, val := range mix {
binary.LittleEndian.PutUint32(digest[i*4:], val)
}
return digest, crypto.Keccak256(append(seed, digest...))
}

这个算法比较复杂,大家可以下面自己细细研究

>>>> clique 共识算法

clique 实现的是PoA(权威证明,Proof of Authority)共识(出块权掌握在部分节点手里,普通节点是无法参与的,无论你有多少算力、多少权益。可见PoA共识牺牲了一部去中心化的特性,换来了一种可控性)。

当前以太坊使用的还是 ethash 共识,clique共识只是用在了测试网络上。

入口位于 consensus/clique/clique.go 584 行

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
72
73
74
func (c *Clique) Seal(chain consensus.ChainReader, block *types.Block, results chan<- *types.Block, stop <-chan struct{}) error {
header := block.Header()

// Sealing the genesis block is not supported
number := header.Number.Uint64()
if number == 0 {
return errUnknownBlock
}
// For 0-period chains, refuse to seal empty blocks (no reward but would spin sealing)
if c.config.Period == 0 && len(block.Transactions()) == 0 {
log.Info("Sealing paused, waiting for transactions")
return nil
}
// Don't hold the signer fields for the entire sealing procedure
c.lock.RLock()
signer, signFn := c.signer, c.signFn
c.lock.RUnlock()

// Bail out if we're unauthorized to sign a block
snap, err := c.snapshot(chain, number-1, header.ParentHash, nil)
if err != nil {
return err
}
// 检查签名者是不是被授权
if _, authorized := snap.Signers[signer]; !authorized {
return errUnauthorizedSigner
}
// If we're amongst the recent signers, wait for the next block
// 如果自己在最近已经出过块了,就不出块了
for seen, recent := range snap.Recents {
if recent == signer {
// Signer is among recents, only wait if the current block doesn't shift it out
if limit := uint64(len(snap.Signers)/2 + 1); number < limit || seen > number-limit {
log.Info("Signed recently, must wait for others")
return nil
}
}
}
// Sweet, the protocol permits us to sign the block, wait for our time
delay := time.Unix(int64(header.Time), 0).Sub(time.Now()) // nolint: gosimple
// 判断如果不是轮到我出块,则多延迟一段时间,这样的话可以让其他签名者优先出块
// header.Difficulty是在构建block的时候填进去的,算法大致是当前区块高度对签名者数量求余得到这个块应该由谁来签名,如果不是我,则填充为diffNoTurn
if header.Difficulty.Cmp(diffNoTurn) == 0 {
// It's not our turn explicitly to sign, delay it a bit
wiggle := time.Duration(len(snap.Signers)/2+1) * wiggleTime
delay += time.Duration(rand.Int63n(int64(wiggle)))

log.Trace("Out-of-turn signing requested", "wiggle", common.PrettyDuration(wiggle))
}
// Sign all the things!
// 进行签名
sighash, err := signFn(accounts.Account{Address: signer}, accounts.MimetypeClique, CliqueRLP(header))
if err != nil {
return err
}
copy(header.Extra[len(header.Extra)-extraSeal:], sighash)
// Wait until sealing is terminated or delay timeout.
log.Trace("Waiting for slot to sign and propagate", "delay", common.PrettyDuration(delay))
go func() {
select {
case <-stop:
return
case <-time.After(delay): // 延时到达,跳出select
}

select {
case results <- block.WithSeal(header): // 提交成果
default: // 成果没人读取的话就走这里
log.Warn("Sealing result is not read by miner", "sealhash", SealHash(header))
}
}()

return nil
}

文章仅供参考,若有错误,还望不吝指正 !!!




微信关注我,及时接收最新技术文章