スマートコントラクトでクソゲーを作れ!

By @c1e4/5/2018japanese

NANJコインの快進撃の興奮冷めやらぬ中、1sat買の2sat半分売りで早々に元本を回収してしまい、今年始まって以来一番の後悔の年に苛まれている筆者は、NANJコインの開発言語であるSolidityに興味を持ったのであった。

そもそもNANJコインとは、その名の通り5chのなんJ発の仮想通貨 (正確には暗号通貨) で、イーサリアムというプラットフォーム上で定義されたトークンである。何を言ってるかわけわからねーと思うが、イーサリアムというのは仮想通貨ではあるのだが、それ以前にスマートコントラクトと呼ばれるプログラムをデプロイできるプラットフォームなのだ。イーサリアム上でのオフィシャル通貨のこと自体もイーサリアムと呼ばれるため、大変に混同しやすい。ちなみに通貨としてのイーサリアムは、ビットコインに次ぐ時価総額を誇り、最近はやりのドラゴンボールに例えると、ビットコインが悟空だとしたら、イーサリアムはベジータのようなポジションだ。いや、ポテンシャルはビットコインよりもありそうな感じがするので、もしかすると悟飯が正しいかもしれない。ちなみにドラゴンボール超の最終回は大変に素晴らしいものであった。

話が逸れたが、プラットフォームとしてのイーサリアムで何ができるのか?というと、だいたいなんでもできる。スマートコントラクト 記述言語は何個かあるらしいが、ここでは代表的なSolidityに絞って話をすることにする。っていうかそれ以外見たいことがないので、きっと無視しても構わないのだろう。Solidityはみんな大好きチューリング完全なプログラミング言語で、構文的にはjavascriptに似ている、とされている。個人的にはJavaに近くね?という気もするが、ES2017という意味でのjavascriptであれば確かに似てなくもない。参考までにSolidityのソースコードを貼ってみよう。

pragma solidity ^0.4.19;

import "./ownable.sol";

contract ZombieFactory is Ownable {

    event NewZombie(uint zombieId, string name, uint dna);

    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;
    uint cooldownTime = 1 days;

    struct Zombie {
      string name;
      uint dna;
      uint32 level;
      uint32 readyTime;
      uint16 winCount;
      uint16 lossCount;
    }

    Zombie[] public zombies;

    mapping (uint => address) public zombieToOwner;
    mapping (address => uint) ownerZombieCount;

    function _createZombie(string _name, uint _dna) internal {
        uint id = zombies.push(Zombie(_name, _dna, 1, uint32(now + cooldownTime), 0, 0)) - 1;
        zombieToOwner[id] = msg.sender;
        ownerZombieCount[msg.sender]++;
        NewZombie(id, _name, _dna);
    }

    function _generateRandomDna(string _str) private view returns (uint) {
        uint rand = uint(keccak256(_str));
        return rand % dnaModulus;
    }

    function createRandomZombie(string _name) public {
        require(ownerZombieCount[msg.sender] == 0);
        uint randDna = _generateRandomDna(_name);
        randDna = randDna - randDna % 100;
        _createZombie(_name, randDna);
    }

}

https://cryptozombies.io/ より引用

関数のスコープ宣言が引数の後に来たり、require文など多少の見慣れない表現はあるものの、オブジェクト指向風言語でなんとなく読める気がしてくるだろう。

さて、このコードがどのように動くか、ということをまずざっくり説明する。このコード単体では、ただのスマートコントラクト なので、これをインスタンス化する。インスタンス化という言葉が正しいのかどうかは置いといて、インスタンス化しないことには動かない。

インスタンス化はデプロイと呼ばれ、このソースコードをコンパイルしたものをnewしてイーサリアムのネットワーク上にトランザクションとして放流することで行う。

ZombieFactory = contract.new({ from: eth.accounts[0], data: bin, gas: 1000000 })

ここでのbinには先ほどのソースコードがコンパイルされてできたバイナリが入る。事前にコンパイルしておき、binにバイナリを代入した上で、上のコマンドをイーサリアムのクライアント上で実行するのだ。そうすることで、クライアントがスマートコントラクトをデプロイするトランザクションを生成し、イーサリアムネットワークに放流してくれる。

では、このプログラムは誰が実行するのだろうか?この問いに答えるには、ブロックチェーンの大まかな仕組みを知っておく必要があるが、話せば長くなるので細かくは端折るが、イーサリアムでは、マイナーが新しいブロックを生成する際にそのブロックに含まれるトランザクションが呼び出す関数の実行も行い、さらにその結果も併せてブロックチェーンに書き込むのだ。そうすることで、プログラムと生成されたインスタンスはブロックチェーン上に永久に保存される。一度作られたインスタンスは、明示的に破棄されるまではブロックチェーン上に残り続け、インスタンス変数もそのまま保存される。クソゲーを作るなら、このインスタンスに全ての情報を書き込んでおくイメージになる。

一度デプロイされると一生走り続けるプログラム。それがスマートコントラクト だ。うっかりバグったプログラムをデプロイすると、明示的に終了する方法を作り込んでいない限りそのままなのだ。そして最悪の場合、制御不能になる。

一度インスタンス化してブロックチェーンに取り込まれた後は、プログラムは基本的には誰かが関数を明示的に呼び出さない限りは実行されない。逆にいうと、デプロイされているコントラクトのパブリック関数は、特に制限がない限りは誰でも呼び出すことができるのだ。

例えば、上のSolidityの例にあるcreateRandomZombie関数は、パブリックであるため、誰でも手軽に実行することができる。具体的には、イーサリアムのクライアントから

ZombieFactory.createRandomZombie.sendTransaction("ゾンビくん", { from: eth.accounts[0] })

とコマンドを入力することで、実際のコードを動かすことができる。内部的には、クライアントがこのZombieFactory.createRandomZombieを実行するためのトランザクションを生成し、ネットワークに放流してくれる。この際、実行されるプログラムの複雑さに応じて手数料を支払う必要が出てくる。この手数料はGasと呼ばれ、通貨としてのイーサリアムを使って購入することができる。このように、イーサリアムのプラットフォームを利用するには、何をするにも金がかかる。

こうしてcreateRandomZombieが実行されると、ZombieFactoryのインスタンス変数であるzombiesに新しいZombieがプッシュされ、適宜オーナー情報などが別の変数にセットされる。結構いけてるプログラムの書き方のようだが、手数料やセキュリティなどの都合から、こうするのが良いらしい。

    function _createZombie(string _name, uint _dna) internal {
        uint id = zombies.push(Zombie(_name, _dna, 1, uint32(now + cooldownTime), 0, 0)) - 1;
        zombieToOwner[id] = msg.sender;
        ownerZombieCount[msg.sender]++;
        NewZombie(id, _name, _dna);
    }

ここで、謎の msg.sender という変数があるが、これはSolidity内で使える予約語で、このプログラムを呼び出し元イーサリアムアドレスが格納されている。今回の実行例では、eth.accounts[0] の中身が入っていることになる。この変数をうまく使うことで、誰が関数を呼んだかを確認することができる。

先ほどからなんでゾンビなんだ、と思った読者もいるかもしれないが、それはひとえにこのソースがCryptoZombiesというサイトからの引用だからだ。

https://cryptozombies.io/

インタラクティブにSolidityの文法を学ぶことができるサイトで、ゾンビゲーを作るという程で話が進む。故にゾンビだ。Plants vs Zombiesといい、全く外人のゾンビ好きには呆れるばかりだ。

ここまで読んだところで、ふーん、で、何がそんなに他と違うの?と思った読者もいるかもしれない。Solidityの凄さはここからだ。まずは下のコードを見て欲しい。

pragma solidity ^0.4.21;

contract SimpleAuction {
    address public beneficiary;
    uint public auctionEnd;

    address public highestBidder;
    uint public highestBid;

    mapping(address => uint) pendingReturns;

    bool ended;

    event HighestBidIncreased(address bidder, uint amount);
    event AuctionEnded(address winner, uint amount);

    function SimpleAuction(
        uint _biddingTime,
        address _beneficiary
    ) public {
        beneficiary = _beneficiary;
        auctionEnd = now + _biddingTime;
    }

    function bid() public payable {
        require(now <= auctionEnd);

        require(msg.value > highestBid);

        if (highestBid != 0) {
            pendingReturns[highestBidder] += highestBid;
        }
        highestBidder = msg.sender;
        highestBid = msg.value;
        emit HighestBidIncreased(msg.sender, msg.value);
    }

    function withdraw() public returns (bool) {
        uint amount = pendingReturns[msg.sender];
        if (amount > 0) {
            pendingReturns[msg.sender] = 0;

            if (!msg.sender.send(amount)) {
                pendingReturns[msg.sender] = amount;
                return false;
            }
        }
        return true;
    }

    function auctionEnd() public {
        require(now >= auctionEnd);
        require(!ended);

        ended = true;
        emit AuctionEnded(highestBidder, highestBid);

        beneficiary.transfer(highestBid);
    }
}

注目すべきはbid関数だ。見慣れない修飾子であるpayableが付いている。これはどういう意味かというとその通り、関数呼び出しの際にお金を払うことができる関数なのだ。

    function bid() public payable {
        require(now <= auctionEnd);

        require(msg.value > highestBid);

        if (highestBid != 0) {
            pendingReturns[highestBidder] += highestBid;
        }
        highestBidder = msg.sender;
        highestBid = msg.value;
        emit HighestBidIncreased(msg.sender, msg.value);
    }

先ほどの例で、関数の呼び出しのためにトランザクションを生成し、ネットワークに放流した。もともとブロックチェーンにおけるトランザクションは、特定のアドレスに対して送金を行うためのものであった。なので、それをそのまま関数呼び出しにも利用することができる。するとこのように、オークションでビッドをする際に、通常の送金処理同様にお金を送り、そのお金を引数としてプログラムを実行することができるのだ。つまり、それはSolidityがプログラミング言語レベルで課金処理をサポートしているということに他ならない。なんと金の取りやすいプラットフォームなことか!実際Solidityで書かれたほとんどのDappsはカジュアルに課金してくる。というか課金しないと遊べないのだ。基本無料のソーシャルゲームがバカみたいだ。

次に注目すべきは、withdraw関数だ。

    function withdraw() public returns (bool) {
        uint amount = pendingReturns[msg.sender];
        if (amount > 0) {
            pendingReturns[msg.sender] = 0;

            if (!msg.sender.send(amount)) {
                pendingReturns[msg.sender] = amount;
                return false;
            }
        }
        return true;
    }

ここでは、メッセージの送信元アドレスに対しての返金すべき額があれば、そのままお金を送り返す処理を実行する。つまりこのシステムの流れはこうだ

  1. Aさんが入札金額分のETH(通貨としてのイーサリアムの単位)を添えて、bid関数を呼び出すトランザクションを生成し、ネットワークに流す
  2. Bさんがマイニングを行いそのトランザクションをブロックに取り込む際に、bid関数を実行し、結果と共に最新のコントラクトの状態をブロックに書き込む
  3. Cさんがより高い金額で入札を行うトランザクションを流す
  4. Bさんがマイニングを行い、最高入札者をCさんにセットし、Aさんへの変金額をpendingReturnsに記録する
  5. Aさんがwithdraw関数を呼び出すトランザクションを流す
  6. Bさんがマイニングを行い、Aさんに対する返金処理を実行する

こうしてAさんの手元にお金が帰ってくる。Aさんが呼びださない限りはお金が帰ってこないのだが、送りつけるよりも返金処理を本人にしてもらうのがセキュリティ的に正しいなやり方だそうだ。高値更新されたタイミングで即返金してしまうプログラムでは、高値を更新された後の返金処理で意図的にエラーを起こし、後続の入札をできなくしてしまうようなスマートコントラクト が実装できてしまうからなのだ。

詳しくは

堅牢なスマートコントラクト開発のためのブロックチェーン[技術]入門

を参考にされたい。

さぁ、面白くなって来ただろう。さらに面白いのが、この返金処理のプログラムにバグがあると、二度とお金を取り戻せなくなるということだ。実際にスマートコントラクト のバグのせいで、ETHがGOX(仮想通貨が取り出せなくなること。以前に仮想通貨をごそっと盗まれたMt.GOXに由来) したことがあったのだ。被害総額が数十億円というからマジで笑えない。DAO事件と呼ばれるらしいが、詳しいことはGoogleに任せようと思う。SAO事件ではないことに注意だ。

関係ないがSAOのアニメの新シーズン、アリシゼーション編は2018年10月オンエアらしい。大変楽しみだ。

このように、イーサリアムのスマートコントラクトを用いてアプリケーションを作ると、手軽に課金でき、さらに手軽にお金を返すことができるのだ。筆者がやり始めたBitpetsと呼ばれるゲームでは、ペットを買い、交配させっつつ、新しく生まれたペットを売ることができる。売って儲かったお金はそのままETHにして引き出せるので、リアルマネートレードとは一体、という世界観になっている。万が一そういうゲームが爆発的に流行ると、違う意味でのプロゲーマーがたくさん誕生することになるだろう。

現状のアプリケーションでは、リアルタイム性の制約だったり、ブロックチェーンに書き込むデータサイズの制約だったりするせいで、主にクソペットを量産してトレードするゲームが主流だが、イーサリアムのアップデートやSolidityの知見が溜まってくることで、よりインタラクティブなゲームの開発も可能になってくると思われる。

そうそう、あまり解説していなかったが、Solidityはプログラミング言語としても非常に面白い機能を備えている。その一つがrequire文だ。

    function createRandomZombie(string _name) public {
        require(ownerZombieCount[msg.sender] == 0);
        uint randDna = _generateRandomDna(_name);
        randDna = randDna - randDna % 100;
        _createZombie(_name, randDna);
    }

createRandomZombie関数の1行目がまさしくそれであり、これはこの関数を実行したアドレスに紐づくゾンビが0であることを関数実行の必要条件として定義している。そのため、すでにゾンビを所有している人が実行するとエラーが返されるのだ。これはまさしく契約プログラミング。

さらに、このようによく使うrequire文をpayableのような修飾子として実装することができる。

 modifier onlyOwnerOf(uint _zombieId) {
    require(msg.sender == zombieToOwner[_zombieId]);
    _;
  }

例えばこのonlyOwnerOf修飾子は、指定された_zombieIdの所有者が、関数の実行者である場合にのみ、この修飾子が付いた関数の実行を許可する。具体的な使い方は以下だ。

  function changeName(uint _zombieId, string _newName) external aboveLevel(2, _zombieId) onlyOwnerOf(_zombieId) {
    zombies[_zombieId].name = _newName;
  }

ゾンビの名前を変えられるのは、そのゾンビのオーナーであり、かつそのゾンビのレベルが2以上である場合だけに絞っている。もちろんこのaboveLevel修飾子もカスタム修飾子で、定義は

  modifier aboveLevel(uint _level, uint _zombieId) {
    require(zombies[_zombieId].level >= _level);
    _;
  }

だ。ちなみにrequire文の後の_;は、元の関数の実行に戻るという意味の文で、return文のようなものだ。

さて、ここまでざっと駆け足でSolidityの凄さ、およびスマートコントラクト のポテンシャルについて語ってきたわけだが、いかがだっただろうか。ここから先、実際のコードを書きテストをするフローなどは、適宜Qiitaなどを参考にされたい。

Ethereum スマートコントラクト入門:geth のインストールから Hello World まで

この記事でプライベートチェーンを生成し、その中でデプロイすれば、実際のETHを消費することなく好きなだけスマートコントラクト で遊ぶことができるはずだ。

今日から君も、スマートコントラクト マスターだ!

3

comments