牛さんの飼養をブロックチェーンで管理する Part 3

Yuya Sugano
25 min readApr 7, 2019

--

物流などのトレーサビリティはブロックチェーンの利活用に適している分野だと言われています。Part 2でOpenZeppelinのERC721トークンを継承して実装した牛さんの個体管理および異動管理のコードを見直しました。追加でMetadata ExtensionとIPFS(Inter-Planetary File System)を使用した牛さんの画像データの登録を行ってみたいと思います。

Looks like Brown Swiss ?

以下、Part 1およびPart 2からの変更点。

  • ERC721のMetadata Extensionを利用して牛さんの画像データのURIを追加および参照できる
  • 牛さんの所有者はIPFS(Inter-Planetary File System)を利用して牛さんの画像をアップロードできるおよび参照できる

コントラクトの編集前にフロントエンドを少し編集しました。Bootstrap4を導入し、またindex.htmlに直接記載していたJavascriptをapp.jsとして別ファイルに書き出しました。ReactやVueなどモダンなフロントエンドのライブラリは使用していません(使えないだけ (-_-;))。

IPFSへ画像をアップロードし、そのIPFSオブジェクトのハッシュ値をメタデータとして牛さんのERC721トークンへ付与することで、アプリケーションやコントラクトから画像を参照できるようにしていきます。

以下がコントラクト編集前のUIの状態です。BirthはUNIX時刻で子牛の生誕時刻です。

Cow Exchange Market

ERC721 Metadata Extension

ERC721ではメタデータとしてERC20にも実装されているトークン名やシンボルを拡張します。各トークンに対するメタデータとしてJSON形式で以下のような情報を定義しています。[1]

構造としてpropertiesの要素の中にimage要素があり、そのdescription要素内で画像のURIを指定する構造になっています。

{
"title": "Asset Metadata",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Identifies the asset to which this NFT represents"
},
"description": {
"type": "string",
"description": "Describes the asset to which this NFT represents"
},
"image": {
"type": "string",
"description": "A URI pointing to a resource with mime type image/* representing the asset to which this NFT represents. Consider making any images at a width between 320 and 1080 pixels and aspect ratio between 1.91:1 and 4:5 inclusive."
}
}
}

ここでは簡略化して定義されているJSON形式でなく、画像ファイル(IPFSオブジェクト)のハッシュ値だけを格納することにしてみます。

ERC721の実装には引き続きセキュアなスマートコントラクト開発のライブラリであるOpenZeppelinを使用します。[2]

以下、OpenZeppelinでのERC721Metadataの実装です。_setTokenURIおよび_burn関数がinternalなのでERC721Metadataを継承した上でこれらの関数をオーバーライドして記述してあげる必要があります。

まずはライブラリをインポートします。

import "openzeppelin-solidity/contracts/token/ERC721/ERC721Metadata.sol";

ERC721MetadataがすでにERC721を継承しているので、ERC721Metadataのみの継承でOKです。コンストラクタに定義したトークン名とシンボルを渡します。CowTokenとCWTKとしてみました。

contract CowOwnership is CowBreeding, ERC721Metadata {  /// @notice Name and Symbol of the NFT token
string private _name = "CowToken";
string private _symbol = "CWTK";
// Mapping from token ID to approved address
mapping (uint256 => address) private _tokenApprovals;
// Mapping from owner to operator approvals
mapping (address => mapping (address => bool)) private _operatorApprovals;
constructor() public ERC721Metadata(_name, _symbol) {}...

setTokenURIおよびburn関数を追加で定義します。トークンの所有者のみがURIの設定やトークンのburnをできるようにしてみました。require(msg.sender == owner)の箇所です。

/**
* @dev Set the token URI for a given token by the owner
* Reverts if the token ID does not exist
* @param _tokenId uint256 ID of the token to set its URI
* @param _uri string URI to assign
*/
function setTokenURI(uint256 _tokenId, string _uri) external {
address owner = ownerOf(_tokenId);
require(msg.sender == owner);
super._setTokenURI(_tokenId, _uri);
}
/**
* @dev External function to burn a specific token
* Reverts if the token does not exist
* Deprecated, use _burn(uint256) instead
* @param _tokenId uint256 ID of the token being burned by the msg.sender
*/
function burn(uint256 _tokenId) external {
address owner = ownerOf(_tokenId);
require(msg.sender == owner);
super._burn(owner, _tokenId);
}

コンパイルします。OK。

$ truffle compile
Compiling ./contracts/CowBreeding.sol...
Compiling ./contracts/CowOwnership.sol...
...
Writing artifacts to ./build/contracts

IPFS(Inter-Planetary File System)の設定

Protocol Labsによって開発が進められているP2Pネットワーク上で展開されるHTTPプロトコルの実装です。既存のHTTPプロトコルの補完もしくは置換を目的に開発され、分散型のファイルシステムであるという特徴を生かして様々なブロックチェーンサービスに利用されています。[3]

  • ロケーション指向でなくコンテンツ指向、サーバがどこにあるかは関係ない、コンテンツの内容からコンテンツを持つノードを走査する
  • URIやIPアドレスではなく、コンテンツがアドレスを決定する

「コンテンツをlocationで指定する識別子を生成するのではなく、コンテンツ自体の表現でコンテンツを指定します」

既存のHTTPプロトコルと比べ、以下のようなメリットが考えられます。

  • 耐障害性(コンテンツのハッシュ値から対象ノードを走査)
  • 負荷分散(ユーザはコンテンツを最寄りのノードから取得)
  • 耐検閲性(一部のノードが隔離されたとしても影響がない)
  • 耐改竄性(コンテンツ自身のハッシュから正当性を検証可能)

以下のIPFS入門のサイトに沿いながらIPFSのノードを走らせて使用する画像を追加していきます。※環境はUbuntu 16.04.1 LTSを使用

公式サイトから環境に合わせたバイナリパッケージをダウンロードして、実行モジュールをPATHが通っているディレクトリへ配置します。

Linux Binaries
$ wget https://dist.ipfs.io/go-ipfs/v0.4.19/go-ipfs_v0.4.19_linux-amd64.tar.gz
$ tar xvzf go-ipfs_v0.4.19_linux-amd64.tar.gz
go-ipfs/build-log
go-ipfs/install.sh
go-ipfs/ipfs
go-ipfs/LICENSE
go-ipfs/README.md
$ cd go-ipfs/
$ sudo ./install.sh
Moved ./ipfs to /usr/local/bin
$ ipfs version
ipfs version 0.4.19

バイナリが環境に合っていない場合は以下のようなエラーが出るので、正しいバイナリをダウンロードし直す必要があります。※環境はUbuntu 16.04.1 LTSを使用

-bash: /usr/local/bin/ipfs: cannot execute binary file: Exec format error

ipfs initコマンドでローカルのipfsリポジトリを初期化し、ホームディレクトリに作成されるリポジトリを確認します。.ipfsというフォルダがリポジトリです。.ipfs内に設定ファイルなども保存されます。

$ ipfs init
initializing IPFS node at /home/.ipfs
generating 2048-bit RSA keypair...done
peer identity: QmW2bRPsnaNaXynmEWSVfXZP9333pHy5zrDAwzop1FMECM
to get started, enter:
ipfs cat /ipfs/QmS4ustL54uo8FzR9455qaxZwuMiUhyvMcX9Ba8nUH4uVv/readme

readmeがローカルのipfsリポジトリに作成されているのでipfsのファイル内容を確認するipfs catコマンドで内容を確認します。

$ ipfs cat /ipfs/QmS4ustL54uo8FzR9455qaxZwuMiUhyvMcX9Ba8nUH4uVv/readme
Hello and Welcome to IPFS!
If you're seeing this, you have successfully installed
IPFS and are now interfacing with the ipfs merkledag!
-------------------------------------------------------
| Warning: |
| This is alpha software. Use at your own discretion! |
| Much is missing or lacking polish. There are bugs. |
| Not yet secure. Read the security notes for more. |
-------------------------------------------------------
Check out some of the other files in this directory:./about
./help
./quick-start <-- usage examples
./readme <-- this file
./security-notes

ipfs object getコマンドではIPFSオブジェクトおよびLink構造体が確認可能です。

$ ipfs object get QmS4ustL54uo8FzR9455qaxZwuMiUhyvMcX9Ba8nUH4uVv
{"Links":[{"Name":"about","Hash":"QmZTR5bcpQD7cFgTorqxZDYaew1Wqgfbd2ud9QqGPAkK2V","Size":1688},{"Name":"contact","Hash":"QmYCvbfNbCwFR45HiNP45rwJgvatpiW38D961L5qAhUM5Y","Size":200},{"Name":"help","Hash":"QmY5heUM5qgRubMDD1og9fhCPA6QdkMp3QCwd4s7gJsyE7","Size":322},{"Name":"ping","Hash":"QmejvEPop4D7YUadeGqYWmZxHhLc4JBUCzJJHWMzdcMe2y","Size":12},{"Name":"quick-start","Hash":"QmXgqKTbzdh83pQtKFb19SpMCpDDcKR2ujqk3pKph9aCNF","Size":1692},{"Name":"readme","Hash":"QmPZ9gcCEpqKTo6aq61g2nXGUhM4iCL3ewB6LDXZCtioEB","Size":1102},{"Name":"security-notes","Hash":"QmQ5vhrL7uv6tuoN9KeVBwd4PwfQkXdVVmDLUZuTNxqgvm","Size":1173}],"Data":"\u0008\u0001"}

この記事のサムネに使用している牛さんの画像をIPFSへ追加してみます。ファイルが追加され太字のハッシュ値が表示されています。※商用利用可能の画像です

$ ipfs add animals-cattle-cows.jpg
added QmXr7KNc5436EZXPPwJP1KUeMhBQHLNSC2oEMTKBGQ8Vs3 animals-cattle-cows.jpg
1.89 MiB / 1.89 MiB [==================================================================================] 100.00%

コンテンツのサイズが256Kを超える場合は、256K以下のサイズのチャンクに分割して保存されます。ipfs lsコマンドでハッシュ値を指定することで以下のように各チャンクが確認できます。

$ ipfs ls QmXr7KNc5436EZXPPwJP1KUeMhBQHLNSC2oEMTKBGQ8Vs3
QmcTKH3Pj5yqTBFwk7zV4dxEvEXyvygndWPPwUD2PZncuV 262144
QmQu5T5VTbEkF4FGNJ4j587gH6KydAa8fiGUBPr572N3LL 262144
QmQcpb3Tyuw6M6WrLzsM1DRRNe6iXPojf9QNNvEvHoo4BD 262144
QmVULKwDAgY9DZkfKDBcEpxt57jSMUGcLQnaRk5VSr7fXz 262144
QmSzWYdnjieQ3h8BXpWqJu1NtBVnmjrpkGh8h1d2KzVXkZ 262144
QmUjoVQpRkCVjjfqdN7gcYdJAXjE7py4xYsgVJhSXn1g4F 262144
QmbiyWeui1MWtgeXaubsNSf2VSPf99SwXUH539w5d1Lzy7 262144
QmdiPCErwAoejiKhzCb9GNcE6pJxFs2MSDhcfFhqM6C2Ca 150803

.ipfs/configファイルを変更してAPIおよびGatewayの設定に自サーバのIPアドレスを入力してください。daemonを起動することで追加したファイルがネットワーク経由でipfsネットワークに伝播され、パブリックゲートウェイおよびプライベートゲートウェイからデータを確認できるようになります。

"Addresses": {
"Swarm": [
"/ip4/0.0.0.0/tcp/4001",
"/ip6/::/tcp/4001"
],
"Announce": [],
"NoAnnounce": [],
"API": "/ip4/X.X.X.X/tcp/5001",
"Gateway": "/ip4/X.X.X.X/tcp/8080"
}

daemonを起動して画像ファイルを伝播します。

$ ipfs daemon
Initializing daemon...
go-ipfs version: 0.4.19-
Repo version: 7
System version: amd64/linux
Golang version: go1.11.5
Swarm listening on /ip4/127.0.0.1/tcp/4001
Swarm listening on /ip4/X.X.X.X/tcp/4001
Swarm listening on /p2p-circuit
Swarm announcing /ip4/127.0.0.1/tcp/4001
Swarm announcing /ip4/X.X.X.X/tcp/4001
API server listening on /ip4/X.X.X.X/tcp/5001
WebUI: http://X.X.X.X:5001/webui
Gateway (readonly) server listening on /ip4/X.X.X.X/tcp/8080
Daemon is ready

コンテンツ共有の確認

ネットワーク上のピアおよびコンテンツが共有されているかを確認します。ipfs swarm peersでピアの確認ができます。

$ ipfs swarm peers
/ip4/1.173.34.68/tcp/4001/ipfs/QmRpz2j1dr6Jo5VZ5nZ6zqEYuqFpTXWupF69ccNtfJgQwT
/ip4/1.195.28.42/tcp/1062/ipfs/QmXSuy2t8eTETCaaSpnqAtfwe8ShHdhAuMkX67vcCR61d4
...
/ip4/99.74.58.141/tcp/4001/ipfs/QmTcqpgP6hmzsUbViS7wdkSVXgs2LLbqp359qn4xc79Mry

対象のハッシュ値を持つコンテンツがどのピアと共有されているか確認ができます。ピアが表示されていればネットワーク上でオブジェクトの走査が可能です。ここでは2つのピアが対象のコンテンツを保持していることが分かりました。

$ ipfs dht findprovs QmXr7KNc5436EZXPPwJP1KUeMhBQHLNSC2oEMTKBGQ8Vs3
QmW2bRPsnaNaXynmEWSVfXZP9333pHy5zrDAwzop1FMECM
QmdKFbzojwRAqFJ3DZDMKxaW8EZsaCCp8Kv1LBEv1PUdKh

本家、Protocol Labsのパブリックゲートウェイhttps://ipfs.ioはコンテンツの保持期間を極端に短くしているためIPFSの仕様通りの動作をしないことがあるようです。[4]

https://ipfs-gateway.decentralized-web.jp/というパブリックゲートウェイが使用できますので本家かこちらのどちらかでコンテンツが参照できることを確認します。以下はそれぞれ牛画像のハッシュ値を埋め込んだ画像のリンクです。

https://ipfs.io/ipfs/QmXr7KNc5436EZXPPwJP1KUeMhBQHLNSC2oEMTKBGQ8Vs3

https://ipfs-gateway.decentralized-web.jp/ipfs/QmXr7KNc5436EZXPPwJP1KUeMhBQHLNSC2oEMTKBGQ8Vs3

IPFS連携とフロントエンド修正

ここで行いたいことはJavascriptからIPFSを操作して、牛さんの画像ファイルをネットワークへアップロード、対象となるトークンID(牛さんのID)のsetTokenURI関数を呼び出してトークンIDに対するハッシュ値を保存すること、フロントエンド側から上記の操作およびtokenURI関数を呼び出して牛さんのカードへ画像を表示すること、です。

カードはBootstrap4で導入されたコンテナコンテンツで、 パネル(Panels)、囲み枠(Wells)、サムネイル(Thumbnails)を置き換えるものです。

ipfsライブラリの使用については以下のgistを参考にしたのですが、画像データをバッファする https://wzrd.in/standalone/bufferが提供されていないようです(2019/4/7現在)。Javascriptによるアップロードの実動作は確認できませんでしたが、setTokenURIおよびtokenURIを使用したIPFSオブジェクトのハッシュ値の保存とサイト上での表示を確認しています。

https://gist.github.com/sogoiii/e07ff464c4ff8a6fa9daa0ca927af3cb

JavascriptからIPFSを操作するためのライブラリを読み込みます。

<script src="https://unpkg.com/ipfs-api@9.0.0/dist/index.js"></script>

トークンIDを指定してIPFSへファイルのアップロードを行うHTMLを記述します。Uploadボタンをクリックすることで、画像をIPFSへアップロードできるようにします。HTMLについてはBootstrap4の記述に倣っています。

<form style="margin-top: 50px">
<div class="form-group row">
<label for="exampleForm2" class="col-sm-2 col-form-label">Token ID</label>
<div class="col-sm-10">
<input type="text" name="tokenid" class="form-control">
</div>
</div>
<div class="form-group row">
<label for="File" class="col-sm-2 col-form-label">File Input</label>
<div class="col-sm-10">
<input type="file" class="form-control-file" id="File">
</div>
</div>
<div class="form-group row">
<div class="offset-sm-2 col-sm-10">
<button class="btn btn-upload" type="submit">Upload</button>
</div>
</div>
</form>

Javascriptを追記します。Uploadがクリックされると呼ばれます。

handleUpload: function(event) {
event.preventDefault();
var tokenid = $('input:text[name="tokenid"]').val();
var reader = new window.FileReader();
return App.saveIpfs(reader, tokenid);
}

画像をIPFSへ保存し、setTokenURI関数を呼び出してハッシュ値をコントラクトに保存します。

saveIpfs: function(reader, tokenid) {
var buf = buffer.Buffer(reader.result);
App.ipfs.files.add(buf, function(error, result) {
if (error) {
console.log(error);
return;
}
var OwnershipInstance;
var hash = result[0].hash;
var url = "https://ipfs.io/ipfs/" + hash;
App.contracts.CowOwnership.deployed().then(function(instance) {
OwnershipInstance = instance;
// Call a setTokenURI function relavant hashed value
return OwnershipInstance.setTokenURI(tokenid, hash);
}).then(function(result) {
// Transaction was accepted into the blockchain, redraw the UI
$("#txStatus").text("Successfully uploaded " + tokenid + " !").show();
return App.markRetrieved();
}).catch(function(error) {
// Transaction returned with an error
$("#txStatus").text(error).show();
});
});
}

https://wzrd.in/standalone/bufferが動かなかったので手動でハッシュ値をセットします。

truffle(live)> c.setTokenURI(4237396338, "QmXr7KNc5436EZXPPwJP1KUeMhBQHLNSC2oEMTKBGQ8Vs3", {from: web3.eth.accounts[0]})
...
truffle(live)> c.tokenURI(4237396338)
'QmXr7KNc5436EZXPPwJP1KUeMhBQHLNSC2oEMTKBGQ8Vs3

tokenURI関数で取り出したハッシュ値とパブリックゲートウェイのURLを結合して、牛さんのカード内で表示します。以下のように表示できました。

IPFS Integration

牛といえば、ミルクラッパー『Shibori』ということで、この記事はこちらの曲『酪農』をリピしながら書きました。M to the I to the L to the K !!

まとめ

  • ERC721ではMetadata Extensionとしてトークン・資産の名前や説明および画像などをJSON形式で付与することができる
  • IPFS(Inter-Planetary File System)を利用することで分散型のストレージ上に画像を保存することができる
  • IPFS(Inter-Planetary File System)は分散型のためDappsと相性が良い

--

--

Yuya Sugano
Yuya Sugano

Written by Yuya Sugano

Cloud Architect and Blockchain Enthusiast, techflare.blog, Vinyl DJ, Backpacker. ブロックチェーン・クラウド(AWS/Azure)関連の記事をパブリッシュ。バックパッカーとしてユーラシア大陸を陸路横断するなど旅が趣味。

No responses yet