Введение в варианты использования событий и журналов в блокчейне Ethereum с образцом кода.

События и журналы важны в Ethereum, потому что они облегчают взаимодействие между смарт-контрактами и их пользовательскими интерфейсами. В традиционной веб-разработке ответ сервера предоставляется в виде обратного вызова веб-интерфейсу. В Ethereum, когда транзакция добывается, смарт-контракты могут генерировать события и записывать журналы в цепочку блоков, которые затем может обрабатывать интерфейс. Есть разные способы обработки событий и журналов. В этом техническом введении будут объяснены некоторые источники путаницы в отношении событий и приведен пример кода для работы с ними.

События могут сбивать с толку, потому что их можно использовать по-разному. Событие для одного может не выглядеть событием для другого. Есть 3 основных варианта использования событий и журналов:

  1. возвращаемые значения смарт-контракта для пользовательского интерфейса
  2. асинхронные триггеры с данными
  3. более дешевая форма хранения

Терминология между событиями и журналами - еще один источник путаницы, и он будет объяснен в третьем варианте использования.

1) Умные контракты возвращают значения для пользовательского интерфейса

Самое простое использование события - передать возвращаемые значения из контрактов во внешний интерфейс приложения. Чтобы проиллюстрировать, вот в чем проблема.

contract ExampleContract {
  // some state variables ...
  function foo(int256 _value) returns (int256) {
    // manipulate state ...
    return _value;
  }
}

Предполагая, что exampleContract является экземпляром ExampleContract, внешнего интерфейса, использующего web3.js, можно получить возвращаемое значение, моделируя выполнение контракта:

var returnValue = exampleContract.foo.call(2);
console.log(returnValue) // 2

Однако, когда web3.js отправляет вызов контракта как транзакцию, он не может получить возвращаемое значение [1]:

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;
  }
}
A frontend can then obtain the 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})
When the transaction invoking foo is mined, the callback inside the watch will be triggered.  This effectively allows the frontend to obtain return values from foo.

2) асинхронные триггеры с данными

Return values are a minimal use case for events, and events can be generally considered as asynchronous triggers with data. When a contract wants to trigger the frontend, the contract emits an event. As, the frontend is watching for events, it can take actions, display a message, etc. An example of this is provided in the next section (a UI can be updated when a user makes a deposit.)

3) более дешевая форма хранения

The third use case is quite different from what’s been covered, and that is using events as a significantly cheaper form of storage. In the Ethereum Virtual Machine (EVM) and Ethereum Yellow Paper[2], events are referred to as logs (there are LOG opcodes). When speaking of storage, it would be technically more accurate to say that data can be stored in logs, as opposed to data being stored in events. However, when we go a level above the protocol, it is more accurate to say that contracts emit or trigger events which the frontend can react to. Whenever an event is emitted, the corresponding logs are written to the blockchain. The terminology between events and logs is another source of confusion, because the context dictates which term is more accurate.
Logs were designed to be a form of storage that costs significantly less gas than contract storage. Logs basically[3] cost 8 gas per byte, whereas contract storage costs 20,000 gas per 32 bytes. Although logs offer gargantuan gas savings, logs are not accessible from any contracts[4].
Nevertheless, there are use cases for using logs as cheap storage, instead of triggers for the frontend.  A suitable example for logs is storing historical data that can be rendered by the frontend.
A cryptocurrency exchange may want to show a user all the deposits that they have performed on the exchange. Instead of storing these deposit details in a contract, it is much cheaper to store them as logs. This is possible because an exchange needs the state of a user’s balance, which it stores in contract storage, but does not need to know about details of historical deposits.
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);
}

Suppose we want to update a UI as the user makes deposits. Here is an example of using an event (Deposit) as an asynchronous trigger with data (_market, msg.sender, _amount, now). Assume cryptoExContract is an instance of CryptoExchange:
var depositEvent = cryptoExContract.Deposit({_sender: userAddress});
depositEvent.watch(function(err, result) {
  if (err) {
    console.log(err)
    return;
  }
  // append details of result.args to UI
})
Improving the efficiency of getting all events for a user is the reason why the _sender parameter to the event is indexed: event Deposit(uint256 indexed _market, address indexed _sender, uint256 _amount, uint256 _time)
By default, listening for events only starts at the point when the event is instantiated.  When the UI is first loading, there are no deposits to append to.  So we want to retrieve the events since block 0 and that is done by adding a `fromBlock` parameter to the event.
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
})
When the UI is rendered depositEventAll.stopWatching()  should be called.

В сторону - индексированные параметры

Можно проиндексировать до 3 параметров. Например, в предлагаемом стандарте токенов есть: передача события (адрес проиндексирован _from, адрес проиндексирован _to, uint256 _value). Это означает, что интерфейс может эффективно просто наблюдать за передачей токенов, которые:

  • отправлено адресом tokenContract.Transfer ({_ from: senderAddress})
  • или получен адресом tokenContract.Transfer ({_ to: ReceiverAddress})
  • или отправлено с адреса на определенный адрес
    tokenContract.Transfer ({_ from: senderAddress, _to: ReceiverAddress})

Заключение

Three use cases have been presented for events. First, using an event to simply get a return value from a contract function invoked with sendTransaction(). Second, using an event as an asynchronous trigger with data, that can notify an observer such as a UI. Third, using an event to write logs in the blockchain as a cheaper form of storage. This introduction has shown some of the APIs[5] for working with events. There are other approaches to working with events, logs, and receipts[6] and these topics can be covered in future articles.
-Joseph Chow. Thanks to Aaron Davis, Vincent Gariepy, and Joseph Lubin for feedback on this article.
References
[1] web3.js could watch for the transaction to be included the blockchain, then replay the transaction in an instance of the EVM, to get the return value, but this is a significant amount of logic to add to web3.js
[2] https://github.com/ethereum/yellowpaper
[3] There are gas costs of 375 for a LOG operation, and 375 gas per topic, but when many bytes are being stored, these costs represent an insignificant fraction of the total cost of the storage.
[4] Merkle proofs for logs are possible, so if an external entity supplies a contract with such a proof, a contract can verify that the log actually exists inside the blockchain.
[5] https://github.com/ethereum/wiki/wiki/JavaScript-API#web3ethfilter
[6] http://ethereum.stackexchange.com/questions/1381/how-do-i-parse-the-transaction-receipt-log-with-web3-js

Больше технических руководств

Подпишитесь на нашу рассылку для разработчиков Ethereum

Получайте последние учебные пособия, инструменты и советы профессионалов прямо на свой почтовый ящик.