【Ethereum】Solidityを使ってSmartContractの作成してみる
引き続き、Ethereumを勉強中です。
前回はEthereumのプライベートネットワークを構築する方法について調べました。
今回は、Ethereumのコアな機能であるSmartContractについて実際に動かしてみたいと思います。
実行環境
- Ubuntu14.04 on docker
スマートコントラクトとは
スマートコントラクトは、Ethereumを使う上で重要な機能の一つです。
スマートコントラクトとはその名のとおり契約を行う機能で、簡単にいうとブロックチェーンを利用して自動で契約の実行を行うことができるというものです。
例えば、初めに契約の一定の条件と契約時の実行内容を定義しておくと、その条件を満たした時に自動で契約内容が処理される。
といったことが実現できます。
イメージとしては、ブロックチェーン上で行われるやり取り(トランザクション)にブログラムソースが付与されていて、任意のタイミングで実行されるという感じです。
Ethereumではこのスマートコントラクトを柔軟に低コストで実現する機能を提供しています。
コントラクトコード
Ethereumには2つのタイプのアカウントが存在します。
一つはEOA(Externally Owned Account)です。
これは我々ユーザー自身が持つアカウントです。
そしてもう一つがContractです。このContractを使ってスマートコントラクトを実現しています。
Contractは、内部に実行コードである「コントラクト・コード」を持っています。
これはプログラム言語のメソッドのようなイメージです。
Solidity
コントラクトコードは、ネットワーク上で「EVM Code(Ethereum Virtual Machine Code)」と呼ばれる、バイトコードの形式で記述されています。
EVN Codeは低水準言語のため人間には読みにくいコードです。
そのためEVN Codeへのコンパイル言語として「Solidity」というJavascriptライクな言語が用意されています。
Solidityはsolc
というコンパイラを使用して、EVMへのコンパイルを行います。
Remix(browser-solidity)
- Solidityをビルドする環境(IDE)として
Remix
というツールが提供されています。 - 単独でsolcをインストールしてコンパイルすることも可能ですが、今回はRemixを使って実装します。
- geth上でsolcを連携してgethでコンパイルする方法もあったようですが、最新のバージョン(1.6.6)では使えなくなっているようです。
Remix(browser-solidity)をインストール
git clone https://github.com/ethereum/browser-solidity cd browser-solidity npm install
アプリケーションの起動
npm start
- 起動して
127.0.0.1:8080
にブラウザ上でアクセスするとRemixの画面が確認できます。
- 上記のように表示されていれば正しく動いています。
ローカルファイルとの同期
Remixではローカルファイル(ディレクトリ)と同期を取ることが可能です。
- (デフォルトの画面上でソースを追加することもできるのですが、そこで作成したファイルがどこのディレクトリに作成されるがよくわかりませんでした。。)
ファイルの同期には
remixd
というツールを使います。
インストール
npm install -g remixd
同期ディレクトリの指定
remixd -S <同期を取りたいディレクトリ(絶対パス)>
- remixdでは65520ポートを使用しているため、
127.0.0.1:65520
へアクセスできるようにしておきます。
ディレクトリ同期
remixのブラウザ画面に戻って左上のメニューから鎖のようなアイコンをクリックしConnect
ボタンを押下します。
左のファイルエクスプローラに指定したディレクトリとファイルが表示されていれば正しく同期できています。
Contractの登録
contractを動かす環境が整ったので、実際にサンプルソースを動かしてみます。 まずはcontractのサンプルソースをsolidityで記述し、プライベートネットワーク上に登録してみます。
pragma solidity ^0.4.0; contract sample { int num; function set_num(int n){ num = n; } function get_num() returns(int) { return num; } }
- 上記が動かすサンプルソースです。
pragma solidity ^0.4.0;
はSolidityのバージョン記述です。- 上記ではバージョン0.4.0のコンパイラ以外は動作しないという意味です。
contract
の後ろに続くのはコントラクト名です。他の言語でいうクラス名みたいなものです。
細かい文法は割愛しますが、これはnumという変数に値をセットするメソッドset_num
と値を取得するメソッドget_num
という2つのメソッドを定義したcontractになります。
上記のソースをRemix上のファイルに記述します。
ソースが書けたらGethと連携してプライベートネットワークにこのContractを登録します。 まずは、プライベートネットワークへアクセスします。
geth \ --datadir /ethereum/eth_private \--mine \--nodiscover \--maxpeers 0 \--networkid 13 \--rpc \--rpcport 8545 \--rpcaddr "0.0.0.0" \--rpccorsdomain "*" \--unlock 0xd8a4f5db00a5f1648ce8731b15ac3a1f72ce106e \console
細かいオプションはここでは割愛します。
プライベートネットワークの作成方法については、こちらの記事を参照いただけると幸いです。
rpc
: JSON-RPCの許可rpcport
: JSON-RPCのポートrpcaddr
: JSON-RPCの接続ホストunlock
: アカウントへの接続にはアカウントのロックを解除する必要があります。ここでは予めContractを登録するアカウントのロックを解除するためにアドレスを指定しています。- Geth起動時にパスフレーズを求められます
Remixは、nodeとJSON-RPC経由でアクセスするためノードの起動オプションとしてJSON-RPCを許可する必要があります。
環境に合わせて適宜ポートやアドレスを設定します。
Gethの起動が完了したら、Remix上から作成したコントラクトの登録処理を行います。
Remixの右画面 > Contract
> Enviroment
を Web3 Provider
に変更してください。
上記のように接続ノードの情報を求められるのでGeth起動時の情報に合わせて接続します。
エラーが表示されなければ正しく接続できていると思います。
(右画面のAccount
に接続ノードのアカウントが反映されていれば、正しく接続できていると思います。)
次に右画面の下部にあるCreate
というボタンを押下してContractを登録します。
「Waiting for transaction to be mined…」と表示されています。
INFO [07-12|04:11:21] Submitted contract creation fullhash=0x972db68b4a28be14baebfd17b466817a4bd2e77f0e0e3dbd5c8c87a324bc6857 contract=0xda1dd064dd2c5cb2aa7f3b4603ee973021edb329
Gethに戻ってみると、上記のようにcontractがSubmitされたログが表示されています。
> eth.pendingTransactions [{ blockHash: null, blockNumber: null, from: "0xd8a4f5db00a5f1648ce8731b15ac3a1f72ce106e", gas: 107586, gasPrice: 18000000000, hash: "0x972db68b4a28be14baebfd17b466817a4bd2e77f0e0e3dbd5c8c87a324bc6857", input: "0x6060604052341561000f57600080fd5b5b60ce8061001e6000396000f30060606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680633e27a8e8146047578063545a48d014606d575b600080fd5b3415605157600080fd5b6057608d565b6040518082815260200191505060405180910390f35b3415607757600080fd5b608b60048080359060200190919050506097565b005b6000805490505b90565b806000819055505b505600a165627a7a723058204eddaa62d484bcd6eb395ac4fdccd0dcf6c06e86a755c47db93b7cab60d930500029", nonce: 8, r: "0x76e10da1414ee85ea215b78d9b3e97e5021aeb4d47617c05395cc10195042d8c", s: "0x5b3a86db12a335952bb99ee17ff6bd0939204cb43ad56fec5b994c9b830c313a", to: null, transactionIndex: 0, v: "0x3d", value: 0 }]
今の状態は、マイニングがされていないためノードに登録されたContractがブロックには登録されていない状態です。
(上記のようにpendingTransactionsコマンドで登録されていないトランザクション情報が確認できます。)
これをGeth上でマイニングしてブロックに登録します。
> miner.start() INFO [07-12|04:16:43] Updated mining threads threads=0 INFO [07-12|04:16:43] Transaction pool price threshold updated price=18000000000 INFO [07-12|04:16:43] Starting mining operation null > INFO [07-12|04:16:43] Commit new mining work number=2470 txs=1 uncles=0 elapsed=600.469µs INFO [07-12|04:16:53] Successfully sealed new block number=2470 hash=ddb1b6…f5f1ef INFO [07-12|04:16:53] 🔗 block reached canonical chain number=2465 hash=2ae180…48ff9c INFO [07-12|04:16:53] 🔨 mined potential block number=2470 hash=ddb1b6…f5f1ef INFO [07-12|04:16:53] Commit new mining work number=2471 txs=0 uncles=0 elapsed=586.566µs
マイニングをしばらくして、再度eth.pendingTransactions
を実行するとペンディングのトランザクションがなくなっていることがわかります。
Remixに戻ってみると止まっていた処理が完了していることがわかります。 これでContractをアカウントに登録することができました。
Contractの実行
次は登録したContractを実際に動かしてみます。
Contractに作成ユーザー以外がアクセスするためには、以下の2種類の情報を他のユーザーに伝える必要があります。
- Contractのアドレス
- ContractのABI (Application Binary Interface)
- ※ABIとはパラメタや戻り値の型など関数の定義が記述された情報です
コントラクトのオブジェクトは、Geth上で以下のように生成することができます。
eth.contract(<ABI_DEF>).at(<ADDRESS>);
それでは、この2つの情報をRemixから取得して、ContractObjectを作成してみます。
Remix上で上記ボタンを押下するとAddressがクリップボードにコピーされます。
Contract details (bytecode, interface etc)
のリンクをクリックするとContractの詳細情報を確認できます。- ここにある
Interface
という項目がABIになります。
この2つを使ってcontractのオブジェクトを作成してみます。
> var address = "0xed9d193ecdd86033dbfd74088f3cbfa0f3bb5f55" undefined > var abi = [{"constant":false,"inputs":[],"name":"get_num","outputs":[{"name":"","type":"int256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"n","type":"int256"}],"name":"set_num","outputs":[],"payable":false,"type":"function"}] undefined > var contract = eth.contract(abi).at(address); undefined > contract { abi: [{ constant: false, inputs: [], name: "get_num", outputs: [{...}], payable: false, type: "function" }, { constant: false, inputs: [{...}], name: "set_num", outputs: [], payable: false, type: "function" }], address: "0xed9d193ecdd86033dbfd74088f3cbfa0f3bb5f55", transactionHash: null, allEvents: function(), get_num: function(), set_num: function() }
- address情報とabi情報を保持したオブジェクトが生成されています。
- 定義した
get_num
、set_num
についてもfunctionとして定義されています。
このcontractの関数を実行するには以下のようにします。
> contract.get_num.call() > 0 > contract.set_num.call() > []
Contractへ正しくアクセスできていることが確認できます。
contract.set_num(10,{from:eth.accounts[0]}) or contract.set_num.sendTransaction(10,{from:eth.accounts[0]})
- トランザクションを発行してメソッドを実行するには上記のように記載します。
- 詳細はcontract-methods参照
- ここでは、numに
10
をセットするトランザクションを作成しています。
> contract.set_num(11,{from:eth.accounts[0]}) INFO [07-12|05:56:30] Submitted transaction fullhash=0x61086bb8f21b6a7bf3735b7f560996680a6021e4bcf9593799f05515e9731667 recipient=0xed9d193ecdd86033dbfd74088f3cbfa0f3bb5f55 "0x61086bb8f21b6a7bf3735b7f560996680a6021e4bcf9593799f05515e9731667"
上記のように新しいトランザクションが作成されます。
この状態では先程と同様ブロックに登録されていないため、値が反映されていません。
> contract.get_num.call() 0 > eth.pendingTransactions [{ blockHash: null, blockNumber: null, from: "0xd8a4f5db00a5f1648ce8731b15ac3a1f72ce106e", gas: 90000, gasPrice: 18000000000, hash: "0x61086bb8f21b6a7bf3735b7f560996680a6021e4bcf9593799f05515e9731667", input: "0x545a48d0000000000000000000000000000000000000000000000000000000000000000b", nonce: 10, r: "0x256450e05ed9a61147ba3a9b8ba6b63ba03fbec73abdb93ffe4a015a17bacd1", s: "0x5ef0655ae3c76673cd608d0a71ef8b1a0a6ac9947fb27492782ef38b262dcabc", to: "0xed9d193ecdd86033dbfd74088f3cbfa0f3bb5f55", transactionIndex: 0, v: "0x3d", value: 0 }]
先程と同様にマイニングを行いトランザクションをブロックに登録します。
> miner.start() ・・・ > contract.get_num.call() 11
トランザクションがブロックに登録され、メソッドが実行されると値が正しく反映されます。
> eth.getTransaction("0x61086bb8f21b6a7bf3735b7f560996680a6021e4bcf9593799f05515e9731667") { blockHash: "0xd4e2f46e803deb100c3b852c07919ab2533ac1194f1ae3b0cef71f0b9be85a4e", blockNumber: 2476, from: "0xd8a4f5db00a5f1648ce8731b15ac3a1f72ce106e", gas: 90000, gasPrice: 18000000000, hash: "0x61086bb8f21b6a7bf3735b7f560996680a6021e4bcf9593799f05515e9731667", input: "0x545a48d0000000000000000000000000000000000000000000000000000000000000000b", nonce: 10, r: "0x256450e05ed9a61147ba3a9b8ba6b63ba03fbec73abdb93ffe4a015a17bacd1", s: "0x5ef0655ae3c76673cd608d0a71ef8b1a0a6ac9947fb27492782ef38b262dcabc", to: "0xed9d193ecdd86033dbfd74088f3cbfa0f3bb5f55", transactionIndex: 0, v: "0x3d", value: 0 }
eth.getTransaction
でトランザクション情報を参照するとどのブロックに登録されたかも確認することができます。eth.getTransaction
の引数はcontractのAddressではなくHashなので注意が必要です。
これでSolidityという言語でContractを動かすことができるようになりました。
Contractは基本的にプログラムの実行なのでエンジニア的には理解し易い気がします。
どのようにトランザクションが処理されるかなど詳しく知りたいことはまだありますが少しずつ覚えたいと思います。
参考リンク
Accessing a shared folder in Remix IDE using Remixd — Remix 1 documentation
Installing Solidity — Solidity 0.4.14 documentation
wiki/[Japanese]-Solidity-Tutorial.md at master · ethereum/wiki · GitHub