🏭

【Solidity 超入門】CryptoZombiesを通してdAppを作る 【part3】

2023/01/07に公開

Block Chainを勉強しようと志す人はスマートコントラクトの実装を避けては通れないと思います。solidityというプログラミング言語でスマートコントラクトを書くのがデファクトスタンダードですので、まずsolidiyを学習することになると思います。
solidityを学習しようと思い、ググってみると大体「CryptoZombies」なるサービスを推している人や記事が多くヒットします。ですが実際にサイトを見ると英語やん...日本語もあまりちゃんと翻訳されないし、取っ付きづらいと思われる方も多いと思います。今回はCryptoZombiesの取っ付きづらさをこの記事で可能な限り払拭でき、solidity学習で躓いていた方の助けになればと思います。

part1の記事はこちら
part2の記事はこちら

対象読者

JavaScript等の他の言語でのプログラミングの経験がある方。
ですが、そこまで文法的には難しくないのでわからない用語があれば都度ググっていただければ読み進めることはできると思います。

solidityとは

solidityはイーサリアム上で動作するスマートコントラクトを実装する為の開発言語で、JavaScriptと似た性質を持った「コントラクト指向言語」と呼ばれるものです。

CryptoZombiesとは??

CryptoZombiesは、暗号からゾンビを生み出すゲームの開発を通じて、Solidityでスマートコントラクトの構築を学習できる、インタラクティブなオンラインレッスンです。

Making the Zombie Factory

ゾンビ兵士の工場を作りながら、solidityの基礎を学んでいきます。最終的に名前を受け取り、それを使ってランダムなゾンビの兵士を生成し、ブロックチェーン上のアプリのゾンビデータベースに生成したゾンビを追加する関数が完成します。

それでは続きから見ていきましょう。

Chapter9

Private関数とPublic関数

Solidity では、関数はデフォルトでpublicです。これは、誰でも (あるいは他のコントラクトでも) あなたのコントラクトの関数を呼び出して、そのコードを実行できることになります。
どこからでもアクセスできて良いのかと言われるとこれは危険です、あなたのコントラクトが攻撃に対して脆弱になる可能性があります。したがって、デフォルトでは関数をprivateにしておき、公開したい関数のみを publicにするのが良きです。このprivatepublicといったものがアクセス修飾子と呼ばれるもので、可視性修飾子とも呼ばれています。

それでは、private関数の宣言の仕方を見てみましょう。

uint[] numbers;

function _addToArray(uint _number) private {
  numbers.push(_number);
}

つまり、このコントラクト内の他の関数だけがこの_addToArray関数を呼び出して、numbers配列に追加することができます。
上記のように、関数名の後にprivateというキーワードを使っており、また関数の引数と同様にprivate関数名の先頭にはアンダースコア(_)を付けるのが慣例となっています。

アクセス修飾子(可視性修飾子)

上記のprivatepublicを含めて solidityでは4つのアクセス修飾子が存在しています。

コントラクト外部 コントラクト内部 継承先
public ⭕️ ⭕️ ⭕️
private ⭕️
internal ⭕️ ⭕️
external ⭕️

ref: https://daiki-sekiguchi.com/2018/07/16/ethereum-solidity-access-modifiers/

もう少し説明しておくと、

publicは、どこからの実行を許容するもので、publicで定義された変数や関数は、

  • 同じコントラクト内からの呼び出し
  • 継承したコントラクトからの呼び出し
  • コントラクト外部からの呼び出し

に対応しています。
後述する関数の修飾子を何も付けなかった場合、その変数や関数はpublicとして扱われます。

privateは、

  • 同じコントラクト内の呼び出し

にのみ許容します。
コントラクト外部や継承されたコントラクトからの呼び出しには対応していません。

internalは、

  • 同じコントラクト内の呼び出し
  • 継承されたコントラクトからの呼び出し

を許容します。また、変数へ修飾子を何も付けなかった場合、internalになります。

externalは、

  • コントラクト外部からの呼び出し

を許容するのみです。

Chapter9の答え

コントラクトのcreateZombie関数は、デフォルトでpublicになっているので、これをブライベートにしましょう。createZombieを変更して、private関数にしてね、命名規則を忘れないでね。ということですので実際のコードは以下になります。

pragma solidity >=0.5.0 <0.6.0;

contract ZombieFactory {

    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;

    struct Zombie {
        string name;
        uint dna;
    }

    Zombie[] public zombies;

    function _createZombie(string memory _name, uint _dna) private {
        zombies.push(Zombie(_name, _dna));
    }
}

Chapter10

関数を詳細に

関数の戻り値や関数の修飾子についてみていきましょう。

返り値

関数からの返り値を指定するには以下のような書き方をします。

string greeting = "What's up dog";

function sayHello() public returns (string memory) {
  return greeting;
}

solidity では、関数宣言に戻り値の型 (この場合はstring) が含まれます。solidityはJavaScriptやTypesScriptと似ていると言われがちですが、ここはTypesScriptとはちょっと異なりますね。

関数の修飾子(view修飾子とpure修飾子)

solidityは変数スコープを厳格に宣言する必要があります。(どの言語と比べるかにも寄るが、C#とかと比べると厳格かも)この宣言時に修飾子を付けるのですがこれがview修飾子とpure修飾子です。view修飾子とpure修飾子は関数の内で参照されるスコープによって使い分ける必要があります。それぞれ見ていきましょう。

view修飾子

インスタンス変数を参照するときです。例えば、

contract SampleContract {  
    uint8 hoge = 16;  

    function viewFunction() public view returns(uint8) {  
        return hoge;  
    }
}

というコードで見てみましょう。ここでいうインスタンス変数はhogeです。このインスタンス変数をviewFunction内で使用しています。この時はview修飾子をつける必要がります。もしpure修飾子を指定した場合、Remix[1]でのコンパイル時に以下のエラー出ます。

TypeError: Function declared as pure, but this expression (potentially)  
reads from the environment or state and thus requires "view". 

view修飾子は関数の中で関数外の変数にアクセスしたいかつその変数は変更しない場合につけることができます。

pure修飾子

ローカル変数を参照する時です。以下のコードを見てください。

function _multiply(uint a, uint b) private pure returns (uint) {
return a * b;
}

この関数は、アプリの状態を読み取ることもなく、返り値は関数のパラメータにのみ依存します。つまり関数がローカル変数abのみを参照しています。この場合は関数にpure修飾子をつけます。

まとめると、

  • view関数はアプリ内のデータは読み取り専用になる。
  • pure関数はアプリ内のデータそのものにアクセスできない。

となります。

Chapter10の答え

  • 文字列からランダムなDNA番号を生成するヘルパー関数が欲しい。
  • generateRandomDnaと呼ばれるprivate関数を作成する。
  • この関数は_str(string型)という1つのパラメータを受け取り、uintを返す。
  • パラメータ_strのデータロケーションをメモリに設定する。
  • この関数はコントラクトの変数を見ることはできるが変更することはできないので、viewとしてマークする。
  • 因みに関数の中は空で良い。

とあるので、

pragma solidity >=0.5.0 <0.6.0;

contract ZombieFactory {

    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;

    struct Zombie {
        string name;
        uint dna;
    }

    Zombie[] public zombies;

    function _createZombie(string memory _name, uint _dna) private {
        zombies.push(Zombie(_name, _dna));
    }

    function _generateRandomDna(string memory _str) private view returns(uint){}
}

となります。part9同様にprivate関数名の先頭にはアンダースコア(_)を付けるのが慣例となっているのでこちらも忘れないようにしましょう。

Chapter11

Keccak256と型キャスティングについて学びます。

型キャスティング(型キャスト)は「データ型を変換すること」です。型をキャストする(変換する)ですね。このChapterでは、ランダムなゾンビ兵士のDNAを返す_generateRandomDna関数を作ります。この時にKeccak256と型キャスティングを用いるよという話です。それぞれ見ていきましょう。

Keccak256

Keccak256はハッシュ関数の一つです。Ethereumで使われており、公開鍵からアドレスを導出するのに使われています。SHA3-256[2]とも呼ばれます。要は文字列を与えてランダムなハッシュ値(これも文字列)を返してくれるものです。ブロックチェーンにおいて、安全な乱数生成は非常に難しい問題でCryptoZombiesで出てくる暗号化がセキュアであるわけではないのでそこだけは注意してください。

型キャスティング

先ほども述べましたが、型変換です。データ型間の変換が必要な場合があります。次のような例を見てみましょう。

uint8 a = 5;
uint b = 6;
// a * b は uint8 ではなく uint を返すので、エラーを投げます。🙅‍♂️
uint8 c = a * b;

// b を uint8 に型キャスティングして動作させます。🙆‍♂️
uint8 c = a * uint8(b);

上記はa * bはuint型なのに、これをuint型のcに格納しようとしています。当然ですが、コンパイラーに怒られます。なのでuint8(b)として型変換を行なっています。このようにすることで潜在的な問題を回避することができます。

Chapter11の答え

_generateRandomDna関数の本体を埋めてください!とのことです。以下の条件を満たす必要があります。

最初の行は

  • abi.encodePacked(_str)のkeccak256ハッシュを受け取って疑似ランダムな16進数を生成
  • それをuint型として型キャスティングする。
  • その結果をuint型のrandという変数に格納する。

2行目のコードはdnaModulusの時に書かれていたようにDNAを16桁の長さにしたいので

  • 上記の値モジュラス(%)dnaModulusを返す。

上記の条件を満たすコードは以下です。

pragma solidity ^0.4.25;

contract ZombieFactory {

    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;

    struct Zombie {
        string name;
        uint dna;
    }

    Zombie[] public zombies;

    function _createZombie(string memory _name, uint _dna) private {
        zombies.push(Zombie(_name, _dna));
    }

    function _generateRandomDna(string memory _str) private view returns (uint) {
        // here
        uint rand = uint(keccak256(abi.encodePacked(_str)));
        return rand % dnaModulus;
    }
}

Chapter12

今まで作成したものをすべてを結びつけるpublic関数を作成するChapterです。入力であるゾンビの名前を受け取り、その名前を使ってランダムなDNAを持つゾンビ兵士を作成するpublic関数を作成してみましょう。この章は特に何かトピックがあるわけではないので問題文の通りにコーディングすれば終わりです。

  • createRandomZombieという名前のpublic関数を作成します。この関数は、_nameという名前の1つのパラメータ(メモリにデータ位置が設定された文字列)を受け取ります。(注意: 以前の関数をprivateと宣言したのと同様に、この関数もpublicと宣言してください。)

  • 関数の1行では、`_generateRandomDna`関数を_nameに対して実行し、それをrandDnaという名前のuintに格納します。

  • 2行目は、_createZombie関数を実行し、_namerandDnaを引数で渡します。

  • 答えのコードは、4行のコード(関数の終了 }を含む)でなければなりません。

といった条件が提示されています。コードにすると以下です。

pragma solidity  >=0.5.0 <0.6.0;

contract ZombieFactory {

    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;

    struct Zombie {
        string name;
        uint dna;
    }

    Zombie[] public zombies;

    function _createZombie(string memory _name, uint _dna) private {
        zombies.push(Zombie(_name, _dna));
    } 

    function _generateRandomDna(string memory _str) private view returns (uint) {
        uint rand = uint(keccak256(abi.encodePacked(_str)));
        return rand % dnaModulus;
    }
    // here
    function createRandomZombie(string memory _name) public {
        uint randDna = _generateRandomDna(_name);
        _createZombie(_name,randDna);
    }
}

Chapter13

実質最後のChapterです!🍻イベントについて学んでいきましょう。

イベント

イベントは、コントラクトがブロックチェーン上で何かが起こったことをアプリのフロントエンドに伝えるための方法です。フロントエンドは特定のイベントを「リスニング」して、それが起こったときにアクションを起こすことができます。以下の例を見てください。

// イベントを宣言する
event IntegersAdded(uint x, uint y, uint result);

function add(uint _x, uint _y) public returns (uint) {
  uint result = _x + _y;
  // 関数が呼び出されたことをアプリに知らせるためにイベントを発生させます
  emit IntegersAdded(_x, _y, result);
  return result;
}

上記のイベントを以下のようにフロントエンドから呼ぶことができます。

YourContract.IntegersAdded((error, result) => {
  // do something with result
})

Chapter13の答え

新しいゾンビが作成されるたびに、フロントエンドにイベントを通知し、アプリがそれを表示できるようにしたいとのことです。以下の条件を満たすようにコーディングしましょう。

  • NewZombieというイベントをまず宣言します。このイベントには、zombieId (uint), name (string), dna (uint) を引数として渡します。

  • zombies配列に新しいZombieを追加した後にNewZombieイベントを発生させるように、_createZombie関数を変更する。

  • array.push()は配列の新しい長さをuint型で返し、配列の最初の要素のインデックスは0なのでarray.push() - 1が先ほど追加したゾンビのインデックスとする。

  • zombies.push() - 1の結果をidという uint に格納し、次の行のNewZombieイベントで使用できるようにする。

コードは、

pragma solidity >=0.5.0 <0.6.0;

contract ZombieFactory {

    // declare our event here
    event NewZombie(uint zombieId, string name, uint dna);

    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;

    struct Zombie {
        string name;
        uint dna;
    }

    Zombie[] public zombies;

    function _createZombie(string memory _name, uint _dna) private {
        uint id = zombies.push(Zombie(_name, _dna)) - 1;
        // and fire it here
        emit NewZombie(id, _name, _dna);
    }

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

    function createRandomZombie(string memory _name) public {
        uint randDna = _generateRandomDna(_name);
        _createZombie(_name, randDna);
    }
}

Chapter14

Web3.js

これにて solidityのコントラクトは完成です👏👏
次に、コントラクトと対話するjavascript/typescriptのフロントエンドを作成する必要があります。Ethereumには Web3.js と呼ばれるJavascript ライブラリがあります。
Making the Zombie Factoryの後のレッスンで、コントラクトをデプロイして Web3.js をセットアップする方法について詳しく説明していますので、ぜひ次のレッスンもやってみてください。

Web3.js がデプロイされたコントラクトとどのように相互作用するかについては、Chapter14のVue.jsでのサンプルコードを貼っておきますのでご参考までにみていただけると良いかなと思います。

// Here's how we would access our contract:
const abi = /* abi generated by the compiler */;
const ZombieFactoryContract = web3.eth.contract(abi);
const contractAddress = /* our contract address on Ethereum after deploying */;
const ZombieFactory = ZombieFactoryContract.at(contractAddress);
// `ZombieFactory` has access to our contract's public functions and events

// some sort of event listener to take the text input:
$("#ourButton").click((e) => {
  const name = $("#nameInput").val()
  // Call our contract's `createRandomZombie` function:
  ZombieFactory.createRandomZombie(name)
})

// Listen for the `NewZombie` event, and update the UI
const event = ZombieFactory.NewZombie((error, result) => {
  if (error) return;
  generateZombie(result.zombieId, result.name, result.dna)
})

// take the Zombie dna, and update our image
function generateZombie(id, name, dna) {
  let dnaStr = String(dna)
  // pad DNA with leading zeroes if it's less than 16 characters
  while (dnaStr.length < 16)
    dnaStr = "0" + dnaStr

  let zombieDetails = {
    // first 2 digits make up the head. We have 7 possible heads, so % 7
    // to get a number 0 - 6, then add 1 to make it 1 - 7. Then we have 7
    // image files named "head1.png" through "head7.png" we load based on
    // this number:
    headChoice: dnaStr.substring(0, 2) % 7 + 1,
    // 2nd 2 digits make up the eyes, 11 variations:
    eyeChoice: dnaStr.substring(2, 4) % 11 + 1,
    // 6 variations of shirts:
    shirtChoice: dnaStr.substring(4, 6) % 6 + 1,
    // last 6 digits control color. Updated using CSS filter: hue-rotate
    // which has 360 degrees:
    skinColorChoice: parseInt(dnaStr.substring(6, 8) / 100 * 360),
    eyeColorChoice: parseInt(dnaStr.substring(8, 10) / 100 * 360),
    clothesColorChoice: parseInt(dnaStr.substring(10, 12) / 100 * 360),
    zombieName: name,
    zombieDescription: "A Level 1 CryptoZombie",
  }
  return zombieDetails
}

part3の最後に

これでCryptoZombiesで使用するゾンビの兵士を生成するコントラクトが完成です。名前を受け取り、それを使ってランダムなゾンビの兵士を生成し、ブロックチェーン上のアプリのゾンビデータベースに生成したゾンビを追加する関数です。

Making the Zombie Factory編 お疲れ様でした。今回はChapter9~14までをまとめてみました。なるべく「Making the Zombie Factory」は1記事にまとめたかったですが、分量が多くなりそうなのでpart分けしております。ここまででsolidityの初級レベルは触れたかなと思います。この後のレッスン2以降でより詳しくsolidityを学べますので興味がある方はぜひ挑戦してみてください。(私も機会があれば記事にしたいと思います。)それでは。

References

https://cryptozombies.io
https://daiki-sekiguchi.com/2018/07/16/ethereum-solidity-access-modifiers/
https://eng.shibuya24.info/entry/solidity_view_pure
https://daiki-sekiguchi.com/2018/07/23/ethereum-solidity-keccak256/

脚注
  1. まずEVMについて、EVM(ETHEREUM VIRTUAL MACHINEの略)とはEthereumのスマートコントラクトの実行やアカウントの状態が保持できるVIRTUAL MACHINEです。Remixはブラウザ上でEVMのスマートコントラクトの開発言語であるSolidityを用いてコントラクト開発をするためのIDEです。 ↩︎

  2. SHA3(Secure Hash Algorithm 3)は、任意の長さの原文から特徴的な固定長の値(ハッシュ値)を算出するハッシュ関数(要約関数)の標準規格の一つで、SHA-1およびSHA-2の後継です。データの同一性の確認に用いられます。SHA3-256の-256はハッシュ値の出力長(bits)を表しています。基本的にSHA-256もその他のハッシュ関数も理論上セキュアと言われていますが、確実に危険がないというわけではありません。
    そしてここまで知らなくても良いですが実はKeccak256===SHA3-256というわけではありません。これは両方に同じ文字列を与えて生成されるハッシュ値を見ればわかります。設計されたKeccakがSHAに採用されましたがSHA-3としての採用が遅れ、それを見たEthereumがKeccakを採用しています。なのでここで使われているのはKeccak256が正しいです。 ↩︎

Discussion