Solidity 開発における落とし穴と、混同されやすい点のメモです。
Array vs Mapping
Array と Mapping はけっこう違います。
他の言語の Mapping と同じと解説してあるところもありますが、そうだと思って開発を進めるとハマります。
特に、Ethereum 本番環境で、Gas を使う必要があるトランザクションの場合にはコスト面において大きな違いになります。マイナー(採掘者)は、Gas Priceが低いトランザクションを無視する自由があり、通常のマイナーは、Gas Priceが高いものから実行していきます。そのため、Gas を適切に消費する戦略が Ethereum アプリを開発する上で必要になります。
Array は Linear time search で、 Mapping は Constant time search
Array は Linear time search です。O(n) となるので、オブジェクトの数が増えれば増えるほど Gas を使うことになります。
一方で、Mapping は Constant time search です。O(1) となるので、オブジェクトの数が増えても Gas を使う量は一定です。
https://en.wikipedia.org/wiki/Time_complexity
使用目的は両者とも配列、もしくは Key Value な形で "情報を格納する" ということなので、似ていますが、取得方法が違うので、使いどころには気をつける必要があります。
Array: 全件操作する
Mapping: 1件の操作をする
例えば、Array にアドレス情報をどんどん格納していき、最終的に For ループで該当の1件を探してなんらかのアップデートの操作をする。といったことは基本的にはバッドプラクティスです。
そういう時には Mapping を使って狙い撃ちしましょう
mapping (address => bool) public wallets;
function setWallet(address _wallet) public{
wallets[_wallet]=true;
}
Mapping は必ず何かしらを返す
Solidity は各型ごとに デフォルトバリュー が存在します。例えば以下のようになっている時
// wallets を public で定義
mapping (address => bool) public wallets;
// function 内で下記のように定義
wallets[0x123456...123]=true;
wallets[0x111111...111]=false;
じゃあ、wallets[0x222222...222]
これは? まだこのアドレス(key)に紐づく ブール(value) は定義されていません。
この答えは false
です。何故ならば bool 型のデフォルトバリューは false だからです。
例えば、undefined / false / true の3種類で何かを格納して判断材料にしたいという場合に Mapping を使うことはできません。Solidity に Null の概念はないのです。
デフォルトバリューの例は以下です。詳しくはドキュメントを参照してください。
type | default value |
---|---|
uint | 0 |
string | “” |
address | 0x0 |
bytes | 0x |
Mapping にキーはストアされない
Solidity の Mapping は Hash化されます。そのため、検索時には下記のような順で到達します。
- 0x987654...987 のアドレスを登録
- ハッシュ化される
- n個目に格納され、index登録
- Lookup! [true, false,
true
, false, ... ,false] - return
true
なので、value から key を探し出す。というようなことができないので注意が必要です。
変数の格納場所
Storage と Memoryには違いがあります。
書こうと思いましたが、ここはリンクの紹介にします。
- 【Solidity基礎】storageとmemory
- Solidityの変数の格納領域(storage, memory, stack)
- solidity の memory とか storageというkeywordが実際に何をしているのか
- Solidityのstorage,memoryキーワードとは何か?
Getter と Setter
ここもちょっとした落とし穴です。javascript とか、一般的な言語のままの感覚ですすめるとハマるかもしれません。
即時に返り値を提供する Setter 関数を作ることは不可能
これは Ethereum の作りを考えれば当たり前ですが、トランザクションの処理には時間がかかります。
そのため、getter メソッドは即時に値を返せても、setter メソッドは即時に値を返すことができません。
なので、下記はバッドプラクティス。数十秒の登録処理がはさまるので、即時に変えるような挙動を期待することはできません。
function registerAddress() public view returns {
//
// 登録処理
//
return 'You have successfully registered!';
}
Array のデフォルト Getter は配列の全コンテンツを返すわけではない
contract に public
で定義した変数は自動的に Getter を持つことになります。
たとえば下記のように定義したとします。
contract Lottery {
address public user;
address[] public users;
これで、users
と user
Getter が作られました。user
は user 一人の値を返し、 users
は user 一人の値を返します。え。
ここで、users で作られる Getter は配列のインデックスを引数に渡すと該当ユーザーを返す作りになっています。そのため、リストコンテンツ全体を返すには別に Getter を作る必要があります。
function getUsers() public view returns (address[]) {
return users;
}
これなら下のような値がかえってきます。
0xdD870fA1b7C4700F2BD7f44238821C26f7392148,0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C
Web3 バージョンの考慮
Ethereum の Web アプリケーションの場合は MetaMask を利用していることが多いとおもいますが。
その際に、MetaMask 側で持っている Web3 のバージョンと、アプリケーションが使いたい Web3 のバージョンが異なる可能性が高いので、それを考慮する必要があります。
そもそも、一般的にスクリプトだったらこんな感じで書きます。
例えば、INFURA を使うとすると以下になるのですが、実際のアプリを使う際には MetaMask に格納されているアカウント情報を使いたいとなると思います。
const HDWalletProvider = require('truffle-hdwallet-provider');
const Web3 = require('web3');
const { interface, bytecode } = require('./compiled');
const provider = new HDWalletProvider(
'<Your BIP39 phrase here>',
'https://rinkeby.infura.io/<yourainfuraapi>'
);
const web3 = new Web3(provider);
web3 のバージョンごとの違いについて
v0.1.xx ベーシックな機能のみ。jsとしてもプリミティブな機能がメイン
v0.2.xx Promise / async, await が追加されたり、ちょっとモダンに。2018年12月時点でのStable
v1.0.xx 今後のスタンダード。2018年12月時点ではまだBeta
詳しくはリリースノートをみてください。
https://github.com/ethereum/web3.js/releases
web3 のバージョンが異なると動かなくなるコードがありそうです。
プロバイダーとは
上記の Infura を使った場合、ざっくりと概要図を書くとこんな感じになります。
web3 の中に埋め込まれます。
この Provider に BIP39 の Account Mnemonic を提供すると、例えば、MetaMask が提供する web3 (小文字のほう) はそのアカウント情報を使って送信できるようになります。
MetaMask が提供する。
プロバイダのハイジャック
ハイジャックといっても、ただ単に適切な web3 を使いましょうということです。
プロバイダーにはアカウント情報、Pub/Private Key が入っていますが、例えば、React で作ったアプリが持っている web3、Provider でメタマスクと連携してもうまくいきません。
なので、単純に下記のようにして、適切なプロバイダーを指定します。
const web3 = new Web3(window.web3.currentProvider);
MetaMask がない環境における考慮
一方で、メタマスクが入っていない場合にも気を使う必要があります。ライブラリの共通化や、テスト目的で使いまわしたいものがある時などは下記のように表記します。
if (typeof window !== 'undefined' && typeof window.web3 !== 'undefined') {
// メタマスクがある
web3 = new Web3(window.web3.currentProvider);
} else {
// サーバー上でコードが実行される場合、もしくは MetaMask が入っていない場合。
const provider = new Web3.providers.HttpProvider(
'https://rinkeby.infura.io/<yourinfuraapi>'
);
web3 = new Web3(provider);
}
ランダム関数
ランダム関数は提供されていないので、下記のような情報をもとに生成する必要があります。
- 材料を用意
- current block difficulty
- address
- current time
- SHA3 にする
- big number から割り算したり。
上は簡単な実装の代表例ですが、色々あるみたいです。参考記事
スマートコントラクトでの擬似乱数生成について
- https://blockchain.gunosy.io/entry/prngs-in-smartcontract
Struct の注意点
Struct は構造体を扱います。下記のように定義します。
struct User {
string description;
uint value;
address userAddress;
}
一方で、使い方は2種類あります。
// 使い方 ①
User memory newUser = User({
description: 'hogehoge',
value: 100,
userAddress: 0x123456...123
});
// 使い方 ②
User memory newUser = User('hogehoge', 100, 0x123456...123);
使い方①がベターなプラクティスです。
もし定義が下記に変わった場合、使い方②の書き方だとエラーを起こします。
struct User {
uint value; //2番目に定義されていた
string description; //1番目に定義されていた
address userAddress;
}
まだ上記のエラーだとデプロイ前にエラーに気づくことができます。
しかし下記の場合どうでしょう。
struct User {
bool hoge;
bool foo;
bool bar;
}
もし、順番が入れ替わったとしても気づくことができません。そして運用の時に「なんか変だな」となるわけです。そのため、Struct を使う場合には使い方 ① で安全にイニシャライズしましょう。