画像以外へCNN(畳み込みニューラルネット)を利用した研究を調べてみた
麻雀AIでCNN(Convolutional Neural Network: 畳み込みニューラルネットワーク)の構成を考えるにあたって、そもそも画像以外のCNNってどんなのあるんだっけ?と思い、簡単に調べた。
ゲーム関連
盤面を使ったゲーム(囲碁・将棋・チェスなど)
盤面が9x9や19x19などの二次元座標で表現できて、CNNと親和性が非常に高い。
DeepMindが開発したAlphaGO*1, AlphaZero*2が特に有名。
囲碁であれば、黒石・白石の有無(one-hot)、数手前の状況等をそれぞれ19x19の特徴ベクトルで表現し、複数チャンネルあるものとして、CNNを行う。CNNとしてはResNetを利用。
将棋・チェスの場合、自分・相手のコマの種類ごとに一つのチャンネルを用意する。
位置の厳密性のため、プーリング層を設けていないのも特徴。
また、「手番が自分と相手どちらか?」「(将棋で)どの持ち駒を持っているか?」といった2値(True/False)情報は、盤面全部を0か1のチャンネルとして表現しているのも面白い。そういった情報は、畳み込みせずに特徴量ベクトルとして合成してもいい気がするが、構成をシンプルにしたいからなのか、計算速度の影響か、このほうが精度がいいからなのかよく分からない。
盤面を使ったゲームとして、正方形型の盤面以外にも、Hex*3と呼ばれるひし形をした盤面のゲームでもCNNを使った研究*4があった。 いわゆる3x3といった正方形型とは違うが、隣接するマス目を畳み込みフィルタで畳み込んでいるみたい。
トランプ(ポーカー)
ポーカーは不完全情報ゲームとして世界中で特にポピュラーなゲームのため、AI研究もよくされている。
トランプは、4(ハート等の色) x 13 (A~K)の二次元情報に落とし込むことが可能であり、それを活用している。*5
特にポーカーでは、2ペアのような数字と、フラッシュのような色を使うもの両方があり、二次元ベクトルを畳み込むことでうまく特徴量抽出できるらしい。
ただし、方法の一つとしてCNNが提案されているだけで、他のポーカーAIで全結合のネットワーク構造を採用しているものも多く、CNNの効果や効率的なアーキテクチャ等はまだまだ分からない印象。
麻雀
麻雀でも昨年のゲームプログラミングワークショップで論文が出ている。 *6
ちょうど私も麻雀AIで昨年にCNNを少し試したことがあって、最初は先に越された思いや、やっぱり有効だったといった思いが混ざった複雑な気持ちになった。
ただ、いろんなアーキテクチャを試していて、非常に参考になっていて、助かっているのは間違いない。
麻雀の牌の種類(34種)と枚数(0~4)を二次元ベクトルとして畳み込みをしている。
いくつかの方法を試しているが、特に精度が高かったのが、マンズ・ピンズ・ソウズそれぞれを別チャンネルとして9(1~9の牌)x5(0~4枚)のベクトルで畳み込む方法。
字牌は前後の牌との関係がほぼないので、全結合の層で結合している。
また、マンズ・ピンズ・ソウズには対称性がある(緑一色を除く)のでチャンネル間のフィルターの重みを共有しているといった工夫もしている。
畳み込みのフィルターサイズは5x9の大きいものを含めたほうが精度がいいらしい。
畳み込み層とプーリング層一つずつであり、あまりDeepではやっていない。
牌譜を使った教師あり学習で全結合よりも牌譜との一致率は少し高くなっていた。
自然言語処理
かなり研究が盛んな分野であるため、論文も多数ある。まだまだこれから新しい発表がありそう。
こちらにまとまっているのが分かりやすかった。
基本的には言語情報をWord2Vecなどのベクトル情報にして、1次元の畳み込みのフィルターを適用している例が多い印象。
あとは、単語の前後情報を抽出するためにRNN(Recurrent Neural Network)と組み合わせる例も多い。
言語情報をベクトルにする部分のほうが重要だったりもするが、自然言語処理系は深すぎて追いきれていない。
時系列データ
時系列の信号データをスカラー値と時系列情報でグラフのような形にして、そこにCNNを適用させる例も多くある。 *7 *8 *9
例えば、モーションセンサーからの行動認識とかであれば、センサーから取れる複数の情報(センサーの場所、加速度・位置など)をチャンネルとして扱う。
あまり読めてないが、時系列といえばLSTMなどのRNN系も効果的であり、RNNとの組み合わせの例も多そう。
見つけた例としては、モーションセンサーからの行動認識、Wi-Fi信号を使った室内空間の認識、株価の予測など。
個人的には、投資系と相性がいいように思えるし、いずれやってみたい。
全体を通して
恐らく他にもたくさんありそうだが、探しきれないので、いったんこんなところで。
CNN自体が人間の視覚を参考にしているため、基本は画像系が多いが、他への応用もかなりあった。
ただ、それぞれの分野で何人も研究しているわけではないので、本当にそのアーキテクチャが適切なのかがよく分からない。(画像とかだと競争も進みより制度の高いアーキテクチャが発見されるが)
特に、囲碁・将棋の手番や麻雀の字牌といった情報の特徴量化の方法や、畳込みフィルターのサイズ、プーリング層の必要有無、層の深さなどが研究によって大きく違い、また設定した根拠があまり書かれてないケースも多く、ノウハウ次第みたいなとこが気になった。
誰かノウハウを伝授してください。
目的としていた麻雀のCNN適用については、先行研究を参考にしながら改めて検証していく予定。
*1:https://deepmind.com/research/alphago/
*2:[1712.01815] Mastering Chess and Shogi by Self-Play with a General Reinforcement Learning Algorithm
*4:Reinforcement Learning for Creating Evaluation Function Using Convolutional Neural Network in Hex - Semantic Scholar
*5:[1509.06731] Poker-CNN: A Pattern Learning Strategy for Making Draws and Bets in Poker Games
*7:Deep Convolutional Neural Networks On Multichannel Time Series For Human Activity Recognition
*8:A Deep Learning Approach to on-Node Sensor Data Analytics for Mobile or Wearable Devices
*9:Detecting State Changes of Indoor Everyday Objects using Wi-Fi Channel State Information
深層強化学習を用いた一人麻雀AIを作ってみた
最近はブロックチェーンまわりばかりだったが、 前々からやっていた麻雀AIの強化学習について一旦まとめておく。
麻雀好きの人から機械学習興味ある人まで読めるよう書いたみたけど、どうでもいいとこは飛ばしながら、分からないとこあれば気軽にコメントください。
概要
- 一人麻雀のAIを深層強化学習で作ってみた
- 自分が持っている牌すら選択できない状況から、ある程度有望そうな牌を選べるところまで学習した
- 最終的には、18順の一人麻雀でテンパイ率60%弱までになった
- ただ、他の手法に比べるとかなり弱く、まだまだこれからといった感じ
背景
麻雀AIでは、「どの牌を切るか?」「鳴くべきか?」「リーチすべきか?」など多くの判断が必要だが、 その中で基本となるのは「どの牌を切るか?」といった部分であり、点棒状況や相手の手を考慮しないシンプルなものを一人麻雀という。
※ここでいう一人麻雀は副露しないものとして以降扱います。
AIというと最近流行りの深層学習(ディープラーニング)のイメージがあるが、 麻雀AIでは研究はされているが、実用的にはあまり使われてない。
理由としては、モンテカルロ法とかで十分強いのと、そもそも教師あり学習自体、教師となる人間を真似するだけで超えられないといったものがある。
(有名な麻雀AI 爆打はモンテカルロ法と教師あり学習(単純パーセプトロン)を使っているはず。)
後者を解決する方法として、強化学習というものがある。
これは真似すべき正解はなく、AIが自ら試行錯誤して学んでいくという方法である。
そしてこれに深層学習を応用したものを深層強化学習という。
AlphaGOでも使われていたり、マリオカートなんかも学習している例もある。
強化学習だと、AIが自分で学んでいくので、人間を超えることも可能になる。
というワクワクする方法なので、深層強化学習の麻雀AIを作ってみた。
手法
深層強化学習の中で、A3Cという手法を用いた。
詳しくは説明しないが、2016年頃に出たもので、強化学習の中ではメジャーな手法。
Actor-Criticという状態価値と方策を同時に学習するところが、AlphaGoZeroとも共通であり、麻雀にも有効と何となく考えたため採用した。
ネットワーク
状態価値関数と方策関数で前半は共通のネットワークを用いた。
全て全結合のネットワーク。
畳み込みニューラルネットも試していたが、学習に時間がかかるのと、強化学習では全結合することが多いため、この形とした。
ざっくりとした図は下の通り。
特徴量の数は180、共通部分の中間層のユニット数は200、状態価値と方策のユニット数は100、方策の出力数は34(牌の種類数)。
特徴量
シンプルに34種類の牌それぞれが0~4枚のうち何枚手牌に含まれているか?を34x5=170。
とする予定だったが、ここ少し間違えた。
字牌も9種類分用意していて、36x5=180の特徴量としてしまった。
(常に0の特徴量が20次元ある)
あとは手動の特徴量(メンツがいくつあるか等)も用意してもよかったが、深層学習でその辺は自動で抽出して欲しかったので入れてない。
それで学習してくれた方が面白いし!
報酬の設計
ここ少し工夫をした。
そもそも報酬というのは、強化学習で試行錯誤していく中で何を成功体験、失敗体験とするかを決めるもの。
ゲームでいうと、相手に勝ったときとか、ステージをクリアした時にプラスの報酬を設定し、ミスをしたときにマイナスの報酬(罰)を設定する。
麻雀でシンプルに考えると、 和了したときにプラスの報酬を与えるべきだが、これだけでは上手くいかない。
強化学習では最初はほぼランダムに行動するが、34種類の牌をランダムに選んでも、天和でもない限り、ほぼ和了にたどり着くことがないためである。
そうすると、いつまで経っても学習が進まない。
例えるなら、歩くこともできない子供に野球でホームランを打ってと課題を与えるような感じ。
そこで、今回はステージを段階的に分けて、学習させる。
野球の例でいうと、まずは歩き方をおぼえる、次にバットの握り方、振り方、ボールの当て方、遠くへの飛ばし方と順に教えるようなイメージ。
今回は、以下のステージに分けて学習を行った。
- ステージ0: 3巡目まで持っている牌を選択できたら報酬
- ステージ1: 7巡目まで持っている牌を選択できたら報酬
- ステージ2: 15巡目まで持っている牌を選択できたら報酬
- ステージ3: 流局まで持っている牌を選択できたら報酬
- ステージ4: イーシャンテンまで進めたら報酬
- ステージ5: テンパイまで進めたら報酬
- ステージ6: 和了できたら報酬
マイナスの報酬は共通して、持っていない牌を選択したときとした。
各ステージで報酬をもらえる成功率が一定の割合を超えたら次のステージに進むようにした。
本来であれば、和了だけでなく、和了時の点数まで報酬に含めるところまですべきだが、 簡略化のためそこまではしていない。
アカデミックな観点でいうと、報酬の段階を手動で設計するのは、あまり評価されない気がするが、他にいい方法が思いつかなかった。 もしいい方法を知っている方がいたら教えてください。
ソース
強化学習フレームワークとしてChainerrlを利用。
強化学習アルゴリズムの実装とか結構めんどくさいので、かなり便利。
ソースはこちら
学習結果
結論
ステージ5(テンパイしたら報酬)で、60%くらいまでいったところで学習が収束してきて終了した。
学習過程
ステージ4まで
横軸が試行回数、縦軸が成功率(報酬をもらえる割合)になっている。
ステージ0~3の成功とは、規定の巡目まで持っている牌を選択し続けたこと、ステージ4はイーシャンテンになったことを指す。
各ステージで右肩上がりになっていて、ステージが上がるとまた成功率が下がり、そこから右肩上がり、、となっているのがわかる。
ステージ4のクリア成功率を0.7としていたので、約100万回でステージ4までクリアした。
ステージ5
成功率、すなわちテンパイ率が最初は上がっているが、徐々に収束しているのがわかる。
試行回数について、ステージ4までよりもかなり大きく最終1000万回まで行った。
これ以上、上がりそうになく、コンピュータの電気代も馬鹿にならないのでここで終了した。
実際にどう打つか
学習したモデルで実際にどういった選択をするかを見てみた。
天鳳の鳳凰卓で打っている自分の批評も踏まえながらw
というか、本当にテンパイ率60%なのか疑問なくらい残念な打牌です。
適当なルールベースのほうが全然マシです。
画像作るの大変なのでシミュレーションで打った手を表示します。
2m7m8m3p4p7p8p8p1s南北発中中
dahai:北
南か北切っておけばいいので、これは問題なし
2m7m8m3p4p7p8p8p1s6s南発中中
dahai:6s
???まずは字牌切ってほしいところ。6sならせめて1sでしょ。。
2m2m7m8m3p4p7p8p8p1s南発中中
dahai:発
南のほうがといいたいところだが、点数考慮してないので南でも発でも問題なし。
2m2m7m8m3p4p7p8p8p1s南発中中
dahai:発
裏目ったらしい。どんまい。
2m2m5m7m8m3p4p7p8p8p1s南中中
dahai:2m
これはちょっとないかな。。1sか南切りでしょ。
門前のみってこと考えると、3対子は微妙なので対子落としは悪くないけど。
2m5m5m7m8m3p4p7p8p8p1s南中中
dahai:2m
すでに1枚落としてるし、まぁいいでしょう。
5m5m7m8m3p3p4p7p8p8p1s南中中
dahai:1s
南のほうがいいけど、1sは悪くない
5m5m7m8m3p3p4p7p8p8p9s南中中
dahai:中
早く南を切ってくれ。。南以外であれば中は悪くないけど。
5m5m7m8m3p3p4p7p8p8p3s9s南中
dahai:3s
3sはないですね。。9sか南か中のどれかにしてほしい。
5m5m7m8m3p3p4p4p7p8p8p9s南中
dahai:中
ようやく中切ったか。あとは南。
5m5m7m8m3p3p4p4p7p7p8p8p9s南
dahai:南
待ってました南切り。これで手牌がすっきりした。
5m5m7m8m9m3p3p4p4p7p7p8p8p9s
dahai:3p
9sを切ってください。9sが9mだったら3pはかなりあり。
5m5m7m8m9m3p4p4p7p7p8p8p4s9s
dahai:4s
4sは切ってくれるのね。あとは9s切ってください。。
5m5m7m8m9m3p4p4p7p7p8p8p9s9s dahai:9s なぜ9s重なったところで9sを切るのか。。。
5m5m7m8m9m3p4p4p6p7p7p8p8p9s dahai:3p この3pはないですね。。9s切らないならせめて4p
5m5m7m8m9m4p4p6p7p7p8p8p9s発
dahai:発
これはOK
5m5m7m8m9m4p4p6p7p7p8p8p9s白
dahai:白
これもOK
5m5m7m8m9m4p4p6p7p7p8p8p7s9s
dahai:7s
なぜ9sだけ切らないのか。。。
考察
いくつか今回うまく行かなった原因や改善ポイントについてまとめます。
特徴量設計がうまくいっていない
特徴量で不要な次元数を与えてしまったというミスも含めて、ここを改善すればもう少しましになると思います。
まだメンツやシュンツなどの特徴量を入れなくても、せめて山にいる可能性がある枚数を入れるとかすればましになると思います。
強化学習単体では限度がある
強化学習だけで強いAIを作るのは難しい可能性があります。
AlphaGOでも強化学習結果をそのまま使うのではなく、その結果をモンテカルロ木探索の中で使うといったやり方をしています。
なので、麻雀でも強化学習の結果をモンテカルロに活かすようなことをしたほうがいいかもしれないです。
強化学習部分の改善
強化学習における報酬の設定の仕方、利用する手法など改善の余地はあります。
ゲームによって向き・不向きもあるみたいなので、もう少し強化学習を調査して改善したいと思います。
深層学習部分の改善
今回の深層学習部分のネットワーク構造はかなり適当に全結合をしたものです。
畳み込み層を入れるなどのネットワーク構造の改善やその他の学習改善手法を取り入れることで精度が上る可能性があります。
プログラムにバグが有る可能性
上の例で9sを全然切らなかったことを考えると、報酬を決めるときのシャンテン数計算とか、特徴量作る部分とか我ながら疑いたくなります。
改めてバグがないか見直す予定です。
まとめ
今回は深層強化学習を使った麻雀AIを作ってみました。
まだまだ弱いけれど、教師データもなく、持っている牌すら選択できない状態から考えると、強化学習の可能性は示せたかなと思います。
今後もっと実用的になるように改善していく予定です。
【Ethereum】ブロックチェーンのじゃんけんdAppゲームを作ってみた
ブロックチェーンを使ったジャンケンゲーム(デモ)のdAppを作りました。
ゲーム自体はジャンケンなので大して面白くないですが、
ジャンケンというゲームがブロックチェーンの特性理解およびdappsの勉強になるいい題材と思ったため
ジャンケンのゲームを作りました。
ゲームの紹介
ゲームのページ
※PC用サイトです。
※ジャンプする前にMetaMaskでログインしてください。
※ゲームの具体的な手順はリンク先の下の方に記載されています。
ゲームのポイント
ブロックチェーンのパブリックな特性ゆえに、処理フローを工夫しています。
これをしないと後出しジャンケンが可能になってしまうためです。
詳しくは↓の「ブロックチェーンならではのゲームの特徴」に書いています。
作ったゲームの概要
- 2人対戦用のジャンケンゲーム
- HostPlayerが先にゲームを作り、GuestPlayerがそのゲームに参加する
- HostPlayerが賭け金(Ether)を設定でき、敗者から勝者に賭け金が渡される
ゲームの前提
- HostとGuestを同じユーザがプレイするデモゲーム
- RopstenNet(イーサリアムのテストネット)上に存在
- MetaMaskのインストールが必要
- PCのみ利用可能
- MetaMaskにRopstenのEtherを持ったアカウントが2つ必要
MetaMaskのインストール及びRopstenのEtherの貰い方は下記サイトを参考にすればと思います。
ブロックチェーンならではのゲームの特徴
課題:シンプルにやると後出しジャンケンが可能になってしまう
dappsでは一般的にユーザの操作内容やデータをブロックチェーン上に記録します。
ジャンケンの場合、ユーザが出した手(グー、チョキ、パー)はEthereumのブロックチェーン上に記録されます。
その結果、先に出したプレーヤー(ここでいうHost)の手をGuestがブロックチェーンから見た上で勝てる手を出すという、後出しジャンケンが可能になってしまいます。
解決策:ランダム文字列と組み合わせたハッシュ化を活用
後出しジャンケンをさせないために、ランダムな文字列とハッシュ化を使っています。
ハッシュ化とは、元のデータからハッシュ値と呼ばれる規則性のない値を計算することで、 ハッシュ値から元のデータを逆算するのは、ほぼ不可能と言われています。
Hostは、ジャンケンの手とランダム文字列を最初のMakeGameで提出しますが、 それらを結合して事前にハッシュ化したデータのみをブロックチェーン上に記録します。
こうすることで、GuestPlayerはHostが何の手を出したのか分からず、後出しができなくなります。
そしてGuestPlayerが手を提出した後に、Hostハッシュ化する前の手とランダム文字列を提出し、 それをEVM側でハッシュ化し、最初のハッシュ値と同じかどうかをチェックし、改竄していないことを確認します。
実装に関して
以下は開発する人向けの細かい話です。
利用したライブラリ、ツール等
- Web3js:フロントエンドにほぼ必須ですね。
- Truffle:テストやデバッグがかなり楽になりますね。
- Remix:デバッグに使いました。EVM上での処理の流れとかも見れるの便利すぎるとようやく気付きました。
- MetaMask:PCでの利用として現時点では必須ですね。今後はOperaのようにスマホブラウザでもアクセスできる流れでしょう。
その他実装した機能
賭け金のデポジットおよび移動
賭けれないと面白くないので、HostとGuestが同じ額だけBetして、敗者から勝者に賭け金が移動するようにしました。
よくある方法でゲームに参加する時の関数をpayableにしていて、EtherをBetできるようにしておき、
コントラクトがお金を一時的に預かり、決着が決まったら勝者もしくは引き分けの者に送金するようにしています。
コントラクトのデプロイ
デフォルトで指定しているコントラクトアドレスに複数人が同時アクセスすると、ゲームのStateがぐちゃぐちゃになるので、
各ユーザが自分用のコントラクトをデプロイできるようにしています。
Web3.jsでデプロイ済みのコントラクトオブジェクトと、デプロイ時のbytecode、gasを指定すれば下記のようにデプロイできます。
本当はゲーム管理用のコントラクトと組み合わせた方が良さそうですが。
イベントのウォッチ、トランザクション状況の表示
ユーザの操作によりトランザクション発行するだけでは、ブロックチェーン上で正しく受理されたかどうかは分かりません。
そこで、Solidity側でイベントを定義して、それをフロントエンド(Web3js)側でウォッチしておき、その状況をユーザに伝えることが望ましいです。
今回のジャンケンでは、下記のような対応をしました。
ハッシュ値の確認
上記の通り、ハッシュ値を確認することで手の改ざんなどがないかをチェックしています。
具体的には、「ローカルでハッシュ化してコントラクトに保存されたもの」と「コントラクトに送られた手とランダム文字列からハッシュ化したもの」について
比較しています。
それぞれのハッシュ値を確認できるような機能をページに用意しました。
残っている課題・改善点
別技術との組み合わせによる改善
今回は純粋にパブリックチェーンを使った場合のジャンケンとして実装しましたが、 Hostが一度ハッシュを提出した上で、再度手を提出するとか、ただのジャンケンにしては面倒すぎます。
まだ勉強不足ですが、「zk-snarks」や「Enigma」といった暗号化関連技術や、「state-channel」を使うなどして、
この面倒なフローを改善できるかもしれません。
それでこそブロックチェーンの可能性も広がると思うので、この辺りを追求していきたいです。
適切なゲームサイクル等の管理
デモプレイ用に作っているため、細かい条件チェックなどは実装していません。
例えば、Hostがゲームを作った後に、Guestが来なかったりHostが再度手の提出をしなかった場合、
最初にベットしたEtherはコントラクトに残ります。
本来であれば、一定時間経ったら資金を返す、再提出しなければペナルティとして没収するなどを実装すべきだと思います。
コントラクトの設計
今回は一つのコントラクトに全てのFunctionやState変数をまとめて実装していますが、 本格的にやるならば、ゲーム毎や、複数ゲームを管理するコントラクト等複数に分けた方が良いかと思います。
セキュリティ対策強化
Consensysのセキュリティプラクティスは読みましたが、
このコントラクトでそれを網羅して、しっかりと試験をしたわけではありません。
メインネットにデプロイするなら、ここはしっかりやりたいですね。
OpenZeppelinの導入もしたいところです。
Gas節約のための最適化
Solidityの書き方によってユーザがゲームするときの手数料GASが変わってきます。
無駄のないコードを書くことでこのGASの節約ができますが、今回はそこまで考えていません。
別ユーザ同士の対戦
別にページを二つに分ければ出来るのですが、
誰かがサイトにアクセスしてくれても、同時にサイトにアクセスしているユーザがいないと遊べないので、
やめました。
あくまでデモであり、過疎ったオンラインゲームを作りたいわけではないので。。
フロントエンド側の改良
Javascript初心者過ぎてフルスクラッチで書きましたが、
よほど腕に自信ない限り、React+Reduxといったフレームワークを活用したほうが分かりやすく質の良いソースになるはず。
Web3+React+Reduxといった組み合わせが主流になりそうな感じもあるので、勉強がてら変えていきたいですね。。
あとデザインも残念な感じですが、そこメインでないので目をつぶる予定です。
(メインネットにデプロイする場合)法律調査など
今回のdAppsはEtherを賭けるゲームです。
メインネットにデプロイすると「賭博罪」や「資金決済法における仮想通貨交換業としての規制」に触れる可能性あります。
メインネットにあげるならそのあたりの調査をした上でのデプロイになります。
(何となくダメな気がしますが。。)
作ってみての感想
作ってみて初めてわかることが沢山
どんなことでもそうですが。。。
Ethereumの基本知識を本とかサイトで勉強して、CryptoZombiesでもやれば、
簡単なdAppsであれば作ることはできるとは思います。
ただ実際作ってみると、MetaMaskを通じてのトランザクションやら、どういうView Functionを用意すべきか、変数の型は何が適切か・・・
とか悩むことはいっぱいあって、細かいところで沢山つまづきました。
でもそうして、作ってみて初めて悩むことやわかることによる学びが沢山あります。
ということで作ってよかったです。
サーバサイドが不要だと個人開発もやりやすい
サーバサイドの開発も必要だと専用のサーバを用意したりとか、サーバ構成、DB設計などやらないといけないことが増えていきます。
デプロイ後の保守運用も大変。。
でもSolidityで記述したEthereum上のコントラクトがサーバのような役目を果たしてくれて、
基本シンプルな内容を書くだけなので、非常に楽です。
個人開発のハードルを下げてくれます。
※もちろんオフチェーン部分をサーバ使ってやるタイプのdAppもあると思うので一概には言えないですが。
非中央集権でやる意味を改めて考えさせられる
上にも書いた通り、パブリックチェーンで完結させたジャンケンは正直ジャンケンにしては面倒です。
手順の複雑さを踏まえても、本当にブロックチェーンで非中央集権的にやる必要があるのでしょうか。
ジャンケンくらいであれば正直、どこかのゲーム会社のサーバに置く中央集権的な方法でも良い気もします。
ただこれがもっと大規模なもの(eスポーツの国際大会とかオリンピックとか?)であれば、サーバ保有者への信用問題が発生しうるので
非中央集権的に実現できるブロックチェーンは魅力的だったりもします。
どういう場面で、オンチェーン上の非中央集権とすべきで、どこは中央集権化すべきなのか改めて考え続けていき 世の中の流れもつかんでいく必要があると思いました。
ソースコード
もっといい方法があるといったご意見があれば、ぜひお待ちしております。
【Ethereum】Gethで起動したプライベートネットをMetaMaskと連携
開発時にGethから立てたプライベートネットでのテストでもRopstenのようなテストネットやメインネットを想定して、
MetaMaskからアクセスできるようにしておいた方がいいと今更ながら思ったので、その対応のメモ。
特に難しい話ではないけど。
Gethのインストールやプライベートネット、MetaMaskのインストールの方法は割愛。
1. Gethの起動。
まずはGethの起動。このとき、MetaMaskからネットワークにアクセスできるようrpscorsdomain=*
などにしてアクセスできるようにしておくこと。
以下は一例。
geth --networkid [genesis.jsonで指定したnetworkID] --rpc --rpccorsdomain "*" --rpcport "8545" --rpcapi web3,eth,net,db,personal --datadir "プライベートネットのブロック格納先パス" --nodiscover --unlock 0,1 console 2>> ./geth.log
2. MetaMaskを起動。
MetaMaskをブラウザから起動。MetaMaskの左上の接続先からLocalHost 8545を選択。 この時点でネットワークには繋がるが、Geth側で作成したアカウントはMetaMaskには表示されない。
3. アカウントのインポート
右上の人+更新マークみたいなのをクリックし、「Import Account」を選択。
SELECT TYPEを「JSON File」にして、ファイルを選択。
(もし秘密鍵が分かるのであれば、Private Keyを選択してコピペでも良い)
ここで、Gethでプライベートネットを立ち上げたブロック格納先パス(datadirで指定するとこ)の中に「keystore」ディレクトリがあるので その中の該当アカウントのJSONファイルを選択。
これでアカウントがインポートされ、MetaMaskにも表示される。 アカウント同士の送金もMetaMaskから普通にできる。
【Ethereum】Solidityでの例外処理(require,revert)でエラーメッセージを出力する
日本語での情報があまりなかったので書いておく。
Solidity0.4.22より、require()
やrevert()
による例外処理でエラーメッセージが出力できるようになった。
記述方法
require(条件, メッセージ); revert(メッセージ);
具体例(require)
require(msg.value % 2 == 0, "Even value required.");
具体例(revert)
if (msg.value % 2 != 0){ revert("Even value required."); }
デバッグが捗って嬉しい!
例えば、以下のsetNumber()のようなやたら制限したがる関数があったとする。
この関数を呼び出すトランザクションを発行してエラーになったとき、どこでエラーになったのかパッと見て分からない。
そこで、setNumber2()のように書いてあると、エラーが出力されエラーの原因がすぐにわかる!
pragma solidity ^0.4.22; contract test{ address owner; uint number; constructor () public{ owner = msg.sender; } //エラーの理由がわからない function setNumber(uint _number) public { require(msg.sender == owner); require(msg.sender.balance > 100000); require(_number > 0); require(_number%10 == 0); number = _number; } //エラーの理由がわかる!!! function setNumber2(uint _number) public { require(msg.sender == owner, "only owner"); require(msg.sender.balance > 100000, "balance: more than 100000 wei required"); require(_number > 0, "_number: more than 0 required"); require(_number%10 == 0, "_number: cannot be devisible by 10"); number = _number; } }
ちなみに、assert()
についてはメッセージ出力は現時点では対応していないっぽい。
以下、参考。
CryptoZombiesを一通りやってみて。。。
EthereumでのdApp開発の学習ツールとして有名な「CryptoZombies」を一通りやってみました。
(今出ているLesson6まで)
「CryptoZombies」はEthereum上に独自ネットワークを開発しているLoom Network社が提供しています。
「CryptoZombies」という名のdAppのゲームをチュートリアルと共に一から作っていきます。
キャプチャの通り、右側にエディターがあり、左側にコードの書き方やノウハウ、課題が書かれています。
課題の通り、コーディングをすると次のステップに進めます。
キャプチャは英語ですが、日本語も可能です。
(個人的には分かりやすい英語であり、今後dAppの勉強するなら英語はほぼ必須なので英語推奨です)
今後、章が増えていくと思いますが、1〜5章まではSolidityの書き方、6章はWeb3.jsでのフロントエンド部分の書き方を勉強できます。
また、各章の中でもステップ毎に、テーマが決まっており、コントラクトの宣言の仕方や修飾子の宣言の種類などを体系的に学んでいけます。
一通りやってみた上でエンジニアが勉強として使う上でのいいところや注意点などを書いておこうと思います。
いいとこ:スマートコントラクトによってアプリケーションがどう作られるのかを理解できる
Solidityやスマートコントラクトを調べても、だいたいは「トークンを作ってみよう」「コントラクト間でのメッセージのやり取りをしてみよう」といった基本的な内容が多く、「実際ゲームとかのアプリケーションはどう作るの?」というところが具体的にイメージできないことがよくあります。
(プログラミング初心者あるあるですが)
最終的に具体的なゲームとして動作するところまで進んでいくので、アプリケーションがどう出来ているかまで学習できるのがいいところだと思います。
いいとこ:体系的に学べるので、知らなかった部分の穴埋めをできる
まだEthereumの歴史も浅いので、あまり体系的にSolidityやdAppについて学べるものは少ないです。
結果、いろんなとこで調べながら学んでいくわけですが、部分的に理解に抜け漏れも生まれます。
すでにある程度勉強している人は知っているところを飛ばしながら、そういった抜け漏れがありそうなとこだけを勉強して埋めていくっていうのもありだと思います。
個人的には、GASの消費を抑える書き方についてはあまり勉強できておらず、そのことに気づき勉強になったかと思います。
注意点:課題のクリアを目的とせず、仕組みを理解する
各ステップ毎に出てくる課題をクリアすれば次に進めるわけですが、この課題自体は基本的に難しくありません。
具体的に「どこにどういったコードを書け」という指示があり、その通りやるだけなので。(コピペしろみたいなのもあります)
課題の手前に、書き方や注意点・ノウハウが書いてあるので、そこを必ず読むようにしましょう
!!
おすすめ:英語で勉強する
上にも書きましたが、日本語訳もありますが、英語でやることをおすすめします。
利用としては、
- 分かりやすい英語が使われており、苦手な人にも英語の勉強にもなる
- Ethereum関連の英単語や熟語に慣れることができる
といった感じです。
特に大事なのは2つ目です。
今後dAppやEthereumの勉強をしようとすると、各プロジェクトのGithubやWhitePaper、Ethereum.researchなど、ほぼ間違いなく英語のページが出てきます。
そんな時にEthereumやSolidityなどでよく出てくる単語や熟語があり、独特の意味合いで利用されたりします。
それをこのCryptoZombiesとかで勉強しておくと、分かりやすくなったりするのでオススメです。
すでにある程度勉強していたとはいえ、それでも勉強になりました。
学んだことを忘れないうちに、簡単なdAppを作ってみようと思います!
ちなみにここまでオススメしておきながら、LoomNetworkの関係者でもなければ、LOOMトークンも持っていないです(笑)
【Ethereum】スマートコントラクトのベストプラクティスについてまとめる②
前回分はこちら
pao2.hatenablog.com
続きを書きます。
既知の攻撃方法
Race Conditions
最も有名な攻撃の一つ。
DAO事件もこれ。
いくつかのパターンがあるのでそれを紹介する。
Reentrancy
メソッドの最初の呼び出しが終了する前に、fallback関数により、繰り返し呼び出されることで、何度も何度も残高から引き出されたりする。
対処法としては、
someAddress.call.value()()
ではなく、someAddress.send()
を使う- 内部の状態変数の更新を終えてから、外部アドレスを呼び出す
といった感じ。
前者は利用できるGASを制限、後者は繰り返し処理ができないように先に状態を変えておくといった対策。
Cross-function Race Conditions
1つのコントラクトに2つのメソッドがあり、1つのメソッドを実行したときに、fallback関数からもう一方のメソッドを実行する。
すると、一つ目のメソッドが実行途中で、状態更新が終わっていないタイミングにもう一方のメソッドが実行され、不整合が生じることがある。
外部コントラクトを呼び出す前に、内部の状態更新を行っておくのが良い。
これは、直接コード(Cross-function Race Conditionsの部分)を見た方がいいかもしれない。
Pitfalls in Race Condition Solutions
あるメソッドが直接外部コントラクトの呼び出しを行わなくても、他のメソッドを呼び出し、そのメソッドが外部コントラクトを呼び出していたら結局危ないよということ。
Transaction-Ordering Dependence (TOD) / Front Running
ブロック内のトランザクションの順番はマイナーにより意図的に操作でき、それによる攻撃の類。
Timestamp Dependence
ブロックのTimestampはマイナーにより操作可能であり、それを用いた攻撃。
例えば、乱数生成のシードにTimestampを使っていたとしたら、マイナーは乱数を調整できることになる。
Integer Overflow and Underflow
整数型のオーバーフローやアンダーフローにより、変数に予期せぬ値が入りそこを攻撃されることがある。
以前書いた、ERC20トークンでのBatchOverflowもこの攻撃。
DoS with (Unexpected) revert
常に失敗するfallback関数を用意することで、コントラクト自体が機能できなくなるような方法。
例えば、オークションの実装で、入札金額が更新されたとき前の最高金額者に金を戻す必要がある。
このとき前の最高金額者のコントラクトアドレスでfallback関数が失敗するようになっていたら、必ず更新が失敗し、誰も入札できなくなる。
こういったときは、pull型で入札者からお金を取りに来てもらうようにしたほうがいい。
DoS with Block Gas Limit
これは、GAS上限を狙った攻撃。
コントラクトに繰り返し処理などをさせることでGASぎれを狙う。
例えば、クラウドファンディングで寄付を募るコントラクトを作ったときに、寄付したアドレス一つずつに返金する処理をしたとする。
このとき悪意ある人が、少額を繰り返し違うアドレスで募金していたら、返金処理のときに繰り返し処理になりGASぎれが発生して途中で処理を失敗させられる。
Forcibly Sending Ether to a Contract
selfdestruct
を使うことで、fallback関数を呼び出すことなくコントラクトにEtherを送ることが可能。
これを利用してEtherをコントラクトに強制的に送金して、予期しない動作をさせることが可能になる。
ソフトウェアエンジニアテクニック
スマートコントラクトで開発する上でのテクニック。
Upgrading Broken Contracts
コントラクトに不具合が見つかったときに、別のコントラクトを最新のものとしてアップグレード可能にする方法。
非常に重要。
方法1:登録かつ最新バージョンを管理するコントラクトを用意する
管理者のみから登録できるようにし、歴代のコントラクトアドレスを保持しておく。
欠点としては以下がある。
- ユーザが最新のアドレスかどうかチェックしておく必要があり、古いバージョンにアクセスするユーザがいるリスクがある
- 古いコントラクトからデータをどう引き継ぐかを考える必要がある。
方法2:最新のコントラクトにデータを転送する
一つ目の方法の課題であるデータの引き継ぎを解消するために、delegatecall
を使う。
delegatecall
によってコントラクト間のデータの引き渡しが可能になる。
ただし、新しいコントラクトのデータの格納形式が違うと保存できないので注意。
このあたりは非常に重要なので別途勉強してまとめたい。
Circuit Breakers (Pause contract functionality)
コントラクトに不具合が見つかったときに、緊急停止できるようにする方法。
こちらも非常に重要。
コントラクト内の資金を停止と同時にどこか信頼できるアドレスに脱出させる。
サーキットブレーカー自体は、スマートコントラクト・ブロックチェーン に限らず使われているものである。
Speed Bumps (Delay contract actions)
処理を遅らせることで、コントラクトに不具合があって攻撃された場合でも被害を最小限に止めることができる。
例えば、資金の引き出しを10日後や1000ブロック後などとすることで、攻撃されていることに気づいて対処する時間を用意できる。
リアルタイム性が求められない処理であれば利用すればいいと思う。
Rate Limiting
処理できる量を制限する。
例えば、1日100ETHまでしか送金できない等の制限により、不正に大量の送金をすることを防ぐことができる。
Contract Rollout
メインネットでサービスリリースする前に、テストでコントラクトを動かすはずだが、その際の注意・したほうがいいこと。
- 100%でテストを行うこと
- まず自分のプライベートネットにてテストすること
- パブリックなテストネットにてテストとBug Bountyをすること
- テスト時には様々なプレイヤーがそのコントラクトにアクセスできること
- メインネットにも制限付きでベータ版をデプロイする
(特定のブロック番号を超えたらdestructする、1アカウントあたりの扱えるETHを制限するなど)
トークン実装におけるベストプラクティス
最新の標準トークンを用いる
ERC20
やERC721
などが有名ですが、実装する時点での最新の標準トークンを調べるのがよい
ERC20における「front running attacks」に気をつける
ERC20におけるapprove
とtransferFrom
メソッドにより「front running attack」という種類の攻撃が可能になる。
「front running attack」とは、上にも書いたが、トランザクションがブロックに書き込まれるまでの時間を利用した攻撃の種類である。
ここで発生しうる攻撃は以下のようなもの。
例えば、アリスがボブに10ETH分、アリスの口座から引き出すことを許可していて(approve
により)、途中でそれを5ETHに変えようとした場合、
5ETHに変えるapprove
のトランザクションがブロックに書き込まれる前にボブがそれに気づき、10ETH引き出すtransferFrom
を先に実行できるようにする。
すると、ボブは10ETHまず引き出すことが可能で、さらに5ETHに変えるapprove
のあとに5ETHも引き出すことが可能になる。
対策も含めて、こちらに記載されている。
0x0
のアドレスに送信するのを防ぐ
なんと誤送信だけで0のアドレス(0x000・・・・00
)に現在、 8000万ドルものETHが眠っているらしい。。。
そしてそのアドレスは、特に誰のものでもなく引き出せない。
また別ではあるが自身のコントラクトへの送金も含めて以下のようなmodifier修飾子で防ぐことができる
modifier validDestination( address to ) { require(to != address(0x0)); require(to != address(this) ); _; }
ドキュメント化について
以下の内容をドキュメント化して公開するべき。 - 詳細な仕組みやロールアウトに関する方針 - コンパイラのバージョンや、どのソースコードをデプロイしているかといった状態 - よくあるバグ・攻撃への対策・リスク状況 - 更新履歴 - 不具合が発生した時のアクションプラン - 問い合わせ先
セキュリティツールについて
セキュアなスマートコントラクトを書くために必要なツールの紹介。
コードに脆弱性が含まれていないかのチェックであったり、コードを綺麗にするためのリンターなど。
具体的なリンクが羅列されているので、以下を参照
- 日本語版
- 英語版
セキュリティ/脆弱性情報の発信・通知
どこからセキュリティや脆弱性などの情報が発信されてウォッチしておくべきか?といったこと。
具体的には、Ethereumの公式ブログやVitalikのTwitterなど
ここも詳しくは以下を参照。
- 日本語版
- 英語版
以上でベストプラクティスのまとめ終了。
長かった。。。。
まとめてみてわかったのは、
- 実際にコードを見ないと、どんな脆弱性があり対策をすべきなのか分からないものも多数ある
- ここに書いてあるのはあくまで書いた時点のものであり、これから気をつけるべきことはどんどん増えていく
- ブロックチェーン やスマートコントラクト特有の不具合や気をつけるべき点はかなり多い
といったこと。
GASの消費量とかも気にしてしまうけど、まずは脆弱性のない安全なコードを書けるようになることが、まず何よりも重要だと思った。