Solidityの勉強のため、CryptoZomibiesをやりました。
分かりやすい日本語でいい感じです。
左側の説明文を読んだ後に、右側のエディタに入力してみて、答え合わせボタンを押すと正しいか判断してくれるという流れで、スムーズに学習できます。
レッスン1から6まであり、ボリュームがあります。
Solidityの公式ドキュメントも見ながら進めました。
長いので、前半(レッスン1-3)と後半(レッスン4-6)に分けました。
レッスン1:ゾンビファクトリーの作成
変数
- 変数のことを状態変数(state variable)と呼ぶ。
- 変数定義の
uint
はuint256
のエイリアス。他にint
(符号付き)などもある。公式ドキュメントのTypesを参照。 - 構造体もある。公式ドキュメントのStructsを参照。関数の引数として渡すこともできる。mappingとは違うので注意。
struct Person {
uint age;
string name;
}
- 変数を
public
で宣言すると、自動的にgetterが用意される。 宣言の書き方は、
Zombie[] public zombies;
のように、型名、public、変数名の順番なので注意(publicは型名の後)。グローバル変数と区別するため、関数パラメータ変数名には、頭にアンダーバー
_
を付けるのが通例。配列の末尾に要素を追加するのに、
push()
を使う。
uint[] numbers;
numbers.push(5);
numbers.push(10);
numbers.push(15);
// 数字は [5, 10, 15]
-
配列の末尾に
push()
で要素を追加すると、戻り値として、追加後の長さを返す。公式ドキュメントの「Arrays」のMembersを参照。- CryptoZombiesでは「新しい長さのuint配列を返し」とあるが、書くなら「新しい配列の長さをunitで返し」だと思う。
型キャストは、
uint8 c = a * uint8(b);
のように、関数っぽく書く。「(uint8)b」ではないので注意。
関数
関数の公開範囲。関数をコントラクト内からのみ呼び出せるように、関数名(引数)の後ろに
private
をつける。関数名の頭にアンダーバー_
をつけるのが通例。公式ドキュメントのVisibility and Gettersを参照。関数の戻り値は、
function sayHello() public returns (string) {
のように、returns (型名,...)
を付ける。-
関数の修飾子。
view
(データの読み取りだけ)、pure
(データの読み書きもしない)など。詳細は公式ドキュメントのMiscellaneousのModifiersを参照。- 書き順は
function _generateRandomDna(string _str) private view returns (uint) {
- 書き順は
ハッシュ関数keccak256()
- ハッシュ関数
keccak256()
は、文字列をランダムな256ビットの16進数にマッピングする。- ハッシュ化の他に、文字列比較にも使う。
イベント(event)
- イベントを使うと、コントラクトからアプリのフロントエンド(JavaScriptとか)に情報を通知できる。
- 特定のイベントをListeningできる。イベントが発生したタイミングで通知できる。
- パッと見で、関数(何か処理をする)と、イベント(フロント側に通知する)の区別がつきづらいので、名前は工夫する必要がありそう。(処理したつもりがイベント通知だけだったとかありそう)
(Solidity側)
// イベントの宣言
event IntegersAdded(uint x, uint y, uint result);
function add(uint _x, uint _y) public {
uint result = _x + _y;
// 関数が呼ばれたことをアプリに伝えるためにイベントを発生させる:
IntegersAdded(_x, _y, result);
return result;
}
(JavaScript側)
YourContract.IntegersAdded(function(error, result) {
// 結果について何らかの処理をする
});
その他
- 変数名の
dnaModulus
をdnaModulesと打ち間違いました。
レッスン2:ゾンビが人間を襲う
-
mapping・・・キーバリューストア。連想配列のこと。mapping。
-
mapping (uint => string) userIdToName;
のようにmapping (キーの型 => バリューの型) マッピング名
と書く。 - 上記の例では、
userIdToName[id]
のように、uint型を渡すと、string型が返ってくる。 - 配列と同じなので、丸カッコではなく角カッコ
[ ]
なので注意。
-
address型・・・イーサリアムのアカウント?
msg.sender・・・その関数を呼び出したユーザ(コントラクトの場合もある)のアドレス(address型)。グローバル変数。
require(条件式)・・・条件が揃わない場合は、エラーを出して処理を中断する。if文で中断させるより分かりやすい。
継承
contract BabyDoge is Doge { }
のように、コントラクト名の後ろにis
を使う。-
import・・・別ファイルを読み込む。
import "./zombiefactory.sol";
-
import "somepackage/SomeContract.sol";
のように、npmとかで入れた外部パッケージも読み込める。
ストレージvsメモリ
- 変数の格納場所には、
storage
とmemory
がある。- storageは永続的。
- memoryは一時的。
- 変数宣言時に
Zombie storage myZombie;
のように、型名 格納場所 変数名
で指定する。 - 明示しないとコンパイラが自動で判断してくれる(あやしい時はwarningが出る)
- (chapter7は、_zombieIdがzombies[]の添字になる。初回は分からなかったので後で復習しておく)
別の関数とビジビリティ(可視性修飾子)
- Solidityにはpublic とprivateの他に、internal と externalという関数用のビジビリティが用意されている。
- internalはprivateと同じだが、このコントラクトから継承したコントラクトにもアクセスできるようになる。
- externalはコントラクト外からのみ呼び出せる。
- publicはコントラクト内外から呼び出せる。
別なコントラクトとのやりとり
- ブロックチェーン上の別なコントラクト内の変数や関数を使える。
- インタフェース定義を使う。
- コントラクトを書いて、中には関数の型だけ定義して、中身の{}を書かずに;で終わる。
contract NumberInterface {
function getNum(address _myAddress) public view returns (uint);
}
- 使う時は、デプロイされた時のアドレスを渡してインスタンス化する。
NumberInterface n = NumberInterface(0x38ea...);
n.getNum(msg.sender);
レッスン3:Solidityの高度なコンセプト
- コントラクトをイーサリアム上にデプロイすると、イミュータブルになる。つまり編集も更新もできなくなるということ。
外部のコントラクトに依存するコントラクト
- 型とコントラクトのデプロイ先アドレスを指定して、外部のコントラクトをインスタンス化。
- 依存先のコントラクトがバグなどで書き換わっても、アドレスを変えれば新しいコントラクトでインスタンス化できる。
modifier修飾子
OpenZeppelinライブラリの提供する
Ownable
コントラクトを例にして説明している。modifier
修飾子。関数みたいなもの。例ではmodifier onlyOwner{...}
で、デプロイしたアカウントだけが使えるような修飾子を定義している。使い方は、関数宣言の最後に修飾子をつける。例:
function likeABoss() external onlyOwner {
。-
実行時は、先にmodifierが呼ばれ、modifierの中の
_;
が呼ばれると、元の関数が実行される。- 関数の実行前に、requireで権限チェックする時に、簡単に書ける。
DAppsを書く時に、オーナーがなんでもできるようにすると、オーナーがバックドアを仕込んだりできてしまうので、どれだけ権限を与えるかのバランスを考える必要がある。
引数を持つ関数修飾子(modifier)
// 一定の年齢よりユーザーの年齢が高いことを要件とする修飾子だ:
modifier olderThan(uint _age, uint _userId) {
require (age[_userId] >= _age);
_;
}
ガス(gas, 燃料)
- 関数の実行にはgasが必要。実行に必要な計算機資源によってgas代が変わる。
- 関数の状態修飾子に、
view
とpure
がある。 -
view
の関数は、gas代がかからない(データを参照するだけだから)。 -
pure
の関数も、gas代がかからない(こちらは参照もしない)。 - storageへの書き込みは、高いgas代がかかる。
- 関数の状態修飾子に、
- 構造体(struct)の中では、できるだけ小さいuintを使うと、gas代の節約になる。
- 例えば、
uint
よりもuint32
の方がgas代が安くてすむ。 - 同じ型の変数を隣り合わせて書くのもgas代を安くできる。
- 例えば、
- 必要な時以外は
storage
変数に値を書き込まないように、memory
変数に配列を再生成するやり方が重要。
Time units
-
now
変数で、現在時刻をunixtime(秒)で取得できる。uint型(uint256型)なので、場合によって型キャストする。 - seconds、 minutes、 hours、 days、weeks 、yearsという単位があり、uint型の秒に変換される。
- 例では、readyTime変数で捕食の間隔を制限している。
構造体のポインタ??
-
function _triggerCooldown(Zombie storage _zombie) internal {
のように、引数にstorageを指定できる。- 「ポインタ」とあるが、アドレス渡しの事なのか不明。。。
public関数とセキュリティ
- (チャプター7の「まずmyZombieを参照したあと、」という文が何を指しているのか分からない)
memoryで変数を宣言して使う
-
storage
を使わないですむように、memory
で変数を宣言して使う。- 配列の場合は、必ず長さを指定しなければならない。
new
を使う。
- 配列の場合は、必ず長さを指定しなければならない。
// 長さ3の新しい配列をメモリ内にインスタンス化する
uint[] memory values = new uint[](3);
for文の書き方
for (uint i = 0; i < zombies.length; i++) {
if (zombieToOwner[i] == _owner) {
result[counter] = i;
counter++;
}
}