ポケモンを使って Solidity で実現できることを説明する
はじめに
この記事は、Solidity 初学者以前の「 dApps, Ethereum, ビットコイン, 分散ネットワーク ... etc なんか凄そうだけど、どういった旨味があってどういうことができるのか分からないから興味が沸かない」という方に向けて、少しでもこの分野に興味を持ってもらいたくて書きました。
ある程度プログラミングには通じていることを念頭に書いています。
僕自身、この領域を学び始めて数ヶ月程度のため記事中には誤りや古くなっている部分はあるかと思います。経験者の方はぜひ、コメントで教えていただければと思います。
Solidity ってなんよ?(簡易)
公式の概略として以下のように書かれています。
Solidityはスマートコントラクトを扱えるオブジェクト指向の高級言語です。スマートコントラクトはEthereum内でアカウントの動作を制御するものです。
オブジェクト指向の方はおなじみですが、スマートコントラクトはあまり馴染みがないですね。基本的に Ethereum を始めとしたブロックチェーン上で動作するアプリケーションは、スマートコントラクトと呼ばれるものを実装することによってブロックチェーン上の契約を自動化しています。ブロックチェーン上のややこしいコアな実装を気にせず、抽象化されたコーディングを行っているだけでブロックチェーンの良いところを享受できるのが Solidity ということになります。
じゃあブロックチェーン(dApps 分散アプリケーション)使ったいいとこってなんなの?という話ですが、以下のような利点があります。
- 通信情報を原則として暗号化する必要がない
- 分散化されている各々のノードが秘密鍵を所有していて、ブロック上のトランザクションに乗せる必要があるのは公開鍵のみのため(いわゆる中央集権型だと問い合わせ先として中央にクレデンシャルを持つ必要があるので暗号化が必要となる)
- 取引検証が分散化されている(改ざんできない公正な取引ができる)
- 取引は全て公開されていて履歴を完全に追うことができる
- ...... etc
あえて抽象的に表現すると、通信はめっちゃ早い方法を選択すればいいし RMT みたいな不正は蔓延らないしクリエイターにとっても損のない世界感をデジタルに実現しうる。という可能性を秘めたものが分散アプリケーションのいいところになります。
取引は検証されていて改ざんされない。履歴を完全に追うことができる。といった分散アプリケーションの特性は、「デジタル世界においての唯一性を証明する。」ことができます。またその唯一性に価値を付与できるというの点があって NFT と呼ばれているものです。最近だと、ツイッターの初コメントが 3 億で落札されましたね。まだまだ色々なものに価値がついていきそうです。
Solidity でポケモンを実現する
Solidity の柔軟性、NFT の具体例のためポケモンを使って説明してみます。
ポケモンを生み出せるようにする
Solidity はオブジェクト指向言語であることは前述の通りで、contract と呼ばれるクラスのようなものを作ることができます。今回はポケモンを生み出して、リソースの管理をする親元として PocketMonsterFactory contract を作成します。(ゲーム○リークでもいいんですが)
contract PocketMonsterFactory
struct を使うことで、実際のモンスターをどう扱うかといった表現も可能です。
struct PocketMonster {
uint256 id; # ID 内部的に一意にリソースを管理する用
string name; # 名前 1~6文字
uint256 level; # レベル 1~100
uint256 tribe_no; # 種族(図鑑)番号
string tribe_name; # 種族名. サンダー, ピカチュウ
bool is_wild; # 野生かどうか
}
配列表現やアクセサーなどプロパティに対する表現も持ちます。
PocketMonster[] public monsterList;
扱うリソースを決めたら、それを管理するアカウントを作れます。管理するアカウントは増田順一さんでもいいんですが、無難に admin
としておきます。
address private _admin; # ここはプロパティ [ 型 アクセサー 変数名 ]
...
...
constructor() { # よくある new するときに走るところ
_admin = msg.sender;
}
...
...
modifier onlyAdmin() {
require(msg.sender == _admin);
_; # ここはおまじない
}
...
...
function createMonster(uint256 _tribe_no, string memory _tribe_name)
external
onlyAdmin
{
uint256 id = monsterList.length;
monsterList.push(
PocketMonster(id, _tribe_name, 1, _tribe_no, _tribe_name, true)
);
}
function や constructor などまぁ初見でもわかる。という感じのものですが、 modifier onlyAdmin
の modifer は良くわからないですよね。これは function に属性を付与できるものです。今回は onlyAdmin つまり「管理者しか叩けない」という属性を function に付与しています。
コントラクトのデプロイについての説明を省いているため、「管理者のみ叩けるってなんだ?」ってなるかとおもいます。solidity で定義した contract はブロックチェーン上にアップロードすることができて、web3 といったフロントエンドから直接 api を呼ぶ用に叩くことができます。その function が contract リソースに影響を及ぼすものであれば、少額の決済をもって取引されます。function を叩く = 決済が発生するとなっていて、そこが Ethereum などのアプリケーションと web アプリケーションの大きな違いでもあります。function は決済であり、管理するリソースは全ユーザーで共有するものとなるため、solidity 上ではよく防御的なプログラミングを通常のアプリケーションより多く見ます。今回はでませんが、modifier には引数を与えることもできるため柔軟に防御できます。
話をポケモンに戻しまして、上記までで「 contract をデプロイした管理者のみポケモンを生み出せる。」となりました。truffle の機能に Mocha を使ったテストフレームワークが組み込まれていて、テストを書くことができます。
contract("PocketMonsterFactory", (accounts) => {
const [alice, bob] = accounts;
let pocketMonsterFactory;
beforeEach(async () => {
pocketMonsterFactory = await PocketMonsterFactory.new({ from: alice });
});
...
...
context("createMonster", async () => {
it("success from admin", async () => {
# create すると
await pocketMonsterFactory.createMonster(145, "サンダー", { from: alice });
# monsterList に何か追加されてて
const actual = await pocketMonsterFactory.monsterList(0);
# 追加されたものは生み出したサンダーであることが確認できる
assert.equal(actual.tribe_no, 145);
assert.equal(actual.tribe_name, "サンダー");
});
...
...
alice と bob は暗号学よくでるキャラで hoge とか bar みたいな感じで使える仮名みたいな感じのようです。(特に意味はないので、satoshi と takeshi とかでもいいです。)
new({ from: alice })
という箇所で alice がこのコントラクトをデプロイしたことを表現しています。そのため以下のように管理者ではない bob がポケモンを生み出そうとして失敗するところをテストできます。
# コレ自体は assert 自体が機能を持ってそうな気はする ...
async function throws_ok(call) {
try {
await call()
assert.fail();
} catch (e) {
assert.isOk(true)
}
}
...
...
it("failed from another", async () => {
await throws_ok(async () => {
await pocketMonsterFactory.createMonster(145, "サンダー", { from: bob });
});
});
これによって管理者のみポケモンを生み出せることができるようになりました。管理者の裁量によってサンダーはこの世に 1 体しか作らないといったようなリソースの希少性を表現することもできます。
ポケモンを捕まえることができるようにする
生み出せるようになり野生にポケモンが跋扈してる状態です。次はポケモンマスターであるトレーナーがポケモンを捕まえる部分を実装します。
コントラクト上のリソースを「入手する」「交換する」「消去する」... といったように特定のオーナーと結びつけた操作を行うにあたり、そのリソースが何かを完全に識別出来るようにする必要があります。それが非代替トークンと呼ばれているもの(Non-Fungible Token)とリソースを結びつけることで実現されています。それを扱う上での便利な標準が用意されていて、EIP-721 として定義されています。(いわゆる NFT)
EIP/ERC というのは、Ethereum の標準規格です。( EIP は提案、ERC はリクエスト、リクエストで提出され協議の上正式に EIP に組み込まれる)
原文は k8s document のように GitHub で管理されていて、具体的な interface が提唱(議論)されています。
この interface に沿って実装をすすめることで NFT と呼べる実装となりますが、openzeppelin というコミュニティ上でそれは既に実実装として提供されています。
今回の程度であればこれを継承して簡易実装するだけで要件を満たせます。
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
contract PocketMonsterFactory is ERC721 { # A is B で B の継承をできる
...
...
constructor() ERC721("PocketMonsterFactory", "MCO") {
...
...
function obtainMonster(uint256 _id, string memory _name) external {
require(monsterList[_id].is_wild);
monsterList[_id].is_wild = true;
monsterList[_id].name = _name;
super._mint(msg.sender, _id); # super から親の function を呼べるのは良くある感じ
}
実際に生み出されたモンスターを捕まえて入手してみます。今そのリソースは誰所有のもの?といった function も openzeppelin 上で実装されています。
it("success case", async () => {
await pocketMonsterFactory.createMonster(145, "サンダー", { from: alice });
await pocketMonsterFactory.obtainMonster(0, "びりい", { from: bob });
assert.equal(await pocketMonsterFactory.ownerOf(0), bob);
});
実装上 require(monsterList[_id].is_wild);
というものを書いています。これは「野生のポケモンしかゲットできない。」を表現していてそれもテストできます。
it("failed case is not wild", async () => {
await pocketMonsterFactory.createMonster(145, "サンダー", { from: alice });
await pocketMonsterFactory.obtainMonster(0, "びりい", { from: bob });
await throws_ok(async () => {
# 既に bob が入手したポケモンなのでゲットできない
await pocketMonsterFactory.obtainMonster(0, "さんさん", { from: alice });
});
});
ポケモンを交換できるようにする
無事ゲットできるようになったので別のケースも考えてみます。ポケモンといえば、トレードも一つの特徴としてあります。これに関しても 721 で実装されてます。
/**
* @dev See {IERC721-ownerOf}.
*/
function ownerOf(uint256 tokenId) public view virtual override returns (address) {
address owner = _owners[tokenId];
require(owner != address(0), "ERC721: owner query for nonexistent token");
return owner;
}
transfer も 簡易の require を足して super から親 function を呼ぶだけで済みます。
function transfer(uint256 _id, address _to) external {
require(monsterToOwner[_id] == msg.sender);
require(msg.sender != _to);
super._transfer(msg.sender, _to, _id);
}
同様にこれもテストから交換が成立したことを確認できます。
await pocketMonsterFactory.transfer(0, alice, { from: bob });
assert.equal(await pocketMonsterFactory.ownerOf(0), alice);
単にリソースを交換する以外にもオーナー毎に価格を変えて売却のようなこともできます。その辺は要件仕様しだいでいくらでも膨らみます。
終わり
Solidity で出来ること、をポケモンで説明してみました。
まだまだ色々な事ができそうで、あれはどうする?これはどうする?がたくさんでてきそうですが今回は以上となります。少しは興味を持って貰えれば幸いです。
実運用で考えると、typescript 化して型がないと開発がつらそうとか id とかもちゃんとつけないとだし、どこからどこまでをデータの公開範囲とするかとかも厳密に精査する必要がありそうです。その辺は実務を積まないとなんとも...な気がしそうですね。(求む分散アプリケーション副業。。。)
個人的には用語がかっこよくてこの領域が好きです。genesis ブロックとかマークルツリーとか ... 今回の説明では全く説明してない部分でもオモシロイ要素がたくさんあります。少しでも興味を持たれた方はぜひとも初めてもらえればと!
※ 僕はこの辺から勉強しました(こういうのもおすすめとかのコメントもお待ちしてます)
書籍 ビットコインとブロックチェーン
Cryptozombies
Discussion