Develop with pleasure!

福岡でCloudとかBlockchainとか。

Rubyで書かれたテスティングフレームワークtethでコントラクトをテストする

Ethereum使ったアプリケーションをRubyで書けないかなーと思ってRubyのライブラリを調べてみた。

EthereumのRubyライブラリ

公式ドキュメントで紹介されているのは以下の3つ。

ruby-ethereum

Rubyで書かれたEVM(Ethereum Virtual Machine)の実装

github.com

ruby-serpent

EthereumのSerpent*1コンパイラへのRubyバインディングライブラリ

github.com

ethereum-ruby

EthereumのノードへのJSON-RPCをラップしたライブラリ

github.com

teth

またその他にtethというrubyで書かれたテスティングフレームワークがある。

Solidityのコントラクトのテストをgethもしくはruby EVMで実行することができるみたい。
ruby EVMは↑のruby-ethereum。

tethのセットアップ

コントラクトの記述もTDDでということで、minitestベースのテストが書けるtethを使ってみる。

libserpent.soのビルド

tethの依存ライブラリにserpentというgemがあり、libserpent.soが無いとそのgemのビルドに失敗するので、事前にserpentをビルドする。

$ git clone git@github.com:ethereum/serpent.git
..
$ cd serpent
$ make
...
$ sudo make install
cp serpent /usr/local/bin
cp libserpent.a /usr/local/lib
cp libserpent.so /usr/local/lib
rm -rf /usr/local/include/libserpent
mkdir -p /usr/local/include/libserpent
cp bignum.h util.h keccak-tiny-wrapper.h tokenize.h lllparser.h parser.h opcodes.h functions.h optimize.h rewriteutils.h preprocess.h rewriter.h compiler.h funcs.h /usr/local/include/libserpent

ruby-bitcoin-secp256k1のセットアップ

tethはruby-bitcoin-secp256k1にも依存しており、ruby-bitcoin-secp256k1はlibsecp256k1に依存している。libsecp256k1はBitcoin Coreが独自実装した楕円曲線暗号のCライブラリの最適化実装。

そのためruby-bitcoin-secp256k1をクローンしてlibsecp256k1をビルドする。

$ git clone git@github.com:cryptape/ruby-bitcoin-secp256k1.git
...
$ ruby-bitcoin-secp256k1 
$ git submodule update --init --recursive
...
$ ./install_lib.sh
...
途中でsudoのパスワード聞かれるので入力
...

実行が終わると/usr/local/lib/libsecp256k1.soができてる。あとは生成されたライブラリをちゃんと読みこむようsudo ldconfigしておく。

Bundleインストール

あとはGemfileに

gem 'teth'

と書いてbundleすれば良い。

expectのインストール

tethのスクリプトが内部でexpectを使っているので、expectがインストールされていない場合はインストールしておく。aptやyumでインストールできる。

tethの使い方

プロジェクトの作成

newコマンドでSmart Contractアプリケーションを新規作成し、プロジェクトのテンプレートが作成される。

$ teth n sample
Creating project tethsample...
Resolving dependencies...
Using ffi 1.9.10
Using little-plugger 1.1.4
Using multi_json 1.11.2
Using digest-sha3 1.1.0
Using ethash 0.2.0
Using fiddler-rb 0.1.2
Using lru_redux 1.1.0
Using minitest 5.8.4
Using rlp 0.7.3
Using serpent 0.3.0
Using bundler 1.11.2
Using bitcoin-secp256k1 0.4.0
Using logging 2.1.0
Using leveldb 0.1.9
Using block_logger 0.1.2
Using ruby-ethereum 0.9.6
Using teth 0.2.2
Bundle complete! 1 Gemfile dependency, 17 gems now installed.
Use `bundle show [gemname]` to see where a bundled gem is installed.
Done.

そうすると以下のようなディレクトリ/ファイルが生成される。

f:id:techmedia-think:20161012154255p:plain

コントラクトの新規作成

generateコマンドでコントラクトを新規作成する。

$ teth g token
Creating Token contract file...
Create Token.sol contract file...
Creating token test files...
Done.

コントラクトのファイルとしてcontracts/Token.solが、テストファイルとしてgtests/Token_test.jstests/token_test.rbが生成される。

README.mdに記載されているサンプルのようにコントラクトとテストを実装する。

テストの実行

testコマンドで対象のコントラクトのテストを実行する。この際のテストはrubyのEVMで実行される。

$ teth t token
Test Token contract...
Run options: --seed 44085

# Running:

....

Finished in 1.966287s, 2.0343 runs/s, 3.5600 assertions/s.

4 runs, 7 assertions, 0 failures, 0 errors, 0 skips
Done.

gethと連携したプライベートチェーンでのテスト

testコマンドはrubyのEVMで実行したが、続いてgethを使ってテストを実行する。

プライベートチェーンの初期化

initコマンドを使うと新しいgenesis blockを初期化し、プライベートチェーンの新しいデータディレクトリを作成する。

$ teth init
Initialising a new genesis block...
***** Using geth at: geth
I1012 16:55:00.860131 ethdb/database.go:82] Alloted 16MB cache and 16 file handles to /home/azuchi/projects/ethereum/tethsample/data/chaindata
I1012 16:55:00.870799 cmd/geth/main.go:353] successfully wrote genesis block and/or chain rule set: 611596e7979cd4e7ca1531260fa706093a5492ecbdf58f20a39545397e424d04

data/chaindataディレクトリが新規に作られる。

gethへキーのインポート

import_keysコマンドを実行するとprivate_keysディレクトリ内の*.keyファイルがプライベートチェーンにインポートされる。

$ teth ik
Importing keys, this will take a while, please be patient...
***** Using geth at: geth
***** Import all pre-funded private keys
Notice: No need to input your password. The default password is 123456
spawn geth --datadir data account import ./private_keys/9da26fc2e1d6ad9fdd46138906b0104ae68a65d8.key
Your new account is locked with a password. Please give a password. Do not forget this password.
Passphrase: 
Repeat passphrase: 
Address: {9da26fc2e1d6ad9fdd46138906b0104ae68a65d8}
Notice: No need to input your password. The default password is 123456
spawn geth --datadir data account import ./private_keys/81063419f13cab5ac090cd8329d8fff9feead4a0.key
Your new account is locked with a password. Please give a password. Do not forget this password.
Passphrase: 
Repeat passphrase: 
Address: {81063419f13cab5ac090cd8329d8fff9feead4a0}
Notice: No need to input your password. The default password is 123456
spawn geth --datadir data account import ./private_keys/3ae88fe370c39384fc16da2c9e768cf5d2495b48.key
Your new account is locked with a password. Please give a password. Do not forget this password.
Passphrase: 
Repeat passphrase: 
Address: {3ae88fe370c39384fc16da2c9e768cf5d2495b48}
***** Done.
コントラクトのビルド

buildコマンドでコントラクトをビルドする。(コントラクト名を省略した場合は全てのコントラクトがビルドされる)

$ teth build token 
Building contract Token

======= Token =======
Gas estimation:
construction:
   20201 + 73400 = 93601
external:
   issue(address,uint256):  21716
   transfer(address,uint256):   42187
   getBalance(address): 318
internal:


-------------------------------------
Enter Gas: 
400000
Enter Value To Be Transferred: 

Enter Input: 

Done.
gethの起動

serverコマンドでgeth serverを起動

$ teth server
***** Using geth at: geth
Start geth server...
I1012 17:32:18.222462 ethdb/database.go:82] Alloted 128MB cache and 1024 file handles to data/chaindata
I1012 17:32:18.233830 ethdb/database.go:169] closed db:data/chaindata
I1012 17:32:18.235163 ethdb/database.go:82] Alloted 128MB cache and 1024 file handles to data/chaindata
I1012 17:32:18.277175 ethdb/database.go:82] Alloted 16MB cache and 16 file handles to data/dapp
I1012 17:32:18.281127 eth/backend.go:170] Protocol Versions: [63 62 61], Network Id: 31415926
I1012 17:32:18.281287 eth/backend.go:199] Blockchain DB Version: 3
I1012 17:32:18.281863 core/blockchain.go:206] Last header: #0 [611596e7…] TD=131072
I1012 17:32:18.281887 core/blockchain.go:207] Last block: #0 [611596e7…] TD=131072
I1012 17:32:18.281899 core/blockchain.go:208] Fast block: #0 [611596e7…] TD=131072
I1012 17:32:18.282866 p2p/server.go:311] Starting Server
I1012 17:32:18.283063 p2p/server.go:554] Listening on [::]:30303
I1012 17:32:18.284900 node/node.go:298] IPC endpoint opened: data/geth.ipc
I1012 17:32:18.285096 node/node.go:368] HTTP endpoint opened: http://localhost:8545
I1012 17:32:19.556940 cmd/geth/accountcmd.go:187] Unlocked account 3ae88fe370c39384fc16da2c9e768cf5d2495b48
デプロイ

migrateコマンドでコントラクトをgethにデプロイする。(コントラクト名を省略した場合は全てのコントラクトがデプロイされる)

$ teth m
Migrating all contracts
***** Using geth at: geth
null
Contract transaction send: TransactionHash: 0xc8cba6ded9720d2db8c69551bf1ee1ddee1fe2c7b82a7adda1442da4d3362e1a waiting to be mined...
Compiled Object : TokenCompiled
Contract : TokenContract
Contract Instance : Token
true
Contract mined! Address: 0x3ff58e2e4f9ab2d47f0cd84bead42dd0369846c9
Done.

アカウントがロックされている場合はエラーが発生するので、teth consoleでgethのコンソールにアクセスしアカウントをアンロックする。(初期パスワードはimport.shに書いてあるが123456)

2つのファイルが生成され、1つはabiとアドレスを保持するtemp/db/Token.jsonで、もう1つがtemp/migrations/Token.js

javascriptのテストを書く

gtests以下にgethの環境で実行するテストを記述する。↓はgtests/Token_test.js

loadScript('temp/migrations/Token.js');

var balance = Token.getBalance.call(web3.eth.accounts[0], { from: web3.eth.accounts[0] })

console.log("balance is: ", balance);

Token.issue.sendTransaction(web3.eth.accounts[0], 10000, { from: web3.eth.accounts[0] }, function(err, tx){
  if(err){
    console.log("issue error!");
  } else {
    console.log("issue success. tx: ", tx);
  }
})

miner.start();admin.sleepBlocks(2);miner.stop();

balance = Token.getBalance.call(web3.eth.accounts[0], { from: web3.eth.accounts[0] })

console.log("balance is: ", balance);

作成したテストはgtestコマンドで実行する。

$ teth gt
***** Using geth at: geth
Testing all contracts on geth...
balance is:  0
issue success. tx:  0x4fb32aaf2c9272478dbb549374349654b3785eee3cef8d671603efb54ee62e95
balance is:  10000
true
Done.
gethのコンソールアクセス

consoleコマンドでgethのコンソールにアタッチできる。

$ teth console
***** Using geth at: geth
Starting geth attach...
instance: Geth/v1.5.0-unstable/linux/go1.5.1
coinbase: 0x9da26fc2e1d6ad9fdd46138906b0104ae68a65d8
at block: 13 (Wed, 12 Oct 2016 18:16:38 JST)
 datadir: data

rubyのEVMでサクッとテストが実行できるのは便利だなー。

*1:SerpentはContractを記述する言語の一つでPythonライクな言語。