以太坊C++系列(03)-交易gasUsed错误统计问题

问题描述

这个问题是在一个客户那里获到的。一个块里面有多笔交易,其中一笔交易使用

1
eth.getTransactionByHash("0x4ae91a30dcc6433815fa794c65f9ba341031c7a94b8cfe33232efcc7b14b3cda")

查询之后获得的交易信息如下(为了描述问题,信息有删减):

1
2
3
4
5
6
{
blockHash: "0x4a65696080e087c5e509c16881bc313792b4a5d9adc11602c5c9551fd8e33080",
gas: 200000000, // 看这里
gasPrice: 21000000000,
hash: "0x4ae91a30dcc6433815fa794c65f9ba341031c7a94b8cfe33232efcc7b14b3cda"
}

关注信息中的gas即可,表示我这笔交易最多花费200000000来执行这笔交易。

再使用eth.getTransactionReceipt("0x4ae91a30dcc6433815fa794c65f9ba341031c7a94b8cfe33232efcc7b14b3cda")获得的交易回执信息如下(同样信息有删减):

1
2
3
4
5
6
7
8
{
blockHash: "0x4a65696080e087c5e509c16881bc313792b4a5d9adc11602c5c9551fd8e33080",
blockNumber: 1054269,
contractAddress: "0x5d6896264d20e19b7d34a61bda86427185ee4c7e",
cumulativeGasUsed: 1000000348819,
gasUsed: 1000000348819, // 看这里
transactionHash: "0x4ae91a30dcc6433815fa794c65f9ba341031c7a94b8cfe33232efcc7b14b3cda"
}

关注信息中的gasUsed即可,为1000000348819。

用客户在当时的话描述就是,我们发现一个严重的问题,这笔交易设置的gas: 200000000, 但是实际交易的gasUsed:1000000348819远大于这个值。打个比方就是,我只给你 200000000 去给我建房子,不管能不能建好,只能最多花这么多。最后你告诉我给我花了 1000000348819 。远远大于我给的预算。

问题追踪

我首先去查了一下使用eth.getTransactionReceipt返回的字段的相关信息描述,如下:

  • transactionHash: DATA, 32字节 - 交易哈希
  • transactionIndex: QUANTITY - 交易在块内的索引序号
  • blockHash: DATA, 32字节 - 交易所在块的哈希
  • blockNumber: QUANTITY - 交易所在块的编号
  • from: DATA, 20字节 - 交易发送方地址
  • to: DATA, 20字节 - 交易接收方地址,对于合约创建交易该值为null
  • cumulativeGasUsed: QUANTITY - 交易所在块消耗的gas总量
  • gasUsed: QUANTITY - 该次交易消耗的gas用量
  • contractAddress: DATA, 20字节 - 对于合约创建交易,该值为新创建的合约地址,否则为null
  • logs: Array - 本次交易生成的日志对象数组
  • logsBloom: DATA, 256字节 - bloom过滤器,轻客户端用来快速提取相关日志

其中cumulativeGasUsed,我理解的该区块所有交易消耗的gas之和。英文的原文描述是 cumulativeGasUsed: Number - The total amount of gas used when this transaction was executed in the block. 为什么我关注这个字段呢,因为我看到交易回执的信息中 cumulativeGasUsed 跟 gasUsed 是相等的,所以我推测应该是底层将 gasUsed 使用字段 cumulativeGasUsed 返回了,为了验证我的猜想,我连续发了三笔交易,使用eth_getBlockByNumber返回的块信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"gasUsed": "0x13d63",
"hash": "0xf927315ac785d0e6e61e26bd38f47df99e12f44356e12fb851c52f5ceb2a2ce4",
"transactions": [
{
"blockHash": "0xf927315ac785d0e6e61e26bd38f47df99e12f44356e12fb851c52f5ceb2a2ce4",
"gas": "0x6d60",
"hash": "0x1eadae82718d34207f454388ac2b188df910a57db4dc712d2c88152e3da4d3ab"
},
{
"blockHash": "0xf927315ac785d0e6e61e26bd38f47df99e12f44356e12fb851c52f5ceb2a2ce4",
"gas": "0x6d60",
"hash": "0xdcfb524d14b981155e59ad57448157ae9064a70c92ff9897e5cc9acb98e6fc26"
},
{
"blockHash": "0xf927315ac785d0e6e61e26bd38f47df99e12f44356e12fb851c52f5ceb2a2ce4",
"gas": "0x6d60",
"hash": "0x10b1a228ae351424ec8d9092c8c806e6866bf77f2fe362fac0e6a3f872492533"
}
]
}

然后再使用eth.getTransactionReceipt将三笔交易的回执查出来如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"blockHash": "0xf927315ac785d0e6e61e26bd38f47df99e12f44356e12fb851c52f5ceb2a2ce4",
"cumulativeGasUsed": "0x69e1",
"gasUsed": "0x69e1",
"transactionHash": "0x1eadae82718d34207f454388ac2b188df910a57db4dc712d2c88152e3da4d3ab",
"transactionIndex": 0
}

{
"blockHash": "0xf927315ac785d0e6e61e26bd38f47df99e12f44356e12fb851c52f5ceb2a2ce4",
"cumulativeGasUsed": "0xd3c2",
"gasUsed": "0xd3c2",
"transactionHash": "0xdcfb524d14b981155e59ad57448157ae9064a70c92ff9897e5cc9acb98e6fc26",
"transactionIndex": 1
}

{
"blockHash": "0xf927315ac785d0e6e61e26bd38f47df99e12f44356e12fb851c52f5ceb2a2ce4",
"cumulativeGasUsed": "0x13d63",
"gasUsed": "0x13d63",
"transactionHash": "0x10b1a228ae351424ec8d9092c8c806e6866bf77f2fe362fac0e6a3f872492533",
"transactionIndex": 2
}

确实最后一笔交易的"gasUsed": "0x13d63"就是跟区块里面查出来的值一摸一样。而且,用第二个交易的gasUsed减去第一个gasUsed差不多都是0x69e1。由此基本可以断定,交易的gasUsed是累计算出来的。同时,对于所有的交易回执,也有cumulativeGasUsed等于gasUsed。

定位到eth_getTransactionByHash转JSON代码处(libweb3jsonrpc/JsonHelper.cpp:203),代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
Json::Value toJson(dev::eth::LocalisedTransactionReceipt const& _t)
{
Json::Value res;
res["transactionHash"] = toJS(_t.hash());
res["transactionIndex"] = _t.transactionIndex();
res["blockHash"] = toJS(_t.blockHash());
res["blockNumber"] = _t.blockNumber();
res["cumulativeGasUsed"] = toJS(_t.gasUsed()); // TODO: check if this is fine
res["gasUsed"] = toJS(_t.gasUsed());
res["contractAddress"] = toJS(_t.contractAddress());
res["logs"] = dev::toJson(_t.localisedLogs());
return res;
}

确实,字段返回的 cumulativeGasUsed 跟 gasUsed 是用同一个函数计算得来的。所以,我们要修正 gasUsed 的值即可。

找到交易出回执的地方,在文件libethereum/Block.cpp函数Block::execute里面有那句std::pair<ExecutionResult, TransactionReceipt> resultReceipt = m_state.execute(EnvInfo(info(), _lh, gasUsed()), *m_sealEngine, _t, _p, _txType, _onOp);m_state.execute实现如下(libethereum/State.cpp,代码有删减):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
std::pair<ExecutionResult, TransactionReceipt> State::execute(EnvInfo const& _envInfo /* 其他参数 */)
{

Executive e(*this, _envInfo, _sealEngine);
e.setTransactionType(_txType);
ExecutionResult res;
e.setResultRecipient(res);
e.initialize(_t);

u256 startGasUsed = _envInfo.gasUsed();

// 其他代码

return make_pair(res, TransactionReceipt(rootHash(), startGasUsed + e.gasUsed(), e.logs()));
}

交易回执的gasUsed,是 startGasUsed 加上交易本次执行耗费的gasUsed。我们继续追踪startGasUsed。startGasUsed 是传进来的,我们看最开始的_envInfo初始化的地方为EnvInfo(info(), _lh, gasUsed())。即传进来的startGasUsed是libethereum/Block.h里面的gasUsed(),贴出代码如下:

1
2
3
4
u256 gasUsed() const
{
return m_receipts.size() ? m_receipts.back().gasUsed() : 0;
}

也就是说每个块的gasUsed就是最后一个交易回执的gasUsed。由此,一切都明白了。跟上面的猜想完全一致。每一个交易所耗费的gasUsed,是所有前面执行过的交易所消耗的gasUsed的值加上本交易所耗费的gasUsed。

问题修正

找到原因了那就好改了,首先修正每个交易的gasUsed。这个好办,不累加startGasUsed即可,将代码

1
return make_pair(res, TransactionReceipt(rootHash(), startGasUsed + e.gasUsed(), e.logs()));

改为

1
return make_pair(res, TransactionReceipt(rootHash(), e.gasUsed(), e.logs()));

即可。

但是这样会带来一个问题,块的gasUsed就不正确了,因为每个块耗费的gasUsed是最后一个交易的gasUsed。修改也简单,将所有交易的gasUsed累加即可。代码如下:

1
2
3
4
5
6
7
8
9
u256 gasUsed() const
{
u256 gasUsed = 0;
for(const TransactionReceipt &tr : m_receipts)
{
gasUsed += tr.gasUsed();
}
return gasUsed;
}

至于交易回执里面返回的cumulativeGasUsed,我们认为没必要知道一个交易在一个块里面这个块所消耗的gasUsed,如果需要,那么去调用块消耗的gasUsed即可,所以我们在交易回执里面干脆去掉了这个字段。

验证

因为上面我发的交易每次大概要消耗0x69a1的gas,我让他小于这个gas,我设置了交易的最大gas是0x59a1,eth_sendTransaction我组了一个如下的包:

1
2
3
4
5
6
7
8
9
[
{
"gas": "0x59a1",
"gasPrice": "0x174876e800",
"from": "0x1c22736623901b437ccddd56a1db6573d9ffec51",
"data": "0x60fe47b10000000000000000000000000000000000000000000000000000000000000fff",
"to": "0x0d2bf7651722048b3b055d63b8acdcd4f2a385cd"
}
]

返回的块信息:

1
2
3
4
5
6
7
8
{
"blockHash": "0xd799bb8d11d502ea6b283125bd94eab9b4aa077ac0a661823603aa87b6e78424",
"cumulativeGasUsed": "0x59a1",
"gasUsed": "0x59a1",
"logs": [],
"transactionHash": "0xde4cb2fcf3b62198c4bc9dd53d3e7e68059f4e4d5f80d2bedd800d033cdd6448",
"transactionIndex": 0
}

交易信息:

1
2
3
4
5
6
{
"blockHash": "0xd799bb8d11d502ea6b283125bd94eab9b4aa077ac0a661823603aa87b6e78424",
"gasUsed": "0x59a1",
"transactionHash": "0xde4cb2fcf3b62198c4bc9dd53d3e7e68059f4e4d5f80d2bedd800d033cdd6448",
"transactionIndex": 0
}

均没有超过我设定的 0x59a1。而且去查询合约,执行结果确实没有生效。

再来测试一个块里面含三笔交易的情况,而且保证gas足够。返回的块信息如下:

1
2
3
4
5
6
7
8
9
{
"gasUsed": "0x13d23",
"hash": "0x957522a3cb5154d81d1f43b3af6eec337d7505b56d72f4e4b8c7328d360a8d50",
"transactions": [
"0x04f5447961f9602fefb5c3580e6ccf3b308e7495d3d8c6b8d8c7fc1b665d60d8",
"0x66c27da3bf27e2a35e106d4fb48543ad6317f52f978842a91162811369320735",
"0xbdc6663b61bffac4692aa2939753310e5f8ef820e58deafb717d24a9010c4ba0"
]
}

查询块里面的三笔交易回执,信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"blockHash": "0x957522a3cb5154d81d1f43b3af6eec337d7505b56d72f4e4b8c7328d360a8d50",
"gasUsed": "0x69a1",
"transactionHash": "0x04f5447961f9602fefb5c3580e6ccf3b308e7495d3d8c6b8d8c7fc1b665d60d8",
"transactionIndex": 0
}

{
"blockHash": "0x957522a3cb5154d81d1f43b3af6eec337d7505b56d72f4e4b8c7328d360a8d50",
"gasUsed": "0x69a1",
"transactionHash": "0x66c27da3bf27e2a35e106d4fb48543ad6317f52f978842a91162811369320735",
"transactionIndex": 1
}

{
"blockHash": "0x957522a3cb5154d81d1f43b3af6eec337d7505b56d72f4e4b8c7328d360a8d50",
"gasUsed": "0x69e1",
"transactionHash": "0xbdc6663b61bffac4692aa2939753310e5f8ef820e58deafb717d24a9010c4ba0",
"transactionIndex": 2
}

三笔交易所消耗的gasUsed相加 0x69a1 + 0x69a1 + 0x69e1 === 81187,而块所消耗的gasUsed为0x13d23,转为十进制正好也是81187。问题修正!

其实这个问题我有点不太明白以太坊为什么要这么写,不知道是不是它的字段就是这么设置的,还是我们理解的问题。我觉得这是一个显而易见的bug。我还特地去查了最新的以太坊C++版本的代码aleth交易的gasUsed还是采用累加的值。但是我去看以太坊的浏览器etherscan 的这个块消耗的gasUsed是554482,而我将里面的9个交易所消耗的gasUsed值累计相加22786 + 21000 + 21000 + 16667 + 21000 + 14762 + 151105 + 143081 + 143081 确实也是554482。当然,我看到以太坊代码好像也在尝试修复这个问题,见gasUsed value is incorrect in eth_getTransactionReceipt response

其他

我一直以为如果对交易设置gas之后,一旦执行gas消耗完毕,那么交易就不会执行,不会落链写数据库,块也不会去打包这笔交易。其实不是这样的,交易还是会执行,也有回执,也会将这笔交易打包。但是不会去改变状态树,不改变状态树,那么这笔交易等于没有执行!结果虽然是一致的,但是过程跟我之前想的不一致。

后续验证

因为这个bug太过明显,后续我问了同事关于以太坊Go版本的实现,一个块多笔交易返回的数据确实是跟我理解的一致。

相关资料

Field tx.gasUsed when there are multiple tx per bloc
What is and how to calculate cumulativeGasUsed?

您的支持将鼓励我继续创作!
0%