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 (送金に必要)

の情報を取得します。

f:id:nekonenene:20190322165041p:plain

今回はアドレスが「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 の単位を基準としています。

の関係性です。

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コマンドラインインターフェイス)上での送金をおこなってみると、
トランザクションがどのようなデータを持つのか知ることができ、通貨への理解も深まると思います。

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

Bitcoinのtransactionの署名検証をscriptから理解する

ブログを書くにあたって

この度、コインチェックでは技術ブログを始めることになりました。これから不定期更新ではありますが、このブログでは主に仮想通貨やblockchainの技術周りについての情報発信を行っていけたらなと思います

 

ブログの初回を担当します、仮想通貨開発グループの善方(ぜんぽう)と申します。

 

では、早速始めて行きましょう!

 

初回となる今回は、Bitcoinのtransactionの署名検証の仕組みについて説明します。この記事を通じて、transactionの署名検証は一体なにを行なっているのか理解をし、transactionの本質を理解して行きましょう!!

 

transactionに関する記事を見たり、関連する本を参照すると、秘密鍵で署名を行なってBitcoinを自分の物にする、といった説明がされていると思います。一般的に、自分が所有する秘密鍵でtransactionに署名を行なって、そのBitcoinが自分の物であるという証明を行うのですが、実際にBitcoinのノードを触ったことがある方なら分かると思いますが、ノードに対して送金コマンドを実行すると、transactionに対して勝手に署名検証が実行されます。今回は、ここの署名検証の仕組みについて、Bitcoinscriptの仕組みまで遡って説明します。

 

tranasctionのデータ構造

図 1

 

まずは、transactionの中身から見て行きましょう。transactionの中身は図 1の様になっています。transactionは、input群(txins)とoutput群(txouts)、その他transactionに関わる情報を保持しています。Bitcoinは、utxo(unspent transaction output)をというoutputを参照して送金処理を行うのですが、outputとそのoutputを使用するinputは接続関係を持っており、outputにはBitcoinをlockする為のscriptであるscript_pubkeyが、inputにはBitcoinをunlockするscriptであるがscript_sig含まれています。

 

図 2

 

  図 2においては、transaction1のoutput群の中のNo.0のoutputを参照する形で、transaction2のinputにoutputの内容が取り込まれています。

 

ここでは、utxoであるtransaction 1に対する署名の検証を行なっている訳なのですが、署名の検証はどういうもので、一体どの様に行なっているのでしょうか?

 

scriptとは何か?

Bitcoinにおいては、スクリプト言語を利用して署名の検証を行なっており、署名の検証とは、自分のutxoに対してデジタル署名が正しい物かどうかを、公開鍵のハッシュ値などを用いてscriptの計算により検証する事をさします。

 

outputのscript_pubkeyにはデジタル署名を検証する命令があり、inputのscript_sigにはデジタル署名をそれぞれスクリプト言語で記述します。

 

ここでいうスクリプト言語とは、一体なんなのでしょうか?

 

Bitcoinにおいては、scriptと呼ばれるスクリプト言語の一種をtransaction内に記述する事で、上記の様なデジタル署名の検証を行なっており、その部分がscript_sigやscript_pubkeyのフィールドに記載されています(図 1を参照)。

 

scriptは、stackと呼ばれる様なデータ構造をもち、データを、先入れ、後出し、で保持する様な挙動を示します。

 

図 3

 

図 3を見てください。まず最初に空のstackがあり、そのstackに対してdataを一つ追加します。すると、空のstackにdata1が追加されます。そのスタックにdataをまた追加すると、data1の上にdata2が乘る形で追加され、さらにそのstackからdataを取り出すと、data2が取り出される様な挙動を示します。

 

図 4

 

Bitcoinでは、このscriptを使用して署名の検証を行なっており、デジタル署名の検証では、scriptの実行後、stackに1(true)が残れば署名検証が成功(success)、0(false)や2つ以上のdataが残った場合や、stackが空の場合は、署名検証が失敗(failure)となります。(図 4)

 

OP_codeを理解しよう

上記で説明したscriptには、OP_code(オペコード)と呼ばれる命令が使用されます。例えば、Bitcoinのデジタル署名の検証で使用されるop_codeは下図の様になります。

 

図 5

 

一体何の事かわかりませんね。順を追って説明します。

 

Bitcoinのscript_sigやscript_pubkeyは、scriptで書かれていて、それは上図の様なop_codeと呼ばれるものであると説明しました。今回は、P2PKH(Pay to Public Key Hash)と呼ばれるscriptの中身を見てみます。

 

図 6

 

6を見て頂くと分かる通り、script_pubkeyやscript_sigの中身は、図 5のOP_codeでプログラムさている事が分かると思います。これから、図 6のscriptと図 7のstackの図を参考にして、transaction上でなされているデジタル署名の検証のscriptの実行手順を見ていきたいと思います。

 

<デジタル署名の実行手順>

①. script_pubkeyとscript_sigを連結

②. <signature> <public key>をstackへ移動

③. OP_DUPを使って、<public key> を複製

④. OP_HASH160を使って、<public key> をハッシュ化

⑤. check scriptより、stackへ新たに<public key hash>を追加

⑥. ハッシュ関数によって作成された<public key hash>と、script_pubkeyに元々あった<public key hash>を比較

⑦. OP_CHECKSIGによって、script全体の検証を行い、有効であれば ”1” を返す

 

①. script_pubkeyとscript_sigを連結 (図 6, 図 7 ①) 

script_pubkeyscript_sig、それぞれを連結して、図 6のcheck scriptの様に連結する。デジタル署名<signature>と公開鍵<public key>を使ってutxoをunlockする準備をする。この連結したものを、左から順番にstackに移動していき、実行していく。

chaeck scriptの状態: <signature> <public key> OP_DUP OP_HASH160 <public key hash> OP_EQUALVERIFY OP_CHECKSIG

 

②. <signature> <public key>をstackへ移動 (図 7 ②)

chaeck scriptの状態: OP_DUP OP_HASH160 <public key hash> OP_EQUALVERIFY OP_CHECKSIG

 

③. OP_DUPを使って、<public key> を複製(図 7 ③)

OP_DUPは、stackの1番上に乗っている変数を複製する関数( 5を参照)

chaeck scriptの状態: <public key hash> OP_EQUALVERIFY OP_CHECKSIG

 

④. OP_HASH160を使って、<public key> をハッシュ化(図 7 ④)

OP_HASH160は、stackの1番上に乗っている変数を160bitでハッシュ化( 5を参照) 

chaeck scriptの状態: <public key hash> OP_EQUALVERIFY OP_CHECKSIG

 

⑤. chaeck scriptより、stackへ新たに<public key hash>を追加(図 7 ⑤)

chaeck scriptの状態:  OP_EQUALVERIFY OP_CHECKSIG

 

⑥. ハッシュ関数によって作成された<public key hash>と、script_pubkeyに元々あった<public key hash>を比較(図 7 ⑥)

chaeck scriptの状態:  OP_CHECKSIG

 

⑦. OP_CHECKSIGによって、script全体の検証を行い、有効であれば ”1” を返す(図 7 ⑦)

ここで、”1(true)”がstackに残っていれば、署名検証が成功したと見なされ、stackに”0(false)や2つ以上のdataが残った場合や、stackが空だった場合”、署名検証が失敗したと見なされます。

chaeck scriptの状態: なし

 

図 7

 

この様な手順を追って、BitcoinはscriptのOP_codeを実行して、transactionのutxoにデジタル署名の検証を行なっています。

今回は、P2PKH(Pay to Public Key Hash)と呼ばれるscriptを見てきましたが、他にも、multi signatureのscriptやP2SH(Pay to Script Hash)のscriptなど、複数種類があります。大まかな違いは、multi signatureであれば、<signature>と<public key>が複数個になる事(n of mのnとmの数で個数が前後します)や、P2SHでは、multi signatureのscriptでは、scriptのbyte数が大きくなってしまう問題から、redeem scriptと呼ばれるscriptのある部分のハッシュを持たせるなど、scriptの使用が異なります。考え方は、今説明したP2PKHのscriptとほぼ同じです。

 

segwitを理解する

script_sigには<signature> と <public key>の2つの情報が入っていました。

 

<signature>は、秘密鍵によって署名された署名情報が入っています。 ここで、もう一度、下図のtransactionの構造をみて下さい。

 

図 8

 

transactoinに署名をするというのは、図 8の水色の部分である署名対象データのハッシュ値を生成し、そのハッシュ値秘密鍵で署名を実行し、その署名データをscript_sigにセットすることを言います。ここの記事で言う所の、<signature>ですね。しかし、署名データがセットされたscript_sigには署名する事が出来ないので、ハッカーにとってみたら、ここだけが唯一、transactoinのdata構造で改ざんをする事が出来るポイントになります。script_sig以外の部分で改ざんをしてしてしまうと、署名検証時エラーになってしまうので、transactionを改ざんすることが出来ません。

 

しかしながら、transactionのidであるtx_idは、script_sigも含めた図. 8のtransactionの全データから生成されたハッシュ値であり、もしscript_sigをハッカーに改ざんされてしまった場合、tx_idの値も別の値になってしまいます。つまり、署名検証に影響を与えない様にscript_sigを改ざんすれば、tx_idの異なるtransactionを作成する事が出来ます。これがいわゆる、transactionのmalleabilityと呼ばれるものです。

 

このtransactionのmalleability問題を解決しようと実装されたのが、segregated witness(segwit)です。segwitでは、transactionのinputからscript_sigを分離し、witnessという別のデータ領域に移動する事でtransactionのmalleability問題を解決します。

図 9

 

segwitのtransactionは、署名データがinputから分離され、tx_idの算出にscript_sigを使用しなくなるので、script_sigを改ざんによるtransactionのmalleabilityを解決し、未署名なtransactionでもtx_idを算出できる様になります。

 

segwitは未署名な状態でもtransactionのtx_idが算出する事が出来るので、未署名なtransactionをinputにした、transactionのチェーンを作る事ができます(厳密にはオフチェーン上でtransactionのやり取りを行うのですが、話が長くなるので別途記事を書く関係から、この様に表現しています)。この、未署名なtransactionのtx_idを算出する事が出来ると言う技術が、Lightning Networkpayment channelの部分で利用されていますpayment channelでは、transctionのやり取りをする為にチャネルをオープンにする必要があり、チャネルをオープンにする為には、未署名であるtransactionのtx_idが必要になります。

 

payment channelやLightning Networkについては、後日にブログを書きます。

 

 

この記事のまとめ

  • transactionのデジタル署名の検証には、op_codeを並べたscriptが使用されている
  • transactionのscript_sigの部分には署名出来ないので、transactionのmalleability問題が起る
  • transactionのmalleability問題を解決する為に開発されたのが、segwitである
  • segwitの未署名でもtx_idを作成できるよ、という特性がpayment channelやLightning Networkの技術基盤になっている

 

参考: ブロックチェーン・プログラミング 仮想通貨入門(著者: 山崎重一郎、 安土茂亨、 田中俊太郎)

 

コインチェックでは様々なポジションでメンバーを募集中です。まずは話を聞いてみたい、という方も下記のリンクから是非ご応募ください。

 

corporate.coincheck.com