solidity return data和revert/require的reason string的获得
前言:
在使用solidity写智能合约的时候,会使用到revert和require来进行断言,比如:
require(tokenOwner[tokenId] == 0x0,'this is not the first create');
在上面的断言中,只有当你满足了tokenOwner[tokenId] == 0x0这个 条件,你才能继续往下执行,否则就会报错“this is not the first create”。
然后当我们使用remix这个编译器的时候,是能够在出错的时候得到reason string这个错误信息的,如下:
Transact to token.create errored:VM error:revert. revert The transaction has been reverted to the initial state. Reason provided by the contract: “this is not the first create”. Debug the transaction to get more information.
在remix-ide中当transaction出错,即require断言没通过时,在terminal处会返回这样的信息,但是如果你在geth私有链中运行的时候,你只能知道你的transaction失败了,但是这个时候我们更多的是希望知道是哪里出错了,而不是仅仅得到交易失败的结果。
目的:
就是查找如何能够在使用geth搭建的私有链及web3js来实现revert/require的reason string的获得。
过程:
首先去查看了remix-ide的源码,然后发现这个返回的错误信息是通过读取获得的
网址:https://github.com/ethereum/remix/blob/73849adc6bf0eb5f1b8198842cfb9a8f417264b9/remix-lib/src/execution/txExecution.js
然后在GitHub中的EIP758(https://github.com/ethereum/EIPs/blob/master/EIPS/eip-758.md)可以看见这的确是一个没有实现的功能,就是在remix中能够返回require和revert中的信息,但是geth是会把这个信息直接扔掉的。除此之外,从EIP758也知道,如果你智能合约中的函数有返回值(return data),那么也是会被抛弃的。
⚠️但是EIP758一直讲的都是return data的问题,但是很多地方谈reason string的时候又把他们合在一起,可能两者的实现是一起的吧!!!!!
这个好像就是大家也都发现的问题,就是geth是会把内部得到的返回值(或者require和revert中的信息reason string)信息直接扔掉的,但是后面EIP758提出了解决了这个问题的方案:
This EIP proposes that callers should be able to subscribe to (or poll for) completed transactions. The Ethereum node sends the return data to the caller when the transactions are sealed.
目前如果当你的函数是通过eth_sendTransaction
or eth_sendRawTransaction
RPC request来执行的,那么外部访问者是没有方法获得return data的。
解决方法是在web3js中的函数eth_subscribe中添加查看交易的return data的功能,详细情况自己看。
后面在GitHub中的确有看见别人问和我一样的问题:https://github.com/ethereum/go-ethereum/issues/15721,是2017/11/21号的时候问的,那时候是说还没有办法:
Directly - not yet, pending ethereum/solidity#1686.
You should be able to step it through a debugger, like the one in Remix.
您应该能够通过调试器进行调试,比如Remix中的调试器。其实就是使用remix,但我就是想知道能不能用geth。
参考:https://github.com/ethereum/go-ethereum/pull/16424
https://github.com/ethereum/EIPs/issues/963
这里有详细解释了要如何实现EIP758这个提案:
This functions in two different ways,:
one
via the JSON-RPC command eth_subscribe. If the client has a full-duplex
connection (ipc or ws), it can issue an eth_subscribe command with
params= [“returnData”].For any transactions submitted by the same client
after that, the tx hash is saved internally while the transaction is
pending, and the appropriate client is notified when it completes.
就是如果你的连接方式是ipc or ws,那你就可以使用命令eth_subscribe,参数指定是“returnData”,即返回信息。然后当交易pending的时候,tx hash并不会给你,而是会保存在内部,直到成功写上区块的时候才会通知你信息
the
client has only a half-duplex connection (http), then eth_subscribe is
not allowed since there are no callbacks. Instead,
eth_newReturnDataFilter is sent, the return data of subsequent
transactions are stored up in a queue internally, and returned to the
client when eth_getFilterChanges is called with the same subscription
id.
如果你使用的是http的话,就不能使用命令eth_subscribe,要变成使用eth_newReturnDataFilter,并且返回数据会以队列的形式保存在内部,知道你调用命令eth_getFilterChanges才会返回给你,不像上面那个直接给你
My
proposal is to add a from field in the params of
eth_newReturnDataFilter, so that they only listen to transactions sent
from a specific address, or a list of addresses--not everyone's
transactions. (I've also mentioned an alternative of adding a subID
param to eth_sendTransaction and eth_sendRawTransaction, which would
also solve the problem.)
就是eth_newReturnDataFilter会监听所有的返回数据,可以通过设置{from:…}来限制只监听哪个from的返回数据,或者在发送数据时(即使用eth_sendTransaction和eth_sendRawTransaction)添加subID参数??
Another
improvement I plan to add soon is an optional bool param noEmpty, which
can suppress notifications for transactions where there is no return
data. For noEmpty=false, the client will still be notified when the
requested transactions complete, but the return data field will be []
noEmpty=true,就是在当没有返回数据时就可以不用返回东西,否则仍会返回一个[]空数组
因为EIP758的status是Draft,说明该建议在ethereum还没有真正实现,还在实现中,我们只好等待了!!!!!!
EIP Status Terms Draft - an EIP that is undergoing rapid iteration and changes Last Call - an EIP that is done with its initial iteration and ready for review by a wide audience Accepted - a core EIP that has been in Last Call for at least 2 weeks and any technical changes that were requested have been addressed by the author Final (non-Core) - an EIP that has been in Last Call for at least 2 weeks and any technical changes that were requested have been addressed by the author. Final (Core) - an EIP that the Core Devs have decide to implement and release in a future hard fork or has already been released in a hard fork Deferred - an EIP that is not being considered for immediate adoption. May be reconsidered in the future for a subsequent hard fork.
这个建议的依据是EIP 658:
https://github.com/ethereum/EIPs/blob/master/EIPS/eip-658.md
EIP 658是建议将return data写到 transaction receipts,但是这样会导致 return data is not charged for (as it is not stored on the blockchain), so adding it to transaction receipts could result in DoS and spam opportunities.
所以最后的解决方案是在transaction receipts加上一个状态status,通过eth.getTransactionReceipt()查看,status为0x0,则交易失败;status为0x1,则交易成功。该建议的status是final,说明已经实现。
但是后面有其他的惊喜:
https://github.com/trufflesuite/ganache-core/issues/116
https://github.com/trufflesuite/truffle/issues/976
在这里我们能够发现现在ganache-core和truffle的新版本v5.0(beta)已经实现了能够查看reason string的功能,但是我还没试过,大家可以自己去看看(但是我就是想使用geth的)
-
Remix gets the reason string because it runs
ethereumjs-vm
in the browser and can just grab the return data directly out of the vm. The clients' default behavior is to throw that data away rather than include it in the response, so we need changes at that layer to be able to do the same thing. -
Ganache just merged a PR last week that attaches return data to the error message on
eth_call
. That change is queued for the next release. As soon as it becomes available we'll begin work on integrating this into the next version oftruffle-contract
.
通过查看ganache-core的代码来看它是如何实现这个操作的:
https://github.com/trufflesuite/ganache-core
感觉自己好像低估了ganache的功能,后面好好学学。
最终:
终于查明这个功能还没有实现,只能等待了
https://github.com/OpenZeppelin/openzeppelin-solidity/issues/917
web3.js is not the culprit. There is actually no way to retrieve the revert reason from a node.
EIP 758 is a proposal to solve that, and there's Geth (ethereum/go-ethereum#16424) and Ganache (trufflesuite/ganache-core#116) issues to implement it.
即上面提到的:
https://github.com/ethereum/go-ethereum/pull/16424(ethereum/go-ethereum#16424)
https://github.com/trufflesuite/ganache-core/issues/116(trufflesuite/ganache-core#116)
I guess we'll have to wait and see how that plays out.
looks like truffle 5.0 will allow for this to be possible: see the relevant beta release notes.
https://github.com/trufflesuite/truffle/releases/tag/v5.0.0-beta.0#reason-strings(truffle的新版本)
truffle v5.0.0-beta.0 是如何解决这个问题的呢:
Revert with reason strings!!
Find out the reason. At the moment this feature is only supported by the ganache-cli client (>= 6.1.3). Parity and Geth are still working out their implementations.
Solidity中运行:
require(msg.sender == owner, 'not authorized');
再在Javascript中运行
try {
await example.transferToSelf({from: nonOwner})
} catch (err) {
assert(err.reason === 'not authorized');
assert(err.message.includes('not authorized');
}
软件版本阶段说明
Alpha版: 此版本表示该软件在此阶段主要是以实现软件功能为主,通常只在软件开发者内部交流,一般而言,该版本软件的Bug较多,需要继续修改。
Beta版: 该版本相对于α版已有了很大的改进,消除了严重的错误,但还是存在着一些缺陷,需要经过多次测试来进一步消除,此版本主要的修改对像是软件的UI。
RC版: 该版本已经相当成熟了,基本上不存在导致错误的BUG,与即将发行的正式版相差无几。
Release版: 该版本意味“最终版本”,在前面版本的一系列测试版之后,终归会有一个正式版本,是最终交付用户使用的一个版本。该版本有时也称为标准版。一般情况下,Release不会以单词形式出现在软件封面上,取而代之的是符号(R)。
所以该truffle的beta版还是测试版
其他:
(1)
一开始以为eth_subscribe已经实现了上面的功能,所以去查看并希望使用web3的1.0版本,在这里遇见了个问题:为了实现上面的函数eth_subscribe,来获得solidity中require里的断言失败信息:
新生成一个文件remix-revert,将之前运行智能合约的文件夹中的package.json复制粘贴过去,然后将里面的web3版本改为1.0.0,而不是还使用0.20.1,然后使用npm init来把package.json中的模块都安装下来。但是后面发现这样不能成功,老是报错,所以打算还是直接一个个安装吧。
npm init :生成package.json 安装时出现了这样的错误: npm ERR! Unexpected end of JSON input while parsing near ‘...xpress/-/express-2.1. 解决方法是: npm cache clean —-force
为什么要改web3
因为要试着使用web3.eth.subscribe(“pendingTransactions”)来获得solidity中require中的信息,但是这个是1.0版本才有的功能,0.20中报错
(2)
再后面我看见一个问题就是有人问他发现就是如果使用了reason string,它所使用的gas会变得很高,就是使用reason string是一件十分奢侈的事情:
https://github.com/ethereum/solidity/issues/4588
It will not be part of storage, but more likely be stored as a combination of push and mstore
原因可能就是在编译的过程中,需要将这些reason string压到内存当中,这个过程当然是会花费gas的。所以我的交易总是频繁地出现out of gas的原因很有可能就是使用了require的reason string的原因了,所以我的tx的gas才会这么高
https://github.com/ethereum/solidity/issues/4774
说明gas高的原因
Currently all strings are broken up into 32 byte items, PUSHd and MSTOREd.
The helper function CompilerUtils::storeStringData is doing this, however it has a fixed rule for only doing it for >128 characters.
It should be exposed to the optimiser (or user) to decide based on cost.