EOS 通知仕組み

[_code と _self の違い - Qiita](https://qiita.com/blueplanet/items/5aca11d406bb0c7e02c8) でも少し触りましたが、 `EOS`には`通知`という仕組みがあります。

公式の `eosio.token` コントラクトはこの仕組を使っているので、使い方と動きを確認してみます。

## `eosio.token` の `transfer` ソース確認

https://github.com/EOSIO/eos/blob/master/contracts/eosio.token/eosio.token.cpp#L62-L85

`transfer` の処理では、`require_recipient( from );` と　`require_recipient( to );`があります。

```c++
void token::transfer( account_name from,
                      account_name to,
                      asset        quantity,
                      string       memo )
{
    eosio_assert( from != to, "cannot transfer to self" );
    require_auth( from );
    eosio_assert( is_account( to ), "to account does not exist");
    auto sym = quantity.symbol.name();
    stats statstable( _self, sym );
    const auto& st = statstable.get( sym );

    require_recipient( from );
    require_recipient( to );

    eosio_assert( quantity.is_valid(), "invalid quantity" );
    eosio_assert( quantity.amount > 0, "must transfer positive quantity" );
    eosio_assert( quantity.symbol == st.supply.symbol, "symbol precision mismatch" );
    eosio_assert( memo.size() <= 256, "memo has more than 256 bytes" );


    sub_balance( from, quantity );
    add_balance( to, quantity, from );
}
```

実際送信する時は、下記のように出力されています

```shell
$ cleos push action eosio.token transfer '["bob", "alice", "10.0000 EOS", "memo"]' -p bob
executed transaction: 7f7e3491e13f05926aee6bfdab44f9acfba2e333893d2322652772ac2f70dcf1  136 bytes  4445 us
#   eosio.token <= eosio.token::transfer        {"from":"bob","to":"alice","quantity":"10.0000 EOS","memo":"memo"}
#           bob <= eosio.token::transfer        {"from":"bob","to":"alice","quantity":"10.0000 EOS","memo":"memo"}
#         alice <= eosio.token::transfer        {"from":"bob","to":"alice","quantity":"10.0000 EOS","memo":"memo"}
warning: transaction executed locally, but may not be confirmed by the network yet         ]
```

上記通りに、`eosio.token` の `transfer` アクションを呼び出したら、`bob`と`alice`向けにも同じパラメータで送信されました。
送信されたと言っても何も動いてないように見えますが、何故でしょう。

## 通知の動き
通知の動きは下記になっています。

- 通知先を配列に保持しておく
- 各通知先アカウントに対して、スマートコントラクトを確認し、スマートコントラクトがある場合は、同じパラメータを渡して実行する

通知先が普通のアカウント（スマートコントラクトデプロイされてない場合）は実行されないのはよくて、スマートコントラクトである場合でも特に何も発生してないのは何故でしょう。

それは、デフォルトの `EOSIO_DISPATCH` 処理では、`アクションの呼び出し先がこのコントラクトでない場合はスキップする` ようにしているからです。

```c++
// https://github.com/EOSIO/eosio.cdt/blob/master/libraries/eosiolib/dispatcher.hpp#L123-L133

#define EOSIO_DISPATCH( TYPE, MEMBERS ) \
extern "C" { \
   void apply( uint64_t receiver, uint64_t code, uint64_t action ) { \
      if( code == receiver ) { \
         switch( action ) { \
            EOSIO_DISPATCH_HELPER( TYPE, MEMBERS ) \
         } \
         /* does not allow destructor of thiscontract to run: eosio_exit(0); */ \
      } \
   } \
} \
```

スマートコントラクトを実装する時、最後に定義しているアクションを`EOSI_DISPATCH`マクロに渡していると思いますが、
このマクロの中では、上記通りに、`if( code == receiver ) { `、トランザクションにあるアクションの呼び出し先(code)が今処理されているアカウント(receiver)であるかどうかをチェックしています。
当たり前ですが、自分のアカウントの場合だけ、実行するようにしています。

`eosio.token`から通知がきた場合は、`code` が `eosio.token`になっているので、ここでスキップされています。

[_code と _self の違い - Qiita](https://qiita.com/blueplanet/items/5aca11d406bb0c7e02c8)　のように、通知を受け取るようにしてみます。

```c++
#include <eosiolib/eosio.hpp>
#include <eosiolib/asset.hpp>
#include <string>

using namespace eosio;
using std::string;

class[[eosio::contract]] alice : public eosio::contract{

  public :
    alice(name receiver, name code, datastream<const char *> ds) : contract(receiver, code, ds){}

    [[eosio::action]]
    void transfer(name from, name to, asset quantity, string memo) {
      print("_code : ", name{_code}, ", _self : ", name{_self});
    }
};

#define EOSIO_DISPATCH_EX(TYPE, MEMBERS)                                     \
extern "C" {                                                                 \
  void apply(uint64_t receiver, uint64_t code, uint64_t action)              \
  {                                                                          \
    if (code == receiver || code == "eosio.token"_n.value)                   \
    {                                                                        \
      switch (action)                                                        \
      {                                                                      \
        EOSIO_DISPATCH_HELPER(TYPE, MEMBERS)                                 \
      }                                                                      \
      /* does not allow destructor of thiscontract to run: eosio_exit(0); */ \
    }                                                                        \
  }                                                                          \
}                                                                            \

EOSIO_DISPATCH_EX(alice, (transfer))
```

このコントラクトをビルドし `alice`アカウントにデプロイしておいてから、再度 `alice`向けに送金してみます。

```shell
$ cleos push action eosio.token transfer '["bob", "alice", "11.0000 EOS", "memo"]' -p bob
executed transaction: 2a13ad27b13dfed936a0a56d6892b5208f64f3ee4896103b464bd67d13a0301c  136 bytes  537 us
#   eosio.token <= eosio.token::transfer        {"from":"bob","to":"alice","quantity":"11.0000 EOS","memo":"memo"}
#           bob <= eosio.token::transfer        {"from":"bob","to":"alice","quantity":"11.0000 EOS","memo":"memo"}
#         alice <= eosio.token::transfer        {"from":"bob","to":"alice","quantity":"11.0000 EOS","memo":"memo"}
>> _code : eosio.token, _self : alice
warning: transaction executed locally, but may not be confirmed by the network yet         ]
```

出力 `>> _code: eosio.token, _self : alice` 通りに、通知されたことが確認出来ました。

### 通知先のアクションでエラーになったら？
わざと`transfer`にエラーを発生させておいて、

```c++
[[eosio::action]]
void transfer(name from, name to, asset quantity, string memo) {
  print("_code : ", name{_code}, ", _self : ", name{_self});

  eosio_assert( false, "throw error" );
}
```

再度送金してみると

```shell
$ cleos push action eosio.token transfer '["bob", "alice", "11.0000 EOS", "memo"]' -p bob
Error 3050003: eosio_assert_message assertion failure
```

送金処理がエラーになってしまいました。
`nodeos`側のログは下記になっています。

```shell
error 2018-12-11T20:17:34.146 thread-0  wasm_interface.cpp:933        eosio_assert         ] message: throw error
error 2018-12-11T20:17:34.152 thread-0  http_plugin.cpp:580           handle_exception     ] FC Exception encountered while processing chain.push_transaction
debug 2018-12-11T20:17:34.152 thread-0  http_plugin.cpp:581           handle_exception     ] Exception Details: 3050003 eosio_assert_message_exception: eosio_assert_message assertion failure
assertion failure with message: throw error
    {"s":"throw error"}
    thread-0  wasm_interface.cpp:934 eosio_assert
pending console output: _code : eosio.token, _self : alice
    {"console":"_code : eosio.token, _self : alice"}
    thread-0  apply_context.cpp:72 exec_one
```

**結論としては、`inline action`と同じ動作になって、通知先の処理で失敗したら、通知元の処理もエラーになります。**

### アクションがなかったら？
もし通知先のアカウントで、通知は受ける定義をしていますが、実際にそのアクションがない場合はどうなりますか？

```c++
#include <eosiolib/eosio.hpp>
#include <eosiolib/asset.hpp>
#include <string>

using namespace eosio;
using std::string;

class[[eosio::contract]] alice : public eosio::contract{

  public :
    alice(name receiver, name code, datastream<const char *> ds) : contract(receiver, code, ds){}

    [[eosio::action]]
    void trans(name from, name to, asset quantity, string memo) {
      print("_code : ", name{_code}, ", _self : ", name{_self});
    }
};

#define EOSIO_DISPATCH_EX(TYPE, MEMBERS)                                     \
extern "C" {                                                                 \
  void apply(uint64_t receiver, uint64_t code, uint64_t action)              \
  {                                                                          \
    if (code == receiver || code == "eosio.token"_n.value)                   \
    {                                                                        \
      switch (action)                                                        \
      {                                                                      \
        EOSIO_DISPATCH_HELPER(TYPE, MEMBERS)                                 \
      }                                                                      \
      /* does not allow destructor of thiscontract to run: eosio_exit(0); */ \
    }                                                                        \
  }                                                                          \
}                                                                            \

EOSIO_DISPATCH_EX(alice, (trans))
```

何も起こりませんでした。

## まとめ
`inline action`と比べると、通知の仕組みを活用するほうが、権限設定が要らないので、より安全に処理できそうです。