Ripple ノードを Docker で構築しテスト送金してみる

「ホットウォレットチーム」(採用ページでは「ノード運用」として募集しています)のハトネコエです。

1. ホットウォレットとは?

「ホットウォレット」 というのは、あまりなじみのないワードかもしれません。
「コールドウォレット」 の対となるもので、コールドウォレットがインターネットから完全に分断されたオフライン環境にあるのに対し、
ホットウォレットは、ブロックチェーン情報の同期(ブロック同期)やトランザクションの送信をおこなうため、P2Pネットワーク上に存在します。

ホット"ウォレット" という名前なので、ウォレット情報(秘密鍵情報)を持つ仮想通貨ノードを言うことが一般的ですが、
便宜上、コインチェック社内では、P2Pネットワーク上にありブロック同期をおこなう仮想通貨ノードはすべてホットウォレットと呼び、ホットウォレットチームが管理しています。

ホットウォレットが正常に稼働していないと、お客様の送金が出来なくなってしまいますし、
コールドウォレットからの送金も(最終的には raw transaction をどこかの仮想通貨ノードから送金しなくてはならないため)出来なくなってしまいます。
なかなかに大事なチームなのです。

さて、そのチームでよくおこなうタスクとして、仮想通貨クライアントのバージョンアップがあります。
新規バージョンのノードを構築し、送金に不具合が発生しないか検証するのです。

この記事では、それにならい、
Docker を使って Ripple ノードを構築し、testnet での送金がおこなえるのか試してみようと思います。

2. Dockerfile の作成

公式で案内されているインストール方法に従って、
rippled のインストールがおこなえるよう Dockerfile を作成します。

出来上がったものを用意しました。こちらのリポジトリをご覧ください。

このリポジトリを clone したのち README にある通り、
make init したあとに make run をすれば、rippled と後述する ripple-lib が起動します。

Dockerfile は、このようになりました。

FROM ubuntu:18.04

ARG rippled_ver=1.2.2

RUN \
  apt-get update && \
  apt-get upgrade -y && \
  apt-get install -y yum-utils alien

RUN rpm -Uvh https://mirrors.ripple.com/ripple-repo-el7.rpm

RUN yumdownloader --enablerepo=ripple-stable --releasever=el7 rippled-${rippled_ver}

RUN \
  rpm --import https://mirrors.ripple.com/rpm/RPM-GPG-KEY-ripple-release && \
  rpm -K rippled-${rippled_ver}*.rpm && \
  alien -i --scripts rippled-${rippled_ver}*.rpm && \
  rm rippled-${rippled_ver}*.rpm

RUN cp /opt/ripple/bin/rippled /usr/local/bin/

# Config files
COPY files/rippled.cfg /opt/ripple/etc
COPY files/validators.txt /opt/ripple/etc

CMD [ "rippled", "--conf", "/opt/ripple/etc/rippled.cfg" ]

2-1. バージョン指定

インストールする rippled のバージョンが指定できるよう、
yumdownloader を実行する箇所では、単に rippled でなく rippled-${rippled_ver} としています。
これで docker build 時に、--build-arg オプションを使ってインストールするバージョンを指定できます。

例:

docker build . --build-arg rippled_ver=1.2.2 -t rippled_testnet:latest

2-2. config ファイルの更新

Bitcoin であれば、config ファイルに testnet=1 と記述するだけで testnet になりましたが、
Ripple の場合は少しだけ面倒です。

rippled.cfgvalidators.txt のそれぞれを変更する必要があります。
それぞれのファイルで「To use the XRP test network」とコメントアウトされている箇所に説明があります。

rippled.cfg には

[ips]
r.altnet.rippletest.net 51235

の2行を追加。

validators.txt

[validator_list_sites]
https://vl.ripple.com

[validator_list_keys]
ED2677ABFFD1B33AC6FBC3062B71F1E8397C1505E1C42C64D11AD1B28FF73F4734

をコメントアウトし、

# [validator_list_sites]
# https://vl.altnet.rippletest.net
#
# [validator_list_keys]
# ED264807102805220DA0F312E71FC2C69E1552C9C5790F6C25E3729DEB573D5860

とコメントアウトされている箇所をコメントでなくします。

Dockerfile で COPY をおこなっている箇所があるのは、
上の更新がおこなわれた状態の config ファイルを使用するためです。

rippled.cfg はその他に port_rpc_admin_local や node_size の設定にも変更を加えています。最初にご紹介したリポジトリをご参照ください)

2-3. rippled の起動

docker run 時にデフォルトでは rippled が起動するようにしたいので、
CMD 命令を使用しています。

docker build 後に、以下のように docker run コマンドを実行することで、
rippled サーバーが 5005 番ポートを使いつつ立ち上がり、
ホストPC側からは 15005 ポートで rippled の API へアクセスできるようになります。

rippled はメモリを多く消費するので、 Docker for Mac をお使いの場合は、
あらかじめ設定画面にて、使用できるメモリ容量を増やしておくことをおすすめします。

Docker for Mac の設定画面のスクリーンショット
Docker for Mac の設定画面にて、メモリ容量を増やします

メモリが足りなくなると rippled は自動で再起動してしまうので、できるだけ大きな値を指定してください。

docker run \
    -d \
    --name rippled_testnet_01 \
    -p 15005:5005 \
    rippled_testnet:latest

ホストPCから以下のコマンドを試し、どこまで Ledger が同期されたか(Bitcoinで言うところのブロック高)を確認できます。
同期が終わるまでは 5 分程度かかります。

curl -X POST http://127.0.0.1:15005 -d '{"method": "ledger_closed"}'

# レスポンス例
# {"result":{"ledger_hash":"D42E731D478FC2826EEE196F6E2D7824B7C9F6D14EAE1906808F56ED85EC0C5E","ledger_index":17885519,"status":"success"}}

公式のテストネットノードがあるので、そこの ledger_index と同じ程度の値になっているか確認できると良いと思います。

curl -X POST https://s.altnet.rippletest.net:51234 -d '{"method": "ledger_closed"}'

# レスポンス例
# {"result":{"ledger_hash":"3097AB83E4FD0E097FD88E4C4C0A79D3A0D792E9A6FFB4389884F18F06348A5D","ledger_index":17885520,"status":"success"}}

3. XRP を送金してみる

3-1. testnet 用のコインを取得

XRP Test Net Faucet から、 testnet 用の XRP を取得します。

https://developers.ripple.com/xrp-test-net-faucet.html

サイトを訪れると、「Generate credentials」のボタンがありますので、そこをクリックして、

  • テスト用のコインが付与されたウォレットのアドレス
  • そのアドレスの Secret (送金に必要)

の情報を取得します。

今回はアドレスが「rKXCHMM9U7qeKPeNSb9e7tVtojghjQVmvh」、
Secret が「shfXBTqLeoqDzkpDpGd9q8Q1uB4ZW」です。
(※ testnet なので Secret を記載しましたが、mainnet であなたが扱うアドレスの Secret を公開するのは絶対にいけません!)
testnet で使える 10,000 XRP が付与されたようです。

コインが手に入っているか確認してみましょう。
account_info API を用います。

curl -X POST http://127.0.0.1:15005 -d '{"method": "account_info", "params": [{"account": "rKXCHMM9U7qeKPeNSb9e7tVtojghjQVmvh"}]}'

このようなレスポンスが返ります。

{
  "result": {
    "account_data": {
      "Account": "rKXCHMM9U7qeKPeNSb9e7tVtojghjQVmvh",
      "Balance": "10000000000",
      "Flags": 0,
      "LedgerEntryType": "AccountRoot",
      "OwnerCount": 0,
      "PreviousTxnID": "DD725AA909B9E7FEA336FA712DCEDB2833D594E1B8097BF73B376601731376F0",
      "PreviousTxnLgrSeq": 17753819,
      "Sequence": 1,
      "index": "6377A7C1BE5D2FF4C3BC85A50F4F8920200433B5273037E4FDF42898F41B439F"
    },
    "ledger_current_index": 17753988,
    "status": "success",
    "validated": false
  }
}

レスポンスに "Balance": "10000000000" とあるのが見て取れます。

「10,000 XRP なのに残高が 10000000000?」と不思議に思った方もいるかも知れません。
API上では、drop の単位を基準としています。

  • 1 drop = 0.000001 XRP
  • 1 XRP = 1,000,000 drop

の関係性です。

10,000 XRP = 10,000,000,000 drop ですから、
"Balance": "10000000000" と表示されたのも納得できますね。

3-2. トランザクションを確認

10,000 XRP を受け取ったことは、送金トランザクションとしてしっかり記録されています。

account_tx API で見てみましょう。

curl -X POST http://127.0.0.1:15005 -d '{"method": "account_tx", "params": [{"account": "rKXCHMM9U7qeKPeNSb9e7tVtojghjQVmvh"}]}'

このようなレスポンスが返ります。

{
  "result": {
    "account": "rKXCHMM9U7qeKPeNSb9e7tVtojghjQVmvh",
    "ledger_index_max": 17754201,
    "ledger_index_min": 17753748,
    "status": "success",
    "transactions": [
      {
        "meta": {
          "AffectedNodes": [
            {
              "ModifiedNode": {
                "FinalFields": {
                  "Account": "rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe",
                  "Balance": "46297609737790846",
                  "Flags": 0,
                  "OwnerCount": 0,
                  "Sequence": 362041
                },
                "LedgerEntryType": "AccountRoot",
                "LedgerIndex": "31CCE9D28412FF973E9AB6D0FA219BACF19687D9A2456A0C2ABC3280E9D47E37",
                "PreviousFields": {
                  "Balance": "46297619737790858",
                  "Sequence": 362040
                },
                "PreviousTxnID": "9CD707224F5C904E7F7B86EFB1FFBECA40E292D1285B22E979675EEEA94A32CF",
                "PreviousTxnLgrSeq": 17753816
              }
            },
            {
              "CreatedNode": {
                "LedgerEntryType": "AccountRoot",
                "LedgerIndex": "6377A7C1BE5D2FF4C3BC85A50F4F8920200433B5273037E4FDF42898F41B439F",
                "NewFields": {
                  "Account": "rKXCHMM9U7qeKPeNSb9e7tVtojghjQVmvh",
                  "Balance": "10000000000",
                  "Sequence": 1
                }
              }
            }
          ],
          "TransactionIndex": 1,
          "TransactionResult": "tesSUCCESS",
          "delivered_amount": "10000000000"
        },
        "tx": {
          "Account": "rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe",
          "Amount": "10000000000",
          "Destination": "rKXCHMM9U7qeKPeNSb9e7tVtojghjQVmvh",
          "Fee": "12",
          "Flags": 2147483648,
          "LastLedgerSequence": 17753822,
          "Sequence": 362040,
          "SigningPubKey": "02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC",
          "TransactionType": "Payment",
          "TxnSignature": "304402202B45F5542CC8BC844E4D0CDCA63B41958F51CFB39D0CD5F062FC8B9DFF6E07A6022046AD4AC2763B3BBB061F27E080276A900C6D9DD4424930933EC8BFA7B59D483A",
          "date": 605878520,
          "hash": "DD725AA909B9E7FEA336FA712DCEDB2833D594E1B8097BF73B376601731376F0",
          "inLedger": 17753819,
          "ledger_index": 17753819
        },
        "validated": true
      }
    ]
  }
}

長いですね。

特に注目してほしいのが、最後の tx キーのデータです。

{
  "Account": "rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe",
  "Amount": "10000000000",
  "Destination": "rKXCHMM9U7qeKPeNSb9e7tVtojghjQVmvh",
  "Fee": "12",
  "Flags": 2147483648,
  "LastLedgerSequence": 17753822,
  "Sequence": 362040,
  "SigningPubKey": "02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC",
  "TransactionType": "Payment",
  "TxnSignature": "304402202B45F5542CC8BC844E4D0CDCA63B41958F51CFB39D0CD5F062FC8B9DFF6E07A6022046AD4AC2763B3BBB061F27E080276A900C6D9DD4424930933EC8BFA7B59D483A",
  "date": 605878520,
  "hash": "DD725AA909B9E7FEA336FA712DCEDB2833D594E1B8097BF73B376601731376F0",
  "inLedger": 17753819,
  "ledger_index": 17753819
}

ここを見ると、いつ、どのアドレスからどのアドレスにいくら送られ、
それがどの ledger_index に記録されているかがわかります。

"hash": "DD725AA909B9E7FEA336FA712DCEDB2833D594E1B8097BF73B376601731376F0" とありますね。
これがトランザクションIDにあたります。

"date": 605878520 とありますが、これは 2000-01-01 00:00 UTC から何秒経ったかなので、
UNIXTIME で言うと、 946684800 + 605878520 = 1552563320 にあたります。

rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe から rKXCHMM9U7qeKPeNSb9e7tVtojghjQVmvh へ 10,000 XRP 送金され、
手数料として 12 drop (= 0.000012 XRP) かかっていることがわかります。

3-3. 送金する

送り主のアドレスが rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe であることがわかりましたので、
そのアドレスに受け取ったテストコインのいくらかを送り返してみましょう。

sign API が昔は使われていましたが、
現在はこの API を使うのはセキュリティの観点から非推奨になっており、
ripple-lib を用いて署名済みトランザクションを作成したのち、
submit API を用いてトランザクションをネットワークに送信する方法が推奨されています。

P2Pネットワーク内に存在するノードに Secret 情報を渡してしまうのはセキュアとは言えないので、
ネットワーク的に強く制限されたコンピューターへ Secret を渡し、署名済みトランザクションを作成してもらおうという考えですね。

ripple-lib を使うための実装については省きまして、
今回は、最初にご紹介したリポジトリを使って ripple-lib を走らせているものとします。

まずは 3-1. で使った account_info API を用いて、Sequence を確認します。

curl -X POST http://127.0.0.1:15005 -d '{"method": "account_info", "params": [{"account": "rKXCHMM9U7qeKPeNSb9e7tVtojghjQVmvh"}]}'

レスポンスはこうでした。

{
  "result": {
    "account_data": {
      "Account": "rKXCHMM9U7qeKPeNSb9e7tVtojghjQVmvh",
      "Balance": "10000000000",
      "Flags": 0,
      "LedgerEntryType": "AccountRoot",
      "OwnerCount": 0,
      "PreviousTxnID": "DD725AA909B9E7FEA336FA712DCEDB2833D594E1B8097BF73B376601731376F0",
      "PreviousTxnLgrSeq": 17753819,
      "Sequence": 1,
      "index": "6377A7C1BE5D2FF4C3BC85A50F4F8920200433B5273037E4FDF42898F41B439F"
    },
    "ledger_current_index": 17753988,
    "status": "success",
    "validated": false
  }
}

"Sequence": 1 とありますね。この値を使います。

ripple-lib の sign API のドキュメント を参考に、
以下のようにリクエストします。

# 注: ripple-lib へのリクエストです。 -H 'Content-Type:application/json' をお忘れず
curl -X POST -H 'Content-Type:application/json' http://127.0.0.1:50080/create_tx -d '{"secret": "shfXBTqLeoqDzkpDpGd9q8Q1uB4ZW", "tx_json": {"TransactionType": "Payment", "Account": "rKXCHMM9U7qeKPeNSb9e7tVtojghjQVmvh", "Destination": "rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe", "Amount": "100", "Fee": "20", "Sequence": "1"}}'

"Sequence": "1""secret": "shfXBTqLeoqDzkpDpGd9q8Q1uB4ZW" である
"Account": "rKXCHMM9U7qeKPeNSb9e7tVtojghjQVmvh" から、
"Destination": "rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe" へ 100 drop の送金を、手数料 20 drop でおこなおうとしています。

リクエストすると、このようなレスポンスが返ってきます。

{
  "signedTransaction": "12000024000000016140000000000000646840000000000000147321026BA68EFEB1029547D7BE7D712C67CDBCFDF1E47ABE05655D2923BA879303A82D74473045022100D3F6EC1F6171DA8D470D3A5649BF740A47738F18D4229A0725393D527F7E223E0220120CE33797C90132EDED2175D53274485C36124D4851E46CE84A49340272E3088114CB3EB2F7A684F331489B53E8F13ABBECCC5B26A88314F667B0CA50CC7709A220B0561B85E53A48461FA8",
  "id": "0ABE35BAC5BD1223BCD7B9508B3EC497ADEF7B0BEC084DA2355DFEC048728D74"
}

この signedTransaction で示された値が、署名済みトランザクションのデータです。(id はトランザクションIDです)
submit API を使い、ネットワークに送信します。

curl -X POST http://127.0.0.1:15005 -d '{"method": "submit", "params": [{"tx_blob": "12000024000000016140000000000000646840000000000000147321026BA68EFEB1029547D7BE7D712C67CDBCFDF1E47ABE05655D2923BA879303A82D74473045022100D3F6EC1F6171DA8D470D3A5649BF740A47738F18D4229A0725393D527F7E223E0220120CE33797C90132EDED2175D53274485C36124D4851E46CE84A49340272E3088114CB3EB2F7A684F331489B53E8F13ABBECCC5B26A88314F667B0CA50CC7709A220B0561B85E53A48461FA8"}]}'

このようなレスポンスが返ってきました。

{
  "result": {
    "engine_result": "tesSUCCESS",
    "engine_result_code": 0,
    "engine_result_message": "The transaction was applied. Only final in a validated ledger.",
    "status": "success",
    "tx_blob": "12000024000000016140000000000000646840000000000000147321026BA68EFEB1029547D7BE7D712C67CDBCFDF1E47ABE05655D2923BA879303A82D74473045022100D3F6EC1F6171DA8D470D3A5649BF740A47738F18D4229A0725393D527F7E223E0220120CE33797C90132EDED2175D53274485C36124D4851E46CE84A49340272E3088114CB3EB2F7A684F331489B53E8F13ABBECCC5B26A88314F667B0CA50CC7709A220B0561B85E53A48461FA8",
    "tx_json": {
      "Account": "rKXCHMM9U7qeKPeNSb9e7tVtojghjQVmvh",
      "Amount": "100",
      "Destination": "rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe",
      "Fee": "20",
      "Sequence": 1,
      "SigningPubKey": "026BA68EFEB1029547D7BE7D712C67CDBCFDF1E47ABE05655D2923BA879303A82D",
      "TransactionType": "Payment",
      "TxnSignature": "3045022100D3F6EC1F6171DA8D470D3A5649BF740A47738F18D4229A0725393D527F7E223E0220120CE33797C90132EDED2175D53274485C36124D4851E46CE84A49340272E308",
      "hash": "0ABE35BAC5BD1223BCD7B9508B3EC497ADEF7B0BEC084DA2355DFEC048728D74"
    }
  }
}

送ったトランザクションがすぐに受け付けられたようです。

3-4. 送金できたことを確認する

再び account_info API を使い、 残高が減っているかどうか確認します。

curl -X POST http://127.0.0.1:15005 -d '{"method": "account_info", "params": [{"account": "rKXCHMM9U7qeKPeNSb9e7tVtojghjQVmvh"}]}'

リクエストすると、以下のように返りました。

{
  "result": {
    "account_data": {
      "Account": "rKXCHMM9U7qeKPeNSb9e7tVtojghjQVmvh",
      "Balance": "9999999880",
      "Flags": 0,
      "LedgerEntryType": "AccountRoot",
      "OwnerCount": 0,
      "PreviousTxnID": "0ABE35BAC5BD1223BCD7B9508B3EC497ADEF7B0BEC084DA2355DFEC048728D74",
      "PreviousTxnLgrSeq": 17857969,
      "Sequence": 2,
      "index": "6377A7C1BE5D2FF4C3BC85A50F4F8920200433B5273037E4FDF42898F41B439F"
    },
    "ledger_current_index": 17862845,
    "status": "success",
    "validated": false
  }
}

"Balance": "9999999880" で示されるように、しっかり送金額と手数料の合計である 120 drop ぶん、残高が減っていることが確認できますね!

また、 "Sequence": 2 となっていて、 Sequence が 1 増えていることがわかります。

アカウントで送金が行われるごとに Sequence は 1 ずつ増えていきます。
XRP のトランザクションに Sequence 情報を含めることは必須で、
同じアカウントからの同じ Sequence のトランザクションは Ledger に取り込まれませんから、
これにより、アカウントから同じ送金を誤って二度おこなう(二重送金する)ことを防ぐことが出来ています。

4. おわりに

以上、テストネットにおける XRP の送金についてでした。

一般的なウォレットアプリケーションを使わず、
このように CLI(コマンドラインインターフェイス)上での送金をおこなってみると、
トランザクションがどのようなデータを持つのか知ることができ、通貨への理解も深まると思います。

いろいろな仮想通貨に触れると、各通貨における興味深い仕様の違いを知られて、おもしろいですよ。
コインチェックでは様々なポジションでメンバーを募集中です。
まずは話を聞いてみたい、という方も下記のリンクからぜひ、ご応募ください。