Develop with pleasure!

福岡でCloudとかBlockchainとか。

KZG commitmentをRubyで実装してみた

多項式コミットメントの1つであるKZG commitmentについて↓

techmedia-think.hatenablog.com

より詳しく理解するためにRubyで実装してみた↓

https://github.com/azuchi/kzg/

セットアップ

KZG commitmentはまず最初に公開パラメーターを生成する。これは基本的に、多項式の最大次数が変わらない限り、最初に1回すればいい計算。

計算自体は単純で、シークレット値(sとする)を生成して、BLS曲線のPointG1のジェネレーターポイントに対して(Gとする)、 {sG, s^{2}G, s^{3}G, ..., s^{n}G}を計算する。同様にPointG2のジェネレーターポイントに対して(Hとする)も。

このシークレット値は知られるといけない値なので、基本的にTrusted Setupになる。最近EthereumでもPROTO-DANKSHARDINGで使用するためのパラメーターを生成するセレモニーやってる。

今回は、調査目的なので適当なシークレット生成してセットアップする(※ 運用用途では使わないように)。

require 'kzg'

secret = xxx # secret number
n = 10
setting = KZG.setup_params(secret, n)

↑で9次の多項式まで表現できるパラメーターを生成。※ Pure Rubyで書いてるので、ちょっと遅い。

コミットメントの作成

続いて任意の多項式に対するコミットメントを作成は↓

coefficients = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
commitment = KZG::Commitment.from_coeffs(setting, coefficients)
commitment.value # コミットメント値

↑は {10x^{9} + 9x^{8} + 8x^{7} + 7x^{6} + 6x^{5} + 5x^{} + 4x^{3} + 3x^{2} + 2x + 1}へのコミットメントになる。

コミットメントの計算の結果得られるのは、多項式の各係数をBLS曲線の有限体の要素として対応する各公開パラメーター(PointG1の点)に乗算して、それらを加算したPointG1の点。

プルーフの作成

続いて、コミットした多項式の評価値(あるxの値におけるf(x)の値)を多項式を開示することなく証明する。たとえば↑の多項式で、x = 35とした場合、f(35) = 808951170278371であることを証明するプルーフを作成する。

proof = commitment.compute_proof(35)

この計算の中身は、

 {q(x) = \frac{f(x) - 808951170278371}{x - 35}}のようなq(x)が成立するのは、f(35) = 808951170278371の場合のみ。

このプルーフもPointG1の点となる。

検証

コミット値と↑の証明を提供することで、(x, f(x)) = (35, 808951170278371)がコミットされた多項式の評価結果であることを検証することができる。

x = 35
y = 808951170278371

setting.valid_proof?(commitment.value, proof, x, y)

ここでは、BLSのペアリング特性を利用して、 {\displaystyle e(\pi, (s - x)H) = e(C - yG, H)} が成立するか検証する。つまりBLSを使って、(x, f(x))のペアに対して、↑のq(x)が成立することを検証している。

↑のペアリングの計算から分かるけど、KZG commitmentの単一のプルーフにおいては、PointG2側の公開パラメータ( {sH, s^{2}H, s^{3}H, ..., s^{n}H})は、最初のsHのみで済む。

Vector commitmentとしての利用

↑のKZG commitmentをVector commitmentとして利用する場合は、多項式の評価値がコミットする値になる多項式を構成する。たとえば、[3, 2, 9]という3つの値にコミットする場合、(x, f(x))が(1, 3), (2, 2), (3, 9)となる点を通過する多項式を構成する。多項式補間を利用すればこのような多項式を計算することができる。

x = [1, 2, 3]
y = [3, 2, 9]

# x, y座標から多項式を構成
polynomial = KZG::Polynomial.lagrange_interpolate(x, y)

# 計算した多項式のコミットメントを作成
commitment = KZG::Commitment.from_coeffs(setting, polynomial.coeffs)

# x = 3の場合のプルーフを計算
proof = commitment.compute_proof(3)
# f(3) = 9であることを検証
setting.valid_proof?(commitment.value, proof, 3, 9)

今回は、ラグランジュ補間を使って実装してみたけど、go-kzgとかではFFT使ってる。

ということで、実際に実装してみるとふんわり理解してたところが、少し明確になった。次はマルチプルーフとか作ってみるかな。

ウォレットからラベルをエクスポート/インポートするフォーマットを定義したBIP-329

先日、Bitcoinのウォレットからラベルをエクスポートしたりインポートするためのフォーマットを定義したBIP-329が公開されたので見ておく↓

https://github.com/bitcoin/bips/blob/master/bip-0329.mediawiki

BIP-32のHDウォレットやBIP-39のようなニーモニックBIP−44によるマルチアカウント階層の定義など、マスターシードや拡張秘密鍵をエクスポートして他のウォレットにインポートすることで、ウォレット間の移行は割と簡単にできるけど、その際に各ウォレットで付けられたラベルの情報は欠落してしまう。

そんなラベルの情報もウォレット間で移行できるようにするため、エクスポート/インポートするためのフォーマットを定義しようというのがBIP-329。

データのフォーマットはJSON Linesフォーマットで、現在、以下のデータに対するラベルのフォーマットが定義されている。JSONベースなので、その後の拡張も簡単。

フォーマットの仕様や、実際のデータイメージは、↓のBIPの意訳参照。

概要

このドキュメントは、ウォレット内のさまざまな共通タイプのレコードに添付できるラベルのエクスポートフォーマットを定義する。

著作権

このBIPはBSD 2-clauseライセンス。

動機

異なるBitcoinウォレットアプリケーション間での、資金のエクスポートとインポート方法は、BIP39やBIP32、BIP44などの標準で明確に定義されている。これらの標準は十分にサポートされており、ユーザーは異なるウォレット間で資金を簡単に移動することができる。しかし、ユーザーが自分のウォレット内のトランザクションやアドレス、公開鍵、インプット、アウトプット、xpubに適用したラベルを移行するために定義された標準はない。Bitcoinが使用するUTXOモデルは、外部から受け取った資金であれ、前のトランザクションのお釣りとして受け取った資金であれ、資金の出所を示す可能性があるため、これらのラベルを価値あるものにしている。どちらの場合も、望ましくない個人情報の漏洩を避けるため、支払い時に注意を払う必要があり、ラベルはこの点で貴重なガイダンスを提供し、いくつかのBitcoinウォレットで使用する際に必須にもなっている。標準化された方法でラベルをインポートおよびエクスポートできるようにすることで、ユーザーが特定のウォレットアプリケーションにロックインされることが無いようにする。

論拠

現在、ラベルのエクスポートとインポート用に広く受け入れられているフォーマットはないが、いくつか使用されている既存のフォーマットがある。SLIP-0015は、アドレスとアウトプットのラベルをエクスポートするためのフォーマットを定義しているが、ウォレットシードと関連する秘密鍵を使用して暗号化する必要があるため、秘密鍵にアクセスできないコーディネーターウォレットが単独で使用することはできない。Electrumウォレットは、アドレスとトランザクションのラベルをJSON形式でインポート/エクスポートしており、他のレコードタイプでも使用できるものの、使用されているフォーマットは自己記述型ではないため、レコードタイプの識別が難しい。

仕様

このBIPでは、軽量で可読性が高く、構造化されているため、JSONフォーマットを使用する。さらにJSON Linesフォーマットを使用する。これにより、ドキュメントの分割や、ストリーミング、インクリメンタルな追加が可能となり、フォーマットエラーによるインポート全体の無効化の可能性を制限することができる。また、行指向であることが多いコマンドライン処理にも便利な形式である。

JSON Lines仕様に加えて、ウォレットからのラベルのエクスポートは、有効なJSONオブジェクトで構成される1行につき1レコードを含むUTF-8エンコードされたテキストファイルである必要がある。行は\nで区切られる。複数行の値は許可されない。各JSONオブジェクトには、以下に定義する3つのkey/valueのペアが含まれていなければならない:

キー 定義
type txaddrpubkeyinputoutputxpubのいずれか。
ref トランザクション、アドレス、公開鍵、インプット、アウトプットまたは拡張公開鍵への参照。
label 参照先に適用されるラベル

参照は、以下のようにタイプごとに定義される:

タイプ 定義 サンプル
tx hexフォーマットのトランザクションID f91d0a8a78462bc59398f2c5d7a84fcff491c26ba54c4833478b202796c8aafd
addr base58またはbech32フォーマットのアドレス bc1q34aq5drpuwy3wgl9lhup9892qp6svr8ldzyy7c
pubkey 32、33、65バイト公開鍵のhexフォーマット 0283409659355b6d1cc3c32decd5d561abaac86c37a353b52895a5e6c196d6f448
input コロン区切りのトランザクションIDとインプットのインデックス f91d0a8a78462bc59398f2c5d7a84fcff491c26ba54c4833478b202796c8aafd:0
output コロン区切りのトランザクションIDとアウトプットのインデックス f91d0a8a78462bc59398f2c5d7a84fcff491c26ba54c4833478b202796c8aafd:1
xpub BIP32で定義されている拡張公開鍵 xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8Nq...

プライバシーに配慮したデータであるため、エクスポートする際には注意が必要。信頼できないネットワークを経由する場合は、暗号化を強く推奨し、保管中の暗号化も検討する必要がある。暗号化されていないエクスポートデータは、できるだけ早く削除する必要がある。セキュリティ上の理由から秘密鍵のタイプは定義されていない。

インポート

  • インポートするウォレットは、保存していないレコードを無視し、必要に応じてラベルを切り捨てることができる。
  • 公開鍵レコードをインポートするウォレットは、既知のウォレットアドレスと照合するため、、それらからアドレスを導出する場合がある。
  • 拡張公開鍵をインポートするウォレットは、例えばマルチシグの設定において、署名者と照合する場合がある。

後方互換

このフォーマットの性質により、他のレコードタイプを処理できるように自然に拡張することができる。ただし、この仕様に準拠したウォレットをインポートする場合、ここで定義されていないタイプは無視される可能性がある。

Test Vector

以下の断片が、ウォレットラベルエクスポートを表している。

{ "type": "tx", "ref": "f91d0a8a78462bc59398f2c5d7a84fcff491c26ba54c4833478b202796c8aafd", "label": "Transaction" }
{ "type": "addr", "ref": "bc1q34aq5drpuwy3wgl9lhup9892qp6svr8ldzyy7c", "label": "Address" }
{ "type": "pubkey", "ref": "0283409659355b6d1cc3c32decd5d561abaac86c37a353b52895a5e6c196d6f448", "label": "Public Key" }
{ "type": "input", "ref": "f91d0a8a78462bc59398f2c5d7a84fcff491c26ba54c4833478b202796c8aafd:0", "label": "Input" }
{ "type": "output", "ref": "f91d0a8a78462bc59398f2c5d7a84fcff491c26ba54c4833478b202796c8aafd:1", "label": "Output" }
{ "type": "xpub", "ref": "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8", "label": "Extended Public Key" }

楕円曲線上の点を出力するハッシュ関数の標準化 Hash to Curves

楕円曲線を使用したPedersen CommitmentやBLS署名など、誰もその離散対数を知らない点を利用する暗号プロトコルを最近よく見かける。ただ、プロトコルの説明では、そのような点の導出方法があまり詳しく書かれていなかったりすることもあり、アルゴリズムの選択を誤ると脆弱性の原因となる可能性が出てくる。

そんな点の導出方法について、さまざまな楕円曲線に対応した推奨アルゴリズムを定義しようというのが、現在IETFでドラフトが提案されている任意の入力値を楕円曲線上の点に変換するハッシュ関数

datatracker.ietf.org

ハッシュ関数の仕様

この仕様では、入力値(バイト列)を楕円曲線の点にエンコードするのに以下の3つの関数を定義している。※ 正式な仕様は↑参照。

hash_to_field

hash_to_field関数は、任意の入力値(バイト列)msgを有限体F上の1つ以上の要素にハッシュする関数。

  • 入力
    • 任意の入力値msg
    • 出力する要素の数count
  • 出力
    • count個の有限体Fの要素のリスト

hash_to_field関数は、まず補助関数expand_messageを呼んで、msgハッシュ値を計算する。仕様では、SHA-2やSHA-3、BLAKE2のハッシュ関数に加えて、可変長の出力値をサポートするSHAKE128、BLAKE2Xが利用可能になっている。 続いて、生成したハッシュ値を有限体F上の要素として解釈する。※ expand_message関数は、countで指定した要素数を満たす長さのランダムデータを出力する。

具体的な疑似コードは、5.1章に記載されてる

map_to_curve

map_to_curve関数は、有限体F上の要素から有限体F上に構成された楕円曲線E上の点を計算する写像関数。

  • 入力
    • 有限体Fの要素u
  • 出力

写像関数自体は、対象とする楕円曲線の形式(モンゴメリ曲線、ワイエルシュトラス曲線、ツイストされたエドワーズ曲線など)によって、それぞれ推奨されるロジックがある模様。具体的な仕様は、6章で定義されている。SSWU(Simplified Shallue-van de Woestijne-Ulas)方式とか*1、Elligator 2方式とか。

基本的には楕円曲線の方程式から座標を計算することになるので、y座標の候補は2つ存在することになる。この曖昧さの解消のため、関数の入力値でy座標の偶奇を指定できるようにしているっぽい。どちらかに固定すると出力される点の数が半分になるから?

clear_cofactor

clear_cofactor関数は楕円曲線E上の点を部分群G上の点に変換する関数。

Bitcoin楕円曲線secp256k1やNISTの曲線P-256、P-384、P-521などcofactor = 1の曲線においては、この操作は必要ない。

2つのエンコード方式

基本的には上記の3つの関数を順に実行して、任意の入力値を楕円曲線G上の点に変換する。

  1. hash_to_field関数で有限体Fの要素を出力
  2. 1の要素をmap_to_curve関数で楕円曲線上の点に変換
  3. 2の点に対してclear_cofactor関数を実行した点が最終的に出力される点

上記のシンプルなエンコード方式をencode_to_curve関数として定義している。ただ、この関数は入力値に対して出力される点が一様にランダムではないとされていて、一様に分布する点を出力する関数としてhash_to_curve関数を定義している。この関数の処理は、

  1. hash_to_field関数で有限体Fの要素を2つ出力(count = 2を指定)
  2. 1の2つの要素をmap_to_curve関数で楕円曲線上の点に変換(Q0, Q1)
  3. 2の2つの点を加算してR = Q0 + Q1を計算
  4. 3の点Rに対してclear_cofactor関数を実行した点が最終的に出力される点P

というもので、出力する有限体の要素が2つになり、それらの点を加算しているのが違い。これが一様な分布でG上の点を出力するポイントなのか。

暗号スイート

現在定義されている暗号スイートのリストがこちら

Bitcoinが採用しているsecp256k1以外に、NIST系の曲線やcurve25519、BLS12-381などが定義されている。これ以外の楕円曲線を利用したHash to Curveを定義する方法はこちら

参照実装

参照実装はこちら。現状、Sage、Go、Rustで書かれたものがある。

Hash and Increment

単純な楕円曲線ハッシュ関数だとHash and Incrementという手法がある。ハッシュ値楕円曲線上の点のx座標として対象となる点を探し、有効な点でなければ有効な点が見つかるまでハッシュ値を1ずつインクリメントしていく。

この場合、入力する値によって計算コストが変わってくるのと、定数時間処理にするのが難しいので、今回のHash to Curvesみたいな提案が出てきてるのかな。

*1:曲線の方程式のパラメーターa, bが0の場合(secp256k1やBLSなどの曲線の場合)、SSWU方式はそのまま使えないので、aとbが0ではない同種写像を持つ曲線を使ってSSWUで計算した点を、元の曲線の点に変換するSSWUAB0方式が採用されている。

Erlayをサポートするために必要なP2Pプロトコルを定義したBIP-330

Bitcoinで新しいトランザクションリレープロトコルの提案Erlayをサポートするために必要な、新しいP2Pメッセージを定義したBIP-330が公開されてたので見てみる↓

https://github.com/bitcoin/bips/blob/master/bip-0330.mediawiki

BIP-330では、以下の5つの新しいP2Pメッセージとそのプロトコルフローが定義されてる。

  • sendtxrcncl:ノード間でトランザクションの照合をサポートを通知するためのメッセージで、ノードとのハンドシェイク時にやりとりする。
  • reqrecon:ノード間で照合ラウンドを開始する際のメッセージ。
  • sketch:照合のために必要なスケッチを送信するためのメッセージ。以下の2つのケースで送信される。
    • reqreconを送信したノードに対して、受信側のノードが自身の未通知のトランザクションから作成したスケッチを送信する際。
    • スケッチのデコードに失敗したノードからreqsketchextが送られてきた場合に、拡張スケッチを作成し、それを送信する際。
  • reqsketchext:照合に失敗して、差分を見つけるのに拡張スケッチが必要であることを相手ノードに伝える際のメッセージ。
  • reconcildiff:照合の結果、送信者が持っていないトランザクションを相手ノードに伝えるためのメッセージ。

2つのノード間のセットの照合の仕組みであるMinisketchについては、以前書いた記事参照↓

techmedia-think.hatenablog.com

各メッセージの詳細仕様や、プロトコルのフローについては、以下のBIPの意訳参照↓

Bitcoin Coreへの実装は、現在PRのドラフトが作成されており、sendtxrcnclの実装はマージされており、残りはまだこれから。

概要

このドキュメントは、効率的なトランザクションリレープロトコル(例:Erlay)の構成要素である、2つのノード間のトランザクション照合の通知のためのP2Pプロトコルの拡張を定義するものである。これは、ほぼ帯域幅コストをかけずにネットワークの接続性を高めるための一歩となる。

動機

現在のBitcoinネットワークでは、32バイトのトランザクションIDが、invメッセージを介して、接続されたピア間で少なくとも一方向に通知される。その結果、トランザクションの通知コストが高くなる:O(ノード数✕ノード毎の接続数)。

このドキュメントで提案する技術を使用する照合ベースのプロトコルは、invベースのフラッディング方式よりも優れたスケーリング特性を持つことができる。

ネットワークの接続性を高めることで、パーティショニング攻撃に対する堅牢性を高めることができる。したがって、(高オーバーヘッドなしに)トランザクションリレーの帯域幅をO(ノード数)に改善すれば、接続性を高めることでネットワークのセキュリティを向上させることができる。また、Bitcoinノードの実行に必要な帯域幅を削減し、より多くのユーザーがフルノードを実行できるようにする可能性がある。

Erlay

Erlayは、帯域幅効率のために設定されたセット(集合)の照合(set reconciliation)を使用する高レベルのトランザクションリレープロトコルの例である。

ここで説明する内容は、プロトコルの修正版であることに注意してほしい(ペーパーに記載されているものとは異なる)。

Erlayでは、フラッディング(invメッセージを使用してすべてのピアに通知する)と照合の両方を使ってトランザクションを通知する。フラッディングは高価なため、Erlayでは接続の小さなサブセットでの高速リレーを促進するために、必要な場合にのみ フラッディングを使用する。

効率的なセットの照合は、フラッディングによってトランザクションを受け取らなかったノードにトランザクションを配信し、さらに残りの接続の同期を確実にすることを意図している(直接接続しているノードのペアは、互いに知らないものはないはず)。

効率的なセットの照合は、次のように動作する。

  1. 各ノードは各ピア用に照合セットを保持し、このプロトコルが無ければinvメッセージを使用して通知されたであろうトランザクションが配置する。
  2. 時々、各ノードはその照合キューから照合するピアを選び、その結果、双方が相手側に知られているトランザクションを知ることになる。
  3. 照合ラウンドが終われば、対応する照合セットをクリアする。

セットの照合ラウンドの詳細は以下を参照。

Erlayは、

このドキュメントでは、トランザクションリレーのための効率的な照合ベースのプロトコル(Erlayなど)を可能にするために必要なP2Pレイヤーの拡張を提案する。

仕様

新しいデータ構造

P2Pプロトコルでは、まず効率的なトランザクションリレーを支援するためにいくつかの新しいデータ構造が導入される。

32 bitの短縮トランザクションID

短縮IDは以下のように計算される:

  • 双方から与えられるエントロピー {salt_1} {salt_2}とする。これらの交換方法の詳細については、sendtxrcncl参照。
  •  {salt_1 < salt_2}となるように2つのsaltをソートする(どちらが何を送ったかは重要ではない)。
  •  {h = TaggedHash("Tx Relay Salting", salt_1, salt_2)}を計算する。この2つのsaltは、64 bitのリトルエンディアンのバイトオーダーでエンコードされ、TaggedHashはBIP-340で定義されているもの。
  • hのリトルエンディアンのバイトオーダーの先頭8バイトを64 bit整数として解釈したものを {k_0}とする。
  • hのリトルエンディアンのバイトオーダーの2つめの8バイトを64 bit整数として解釈したものを {k_1}とする。
  •  {s = SipHash-2-4((k_0, k_1), wtxid)}とする。ここでwtxidはBIP-141で定義されたwitnessデータを含むトランザクションハッシュ。
  • 短縮IDは、1 + (s mod 0xFFFFFFFF)

この結果、IDは[1..0xFFFFFFFF]の範囲に一様に分布し、これは32 bitのスケッチの要素として使用するための条件となる。詳しくは次の段落を参照。

短縮トランザクションIDのスケッチ

照合ベースのリレーでは、Fuzzy Extractorsのペーパーで紹介されたBCHベースの安全なスケッチPinSketchを使用する。

  • スケッチには所定の容量があり、セットの要素数が容量を超えない場合は、スケッチをデコードすることで、常にセット全体を復元することができる。容量cの非ゼロのbビット要素のスケッチはbc bitで格納できる。
  • 2つのセット間の対称差(つまり、一方のセットにはあるが、両方のセットにはないすべての要素)のスケッチは、2つのセットのスケッチを結合することで入手できる。

ここで使用されるスケッチは、有限体 {GF(2^{32})}の要素で構成される。具体的には、有限体の要素を {x^{32} + x^{7} + x^{3} + x^{2} + 1}を法とするGF(2)上のxの多項式として表す。整数を有限体の要素にマップするには、整数の各ビットi(値 {2^{i}})を多項式表現における {x^{i}}の係数として扱えばいい。たとえば、整数 {101 = 2^{6} + 2^{5} + 2^{2} + 1}は、フィールド要素 {x^{6} + x^{5} + x^{2} + 1}マッピングされる。これらのフィールド要素は、互いに加算、乗算することができるが、その具体的な内容は本ドキュメントの対象外である。

容量cの短縮IDのスケッチは、c個のフィールド要素のシーケンスで構成される。1つめは、セット内の短縮IDの和、2つめはすべての短縮IDの3乗和、3番目は5乗和...となり、最後の要素(2c - 1乗の和)まで続く。これらの要素はリトルエンディアンのバイトオーダーの32ビット整数としてエンコードされ、4c byteでシリアライズされる。

以下は、スケッチの作成をするPython 3.2+のコード実装。

FIELD_BITS = 32
FIELD_MODULUS = (1 << FIELD_BITS) + 0b10001101

def mul2(x):
    """Compute 2*x in GF(2^FIELD_BITS)"""
    return (x << 1) ^ (FIELD_MODULUS if x.bit_length() >= FIELD_BITS else 0)

def mul(x, y):
    """Compute x*y in GF(2^FIELD_BITS)"""
    ret = 0
    for bit in [(x >> i) & 1 for i in range(x.bit_length())]:
        ret, y = ret ^ bit * y, mul2(y)
    return ret

def create_sketch(shortids, capacity):
    """Compute the bytes of a sketch for given shortids and given capacity."""
    odd_sums = [0 for _ in range(capacity)]
    for shortid in shortids:
        squared = mul(shortid, shortid)
        for i in range(capacity):
            odd_sums[i] ^= shortid
            shortid = mul(shortid, squared)
    return b''.join(elem.to_bytes(4, 'little') for elem in odd_sums)

minisketchライブラリは、これらのスケッチの構築、結合、デコードを効率的に実装している。

プロトコルフロー

セットの照合は、主にリクエストに応じた照合セットスケッチの送信とデコードで構成される。

スケッチはwtxidベースなので、双方のピアがBIP-339サポートを通知した場合にのみ、Erlayのネゴシエーションとサポートが有効になるはず。

https://github.com/bitcoin/bips/raw/master/bip-0330/recon_scheme_merged.png

スケッチの拡張

ノードが受信したスケッチから、セットの差分を再構築できない場合、ノードはスケッチの拡張を要求する。ピアは次に拡張を送信する。拡張というのは、同じトランザクションで(より多くの差分をデコード可能な)より容量の大きいスケッチから、最初にすでに送信されたスケッチを差し引いたもの(帯域幅を節約するため)を指す。この最適化を可能にするため、イニシエーターは最初に受信したスケッチをローカルに保存することになっている。スケッチの拡張は、単に新しい要素を配列に連結するだけなので、この最適化は可能だ。

新しいメッセージ

いくつかの新しいプロトコルメッセージ:sendtxrcnclreqreconsketchreqsketchextreconcildiffが追加される。このセクションでは、そのシリアライズ方法、内容、セマンティクスについて説明する。

以下では、整数はすべてリトルエンディアンのバイトオーダーでシリアライズされる。ブール値は1バイトでエンコードされ、正確に0か1でなければならない。配列は、他のP2Pメッセージと同様、長さを表すCompactSizeというプレフィックスを付けてシリアライズされる。

sendtxrcncl

sendtxrcnclメッセージは、照合プロトコルのサポートを通知する。これは一度だけ送信され、サポートしないノードでは無視されることが期待される。

verackの前にwtxidrelayを付けて送信すること(順序は問わない)。

verackの後にsendtxrcnclが送信された場合、送信者は切断されるはずである。

sendtxrcnclverackの前に送信されたが、verackまでにwtxidrelayメッセージが受信されない場合、sendtxrcnclは無視されるはずである。接続は正常に進行するが、照合はサポートされないようにする必要がある。

相手側がversionトランザクションリレーをサポートしない(fRelay=0)と指定した場合は、送信してはならない。それ以外の場合、送信者は切断されるべき。

このペイロードは、以下で構成される:

名称 定義
uint32 version 送信者は現在1にセットする必要があり、それ以外の場合は受信者はメッセージを無視する必要がある。v1は最小のプロトコルバージョンで、それ以下はプロトコル違反となる。
uint64 salt 短縮トランザクションIDの計算に使用されるsalt。

双方のピアが、sendtxrcnclを送信しサポートを確認した後、P2P接続のイニシエーターが照合のイニシエーターのロールを担い(reqreconメッセージを送信する)、もう一方のピアが照合のレスポンダの役割を担う(reqreconメッセージに応答する)。reqreconメッセージは、照合のイニシエーターによってのみ送信される。

reqrecon

reqreconメッセージは照合ラウンドを開始する。

名称 定義
uint16 set_size セットの差分の推定に使用される、送信者の照合セットのサイズ。
uint16 q セットの差分を推定するために使用される係数。送信者側でPRECISION=(215) - 1を乗算、切り上げし、受信者側でPRECISIONで除算する。

reqreconを受信した受信者は、

  • 特定のcapacity=f(set_size, local_set_size, q)sketchメッセージを構築し送信する(正確な関数は以下で提案)。local_set_sizeは、受信者の照合セットの大きさを表す。
  • 受信者の現在の照合セットのスナップショットを作成し、セット自体はクリアする。スナップショットは、ノードがreconcildiffメッセージを受信するまで保持される。

reconcildiffメッセージが送信されるまで、新しいreqreconメッセージは送信できない。

sketch

sketchメッセージは、セットの照合を実行するために必要なスケッチを通信するために使用される。

名称 定義
byte[] skdata 送信者の照合用のスナップショットのスケッチ

sketchメッセージは、次の2つの場合に受信する:

1. 初期のスケッチ。sketchメッセージを受信すると、ノードは受け取ったスケッチを、対応する照合セットに対してローカルで計算したスケッチを組み合わせることで、差分のスケッチを計算する。受信ノードは、続いて結果の差分のスケッチをデコードする。

  • デコードに失敗した場合、受信ノードはreqsketchextメッセージを送信して、拡張スケッチを要求する。あるいは、ノードは、失敗フラグをセット(success=false)したreconcildiffメッセージを送信して、照合をすぐに終了させることができる。
  • デコードが成功した場合、reconcildiffメッセージにsuccess=trueをセットする。

また、受信者は現在の照合セットのスナップショットを作成し、セット自体はクリアする。スナップショットはreconcildiffメッセージが送信されるまで保持される。これはスケッチの拡張を有効にするために必要である。

2. スケッチの拡張。スケッチの拡張と最初に受信したスケッチを結合することで、拡張されたスケッチが得られる。次に、受信者側のノードは、受信した拡張されたスケッチと対応する照合セットのスナップショットに対してローカルで計算された拡張スケッチと結合することで、拡張差分スケッチを計算する。次に、受信ノードは、その結果の拡張差分スケッチをデコードしようとする。

  • デコードに失敗した場合、受信側のノードは、失敗フラグをセット(success=false)したreconcildiffメッセージを送信して、すぐに照合を終了させる。
  • デコードが成功した場合、reconcildiffメッセージにsuccess=trueをセットする。

いずれの場合も、success=falseのreconcildiffは、フラッディングへのフォールバックとして、照合セット(または拡張後に失敗したセットのスナップショット)から全トランザクションを通知することも伴う必要がある。success=trueのreconcildiffには、デコードされた差分から、送信側で欠落しているトランザクションに対応する未知のトランザクションの短縮IDを含んでなければならない。差分から得られる短縮IDは、メッセージの受信者に不足しているものにも対応しており、これらはinvメッセージによって通知されるべきである。

reqsketchext

reqsketchextメッセージは、最初のセットの照合が失敗し、セットの差分を見つけるのにスケッチの拡張が必要であることを知らせるために、照合のイニシエーターによって使用される。

このメッセージのペイロードは空である。

reqsketchextメッセージを受信すると、ノードはそれにsketchメッセージで対応する。このメッセージには最初に送られた部分を除いたより大きな容量(最初にスケッチされた同じトランザクションの)スケッチの拡張が含まれている。

reconcildiff

reconcildiffメッセージは、送信側で照合中に欠落が見つかったトランザクションを照合のイニシエーターが通知するのに使用される。

名称 定義
uint8 success メッセージの送信者が、セットの差分のデコードに成功したかどうかを示す
uint32[] ask_shortids 送信者が持っていない短縮IDの配列

(照合が成功した)success=1のreconcildiffメッセージを受信したノードは、wtxidを含む32 bitのIDで要求されたトランザクション(依存関係のある親トランザクションを含む)の要求のためにinvメッセージを送信する。(照合が失敗した)success=0の場合、受信者は照合セットのすべてのトランザクションinvメッセージで通知する必要がある。どちらの場合も、メッセージの送信者が受信者に欠落していると思われるトランザクションが、invメッセージを介して通知され、従来のinvの重複の排除が適用される。

対応する照合セットのスナップショットは、その後メッセージの送信者と受信者によりクリアされる。

送信者は、受信者側に欠落しているトランザクションを通知するために、reconcildiffメッセージと一緒にinvメッセージも送信しなければならない。

ローカルステート

このBIPはステートフルなプロトコルを示唆しており、適切に動作するためにすべてのノードにおいていくつかの変数を格納する必要がある。

照合用のsalt

照合のサポートをネゴシエーションする際、ピアは照合セットに使用するのsaltを互いに送信する(上記の短縮IDの構築方法を参照)。これらのsalt(または結果のsaltのみ)は、接続の両側で保持される必要がある。

照合セット

各ノードは、通常のフラッディングプロトコルで送信されたであろうトランザクションを表す、トランザクションの照合をサポートするすべてのピアのためにwtxidのセットを格納する。トランザクションを受信すると、このセットに追加される(ピアによって設定された最低手数料などのポリシーを満たす場合)。照合セットは、最初のスケッチの送信後に、対応するセットのスナップショットに移行する。

照合セットのスナップショット

最初のスケッチを送信後(reconcildiffメッセージの送信または受信のいずれか)、すべてのノードは現在の照合セットのスナップショットを保存し、セットをクリアする必要がある。これはスケッチの拡張をより安定させるために重要である(拡張はセットのスナップショットに対して計算されるべきであるため)。そうでなければ、拡張は最初のスケッチを送信した後に受信したトランザクションを含んでしまうことになる。スナップショットは照合ラウンドの終了後(reconcildiffメッセージの送信または受信)、クリアされる。

スケッチの容量の見積もりと係数q

上記で、照合の要求を受信した際、ノードは送信すべきスケッチの容量を推定することを提案したcapacity=f(set_size, local_set_size, q)

これには次の関数を提案する。

capacity=|set_size - local_set_size| + q * min(set_size, local_set_size) + c

直感的に、qはセットの不一致を表す。セットが近いほど最適なqの値は小さくなる。Erlayのペーパーによると、qは実際のセットのサイズとセットの差分が分かると、ピアとの前回の照合のための最適なqの値として導出されべるべきである。たとえば、前回のラウンドで、set_size=30、local_set_size=20で実際の差分が12にだった場合、ノードは次のようにqを計算する必要がある。

q = (12 - |30-20|) / min(30, 20)=0.1

qの導出は、プロトコルのバージョンに応じて変更することができる。たとえば、簡略化のために静的な値を選択することもできる。ただ、より洗練された(非静的な)パラメーターの選択と将来的に互換性を持たせるためには、qはすべての照合要求で送信されるパラメーターのままであることを提案する。

パラメーターcについては、空のスケッチの送信を避け、過小見積もりによるオーバーヘッドを減らすために、c=1を使用することが提案される。

後方互換

従来のクライアントは、この変更後も完全に互換性があり、相互運用可能である。

このプロトコルを実装していないクライアントは、この変更後も既存のプロトコルを使用して完全な互換性を保つ。なぜならトランザクション通知の照合は、そのサポートをネゴシエートしたピアに対してのみ使用されるためである。

論拠

セットの照合にPinSketchを使用する理由は?

PinSketchは、IBLTよりも帯域幅効率が高く、特に私たちが想定している差分の小さいセットに対して動作する。PinSketchはCPISyncと同程度の帯域幅だが、デコードについてCIPSyncが3次の計算量であるのに対して、PinSketchは2次の計算量になる。このためPinSkechは大幅に高速化される。

32 bitの短縮トランザクションIDを使用するのはなぜ?

実際にMinisketchを使用するには、トランザクションIDを短くする必要がある(理想的には1要素あたり64 bit)。トランザクションあたりのbit数が少なければ、余計な帯域幅を節約し、スケッチに対する演算をより速くすることもできる。私達の試算では、32 bitは(リンク毎に個別のsaltを使用するすることで可能になる)非Adversarial Modelにおいて、低い衝突率を提供する。

Bisectionではなくスケッチの拡張を使用するのはなぜ?

Bisectionは、スケッチの拡張に代わるもので、同じ初期容量を持つ2つめのスケッチがtxID空間の半分で計算される。スケッチの線形特性により、この1つだけを送信することで、照合のイニシエーターは別の半分の同じ容量のスケッチを計算することができる。2つのスケッチにより、イニシエーターは、最初のスケッチで可能な2倍の差分を再構築することができる。

実際には、これによりイニシエーターは、拡張スケッチと同様に、最初の照合失敗の帯域幅のオーバーヘッドを償却し、オーバーヘッドを無視できるようにすることができる。

スケッチの拡張の主なメリットは、実装がシンプルであること。Bisectionの実装は難しい。なぜなら、最終的には2つのスケッチで動作し、1つのスケッチがデコードされ、別のスケッチが失敗するシナリオを処理しなければならないため。

将来、複数の拡張/Bisectionを許可することになると、さらに難しくなる。この場合、Bisectionは再帰的でなければならず(そして、4/8/16...のスケッチを生成)、一方スケッチの拡張では常に1つの拡張スケッチで終えることができる。

スケッチの拡張はまた、より柔軟である。容量10のスケッチを4つ拡張するのは、容量14のスケッチを計算し、拡張を送るだけですむ。一方Bisectionで、容量を102/104/10*8/...と異なるものに増やすのは実装的には高度なものになる。

Bisectionの唯一のメリットは、より大きな容量のスケッチを計算する必要がないことである(指数関数的なコスト)。私たちは、このプロトコルは現在、通常最大でも20の容量を持つ条件で動作するよう設計されているため、この効率は重要ではないと考えている。

実装

https://github.com/bitcoin/bitcoin/pull/21515

lnd v0.15.3-betaで発生した2度目の障害の内容

少し間が空いたけど、

techmedia-think.hatenablog.com

の障害からそんなに間を開けずに、再度LNDでチェーン同期ができなくなる障害が発生したので、その内容をまとめておく。

障害の原因

障害のトリガーとなったは、500,142 byte(125,109 vbyte)のこのトランザクション

トランザクションには1つのインプットと1つのアウトプットがあり、アウトプットは話題に挙がった

you'll run cln. and you'll be happy.

というデータがOP_RETURNで書かれているだけ。

インプットが参照するUTXOは、このUTXO。TaprootのUTXOであることが分かる。

サイズが大きいのはインプットのwitnessデータ(500,042 byte)で、以下のデータで構成されている。

  • 500,000個の空データ
  • 0x50:TapscriptではOP_SUCCESS80として機能
  • c11dae61a4a8f841952be3a511502d4f56e889ffa0685aa0098773ea2d4309f624:Tapscript利用時のControl Blockで、
    • Control Byte:c1
    • Taprootの内部鍵:1dae61a4a8f841952be3a511502d4f56e889ffa0685aa0098773ea2d4309f624
    • マークルパスは空

つまり、呼び出された時点でスクリプトは成功するOP_SUCCESS系のopcode単体のスクリプトを1つもつTapscriptにコインがロックされている。単にコインを使用する(アンロックする)のであれば、↑のwitnessデータの内、0x50とControl Blockの2つの要素のみがあれば良い。今回はその前に500,000個の空データが余計にプッシュされている。インタープリターの処理としては、

  1. まずwitnessデータをスタックプッシュする。
    1. 500,000個の空データをプッシュ
    2. 0x50をスタックにプッシュ
    3. Control Blockのデータをスタックにプッシュ
  2. TaprootのscriptPubkey(OP_1 9bb9efbddf9d70afd3ac2cef011747236bdf90832a78b08f57d1139f07aa9185)を実行
    1. Control Blockをスタックからポップして検証
    2. 0x50を評価した時点で、成功判定(スタックには500,000個の空データが残ったまま)

という処理が実行される。

LNDがライブラリとして使用しているBTCDでは、(DoS対策としているが)witnessデータの最大要素数を500,000個に制限していたため、それを超える↑のトランザクションを不正なトランザクションとして処理してしまったのが今回の障害の原因。

マイナーの協力が必要

前回の障害と違うのは、↑のトランザクションは以下の2つの理由から非標準トランザクションであること。

  • まずトランザクションサイズが125,109 vbyteで、これは標準ルールの制約である100,000 vbyteの上限を超えている。
  • OP_SUCCESS系のopcodeが含まれている。これらのopcodeは、将来のopcodeの拡張のため従来予約されていたOP_NOP系のopcodeの取り回しを改善するために導入されたもの。現状Tapscriptの実行中にこれらのopcodeが登場した場合、非標準として処理される。

非標準トランザクションBitcoin Coreではmempoolへは受け入れられずリレーもされない。それでもコンセンサスルールとしては有効なので、マイナーの協力があればブロックに格納することができる。ブロックに格納されていれば非標準トランザクションでも↑のインタープリターの処理が実行され有効なトランザクションとして判定される。

なので、このトランザクションのマイニングに協力したマイナーがいたということ。トランザクションアウトプットはOP_RETURNのアウトプットのみなので、インプットのビットコイン(0.03682719 BTC)はすべてマイナーの手数料になる。これが報酬?

↑のスクリプト見る限り署名検証の必要はないので、むしろマイナーは500,000個の空データをプッシュ部分削除して、単に0.03682719 BTCだけ頂くということも可能よね。

修正内容

修正内容は、BTCDのwitnessデータの最大要素数のチェックの値を500,000から4,000,000(コンセンサスである最大トランザクションweight)に引き上げるというもの↓

https://github.com/btcsuite/btcd/pull/1907/files

責任ある開示

もともとこのバグ自体は、Anthony Townsによって発見され、LNDとBTCDのリードメンテナである@roasbeefに開示されていた模様。ただ、障害を発生するためには非標準トランザクションをマイニングするマイナーの協力が不可欠であったため、他の変更と一緒に修正することで脆弱性の修正を隠して対応する予定(脆弱性の悪用がされないように)だった模様↓

残念ながら、そのリリース前に悪用されてしまった。

Liquidにも影響

ちなみにこのトランザクションは、BlockstreamのLiquidのWatchmenにも影響し、ペグアウトが一時できなくなった模様。

使用しているrust-bitcoinのバグみたいだけど、rust-bitcoinでは数ヶ月前に修正版はリリースしていたっぽい。

状況的には、全体的にTaprootのコンセンサスについて各ライブラリがテストされてるような感じだなー。