【Ethereum】【Solidity】OpenZeppelinのDayLimitのソースを読んでみる
OpenZeppelinというフレームワークのソースコードを引き続き読んでいます。
過去はこちら inon29.hateblo.jp
今回は、DayLimit
というContractのソースを読んでみました。
DayLimitコントラクトは、関数に日付の実行制限を付与するためのコントラクトです。
ソース
https://github.com/OpenZeppelin/zeppelin-solidity/blob/v1.2.0/contracts/DayLimit.sol
フィールド
uint256 public dailyLimit; uint256 public spentToday; uint256 public lastDay;
dailyLimit
: 一日に動かせる制限値spentToday
: 一日に動かした量lastDay
: 最後に動かした日(int)
コンストラクタ
function DayLimit(uint256 _limit) { // 1日あたりの制限値を設定 dailyLimit = _limit; // 最後に実行された日を今日に初期化 lastDay = today(); }
_limit
: 一日に動かせる制限値を初期化できます。
制限値の初期化(_setDailyLimit)
function _setDailyLimit(uint256 _newLimit) internal { dailyLimit = _newLimit; }
- 一日の制限値の変更。
- internalのため、このコンストラクタまたは継承コンストラクタからのみ実行可能です。
使用値の初期化(_resetSpentToday)
function _resetSpentToday() internal { spentToday = 0; }
- 一日に使用した量の初期化
制限チェック(underLimit)
function underLimit(uint256 _value) internal returns (bool) { // 日付が変わっていれば、使用値とチェック日付をリセットする if (today() > lastDay) { spentToday = 0; lastDay = today(); } // 制限値を超えていなければ、使用値に加算してtrueを返す if (spentToday + _value >= spentToday && spentToday + _value <= dailyLimit) { spentToday += _value; return true; } return false; }
- 現在の値が、制限を超えているかをチェックします
spentToday + _value >= spentToday
は_valueがマイナスかをチェックしている?- この処理は、他のcontractでもよく見かけるが、uintでもマイナス値が入ることがあるのか?
今日の日付を返す(today)
function today() private constant returns (uint256) { return now / 1 days; }
- 現在日付を1日で割り、今日の日付のint値を返しています。
実行制限のmodifier(limitedDaily)
modifier limitedDaily(uint256 _value) { require(underLimit(_value)); _; }
- 関数に制限値のチェックを行う実行チェックを付与できます。
- underLimitでfalseが返されるとエラーになります。
感想
- Shareableに比べるとかなりシンプルです。
- 1dayの計算箇所を変更すれば、色々な期間の制限に応用ができそうです。
【Ethereum】【Solidity】OpenZeppelinのShareableのソースを読んでみる
Solidity言語を使って実装された、 基本的なContractをフォーマット化したOpenZeppelinというフレームワークが開発されています。
このフレームワークに含まれるShareable
というContractのソースを読んでみたので、メモを記載します。
Shareableコントラクトは、コントラクトの実行権を複数のオーナーで管理するためのコントラクトです。
このContractを継承すると、オーナーの実行制御を付与するためのmodifierを関数に定義することができます。(onlyOwner
,onlymanyowners
)
ソース
https://github.com/OpenZeppelin/zeppelin-solidity/blob/v1.2.0/contracts/ownership/Shareable.sol
コンストラクタ
function Shareable(address[] _owners, uint256 _required) { // contract登録者をownerとして登録する // オーナーのindexは後述の処理でビットフラグとして使用される。 // その為、はじめの格納位置は0ではなく1からになる owners[1] = msg.sender; // 逆引き用の配列にindex(1)をセット ownerIndex[msg.sender] = 1; // 初期引数で渡されるオーナーを登録する for (uint256 i = 0; i < _owners.length; ++i) { owners[2 + i] = _owners[i]; ownerIndex[_owners[i]] = 2 + i; } required = _required; require(required <= owners.length); }
- コンストラクタでは、オーナーの登録と必要なindexパラメタの初期化を行います。
_owners
には、共同管理するオーナーアドレスの配列を指定します。_required
には、実行に必要なオーナー数(承認数)を指定します。owners
はオーナーのアドレスを管理するフィールド変数です。address[256] owners;
- 最大255人(初めにindex1をつかっているので255)のオーナーを登録できます。
ownerIndex
は、アドレスからオーナーのindexを逆引きするためのフィールドです。mapping(address => uint256) ownerIndex;
- [気になった点]初めに仮引数で渡されるownersを登録していますが、senderを含めていないためowndersにsenderと同じアドレスがいると上手く動かなそうに見えます。
_owners
配列自体もそもそも重複チェックしていないため、_ownersに重複addressが存在すると逆引きのownerIndexがズレてしまう気がします。
onlymanyowners modifier
modifier onlymanyowners(bytes32 _operation) { if (confirmAndCheck(_operation)) { _; } }
- このmodifierを使用して、複数の登録オーナーが関数を承認(実行)しないと実行されない関数を実現できます。
_operation
には実行許可の操作を管理するためのバイトコードが渡されます。confirmAndCheck
は内部で実行したオーナーが_operation
の操作に対して実行(承認)を行ったかを記録しています。- その為、このmodifireを指定した関数は、規定数に達するまでオーナーからの実行を拒否することができます。
confirmAndCheck
function confirmAndCheck(bytes32 _operation) internal returns (bool) { // 関数実行者からindexを逆引き uint256 index = ownerIndex[msg.sender]; // 対象indexがなければ終了 require(index != 0); // 現在、ペンディング中のoperation情報を検索 var pending = pendings[_operation]; // そのオペレーションがまだ一度も実行されていないときは値を初期化 if (pending.yetNeeded == 0) { // 必要な実行数を初期化 pending.yetNeeded = required; // 確認済みオーナー数を0リセット pending.ownersDone = 0; // indexをセット pending.index = pendingsIndex.length++; pendingsIndex[pending.index] = _operation; } // オーナーのビットを決定 uint256 ownerIndexBit = 2**index; // オーナーが既に実行していないかのチェック(論理積) if (pending.ownersDone & ownerIndexBit == 0) { Confirmation(msg.sender, _operation); // この実行で承認数を満たす場合 if (pending.yetNeeded <= 1) { // 保留中の操作情報を削除(初期化)して終了 delete pendingsIndex[pendings[_operation].index]; delete pendings[_operation]; return true; } else { // 必要数に足りない場合 // 必要数を減算 pending.yetNeeded--; // 承認フラグにbitを立てる pending.ownersDone |= ownerIndexBit; } } return false; }
- このContractのキーとなる関数です。各オーナーが対象の操作に対して承認(実行)を行ったかを管理する関数しています。
pendings
は、実行を保留されている操作を管理する配列フィールドです。mapping(bytes32 => PendingState) pendings;
PendingState
という構造対を操作のバイト列をキーとして持ちます。
struct PendingState { uint256 yetNeeded; // その操作に必要な残りの実行オーナー数 uint256 ownersDone; // 現在実行したオーナー数 uint256 index; // 保留中の操作のindex }
PendingState
は操作の各状態をプロパティとして持っています。pendingsIndex
は、保留中の操作のindexを逆引きするための配列フィールドです。
revoke
function revoke(bytes32 _operation) external { uint256 index = ownerIndex[msg.sender]; if (index == 0) { return; } // 自分のビットを立てる uint256 ownerIndexBit = 2**index; var pending = pendings[_operation]; // 自分が承認(実行)しているか確認 if (pending.ownersDone & ownerIndexBit > 0) { pending.yetNeeded++; // 必要数の加算 pending.ownersDone -= ownerIndexBit; // ビットを削除 Revoke(msg.sender, _operation); } }
- 確認操作を取り消すための関数です。
onlyOwner modifier
modifier onlyOwner { require(isOwner(msg.sender)); _; }
- オーナーであれば実行できる権限を付与するためのmodifierです。
isOwner
function isOwner(address _addr) constant returns (bool) { return ownerIndex[_addr] > 0; }
- 自分がオーナーに登録されているかチェックする関数です。
hasConfirmed
function hasConfirmed(bytes32 _operation, address _owner) constant returns (bool) { var pending = pendings[_operation]; uint256 index = ownerIndex[_owner]; // オーナーであるかのチェック if (index == 0) { return false; } uint256 ownerIndexBit = 2**index; // ビット判定 // pending.ownersDone & ownerIndexBit > 0 でも良さそう? return !(pending.ownersDone & ownerIndexBit == 0); }
- 指定のオーナーが指定の操作に対して承認(実行)をチェックする関数です。
clearPending
function clearPending() internal { uint256 length = pendingsIndex.length; for (uint256 i = 0; i < length; ++i) { if (pendingsIndex[i] != 0) { delete pendings[pendingsIndex[i]]; } } delete pendingsIndex; }
- 保留中操作の初期化処理。全ての保留中操作をリセットする関数です。
- internalのためContract内部または、継承するContractからのみ実行を許可します。
感想
- ビット演算を使ってシンプルな仕組みで承認を管理する仕組みを実現しています。
- Ethereum上のプログラムは、チェーンサイズが限られていることや、Contractサイズが手数料に影響することもあり、バイトサイズを意識した実装は勉強になります。
- オーナーの数とビットを連動しているため、現状の実装だと255人が最大のようなので、大人数の承認システムを作りたい場合は別の方法を検討する必要がありそうです。
【Ethereum】Contractをネットワークにデプロイする
Ethereumの勉強を引き続きしています。
Contractをネットワークへデプロイする場合、
今までは、Ethereum WalletやRemix経由で行っていたのですが、ソースをコンパイルしていデプロイする方法がよくわからなかったので調べてまとめてみました。
古いバージョンのgethでは、コンソール上でコンパイルができるようなのですが、最新のgethではできないようです。
なので今回は、javascriptのAPIであるweb3
を使ってSollidityのソースをコンパイルします。
Solidityのインストール
npm install solc
- SolidityのコンパイルをJavascript上で行うためにSolidityのパッケージをインストールします
web3のインストール
npm install web3@0.20.0
- JavaScriptAPI環境であるweb3をインストールします。
- 現状のnpmの最新バージョンは、ベータ版の
1.0.0-beta.13
でしたが、想定のAPIが上手く動かなかったりとハマったためStable版の0.20.0
をインストールしました。
ノード起動
geth --datadir <ネットワークのrootディレクトリ> --mine --nodiscover --maxpeers 0 --networkid <ネットワークID> --rpc --rpcport 8545 --rpcaddr "0.0.0.0" --rpcapi="db,eth,net,web3,personal,web3" --rpccorsdomain "*"
- プライベートネットワークのノードを起動します。
datadir
はプライベートネットワークを作成したディレクトリです。networkid
は任意のIDで大丈夫です。- web3はRPC経由でアクセスするため、RPCを許可します。
- プライベートネットワークの構築方法は以下を参照いただければ幸いです。
Contract
pragma solidity ^0.4.0; contract TestContract { uint data; function set(uint x) { data = x; } function get() constant returns (uint) { return data; } }
- 今回は、このテスト用のContractをコンパイルしてノード上にデプロイしてみます。
- Javascriptを実行するディレクトリ上に上記ファイルを配置しておきます。
Javascript
// 必要なパッケージをインポート const fs = require("fs"); const solc = require('solc'); const Web3 = require('web3'); // web3の初期化 web3 = new Web3(); // プライベートネットワークと接続 if (!web3.currentProvider) { web3.setProvider(new web3.providers.HttpProvider("http://localhost:8545")); } // contractソースをファイルから読み込み // ここは、ソースを文字列として読み込んでいるだけです。 let source = fs.readFileSync('test_contract.sol', 'utf8'); // コンパイル let compiledContract = solc.compile(source, 1); // ABIを取得(配列のキーはなぜか頭に:が必要でした) let abi = compiledContract.contracts[':TestContract'].interface; // コントラクトのバイトコードの取得(頭に0xを付けないと後述のAPIで弾かれる) let bytecode = "0x" + compiledContract.contracts[':TestContract'].bytecode; // デプロイに必要なGasを問い合わせる let gasEstimate = web3.eth.estimateGas({data: bytecode}); // コントラクトオブジェクトの生成 let TestContract = web3.eth.contract(JSON.parse(abi)); // デプロイアカウントのLockを外す web3.personal.unlockAccount(web3.eth.accounts[0], "<アカウントのパス>"); // ネットワークにデプロイ // コンストラクタがある場合は、ハッシュの前の引数にコンストラクタのパラメタを渡すことで初期化可能 TestContract.new({from: web3.eth.accounts[0], data:bytecode, gas:gasEstimate });
- このJavascriptを実行します
node deploy.js
デプロイの確認
INFO [08-02|22:49:38] Submitted contract creation fullhash=0x7605a5560f0d729f89f5b1e4ea632d84b4ebd038f930d53620c521adc8f90f0d contract=0xd06c3fce760aa1ab550b0cde8fe4792a1e4a3e11
- 上記のようなログがgethに出力されていればデプロイに成功しています。
デプロイしたコントラクトを使う場合
const Web3 = require('web3'); web3 = new Web3(); // providerとの接続 if (!web3.currentProvider) { web3.setProvider(new web3.providers.HttpProvider("http://localhost:8545")); } let abi = [{"constant":false,"inputs":[{"name":"x","type":"uint256"}],"name":"set","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"get","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"}]; let address = "0xd06c3fce760aa1ab550b0cde8fe4792a1e4a3e11"; let TestContract = web3.eth.contract(abi); let contract = TestContract.at(address); let num = contract.get.call(); console.log(num.toPrecision());
- 一度ネットワークにデプロイされたコントラクトは
ABI
とコントラクトのアドレス
からアクセスすることが可能です
0
- 現状は、値のセットがされていないため0が返ります。
参考リンク
https://github.com/ethereum/wiki/wiki/JavaScript-API#web3ethcontract
【Ethereum】Ethereumで独自のトークンの作成してみる
Ethereumのトークンとは
Ethereum上でトークンとよばれるコントラクトを作成することで独自の暗号通貨を作成することが可能です。 トークンを使うことで、コイン、ポイントなど様々な仮想的な価値をEthereumの仕組みの上で利用することができます。 トークンはEhereumのネットワーク上のアカウント間で自由にやりとりすることが可能です。
トークンコンストラクト
トークンを定義するためのコンストラクトには、幾つかの標準APIが定義されています。
トークンAPIの議論についてはここでされています。
Ethereumのトークンのサンプル実装は、ここにあります。
pragma solidity ^0.4.8; contract tokenRecipient { function receiveApproval(address _from, uint256 _value, address _token, bytes _extraData); } contract MyToken { /* Public variables of the token */ string public standard = 'Token 0.1'; string public name; string public symbol; uint8 public decimals; uint256 public totalSupply; /* This creates an array with all balances */ mapping (address => uint256) public balanceOf; mapping (address => mapping (address => uint256)) public allowance; /* This generates a public event on the blockchain that will notify clients */ event Transfer(address indexed from, address indexed to, uint256 value); /* This notifies clients about the amount burnt */ event Burn(address indexed from, uint256 value); /* Initializes contract with initial supply tokens to the creator of the contract */ function MyToken( uint256 initialSupply, string tokenName, uint8 decimalUnits, string tokenSymbol ) { balanceOf[msg.sender] = initialSupply; // Give the creator all initial tokens totalSupply = initialSupply; // Update total supply name = tokenName; // Set the name for display purposes symbol = tokenSymbol; // Set the symbol for display purposes decimals = decimalUnits; // Amount of decimals for display purposes } /* Send coins */ function transfer(address _to, uint256 _value) { if (_to == 0x0) revert(); // Prevent transfer to 0x0 address. Use burn() instead if (balanceOf[msg.sender] < _value) revert(); // Check if the sender has enough if (balanceOf[_to] + _value < balanceOf[_to]) revert(); // Check for overflows balanceOf[msg.sender] -= _value; // Subtract from the sender balanceOf[_to] += _value; // Add the same to the recipient Transfer(msg.sender, _to, _value); // Notify anyone listening that this transfer took place } /* Allow another contract to spend some tokens in your behalf */ function approve(address _spender, uint256 _value) returns (bool success) { allowance[msg.sender][_spender] = _value; return true; } /* Approve and then communicate the approved contract in a single tx */ function approveAndCall(address _spender, uint256 _value, bytes _extraData) returns (bool success) { tokenRecipient spender = tokenRecipient(_spender); if (approve(_spender, _value)) { spender.receiveApproval(msg.sender, _value, this, _extraData); return true; } } /* A contract attempts to get the coins */ function transferFrom(address _from, address _to, uint256 _value) returns (bool success) { if (_to == 0x0) revert(); // Prevent transfer to 0x0 address. Use burn() instead if (balanceOf[_from] < _value) revert(); // Check if the sender has enough if (balanceOf[_to] + _value < balanceOf[_to]) revert(); // Check for overflows if (_value > allowance[_from][msg.sender]) revert(); // Check allowance balanceOf[_from] -= _value; // Subtract from the sender balanceOf[_to] += _value; // Add the same to the recipient allowance[_from][msg.sender] -= _value; Transfer(_from, _to, _value); return true; } function burn(uint256 _value) returns (bool success) { if (balanceOf[msg.sender] < _value) revert(); // Check if the sender has enough balanceOf[msg.sender] -= _value; // Subtract from the sender totalSupply -= _value; // Updates totalSupply Burn(msg.sender, _value); return true; } function burnFrom(address _from, uint256 _value) returns (bool success) { if (balanceOf[_from] < _value) revert(); // Check if the sender has enough if (_value > allowance[_from][msg.sender]) revert(); // Check allowance balanceOf[_from] -= _value; // Subtract from the sender totalSupply -= _value; // Updates totalSupply Burn(_from, _value); return true; } }
- 上記がサンプル実装のトークンコントラクトのソースです。
- プログラム処理としてはそれほど難しくないため、何となく書いてあることは理解できます。
各構文の理解もしていきたいのですが、今回はこのトークンコントラクトのサンプルソースを使って、新しいトークンをデプロイしてみます。
実行環境
- Ubuntu14.04 on docker
Ethereumのプライベートネットワークを作成する
- トークンは本番ネットワークではなくプライベートネットワークで作成します。
- プライベートネットワークの作成については以下を参照ください。
プライベートネットワーク(node)の起動
プライベートネットワークを起動してコントラクトを作成する準備をします。
コントラクトを作成するトランザクションでマイニングが必要なため予めマイニングをスタートしておきます。
geth \ --datadir /ethereum/eth_private \ --mine \ --nodiscover \ --maxpeers 0 \ --networkid 13 \ --rpc \ --rpcport 8545 \ --rpcaddr "0.0.0.0" \ --rpcapi="db,eth,net,web3,personal,web3" \ --rpccorsdomain "*"
mine
: マイニングを許可する。このオプションを指定するとマイニングが始まります。nodiscover
: Gethはデフォルトで自動的に(同じネットワークID)のEthereumネットワークのノード(Peer)を探し接続を試みます。プライベート・ネットで未知のノードとの接続を避けるため、このオプションを指定することで自動Peer探索機能を無効にします。maxpeers
: ネットワークに接続するノードの最大数(デフォルト25) 0を指定するとネットワークが無効になります。networkid
: ネットワークID(デフォルト1) 1を指定するとグローバルのネットワークに接続されます。- 2,3,4も予約されているようです。(2=Morden (disused), 3=Ropsten)
rpc
: HTTP-RPC serverとして動作を許可するrpcport
: HTTP-RPCで使用するポート番号rpcaddr
: 接続ホストrpcapi
: HTTP-RPCのインタフェースの種類rpccorsdomain
クロスオリジンを許可するリスト
後述するEthereum wallet
でJSON-RPC経由で接続するためにRPCの許可をしています。
送信先のテストアカウントを作成
作成したトークンを送信するためのテストアカウントをコンソール上で作成しておきます。
> personal.newAccount('<任意のパスフレーズ>') "0xe41253c3218ade0650469b87dcb97a3298d3b5d8"
- コンソールからアカウントを作成します。
Ethereum walletのインストール
- Ethereum walletとはEthereumの公式のツールです。Ethereum Walletを使うとウォレットの管理をGUIベースで行うことができます。
- その他にもEthereum walletでは、コントラクトの作成や独自トークンの管理など便利な機能があります。
- 今回はこのEthereum walletを使って独自のトークンを作成&デプロイします。
### インストール
- ダウンロードページから自分にあった環境のツールをダウンロードします。
Ethereum walletの起動(Mac環境)
% /Applications/Ethereum\ Wallet.app/Contents/MacOS/Ethereum\ Wallet --rpc http://localhost:8545
- MacアプリのEthereum walletから先程起動したプライベートノードへ接続します
- 僕は、Geth環境をDocker上に作成しているため、あらかじめDockerで
8545
ポートをフォワーディングしています。
- 僕は、Geth環境をDocker上に作成しているため、あらかじめDockerで
- rpcのセキュリティ警告がでますがOKを押下して進みます。
- (RPCは接続制御ができないため、テスト環境以外では使わない方がいいと思います。)
LAUNCH APPLICATION
を押下します
- ネットワークの接続に成功するとwallet画面が表示されます
- 先程作成したアカウント(coinbaseと送信用)が2つ表示されていれば正しく接続されています。
カスタムトークンをデプロイする
- Ethereum wallet上からカスタムのトークンを作成してみます。
- アプリ上メニューの
CONTRACTS
>DEPLOY NEW CONTRACT
を押下
Deploy contract画面を少しスクロールして
SOLIDITY CONTRACT SOURCE CODE
の部分にサンプルソースをそのまま添付しますしばらくするとコンパイルが完了するので、画面右の
SELECT CONTRACT TO DEPLOY
からMy Token
を選択します
- 作成するトークンの情報を入力します
Initial supply
: 初期のトークン総量。今回は10000トークン発行します。Token name
: トークンの名前です。今回はMyToken
という名前のトークンを発行します。Decimal units
: トークンの小数点以下の桁数を入力します。(下2桁であれば2
と入力する)。今回は2
にします。- Initial supplyが10000でDecimal unitsが2の場合、
100.00
になります。下2桁までいれて合計10000トークンということです。
- Initial supplyが10000でDecimal unitsが2の場合、
Token symbol
: トークンの単位です。今回はmt
としました。(1トークン=1mt)
この辺、ちゃんと理解できていないのですが、SELECT FEEは1トランザクションを処理する時の手数料で、 大きな値を設定するほど、トランザクション処理の時間が短くなるということっぽいです。 今回は、そのまま指定の値で作成します。
全て入力を終えたらDeploy
ボタンを押下します。
- 作成するContractの情報が表示されるので、作成アカウントのパスワードを入力して
SEND TRANSACTION
を押下します。
- wallet画面にもどるので、トランザクションの承認が終わるまで待ちます。
- 承認が終わったら、Contractを作成したアカウントを押下してアカウントの詳細画面へ遷移すると自分が作成したトークン情報が確認できます。
トークンを送信してみる。
- アカウント画面のMyTokenの右にある
Send
ボタンを押下してみます。
- パスワードを入力してトランザクションを送信します。
- 送信相手のアカウント情報を見てみると
30mt
が送られていることが確認できます。
参考リンク
【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
【Ethereum】EthereumでPrivateNetworkを作ってみる
仕事の関係でブロックチェーンのプラットフォームであるEthereumを調べることになり、
簡単な動作確認を行うためにプライベートのEthereumのネットワークを作成してみました。
- ※ 調べながらのものなのでところどころ間違っているかもしれません。
- ※ ブロックチェーン、仮想コイン自体の知識も深くないためそちらも認識に齟齬があるかもしれません。
動作環境
OS: ubuntu:14.04 * ※ 僕の環境はMacOSなのですが、試行錯誤なので環境がよごれないようにDockerでubuntuをインストールして試しています。 * ※ Docker周辺の操作自体は割愛させていただきます。
Ethereumとは
Ethereumは、ブロックチェーンを使ったアプリケーションのプラットフォームです。
(Ethereum、ブロックチェーンの技術詳細については、ここでは割愛します。)
ブロックチェーンを使ったアプリケーションを作成する方法は大きく分けて2つあります。
1つは、「ゼロからブロックチェーンのネットワークを構築する方法」、もう一つは「既存のブロックチェーンの技術基盤を利用して構築する方法」です。
Ethereumは後者にあたるもので、Ethereumを利用することでブロックチェーンを利用したアプリケーションを低コストで作成することができます。
Gethをインストールする
EthereumのP2Pネットワークに参加するためにGeth
というクライアントツールが提供されています。
(GethはGo言語製のツールです)
Gethをインストールすることで、
といった操作を行うことができます。 まずは、このツールをインストールします。
sudo apt-get install software-properties-common sudo add-apt-repository -y ppa:ethereum/ethereum sudo add-apt-repository -y ppa:ethereum/ethereum-dev sudo apt-get update sudo apt-get install ethereum
ネットワーク用のディレクトリを作る
mkdir eth_private cd eth_private
※ このテスト実装では、/ethereum
というディレクトリをルートディレクトリとして作業しています。
アカウントをつくる
一番初めのアカウントを定義します。
geth --datadir /ethereum/eth_private account new
datadir
に先程作成したディレクトリを指定します
WARN [07-09|03:57:57] No etherbase set and no accounts found as default Your new account is locked with a password. Please give a password. Do not forget this password. Passphrase:
No etherbase set and no accounts found as default
- この警告はなんでしょう?デフォルトのアカウントがないよという意味のようですが、何もない状態でアカウントを作成しているから?一旦無視します。
パスワードを聞かれるため入力する
- ※ 注意 パスワードを忘れるとあとで確認する方法がないため、忘れない方法で保管しておきます。
Address: {4e2e0ef96802a54c8e1d5c937e38290c42ced5f0}
- 発行されるとアカウントの公開キーが表示されます。(アカウントのアドレスと呼ばれるものです。)
- アカウントの作成が成功すると作業ディレクトリ配下に
keystore
というディレクトリが作成されています。 - さらにその配下にはUTC-xxxから始まるjsonファイルが作成されています。
root@03be5503f430:/ethereum/eth_private# ls -la total 0 drwxr-xr-x 3 root root 102 Jul 9 03:58 ./ drwxr-xr-x 5 root root 170 Jul 9 03:57 ../ drwx------ 3 root root 102 Jul 9 2017 keystore/ root@03be5503f430:/ethereum/eth_private# ls -la keystore/ total 4 drwx------ 3 root root 102 Jul 9 04:04 ./ drwxr-xr-x 3 root root 102 Jul 9 03:58 ../ -rw------- 1 root root 491 Jul 9 03:58 UTC--2017-07-09T03-58-15.183698355Z--4e2e0ef96802a54c8e1d5c937e38290c42ced5f0
{ "address": "4e2e0ef96802a54c8e1d5c937e38290c42ced5f0", "crypto": { "cipher": "aes-128-ctr", "ciphertext": "ee886a6635bc095337d643de774ec86c38c32016a1f56c5f3d1e9fb092993193", "cipherparams": { "iv": "caa069b4c621db2ddd3c9aa11acd9de2" }, "kdf": "scrypt", "kdfparams": { "dklen": 32, "n": 262144, "p": 1, "r": 8, "salt": "8453090efc0f98075afbb046521273909a3c26d6244e2453b83ddae7d6d0540c" }, "mac": "85249864f73dd75ed70e9e8e2123aa0d10d2c5f73d7bfde510fa547804d092d3" }, "id": "a9d3805d-0a8a-4ef4-a89a-1c0949737951", "version": 3 }
- addressは先程作成したアカウントの公開キーと一致するためこれがアカウント情報のようです。(詳細はここでは追いません)
GenesisBlockの設定ファイルを作成する
{ "config": { "chainId": 13, "homesteadBlock": 0, "eip155Block": 0, "eip158Block": 0 }, "difficulty": "200000000", "gasLimit": "2100000", "alloc": { "4e2e0ef96802a54c8e1d5c937e38290c42ced5f0": { "balance": "100000000" } } }
chainId
: 後に指定するネットワークIDと合わせたIDhomesteadBlock
: 0ブロック以降がHomestead
であるという設定homestead
とは、Ethereumのバージョンのことでこのバージョンの前後で処理が異なるみたいです
eip155Block
: 不明eip158Block
: 不明difficulty
: マイニングの難易度。低ければ低い程、短時間でマイニングできるようです。gasLimit
: トランザクションで指定できる最大gasalloc
: 初期Etherの設定。ここで先程作成したアカウントを指定しています。- 単位は
wei
- 単位は
GenesisBlockを初期化する
geth --datadir /ethereum/eth_private init /ethereum/eth_private/CustomGenesis.json
- ここでもネットワークのディレクトリを指定してます。
INFO [07-09|04:15:31] Allocated cache and file handles database=/ethereum/eth_private/geth/chaindata cache=16 handles=16 INFO [07-09|04:15:31] Writing custom genesis block INFO [07-09|04:15:31] Successfully wrote genesis state database=chaindata hash=6aa7ce…33ec9a INFO [07-09|04:15:31] Allocated cache and file handles database=/ethereum/eth_private/geth/lightchaindata cache=16 handles=16 INFO [07-09|04:15:31] Writing custom genesis block INFO [07-09|04:15:31] Successfully wrote genesis state database=lightchaindata hash=6aa7ce…33ec9a
- 上記のように表示されれば正しく初期化がされています。
root@03be5503f430:/ethereum/eth_private# ls -la total 4 drwxr-xr-x 5 root root 170 Jul 9 04:15 ./ drwxr-xr-x 5 root root 170 Jul 9 03:57 ../ -rw-r--r-- 1 root root 288 Jul 9 04:14 CustomGenesis.json drwxr-xr-x 4 root root 136 Jul 9 04:15 geth/ drwx------ 3 root root 102 Jul 9 04:04 keystore/
root@03be5503f430:/ethereum/eth_private# ll geth/ total 0 drwxr-xr-x 4 root root 136 Jul 9 04:15 ./ drwxr-xr-x 5 root root 170 Jul 9 04:15 ../ drwxr-xr-x 7 root root 238 Jul 9 04:15 chaindata/ drwxr-xr-x 7 root root 238 Jul 9 04:15 lightchaindata/
- geth配下には
chaindata
というディレクトリがあり、ここにはブロックチェーンの起源を表すleveldbデータベースファイルと、それが採掘されてブロックチェーンに追加された後のブロックが含まれているようです。 - つまり新しいブロックが作られるとこのDBにデータが追加されていくようです。
Gethを起動(コンソール)
- Gethを起動しネットワークにattachします。
- Gethには対話式のコンソールが用意されています。このコンソールを起動して色々確認してみます。
geth --networkid 13 --datadir /ethereum/eth_private console 2>> /ethereum/eth_private/geth_err.log
instance: Geth/v1.6.6-stable-10a45cb5/linux-amd64/go1.8.1 coinbase: 0x4e2e0ef96802a54c8e1d5c937e38290c42ced5f0 at block: 0 (Thu, 01 Jan 1970 00:00:00 UTC) datadir: /ethereum/eth_private modules: admin:1.0 debug:1.0 eth:1.0 miner:1.0 net:1.0 personal:1.0 rpc:1.0 txpool:1.0 web3:1.0 >
- networkidには、Genesisファイルで定義したchainIdを指定します。
- errer出力をlogファイルに書き出しています。
アカウントの確認
> eth.accounts ["0x4e2e0ef96802a54c8e1d5c937e38290c42ced5f0"]
- 先程作成したアカウントが確認できます。
etherbase(coinbase)の確認
> eth.coinbase "0x4e2e0ef96802a54c8e1d5c937e38290c42ced5f0"
- etherbase(coinbase)とは、そのノードで発掘を行った時にその報酬を紐付けるEOAのアドレスを指します。
所持Etherの確認
> eth.getBalance("0x4e2e0ef96802a54c8e1d5c937e38290c42ced5f0") 100000000
- 引数にアカウントのaddressを指定します。
- Genesisファイルで初めに指定したEther(単位はwei)を所持していることが確認できます
Gethを起動(JSON-RPC)
- コンソールを使わずにネットワークにattachします。
geth \ --datadir /ethereum/eth_private \ --mine \ --nodiscover \ --maxpeers 0 \ --networkid 13 \ --rpc \ --rpccorsdomain "*"
mine
: マイニングを許可する。このオプションを指定するとマイニングが始まるようです。nodiscover
: Gethはデフォルトで自動的に(同じネットワークID)のEthereumネットワークのノード(Peer)を探し接続を試みます。プライベート・ネットで未知のノードとの接続を避けるため、このオプションを指定することで自動Peer探索機能を無効にします。maxpeers
ネットワークに接続するノードの最大数(デフォルト25) 0を指定するとネットワークが無効になります。networkid
ネットワークID(デフォルト1) 1を指定するとグローバルのネットワークに接続されます。- 2,3,4も予約されているようです。(2=Morden (disused), 3=Ropsten)
rpc
HTTP-RPC serverとして動作を許可するrpccorsdomain
クロスオリジンを許可するリスト
root@03be5503f430:/ethereum/eth_private# geth --datadir /ethereum/eth_private --mine --nodiscover --maxpeers 0 --networkid 13 --rpc --rpccorsdomain "*" INFO [07-09|06:02:23] Starting peer-to-peer node instance=Geth/v1.6.6-stable-10a45cb5/linux-amd64/go1.8.1 INFO [07-09|06:02:23] Allocated cache and file handles database=/ethereum/eth_private/geth/chaindata cache=128 handles=1024 INFO [07-09|06:02:23] Initialised chain configuration config="{ChainID: 13 Homestead: 0 DAO: <nil> DAOSupport: false EIP150: <nil> EIP155: 0 EIP158: 0 Metropolis: <nil> Engine: unknown}" INFO [07-09|06:02:23] Disk storage enabled for ethash caches dir=/ethereum/eth_private/geth/ethash count=3 INFO [07-09|06:02:23] Disk storage enabled for ethash DAGs dir=/root/.ethash count=2 INFO [07-09|06:02:23] Initialising Ethereum protocol versions="[63 62]" network=13 INFO [07-09|06:02:23] Loaded most recent local header number=0 hash=6aa7ce…33ec9a td=200000000 INFO [07-09|06:02:23] Loaded most recent local full block number=0 hash=6aa7ce…33ec9a td=200000000 INFO [07-09|06:02:23] Loaded most recent local fast block number=0 hash=6aa7ce…33ec9a td=200000000 INFO [07-09|06:02:23] Starting P2P networking INFO [07-09|06:02:23] RLPx listener up self="enode://9df46f04a32634b47ce6fe8203b8077029550890d5f118b6ad88aaee15db9d96ac819f119a3997c5b5c9cecff395b866c7c45c39f830ce3111d63fb7c5853bed@[::]:30303?discport=0" INFO [07-09|06:02:23] IPC endpoint opened: /ethereum/eth_private/geth.ipc INFO [07-09|06:02:23] HTTP endpoint opened: http://127.0.0.1:8545 INFO [07-09|06:02:23] Transaction pool price threshold updated price=18000000000 INFO [07-09|06:02:23] Starting mining operation INFO [07-09|06:02:23] Commit new mining work number=1 txs=0 uncles=0 elapsed=178.695µs INFO [07-09|06:02:28] Generating DAG in progress epoch=0 percentage=0 elapsed=3.764s INFO [07-09|06:02:30] Generating DAG in progress epoch=0 percentage=1 elapsed=6.657s
- gethを起動するとしばらく
Generating DAG in progress
というログが流れる。これはDAGという数Gバイトのデータセットを作成しているログのようです。 - これは
Ethash
というProof of Work
を行うための仕組みで使われるもの。詳しくはここを参照。 - DAGはデフォルトで
$(HOME)/.ethash
配下に作成されています。
INFO [07-09|07:09:58] Successfully sealed new block number=1 hash=ef4ec0…62e718 INFO [07-09|07:09:58] 🔨 mined potential block number=1 hash=ef4ec0…62e718 INFO [07-09|07:09:58] Commit new mining work number=2 txs=0 uncles=0 elapsed=431.601µs
- しばらくすると(1,2時間くらい)マイニングが始まりブロックが作成されます
JSON-RPCで通信してみる
クライアントのバージョン(web3_clientVersion)
curl -X POST --data '{"jsonrpc":"2.0","method":"web3_clientVersion","params":[],"id":1}' http://localhost:8545
{"jsonrpc":"2.0","id":1,"result":"Geth/v1.6.6-stable-10a45cb5/linux-amd64/go1.8.1"
Etherの確認(eth_getBalance)
curl -X POST --data '{"jsonrpc":"2.0","method":"eth_getBalance","params":["0x4e2e0ef96802a54c8e1d5c937e38290c42ced5f0", "latest"],"id":1}' http://localhost:8545
{"jsonrpc":"2.0","id":1,"result":"0x5f5e100"}
- resultには16進数の値が入っている。これを10進数に変換すると
100000000
となるので正しい値が入っていることがわかります。
参考
Kerasのサンプルソースが何をやっているか読んでみる
前回は、Kerasのインストールを行い機械学習を行うための環境を構築しました。
しかし、今の状態だと何をどうすればどう使えるのかも全く分からないため、とりあえずKerasのサンプルソースを読んで理解を深めていこうと思います。
その前にニューラルネットワークの前提知識が完全にゼロでソースを理解するのはおそらく難しいだろうなと思い、各所で紹介されている以下の書籍を読みました。
ただし、数学や計算処理については、完全に理解するのは大変そうだったため、難しそうな部分はサラッと流しています。
まずは、なんとなく理解したディープラーニングについて自分なりの解釈をまとめます。
※ これは自分の学習用のまとめになります。 ※ 上記のため単語やアルゴリズムの解釈が間違っている。齟齬がある。場合があるかとおもいます。 ※ 理解がない人が初めて触れた時に噛み砕くための解釈として捉えてもられれば幸いです。
MLPとは何なのか?
多重パーセプトロンの略(Multi-Layer Perceptron)。
パーセプトロンとは複数の信号を入力として受取り一つの結果を返す有るゴリスムのこと。
このパーセプトロンの層を複数に重ねたものを多重パーセプトロン
という。
この多重パーセプトロンをニューラルネットワーク
と表現する。
このニューラルネットワークを利用した学習をディープラーニング
と呼ぶ。
Kerasを使ったディープラーニングの大まかな流れ
① 多重パーセプトロンのための層を構築する
- このサンプルでは
Sequential
というモデルを使って構築する
- このサンプルでは
② モデルの最適化アルゴリズムなどの設定(compile)
③ モデルに学習データを与えて学習させる(fit)
④ 損失値(どれだけ精度がでているか)を計算する(evaluate)
※ ② ~ ④を繰り返し、モデルを最適化(損失値を少なく)する
- ⑤ 実際の予測値を算出する(predict)
大枠こんな感じかと思います。
訓練データとテストデータ
機械学習では、学習
と推論
という2つのプロセスがあります。
学習のプロセスでは予め用意したデータを使ってパラメタをモデルに学習させます。
その時に使うデータとして訓練データというものを用意します。
学習が完了したあとは、モデルに対して推論(実際にある値に対しての分類を抽出する)を行い精度を検証していきます。 上記検証に使うデータをテストデータといいます。
サンプルの読み込み
では、サンプルソースを追っていきたいと思います。
サンプルソースには、reuters_mlp.pyというロイターのニュース・トピック分類
のサンプルを使用しています。
このサンプルを選んだ理由としては、自前のMacでは計算にGPUを使えないこともあり、動かして色々と見て見るにはテキストデータの方が計算が早そうだなと思ったためです。
サンプルデータの説明は、こちらのページに載っています。
'''Trains and evaluate a simple MLP on the Reuters newswire topic classification task. ''' from __future__ import print_function import numpy as np import keras from keras.datasets import reuters from keras.models import Sequential from keras.layers import Dense, Dropout, Activation from keras.preprocessing.text import Tokenizer max_words = 1000 batch_size = 32 epochs = 5 print('Loading data...') # ① サンプルデータの読み込み (x_train, y_train), (x_test, y_test) = reuters.load_data(num_words=max_words, test_split=0.2) print(len(x_train), 'train sequences') print(len(x_test), 'test sequences') # ② ラベルの最大値を求める num_classes = np.max(y_train) + 1 print(num_classes, 'classes') # ③ テキストをベクトル化する print('Vectorizing sequence data...') tokenizer = Tokenizer(num_words=max_words) x_train = tokenizer.sequences_to_matrix(x_train, mode='binary') x_test = tokenizer.sequences_to_matrix(x_test, mode='binary') print('x_train shape:', x_train.shape) print('x_test shape:', x_test.shape) # ④ ラベルをバイナリクラス行列に変換する print('Convert class vector to binary class matrix ' '(for use with categorical_crossentropy)') y_train = keras.utils.to_categorical(y_train, num_classes) y_test = keras.utils.to_categorical(y_test, num_classes) print('y_train shape:', y_train.shape) print('y_test shape:', y_test.shape) # ⑤ モデルを構築する print('Building model...') model = Sequential() model.add(Dense(512, input_shape=(max_words,))) model.add(Activation('relu')) model.add(Dropout(0.5)) model.add(Dense(num_classes)) model.add(Activation('softmax')) # ⑥ 学習の設定 model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy']) # ⑦ 学習の実行 history = model.fit(x_train, y_train, batch_size=batch_size, epochs=epochs, verbose=1, validation_split=0.1) # ⑧ 損失値の計算 score = model.evaluate(x_test, y_test, batch_size=batch_size, verbose=1) print('Test score:', score[0]) print('Test accuracy:', score[1])
① サンプルデータの読み込み
(x_train, y_train), (x_test, y_test) = reuters.load_data(num_words=max_words, test_split=0.2)
- 訓練データとテストデータをKerasが用意しているデータ・セットから読み込んでいます
num_words
で指定しているのは単語の最大数です。このサンプルでは1000個の単語を使用するということになります。test_split
では、使用するデータに中でテストデータとして使う割合です。(20%をテストデータとして使用するということ)- (x_train, y_train), (x_test, y_test)はnumpy.arrayのタプルです
- trainには訓練データ、testにはテストデータが返ってきます
- x_train, x_testには、ニュース文字列のインデックス値(文字列を一意に特定する値)が入っています。
- このサンプルだと
8982
の文字列が格納されています。((*, 8982)の2次元配列ということ) - num_wordsで単語の最大数をしているため各配列に格納されている値は、1~1000の値になります。
[1, 2, 699,・・・ 12]
のようなデータが格納されている
- このサンプルだと
- y_train, y_testには、分類のラベルが格納されています。ラベルとは分類される値(正解のようなもの)が格納されています。
- ラベルはIntegerの値で格納されています
② ラベルの最大値を求める
num_classes = np.max(y_train) + 1 print(num_classes, 'classes')
- ラベルの値で使われている最大値を求めます。
- 後述しますが、このラベルの値はKerasの計算においては、0と1の配列として変換する必要がありその要素数として使われます。
- 1を加算するのは、配列の要素は0から始まるため
③ テキストをベクトル化する
print('Vectorizing sequence data...') tokenizer = Tokenizer(num_words=max_words) x_train = tokenizer.sequences_to_matrix(x_train, mode='binary') x_test = tokenizer.sequences_to_matrix(x_test, mode='binary') print('x_train shape:', x_train.shape) print('x_test shape:', x_test.shape)
- Tokenizerというのは、テキストをベクトル化するクラスです。
- ①で取得した単語データの配列(長さ8982の配列)は可変長のためこれを1次元の配列に変換します。
- 例えば、
3
という数値については、[0, 0, 0, 1]という配列に変換します。 - [1, 2]という配列であれば[0, 0, 1, 1]となります
- 例えば、
- Tokenizerの初期化時に
num_words
というパラメタを渡しています。これはnum_words長の配列に変換するということです。- 配列にはmax_wordsの数値しか格納されていないため、最大長をmax_wordsの配列にすれば、全てのデータが1次元配列に変換できるということです。
x_train shape: (8982, 1000) x_test shape: (2246, 1000)
- printの出力は上記になります。最大長が1000(max_words)の配列に変換されていることがわかります。
④ ラベルをバイナリクラス行列に変換する
y_train = keras.utils.to_categorical(y_train, num_classes) y_test = keras.utils.to_categorical(y_test, num_classes) print('y_train shape:', y_train.shape) print('y_test shape:', y_test.shape)
- ラベルデータについても、③のように0と1の行列データに変換します。(https://keras.io/ja/utils/np_utils/)
num_classes
にはラベルで使用されるデータの最大値が格納されているためnum_classesを要素数とする配列に変換されます。
y_train shape: (8982, 46) y_test shape: (2246, 46)
- printの出力は上記になります。最大長が46(num_classes)の配列に変換されていることがわかります。
⑤ モデルを構築する
print('Building model...') model = Sequential() model.add(Dense(512, input_shape=(max_words,))) model.add(Activation('relu')) model.add(Dropout(0.5)) model.add(Dense(num_classes)) model.add(Activation('softmax'))
- Kerasが用意するSequentialモデルを使用することで複数の層を重ねるモデルが作成できます。
model.add(Dense(512, input_shape=(max_words,)))
- 一番はじめの層で入力する次元数を指定します。
- max_words(=1000)なので1000個の入力を受け取る指定になります
- 第一引数は出力次元数です(512の意味はまだわかってません..)
- Kerasでは2層以降の入力数は指定しなくていいようです。
Activation('relu')
は活性化関数の指定です。(reluはアルゴリズムの方式)Dropout(0.5)
は過学習を防ぐためにニューロンを無効化する処理です。引数はその割合(この場合は半分の値を捨てている)を指します。Dense(num_classes)
で2層目の指定をしています。ここでは出力をnum_classes(=ラベルのクラス数)に指定しています。Activation('softmax')
2層目の活性化関数にはsoftmax
というアルゴリズムを使っています。
⑥ 学習の設定
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
- 層を構築したモデルに対して学習の設定を行います。
loss
は損失関数の指定です。categorical_crossentropy
は交差エントロピー誤差
というアルゴリズムを指定しています。- 損失関数とは、
どれだけ精度が出ているかという指標
として使われる関数で、どれだけ正しくないか?
という値を求めるためのものです。(この結果が小さい=精度が高いと判断できます)
- 損失関数とは、
optimizer
は、パラメタ最適化のための探索アルゴリズムを指定しています。- ニューラルネットワークでは、損失関数ができるだけ小さくなるための最適パラメタを学習によってもとめます。その為に指定するアルゴリズムの手法をここで設定しています。
metrics
は評価を行うためのリストの方式?を指定します。- 一般的に
['accuracy']
を指定する?
- 一般的に
⑦ 学習の実行
history = model.fit(x_train, y_train, batch_size=batch_size, epochs=epochs, verbose=1, validation_split=0.1)
- モデル層の構築、学習の設定から実際に学習を行います。
- 第一引数は、訓練入力データ
- 第二引数は、訓練ラベル
batch_size
は、同時計算するサンプル数- ニューラルネットワークではある一定のデータ量をまとめて処理することができる。まとめて計算することで計算上のコストを削減させている。
epochs
学習のサイクル数verbose
結果の出力validation_split
ホールドアウト検証として使うデータの割合- 標本から一定数の事例を無作為に選択し、残りを訓練事例として使う。
⑧ 損失値の計算
score = model.evaluate(x_test, y_test, batch_size=batch_size, verbose=1) print('Test score:', score[0]) print('Test accuracy:', score[1])
- 学習完了したモデルからテストデータを使って損失値を計算する
- 第一引数は、テスト入力データ
- 第二引数は、テストラベル
- batch_sizeは、同時計算サンプル数(fitと同じ)
- socoreの0番目には損失値(1-損失値)が入っている
- socoreの1番目には、accuracy(精度)が入っている
- ⑥のmodel.compileの
metrics
で指定した値が入る
- ⑥のmodel.compileの
- ※ lossとaccuracyの使い分けがあまりわかっていません。ココらへんもおいおい。
score: 0.889519923517 Test accuracy: 0.793410507569
- printの出力結果は上記になります。結果としてこのモデルの精度は79%となっています。
まとめ(感想)
ざっくりとやっていることを追っていくとなんとなくどんなことをやっているかは理解できました。 ただ、これらを使って自分がやりたい学習をどう実現するか?どのようなモデルを構築すればよいか?などはまだまだ理解できていません。 自分で学習したいデータを作成する方法など、少しずつ勉強していきたいと思います。