Develop with pleasure!

福岡でCloudとかBlockchainとか。

IONのDIDをIPFSとBitcoinにアンカリングしてみる

前回は、IONのDIDを生成した↓

techmedia-think.hatenablog.com

↑では、DID Documentを生成し、IONのプロトコルに沿って識別子であるDIDを計算したけど、実際にこれだけだとローカルで計算してるだけなので、続いて、このDIDをCAS(Content Addressable Storage)とブロックチェーンにアンカリングしてみる。

CASへのアンカリング

Sidetreeでは、以下のファイルでDID操作のデータを管理している。

https://identity.foundation/sidetree/spec/diagrams/file-topology.svg

  • Core Proof File:DIDのRecoverDeactivate操作について、それらが適切な操作であることを証明する暗号学的な証明を含むファイル。具体的には、現在のRecovery Keyとそれに対して有効な署名など(Recoverの場合のみ、次のRecovery Keyのコミットメントを含む)。
  • Provisional Index File:DIDのUpdate操作を証明するデータ(DIDのsuffixデータ、その最新のUpdate Keyのコミットメントに対応するUpdate Key)に加えて、Provisional Proof FileのURIと各Chunk FileのURIが含まれるファイル。
    • Provisional Proof File:Update操作について、それが適切な操作であることを証明する暗号学的な証明を含むファイル。具体的には、現在のUpdate Keyとそれに対して有効な署名が含まれるファイル。
    • Chunk File:DIDの状態を更新する際の、変更のPatchデータや、次のUpdate Keyのコミットメントなどが含まれる。
  • Core Index File:CreateRecoverDeactivateの3つのDID操作の値と、関連するProvisional Index FileのCASのURIが定義されたファイル。RecoverDeactivate操作の場合は、追加でCore Proof FileのURIが含まれる。

この内、最後のCore Index FileのURIブロックチェーンにアンカリングされる。URIからこのファイルを取得すれば、このファイルで行われたDID操作がリストされおり、また各関連ファイルを辿れるようになっている。

新規作成したDIDのアンカリング

今回はDIDのCreate操作をアンカリングしてみる。この場合、作成するのは、Chunk Fileと作成したChunk FileのURIが含まれるProvisional Index Fileおよび、Provisional Index FileのURIとCreateオペレーションのデータが含まれるCore Index Fileの3つ。※ Update/Recover/DeactivateはしないのでProof系のファイルは作らない。

まず、アンカリング対象のDIDを作成(did:ion:test:EiCFPfLlfe_2D8UxjJtGsaE5zsTisGZiOSU16dgq6vHDOw)↓*1

require 'sidetree'

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

recovery_key = Sidetree::Key.generate
update_key = Sidetree::Key.generate
signing_key = Sidetree::Key.generate(id: 'signing-key')

document = Sidetree::Model::Document.new(public_keys: [signing_key])

did = Sidetree::DID.create(document, update_key, recovery_key, method: Sidetree::Params::METHODS[:ion])

Long-FormのDIDは、

Long-Form DID: did:ion:test:EiCFPfLlfe_2D8UxjJtGsaE5zsTisGZiOSU16dgq6vHDOw:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJzaWduaW5nLWtleSIsInB1YmxpY0tleUp3ayI6eyJjcnYiOiJzZWNwMjU2azEiLCJrdHkiOiJFQyIsIngiOiJ0TVFEbnFkLUZIRGJjaGxwYzl6b0JsbVlib29hNk9NRVZyNlZTWERhTHpZIiwieSI6IlVzNEtWYy12OFBRYnJWUTRtdjNvczhoVG1lQUZ3OE5XRFJ6TmY4NHltdXMifSwicHVycG9zZXMiOltdLCJ0eXBlIjoiRWNkc2FTZWNwMjU2azFWZXJpZmljYXRpb25LZXkyMDE5In1dLCJzZXJ2aWNlcyI6W119fV0sInVwZGF0ZUNvbW1pdG1lbnQiOiJFaURCUTU2aXJsQnNrcjdaYXdGTGNvSDEzMVp2U0xiZmEyaksxTU9ueS1mZUlRIn0sInN1ZmZpeERhdGEiOnsiZGVsdGFIYXNoIjoiRWlCeExEX1BVNEVlQlpVNXpKV2wxMGtvTUZmTmtjQTZ4TExvZ0FOWTdfakxKQSIsInJlY292ZXJ5Q29tbWl0bWVudCI6IkVpQ0NhR3l5YU4yOFMydUc4MS1kTjl3c1ZQVTZoUTVYb1AxZFJtQ3pONU9uNUEifX0

↑のDIDを実際にアンカリングしてみる。手順は、

  1. 新規DIDのCreate Operationデータを生成。
  2. 1のデータを使ってChunk Fileを生成。今回のケースだとCreate OperationのDeltaデータが格納される。つまり、登録するDID Documentが含まれるデータ。
  3. Chunk FileをIPFSに書き込み、URIを取得。
  4. 3のURIを持つProvisional Index Fileを生成。今回のケースだとCreateのみなので、2のURIのみ。Updateが含まれる場合は、Provisional Proof FileのURIやオペレーションデータが含まれる。
  5. Provisional Index FileをIPFSに書き込み、URIを取得。
  6. 1のCreate Operationのデータと5のProvisional Index FileのURIからCore Index Fileを生成。今回のケースだとCreateのみなので、5のURIとオペレーションデータのみ。UpdateやRecoverがあれば別途Core Proof FileのURIが含まれる
  7. Core Index FileのURIをIPFSに書き込み、URIを取得。

↑のDIDについて、生成した2のChunk Fileのデータは↓

{"deltas":[{"patches":[{"action":"replace","document":{"publicKeys":[{"id":"signing-key","publicKeyJwk":{"crv":"secp256k1","kty":"EC","x":"tMQDnqd-FHDbchlpc9zoBlmYbooa6OMEVr6VSXDaLzY","y":"Us4KVc-v8PQbrVQ4mv3os8hTmeAFw8NWDRzNf84ymus"},"purposes":[],"type":"EcdsaSecp256k1VerificationKey2019"}],"services":[]}}],"updateCommitment":"EiDBQ56irlBskr7ZawFLcoH131ZvSLbfa2jK1MOny-feIQ"}]}

Provisional Index Fileのデータは↓

{"chunks":[{"chunkFileUri":"QmSVy6tUexQdmzBPpMEGjBs7ajB5dyGKuqZrhZMgvxqt1Q"}]}

Core Index Fileのデータは↓

{"provisionalIndexFileUri":"Qmc6LnGEqYNZ3YKhGNo3fMn8pUVrhXtTWKZEwhWt3H5vMo","operations":{"create":[{"suffixData":{"deltaHash":"EiBxLD_PU4EeBZU5zJWl10koMFfNkcA6xLLogANY7_jLJA","recoveryCommitment":"EiCCaGyyaN28S2uG81-dN9wsVPU6hQ5XoP1dRmCzN5On5A"}}]}}

いずれもIPFSに格納されるデータは↑をGZIP圧縮したデータになる。

sidetreerbに実装してみた↓(事前にIPFS daemonをローカルで実行しておく)。

require 'sidetree'

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

# DIDの生成
recovery_key = Sidetree::Key.generate
update_key = Sidetree::Key.generate
signing_key = Sidetree::Key.generate(id: "signing-key")

document = Sidetree::Model::Document.new(public_keys: [signing_key])

did = Sidetree::DID.create(
      document,
      update_key,
      recovery_key,
      method: Sidetree::Params::METHODS[:ion]
    )

# DIDのCreate Operationを生成
create_op = did.create_op

# Chunk Fileを生成
chunk_file =  Sidetree::Model::ChunkFile.create_from_ops(create_ops: [create_op])

# IPFSクライアントを初期化(パラメータ省略するとデフォルトでhttp://localhost:5001/api/v0にアクセス)
ipfs = Sidetree::CAS::IPFS.new

# Chunk FileをIPFSに書き込み
chunk_file_uri = ipfs.write(chunk_file.to_compress)

# Provisional Index Fileを作成しIPFSに書き込み
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を作成しIPFSに書き込み
core_index_file = Sidetree::Model::CoreIndexFile.new(
          create_ops: [create_op],
          provisional_index_file_uri: provisional_index_file_uri
        )
core_index_file_uri = ipfs.write(core_index_file.to_compress)

最終的なCore Index FileのURIは、

QmQPk9Vi9iSm4aAPEd73txGjqwbHhLYWfoUEpG5eYmy2PW

Bitcoinへのアンカリング

CASへのアンカリングが出来たら、続いてブロックチェーンにアンカリングする。ここで、ブロックチェーンにアンカリングするデータは、Anchor Stringとして定義されているデータで、↑で作成したCore Index FileのURIと、操作の数で構成される。CASにアンカリングされた関連操作はCore Index Fileで参照可能になっているので、そのURIブロックチェーンにアンカリングしている。

Anchor Stringは、単純に操作数とCore Index FileのURI.で連結したデータ(例:1000.QmWd5PH6vyRH5kMdzZRPBnf952dbR4av3Bd7B2wBqMaAcf)。

なので、今回は、Anchor Stringにprefix(ion:)を付けた以下のデータをOP_RETURNを使ってBitcoinトランザクションに埋め込めばいい。

ion:1.QmQPk9Vi9iSm4aAPEd73txGjqwbHhLYWfoUEpG5eYmy2PW

実際に発行したトランザクション45e4459c7f3a36124b8b5df5c5b9314c4187833056f89a0c88ad57997fe1b051。OP_RETURNに↑が記録されているのが分かる。

ちなみに、↑のアンカリングTxの手数料が最低額を下回ってるとBitcoin的にはブロックに格納されるけど、IONとしては有効なCoreファイルとしてエラー処理される。

トランザクションがブロックに格納されると、ionノードを起動していれば新しいブロック内のアンカリングトランザクションを検知し、DIDの処理が行われる↓

Fetched 1 Sidetree transactions from blockchain service in 42 ms.
CommandSucceededEvent {
  connectionId: 'localhost:27017',
  requestId: 13,
  commandName: 'find',
  duration: 28,
  reply: {
    cursor: { firstBatch: [Array], id: 0, ns: 'ion-testnet-core.transactions' },
    ok: 1
  }
}
Downloading core index file 'QmQPk9Vi9iSm4aAPEd73txGjqwbHhLYWfoUEpG5eYmy2PW', max file size limit 1000000 bytes...
Downloading file 'QmQPk9Vi9iSm4aAPEd73txGjqwbHhLYWfoUEpG5eYmy2PW', max size limit 1000000...
Successfully kicked off downloading/processing of all new Sidetree transactions.
Processing previously unresolvable transactions if any...
CommandSucceededEvent {
  connectionId: 'localhost:27017',
  requestId: 14,
  commandName: 'find',
  duration: 1,
  reply: {
    cursor: {
      firstBatch: [],
      id: 0,
      ns: 'ion-testnet-core.unresolvable-transactions'
    },
    ok: 1
  }
}
Fetched 0 unresolvable transactions to retry in 1 ms.
Event emitted: sidetree_observer_loop_success
Waiting for 60 seconds before fetching and processing transactions again.
Read and pinned 240 bytes for CID: QmQPk9Vi9iSm4aAPEd73txGjqwbHhLYWfoUEpG5eYmy2PW.
Event emitted: sidetree_download_manager_download: {"code":"success"}
File 'QmQPk9Vi9iSm4aAPEd73txGjqwbHhLYWfoUEpG5eYmy2PW' of size 240 downloaded.
Downloading provisional index file 'Qmc6LnGEqYNZ3YKhGNo3fMn8pUVrhXtTWKZEwhWt3H5vMo', max file size limit 1000000...
Downloading file 'Qmc6LnGEqYNZ3YKhGNo3fMn8pUVrhXtTWKZEwhWt3H5vMo', max size limit 1000000...
Read and pinned 93 bytes for CID: Qmc6LnGEqYNZ3YKhGNo3fMn8pUVrhXtTWKZEwhWt3H5vMo.
Event emitted: sidetree_download_manager_download: {"code":"success"}
File 'Qmc6LnGEqYNZ3YKhGNo3fMn8pUVrhXtTWKZEwhWt3H5vMo' of size 93 downloaded.
Downloading chunk file 'QmSVy6tUexQdmzBPpMEGjBs7ajB5dyGKuqZrhZMgvxqt1Q', max size limit 10000000...
Downloading file 'QmSVy6tUexQdmzBPpMEGjBs7ajB5dyGKuqZrhZMgvxqt1Q', max size limit 10000000...
Read and pinned 308 bytes for CID: QmSVy6tUexQdmzBPpMEGjBs7ajB5dyGKuqZrhZMgvxqt1Q.
Event emitted: sidetree_download_manager_download: {"code":"success"}
File 'QmSVy6tUexQdmzBPpMEGjBs7ajB5dyGKuqZrhZMgvxqt1Q' of size 308 downloaded.
Parsed chunk file in 1 ms.
CommandSucceededEvent {
  connectionId: 'localhost:27017',
  requestId: 23,
  commandName: 'update',
  duration: 1,
  reply: { n: 1, upserted: [ [Object] ], nModified: 0, ok: 1 }
}
Processed 1 operations. Retry needed: false
Transaction 1.QmQPk9Vi9iSm4aAPEd73txGjqwbHhLYWfoUEpG5eYmy2PW is confirmed at 2315260

そして、ローカルで実行中のIONにDIDを問い合わせると↓、DIDの解決に無事成功!

http://localhost:3000/identifiers/did:ion:test:EiCFPfLlfe_2D8UxjJtGsaE5zsTisGZiOSU16dgq6vHDOw

今回の最小限のCreate操作で、CASやブロックチェーンにどうデータをアンカリングしているのかが大体分かってきた。

*1:前回作成したDIDは、DID Document内の鍵のtypeの設定が漏れてたので再生成。

非対話型のSchnorr署名のハーフアグリゲーション

先日、Blockstreamのブログ記事↓で、非対話型のSchnorrの署名集約スキームについて知ったので、

medium.com

↑にリンクされているドラフトBIPに記載されていた↓のペーパーを斜め読みしてみた。

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

Schnorr署名の集約と言えば、Bitcoinの文脈だと、MuSig2などの対話型の集約プロトコルが有名で、このような集約方法をフルアグリゲーションと呼ぶみたい。これはマルチシグをSchnorr署名の集約特性を利用して実現する方式で、複数の参加者が共通のメッセージに対して対話的に署名を生成し、それを単一の署名に集約する。

↑のハーフアグリゲーションは、マルチシグのような対話型の署名方式ではなく、署名者同士はそれぞれ互いを知らずに個々の署名を生成し、その後不特定の集約者がその署名を集約することを想定している。マルチシグは対話型で、共通のチャレンジに対して署名を生成し、それが単一のSchnorr署名と見分けが付かないような集約アプローチになるのに対して、ハーフアグリゲーションでは各署名者が異なるメッセージにそれぞれ署名し、その署名のサイズが個々の署名データの合算値の約半分になるというもの(フルアグリゲーションのように単一のSchnorr署名と同じサイズになる訳ではない)。そして個々の有効なSchnorr署名をインクリメンタル/シーケンシャルに集約することができる。

この仕組みを活用すると、例えば、

といった事が可能になる。

ペーパーでは、3つのハーフアグリゲーション方式が説明されている(以下、表記は楕円曲線ベースのものに変えてる)。

ASchnorr

Chalkiasらによって提案された集約方式をASchnorrと呼んでいる。

公開鍵 {X_1, X_2}とメッセージ {m_1, m_2}に対するSchnorr署名を {\sigma_1 = (R_1, s_1), \sigma_2 = (R_2, s_2)}として、各チャレンジを {c_1 = H_1(R_1, m_1), c_2 = H_1(R_2, m_2)}とした場合、それぞれ有効なSchnorr署名であれば、以下が成立する。

  •  {s_1G = R_1 + c_1X_1}
  •  {s_2G = R_2 + c_2X_2}

この2つの署名を単純にハーフアグリゲーションしようとすると、 {\tilde{s} = s_1 + s_2}とする方法で、 {\tilde{s}G = R_1 + c_1X_1 + R_2 + c_2X_2}が成立するか検証する方法。

ただ、この集約方法は安全ではない。ある署名のチャレンジ(たとえば、 {c_1})がもう1つの署名のコミットメント( {R_2})に依存していないため、例えば {R_2} {R_2 = r_2G - (R_1 + c_1X_1)}と細工すれば、 {X_1}の離散対数の知識を必要とせずに、 {X_2}の離散対数の知識のみで集約署名を偽造することができる。

ASchnorrでは、各署名に以下のような外部係数を使用して、この偽造問題に対処している。

 {L = \lbrace (R_1, X_1, m_1), (R_2, X_2, m_2) \rbrace}

および、

 {a_1 = H_2(L, 1), a_2 = H_2(L, 2)}

として、集約署名を {\tilde{s} = a_1s_1 + a_2s_2}として、その署名の有効性の検証は、 {\tilde{s}G = a_1 \cdot (R_1 + c_1X_1) + a_2\cdot(R_2 + c_2X_2)}とするというもの。MuSigのアプローチと似てる。

IASchnorr

↑のASchnorrで集約は可能だが、インクリメンタルな集約サポートしていないので、個々の署名がすべて集まってから集約する必要がある。そこで、インクリメンタルな集約をサポートするハーフアグリゲーション方式がIASchnorrと呼ばれる方式。

↑の2つの署名が集約された後に、公開鍵 {X_3}とメッセージ {m_3}に対する署名 {\sigma_3 = (R_3, s_s)}が届いて、これを既存の集約署名に追加するケースを想定する。

単純に集約使用とすると、既に集約済みの署名を通常の署名として扱うことで再度ASchnorrを行うというもの。つまり、

 {L' = \lbrace L, (R_3, X_3, m_3) \rbrace}

および、

 {a'_1 = H_2(L', 1), a'_2 = H_2(L', 2)}

として、 {\tilde{s}' = a'_1 \tilde{s} + a'_2s_3}という集約をする方法。

ただ、この方法の問題は、3つの署名が同じタイミングで集約された場合と、↑のようにインクリメンタルに集約された場合とで、集約結果の署名値が異なり、正しく検証するために、それがどのように集約されたのかという情報が追加で必要になる。

IASchnorrでは、係数を個々の署名すべてから計算するのではなく、それ以前の署名にのみ依存させるよう生成することで、この問題に対処するようになっている。つまり

 {L_i = \lbrace (R_1, X_1, m_1), ..., (R_i, X_i, m_i)\rbrace}

として、i番めの係数は {a_i = H_2(L_i)}となる。こうすると、↑のように署名集約のタイミングによって集約値が異なるようなケースを回避できる。

この方式では、n個の公開鍵とメッセージに対する最終的な署名値は、 {(\lbrace R_1, ..., R_n \rbrace, \tilde{s}'_n)}

SASchnorr

最後に、SASchnorrが、このペーパーで新しく提案されているハーフアグリゲーションの方式で、シーケンシャルに集約した個別の署名をリカバリーできるという特徴を持つ。また集約の方式もASchnorrやIASchnorrとは結構違う。

署名者nは、n-1個の公開鍵 {X_1, ..., X_{n-1}}とメッセージ {m_1, ..., m_{n-1}}に対する集約署名 {(\tilde{R}_{n-1}, \lbrace s_1, ..., s_{n-1} \rbrace)}を受け取ると、 {R_n = r_nG}の代わりに集約コミットメント {\tilde{R_n} = \tilde{R}_{n-1} + R_n}を計算し、チャレンジ {c_n = H(\tilde{R}_n, X_n, m_n, s_{n-1}, n)}を計算し、署名値 {s_n = r_n + c_n x_i}を計算する。

 {(\tilde{R}_{n}, \lbrace s_1, ..., s_{n} \rbrace)}を受け取った検証者は、 {c_n}を計算すると、 {R_n = s_nG - c_nX_n}を計算することで、1つ前の {\tilde{R}_{n-1} = \tilde{R}_n - R_n}を復元することができる。これを繰り返すことで、検証者はすべての個々の署名を復元することができる。

ペーパーでは、ランダムオラクルモデルの下で、Schnorr署名と同等の安全性を証明している。

↑の方式見た感じだと、各署名値が前の署名値に依存しているので、署名フェーズがシーケンシャルになる必要が出てくる。

データサイズ

↑のようにn個の公開鍵とメッセージに対して、以下署名データが必要になる。

  • IASchnorrの場合、 {(\lbrace R_1, ..., R_n \rbrace, \tilde{s}'_n)}=n個のRと1つのs
  • SASchnorrの場合、 {(\tilde{R}_{n}, \lbrace s_1, ..., s_{n} \rbrace)}=1つのR値とn個のs値

個別のSchnorr署名だとn個のRとn個のsで構成されるため、約半分のデータサイズになっていることが分かる。だからハーフアグリゲーションというネーミングなのね。

ドラフトBIP

ドラフトBIPでは、インクリメンタルなハーフアグリゲーションをサポートするようなので、↑の2番めのアプローチがベースになってる模様。まぁ、ブロックやトランザクションの署名を集約するようなケースだと、SASchnorrのようなシーケンシャルな署名プロセスは採用できないので、インクリメンタルなアプローチになるよね。SASchnorrの方は証明書チェーンやルーティングプロトコルなでの利用が期待されているみたい。

IONのDIDを作ってみる

IONはMSが中心になって開発したDIDのプロトコルの1つ(did:ion)。実体は、アンカリング先のブロックチェーンBitcoin、Content-Addressed Storage System(CAS)ノードにIPFSを用いたSidetreeプロトコルになる。この他にアンカリング先をEthereumにしてSidetreeを実装したElement(did:elem)とか、他にもFabricやAmazon QLDB、S3を使った実装もあるみたい。

今回は、そんなION(正確にはSidetree)で発行されるDIDを実際に作ってみる。

DIDの作成

Sidetreeでは、以下の3つの鍵ペアを生成する。

  • 署名鍵:DID自体に関連付けられるメインの鍵で、DIDのユースケースにおける署名や認証に用いる鍵。
  • Update Key:DIDの更新とUpdate Key自体の更新に使用する鍵。
  • Recovery Key:DIDの更新、失効とUpdate Key、Recovery Key自体の更新に使用する鍵。

これらの鍵を利用して、DIDのCreate / Update/ Recover / Deactivateといった操作をサポートする。鍵は、Bitcoinと同じ楕円曲線secp256k1の鍵で、デジタル署名スキームはECDSA。

DIDを作成する場合、まず最初に上記の3つの鍵ペアを生成する。

続いて、DID Documentを作成する。DID DocumentにはDIDに関連付けられる公開鍵のリスト(↑の署名鍵)と、serviceを定義できる。serviceは、このDIDの保持するエンティティのドメインのエンドポイントや、Identity Hubのエンドポイントなどを指定できる。

次に、DIDのステートを変更するためのDID State Patcheを作成する。具体的には、DID Documentの内容を元に以下のようなreplace Patch ActionのJSONオブジェクトを作成する(今回はserviceは未定義)。

[
  {
    "action": "replace",
    "document": {
      "publicKeys": [
        {
          "id": "signing-key",
          "publicKeyJwk": {
            "crv": "secp256k1",
            "kty": "EC",
            "x": "LyZoDfdajWdfTlAqUT8V8Kb3Xy0Rc9PVXoiGpS82prs",
            "y": "vgV-yaevK2o0SXobM996p6RLCurb97ySnEj5mLBd5B0"
          },
          "purposes": []
        }
      ],
      "services": []
    }
  }
]

次にUpdate Keyと作成したPatchデータからDeltaオブジェクトを生成する↓

{
  "patches": [
    <↑のPatchオブジェクト>
  ],
  "updateCommitment": "EiB4CgS-uYVMAEKJOI1rV061MTUPqJW8tzjOCDOrBHERjA"
}

ここでUpdate Keyはそのまま使われるのではなく、Json Web Key形式のJCSで正規化されたJSONデータをSHA256ハッシュし、それをMultihashでエンコードしたものを更にBase64エンコードしたものを使用する。つまりUpdate Keyに対するコミットメントを指定することになる。

※ 基本的にDID Documentで公開する必要のある署名鍵以外(Update KeyおよびRecovery Key)は、登録時にはそのコミットメント値が使用される。

続いて、↑のDeltaオブジェクトのハッシュとRecovery Keyを使ってSuffixオブジェクトを生成する↓

{
  "deltaHash": "EiCUyXdZZrJH18glwLiuSpFM_CT5LRP7SRqcTcDnNRHEOw",
  "recoveryCommitment": "EiAN0dDol63ocIfiQQK9TiPYoOd6lXM4pr7JzpVj7h4VXw"
}

ここでも、Recovery Keyのコミットメントが使われる。また、Deltaオブジェクトのハッシュも、これらのコミットメントと同様の方法で計算される。

そして最後に、↑のSuffixオブジェクトのハッシュ値がDIDの値になる。

IONの場合、DIDの形式としてShort FormのDIDとLong FormのDIDの2通りの表現があり、Long FormのDIDはDeltaオブジェクト、Suffixオブジェクト両方を展開したJSONデータから作られる。つまり、DIDドキュメントを含むすべてのデータで構成される。そのため、ブロックチェーンやIPFSなどにアンカリングされていないDIDでも、Long Form形式のDIDであれば、そのデータを使ってDIDの検証がそのままできる。これは、そもそもDIDをブロックチェーンやIPFSにアンカリングせずに利用可能になるというメリットや*1、アンカリングまでの時間的な遅延の解消などに有用な仕組み。

↑のように、SidetreeはDIDへの変更をPatcheという形で定義して管理しようとしているのが分かる。UpdateRecoverという操作では、変更内容を定義したDeltaオブジェクトを作成し、現在有効なUpdate Key / Recovery Keyのコミットメントに対応する公開鍵とそのデジタル署名を提供することで、DIDに対する変更を適用するようになっている。発行時のDIDに対して、順番にパッチを適用していけば、最新のDIDの有効性が検証できるみたいな感じかな。

Rubyで実装してみた

なかなかドキュメントだけ見てても理解が深まらないので、↑の処理をRuby実装してみた。ロジックの詳細はコード参照

require 'sidetree'

recovery_key = Sidetree::Key.generate
update_key = Sidetree::Key.generate
signing_key = Sidetree::Key.generate(id: 'signing-key')

document = Sidetree::Model::Document.new(public_keys: [signing_key])

did = Sidetree::DID.create(document, update_key, recovery_key, method: Sidetree::Params::METHODS[:ion])

puts "DID: #{did.short_form}"
=> "did:ion:EiC4gAQxXVCnbGQKFW7wgr0nbrOxXsF5CnglU8j1sXARaA"

puts "Long-Form DID: #{did.to_s}"
=> "did:ion:EiC4gAQxXVCnbGQKFW7wgr0nbrOxXsF5CnglU8j1sXARaA:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJzaWduaW5nLWtleSIsInB1YmxpY0tleUp3ayI6eyJjcnYiOiJzZWNwMjU2azEiLCJrdHkiOiJFQyIsIngiOiJyU2RyX3dMSno5WEVFRHFkRGo4RjZCeDNKanlXR3RVZjNJY1c0OWJxQ21VIiwieSI6IlBwTUtvTXpmVHoxSVdnRDlyU3lUMC1Ya2NKTzFIb2JtWmtXMG1zM3lvaE0ifSwicHVycG9zZXMiOltdfV0sInNlcnZpY2VzIjpbXX19XSwidXBkYXRlQ29tbWl0bWVudCI6IkVpRDMtOXE1ZjRYUTEwWWQySlNzM1QxYTh4Vloycmw2VFNic0xYYVdzVVBHbUEifSwic3VmZml4RGF0YSI6eyJkZWx0YUhhc2giOiJFaUFHblhKellkUDREcUk2blRnZjRSblFQQjRVaUVVS3pCZldWQlVRRXlkZ3FRIiwicmVjb3ZlcnlDb21taXRtZW50IjoiRWlCVy1PRWRMLUtWYWo4NG42OEhDclRpcWNFd1RFRFZ6Z1I4bDhsb2tQc2NrdyJ9fQ"

op = did.create_op

puts "DID suffix data: #{op.suffix.to_h.to_json}"
=> "DID suffix data: {"deltaHash":"EiAGnXJzYdP4DqI6nTgf4RnQPB4UiEUKzBfWVBUQEydgqQ","recoveryCommitment":"EiBW-OEdL-KVaj84n68HCrTiqcEwTEDVzgR8l8lokPsckw"}"

puts "Document delta: #{op.delta.to_h.to_json}"
=> "Document delta: {"patches":[{"action":"replace","document":{"publicKeys":[{"id":"signing-key","publicKeyJwk":{"crv":"secp256k1","kty":"EC","x":"rSdr_wLJz9XEEDqdDj8F6Bx3JjyWGtUf3IcW49bqCmU","y":"PpMKoMzfTz1IWgD9rSyT0-XkcJO1HobmZkW0ms3yohM"},"purposes":[]}],"services":[]}}],"updateCommitment":"EiD3-9q5f4XQ10Yd2JSs3T1a8xVZ2rl6TSbsLXaWsUPGmA"}"

DIDの作成や操作方法は分かってきたので、次は、IPFSやBitcoinへのアンカリング方法を確認したい。

*1:ただ、アンカリングしないとそのDIDが最新かどうかは検証できない。こういった特性が必要な場合はアンカリングが必要になる。まぁ多くのユースケースではDIDの有効性よりVCの有効性を検証することの方が重要なんじゃないかとも思えるけど。

USDTを使ってBitcoin Coreをトレース

Bitcoin Coreでは、User-space Statically Defined Tracing(USDT、※Tetherではない)ベースのトレースポイントが提供されており、Linux環境で動作するBitcoin Coreでは、このトレースポイントを使って、内部のデータにアクセスできるようになっている。

Bitcoin Coreのトレースポイント

Bitcoin Coreには、v23.0の時点で以下のトレースポイントが組み込まれている。

参考:https://github.com/bitcoin/bitcoin/blob/master/doc/tracing.md

トレースポイント 内容
net:inbound_message P2Pネットワーク上のピアからメッセージを受信した際に呼び出される
net:outbound_message P2Pネットワーク上でピアにメッセージを送信する際に呼び出される
validation:block_connected ブロックがチェーンに接続された後に呼び出される
utxocache:flush インメモリのUTXOキャッシュがフラッシュされた後に呼び出される
utxocache:add UTXOキャッシュにコインが追加された際に呼び出される
utxocache:spent UTXOキャッシュからコインが使用された際に呼び出される
utxocache:uncache UTXOキャッシュから意図的にアンロードされ際に呼び出される
coin_selection:selected_coins SelectCoinsが完了すると呼び出される
coin_selection:normal_create_tx_internal 最初のCreateTransactionInternalが完了した際に呼び出される
coin_selection:attempting_aps_create_tx CreateTransactionInternalが楽観的なAvoid Partial Spendsの選択試行のために2回目に呼び出された際に呼び出される
coin_selection:aps_create_tx_internal Avoid Partial Spendが有効になっている2回目のCreateTransactionInternalが完了すると呼び出される

各トレースポイントで取得可能なデータについては↑のドキュメント参照。

トーレス

実際に、上記のトレースポイントに対してトレースしてみる。Bitcoin Coreではリポジトリcontrib/tracingにトレースのサンプルが用意されてる。

bpftraceを使ったトレース

最初は、bpftraceを使ったトレース。

bpftraceをまだインストールしていない場合はインストールから↓(Ubuntu

$ sudo apt-get install bpftrace

P2Pのネットワークメッセージの情報を出力するbpftraceのスクリプトが用意されてるので実行してみる。リポジトリのルートから以下のコマンドを実行↓

$ sudo bpftrace contrib/tracing/log_p2p_traffic.bt
Attaching 3 probes...
Logging P2P traffic
outbound 'inv' msg to peer 0 (outbound-full-relay, 73.166.84.222:8333) with 2 bytes
inbound 'inv' msg from peer 0 (outbound-full-relay, 159.100.255.18:8333) with 1 bytes
outbound 'getdata' msg to peer 0 (outbound-full-relay, 159.100.255.18:8333) with 2 bytes
inbound 'inv' msg from peer 0 (outbound-full-relay, 94.231.253.18:8333) with 1 bytes
outbound 'addrv2' msg to peer 0 (outbound-full-relay, 104.36.175.37:8333) with 2 bytes
inbound 'tx' msg from peer 0 (outbound-full-relay, 159.100.255.18:8333) with 1 bytes
inbound 'inv' msg from peer 0 (outbound-full-relay, 65.21.122.162:8333) with 1 bytes
inbound 'inv' msg from peer 0 (outbound-full-relay, 128.0.51.17:8333) with 1 bytes
...

と、net:inbound_messagenet:outbound_messageの2つのトレースポイントをフックして、データを出力できる。

log_p2p_traffic.btに実行中のbitcoindのパスを記載する必要があり、ローカルでビルドしたものではない場合、適宜パスを修正する必要がある。

pythonでのトレース

サンプルとして、Pythonのスクリプトもある。こちらは、事前に↓のインストールが必要

$ sudo apt-get install python3-bpfcc 

そして、p2p_monitor.pyを実行すると、

$ sudo python3 contrib/tracing/p2p_monitor.py <実行中のbitcoindのパス>

ターミナルにP2Pメッセージのやりとりが表示される↓

Pythonの方は、BCC(BPF Compiler Collection)を使ってeBPF用のバイトコードコンパイルしていて、eBPFのコードはPythonのコード内にCで書く必要がある。bpftraceの方は、独自のDSLを記述する形なので、DSLの仕様さえ学習すればこっちの方がカジュアルに使えそう。

MWEBチェーン上での送金と非対話型のトランザクションの構築

前回、MWEBチェーンへのペグインまで行ったので↓

techmedia-think.hatenablog.com

今回は、MWEBチェーン上でMWトランザクションを使った送金をしてみる。

MWEB上での送金

新しいmwebタイプのアドレスを作成し、そこに送金する↓

$  ./litecoin-cli sendtoaddress tmweb1qqfhtsgcqxz0ez0je526ay5v8uerrtjsu33q7may3v43kwe4n0paz7qlwzsag2dschehz7qm4p4f8gfpu3cngpwsthjckhlah9txn24ukxulxx2yf 0.0001
2c83e8f14fed5bb6c8d4630c0d833f860dc48262b31af05f3d3acab8a362ca9e

このトランザクションの内容は↓

$ ./litecoin-cli getrawtransaction 2c83e8f14fed5bb6c8d4630c0d833f860dc48262b31af05f3d3acab8a362ca9e 1
{
  "txid": "2c83e8f14fed5bb6c8d4630c0d833f860dc48262b31af05f3d3acab8a362ca9e",
  "hash": "2c83e8f14fed5bb6c8d4630c0d833f860dc48262b31af05f3d3acab8a362ca9e",
  "version": 2,
  "size": 2203,
  "vsize": 0,
  "weight": 0,
  "locktime": 2352363,
  "vin": [
    {
      "ismweb": true,
      "output_id": "147785fb93659118be70445d947496dbedd7983086446f73d5f803eec2ff7691"
    }
  ],
  "vout": [
    {
      "ismweb": true,
      "output_id": "fc93c77ff288e5e2a0247662ffaf5470878b99a2805a76b8e4715d1b5ebfbf1c"
    },
    {
      "ismweb": true,
      "output_id": "fda598167f8f0973b711427c21673a90093b9ca2b8cd6ef285c7879d6ecb2f22"
    }
  ],
  "vkern": [
    {
      "kernel_id": "9eca62a3b8ca3a3d5ff01ab36282c40d863f830d0c63d4c8b65bed4ff1e8832c",
      "fee": 0.00003900,
      "pegin": 0.00000000,
      "pegout": [
      ]
    }
  ],
...

※ ちなみにトランザクションがブロックに格納されるとgetrawtransactionはデータを返さなくなる。これはトランザクション・カットスルーによりチェーン上から消えるためだと思われる。ただ、ウォレットRPCであるgettransactionを使えば確認は可能。

↑のレスポンスを見ると、アウトプットにはoutput_idしか表示されていないが、MWEBのアウトプットは以下の要素で構成されており、output_idはこれらのデータから生成したハッシュ値になる。

  • commitment:コインの量を秘匿したPedersen commitment
  • sender_pubkey:送信者がトランザクション作成時に作成する一時鍵
  • receiver_pubkey:受信者の公開鍵。ステルスアドレスの使用鍵の公開鍵に共有シークレットから導出した値を乗算して導出される公開鍵であるため、チェーン上でステルスアドレスの公開鍵と直接リンクすることはない。
  • message:アウトプットに関する以下のメタデータ
    • 機能ビット
    • 鍵交換に使用する公開鍵:送信者が生成したシークレットを受信者のステルスアドレスの使用鍵の公開鍵に乗算して導出した公開鍵。
    • View Tag:受信者が自分宛の送金であることの判断に使用するタグ。送信者が生成したシークレットを受信者のステルスアドレスのスキャン鍵の公開鍵に乗算して導出した公開鍵のハッシュ値の先頭1バイト。
    • 暗号化されたブラインドファクター
    • 暗号化されたコインの量
  • rangeproof:コミットしたコインの量がある範囲内にあることを証明する範囲証明のデータ
  • signature:送信者がsender_pubkeyに対応する秘密鍵messageに署名した署名データ

トランザクションは送信者が作成するので、受信者にだけ送信したアウトプットのコインの量とブラインドファクターを伝えるため、それぞれ暗号化した上でmessageにセットしている。暗号鍵は送信者と受信者間のECDHで導出。

↑のデータもgettransaction RPCで返してくれればいいんだけど現状output_idしか返ってこない。一応、ブロックに格納されればgetblock RPCをverbosityを2で呼べば、上記のデータも確認できる。

$ ./litecoin-cli getblock 28a45a7be6451ee0ea3711f98a5ccd873a924527ab57b2770c7f566d381d1edf 2
{
...
  "mweb": {
  ...
    "outputs": [
      {
        "output_id": "fc93c77ff288e5e2a0247662ffaf5470878b99a2805a76b8e4715d1b5ebfbf1c",
        "commit": "09ae2d9b5e4165866b243f0f9904c1cad209fd1555ac73f082ffd54da84f104779",
        "sender_pubkey": "03142b67a14b6a5b25c5ad6dcc27e9460df313e2471fa27423afdba16fb1cd687b",
        "receiver_pubkey": "03de1719eed6d102124709c1ea77f9dd1685a55f65a9528d8f95e9c3266baed22a",
        "range_proof": "45ba23016fe6bd67061a31b075e92368cbcc0866bca5676eafc3e92b2da2d56ef35dc23ef3013667c1fb5247757fbf65b13d2b83b519182b18574cc6939b11d4065cbaa033847a9c5807f1b20cdc6dcae6218c4b0aae437ddb34c64ba45b3878a8bef89605e3bf7616f40197b4e8874d4c3a23cc1e6ac742158dc85c030d832c2cd960f105ceffca0aacfe83d653d34c844592b0cb8cc4ea917b44893e09d340367724ec419de1a5cbd8f5a6b578061496ce2aec1887a1cfa39feb246c4f99b3c8a094501ab5e1d39021415fcb7a215d450218a45404dd5b9432a7b9072bb6a37ca8cbaba7e4d92be693a6b7bd36b2f131840db69a0467aa454b3158af0558cdf690f65dd0b41407cca1a1c6c43e4de0c60383148d9fb115a3194cdab9bfc3abaef46484dd3ff0ce8fde4e23da3c999688c54e71fd7c9a195f535c4e98ce16f887026bc97b4e37bbf65391dbc974d5c376b54b6601e513def1b57f9ed984bb1edba9002e7902990c915c0d0e5eeed1663c8d369965b7ed7352d7389ee0cca3ed62de420439c3598b77ec9093c34913e550bebf63429536b3a8e9571e9b9d8fef5a19ecc784009e0f52c653a81d6ce3bcf9eb1768965ce4724cadc65c6dd198837a57367c7b12913d0bb7c03c28f23101a31cdf41a63087395d413bca5ccfc6c7ac16450d93591ef0bd8ab31315cf46feb42755615c7ba8c3a1572eeab0a78b419e5d5b27dfffdd45cdf252243dad23b3a3341fc2539da2ff2d8aae55799ee6bbcbfba956a4adef7f7a2d45156be7e335357935ace521158a28921bad3192ba5d59b2b7c998399daa1527c7ff71d3291ee87610ab50ee9328efc548d403f573c8ae14e1be5aad1099276c1b2b60c2be22c5e7ec2a86585cce87407027c5dbbeadb76bff361890e9bc92b6d769f09c5c26b8d7a5963696ef8d8d38a6cae2f8a45440a2a2",
        "message": "0102abb3c11c7f9ad6ccdca7cbe4cc77c31ea5c1947fdc3ec4673dd199a0c90fd17f1b1aa7b9a2e319c7a0bfda7fa299099e47879f793cf4b31b45"
      },
...

何故かsignatureは出力されない(ソースコード変更すれば出力は可能)。

アウトプットに受信者ではなく送信者の公開鍵や署名があるのは、LitecoinがMimblewimbleを導入するにあたって非対話的にトランザクションを構築できるようにしているため↓

非対話型のトランザクション構築

Mimblewimbleプロトコルでは、インプットのコミットメントの合計とアウトプットのコミットメントの合計の差分、つまりPedersen commitmentのブラインドファクターの差分値である公開鍵に対して有効なデジタル署名を提供する必要がある。

インプットとアウトプットのブラインドファクターの差分値であるため、それに対してデジタル署名を作成するには、送信者と受信者の協力が必要になる。つまり、トランザクションの作成は対話型になる。

ただ、MWEBではトランザクションを非対話型で構築できるように拡張している。

Mimblewimbleプロトコルトランザクションの構築を対話的に行う必要があるのは、ブラインドファクターがそのコインの所有権を制御する秘密鍵に役割をしているため。送信者がアウトプットのブラインドファクターを知ってしまうと、送信者と受信者両方がコインの所有権を持つことになってしまうのでまずい。そのため、インプットのブラインドファクターは送信者のみが、アウトプットのブラインドファクターは受信者のみが知っているという状況になり、その状態で両者が部分的なSchnorr署名を作成し、それを集約することでデジタル署名を完成させる仕組みになっている。

MWEBでは、この対話型のプロセスを非対話型にするために、コインの所有権の仕組みに手を加えている。

インプットとアウトプットの差分のKernel Excessに対するデジタル署名は、そのままコインのインフレーションが行われていないことの証明に使用するが、コインの所有者であることの証明はこれとは別に行われる。

具体的には、↑でアウトプットにsender_pubkeyreceiver_pubkeyという2つの公開鍵が追加されているが、インプットが参照するアウトプットのreceiver_pubkeyと、アウトプット内のsender_pubkeyの差分であるOwner Offsetの提供を求めるというもの。コインを使用する際のインプットのreceiver_pubkeyは受信者しか知らないため、ブラインドファクターを送信者が知っていたとしても、そのアウトプットのreceiver_pubkeyの所有権の証明はできないため、送信者にブラインドファクターが知られていても、コインが盗まれることはない。

内容が少し古いけど詳細は↓(もともとLIP-0004として提案されていたけど、どうも現在のLIP-0004はまったく別の仕様の提案になってる)

techmedia-think.hatenablog.com