Develop with pleasure!

福岡でCloudとかBlockchainとか。

lnd v0.15.1-beta以下で発生したチェーン同期の障害

lnd v0.15.1-beta以下で発生したlndでチェーンの同期ができなくなる不具合↓についてまとめておく。

github.com

正確には、btcdの不具合↓

github.com

障害の内容

今回発生した障害は、lndでブロックチェーンの同期ができなくなるというもの。そのため、その間、新しいチャネルの開設やクローズができなくなった。※ 稼働中のチャネルを利用したオフチェーン支払いは可能なまま。

実際、自宅で運用してるUmbrelでも、lndの以下のログを吐いてた↓

2022-10-09 21:04:09.448 [ERR] LNWL: Unable to deserialize transaction: readScript: script witness item is larger than the max allowed size [count 33970, max 11000]
2022-10-09 21:04:09.559 [ERR] LNWL: Unable to deserialize block: readScript: script witness item is larger than the max allowed size [count 33970, max 11000]
...
2022-10-11 00:05:36.001 [ERR] LNWL: Unable to process chain reorg: unable to get block 0000000000000000000400a35a007e223a7fb8a622dc7b5aa5eaace6824291fb: readScript: script witness item is larger than the max allowed size [count 33970, max 11000]

トリガーとなったのは、ブロック757922内の以下のトランザクション

7393096d97bfee8660f4100ffd61874d62f9a65de9fb6acf740c4c386990ef73

インプットが参照するUTXOはTaprootのアウトプットで、それをscript-pathで998-of-999のマルチシグを使ってアンロックしている。この結果、インプットのscriptWitnessには、999個の公開鍵と998個の署名データが含まれ、合計98,881バイトのデータになる。

※ ちなみに、998-of-999のマルチシグの場合、今回のケースとは別に998-of-998の組み合わせを999個作って1つの公開鍵と1つの署名に集約するというアプローチを採れば、データは150バイト以内に収められる。

このトランザクションをbtcdが正常にパースできず、チェーンの同期が失敗するようになった。

不具合の原因

問題となったbtcdのコードは、ピアからtxメッセージやblockメッセージを受け取って、そのトランザクションをパースする以下の部分↓

https://github.com/btcsuite/btcd/blob/v0.22.0-beta/wire/msgtx.go#L589-L594

maxWitnessItemSizeが11,000バイトに設定されており、このサイズを超えるscriptWitnessはエラーになる。このため、↑のscriptWitnessが98,881バイトあるトランザクションはパースに失敗する。

もともとこのチェックは、P2WSHの仕様↓から来てるっぽい(ただ、1,000バイトの差は何だろう?)。

The witnessScript (≤ 10,000 bytes) is popped off the initial witness stack. SHA256 of the witnessScript must match the 32-byte witness program.

本来Taprootにはこのルールが適用されないが↓*1、このルールが適用されたまま上限を超えるTxが登場したので今回問題が顕在化した。

Script size limit The maximum script size of 10000 bytes does not apply. Their size is only implicitly bounded by the block weight limit.

ちなみに、Bitcoin Coreの場合、このチェックはScriptを評価するこのコードに実装され、P2SHよびP2WSHに限定されている↓

if ((sigversion == SigVersion::BASE || sigversion == SigVersion::WITNESS_V0) && script.size() > MAX_SCRIPT_SIZE) {
    return set_error(serror, SCRIPT_ERR_SCRIPT_SIZE);
}

不具合の修正内容

不具合の修正内容は単純で、maxWitnessItemSizeの値をブロックの最大サイズである4,000,000に引き上げるというもの。

ただ、この修正でちょっと気になったのが

  • このチェックはトランザクションレベルのサイズチェックで済む話なので、あえてこのインプット毎のチェックとして必要なのか?
  • チェックを残すにしても、P2WSHについては↑のチェック回避することになるけど、そこは問題はないのか?

どうしてlndで影響が出たのか?

今回の障害が発生した際、btcdの不具合で、lndの接続先にBitcoin Coreを選択してる自分には関係ない話かな?と思ったけど、そうでもなかった。

lndは、Bitcoinのノード実装だけでなく、ライブラリとしてbtcdのコードを多く利用している。今回も、フルノードから受け取ったブロックのパースに↑のbtcdのコードを使っているため、パースに失敗し新しいブロックを処理できなくなり、今回の障害に至ったと。

チャネルの開設やクローズのオペレーションができないくらいならまだ良いけど、不正なTxがブロードキャストされたのを検知できなくなる方が問題かな。今回みたいなケースを想定すると、Watchtowerの実装はLNノードの実装とはまた別のコードベースのものを使いたいと思うようになる。

*1:この制限がどうしてなくなったかというと、P2WSHまでは署名対象のメッセージダイジェスト(つまりトランザクションのダイジェスト)を計算する際に、アンロックに使用するScriptのデータもその計算に含まれていた(scriptCodeとして)。Scriptのサイズが増加するとメッセージダイジェストを計算するコストも比例して増加してしまうので、この計算リソースを限定するためにScriptのサイズに最大サイズの制限が設定されていた。Taprootでは、メッセージダイジェストを計算する際のデータ項目が変更され、直接Scriptをメッセージダイジェストに含めなくなったため、このリソース要件をなくした模様。ただ、実行するScriptがTaprootツリーのリーフハッシュと一致することを検証する必要はあるので、いずれにせよハッシュ計算は発生するんだけど。

BLS署名を利用した検証可能なwitness encryption

検証可能なwitness encryptionという新しい暗号プリミティブを利用してDLCを構成する新しい提案のペーパーが発表されてたので↓

https://eprint.iacr.org/2022/499.pdf

「II. TECHNICAL OVERVIEW」の内容を見てみた(まだ一部よく分かってない)。

BLS署名

ペーパーではBLS署名を利用したwitness encryptionスキームが提案されているので、まず前提となるBLS署名について。

ペアリング

BLS署名が利用しているペアリングは、同じ素数qを位数とした、2つの加法巡回群 {G_1, G_2} *1と乗法巡回群 {G_T} *2という3つの巡回群について、

 {e:G_1 \times G_2 \rightarrow G_T}

という関係が成立する写像のことを指す。つまり {G_1} {G_2}の各要素を入力として、 {G_T}の要素を出力できることを意味する。

 {G_1, G_2}については楕円曲線を利用するため、入力は {G_1, G_2}に対応する楕円曲線上の2つの点で、有限体 {G_T}の要素を出力することになる。このことから楕円ペアリングとも呼ばれる。

また、この時、 {G_1}楕円曲線の生成元をP、 {G_2}楕円曲線の生成元をQとした場合、任意の数値a, bが与えられた場合に、

 {e(aP, bQ) = e(bP, aQ) = e(P, Q)^{ab}}

が成立することを双線形性と呼ぶ。

BLS署名

ECDSAやSchnorr署名が1つの群、楕円曲線を使ったデジタル署名方式であるのに対し、BLS署名は↑の3つの群の関係を利用したデジタル署名方式になる。

 {G_1}の曲線の生成点をP、 {G_1}の曲線の生成点をQとした場合、BLS署名は以下のように作成される。

  • ランダムな数値 {x \in \mathbb Z_q}を選択し、これを秘密鍵とする。対応する公開鍵は {G_2}上の点Y = xQ
  • 署名対象のメッセージをmとした場合、 {\sigma = xH_1(m)}を計算する。ここで、 {H_1} {G_1}の曲線の要素を出力するハッシュ関数。この {\sigma}が署名で、かつ {G_1}の曲線上の点になる。

※ 署名方式によって公開鍵と署名に使用する際の {G_1, G_2}が逆になる場合もある。

署名の検証は、

  •  {e(\sigma, Q) = e(H_1(m), Y)}が成立するか検証する。

 {e(\sigma, Q)}は、ペアリングの双線型性から {e(xH_{1}(m), Q) = e(H_{1}(m), Q)^{x} = e(H_{1}(m), xQ) = e(H_{1}(m), Y)}と展開できる。右辺と左辺のペアリング関数の出力結果である有限体 {G_T}の要素が同じであれば検証をパスする。

つまりBLS署名はペアリングの双線形写像の特性を利用して秘密鍵の知識を証明する署名方式であることが分かる。また、ECDSAやSchnorr署名と違って署名の作成に乱数(nonce)を必要としない、決定論的な署名方式。

BLSベースのwitness encryption

検証可能なwitness encryptionに行く前に、前提となるBLSベースのwitness encryptionについて。BLSベースのwitness encryptionというのは、BLS署名を利用した暗号化スキームで、ある公開鍵とメッセージに対応するBLS署名が公開されたら、その署名を使って暗号化されたデータを復号できるというもの。

表記については以下を前提とする。

  • 暗号化対象のメッセージをmとする。
  • 署名に利用する公開鍵の鍵ペアをV = vQとする。vが秘密鍵で、Qは↑の {G_2}楕円曲線の生成点。
  • 署名対象のメッセージを {\tilde{m}}とする(これは暗号化対象のメッセージではない)。
  •  {H_1} {G_1}の要素(点)を出力するハッシュ関数
  •  {H_T} {G_T}の要素のハッシュ値を計算するハッシュ関数

公開鍵Vとメッセージ {\tilde{m}}に対して有効なBLS署名が提供されたら、メッセージmを復号できる暗号スキーム。

暗号化

以下の手順でメッセージmを暗号化する。

  1. ランダムな値 {r_1 \in \mathbb Z_q}を選択する。
  2. ランダムな値 {r_2 \in G_T}を選択する。
  3.  {c_1 = r_1P}とする。
  4.  {h = H_T(r_2)}とする。
  5.  {c_2 = e(V, H_1(\tilde{m}))^{r_1} \cdot r_2}とする。
  6.  {c_3 = h + m}とする。
  7. {c = (c_1, c_2, c_3)}を暗号文とする。

復号化

上記の暗号文は、Vとメッセージ {\tilde{m}}に対するBLS署名 {\tilde{\sigma}(= vH_1(\tilde{m}))}が公開されると、以下の手順で復号できる。

  1. {c = (c_1, c_2, c_3)}をパースする。
  2.  {r = c_2 \cdot e(c_1, \tilde{\sigma})^{-1}}を計算する。
  3.  {h = H_T(r)}を計算する。
  4.  {m = c_3 - h}が復号したメッセージとなる。

↑では、 {\tilde{\sigma} = vH_1(\tilde{m})}であるため、rの計算に出てくる {e(c_1, \tilde{\sigma})^{-1}}は、ペアリング関数の双線形性により、 {e(r_1P, vH_1(\tilde{m}))^{-1} = e(V, H_1(\tilde{m}))^{-r_1}}となる。

そして、 {c_2 = e(V, H_1(\tilde{m}))^{r_1} \cdot r_2}であるため {r = r_2}となり、 {h = H_T(r) = H_T(r_2)}であるため、 {c_3 - h}でメッセージmが入手できると。

復号のためには、h(つまり {r_2})の値が必要になり、それを算出するためには、 {c_2}内のペアリング関数部分を打ち消す必要があり、それをBLSの双線形性と乱数使ってワークするようにしてるの面白いな。

ということで、このスキームを利用すると、(オラクルなどの)ある公開鍵とあるメッセージに対応した署名を入手すると復号可能な暗号文を作成することができる。

条件付きの支払い

DLCが扱うのは、オラクルが証明可能なイベントの結果を条件に支払いを行う条件付き支払いをサポートするユースケース

ここでは、アリスがイベントの結果にコミットし、結果がコミットした値と同じ場合に、ボブに対して支払いをす条件付き支払いを考える。DLCでは、アダプター署名を利用して、署名を完成させるために必要なシークレットを復元するアプローチを採っている。

このような条件付き支払いを成立させるためには、

  • イベントの結果がコミットした内容である場合、ボブは必ず署名を入手することができる。
  • イベントの結果がコミットした内容でない場合、ボブが署名を入手することはできない。

ことを保証する必要がある。さらに追加で、オラクルを分散させて一定数のオラクルによる証明が提供された場合に、署名の入手を可能にする(閾値性)を考慮する必要があるが、今回はシンプルにオラクル1人のケースで考える。

検証可能なwitness encryption

↑の新しいソリューションとして提案されているのが検証可能なwitness encryptionという新しい暗号プリミティブを利用する方法。プロトコルの参加者(アリスとボブ)=ブロックチェーンレイヤーが使用する署名方式はSchnorr署名(もしくはECDSA)で、オラクルの署名方式はBLS署名。まず、前提として、

  • アリスの鍵ペアを {P_A = x_AG}とする。ここで、Gはsecp256k1などの楕円曲線の生成点。
  • ラクルであるオリビアの鍵ペアを {P_O = x_OQ}とする。ここで、Qは上記のペアリングの {G_2}楕円曲線の生成点。
  • アリスがボブに条件付き支払いをするメッセージ(トランザクション)をmとする。

アリスは秘密鍵 {x_A}を使ってメッセージに署名する。通常のSchnorr署名であれば、

  1. ランダムなnonce kを選択
  2. R = kGとする。
  3.  {s = k + H(P_A||R||m)x_A}を計算する。
  4. (R.x, s)が署名

この署名があれば、ボブは資金を手に入れられる。ただ、条件付き支払いなので、アリスは、 {T = tG}を使って、メッセージmに対する事前署名s'を生成する。

  •  {s' = k + t + H(m)x_A}

ボブには、(R, s)の代わりにアダプター署名 {(R, s', T)}を送る。アダプター署名を入手したボブは、tさえ分かれば、有効な署名sを導出できる。

その後は、

  1. アリスはtを暗号化した暗号文cをボブに送信する。この暗号化に↑のwitness encryptionを利用する。
  2. リビアがイベントeをメッセージとして、秘密鍵 {x_O}を使ってBLS署名を作成し、公開する。
  3. ボブはこの署名とcからtを抽出し、s = s' - tをして署名sを手に入れる。

というプロセスを実行するが*3、ここで問題になるのが、ボブがアリスからアダプター署名と暗号文cを受け取った際に、cが暗号化されているので、tが確かに暗号化されているのか検証する術がない点。この検証ができなければ、単にアリスを信頼するしかないということになるのでよろしくない。

そこで、Cut&Chooseという手法を使って、暗号化されたデータが有効なデータ形式であることを検証できるようにしたのが検証可能なwitness encryption。具体的には、アリスが暗号文cを生成する際に、単一のtの暗号文を作るのではなく、以下の手順でλ個の暗号文を作成する(λはセキュリティパラメーター)。

暗号化

t単体を暗号化するのではなく、λ個の乱数 {r_i}(i = 0..λ-1)を生成し、この各 {r_i}を↑のBLSベースのwitness encryptionで暗号化する。そして、このλ個の暗号文とは別に、

  1.  {r_i}について {s_i = r_i + t}を計算する(sym-cipherと呼ばれる)。
  2.  {r_i}について {R_i = r_iG}を計算する。

アリスは、λ個の暗号文と {R_i}とアダプター署名をボブに送信する。

Cut & Choose

その後、

  1. ボブは受け取ったλ個の暗号文と {R_i}からλ/2個のペアを選択して、選択したペアをアリスに伝える。
  2. アリスは、
    • ボブから受け取ったデータに対応する {r_i}の値とwitness encryptionで暗号化に使用した乱数値をボブに送信する。
    • 選択されなかった残りのλ/2個のペアについて、 {s_i}をボブに送信する。
  3. ボブは、
    • 選択したペアについて、 {r_i}をアリスから受け取り、前に受け取った {R_i}について {r_iG = R_i}が成立するか検証する。さらに、オラクルの公開鍵Vとメッセージ {\tilde{m}}およびアリスから受け取った乱数値を使って {r_i}の暗号化計算を行い、それがアリスが送ってきたものと一致するか検証する。これにより、アリスが送ってきた暗号文はすべて {r_i}が暗号化されているだろうことが検証できる。
    • 選択しなかったペアについて、前に受け取った {R_i}とTに対して、 {s_iG = R_i + T}が成立するか検証する。これによりアリスが作成したデータはすべて {s_i = r_i + t}の形式のデータだろうということが検証できる。

そして、オラクルが署名を公開すれば、アリスが開示しなかった {r_i}も復号できるので、 {t = s_i - r_i}を計算して、witness tを入手できるということみたい。

この辺りが、ペーパーの「II. TECHNICAL OVERVIEW」で説明されてるんだけど、これアリスが {r_i}を開示した方のパターンだと、ボブはsym-cipherもらってないからどうやってt計算するんだろう?この辺り、まだよく理解できてない。

ちなみにペーパーの方は、

  • Fiat-Shamir変換した非対話型への変換
  • Cut&Chooseのバッチ化
  • 分散オラクルの閾値

など、実用的な課題に対するプロトコルの拡張が続く。

*1:加法巡回群は、群の元がすべて生成元の整数倍で表現できる群

*2:乗法巡回群は、群の元がすべて生成元の冪乗で表現できる群

*3:イベントの結果に両者が合意していれば、tをそのまま渡して終了

x-only public keyの課題とワークアラウンドな回避策

ちょっと前のBitcoin Optechのニュースレターでx-only public keyの課題とその回避策について取り上げられていたので、詳しく調べてみた。

x-only public keyとは?

Bitcoinに導入されたTaprootでは、署名検証に使用する公開鍵に32バイトのx-only public keyという形式を採用した。

公開鍵はこれまで33バイトの圧縮公開鍵(楕円曲線の点のy座標の偶奇を示す1バイト+x座標32バイト)が使われてきたが、Taprootではy座標は常に偶数となるようルールを設けることで、y座標の1バイトを削除し32バイトの公開鍵を指定するようになった。これがx-only public key。なので、TaprootなUTXOを使用する場合は、以下のTaprootの構成要素でこのx-only public keyが適用される:

  • TaprootのscriptPubkeyであるOP_1 <公開鍵>で構成されるP2TRアウトプットの<公開鍵>部分。
  • ↑の公開鍵は、内部公開鍵 + tGで構成される。この時、
    • この計算時に内部公開鍵はy座標が偶数となる公開鍵として扱われる
    • tは、内部公開鍵のx座標とTaprootのスクリプトツリーのルートハッシュから作られるタグ付きハッシュで、t = H("TapTweak" || 内部公開鍵(x-only) || merkle root)
  • Taprootのスクリプトツリー内の各スクリプトに登場する公開鍵

x-only public keyの課題

x-only public keyにより、公開鍵が登場する際に1バイト分データを節約できるようになったけど、これによる課題も報告されている↓

https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2021-September/019420.html

具体的には、TAPLEAF_UPDATE_VERIFY opcodeを導入してCovenantsをサポートするという提案で見られる。この提案自体は↓

techmedia-think.hatenablog.com

問題となるのは、TLUVで内部公開鍵を更新するケース。例えば、アリス(A)、ボブ(B)、キャロル(C)がP2TRのコインを共有するプールスキームを考える。

3人の公開鍵A、B、Cのy座標がすべて偶数で、かつA + B + C(内部公開鍵)のy座標も偶数のケースを考える。この場合、P2TRの公開鍵は、

Q = (A + B + C) + H("TapTweak" || (A + B + C) || root1)G

この段階で、Taprootのx-only public keyの要件を満たしていたとしても、プールからCが抜ける場合、更新後の内部公開鍵A + Bのy座標が偶数になるとは限らない。A + Bのy座標が奇数である場合、

Q' = -(A + B) + H("TapTweak" || -(A + B) || root2)G

のような変換が必要になる。ここまでは、まぁ変換すればいいだけと捉えても問題ないが、さらにBがプールから抜ける場合、-(A + B)からBを減算することになり、

Q'' = -A - 2B + H("TapTweak" || -A - 2B) || root3)G

となり、Bがプールから抜けても、keypath支払いをする場合、Bの協力が必要になってしまう。

この回避のために↑の投稿では、内部公開鍵のy座標が奇数になる場合はエラーにして、参加者のすべての鍵の組み合わせでy座標が偶数になるよう事前調整するという手法も提案されてる。参加者(公開鍵)の数をnとした場合、約2nの計算が必要になるとされている。nが30以下くらいであれば計算可能な範囲だけど、まぁそれでも計算コストはかかるのと、事前に鍵の組み合わせの検証を求めるのもハードル高いので、あまり現実的ではないように思える。

ワークアラウンドな回避策

これに対して、Tim Ruffingが提案したのがワークアラウンドな回避策↓

https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2022-July/020663.html

この回避策は、内部公開鍵のy座標が偶数になるまで、ベースポイントGを加算するというもの。

Q = (A + B + C)からCが抜ける場合、Q'のy座標を偶数にするために、

Q' = A + B + G

とする。そして、さらにBが抜ける場合Q'' = A + Gのy座標が奇数であれば、さらに

Q'' = A + G + G = A + 2G

とするというもの。必ず1回Gを加算する訳ではなく、y座標が偶数になるまでGを加算する。↑の投稿では、平均1回の加算で成功する(証明はなし)だろうとしている。

すべての鍵の組み合わせを事前に計算するより、対応が簡単な方法だ。

TLUV自体は、まだ提案中のopcodeだけど、こういうコントラクトを作成するアプローチは十分考えられるので、結果的にx-only public keyはあまり良いアプローチではなかった感じかなー。

IONのDIDのリカバリーと無効化操作をアンカリングしてみる

techmedia-think.hatenablog.com

techmedia-think.hatenablog.com

ときたので、残りのRecoverおよびDeactivate操作をアンカリングしてみる。

Recover操作

Recoverは、Update Keyの更新とRecovery Key自体の更新および、DID State Patcheで定義したDID Documentに対する変更を適用する操作になる。DID Documentの更新という意味ではUpdate操作と似ているけど、Update Keyを紛失したり盗まれたとしても、Recovery Keyがあれば(制御下にあれば)、Recover操作によるDIDの更新で、DIDの管理が引き続き可能。一方、Recovery Keyを紛失してしまうと、既存のUpdate KeyによりDIDの更新だけは引き続き可能だけど、盗まれてしまうとDIDの制御権を奪われることになる。そういう意味でRecover Keyの管理の方が重要度が高い。

実際にRecover操作で、DID Documentを更新してみよう。手順は↓

  1. 新しいRecovery Keyを生成
  2. 新しいUpdate Keyを生成
  3. DID Documentに対する変更を定義したDID State Patcheを作成する。今回は新しい署名鍵を生成し、現在のDIDの署名鍵を新しい鍵に置き換えるreplaceアクションを適用。
  4. 2のUpdate Keyのコミットメントと、3のPatchからRecovery Operation Delta Objectを作成。
  5. 4のデータのハッシュと現在のRecovery Keyのデータ、新しいRecovery KeyのコミットメントからRecovery Operation Signed Data Objectを作成。
  6. 5のデータに対して、現在のRecovery Keyで署名したJWSを生成。
  7. JWSデータを使って、Core Proof Fileを作成し、IPFSにアンカリングする。
  8. 4のDeltaオブジェクトを使って、Chunk Fileを作成し、IPFSにアンカリングする。
  9. 8のChunk FileのURIを含む、Provisional Index Fileを作成し、IPFSにアンカリングする。
  10. 9のProvisional Index FileのURIと、7のCore Proof FileのURI、現在のDIDにコミットされているRecovery Keyのコミットメントに対応するReveal ValueからCore Index Fileを作成し、IPFSにアンカリングする。
  11. Core Index FileのURIからAnchor Stringを生成し、それをBitcoinにアンカリングする。

Update操作との違いは、

  • プルーフはProvisional Proof Fileではなく、Core Proof Fileにセット。
  • Recover操作によるDID Documentの変更の差分(delta)や、更新に使用する鍵のReveal Valueは、Provisional Index FileではなくCore Index Fileに配置。

あたりかな。この辺りのデータの配置先をUpdateとRecover/Deactivateでそれぞれ分離するメリットってどこにあるんだろう?同じファイルや方法で扱えた方がシンプルで便利だと思うんだけど。

以下、実際にRecover操作をCASにアンカリングしたコード↓

require 'sidetree'

Sidetree::Params.network = Sidetree::Params::Network::TESTNET

# DIDのsuffix
did_suffix = "EiBRrmEha_Q30GieEwLB-XM8CZd_b49dQ7znhaBxfAHTsQ"

# 現在設定されているRecovery Key
recovery_key =
  Sidetree::Key.new(
    private_key:
      45_036_086_779_068_677_123_348_498_811_216_758_728_966_810_636_876_259_204_257_311_947_782_243_979_294
  )
# 新しいRecovery Keyを生成
new_recovery_key = Sidetree::Key.generate

# 新しいUpdate Keyを生成
new_update_key = Sidetree::Key.generate

# 新しい署名鍵を生成
new_signing_key = Sidetree::Key.generate(id: "signing-key")

# 置き換えるDID Documentを生成
document = Sidetree::Model::Document.new(public_keys: [new_signing_key])
delta =
  Sidetree::Model::Delta.new(
    [document.to_replace_patch],
    new_update_key.to_commitment
  )
# JWSを生成して現在のRecovery Keyで署名
claim = {
  recoveryKey: recovery_key.to_jwk.normalize,
  recoveryCommitment: new_recovery_key.to_commitment,
  deltaHash: delta.to_hash
}
jws = Sidetree::Util::JWS.sign(claim, recovery_key)

ipfs = Sidetree::CAS::IPFS.new

# Core Proof Fileを作成
core_proof_file = Sidetree::Model::CoreProofFile.new(recover_proofs: [jws])
core_proof_file_uri = ipfs.write(core_proof_file.to_compress)

# Recover Operationを作成
recover_op =
  Sidetree::OP::Recover.new(
    did_suffix,
    delta,
    jws,
    recovery_key.to_reveal_value
  )

# Chunk Fileを作成
chunk_file =
  Sidetree::Model::ChunkFile.create_from_ops(recover_ops: [recover_op])
chunk_file_uri = ipfs.write(chunk_file.to_compress)

# Provisional Index Fileを作成
provisional_index_file =
  Sidetree::Model::ProvisionalIndexFile.new(
    chunks: [Sidetree::Model::Chunk.new(chunk_file_uri)]
  )
provisional_index_file_uri = ipfs.write(provisional_index_file.to_compress)

# Core Index Fileを作成
core_index_file =
  Sidetree::Model::CoreIndexFile.new(
    provisional_index_file_uri: provisional_index_file_uri,
    core_proof_file_uri: core_proof_file_uri,
    recover_ops: [recover_op]
  )
core_index_file_uri = ipfs.write(core_index_file.to_compress)

anchor_str =
  Sidetree::Util::AnchoredDataSerializer.serialize(1, core_index_file_uri)

Anchor Stringは、

ion:1.QmT2UBzXXsk4gjmUPYvFkQLXgZayfEUq5hpUEGjVRFphgt

で、アンカリングしたBitcoinトランザクションは、8ed80cf58094d12c4fca6dd9fb2ae136278e78afbd65fa108a5db961f762f7ae。ブロックに取り込まれると、IONのResolverでもDID Documentの更新が確認できる↓

DID Document(署名鍵)に加えて、Recovery Commitment、Update Commitmentが更新されている。

Deactivate操作

Deactivate操作は、Recovery Keyを使ってDID Documentを無効化する操作になる。

手順は、他の操作よりシンプルで、

  1. 無効化対象のDIDのSuffixとRecovery KeyでDeactivate Operation Signed Data Objectを作成
  2. 1のデータに対して、現在のRecovery Keyで署名したJWSを生成。
  3. JWSデータを使って、Core Proof Fileを作成し、IPFSにアンカリングする。
  4. 3のCore Proof FileのURI、現在のDIDにコミットされているRecovery Keyのコミットメントに対応するReveal ValueからCore Index Fileを作成し、IPFSにアンカリングする。
  5. Core Index FileのURIからAnchor Stringを生成し、それをBitcoinにアンカリングする。

DID Documentの更新がないので、Provisional Index FileやChunk Fileが不要になる。

require 'sidetree'

Sidetree::Params.network = Sidetree::Params::Network::TESTNET

# 現在設定されているRecovery Key
recovery_key =
  Sidetree::Key.new(
    private_key:
      22_121_807_773_082_901_989_880_604_296_336_998_182_556_647_810_161_279_933_493_862_174_847_957_422_871
  )

# JWSを生成して現在のRecovery Keyで署名
claim = {
  didSuffix: did_suffix,
  recoveryKey: recovery_key.to_jwk.normalize
}
jws = Sidetree::Util::JWS.sign(claim, recovery_key)

ipfs = Sidetree::CAS::IPFS.new

# Core Proof Fileを作成
core_proof_file = Sidetree::Model::CoreProofFile.new(deactivate_proofs: [jws])
core_proof_file_uri = ipfs.write(core_proof_file.to_compress)

# Deactivate Operationを作成
deactivate_op =
  Sidetree::OP::Deactivate.new(
    did_suffix,
    jws,
    recovery_key.to_reveal_value
  )

# Core Index Fileを作成
core_index_file =
  Sidetree::Model::CoreIndexFile.new(
    core_proof_file_uri: core_proof_file_uri,
    deactivate_ops: [deactivate_op]
  )
core_index_file_uri = ipfs.write(core_index_file.to_compress)

anchor_str =
  Sidetree::Util::AnchoredDataSerializer.serialize(1, core_index_file_uri)

Anchor Stringは、

ion:1.QmWmJ5HWVvga46CcGkem7JTxMZtJrVtsuQDaPmRf3Pi54V

で、アンカリングしたBitcoinトランザクションは、1c18fc397aebd5c1ee0d12a973d8e9fb01d2c2945bf965508480efc422e78a23。ブロックに取り込まれると、IONのResolverでもDID Documentが無効化されたことが分かる(DID Documentの鍵やserviceが削除されている)↓

ということで、Sidetreeに定義された一連の操作(Create / Update / Recover / Deactivate)を一通りやってみた。

ブロックチェーンにアンカリングしたデータを使ってオフチェーンプロトコルを動作させるというコンセプトはやっぱり面白い。ブロックチェーンで担保するのはデータの適用順序だけ、後はオフチェーンのコントラクトやプロトコルで解決する。

IONのDIDを更新してみる

前回の記事でIONのDIDを登録したので↓

techmedia-think.hatenablog.com

今回は、登録したDIDの鍵を更新するUpdate操作をアンカリングしてみる。

Update操作

Update操作は、現在DIDに紐付けられているUpdate Keyを使ってDID Documentの更新(署名鍵やserviceの追加、削除など)を行う操作になる。更新の対象や内容は、DID State Patchesで定義する。

今回は、DIDに署名鍵として登録した公開鍵を新しい公開鍵に更新するreplaceアクションを行うことにする(Patchは複数設定することも可能)。

今回のUpdate操作に必要な手順は↓

  1. 新しい署名用の鍵ペアを生成する。
  2. 1の公開鍵のデータを元に、replaceを行うDID State Patcheを生成する。
  3. DIDに紐付ける新しいUpdate Keyを生成する(このUpdate Keyは、この更新後のDIDのステートを更新する際に使用されるUpdate Keyになる)。なので、更新用の鍵はワンタイムの鍵ということになる。
  4. 3の新しいUpdate Keyのコミットメントを生成する。
  5. 2と4のデータからUpdate Operation Delta Objectを生成する。
  6. 現在のUpdate Keyを使って、JWSデータを生成する。
    • ヘッダーはalg: ES256Kのみ指定。
    • JWSのペイロードは、以下の2つの要素で構成される。
      • 現在のUpdate KeyのデータであるupdateKey
      • 5のUpdate Operation Delta Objectのハッシュ値であるdeltaHash
    • ↑のデータに対して、現在のUpdate Keyで署名した署名値をセットする。
  7. 6のJWSのデータを使って、(Create操作では不要だった)Provisional Proof Fileを生成して、IPFSにアンカリングする。
  8. 5のdeltaファイルからChunk Fileを生成して、IPFSにアンカリングする。
  9. 以下のデータからProvisional Index Fileを生成して、IPFSにアンカリングする。
    • 7のProvisional Proof FileのURI
    • 8のChunk FileのURI
    • 更新対象のDID Suffix(Short-Form形式のDID)
    • 現在のDIDにコミットされているUpdate Keyのコミットメントに対応するReveal Value(JWKのSHA-256 Multihash。コミットメントはdoubule-SHA-256されてる)。
  10. 9のProvisional Index FileのURIを指定して、Core Index Fileを生成し、IPFSにアンカリングする。
  11. 10のCore Index FileのURIからAnchor Stringを生成し、それをBitcoinにアンカリングする。

前回のCreate操作と違うのは、Update操作が可能なことを暗号学的に証明するためのJWSの生成と、それをCASにアンカリングするためのProvisional Proof Fileの作成。

実際に操作を実行

前回登録したDIDを更新しようとしたんだけど、いろいろ試行錯誤して試してたら、

{
code: "canonicalized_object_hash_mismatch",
message: "Canonicalized update request update key object hash does not match expected hash 'EiDBQ56irlBskr7ZawFLcoH131ZvSLbfa2jK1MOny-feIQ'."
}

というエラー以降、更新できなくなってしまったので、新しいDIDを作成して(did:ion:test:EiBRrmEha_Q30GieEwLB-XM8CZd_b49dQ7znhaBxfAHTsQ)↓

こちらを更新することにする。Update処理のサポートをsidetreerbに実装して、↓を実行

require 'sidetree'

Sidetree::Params.network = Sidetree::Params::Network::TESTNET

did = generate_did # ↑で新規作成したSidetree::DIDオブジェクト(作成方法は前回の記事参照)

# DID新規作成時に設定したUpdate Key
update_key =
  Sidetree::Key.new(
    private_key:
      30_922_012_418_511_471_117_584_821_339_133_667_644_396_117_517_409_218_592_473_694_736_410_013_586_765
  )

# 新しい署名鍵を生成
new_signing_key = Sidetree::Key.generate(id: "signing-key")

# 置き換えるDID Documentを生成
document = Sidetree::Model::Document.new(public_keys: [new_signing_key])

# 次のUpdate Keyを生成
next_update_key = Sidetree::Key.generate

# Update delta objectを生成
delta =
  Sidetree::Model::Delta.new(
    [document.to_replace_patch],
    next_update_key.to_commitment
  )

# JWSを生成して署名
claim = { updateKey: update_key.to_jwk.normalize, deltaHash: delta.to_hash }
jws = Sidetree::Util::JWS.sign(claim, update_key)

ipfs = Sidetree::CAS::IPFS.new

# Provisional Proof Fileを作成
provisional_proof = Sidetree::Model::ProvisionalProofFile.new([jws])
provisional_proof_uri = ipfs.write(provisional_proof.to_compress)

# Update operationを作成
update_op =
  Sidetree::OP::Update.new(did.suffix, delta, jws, update_key.to_reveal_value)

# Chunk Fileを作成
chunk_file =
  Sidetree::Model::ChunkFile.create_from_ops(update_ops: [update_op])
chunk_file_uri = ipfs.write(chunk_file.to_compress)

# Provisional Index Fileを作成
provisional_index_file =
  Sidetree::Model::ProvisionalIndexFile.new(
    proof_file_uri: provisional_proof_uri,
    chunks: [Sidetree::Model::Chunk.new(chunk_file_uri)],
    operations: [update_op]
  )
provisional_index_file_uri = ipfs.write(provisional_index_file.to_compress)

# Core Index Fileを作成
core_index_file =
  Sidetree::Model::CoreIndexFile.new(
    provisional_index_file_uri: provisional_index_file_uri
  )
core_index_file_uri = ipfs.write(core_index_file.to_compress)

anchor_str =
  Sidetree::Util::AnchoredDataSerializer.serialize(1, core_index_file_uri)

最終的なAnchor Stringは、

ion:1.QmVKryfVemjT3Nxnea4aJpJRXFJkLpFjNfti1tJngJwBWs

Bitcoinにアンカリングしたトランザクション40ba3f1d10456be725ac176a333e42b0ef9fdf47cb9f325f8f5c4f69b2e931df

ブロックが承認され、IONで取り込まれると、先程のDIDを解決すると、↓のようにdid:ion:test:EiBRrmEha_Q30GieEwLB-XM8CZd_b49dQ7znhaBxfAHTsQの署名鍵が新しく生成した鍵で更新されていることが分かる。