问题描述
这个问题是在一个客户那里获到的。一个块里面有多笔交易,其中一笔交易使用
eth.getTransactionByHash("0x4ae91a30dcc6433815fa794c65f9ba341031c7a94b8cfe33232efcc7b14b3cda")
查询之后获得的交易信息如下(为了描述问题,信息有删减):
{
blockHash: "0x4a65696080e087c5e509c16881bc313792b4a5d9adc11602c5c9551fd8e33080",
gas: 200000000, // 看这里
gasPrice: 21000000000,
hash: "0x4ae91a30dcc6433815fa794c65f9ba341031c7a94b8cfe33232efcc7b14b3cda"
}
关注信息中的gas即可,表示我这笔交易最多花费200000000来执行这笔交易。
再使用eth.getTransactionReceipt("0x4ae91a30dcc6433815fa794c65f9ba341031c7a94b8cfe33232efcc7b14b3cda")
获得的交易回执信息如下(同样信息有删减):
{
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
返回的块信息如下:
{
"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
将三笔交易的回执查出来如下:
{
"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),代码如下:
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,代码有删减):
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(),贴出代码如下:
u256 gasUsed() const
{
return m_receipts.size() ? m_receipts.back().gasUsed() : 0;
}
也就是说每个块的gasUsed就是最后一个交易回执的gasUsed。由此,一切都明白了。跟上面的猜想完全一致。每一个交易所耗费的gasUsed,是所有前面执行过的交易所消耗的gasUsed的值加上本交易所耗费的gasUsed。
问题修正
找到原因了那就好改了,首先修正每个交易的gasUsed。这个好办,不累加startGasUsed即可,将代码
return make_pair(res, TransactionReceipt(rootHash(), startGasUsed + e.gasUsed(), e.logs()));
改为
return make_pair(res, TransactionReceipt(rootHash(), e.gasUsed(), e.logs()));
即可。
但是这样会带来一个问题,块的gasUsed就不正确了,因为每个块耗费的gasUsed是最后一个交易的gasUsed。修改也简单,将所有交易的gasUsed累加即可。代码如下:
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
我组了一个如下的包:
[
{
"gas": "0x59a1",
"gasPrice": "0x174876e800",
"from": "0x1c22736623901b437ccddd56a1db6573d9ffec51",
"data": "0x60fe47b10000000000000000000000000000000000000000000000000000000000000fff",
"to": "0x0d2bf7651722048b3b055d63b8acdcd4f2a385cd"
}
]
返回的块信息:
{
"blockHash": "0xd799bb8d11d502ea6b283125bd94eab9b4aa077ac0a661823603aa87b6e78424",
"cumulativeGasUsed": "0x59a1",
"gasUsed": "0x59a1",
"logs": [],
"transactionHash": "0xde4cb2fcf3b62198c4bc9dd53d3e7e68059f4e4d5f80d2bedd800d033cdd6448",
"transactionIndex": 0
}
交易信息:
{
"blockHash": "0xd799bb8d11d502ea6b283125bd94eab9b4aa077ac0a661823603aa87b6e78424",
"gasUsed": "0x59a1",
"transactionHash": "0xde4cb2fcf3b62198c4bc9dd53d3e7e68059f4e4d5f80d2bedd800d033cdd6448",
"transactionIndex": 0
}
均没有超过我设定的 0x59a1。而且去查询合约,执行结果确实没有生效。
再来测试一个块里面含三笔交易的情况,而且保证gas足够。返回的块信息如下:
{
"gasUsed": "0x13d23",
"hash": "0x957522a3cb5154d81d1f43b3af6eec337d7505b56d72f4e4b8c7328d360a8d50",
"transactions": [
"0x04f5447961f9602fefb5c3580e6ccf3b308e7495d3d8c6b8d8c7fc1b665d60d8",
"0x66c27da3bf27e2a35e106d4fb48543ad6317f52f978842a91162811369320735",
"0xbdc6663b61bffac4692aa2939753310e5f8ef820e58deafb717d24a9010c4ba0"
]
}
查询块里面的三笔交易回执,信息如下:
{
"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?