It’s now or never

IT系の技術ブログです。気になったこと、勉強したことを備忘録的にまとめて行きます。

【Ethereum】Solidityを使ってSmartContractの作成してみる

引き続き、Ethereumを勉強中です。
前回はEthereumのプライベートネットワークを構築する方法について調べました。

inon29.hateblo.jp

今回は、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の画面が確認できます。

f:id:inon29:20170712152955p:plain

  • 上記のように表示されていれば正しく動いています。

ローカルファイルとの同期

  • Remixではローカルファイル(ディレクトリ)と同期を取ることが可能です。

    • (デフォルトの画面上でソースを追加することもできるのですが、そこで作成したファイルがどこのディレクトリに作成されるがよくわかりませんでした。。)
  • ファイルの同期にはremixdというツールを使います。

インストー
npm install -g remixd
同期ディレクトリの指定
remixd -S <同期を取りたいディレクトリ(絶対パス)>
  • remixdでは65520ポートを使用しているため、127.0.0.1:65520へアクセスできるようにしておきます。
ディレクトリ同期

f:id:inon29:20170712153246p:plain

remixのブラウザ画面に戻って左上のメニューから鎖のようなアイコンをクリックしConnectボタンを押下します。

f:id:inon29:20170712153334p:plain

左のファイルエクスプローラに指定したディレクトリとファイルが表示されていれば正しく同期できています。

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

細かいオプションはここでは割愛します。
プライベートネットワークの作成方法については、こちらの記事を参照いただけると幸いです。

inon29.hateblo.jp

  • rpc: JSON-RPCの許可
  • rpcport: JSON-RPCのポート
  • rpcaddr: JSON-RPCの接続ホスト
  • unlock: アカウントへの接続にはアカウントのロックを解除する必要があります。ここでは予めContractを登録するアカウントのロックを解除するためにアドレスを指定しています。

Remixは、nodeとJSON-RPC経由でアクセスするためノードの起動オプションとしてJSON-RPCを許可する必要があります。
環境に合わせて適宜ポートやアドレスを設定します。

f:id:inon29:20170712153925p:plain

Gethの起動が完了したら、Remix上から作成したコントラクトの登録処理を行います。 Remixの右画面 > Contract > EnviromentWeb3 Providerに変更してください。

f:id:inon29:20170712153945p:plain f:id:inon29:20170712153949p:plain

上記のように接続ノードの情報を求められるのでGeth起動時の情報に合わせて接続します。
エラーが表示されなければ正しく接続できていると思います。
(右画面のAccountに接続ノードのアカウントが反映されていれば、正しく接続できていると思います。)

f:id:inon29:20170712154905p:plain

次に右画面の下部にあるCreateというボタンを押下してContractを登録します。

f:id:inon29:20170712154322p:plain

「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を実行するとペンディングトランザクションがなくなっていることがわかります。

f:id:inon29:20170712154453p:plain

Remixに戻ってみると止まっていた処理が完了していることがわかります。 これでContractをアカウントに登録することができました。

Contractの実行

次は登録したContractを実際に動かしてみます。

Contractに作成ユーザー以外がアクセスするためには、以下の2種類の情報を他のユーザーに伝える必要があります。

  • Contractのアドレス
  • ContractのABI (Application Binary Interface)
    • ※ABIとはパラメタや戻り値の型など関数の定義が記述された情報です

コントラクトのオブジェクトは、Geth上で以下のように生成することができます。

eth.contract(<ABI_DEF>).at(<ADDRESS>);

それでは、この2つの情報をRemixから取得して、ContractObjectを作成してみます。

f:id:inon29:20170712154549p:plain

Remix上で上記ボタンを押下するとAddressがクリップボードにコピーされます。

f:id:inon29:20170712154633p:plain

  • 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_numset_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.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