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)が消費されています
デリゲートも使えるみたいなんで自由度高いですね。