カタパルトのトランザクションが承認されるまでの時間を測る

# はじめに

現在、NEMブロックチェーンにおいて、ブロックは比較的空いていて、トランザクションを送信したら、すぐに承認される状態となっています。

しかし、他のブロックチェーンのようにトランザクション詰まりが発生するようになった場合、付加している手数料が多いほど先に承認されるようになるはずです。

その場合、どのくらいの手数料を付加すれば、どのくらいの時間で承認されるのかを知っておくことは重要です。他のブロックチェーンでは、そのためのWebサイトがいくつかあります。

https://ethgasstation.info/

今回は、これらに似たようなことをやろうと思います。

具体的には、カタパルトにおいて、未承認トランザクションがどのくらいの時間で承認されるのかというのを算出することをやってみたいと思います。

# 未承認トランザクションの情報をどうやって取得するか

未承認トランザクションがいつ発生したのか、という情報はブロックには書かれていません。

なので、これはノードから取得する必要があります。

カタパルトのノードは、ZeroMQによる情報の受信ができます。また、ブロックチェーンの情報を、mongoDBに書き込む機能があります。

まず考えられるのは、WebSocketやZeroMQで未承認トランザクションの発生をサブスクライブすることですが、これらはアドレスによるフィルタが前提となっているので、すべて未承認トランザクションを取得することはできません。

いっぽう、mongoDBに直接アクセスするならば、すべての未承認トランザクションを取得することができます。今回はこちらを使います。

# 筋書き

## 未承認トランザクション

mongoDBにはコレクションにドキュメントが追加された場合に通知してくれる機能があります。また、ドキュメントのObjectIdにはタイムスタンプが入っていますのでこれを使用します。

## 承認トランザクション

REST gatewayに接続し、WebSocketを使って新しいブロックが生成されたことを検知します。そのブロックのタイムスタンプを使って、トランザクションが発生してから承認されるまでの時間を計測します。

## 集計

トランザクションハッシュをキーにして、Excelで承認にかかった時間を集計します。

# 手順

- Node.js v8.15.1
- mongodb 3.3.3
- ws 7.2.0
- axios 0.19.0

カタパルト：fushichoのnf testnet

mongoDBのポートはあらかじめ開放しておきます。

## 未承認トランザクションを取得する

### コレクションをウォッチする

mongoDBのコレクションをウォッチして、ドキュメントが追加されたら通知してもらうようにします。

参考URL：https://docs.mongodb.com/manual/changeStreams/

```javascript
const MongoClient = require('mongodb').MongoClient,
    assert = require('assert');

const client = new MongoClient('mongodb://localhost:27017' , {
    useUnifiedTopology: true,
    useNewUrlParser: true
});

client.connect(function(err, mongoclient) {
    assert.equal(err, null);
    console.log('connection open');

    const db = mongoclient.db('catapult');
    
    const stream = collection.watch();
    stream.on('change', next => {
        console.log('Found new record');
        console.log(next);
    });
    
    process.on('SIGINT', () => {
        mongoclient.close();
        console.log('connection close');
        process.exit(0);
    })
});
```

まず、このスクリプトを起動しておきます。

試しに未承認トランザクションを発生させると、このように未承認トランザクションが表示されます。

```javascript
Found new record
{ _id: 
   { _data: '825DAE8185000000012B022C0100296E5A1004D7D015B08101407D8C2781B987A98BCA46645F696400645DAE8185D0A2BC4A9B37A6120004' },
  operationType: 'insert',
  clusterTime: Timestamp { _bsontype: 'Timestamp', low_: 1, high_: 1571717509 },
  fullDocument:
   { _id: 5dae8185d0a2bc4a9b37a612,
     meta:
      { height: 0,
        hash: [Object],
        merkleComponentHash: [Object],
        index: 0,
        addresses: [Array] },
     transaction:
      { signature: [Object],
        signerPublicKey: [Object],
        version: 36865,
        type: 16724,
        maxFee: 30000,
        deadline: 112255906278,
        recipientAddress: [Object],
        message: [Object],
        mosaics: [Array] } },
  ns: { db: 'catapult', coll: 'unconfirmedTransactions' },
  documentKey: { _id: 5dae8185d0a2bc4a9b37a612 } }
```

その後、未承認トランザクションは承認されたので、レコードが削除されます。すると、このような表示が追加されます。

```javascript
Found new record
{ _id: 
   { _data: '825DAE818E000000062B022C0100296E5A1004D7D015B08101407D8C2781B987A98BCA46645F696400645DAE8185D0A2BC4A9B37A6120004' },
  operationType: 'delete',
  clusterTime: Timestamp { _bsontype: 'Timestamp', low_: 6, high_: 1571717518 },
  ns: { db: 'catapult', coll: 'unconfirmedTransactions' },
  documentKey: { _id: 5dae8185d0a2bc4a9b37a612 } }
```

まずは第一歩。

### ウォッチをフィルタする

ここで、未承認トランザクションの発生についての情報のみ取得したい場合は、フィルターを使います。

```javascript
    const pipeline = [{
        $match: {
            operationType: 'insert'
        }
    }]
    const changeStream = collection.watch(pipeline);
```

`$match`を使い、`operationType: 'insert'`のものだけを表示します。

### 必要な情報を抜き出す

未承認トランザクションの集計に必要な情報だけを取り出します。ここで、mongoDBのObjectIdからタイムスタンプを取り出し、これを未承認トランザクションの発生時刻とします。

```javascript
const ObjectId = require('mongodb').ObjectID
...中略
        console.log('Found new record');
        const dateStr = ObjectId(next.fullDocument._id).getTimestamp();
        const hash = next.fullDocument.meta.hash.buffer.toString('hex').toUpperCase();
        console.log(dateStr, hash);
```

試しに未承認トランザクションを発生させるとこのような表示になります。

```
connection open
Found new record
2019-10-22T04:37:05.000Z '1485384C31DDD67909EEB3D7945FD147AC01C34545E68031D8CD1E3133470B4F'
```

時刻とトランザクションハッシュが表示されました。

# 承認トランザクションを取得する

### 新しいブロックの情報を取得する

ブロックが承認されたら、ブロックのタイムスタンプとその中に入っているトランザクションを取得します。もしトランザクションがなければ、何も表示しません。

```javascript
process.env.HOST = 'localhost:3000'

const WebSocket = require('ws');
const ws = new WebSocket('ws://' + process.env.HOST + '/ws');
const axios = require('axios');


function formatTimestamp(str) {
    return new Date(Number(str) + 1459468800000).toISOString()
}

const blockHandler = (obj) => {
    const blockPromise = axios.get('http://' + process.env.HOST + '/block/' + obj.block.height)
    const txPromise = axios.get('http://' + process.env.HOST + '/block/' + obj.block.height + '/transactions')
    Promise.all([blockPromise, txPromise]).then((results) => {
        const block = results[0].data;
        const txs = results[1].data;
        txs.map((tx) => {
            console.log(
                formatTimestamp(block.block.timestamp),
                block.block.feeMultiplier,
                tx.meta.hash,
                tx.transaction.maxFee
            )
        })
    })
}

ws.on('open', () => {
    console.log('connection open')
})
ws.on('close', () => {
    console.log('connection close')
})
ws.on('message', (e) => {
    const obj = JSON.parse(e)
    if ('uid' in obj) {
        const msg = '{"uid": "' + obj.uid + '", "subscribe":"block"}'
        console.log('send', msg)
        ws.send(msg)
    } else if ('block' in obj) {
        blockHandler(obj)
    }
})
```

実行したら、こんな感じで表示されます。

```
connection open
send {"uid": "A5O7FZEUP3HLWHG4FY5WXM6CWFOXBGHU", "subscribe":"block"}
2019-10-22T12:54:36.372Z 352 AB5DE9A1FC624643F4AB93A8181A98E77462C90EAE893109F71870E7C56194F5 58184
2019-10-22T12:54:36.372Z 352 27C0E9C6222A4D759A6E2DBB36A8845568DBA3AF3320E1935D74F443350E2608 64525
```

時刻、`feeMultiplier`、トランザクションハッシュ、`maxFee`の順に表示します。

# 集計

先ほど作成したスクリプトを起動し、いくつかのトランスファートランザクションを送信しました。

その際、手数料はランダムに設定し、5秒間隔で送信しました。このブロックチェーンは、ブロック生成間隔が15秒なのでそれよりは短くしました。

Excelを使い、グラフ化しました。

横軸に`feeMultiplier`、縦軸に承認までの時間を秒数でプロットしたのはこちら。

手数料が低いと、承認までに時間がかかる場合が多いことがわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/5e82bd27-335b-2a46-1104-df92496309f0.png)

手数料は、`feeMultiplier`とトランザクションのサイズの積で決まります。今回使用したトランスファートランザクションは、165バイトなので、`feeMultiplier = 600`で0.99xemとなりました。

なお、このブロックチェーンでは、トランザクション詰まりが全く発生していません。それなのにこのような結果になるのは不思議かもしれません。

あくまで推測ですが、これは、`feeMultiplier`がブロック毎に設定される仕様のためです。

例えば、`feeMultipilier = 200`のブロックを作ると、それより低い手数料のトランザクションはブロックに入ることができません。それにより、ブロック詰まりが起きてなくても、手数料の低いトランザクションは待たされることになります。


ちなみに、横軸に`maxFee`、縦軸に承認までの時間を秒数でプロットしたのはこちら。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/5fc382ee-eed2-9dce-4a55-7a3ffe2851bb.png)

今回は、トランスファートランザクションのみで、しかもサイズが全て同じだったため、`feeMultiplier`のグラフと似たような形になります。

# おわりに

未承認トランザクションが発生してから承認されるまでの時間を取得することができました。

あとはこれをDBに保存するなりして、WebUIを作るとそれっぽいものができるような気がします。