本記事は Blockchain Advent Calendar 2019 22日目の記事です。
2019年夏に、フレセッツ株式会社でインターンとして働いていました。
そこでTrezorでXRPのマルチシグトランザクションを作成するファームウェアの開発をしました。
開発をする上で、公式ドキュメント以外に頼れる情報がなく、ドキュメントも分かりづらかったので、インターン終了レポートも兼ねて、本記事にまとめます。
やったこと
ビットコインをはじめとする暗号通貨は、秘密鍵が盗まれるとコインを第三者が送金できてしまうので、秘密鍵が盗まれないような手法として、秘密鍵を保存・操作するハードウェア「ハードウェアウォレット」が登場しました。
今回使ったのはハードウェアウォレット「Trezor」
https://trezor.io/
機能・特徴としては
- オープンソース
- GNU GPL/LGPLライセンス
- 複数の通貨に対応
- BIP32(HDウォレットのやつ)
- BIP39(単語バックアップのやつ)
- U2F FIDO
- SSHログイン
モデル別の特徴
- Trezor One
- モノクロ有機EL
- C言語で実装されてる
- Cortex M3 CPU
- XRPに非対応
- Trezor T
- フルカラータッチスクリーン
- Pythonで実装され、MicroPythonで実行される
- Cortex M4 CPU
- XRPの単純な送金だけ対応
Trezor OneはXRPを送金する機能を備えておらず、Trezor Tは単純なシングルシグネチャの送金のみに対応し、マルチシグネチャ(マルチシグ)には対応していません。
XRPのトランザクションを発行する計算パワーは備えているはずなので、実装し、プルリクエストを送ることになりました。
Bitcoin, Bitcoin Cash, Litecoinなど、Bitcoin派生のコインだけでなく、Ethereum, Stellar, NEMなどのコインも対応しているにも関わらず、なぜ時価総額3位のXRPには対応していないので、嫌な予感を感じながら開発をしていました。
結局Trezor Oneはシングルシグの送金しか実装できませんでした。その理由は後述。
Trezor T用のプルリクエスト: https://github.com/trezor/trezor-firmware/pull/636
Trezor One用のブランチ: https://github.com/yuki-js/trezor-firmware/tree/ripple-for-legacy
なお、プルリクエストはまだマージされてません
XRPの機能
XRP Ledgerには、XRPの送金だけでなく、エスクローや取引や、マスター秘密鍵とは別の鍵を使って送金できるようにする機能もあります。全部解説するのは無理なので下記サイトを見てください。
https://xrpl.org/transaction-types.html
署名にはBitcoinでおなじみsecp256k1だけでなく、Ed25519も使えます。
銀行などのお堅い場面でも使えるように色々な機能が用意されています。
アドレス生成
アドレス生成はほとんどBitcoinのそれと同じです。
- 秘密鍵から公開鍵を導出
- 公開鍵をSHA256でハッシュ化
- それをRIPEMD160でハッシュ化
- バージョンバイト
0x00
をつけます - SHA256を二回かけた先頭4バイトをチェックサムとして最後につけます
ここまでは一緒ですが、Base58エンコードを施す時の、英数字のペアが異なります。
Bitcoinは123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz
ですが、
XRPではrpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz
です。
トランザクションのシリアライズ
XRP Ledgerのトランザクション情報はキー・バリュー形式になっており、バリューには型があります。
キーとバリューの組をフィールドと言います。
フィールドや型、エラーコードなどはJSONファイルで定義されます。これを定義ファイルと呼びます。あとで使います。
https://github.com/ripple/ripple-binary-codec/blob/master/src/enums/definitions.json
まず、トランザクションの情報をJSONで記述します。これは送金をおこなうPayment
トランザクションの例です。
{
"TransactionType" : "Payment",
"Account" : "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Destination" : "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
"Amount" : {
"currency" : "USD",
"value" : "1",
"issuer" : "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"
},
"Fee": "12",
"Flags": 2147483648,
"Sequence": 2,
}
さて、署名するには、このJSON文字列を一意な、できる限り短い、直列なバイト列に直さなければなりません(シリアライズ)。JSONだと、「インデントのスペースは2個vs4個vsタブ論争」や、フィールドの並び順、JSONなどのテキストは情報を保存する効率が悪いなどの問題がありますからね!
シリアライズは2回行われます。
1回目は署名前のメッセージを作成するため。
2回目は署名文を含んだ、XRP Ledgerに保存するためのバイト列を作るため。
シリアライズには、定義ファイルを使います。
定義されている型が20個、フィールドの種類が149個もあります
そして、各フィールドには型や、シリアライズの処理方法が定義されています。
まず、definitions["TYPES"]
に型名と"type code"という整数が定義されています。
definitions["FIELDS"]
に、フィールドの定義が入っています。
フィールド名とフィールド詳細のタプルです。
フィールド詳細は以下の通り
- nth
- 整数。"field code"と言います。同じ型を持つフィールドの中で、1から順の整数で設定されています。
- isVLEncoded
- 真偽値。length prefix(後述)が必要かどうか。
- isSerialized
- 真偽値。シリアライズの対象に含まれるかどうか
- isSigningField
- 真偽値。署名前のメッセージを作成するときに含めるかどうか。例えば署名前は署名が含まれているわけがないので、無視しないといけません。そんなときにfalseが設定されます。
- type
- 型の名前の文字列
この定義ファイルを使って、JSON形式のトランザクションをバイト列にシリアライズしてきます。
まずはキーを一意な順番に並べ替えます。
type code順に並べ替え、同じ型を持つものはfield code順に並べ替えます。
次に、Field IDを求めます。
field codeとtype codeをこれも少しややこしくて、
field codeもtype codeも16未満(4ビットで表現できる)なら、type code, field codeの順に4ビットずつ並べて、合計8ビット
どちらかが4ビットに収まらないなら
収まらない方が、後続8ビットに飛び出して、0000でパディングされる。合計16ビット
どっちも収まらない場合、
0が8ビット並んだ後、type code, field codeの順に8ビット並んで、合計24ビット
この後、定義された型に沿って、バリューがシリアライズされます。
このField IDとシリアライズされたバリューの組みが、先ほどソートされた順に並びます。
型とシリアライズされ方は次の通り
UInt
UInt8は普通の8ビット整数です。それ以上の整数はビッグエンディアンにシリアライズされます。
Hash
固定長ハッシュ値です。
Blob
可変長のバイト列です。可変長なので、長さが必要ですが、バイト列の前にLength prefixを入れます。これはBitcoinのVarIntと異なります。
元のバイト列の長さをlen
とします
長さが192以下ならば、そのまま8ビット整数のLength prefixになります。
193以上12480以下ならば、Length prefixは16ビット整数になって、上位8ビットは(len-193)/256+193
となり、下位8ビットは(len-193) & 0xff
となります
12481以上918744以下は
{241+(len-12481)/65536, (len-12481)/65536 & 0xff, len-12481 & 0xff}
AccountID
アドレスのデコードしたやつ。
20バイトの固定長ですが、Blobと同じく、Length prefixが入ります。
Amount
64ビットまたは384ビットです。
XRP Ledgerでは、ネイティブ通貨であるXRPと、IOUが扱えます。
XRP
データ長が64ビットになります。
先頭から1番目のビットが0
です。XRPだということを表します。
2番目のビットは1
です。正の数だということを示します。XRPは負の数を取りません。
残りの62ビットがdrops単位でXRPの数量の整数をビッグエンディアンで入れます(1XRP= 1,000,000drops)
IOU
先頭から1番目のビットが1
、2番目が1
または0
で、sとします。
それに続く8ビットが、指数部e です。それにつづく54ビットが実数部mです。
ここで皆さんはIEEE754で浮動小数を扱うと想像したと思いますが、 違います
$$ (-1)^{s+1} \cdot m \cdot 10^{e-97} (1 \leq e \leq 177 , 10^{15} \leq m < 10^{16}) $$
正を表す時、s=1、負を表す時、s=0 です。 さらに1.xの形ではなく、 $ 10^{15} \leq m < 10^{16} $ な整数
また、ゼロの時は、s,e,mの部分がゼロになります。つまり0x8000000000000000000000000000000000000000
になります
この後、96ビット0
が続き、24ビットで、ASCIIの表示可能文字3文字のISO 4217通貨コードやそれっぽいものが入ります。ただし"XRP"は駄目です。
例: USD, BTC, @@@
最後の40ビットは0
です。
STArray
配列です。
配列のフィールド(STArray型のフィールド)に対する配列要素のフィールドが定義されています。配列要素フィールドは配列の中でしか使えません。
例えば、SignerEntriesフィールドに対し、SignerEntryフィールドがあります。
配列フィールドのField IDのあと、配列要素フィールドのField IDと値の組が並んで、配列の終端は0x41
が入ります。
https://xrpl.org/serialization.html#array-fields
STObject
シリアライズされるトランザクションそれ自身もSTObjectです。
なので、今まで解説したのと同じように、STObjectの中身もシリアライズします。
STObjectの終端は0xe1
が入ります。
JSONの入れ子構造と一緒ですね!
署名
シリアライズの仕方を理解したら、署名に移っていきます。
署名用のメッセージを作るときは、定義ファイルのisSigningField
がtrue
なフィールドのみをシリアライズします。
ここで、マルチシグかシングルシグで処理が分かれます
シングルシグの場合は、シングルシグのプレフィックス0x53545800
("STX\0"
),シリアライズしたデータ
マルチシグなら、マルチシグのプレフィックス0x534d5400
("SMT\0"
),シリアライズしたデータ、署名者のAccountID(20バイト)
がハッシュ化されるメッセージになります。
これをSHA512でハッシュし、先頭256ビットに対して署名します。署名の結果はDERフォーマットで表されます。
それを、シングルシグなら、シリアライズされる前のJSONでいうところの、
{
"TxnSignature": "署名文を16進数で",
"SigningPubKey": "公開鍵を16進数で"
}
をシリアライズされる前のJSONにassignしたもの。
マルチシグなら、
{
"Signer": {
"Account": "署名者のAccountID",
"TxnSignature": "署名文",
"SigningPubKey":"公開鍵",
}
}
をJSONのSignersにpushしたものになります。
それらのデータを追加したJSONをisSigningField
がfalse
なフィールドも含め、シリアライズします。
これで完成
実装
このように、149種類のフィールドの種類と、複雑なデータ型があり、署名も不思議なシステムになっているので、実装にはとても苦労しました。
ドキュメントに載っていない情報もあり、JavaScript実装を読みながらやってました。
さらに、Satoshilabsは、今後Trezor One向けの新機能開発をしないそうです。
なのでTrezor One用のC言語実装は諦めたので、社長にTrezor Tを強請ってTrezor Tの開発へ切り替えました。また、PR提出も本来のインターン期間ではおわらず、夏休みが終わっても続けることになりました。
インターン
フレセッツでは、Trezorの開発だけでなく、ICカードや、お高いハードウェアの技術調査をしました。
初めは、TypeScriptを書くと聞いていましたが、TypeScriptは一行も書かず、Java, C言語, Pythonを書きました。
あと、一緒にハードウェア調査をしていたインターンの後輩は電子工作もやってました。趣味じゃないです。仕事です。
暗号通貨の秘密鍵を守るための組み込みシステムの開発ができ、とても為になりました。
C言語とPythonの理解をさらに深くすることができ、大学のC言語のテストでは満点を取ることができました。
ICカードの知識は、今私が取り組んでいることにも繋がっています。
職場の環境は、勤務時間中に後輩が会社支給のモニタにTweetDeckを開けるくらい緩かったです。
服装ももちろん自由。どんな服を着ても何も言われません。
あと、勤務時間中にBeatSaberを遊ばせてもらったりもしました。
時給は1500円ですが、夏休み中は本郷のマンションに住まわせて頂いたので、以前住んでいた筑波大北方のリアル四畳半の宿舎よりよかったです。
給料をいただいてOSSにコントリビュートしたり、色々な経験をさせて下さり、ありがとうございました。
おわりに
XRP Ledgerはトランザクション周りが難しいけどシステム全体としてはしっかりしてると思いました。
XRPのポテンシャルはもっと引き出してもいいと思うし、XRPの技術はもっと広まってもいいと思います。
(でもXRPはすごい、価格が上がるとか馬鹿なことを言うのはやめてください)
間違いや、わからないことがあったらお気軽にコメントください。