主页 > 老版本imtoken > 附注 34 - 挖矿奖励
附注 34 - 挖矿奖励
矿工的收入来自挖矿奖励。 只有这样,才能鼓励矿工积极参与挖矿,维护网络安全。 那么,以太坊是如何奖励矿工的呢?
本文通过提出两个问题帮助您理解该机制:
奖励是如何计算的; 何时何地颁发奖励; 1. 奖励如何计算
奖励分为三部分:
新区块奖励 叔块奖励 矿工费
{总奖励} = 新区块奖励 + 叔块奖励 + 矿工费
1.1 新区块奖励
是矿工消耗电力和完成工作量证明的奖励。 本次奖励调整了两次:
起初,每个区块奖励 5 ETH; 2017年10月16日(区块高度4370000)实施拜占庭硬分叉,奖励减为3 ETH; 2019 年 2 月 28 日(区块高度 Block height 7280000)执行君士坦丁堡硬分叉,再次将奖励减少到 2 ETH。
以太坊在2015年7月正式发布以太坊主网后,其团队规划了发展阶段,分为“边疆”、“家园”、“大都市”和“宁静”四个阶段。 拜占庭和君士坦丁堡是大都市的两个阶段。
新区块奖励是矿工的主要收入来源,新区块奖励低至 2 Ether。 对矿机厂商和矿工,乃至以太坊的挖矿生态都会产生比较大的影响和调整。 由于挖矿收益减少,机会成本增加,以太坊挖矿的成本效益将低于其他币种,这可能会降低矿工的积极性。 这也是一种助燃剂,迫使以太坊升级到以太坊2.0,迫使以太坊更新。
1.2 叔块奖励
以太坊的平均区块间隔为 12 秒。 区块链软分叉是一种普遍现象。 如果和比特币一样处理,只有最长链上的区块才会有区块奖励。 对于最终没有进入最长链的矿工来说是非常不公平的,这种“不公平”将是一种普遍的情况。 这会影响矿工挖矿的积极性,甚至可能削弱以太坊网络的系统安全性,也是一种算力的浪费。 因此,以太坊系统为不在最长链上的叔块设置了“叔块奖励”。
叔块奖励也分为两部分:
奖励叔块的创造者; 奖励收集叔块的矿工;
叔块创建者的奖励根据“远近”关系不同。 离当前区块越远,奖励越少。
{叔块挖矿奖励} = {8-(当前区块高度-叔块高度)} {8} * {当前区块挖矿奖励}
叔块
奖
按挖矿奖励2 ETH计算
第一代
7/8
1.75 以太币
第二代
6/8
1.5 以太币
第三代
5/8
1.25 以太币
第四代
4/8
1 以太币
第五代
3/8
0.75 以太币
第六代
2/8
0.5 以太币
第七代
1/8
0.25 以太币
注意:叔块中产生的交易手续费不会返还给创建者。 毕竟叔块里面的交易是不能统计的。
包含叔块的矿工每包含一个叔块,将获得多 1/32 的块挖矿奖励。
收集叔块的奖励 = 数量 \times \frac{新区块奖励}{32}
1.3 矿工费
矿工处理交易,校验和包含在块中。 这时,交易签名者需要向矿工支付矿工费。 每笔交易收取的矿工费取决于交易消耗的gas量,等于用户设置的GasPrice乘以交易消耗的gas。
费用 = \text{tx.gasPrice} \times \text{tx.gasUsed}
2. 何时何地奖励
奖励是当一个区块被挖出并打包时,奖励的分配已经在其中完成,相当于实时结算。
矿工费的分配,在处理一笔交易时比特币挖矿怎样算收益,根据交易消耗的Gas直接存入矿工账户; 区块奖励和叔块奖励在所有交易处理完毕后实时计算。
3.代码展示
3.1 实时结算交易矿工费
//core/state_transition.go
func (st *StateTransition) TransitionDb() (*ExecutionResult, error) {
//...
var (
ret []byte
vmerr error // vm errors do not effect consensus and are therefore not assigned to err
)
if contractCreation {
ret, _, st.gas, vmerr = st.evm.Create(sender, st.data, st.gas, st.value)
} else {
// Increment the nonce for the next transaction
st.state.SetNonce(msg.From(), st.state.GetNonce(sender.Address())+1)
ret, st.gas, vmerr = st.evm.Call(sender, st.to(), st.data, st.gas, st.value)
}
st.refundGas()
st.state.AddBalance(st.evm.Coinbase, new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), st.gasPrice))
return &ExecutionResult{
UsedGas: st.gasUsed(),
Err: vmerr,
ReturnData: ret,
}, nil
}
3.2 挖矿奖励和叔块奖励实时结算
//consensus/ethash/consensus.go:572
func (ethash *Ethash) Finalize(chain consensus.ChainReader, header *types.Header, state *state.StateDB, txs []*types.Transaction, uncles []*types.Header) {
// Accumulate any block and uncle rewards and commit the final state root
accumulateRewards(chain.Config(), state, header, uncles)
header.Root = state.IntermediateRoot(chain.Config().IsEIP158(header.Number))
}
func accumulateRewards(config *params.ChainConfig, state *state.StateDB, header *types.Header, uncles []*types.Header) {
// Select the correct block reward based on chain progression
blockReward := FrontierBlockReward
if config.IsByzantium(header.Number) {
blockReward = ByzantiumBlockReward
}
if config.IsConstantinople(header.Number) {
blockReward = ConstantinopleBlockReward
}
// Accumulate the rewards for the miner and any included uncles
reward := new(big.Int).Set(blockReward)
r := new(big.Int)
for _, uncle := range uncles {
r.Add(uncle.Number, big8)
r.Sub(r, header.Number)
r.Mul(r, blockReward)
r.Div(r, big8)
state.AddBalance(uncle.Coinbase, r)
r.Div(blockReward, big32)
reward.Add(reward, r)
}
state.AddBalance(header.Coinbase, reward)
}
4.叔块
4.1 什么是叔块
指**未能成为区块链最长链的一部分的区块(废弃区块),但当它们被包含在后续区块中时,这些区块被称为“叔块”。
是针对区块的,是指区块中包含的老祖宗孤块,未能成为区块链最长链的一部分而被包含。
它是当前块的叔块,一个块最多可以记录7个叔块。 叔块也是一个拥有合法数据的块,但是它所在的区块链分支还没有成功成为主链的一部分。
如上图所示,新区块E可以包含两个绿色孤块B和C,但是灰色区块不能包含,因为它们的父区块不在新区块所在的区块链上。 黄色块和红色新块是兄弟块,不能被新块包含为叔块。
4.2 为什么要设计叔块
在比特币中,由于临时分叉(软分叉)而未能成为最长合法链上的区块的区块称为孤儿块,孤儿块没有区块奖励。 研究发现,一个新区块传播到整个比特币网络 95% 的节点需要 12.6 秒。 在比特币系统中,平均每 10 分钟产生一个区块,并且有足够的时间将新区块广播给全网其他节点。 这种临时分叉的概率是相当小的。 根据历史数据,平均3000多个区块会出现一次临时分叉,相当于20多天出现一次这样的临时分叉,属于比较“罕见”的情况。
但是以太坊的出块时间已经缩短到每块 12 到 14 秒。 更短的时间意味着临时分叉的概率大大增加。 这是因为当矿工A挖出一个新区块时,需要向全网广播,广播过程需要时间。 由于以太坊的出块时间较短,其他节点可能在收到矿工A释放的区块之前已经挖出相同高度的区块,造成临时分叉。 在以太坊网络中,临时分叉发生的概率约为 6.6%。
以上数据来自(2020-06-03),目前以太坊叔块率为6.6%。 这意味着在以太坊网络中,每 100 个区块大约产生 7 个叔块。 如果按照平均出块时间13.5秒计算,一个小时内大约有17.6个临时分叉。
以太坊系统中的临时分叉是一种普遍现象。 如果和比特币一样处理,只有最长链上的区块才会有区块奖励。 对于矿工来说,这是非常不公平的,这种“不公平”将是一种普遍的情况。 这会影响矿工挖矿的积极性,甚至可能削弱以太坊网络的系统安全性,也是一种算力的浪费。 因此,以太坊系统为不在最长链上的叔块设置叔块奖励。
4.3 如何在区块中包含叔块
当节点不断收到区块比特币挖矿怎样算收益,尤其是多个相同高度的区块时,以太坊会陷入短期的软分叉,或者在多个软分叉分支之间来回切换。 一旦发生软分叉,就意味着一个区块未能成为最长链的一部分。
比如上图中,挖矿依次收到A、B、C,会在本地验证并存储这些区块,但最终会按照最长链规则切换到分支B。 所以此时A和C暂时成为孤块。 矿工将基于B挖出下一个新的区块D。
此时,D 可以将临时存储在区块 D 本地的孤立区块 A 和 C 作为其叔块。 当然,D不仅可以收集初代祖宗,还可以收集七代以内的孤块。 但是有一些限制。 以下图中的新区块N为例:
N不能包含A:因为A不在七代祖中(区间要求); N不能包括M:因为M不是N的祖先,而是兄弟; N不能包含E、G、K、L:因为它们的父块不在N所在块的分支上,但是B可以; N不能同时包含D、C、B:因为一个区块最多可以包含两个叔块,所以选择两个大于三个; 当D被F包含时,N不能重复包含D; N不包括F或H:因为F和H不是孤立的块;
在挖出一个新区块并准备区块头信息时,矿工会从本地节点存储中获取七代以内的所有家族区块,并根据上述规则选择最多两个叔块。 另外,选择时优先考虑本地叔块。
5. 叔块奖励分配
叔块奖励分为两部分:奖励包含叔块的矿工和奖励叔块创建者。
5.1 奖励叔块创建者
叔块创建者的奖励根据“远近”关系不同。 离当前区块越远,奖励越少。
{叔块奖励} = {8-(当前区块高度-叔块高度)}{8} * {当前区块挖矿奖励}
5.2 收集叔块的矿工
矿工为当前新区块的矿工。 除了获得原始区块挖矿奖励(2 ETH)和交易手续费外,他还可以获得包含叔块的奖励。 每包含一个区块,他就会多获得1/32的区块挖矿奖励。
以块 [10192970]() 为例:
区块矿工2Miners:SOLO共获得2**.**385338652682918613 ETH奖励,其中:
2 ETH为挖矿奖励; 0.322838652682918613 ETH为交易手续费; 0**.**0625 ETH是包含一个叔块的奖励,是2 ETH挖矿奖励的1/32。
包含的[叔块]()为第一代叔块,奖励2个ETH的7/8。
5.3 叔块如何包含在块中
6.块存储
本文提到的挖矿环节中的存储环节,当矿工通过穷举计算找到符合要求的区块Nonce时,就标志着新区块被成功挖出。
这时,矿工会直接将这个合法的区块存储在本地。 下面具体说明矿工如何将自己挖出的新区块存储在geth中。
经过上一个链接“PoW 寻找 Nonce”,我们已经有了完整的区块信息。
在“处理本地交易”和“处理远程交易”之后,你有一个完整的大宗交易收据列表:
区块中的每一笔交易处理完后,都会有一张交易收据。 本次交易的执行结果信息记录在交易回执中。 对于交易收据,我们在之前的课程中已经进行了说明,这里不再赘述。
同时,“区块奖励发放”后,区块状态不会再发生变化。 这时,我们已经获得了一个可以代表区块的状态数据。 状态`state`会在内存中记录本区块交易执行后状态发送的变化信息,包括新增、变化和删除的数据。
上述的区块(Block)、交易回执(Receipt)、状态(State)就是本次挖矿的产物,本地只需要存储这三部分数据。
挖矿中处理和存储这些数据的代码如下:
//miner/worker.go:595
var (
receipts = make([]*types.Receipt, len(task.receipts))
logs []*types.Log
)
for i, receipt := range task.receipts {//❶
// add block location fields
receipt.BlockHash = hash
receipt.BlockNumber = block.Number()
receipt.TransactionIndex = uint(i)
receipts[i] = new(types.Receipt)
*receipts[i] = *receipt
for _, log := range receipt.Logs {
log.BlockHash = hash
}
logs = append(logs, receipt.Logs...)//❷
}
// Commit block and state to database. //❸
_, err := w.chain.WriteBlockWithState(block, receipts, logs, task.state, true)
if err != nil {
log.Error("Failed writing block to chain", "err", err)
continue
}
log.Info("Successfully sealed new block", "number", block.Number(), "sealhash", sealhash, "hash", hash,
"elapsed", common.PrettyDuration(time.Since(task.createdAt)))
遍历交易回执,为每个交易回执添加当前区块信息(blockHash、BlockNumber、TransactionIndex),这样就可以在本地记录交易回执与区块的查找关系。 同时将交易回执中产生的日志信息提取到一个大集合中,整体存储为一个块日志。 开始向本地数据库提交区块(Block)、交易回执(Receipt)、状态(State)和日志(log)。
在 `writeBlockWithState` 中,所有数据都作为批量事务写入数据库:
blockBatch := bc.db.NewBatch()
rawdb.WriteTd(blockBatch, block.Hash(), block.NumberU64(), externTd)
rawdb.WriteBlock(blockBatch, block)
rawdb.WriteReceipts(blockBatch, block.Hash(), block.NumberU64(), receipts)
rawdb.WritePreimages(blockBatch, state.Preimages())
if err := blockBatch.Write(); err != nil {
log.Crit("Failed to write block into disk", "err", err)
}
// Commit all cached state changes into underlying memory database.
root, err := state.Commit(bc.chainConfig.IsEIP158(block.Number()))
//...
// Set new head.
if status == CanonStatTy {
bc.writeHeadBlock(block)
}
在一次交易中,区块难度、区块、交易回执、Preimages(key mapping)分别写入数据库,最后提交状态。
那么,geth是如何将这些数据存储在key-value数据库levelDB本地的呢? 在这里,我为大家整理一张key-value信息表。
钥匙
价值
阐明
"b".blockNumber.blockHash
blockBody:叔叔+交易
通过区块哈希和高度存储对应的区块叔块和交易信息
“H”.blockHash
块号
通过区块哈希记录区块的区块高度
"h".blockNumber.blockHash
块头
通过区块哈希和高度存储区块的区块头
“r”.blockNumber
收据
通过区块高度记录区块的交易回执记录
“h”.blockNumber
块散列
区块高度对应的区块哈希
"l".txHash
块号
记录交易哈希所在的区块高度
“最后一块”
块散列
更新最后一个区块哈希值
“最后一个标题”
块散列
更新最后一个区块头的位置
注意上面的值信息需要序列化成字节后才能存入leveldb。 序列化是以太坊定制的RLP编码技术。 你有没有想过为什么要加前缀? 比如“b”、“H”等等,第一个好处是可以对不同的数据进行分类,另一个重要的原因是leveldb中数据是按key-value排序存储的,这样在遍历区块头和查询的时候同类型读取性能会更好。
正是因为我们在本地有一些块数据的映射关系,我们可以通过本地数据库提供的少量信息,结合一个或多个键值关系,快速查询到目标数据。 下面我列出了一些常见的以太坊 API。 您如何看待从数据库中查找数据?
通过交易哈希获取交易信息:eth_getTransactionByHash("0xb903239f8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238")查询最后一个区块信息: eth_getBlockByNumber("latest")通过交易哈希获取交易回执eth_getTransactionReceipt("0x444172bef57ad978655171a8af2cfd89baa02a97fcb773067aef7794d6913374")