LoginSignup
7
0

More than 3 years have passed since last update.

XRPトランザクションの形式

Last updated at Posted at 2019-12-21

本記事は 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

なお、プルリクエストはまだマージされてません:cold_sweat:

XRPの機能

XRP Ledgerには、XRPの送金だけでなく、エスクローや取引や、マスター秘密鍵とは別の鍵を使って送金できるようにする機能もあります。全部解説するのは無理なので下記サイトを見てください。
https://xrpl.org/transaction-types.html
署名にはBitcoinでおなじみsecp256k1だけでなく、Ed25519も使えます。

銀行などのお堅い場面でも使えるように色々な機能が用意されています。

アドレス生成

アドレス生成はほとんどBitcoinのそれと同じです。

  1. 秘密鍵から公開鍵を導出
  2. 公開鍵をSHA256でハッシュ化
  3. それをRIPEMD160でハッシュ化
  4. バージョンバイト0x00をつけます
  5. 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個もあります:innocent:
そして、各フィールドには型や、シリアライズの処理方法が定義されています。

まず、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ビット
image.png

どちらかが4ビットに収まらないなら
収まらない方が、後続8ビットに飛び出して、0000でパディングされる。合計16ビット
image.png
image.png

どっちも収まらない場合、
0が8ビット並んだ後、type code, field codeの順に8ビット並んで、合計24ビット
image.png

この後、定義された型に沿って、バリューがシリアライズされます。

この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が入ります。
image.png
https://xrpl.org/serialization.html#array-fields

STObject

シリアライズされるトランザクションそれ自身もSTObjectです。
なので、今まで解説したのと同じように、STObjectの中身もシリアライズします。
STObjectの終端は0xe1が入ります。
JSONの入れ子構造と一緒ですね!

署名

シリアライズの仕方を理解したら、署名に移っていきます。

署名用のメッセージを作るときは、定義ファイルのisSigningFieldtrueなフィールドのみをシリアライズします。
ここで、マルチシグかシングルシグで処理が分かれます

シングルシグの場合は、シングルシグのプレフィックス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をisSigningFieldfalseなフィールドも含め、シリアライズします。

これで完成:v:

実装

このように、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はすごい、価格が上がるとか馬鹿なことを言うのはやめてください)

間違いや、わからないことがあったらお気軽にコメントください。

7
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
0