Ubuntu17でSolidityでスマートコントラクトをする
ざっくりすぎるタイトルである。そもそも「スマートコントラクトをする」という表現は正しいのか?
ともあれHyperledger FabricはGo言語でちょっと本腰入れてやらないといけないし、Hyperledger Irohaはスマートコントラクトがまだ実装されていないそうなので、もう少し簡単に試せるEthereum Solidityで感覚を掴んでいくことにします。
http://solidity.readthedocs.io/en/latest/index.html
SolidityはC++, Python, JavaScriptに寄せた記述方式になっているので、モダンで高級な言語が眩しい化石エンジニアでもとっつきやすそうです。
インストール
必要なものは
- Ethereumのクライアント
- solidity
クライアントは簡単のためGethを使います(なんでもいい
$ sudo add-apt-repository -y ppa:ethereum/ethereum $ sudo apt-get update $ sudo apt-get install ethereum
$ sudo apt-get install solidity
gethを動かす
gethノードを開発モードで立ち上げる
$ mkdir ~/test $ cd ~/test $ geth --dev --dtadir .
とりあえずメインのアカウントに大量のEthereumが付与され、テストで使えるようになります。devオプションをつけた場合、マイニングが行われるのはアカウント間の送金が発生するときだけとのことなので、トランザクションを実行した後に必ず送金処理が必要なことに注意しましょう。
参考:https://github.com/ethereum/go-ethereum/issues/15646
gethコンソールを開く
別のターミナルを開いてコンソールとして使用します。
$ geth attach geth.ipc
Welcome to the Geth JavaScript console! instance: Geth/v1.7.3-stable-4bb3c89d/linux-amd64/go1.9.1 coinbase: 0x16e6395ae580d15a3fb6ed98b6bbdd73d42cb96d at block: 27(Thu, 08 Feb 2018 18:50:03 JST) datadir: /home/user/test modules: admin:1.0 clique:1.0 debug:1.0 eth:1.0 miner:1.0 net:1.0 personal:1.0 rpc:1.0 shh:1.0 txpool:1.0 web3:1.0 > ||< ここにあるmodulesっていうのがすでにあるインスタンス。ためしに打ち込んでみます。 >|| > admin { datadir: "/home/user/test", nodeInfo: { enode: "enode://ec271ee7f939f4780ec23230417f4ed2e54f3906123818cf5328f83bba7cf66e7ae0e12d052dd7f7b3f73278660287e3724bbf71e540d1d85003a85cdc31f210@[::]:40771?discport=0", id: "ec271ee7f939f4780ec23230417f4ed2e54f3906123818cf5328f83bba7cf66e7ae0e12d052dd7f7b3f73278660287e3724bbf71e540d1d85003a85cdc31f210", ip: "::", listenAddr: "[::]:40771", name: "Geth/v1.7.3-stable-4bb3c89d/linux-amd64/go1.9.1", ports: { discovery: 0, listener: 40771 }, protocols: { eth: { difficulty: 55, genesis: "0x3b64127031ee4640b0ec10ca369e1d5b442336e7a47aa714c39f692d43f36f43", head: "0xa961a0bb3552403d6ed6e4b3080b8d6465d50a7db48abe414d09ef90b40f5c93", network: 1 }, shh: { maxMessageSize: 1048576, minimumPoW: 0.2, version: "5.0" } } }, peers: [], addPeer: function(), exportChain: function(), getDatadir: function(callback), getNodeInfo: function(callback), getPeers: function(callback), importChain: function(), removePeer: function(), sleep: function github.com/ethereum/go-ethereum/console.(*bridge).Sleep-fm(), sleepBlocks: function github.com/ethereum/go-ethereum/console.(*bridge).SleepBlocks-fm(), startRPC: function(), startWS: function(), stopRPC: function(), stopWS: function() }
adminインスタンスの持ってる変数やら関数やらが見れます。が、どうやって使うのかはよくわからない…昔のが残っていることもあり、使えないのも結構ある。ドキュメントを読むかソースコードを読むしかない。
ちなみに残高を見ると
> eth.getBalance(eth.accounts[0]) 1.15792089237316195423570985008687907853269984665640564039457584007911129021018e+77
大金持ちです。テスト用なので最初から大量の付与してくれているみたいです。eth.accounts[0]は自動で作成されるアカウントで、coinbaseとして設定されています。
アカウントを作成する
> personal.newAccount()
パスフレーズが要求されるけど、テスト用でプライベートなので特に設定しなくても良いです(設定しても良い
アドレスっぽい文字列が表示されればOK。
アカウントをアンロックする
> personal.unlockAccount(eth.accounts[0]) true > personal.unlockAccount(eth.accounts[1]) true
パスフレーズを求められるけど、アカウント作成のときに使ったやつを入れればOK.
gasPriceを設定する
Ethの仕様なのかSolidityの仕様なのかまだちゃんと読んでないので知らないが、SmartContractを実行するのにgasPriceという手数料が課される模様。開発者モードではデフォルトが0なので、本番環境でアレ?ってならないようにとりあえず0より大きな数を設定しておきます。
> miner.setGasPrice(1) true
gethノードを実行しているターミナルの方でpriceがアップデートされてればたぶんOK。
マイニングを開始する
> miner.start() null
開発者モードのときはとりあえずWARNが表示され、"Block sealing failed"って言われるが特に気にしなくてよい。
ようやくSolidityです
まずソースコードを書きます
とりあえず公式ドキュメントのサンプルプログラムを動かしてみます。
pragma solidity ^0.4.0; //includeみたいなもの。他のファイルも同様に読み込める contract SimpleStorage { //クラス的なやつ uint storedData; //変数 function set(uint x) public { //メソッド。public, private など公開範囲が設定できるほか、 storedData = x; //payableとかreturnの型とかも設定できる。 } function get() public constant returns (uint) { return storedData; } }
写経(コピペ)したらtest.solという名前で保存します。とりあえずなんか安心する見た目ですね。
コンパイルする
コンパイルはコマンドラインから。Remix使ってもいいけどとりあえず導入がめんどくさいし大した手間でもないので。
コンパイルは新しいターミナルを開いてから。
$ cd ~/test $ solc --bin --abi --gas test.sol
コンパイルに成功するとbinとabiと消費されるgas評価が出力される(bin, abi, gasオプションを指定しているから)。
======= test.sol:Storage ======= Gas estimation: construction: 88 + 42200 = 42288 external: get(): 416 set(uint256): 20178 Binary: 6060604052341561000f57600080fd5b60d38061001d6000396000f3006060604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c14606e575b600080fd5b3415605857600080fd5b606c60048080359060200190919050506094565b005b3415607857600080fd5b607e609e565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a72305820c9facaea60dbb669543e6b5fdcca2f1aa9f3a32dfc313f846d4bc9d9482ba8460029 Contract JSON ABI [{"constant":false,"inputs":[{"name":"x","type":"uint256"}],"name":"set","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"get","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"}]
結構バスバスgasを消費するようです。Setのほうがコストが高いんだなぁ。
gathノードでSolidityアプリケーションを動かす
アナクロな方法でSolidityアプリケーションをGethに認識させます(デプロイします)。
ここからの作業はgethコンソールで。
> test_bin="0x6060604052341561000f57600080fd5b60d38061001d6000396000f3006060604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c14606e575b600080fd5b3415605857600080fd5b606c60048080359060200190919050506094565b005b3415607857600080fd5b607e609e565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a72305820c9facaea60dbb669543e6b5fdcca2f1aa9f3a32dfc313f846d4bc9d9482ba8460029" > test_abi=[{"constant":false,"inputs":[{"name":"x","type":"uint256"}],"name":"set","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"get","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"}]
コンパイルの結果のところからBinaryとJson ABIをコピペして貼り付けるのです。Binaryは0xをつけてダブルクオテーションでくくること。
準備ができたのでインスタンスを作成します。
> test_contract = eth.contract(test_abi) > test_inst = test_contract.new({from: eth.accounts[0], data: test_bin, gas: 1000000 }) { abi: [{ constant: false, inputs: [{...}], name: "set", outputs: [], payable: false, stateMutability: "nonpayable", type: "function" }, { constant: true, inputs: [], name: "get", outputs: [{...}], payable: false, stateMutability: "view", type: "function" }], address: undefined, transactionHash: "0x6b2690deca3d191dde77ec4e88f51192437023feb684219b0b51d4409aa7e209" }
これでインスタンスは作成されたけど、まだブロックチェーンには登録されていません。addressがundefinedです。登録するためにEthを動かします。
> eth.sendTransaction({ from: eth.accounts[0], to: eth.accounts[1], value:1000000 })
おもむろにインスタンスを見てみます
> test_inst { abi: [{ constant: false, inputs: [{...}], name: "set", outputs: [], payable: false, stateMutability: "nonpayable", type: "function" }, { constant: true, inputs: [], name: "get", outputs: [{...}], payable: false, stateMutability: "view", type: "function" }], address: "0x948cf5dfb747c974e2ac90168bc537a62efc28d4", transactionHash: "0x6b2690deca3d191dde77ec4e88f51192437023feb684219b0b51d4409aa7e209", allEvents: function(), get: function(), set: function() }
addressにアドレスが設定されているのでOKです。早速get/setを使ってみます。read-onlyのメソッドを呼ぶ時はメソッド名.call()、引数がある場合はメソッド名.sendTransaction(引数、{from: 送信アカウント名} )とするらしいです。
> test_inst.get.call() 0 > test_inst.set.sendTransaction(1, {from: eth.accounts[0]}) 16進数 > test_inst.get.call() 1
ちなみにeth.getBalance(eth.accounts[1])とすると適度にEth(というかwei)が消費されています
デリゲートも使えるみたいなんで自由度高いですね。