Node.js
NEM

XEMのトランザクション収集で...

まえがき

NEMへの熱が冷めぬうちに色々勉強しておこうと思う

Proof of Importance(PoI) とかも興味があるが、
外から自分で触ってなにかできるようなものでもないため、
今回は除外

エンドユーザーとして、最も身近なトランザクションデータについて
色々やってみようと思う

といっても、mainnet で mosaic 遊べるほどの資金はないので、
入出金のトランザクションデータを収集して、
データ分析(国税官ごっこ)をしてみたいと思う

トランザクションデータの収集方法

BlockChain Explorer

インターネットブラウザを経由して、ブロックやトランザクションを確認できるサイトがある
(当然他の仮想通貨にも同様のものがある)

例えば、以下のようなサイト

http://explorer.ournem.com/

上記サイトで NC3BI3DNMR2PGEOOMP2NKXQGSAKMS7GYRKVA5CSZ というアドレスの入出金を調べるには以下のアドレスにアクセスすれば良いようだ

http://explorer.ournem.com/#/s_account?account=NC3BI3DNMR2PGEOOMP2NKXQGSAKMS7GYRKVA5CSZ

NEM APIの利用

NEM の ノードに対して httpリクエストをすることでも収集できる
こちらは json形式でのレスポンスであり、非常に操作しやすい

入金の履歴

http://alice2.nem.ninja:7890/account/transfers/incoming?address=NC3BI3DNMR2PGEOOMP2NKXQGSAKMS7GYRKVA5CSZ

出金の履歴

http://alice2.nem.ninja:7890/account/transfers/outgoing?address=NC3BI3DNMR2PGEOOMP2NKXQGSAKMS7GYRKVA5CSZ

address=NC3BI3DNMR2PGEOOMP2NKXQGSAKMS7GYRKVA5CSZ の箇所が履歴を確認したいアドレス
上記のままであれば、最新の履歴25件のみが表示されるだけであるが、
hash=${hash値} を入力することで過去の履歴もみることができる
(id=${id値} でも可能)

取引所におけるメッセージ欄の重要性

XEMの入金については複数のユーザーが同じアドレスに送金することになっており、送金時のメッセージでお客様のアカウントを識別しています。入金時には必ず下記のメッセージを添付して送金してくださいますようお願いします。メッセージは平文のまま(HexやEncryptのチェック無し)送信してください。

上記は Zaif のXEM入金用の画面に表示されるメッセージである

メッセージ欄を収集することで、 だれかはわからないが 同じメッセージならば同じ人のような識別が可能なようだ

収集結果

ということで今回はメッセージ欄も容易に収集できる NEM API で実装することにした
実装例は、補足を参照していただきたい

結果は以下でダウンロードできます

https://gist.github.com/kengos/763db655d49d4329fafcd115a1e02204
※ incoming.csv が入金データ, outgoing.csv が出金データ

フォーマットは以下の通り

列番号 中身
1 日付(JST) 2018-1-30 13:53:45
2 トランザクションID 1598242
3 トランザクションタイプ 257
4 送信者 NXXXXXXXXXXXXX...
5 受信者
6 メッセージ 39303266313136643662323931393661
7 送金額(XEM) 10

データ分析

例の事件以前のデータ(入金は26日以降も含めているが)で概ねこのような感じの収支の様子

入金: 916,000,000 XEM
出金: 389,000,000 XEM

入金の部

a.png
(Data Studioを利用)

全期間を通しての入金額の円グラフ
NBZ... からの入金が90%近くを占めている
(※上記 は Poloniex という海外の取引所のアドレス)
(※報道通り 送金のテストが 2017-6-8 20:59:41 より、実際の送金は 2017-6-12 より始まっている様子)

NBZ... のアドレスから始まる送金から
運営側用のメッセージと思われる 62323832656637313332363432353330 を除外して検索すると
ユーザーが Poloniex で買い付けた XEMの情報がわかる

人数: 543 名(内1名は Zaifと間違えている感じ)
合計: 約 6,512,000 XEM
最も多い人: 約 418,000 XEM

出金の部

2.png

(某事件の影響で1/25 までにフィルタリングしている)

出金は Poloniex, Zaif で全体の50%ほど
その他では Bittrex や Changelly などが人気なようだ

message ごとに集計、送金額が多い人について調べ上げていくことができそう

その他 細かい分析もしているが、データをアップロードしているので
気になる方は各々分析してみてください

例えば Zaif から移動してきた人や、Zaifへ移動した人などがなんとなくわかったり、
月ごとに移動する人数にばらつきがあったりなど

補足

入金履歴の取得プログラム

依存関係
"axios": "^0.17.1"
"nem-sdk": "^1.6.2"
実行方法
$ node incoming.js > incoming.csv
incoming.js
const nem = require('nem-sdk').default;
const networkId = nem.model.network.data.mainnet.id;
const Axios = require('axios');
const NEM_EPOCH = Date.UTC(2015, 2, 29, 0, 6, 25, 0);
const TRANSFER = nem.model.transactionTypes.transfer;
const MULTISIG_TRANSACTION = nem.model.transactionTypes.multisigTransaction;
# ここを調べたいものに変える
const ADDRESS = 'NC3BI3DNMR2PGEOOMP2NKXQGSAKMS7GYRKVA5CSZ';

// transaction のデータを見やすいように加工する
class IncomingTransactionObject {
    constructor(obj) {
        this.metaId = obj.meta.id;
        this.hash = obj.meta.hash.data;
        this.type = obj.transaction.type;

        if (this.type === TRANSFER) {
            // 送信者の PublicKey から アドレスへ変換
            this.sender = nem.model.address.toAddress(obj.transaction.signer, networkId);
            this.message = obj.transaction.message.payload || '';
            this.timeStamp = obj.transaction.timeStamp;
            this.amount = obj.transaction.amount;
        } else if (obj.transaction.type === MULTISIG_TRANSACTION) {
            // multisig の場合は 見る中身を変える
            const otherTrans = obj.transaction.otherTrans;
            if (otherTrans.type === TRANSFER) {
                this.sender = nem.model.address.toAddress(otherTrans.signer, networkId);
                this.message = otherTrans.message.payload || '';
                this.timeStamp = otherTrans.timeStamp;
                this.amount = otherTrans.amount;
            }
        }
    }

    view() {
        if (this.amount === undefined) {
            return;
        }
        console.log(`${this.unixtime},${this.metaId},${this.type},${this.sender},${ADDRESS},${this.message},${this.xemAmount}`);
    }

    get unixtime() {
        const d = new Date(NEM_EPOCH + (this.timeStamp * 1000));
        return d.toLocaleString();
    }

    get xemAmount() {
        return (this.amount / 1000000);
    }
}

class IncomingTransaction {
    constructor() {
        this.url = `http://alice2.nem.ninja:7890/account/transfers/incoming?address=${ADDRESS}`;
    }

    fetch(hash) {
        Axios.get(this.generateUrl(hash)).then((res) => {
            let lastHash = null;
            res.data.data.forEach((tx) => {
                const txObject = new IncomingTransactionObject(tx);
                txObject.view();
                lastHash = txObject.hash;
            });

            if (lastHash && lastHash !== hash) {
                setTimeout(() => {
                    this.fetch(lastHash);
                }, 1000);
            }
        });
    }

    generateUrl(hash) {
        if (hash) {
            return `${this.url}&hash=${hash}`;
        } else {
            return this.url;
        }
    }

}

const inTx = new IncomingTransaction();
inTx.fetch();

出金履歴の取得プログラム

依存関係
"axios": "^0.17.1"
"nem-sdk": "^1.6.2"
実行方法
$ node outgoing.js > outgoing.csv
outgoing.js
const nem = require('nem-sdk').default;
const networkId = nem.model.network.data.mainnet.id;
const Axios = require('axios');
const NEM_EPOCH = Date.UTC(2015, 2, 29, 0, 6, 25, 0);
const TRANSFER = nem.model.transactionTypes.transfer;
const MULTISIG_TRANSACTION = nem.model.transactionTypes.multisigTransaction;
# ここを調べたいものに変える
const ADDRESS = 'NC3BI3DNMR2PGEOOMP2NKXQGSAKMS7GYRKVA5CSZ';

class OutgoingTransactionObject {
    constructor(obj) {
        this.metaId = obj.meta.id;
        this.hash = obj.meta.hash.data;
        this.type = obj.transaction.type;

        if (this.type === TRANSFER) {
            this.recipient = obj.transaction.recipient;
            this.message = obj.transaction.message.payload || '';
            this.timeStamp = obj.transaction.timeStamp;
            this.amount = obj.transaction.amount;
        } else if (obj.transaction.type === MULTISIG_TRANSACTION) {
            const otherTrans = obj.transaction.otherTrans;
            if (otherTrans.type === TRANSFER) {
                this.recipient = otherTrans.recipient;
                this.message = otherTrans.message.payload || '';
                this.timeStamp = otherTrans.timeStamp;
                this.amount = otherTrans.amount;
            }
        }
    }

    view() {
        if (this.amount === undefined) {
            return;
        }
        console.log(`${this.unixtime},${this.metaId},${this.type},${ADDRESS},${this.recipient},${this.message},${this.xemAmount}`);
    }

    get unixtime() {
        const d = new Date(NEM_EPOCH + (this.timeStamp * 1000));
        return d.toLocaleString();
    }

    get xemAmount() {
        return (this.amount / 1000000);
    }
}

class OutgoingTransaction {
    constructor() {
        this.url = `http://alice2.nem.ninja:7890/account/transfers/outgoing?address=${ADDRESS}`;
    }

    fetch(hash) {
        Axios.get(this.generateUrl(hash)).then((res) => {
            let lastHash = null;
            res.data.data.forEach((tx) => {
                const txObject = new OutgoingTransactionObject(tx);
                txObject.view();
                lastHash = txObject.hash;
            });

            if (lastHash && lastHash !== hash) {
                setTimeout(() =>{
                    this.fetch(lastHash);
                }, 1000);
            }
        });
    }

    generateUrl(hash) {
        if (hash) {
            return `${this.url}&hash=${hash}`;
        } else {
            return this.url;
        }
    }
}

const outTx = new OutgoingTransaction();
outTx.fetch();

あとがき

NEM の入出金については 多くの取引所において
全ユーザー同じアドレスへの入金, message での個人識別をしている

そのため、ビットコインとかよりは、トレース機能の実装がしやすいと思った

トランザクションの中身

通常のトランザクション

{
  meta: {
    innerHash: { },
    id: 1598242,
    hash: {
      data: "cd9550b04558b4d8bf1f83b80b7b7b0470136af9fc95e0af26b803bd7561d795"
    },
    height: 1481571
  },
  transaction: {
    timeStamp: 89700440,
    amount: 10000000,
    signature: "2e936e9e4f5924d65bdaad06863e18cd3fb7dd09658c18fcbe4324124e4b2c050e69018721498cad197697ca44310a1b37f0c18c0035e77eecf4c3aa9c714d08",
    fee: 100000,
    recipient: "NC3BI3DNMR2PGEOOMP2NKXQGSAKMS7GYRKVA5CSZ",
    type: 257,
    deadline: 89786840,
    message: {
      payload: "39303266313136643662323931393661",
      type: 1
    },
    version: 1744830465,
    signer: "604a96185b3a51203b8fc27868d5ea2ee88eff568fb5e08de9e0c353b6b9ea07"
  }
}
フィールド 意味
transaction.type トランザクションの種類を示す: 257 の場合は 普通のtransfer transaction
transaction.amount 送金額 1,000,000 = 1XEM)
transaction.recipient 受信者のアドレス
transaction.signer 送信者のPublicKey
message.payload メッセージの中身(UTF8をHexに変換)
message.type 1: 非暗号化, 2: 暗号化

multisig のトランザクション

transaction.type === 4100 のときは
transaction.otherTrans の中身をみれば良い

{  
   meta:{  
      innerHash:{  
         data:"a7233ce3bf9af58105aafa224dcc2b057ed590c16798420ef09665a282676a88"
      },
      id:1577270,
      hash:{  
         data:"5daa1045592c702a632fc177188ebd30ed7142820866330eeb6623a7857cce48"
      },
      height:1476189
   },
   transaction:{  
      timeStamp:89374341,
      signature:"f39fa17ff723d4281fa5f21224c8801b35b2677206fea983893ed40cc3358f0dc199277994a53017dbfe84e05e56e1f408380e90aa0982e8b6cb85f68b5a1607",
      fee:500000,
      type:4100,
      deadline:89377941,
      version:1744830465,
      signatures:[  
         {  
            timeStamp:89374405,
            otherHash:{  
               data:"a7233ce3bf9af58105aafa224dcc2b057ed590c16798420ef09665a282676a88"
            },
            otherAccount:"NAGJG3QFWYZ37LMI7IQPSGQNYADGSJZGJRD2DIYA",
            signature:"01c9645d502085772593b1a262b30644ad00acfe7e3c539ccc9f65254af28c80a9f57c3a44b05dbe3e76cedbb0b3fa6d5e33277369f537a4e0e000d20364c503",
            fee:500000,
            type:4098,
            deadline:89460805,
            version:1744830465,
            signer:"ae6754c70b7e3ba0c51617c8f9efd462d0bf680d45e09c3444e817643d277826"
         }
      ],
      signer:"aa455d831430872feb0c6ae14265209182546c985a321c501be7fdc96ed04757",
      otherTrans:{  
         timeStamp:89374341,
         amount:1800000000,
         fee:100000,
         recipient:"NC3BI3DNMR2PGEOOMP2NKXQGSAKMS7GYRKVA5CSZ",
         type:257,
         deadline:89377941,
         message:{  
            payload:"33343238383963333363663037653539",
            type:1
         },
         version:1744830465,
         signer:"fbae41931de6a0cc25153781321f3de0806c7ba9a191474bb9a838118c8de4d3"
      }
   }
}