bot
Blockchain
NEM

NEMの追跡botを作ってみた

経緯

先日仮想通貨取引所のCoin CheckのNEM約600億円が盗まれるという事件が起きました。

その後の対応として日本の17歳JKが追跡用プログラムを作成したということで話題に話題になりました。(実際はJKではないようですが)

【コインチェック事件】NEM財団所属17歳の天才女子校生ハッカーみなりん氏が流出資金の自動追跡プログラムの開発をして犯人捜査

そこで私も17歳のJKになるために、あるNEMアドレスからの取引を監視し、そのアドレスから送信されたアドレスにもモザイクを送りつけ監視するbotを作成してみました。

環境

Arch Linux
Node.js v9.4.0
nem-sdk 1.6.2
動作確認はテストネットのみで行っています。

GitHubで公開しました。
tohutohu/nem-tracker

はじめに 追跡の仕方

NEMには(お金があれば)誰にでも自由に独自にトークン(NEM用語ではモザイク)を発行することができます。
今回盗まれたと話題になっているNEMも実際はxemという名前のNEMブロックチェーンプラットフォーム上に作られたモザイクの一つです。
(xemはNEMプラットフォームの基軸通貨として扱われているため、私達が作れるモザイクと若干の違いはあります)

モザイクを発行する時の設定次第ではモザイクの移動権限を発行者のみにしたり、後から発行枚数を変更したりすることができます。
またモザイクは受取手によって受取を拒否することができず一方的に送りつけることができ、モザイクを持っているという情報はブロックチェーン上に公開されているため隠したり改ざんすることができません。
そこで、移動権限を発行者のみにしたモザイクを発行し監視対象のアドレスに対してモザイクを送りつけることでそのモザイクを持っているアドレスと危険であると示すことができます。(○○という名前のモザイクが識別子として使われているという情報は他の手段によって共有されていなければなりません)

追跡の方法について知っておくべきことをまとめると
* 誰でもモザイクというものを作れる
* モザイクは一方的に送りつけることができる
* モザイクを持っていることは隠すことができない
* 監視対象の送信先にもモザイクを送り監視対象を広げていく

イメージ画像です
image.png


追記(2018/01/29)

実際の追跡用プログラムで使われたモザイクは移動権限は発行者のみではなかったようです。
モザイク発行時の設定で「徴収」という項目でモザイクの送信の際に手数料を取るように設定できるのですが、その手数料としてまた別のモザイク(復活モザイクとします)を必要とするようにして、もし追跡用モザイクを犯人側でない人が持たされてしまった場合は手動で復活モザイクを送り、追跡用モザイクを返してもらうというようになっているようです。

教えてくださった大空ありすさんありがとうございました。



手順1 モザイクの作成

Botを作る前に追跡用モザイクを作成する必要があります。
追跡用モザイクを作るためには120xem程度が必要になります。
NanoWalletに120xemを準備してください。
Testnetの場合はFaucetやフォーラムでもらいましょう。

NEM Testnet faucet

Paste you address here for beta NEM (Testnet XEM) - Technical Discussion - NEM Forum

xemを手に入れたら、モザイクを作成する前にネームスペースを取得します。
ネームスペースはドメインのようなものだと思ってください。

NanoWalletのサービスからネームスペースの作成を選び、適当にネームスペースを作成します。
Screenshot from 2018-01-27 20-00-37.png
Screenshot from 2018-01-27 20-01-03.png

その後、サービスからモザイクの作成を選びモザイクを作成します。
この時譲渡許可のチェックを外すのを忘れないようにしてください。


追記(2018/01/29)

実際の追跡プログラムでは譲渡許可のチェックはつけたまま、徴収を要求にチェックを入れ別のモザイクを指定していたようです。


Screenshot from 2018-01-27 20-02-22.png

Screenshot from 2018-01-27 20-20-53.png

手順2 botを作る

GitHubで公開しました。
tohutohu/nem-tracker

まずはソースコードを
行数の関係からエラー処理等は省略しています

// PRIVATE_KEYを設定しています
require('dotenv').config()
const nem = require('nem-sdk').default
let network, networkId;

const testnet = true
if (testnet) {
  network = nem.model.nodes.defaultTestnet
  networkId = nem.model.network.data.testnet.id
}else{
  network = nem.model.nodes.defaultMainnet
  networkId = nem.model.network.data.mainnet.id
}

const endpointSocket = nem.model.objects.create('endpoint')(network, nem.model.nodes.websocketPort)
const connector = nem.com.websockets.connector.create(endpointSocket, '')
const endpoint = nem.model.objects.create('endpoint')(network, nem.model.nodes.defaultPort)

const common = nem.model.objects.create('common')('', process.env.PRIVATE_KEY)
const trackerMosaic = nem.model.objects.create('mosaicAttachment')('tohu', 'tracker_mosaic', 1)
let trackerMosaicDefinition

nem.com.requests.namespace.mosaicDefinitions(endpoint, 'tohu')
  .then(res => {
    // 作成したモザイクの定義情報を取得します
    trackerMosaicDefinition = res.data[0]
  })

connector.connect().then(() => {
  nem.com.websockets.subscribe.chain.blocks(connector, res => {
    console.log('new block added! \nsignature: ' +  res.signature, '\n\n')

    res.transactions.forEach(transaction => {
      if (transaction.amount === 0) {
        return
      }

      const sender = nem.model.address.toAddress(transaction.signer, networkId)

      let recipient;
      if (transaction.type === 257) {
        // 通常送金
        recipient = transaction.recipient
      }else if(transaction.type === 4100){
        // マルチシグ
        recipient = transaction.otherTrans.recipient
      }else {
        // 送金以外のときは終了する
        return
      }

      nem.com.requests.account.mosaics.owned(endpoint, sender)
        .then(data => {
          const mosaics = data.data
          mosaics.forEach(mosaic => {
            if(mosaic.mosaicId.namespaceId === 'tohu' && mosaic.mosaicId.name === 'tracker_mosaic') {
              console.log('発見!!!!!')
              sendTrackerMosaic(recipient)
            }
          })
        })
    })
  })
})

const sendTrackerMosaic = address => {
  const transferTransaction = nem.model.objects.create('transferTransaction')(address, 0, 'You are being chased!!')
  transferTransaction.mosaics = [trackerMosaic]
  const transactionEntity = nem.model.transactions.prepare('mosaicTransferTransaction')(common, transferTransaction, trackerMosaicDefinition, networkId)
  transactionEntity.fee = 100000
  nem.model.transactions.send(common, transactionEntity, endpoint)
    .then(res => {
      console.log('モザイクを送信しました')
    })
}

詳しいAPIの詳細やオブジェクトのプロパティは下の参考サイトからご確認ください。

ブロックの追加を検知する

nem-sdkではWebSocketを使ってブロックチェーンにブロックが追加されたということを購読することができます。

const endpointSocket = nem.model.objects.create('endpoint')(network, nem.model.nodes.websocketPort)
const connector = nem.com.websockets.connector.create(endpointSocket, '')

connector.connect().then(() => {
  nem.com.websockets.subscribe.chain.blocks(connector, res => {
    // ブロックが追加されたときの処理
  })
})

resの中身は https://nemproject.github.io/#block を参照

送信者が追跡用モザイクを持っているかを判断する

transaction.amout === 0としているのはnem以外の移動のときはモザイクを送信しないということと、モザイク発行者のモザイク送信に対して無限ループが発生するのを抑えるためです。

手順1で取得したネームスペースが「tohu」、モザイクの名前が「tracker_mosaic」なので以下のようなif文になっています。

if (transaction.amount === 0) {
  return
}

// transactionの情報から送り主のアドレスを取得する
const sender = nem.model.address.toAddress(transaction.signer, networkId)
// 指定したアドレスが持っているモザイクとその量の一覧を取得する
nem.com.requests.account.mosaics.owned(endpoint, sender)
  .then(data => {
    const mosaics = data.data
    mosaics.forEach(mosaic => {
      if(mosaic.mosaicId.namespaceId === 'tohu' && mosaic.mosaicId.name === 'tracker_mosaic') {
        // 追跡用モザイクを持っていたときの処理
      }
    })
  })

モザイクを送信する

ここが一番苦労したところです。
モザイク送信の最小サンプルが以下になると思います。

大切なのはモザイク送信のときはそのモザイクの定義オブジェクトが必要だということです。
モザイクの定義情報は不変なので、一度手動で取得してソースコードにハードコーディングするのもいいかもしれません。

またハマった点としては手数料の部分です。
普通にトランザクションをcreateで作ってprepareで加工すると手数料(transactionEntity.fee)は50000(0.05xem)となっているのですが、メッセージを追加しているため実際に必要な手数料は100000となりそれは手動で設定する必要がありました。
送信するメッセージのサイズが大きくなると更に必要になりそうです。

message: 'FAILURE_INSUFFICIENT_FEE', というメッセージがエラーで出る場合は手数料が足りていません。

const endpoint = nem.model.objects.create('endpoint')(network, nem.model.nodes.defaultPort)
const common = nem.model.objects.create('common')('', process.env.PRIVATE_KEY)
const trackerMosaic = nem.model.objects.create('mosaicAttachment')('tohu', 'tracker_mosaic', 1)
let trackerMosaicDefinition

nem.com.requests.namespace.mosaicDefinitions(endpoint, 'tohu')
  .then(res => {
    // 作成したモザイクの定義情報を取得します
    trackerMosaicDefinition = res.data[0]
  })

const sendTrackerBadge = address => {
  const transferTransaction = nem.model.objects.create('transferTransaction')(address, 0, 'You are being chased!!')
  transferTransaction.mosaics = [trackerMosaic]
  const transactionEntity = nem.model.transactions.prepare('mosaicTransferTransaction')(common, transferTransaction, trackerMosaicDefinition, networkId)
  transactionEntity.fee = 100000
  nem.model.transactions.send(common, transactionEntity, endpoint)
    .then(res => {
      console.log('モザイクを送信しました')
    })
}

手順3 テストしてみる

できたBotを動かして早速テストしてみます。
先程の追跡用モザイクを持っているアカウントから持っていないアカウントに対して、xemを送信してみます。

Screenshot from 2018-01-27 21-11-15.png

送信が承認されるとコマンドラインに
image.png
と出ました。

送信先のアカウントのウォレットを見てみると
image.png

image.png

送金されてきた1xemの後に追跡用コインが送られてきたことがわかります。

最後に

いかがでしたでしょうか。
かなり長くなってしまいましたが、コーディング自体は2時間ほどでできました。
使いやすいAPIが提供されている点は素晴らしいなと思いました。

追跡BotだけでなくXEMやモザイクを用いたプロダクトの参考になれば幸いです。

参考サイト

QuantumMechanics/NEM-sdk: NEM Developer Kit for Node.js and the browser

New Economy Movement(NEM) APIマニュアル和訳 | PR1SM

NEM-sdkを使ってみる - tadajam's blog

Node.jsとNEM-sdkを利用して仮想通貨XEMのばらまきサイトを超簡単に作る。 - Qiita

NEM のトランザクションを作る - Qiita

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

私のNEM アドレス
NAWSMV-EGRMSI-IGUENL-E7OCI4-AWI3CE-72BPVP-6X5L