这篇文章是来自ConsenSys
1。事件和日志在以太坊中异常重要。因为它是合约之间,合约与终端间的沟通桥梁。在传统的Web开发中,服务器通过前端回调函数返回结果(这里更多的想说明的是结果同步返回)。在以太坊中,当交易打包后,某个事件才真正发生,合约将这个事件写入区块链后,前端才能进行对应的响应。有各种方式来跟踪事件和日志。这个技术指引将会解释一些关于事件的困惑,以及对应的解决方案示例。
因为事件可以在不同的方式中使用,所以带来了一些困惑。不同的事件使用上有很大不同,但主要有三种主要的日志和事件方式。
- 智能合约返回给用户界面的值
- 异步的数据触发行为
- 事件是一种低gas消耗的storage
术语事件和日志是另一种引发混乱的源头,我们将在第三个用户场景中解释它们。
1)智能合约返回给用户界面的值
事件最简单的用法是传递合约的返回值给前端。下面是一个简单的说明代码:
contract ExampleContract {
// some state variables ...
function foo(int256 _value) returns (int256) {
// manipulate state ...
return _value;
}
}
假设在使用web3.js
的前端中,exampleContract
是ExampleContract
的一个实例,我们可以通过下述合约执行得到返回值:
var returnValue = exampleContract.foo.call(2);
console.log(returnValue) // 2
然而,当我们用web3.js
以transaction
的方式发起调用,我们发现我们不能立即得到返回值2。
var returnValue = exampleContract.foo.sendTransaction(2, {from: web3.eth.coinbase});
console.log(returnValue) // transaction hash
sendTransaction()
方法的返回值是创建的交易哈希值。因为交易不是立即打包进区块链中的,所以交易不能立即返回一个合约值给前端。
推荐的解决方案是使用事件。这也是事件的设计目标之一。对于下述合约:
contract ExampleContract {
event ReturnValue(address indexed _from, int256 _value);
function foo(int256 _value) returns (int256) {
ReturnValue(msg.sender, _value);
return _value;
}
}
前端可以通过下述代码得到返回值:
var exampleEvent = exampleContract.ReturnValue({_from: web3.eth.coinbase});
exampleEvent.watch(function(err, result) {
if (err) {
console.log(err)
return;
}
console.log(result.args._value)
// check that result.args._from is web3.eth.coinbase then
// display result.args._value in the UI and call
// exampleEvent.stopWatching()
})
exampleContract.foo.sendTransaction(2, {from: web3.eth.coinbase})
当调用foo()
函数的交易被打包了,回调中的watch
将会触发。这将允许前端有效的得到foo()
函数的返回值。
2)异步的数据触发行为
返回值是事件的最小用例,事件通常被认为是一个异步的数据事件触发器。当合约希望与前端交互,合约可以发起一个事件。比如,前端监听某个事件发生后,进行某个行为,显示消息等。下一节中将提供一个这样的例子(当用户存款后,UI进行对应的更新)。
3)事件是一种低gas消耗的storage
第三种使用场景与前两种完全不同。事件是一种低gas消耗的storage
(强调它作为日志数据库的这层属性)。在EVM和以太坊的黄皮书中3,事件被认为是日志(对应的的是LOG
操作码)。当说到存储时,将数据存到日志中比将数据存到事件这种说法,技术上更加准确。然而,当我们在协议这个层面上时,合约发起,或触发一个前端可以响应时,使用事件则更为确切。无论何时事件发生,对应的日志将被写入区块链。事件和日志由于上下文不同,带来了一些混淆,大家可以取决实际情况决定用哪个词。
日志是一种相比合约存储的一种低成本存储。日志每字节花费8gas4,然而合约存储每32字节花费20000gas。虽然日志非常省gas,但在任何合约中均不可直接被访问5。
然而,有些情形下,我们可以使用日志做为一种省钱的存储手段,而不仅仅用来触发前端行为。一种是用存储用于前端渲染的历史数据。
一个加密币的交易所,可以用来展示某个用户的所有存款记录。使用日志的方式相比直接存储到合约中,gas花费低很多。上述实现是可行的,因为交易所往往只需要在合约中存储用户当前的余额(这个比较重要),但不一定需要历史存款的详情。
contract CryptoExchange {
event Deposit(uint256 indexed _market, address indexed _sender, uint256 _amount, uint256 _time);
function deposit(uint256 _amount, uint256 _market) returns (int256) {
// perform deposit, update user’s balance, etc
Deposit(_market, msg.sender, _amount, now);
}
假设我们要在用户存款后更新UI界面上的展示。下面是一个使用事件做为异步数据触发器的例子,数据有_market
,msg.sender
,_amount
,now
。其中cryptoExContract
是CryptoExContract
的一个实例:
var depositEvent = cryptoExContract.Deposit({_sender: userAddress});
depositEvent.watch(function(err, result) {
if (err) {
console.log(err)
return;
}
// append details of result.args to UI
})
事件中,我们对_sender
标记为了indexed
。原因是因为为了提高查找事件的性能。
默认情况下,当事件发生时,我们才开始监听。但当UI首次加载时,由于没有事件发生,没有对应的存款记录需要展示。所以为了展示用户的历史存款记录,我们需要从0号区块开始搜索用户的事件,这可以通过增加一个fromBlock
参数来实现。
var depositEventAll = cryptoExContract.Deposit({_sender: userAddress}, {fromBlock: 0, toBlock: 'latest'});
depositEventAll.watch(function(err, result) {
if (err) {
console.log(err)
return;
}
// append details of result.args to UI
})
UI加载完成后,切记调用depositEventAll.stopWatching()
来去掉监听。
其它-索引参数
最多可以有三个参数是索引的。举例来说,基本的代币行为有event Transfer(address indexed _from, address indexed _to, uint256 _value)
。所以为了有效的监听转帐行为,你可以:
- 监听发送帐号
tokenContract.Transfer({_from: senderAddress})
。 - 监听收款帐号
tokenContract.Transfer({_to: receiverAddress})
。 - 同时监听发送,收款帐号
tokenContract.Transfer({_from: senderAddress, _to: receiverAddress})
。
结语
上面说明了三种使用事件的场景。一,通过事件获取以sendTransaction()
方式进行调用的返回值。二,使用事件作为一个异步的数据触发器,来通知观察者,比如UI。三,将事件记录为日志,以一种低成本的存储的形式使用。这个技术指引也使用了一些与事件相关的API6,不过还有其它处理事件,日志和收据7的方法,这将在未来的文章中提及。
-Joseph Chow. Thanks to Aaron Davis, Vincent Gariepy, and Joseph Lubin for feedback on this article.
关于作者
专注基于以太坊(Ethereum)的相关区块链(Blockchain)技术,了解以太坊,Solidity,Truffle,web3.js。
个人博客: http://tryblockchain.org
版权所有,转载注明出处
参考资料
-
原文翻译自这里。https://media.consensys.net/technical-introduction-to-events-and-logs-in-ethereum-a074d65dd61e ↩
-
web3.js可以监视该交易是否被打包在区块链中,然后在某个EVM实例中重放该交易,以获取返回值,但这会在web3.js中增加大量逻辑。 ↩
-
单次
LOG
指令操作,gas为375,单个topic
为375gas,但由于可存比较多的信息,这些成本平均下来就显得微不足道了。 ↩ -
日志的Merkle证明是可能的,所以在区块链外,如果能提供这样一种证明,可以证明日志确实存在于区块链上。 ↩
-
https://github.com/ethereum/wiki/wiki/JavaScript-API#web3ethfilter ↩
-
http://ethereum.stackexchange.com/questions/1381/how-do-i-parse-the-transaction-receipt-log-with-web3-js ↩
处于某些特定的环境下,可以看到评论框,欢迎留言交流^_^。