ION DID を発行

Bitcoin testnet 上の ION で DID を発行してみる記事。

ION

前提
  • macOS 12.6 Monterey
  • Apple M1 Pro, 32GB
  • Docker 20.10.17
  • Docker Compse 2.10.2
  • Ruby 3.1.2

1. ION Node を起動

testnet 用の Docker が用意されたのでそれを利用

$ git clone https://github.com/decentralized-identity/ion.git $ cd docker $ docker-compose -p ion_testnet -f docker-compose.yml -f docker-compose.testnet-override.yml up -d

構造は

ion ├ mongo : MongoDB ├ bitcoin-core : Bitcoin Core ├ ipfs : IPFS ├ ion-bitcoin : Bitcoin Core と接続する ION のサービス └ ion-core : ION Core

IPFS コンテナ、Bitcoin Core コンテナは 各 RPC ポートがマッピング済。

各コンテナのログが落ち着くまで待つのが吉。特に ion-bitcoin コンテナは bitcoin-core の IBD 終了後に再起動する必要があるかもしれない。

2. 各種 gem をインストール

$ gem install sidetree $ gem install bitcoinrb

もしくはGemfileを用意

gem 'sidetree' gem 'bitcoinrb', require: 'bitcoin' # --- $ bundle install

3. DIDを発行

Sidetree で DID を発行。サービスにはとりあえず LinkedDomains に www.shmn7iii.net をリンク。

以下 irb にて

require 'sidetree' recovery_key = Sidetree::Key.generate update_key = Sidetree::Key.generate signing_key = Sidetree::Key.generate(id: 'signing-key') service = Sidetree::Model::Service.new("linkeddomains", "LinkedDomains", "https://www.shmn7iii.net") document = Sidetree::Model::Document.new(public_keys: [signing_key], services: [service]) did = Sidetree::DID.create(document, update_key, recovery_key, method: Sidetree::Params::METHODS[:ion]) # Short Form puts did.short_form # => did:ion:EiA8bJthOOPX-cOY7J5ntqmXSkJ1F2rQMsy7kxjOEEavRw # Long Form puts did.to_s # => did:ion:EiA8bJthOOPX-cOY7J5ntqmXSkJ1F2rQMsy7kxjOEEavRw:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJzaWduaW5nLWtleSIsInB1YmxpY0tleUp3ayI6eyJjcnYiOiJzZWNwMjU2azEiLCJrdHkiOiJFQyIsIngiOiJ6a2VPWjNUdUtaU2o0LW5ka0pLREJHd2o3elRBWmV4eF83NlBvNHRxOElZIiwieSI6IklMYWtkWjFzdTN6V3FZai1wZVRHMGZESkhlUnRlMlJWbHc4UVpmU3BpX1kifSwicHVycG9zZXMiOltdLCJ0eXBlIjoiRWNkc2FTZWNwMjU2azFWZXJpZmljYXRpb25LZXkyMDE5In1dLCJzZXJ2aWNlcyI6W3siaWQiOiJsaW5rZWRkb21haW5zIiwic2VydmljZUVuZHBvaW50IjoiaHR0cHM6Ly93d3cuc2htbjdpaWkubmV0IiwidHlwZSI6IkxpbmtlZERvbWFpbnMifV19fV0sInVwZGF0ZUNvbW1pdG1lbnQiOiJFaUJnX0VRS19MWUlTdm9sWU9JY0pERjJ1QnJHYUtMMExjeEhubElEOUhEMXJ3In0sInN1ZmZpeERhdGEiOnsiZGVsdGFIYXNoIjoiRWlCdkRXQ29mbm5hYjFYajlwQ2NwVlF2NVZVdEttWnhkbFI4cVV4cnFVVlJ1dyIsInJlY292ZXJ5Q29tbWl0bWVudCI6IkVpQndOeTJ2cldwNHlVS2hKOFY0LUM2aXFFcE9YZlNmaDRZb09uR2tfX0QzN2cifX0

これで DID の発行は完了。ただしローカルで算出しただけ。

Long-Form はドキュメント内容も入っているため、例え未アンカリングであろうと Resolve できる。

Image in a image block

ION explorer

Short はまだ無理。

Image in a image block

4. Anchor String を作成

IPFS へアップロードし Anchor String を取得する。ION Node で利用している IPFS コンテナが 5001 ポートへマッピングされているためそこを利用する。

# 3 の続き create_op = did.create_op ipfs = Sidetree::CAS::IPFS.new chunk_file = Sidetree::Model::ChunkFile.create_from_ops(create_ops: [create_op]) chunk_file_uri = ipfs.write(chunk_file.to_compress) # => "Qmb6YbceRaaQHXS3ZcQb8m9Bfbfhm61541L5zieEcuYPK2" 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) # => "QmPPg3beKR9RcUjwEY8irBzEQAFjgdEXPya3HaiMf7nk2A" 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) # => "QmZf8XdDMphPUAd7DEvhLa6E2Tx1Z77gjP4M6q4wcWWzQD" anchor_str = Sidetree::Util::AnchoredDataSerializer.serialize(1, core_index_file_uri) # => "1.QmZf8XdDMphPUAd7DEvhLa6E2Tx1Z77gjP4M6q4wcWWzQD"

5. Bitcoin へアンカリング

OP_RETURN 以降に Anchor String を格納したトランザクションをブロードキャストすることでアンカリング。

5.1 準備

まずは wallet の作成と Faucet からの資金調達。ION Node 内の Bitcoin Core コンテナで操作する。RPC ポートは 18332 へマッピング済。すでにある場合はスキップ可。

$ docker exec --user bitcoin -it testnet-bitcoin-core /bin/bash # -- 以下コンテナ内で作業 -- # レガシー wallet の作成 $ bitcoin-cli -testnet -rpcuser=user -rpcpassword=password -rpcport=18332 -named createwallet wallet_name="" descriptors=false { "name": "", "warning": "" } # レガシーアドレスを作成 $ bitcoin-cli -testnet -rpcuser=user -rpcpassword=password -rpcport=18332 -rpcwallet="" getnewaddress "" legacy mt2jyxWfbAf4KY13QNW6ibXCyDN3TXx4hY # 以下リンクで Faucet から資金調達 # https://coinfaucet.eu/en/btc-testnet/ # ブロックが取り込まれるのを待つ # UTXO を確認 $ bitcoin-cli -testnet -rpcuser=user -rpcpassword=password -rpcport=18332 -rpcwallet="" listunspent { { "txid": "d1ba56c0ce633e2f9d938995e7248a43ff19adf3d9621f24a641a200305c1643", "vout": 1, "address": "mt2jyxWfbAf4KY13QNW6ibXCyDN3TXx4hY", "label": "", "scriptPubKey": "76a9148943b5f19351c4deccbd203a8b1f2a7cdff0ffb088ac", "amount": 0.03525382, "confirmations": 5, "spendable": true, "solvable": true, "desc": "pkh([c9f0c3a2/0'/0'/2']036a6fd42fb6dc1c0bf9b2bb7c2bb0452fbb787a2b8bf923ae1f7e67dd5aea9ca7)#k35t88xx", "safe": true } } # PrivateKey を取得 $ bitcoin-cli -testnet -rpcuser=user -rpcpassword=password -rpcport=18332 -rpcwallet="" dumpprivkey mt2jyxWfbAf4KY13QNW6ibXCyDN3TXx4hY <priv-key>
5.2 トランザクション発行

以下 irb にて

# 4 の続き anchor_str = "1.QmZf8XdDMphPUAd7DEvhLa6E2Tx1Z77gjP4M6q4wcWWzQD" require 'bitcoin' require 'net/http' require 'json' include Bitcoin::Opcodes Bitcoin.chain_params = :testnet HOST="localhost" PORT=18332 RPCUSER="user" RPCPASSWORD="password" def bitcoinRPC(method, params) http = Net::HTTP.new(HOST, PORT) request = Net::HTTP::Post.new('/') request.basic_auth(RPCUSER, RPCPASSWORD) request.content_type = 'application/json' request.body = { method: method, params: params, id: 'jsonrpc' }.to_json JSON.parse(http.request(request).body)["result"] end # アドレス address = "mt2jyxWfbAf4KY13QNW6ibXCyDN3TXx4hY" priv_key = <priv-key> # input txid = "d1ba56c0ce633e2f9d938995e7248a43ff19adf3d9621f24a641a200305c1643" vout = 1 input_tx = Bitcoin::Tx.parse_from_payload(bitcoinRPC('getrawtransaction',[txid]).htb) input_satoshi = input_tx.outputs[vout].value script_pubkey = input_tx.outputs[vout].script_pubkey outpoint = Bitcoin::OutPoint.from_txid(txid, vout) # output redeem_script = Bitcoin::Script.new << OP_RETURN << "ion:#{anchor_str}".bth # 料金計算 FEE = 0.00003 fee_satoshi = (FEE * (10**8)).to_i change_satoshi = input_satoshi - fee_satoshi # トランザクションを作成 tx = Bitcoin::Tx.new tx.in << Bitcoin::TxIn.new(out_point: outpoint) tx.out << Bitcoin::TxOut.new(value: 0, script_pubkey: redeem_script) tx.out << Bitcoin::TxOut.new(value: change_satoshi, script_pubkey: Bitcoin::Script.parse_from_addr(address)) # 署名 sig_hash = tx.sighash_for_input(0, script_pubkey) key = Bitcoin::Key.from_wif(priv_key) signature = key.sign(sig_hash) + [Bitcoin::SIGHASH_TYPE[:all]].pack('C') tx.in[0].script_sig << signature tx.in[0].script_sig << key.pubkey.htb # 確認 tx.verify_input_sig(0, script_pubkey) # ブロードキャスト bitcoinRPC('sendrawtransaction',[tx.to_hex]) # => "508505bc6ee912f24edea2bffa259d7978290ad2aee8ddb23e505ebd7b449645"

作成したトランザクションは以下

5.3 ION Core で確認

トランザクションがブロックに取り込まれたら log が現れる。

$ docker logs -f testnet-ion-core ... Downloading core index file 'QmZf8XdDMphPUAd7DEvhLa6E2Tx1Z77gjP4M6q4wcWWzQD', max file size limit 1000000 bytes... Downloading file 'QmZf8XdDMphPUAd7DEvhLa6E2Tx1Z77gjP4M6q4wcWWzQD', max size limit 1000000... Successfully kicked off downloading/processing of all new Sidetree transactions. Processing previously unresolvable transactions if any... CommandSucceededEvent { connectionId: 'mongo:27017', requestId: 179491, commandName: 'find', duration: 1, reply: { cursor: { firstBatch: [], id: 0, ns: 'ion-testnet-core.unresolvable-transactions' }, ok: 1 } } Fetched 0 unresolvable transactions to retry in 2 ms. Event emitted: sidetree_observer_loop_success Waiting for 60 seconds before fetching and processing transactions again. Read and pinned 239 bytes for CID: QmZf8XdDMphPUAd7DEvhLa6E2Tx1Z77gjP4M6q4wcWWzQD. Event emitted: sidetree_download_manager_download: {"code":"success"} File 'QmZf8XdDMphPUAd7DEvhLa6E2Tx1Z77gjP4M6q4wcWWzQD' of size 239 downloaded. Downloading provisional index file 'QmPPg3beKR9RcUjwEY8irBzEQAFjgdEXPya3HaiMf7nk2A', max file size limit 1000000... Downloading file 'QmPPg3beKR9RcUjwEY8irBzEQAFjgdEXPya3HaiMf7nk2A', max size limit 1000000... Read and pinned 93 bytes for CID: QmPPg3beKR9RcUjwEY8irBzEQAFjgdEXPya3HaiMf7nk2A. Event emitted: sidetree_download_manager_download: {"code":"success"} File 'QmPPg3beKR9RcUjwEY8irBzEQAFjgdEXPya3HaiMf7nk2A' of size 93 downloaded. Downloading chunk file 'Qmb6YbceRaaQHXS3ZcQb8m9Bfbfhm61541L5zieEcuYPK2', max size limit 10000000... Downloading file 'Qmb6YbceRaaQHXS3ZcQb8m9Bfbfhm61541L5zieEcuYPK2', max size limit 10000000... Read and pinned 356 bytes for CID: Qmb6YbceRaaQHXS3ZcQb8m9Bfbfhm61541L5zieEcuYPK2. Event emitted: sidetree_download_manager_download: {"code":"success"} File 'Qmb6YbceRaaQHXS3ZcQb8m9Bfbfhm61541L5zieEcuYPK2' of size 356 downloaded. Parsed chunk file in 1 ms. CommandSucceededEvent { connectionId: 'mongo:27017', requestId: 179500, commandName: 'update', duration: 2, reply: { n: 1, nModified: 0, upserted: [ [Object] ], ok: 1 } } Processed 1 operations. Retry needed: false Transaction 1.QmZf8XdDMphPUAd7DEvhLa6E2Tx1Z77gjP4M6q4wcWWzQD is confirmed at 2349428

IPFS へ Fetch, Download, Pin している様子がわかる。

6. ローカルで Resolve

ローカルで確認しよう。curl もしくはブラウザでアクセス。URL は以下。

http://localhost:3000/identifiers/did:ion:test:<did> ex) http://localhost:3000/identifiers/did:ion:test:EiA8bJthOOPX-cOY7J5ntqmXSkJ1F2rQMsy7kxjOEEavRw
curl http://localhost:3000/identifiers/did:ion:test:EiA8bJthOOPX-cOY7J5ntqmXSkJ1F2rQMsy7kxjOEEavRw | jq % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 927 100 927 0 0 59392 0 --:--:-- --:--:-- --:--:-- 100k { "@context": "https://w3id.org/did-resolution/v1", "didDocument": { "id": "did:ion:test:EiA8bJthOOPX-cOY7J5ntqmXSkJ1F2rQMsy7kxjOEEavRw", "@context": [ "https://www.w3.org/ns/did/v1", { "@base": "did:ion:test:EiA8bJthOOPX-cOY7J5ntqmXSkJ1F2rQMsy7kxjOEEavRw" } ], "service": [ { "id": "#linkeddomains", "type": "LinkedDomains", "serviceEndpoint": "https://www.shmn7iii.net" } ], "verificationMethod": [ { "id": "#signing-key", "controller": "did:ion:test:EiA8bJthOOPX-cOY7J5ntqmXSkJ1F2rQMsy7kxjOEEavRw", "type": "EcdsaSecp256k1VerificationKey2019", "publicKeyJwk": { "crv": "secp256k1", "kty": "EC", "x": "zkeOZ3TuKZSj4-ndkJKDBGwj7zTAZexx_76Po4tq8IY", "y": "ILakdZ1su3zWqYj-peTG0fDJHeRte2RVlw8QZfSpi_Y" } } ] }, "didDocumentMetadata": { "method": { "published": true, "recoveryCommitment": "EiBwNy2vrWp4yUKhJ8V4-C6iqEpOXfSfh4YoOnGk__D37g", "updateCommitment": "EiBg_EQK_LYISvolYOIcJDF2uBrGaKL0LcxHnlID9HD1rw" }, "canonicalId": "did:ion:test:EiA8bJthOOPX-cOY7J5ntqmXSkJ1F2rQMsy7kxjOEEavRw" } }
Image in a image block

完成 🎉 🎉 🎉

トラブルシューティング

ion-bitoin のタイムアウト
Error occurred while updating blockchain time, investigate and fix: FetchError: request to http://ion-bitcoin:3002/time failed, reason: connect ECONNREFUSED 172.21.0.6:3002

ion-bitoin コンテナが落ちてる。当該コンテナは bitcoin-core と通信するコンテナだが、bitcoin-core の IBD が終了していないと Fetch 作業が進まずエラー落ちしてしまう。ので IBD 終了後にコンテナ再起動で解決。

アドレスについて
The first input must have only the signature and publickey

Input に Witness 領域が存在する = SegWit だとエラーを吐く。レガシーアドレスで操作する必要がある。

Ref: https://developer.bitcoin.org/reference/rpc/getnewaddress.html#argument-2-address-type

手数料について
Invalid core file found for anchor string '1.QmdX6io547tkikns8to5d9kLMvvPop7K9H3ZLo9C8eTf5j', the entire batch is discarded. Error: The actual fee paid: 2000 should be greater than or equal to the normalized fee: 2482 Transaction 1.QmdX6io547tkikns8to5d9kLMvvPop7K9H3ZLo9C8eTf5j is confirmed at 2349421

FEE = 0.00002 (2000 satoshi) だと足りないと怒られた。2482 satoshi 以上でないとけないらしい。

CID の Fetching がタイムアウトする
Downloading core index file 'QmdX6io547tkikns8to5d9kLMvvPop7K9H3ZLo9C8eTf5j', max file size limit 1000000 bytes... Downloading file 'QmdX6io547tkikns8to5d9kLMvvPop7K9H3ZLo9C8eTf5j', max size limit 1000000... Timed out fetching CID 'QmdX6io547tkikns8to5d9kLMvvPop7K9H3ZLo9C8eTf5j'.

対象 CID を保持する IPFS ノードに制限時間内に到達しきれなかった。もし DID 発行を ION Node 内の IPFS コンテナとは別な IPFS ノードで実行した場合、到達するまで時間がかかることがある。サクッと確認したい場合は ION Core の参照先IPFSノードとアンカリング先IPFSノードを同一にすると良い。今回の場合は ION Node 内の IPFS コンテナでアンカリングした。

参考