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の設定が漏れてたので再生成。