Solidityでの時間表現の注意すべきポイントとそのテスト方法TOPSoliditySolidityでの時間表現の注意すべきポイントとそのテスト方法Solidityでの時間表現の注意すべきポイントとそのテスト方法2018/04/24Solidityでスマートコントラクトを記述する際に、何らかの時間依存関係を含むロジックを入れたい場合があります。「ICOである時間が経過したら応募をクローズする」といったロジックや「ゲームで次の相手と戦う場合は最低1時間待たなければならない」といったときに必要になります。そこで、当記事では、このような条件をスマートコントラクトに入れたい場合はどのような表現方法があり、それをテストするためにはどのようにすればいいのか見ていきます。スマートコントラクトで将来の特定の時間に関数を実行したり、定期的に処理を行うような方法について実装したい場合の解決策も紹介していきます。Solidityにおける時間表現Solidityにおいて呼び出した時点でのタイムスタンプを得る方法がいくつかあげられるので、まずはその表現方法について見ていきましょう。javasciprtであればDate.now()でタイムスタンプを得ることができます。ブロックチェーンネットワークでこのようなタイムスタンプを得ようとすると本来であればオラクルが必要になるはずです。タイムスタンプというデータはブロックチェーンのプロトコル内部に存在しないはずだからです。タイムスタンプに関してはマイナーがブロックチェーンに記録することになっているので、そのデータを参照することによって現在のタイムスタンプをスマートコントラクトは取得することができます。3つを使う方法が主にあげられます。0.4.23)block.timestamp:現在のブロックに記録されているタイムスタンプ(senconds)now:block.timestampのエイリアスblock.number:現在のブロック高なので、基本的には、スマートコントラクトで時間依存のあるロジックを組みたい場合はblock.timestampを使用すれば問題ありません。特殊な状況下においてはブロック生成間隔を定義した上で、block.numberを使用した場合が適していることもあります。block.timestampやnowを使った方がいいケースタイムスタンプの取得は基本的にはblock.timestampを使用します。このタイムスタンプは上述したようにそのブロックをマイニングしたマイナーが記録しています。悪意があるにしろないにしろマイナーによってこのタイムスタンプに誤差が生じてしまうことが考えられます。直近のブロックのタイムスタンプが19:00:00だったとき実際にマイニングしたのは19:00:20であったにも関わらず、19:00:01と記録されている可能性が少なからずあります。ちなみに、このブロックに記録するタイムスタンプには「直近のブロックのタイムスタンプより大きなタイムスタンプでなくてはならない」というルールがあるので、時間的に遡ってタイムスタンプを記録することはできません。Paperには記述されています。Paperの情報ではこのルールはなくなっている可能性が高いです。)このような性質を考えるとblock.timestampはクラウドセール(ICO)など中長期的に実行する場面で用いるのが適しています。いうと、数日以上のスパンで見た場合はマイナーによるブロックタイムスタンプの誤差はほとんど影響しなくなっているはずだからです。短期間の時間差を得たいようなケースではマイナーによるタイムスタンプの誤差が影響することも考えられるので下記のblock.numberを使用するといいでしょう。ちなみに、マイナーがこのタイムスタンプを不正操作したり予測できてしまうことが考えられるので、乱数のソースとして使うことは脆弱性の元になり、セキュリティ的に危険です。この乱数が金銭のやり取りを含むゲームに使われていた場合、マイナーがその乱数を予測し、資金を奪ってしまう不正行為が考えられます。block.numberを使った方がいいケースblock.numberはブロック高を表しており、これを利用することでタイムスタンプとして表現することも可能です。ブロックのタイムスタンプはマイナーによる悪意ある操作が少なからず可能であったのに対し、ブロック高はプロトコルで定められているのでマイナー等によって操作することはできません。ブロック生成間隔が15秒であればそのブロック高の差に15をかければ、その時間差を得ることができます。このblock.numberで問題になってくるのがイーサリアムにおけるブロック生成間隔は常に一定ではないということです。ブロック生成間隔はハッシュパワーの影響を受けることになったり、ディフィカルティボムによってブロック生成時間が大きく変化する可能性が考えられます。以下が全期間でのイーサリアムのブロック生成時間のグラフです。ディフィカルティ・ボムが発動している期間は大きく変動していることが確認できます。このような状況が予想される中長期的なスパンではblock.numberは適していません。ブロック生成時間を仮定することが難しいからです。以下が2018年のブロック生成時間のグラフになります。このようにブロック生成時間が安定している時期では、基本的には誤差の範囲となり、15秒弱に収まっていることが分かります。ゲームなど短期的なスパンでタイムスタンプを利用する可能性があるケースでは、マイナーによる誤差が起こりうるblock.timestampよりもblock.numberが適しています。ディフィカルティ・ボムなどの影響でブロックタイムが予測できないときは、block.numberは適していないことになります。時間表現の具体例それでは、代表的なライブラリやプロジェクトでタイムスタンプが実際にどのように使われているのか見ていきます。OpenZeppelinのTimedCrowdsale.solではクラウドセールの期間が過ぎているかどうかに以下のようにhasClosed()を定義しています。closingTime;}このhasClosed関数を呼び出したときのブロックに記録されているタイムスタンプがblock.timestampで得ることができます。コントラクト内に保存されているclosingTimeよりも大きければ、クラウドセールの期間が終了しているということになるのでtrueを返します。他にも、DayLimit.solではその日のインデックスをtoday()関数で返しています。days;}また、block.numberを時間ロジックに使っている例としてはcryptokittiesのソースコードで見られます。し、ブロック生成時間が15秒であると仮定してblock.numberを使用し、クールダウンが終了するブロック高をセットしています。1;}}また、時間依存ではありませんがMiniMeToken.solではトークンが送金されたときにblock.numberを記録することで、ブロック高ごとのトークン履歴を参照することができるようになっています。これにより、任意のブロック高での残高参照やクローン生成が可能になっています。MiniMeTokenの機能は投票やDAOシステムなどと相性が良いのでAragonのappsでもこのMiniMeTokenが利用されています。時間依存ロジックのテストそれでは、block.timestampなどで時間ロジックを定義したスマートコントラクトをテストするためにはどうすればいいのでしょうか。実際にその時間が経過するまで待っていてはテスト処理を行うのに多くの時間を要してしまいます。そのため、truffleのGanacheテスト環境ではevm_increaseTimeというメソッドをブロックチェーンに送ることで任意の時間を進めることができます。(これはEVMの標準操作ではありません。活用し、時間依存のテストを実行してみます。ユーザーはサービスに加入するためにコントラクトに資金をデポジットできる。1週間以内であればユーザーは返金してもらえる。オーナーはデポジット機能を停止できる。デポジット停止後、直近のデポジットから1週間後以上経過していれば、オーナーはコントラクトから資金を引き出せる。このRefundコントラクトを以下のように作成します。0;}}以上のように、block.timestampを活用することでユーザーのrefundとオーナーのwithdrawを制限しています。Units)を表現したい場合は以下を利用することが可能です。daysそして、このコントラクトのテスト時に時間を進めるためのヘルパー関数increaseTime()を以下のように定義します。reject(err)resolve()})})}evm_increaseTimeをメソッドに指定している部分がポイントです。以下のように時間依存のテストを実行することができます。ここでは1.1週間経過している場合にユーザーは返金処理できないことをテストしています。}).should.be.rejectedWith('revert');})また、ここではデポジット機能を停止してから1.1週間以上経過していればオーナーがウォレットに資金を移動できることをテストしています。web3.eth.getBalance(wallet);post.minus(pre).should.be.bignumber.equal(value);})ここでのコントラクトとテストコードはこちらにまとめています。Clockによるスケジューラーblock.timestampやblock.numberを利用することで、時間依存のあるロジックをスマートコントラクトで記述することができ、evm_increaseTimeを利用することで時間依存のあるコードもすぐにテストをすることができました。Netflixのように月額課金サービスをスマートコントラクトを使って実装したい場合はどうすればいいのでしょうか。月額課金についてのロジックを記述したスマートコントラクトをイーサリアムネットワークにデプロイしておき、毎月自動的に処理が実行されるのが理想的です。イーサリアムの仕様上このようなスケジューラー処理は行うことができません。イーサリアムネットワークにデプロイされたスマートコントラクトにおいて、時間指定をして特定の関数を実行したり、定期的に処理を繰り返し行なったりすることはできないのです。コントラクトのデプロイ後、特定の時間に特定の関数を実行することは、現状できないということになります。そのような処理はその処理を行うときに秘密鍵によって署名されたトランザクションの送信が必要であり、特定の時間に送信元となるノードの確保ができないからです。Clock(EAC)が現在choronoLogicのチームよって開発されています。(現在、各種テストネットのみで利用可能です)EACを使うことで、将来の特定の時間にトランザクションを送信することが可能になります。EACの仕組みスマートコントラクトでスケジューリングされた処理が実行できないそもそもの問題は、特定の時間にそれを実行するためのノードが存在しないことでした。Nodeというノードを用意しておき、この特別なノードがそのスケジューラーを実行することで成り立っています。Nodeがその記述内容にしたがって実行してくれます。Nodeに対して報酬金を支払う必要があるからです。Nodeは運営されるのでより高い報酬金が優先して処理されることになります。指定すべきパラメーターは以下のようになります。windowを設定しておき、その時間枠内で処理が実行されるように設定しておきます。Nodeの負担は大きくなるのでその分多くの報酬金が必要になると考えられます。このEACを活用することで、時間設定をしてトランザクションを送信したり、定期的なペイメントなどが可能になるとされています。EACはメインネットで利用することができないので注意してください。