どうも初めまして。株式会社FOLIOのバックエンドエンジニアkraitaです
この記事はFOLIO Advent Calendar 13日目の記事です。昨日は弊社デザイナーさだこえ氏の、ペアデザイニングによるデザイン作業でした
この記事は自己学習がてら書いている備忘録的な記事です。既にEthereumのチュートリアル記事は吐いて捨てる程あるのでそちらをおすすめします。完全に初心者なため間違ったことを書いていたりしますがその辺は悪しからずコメントにてご指摘ください
また、この記事では触れないですが今はTruffle とかInfuraとかを使って開発した方が楽にできるのでその辺も踏まえて見ていただけたらと思います
Ethereumとは
簡単に言うとBitcoinなどのネットワークと違い自分が書いたプログラムをブロックチェーン上で実行する事が可能なブロックチェーンネットワーク。この特性を活かして自前のトークン発行などが可能なため今話題になっているICOとかはだいたいEthereum上で行われている
Ethereumクライアントについて
Ethereumは特定の実装を指すものではなく、スマートコントラクトの実行基盤なのでスマートコントラクトを作成するためのクライアントが必要です。今のところ有名なのはGeth(Golang), Parity (Rust)のどちらかで一応本家Ethereum推奨らしいGethを今回は使って作成する
Gethのインストール
公式?のDockerイメージ があるのでそれを使ってインストールする
$ docker pull ethereum/client-go
$ docker run -d --name ethereum-node -v /tmp:/root -p 8545:8545 -p 30303:30303 ethereum/client-go --fast --cache=512
※ 詳しくは
Gethをプライベートネットワーク(ローカル開発用)で起動
自前でgenesisファイルを書いてあげる方法もあるが、今回は簡単なテストのため下記のような-dev
を使って起動
$ docker exec -it ethereum-node geth --datadir . --dev --gasprice 1000000
--gaspriceは指定しないとdefaultの0になるみたい
※ 詳しくは
アカウントを作成する
EthereumはEOA(Externally Owned Account)とContract(後述する)の2種類のアカウントが存在している
- EOA: ユーザが使用するもので秘密鍵で管理されている
- Contract: Contractを作ってEthereumブロックチェーン上にデプロイしたら作成される
gethにはgeth console or attach
でREPLみたいなjavascript consoleを使えるのでそれでとりあえずEOAを作成してみる
$ docker exec -it ethereum-node geth attach ipc:geth.ipc ← 起動しているdev用にアタッチ
> eth.accounts ← 作成されているアカウントを確認
["0x08f7a1d8e8261a74929ff2763d83811ec31047c5"] ← coinbase用のアカウントがはじめに作成されている(マイニングしたときはこのアカウントに貯まる)
> eth.getBalance(eth.accounts[0])
1.15792089237316195423570985008687907853269984665640564039457584007913129639927e+77 ← ローカルDEVのため予めEtherを持っている
> personal.newAccount("password01") ← 本来はもっと複雑なパスにする
"0xbbd837411c83b9c208918ea6d504d18740c83344" ← 実際に作成されたアカウントのアドレス
> > personal.newAccount("password02")
"0x79070e0997bcff2dbab68919286a11745cac6b7e"
> eth.accounts
["0x08f7a1d8e8261a74929ff2763d83811ec31047c5", "0xbbd837411c83b9c208918ea6d504d18740c83344", "0x79070e0997bcff2dbab68919286a11745cac6b7e"]
すでに持っているCoinbaseユーザを利用してEtherを送金してみる
// 現在の残高の確認
> eth.getBalance(eth.accounts[0])
1.15792089237316195423570985008687907853269984665640564039457584007913129639927e+77
> eth.getBalance(eth.accounts[1])
0
> eth.getBalance(eth.accounts[2])
0
// 現在のブロック数を確認
> eth.blockNumber
0
// マイニングしている状態か確認 (されてなかった場合はminer.start()する)
> eth.minig
true
// from -> to に指定したユーザへ10etherを送金
> eth.sendTransaction({from: eth.accounts[0], to: eth.accounts[1], value: web3.toWei(10, "ether")})
> eth.getBalance(eth.accounts[1])
10000000000000000000
> eth.blockNumber
1 ← blockが進んでいるのがわかる
次にCoinbaseユーザを使わずに自分で作成したユーザ間で送金してみる
> eth.getBalance(eth.accounts[1])
20000000000000000000
> eth.getBalance(eth.accounts[2])
0
// accounts[1] -> accounts[2] に 1 etherを送金
> eth.sendTransaction({from: eth.accounts[1], to: eth.accounts[2], value: web3.toWei(1, "ether")})
Error: could not decrypt key with given passphrase
at web3.js:3143:20
at web3.js:6347:15
at web3.js:5081:36
at <anonymous>:1:1
※ トランザクションの発行にはアンロックが必要になっている
> personal.unlockAccount(eth.accounts[1], "password01", 0) ← 面倒なので0秒を指定してこのプロセス起動中はずっとアンロック状態にする
true
> eth.sendTransaction({from: eth.accounts[1], to: eth.accounts[2], value: web3.toWei(1, "ether")})
"0x78997de5566251b2f7bb08c480b6501927ac9cc8cef3c8ab251c181ac9cabd72"
> eth.getBalance(eth.accounts[1])
18999622000000000000 ← ここでaccounts[1]の方から手数料を引かれているのがわかる
> eth.getBalance(eth.accounts[2])
1000000000000000000
> eth.blockNumber
3
※ 基本的に送金トランザクションには手数料を支払う必要がある (その手数料はマイナーへ渡される)
ここまでが基本的な動作の検証でこれから実際にEthereum上にデプロイするスマートコントラクトを開発していく
スマートコントラクトとは
大きくはWebアプリケーション開発と変わらなく、開発者がプログラムを書いてそれをEVMコンパイラでコンパイルし、Ethereum上で動くEVM(Ethereum Virtual Machine) バイトコードにしてそれをEthereumブロックチェーン上にデプロイする。
それによって作成されたContractアカウントのアドレスにアクセスすることで実行することができる
開発環境を整える
スマートコントラクトの開発言語は色々あるが今回は一番メジャーなJavascriptライクなSolidityを使用する。また、最新バージョンのGethだとGeth内でsolcコンパイルすることができないので。一番簡単にテストできるRemix(IDE)を使ってブラウザ上でコンパイルしweb3.eth.contractを使ってテストネットワークに反映させる
※ Remix
とりあえずHelloworldしてみる
Remix上で下記コードを入力しコンパイルする
pragma solidity ^0.4.19;
contract HelloWorld {
string private greeting = "HelloWorld";
// ブロックチェーン上で保持しているデータの変更を行わない場合はconstantをつけるみたい
function say() public constant returns (string) {
return greeting;
}
}
Detailsを押してWEB3DEPLOYをコピーしGeth Console上で貼り付ける
その後miningをして(fast環境であれば即時反映)反映後実行することができる
> var browser_helloworld_sol_helloworldContract = web3.eth.contract([{"constant":true,"inputs":[],"name":"say","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"}]);
undefined
> var browser_helloworld_sol_helloworld = browser_helloworld_sol_helloworldContract.new(
... {
...... from: web3.eth.accounts[0],
...... data: '0x60606040526040805190810160405280600a81526020017f48656c6c6f576f726c64000000000000000000000000000000000000000000008152506000908051906020019061004f929190610060565b50341561005b57600080fd5b610105565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f106100a157805160ff19168380011785556100cf565b828001600101855582156100cf579182015b828111156100ce5782518255916020019190600101906100b3565b5b5090506100dc91906100e0565b5090565b61010291905b808211156100fe5760008160009055506001016100e6565b5090565b90565b6101bc806101146000396000f300606060405260043610610041576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063954ab4b214610046575b600080fd5b341561005157600080fd5b6100596100d4565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561009957808201518184015260208101905061007e565b50505050905090810190601f1680156100c65780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b6100dc61017c565b60008054600181600116156101000203166002900480601f0160208091040260200160405190810160405280929190818152602001828054600181600116156101000203166002900480156101725780601f1061014757610100808354040283529160200191610172565b820191906000526020600020905b81548152906001019060200180831161015557829003601f168201915b5050505050905090565b6020604051908101604052806000815250905600a165627a7a72305820b4922a28f478e6244eb47a057a7cfe760ef6c5247b80f8a0058e7c824a07b0890029',
...... gas: '4700000'
...... }, function (e, contract){
...... console.log(e, contract);
...... if (typeof contract.address !== 'undefined') {
......... console.log('Contract mined! address: ' + contract.address + ' transactionHash: ' + contract.transactionHash);
......... }
...... })
null [object Object]
undefined
> null [object Object]
Contract mined! address: 0x6722f363e37f77ebc6d44d30e24af7f68598ef57 transactionHash: 0x2d846cd3c3664353a16fee600c0b2977d999acbc853cde4f3bc48f5d13c904d4
> eth.blockNumber ← マイニングされていれば進んでいる
3
> browser_helloworld_sol_helloworld.say()
"HelloWorld"
簡単なオレオレトークンを作ってみる
pragma solidity ^0.4.19;
contract KraitaToken {
mapping (address => uint256) public balanceOf;
function KraitaToken(uint256 initialSupply) public {
balanceOf[msg.sender] = initialSupply;
}
function transfer(address _to, uint256 _value) public {
require(balanceOf[msg.sender] >= _value); // 送り主の残高残高が十分かどうか確認
balanceOf[msg.sender] -= _value; // 送り主の残高から引く
balanceOf[_to] += _value; // 送り先の残高足す
}
}
※ 計算はオーバーフローしてしまう可能性があるためちゃんとやるなら自分でrequireをちゃんと書くか、SafeMathとかを使ってやったほうが良い
同じようにこのコードをRemixでコンパイルしてデプロイし実行してみる
> var initialSupply = 10000; ← 流通額を10000に設定
undefined
> var browser_kraitatoken_sol_kraitatokenContract = web3.eth.contract([{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"initialSupply","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"}]);
undefined
> var browser_kraitatoken_sol_kraitatoken = browser_kraitatoken_sol_kraitatokenContract.new(
... initialSupply,
... {
...... from: web3.eth.accounts[0],
...... data: '0x6060604052341561000f57600080fd5b60405160208061028b83398101604052808051906020019091905050806000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055505061020d8061007e6000396000f30060606040526004361061004c576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806370a0823114610051578063a9059cbb1461009e575b600080fd5b341561005c57600080fd5b610088600480803573ffffffffffffffffffffffffffffffffffffffff169060200190919050506100e0565b6040518082815260200191505060405180910390f35b34156100a957600080fd5b6100de600480803573ffffffffffffffffffffffffffffffffffffffff169060200190919080359060200190919050506100f8565b005b60006020528060005260406000206000915090505481565b806000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020541015151561014557600080fd5b806000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282540392505081905550806000808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000206000828254019250508190555050505600a165627a7a723058207be5e34a71facd3a26356758a6eabac12f1527bbf0abb10301ff5b739579eb500029',
...... gas: '4700000'
...... }, function (e, contract){
...... console.log(e, contract);
...... if (typeof contract.address !== 'undefined') {
......... console.log('Contract mined! address: ' + contract.address + ' transactionHash: ' + contract.transactionHash);
......... }
...... })
null [object Object]
undefined
> null [object Object]
Contract mined! address: 0x3ae6027aa5e153afa3e6fb2a91fefcb294d035ed transactionHash: 0x7ce8e57560fd20c9a73541a93e638ae20d366537b8fde106d6070ced2a2fd98d
null [object Object]
Contract mined! address: 0xf5dcc916394c3d78fd5b7cd89d58e1e23be6dfbb transactionHash: 0x9b5a60d687f8958654053a47e8a968004de06963eab06692fbfe3718946976d5
実行してみる
> eth.accounts
["0xf3b5f7d9181a6bd60e54b18c96355b9e925188a7", "0x9e1588fd031dfe2f98b0476e43d8c1d904ae1031"]
> browser_kraitatoken_sol_kraitatoken.balanceOf("0xf3b5f7d9181a6bd60e54b18c96355b9e925188a7")
10000 ← coinbaseのアドレスに初期化時に指定した額が入る
> browser_kraitatoken_sol_kraitatoken.balanceOf("0x9e1588fd031dfe2f98b0476e43d8c1d904ae1031")
0
> browser_kraitatoken_sol_kraitatoken.transfer("0x9e1588fd031dfe2f98b0476e43d8c1d904ae1031", 1000)
browser_kraitatoken_sol_kraitatoken.balanceOf("0xf3b5f7d9181a6bd60e54b18c96355b9e925188a7")
8000
> browser_kraitatoken_sol_kraitatoken.balanceOf("0x9e1588fd031dfe2f98b0476e43d8c1d904ae1031")
2000
次は送金トランザクションを受け取りそれに対してトークンを支払うようにする
pragma solidity ^0.4.19;
contract KraitaToken2 {
uint256 public maxToken;
uint256 public kraitaRate;
mapping (address => uint256) public balanceOf;
function KraitaToken2() public payable {
maxToken = 10000000;
kraitaRate = 100000000000000;
}
function() public payable { // payableがついている関数はsendTransactionが呼ばれた時に発火する
uint256 num = msg.value / kraitaRate; // rateで割った数を割り当てる
require(maxToken - num > 0); // 在庫以上に売らないように
balanceOf[msg.sender] += num; // トークンを割り当てる
maxToken -= num; // 在庫から減らす
}
function get(address _address) public view returns (uint256) {
return balanceOf[_address];
}
}
※ 本来はもっと細かいチェックや在庫の割当方法を考えないといけない
今までの手順通りにコードをコンパイルしデプロイし実行してみる
-- デプロイログは割愛 --
> browser_kraitatoken_sol_kraitatoken2.address
"0xc7201abfa08e90f3aebe62f32f723e931dbb2ffa"
// 1ether支払ってみる
> eth.sendTransaction({from: eth.accounts[0], to:"0xc7201abfa08e90f3aebe62f32f723e931dbb2ffa", value: web3.toWei(1,'ether')})
INFO [12-12|02:11:49] Submitted transaction fullhash=0xa5ab5a1aefc6bcd57addd20c64fe5916992d94bc2d22c83a418fd7a4606726a9 recipient=0xe7A8163E49B3f8a6f7ac36146719154f8d624C64
"0xa5ab5a1aefc6bcd57addd20c64fe5916992d94bc2d22c83a418fd7a4606726a9"
> browser_kraitatoken_sol_kraitatoken2.get(eth.accounts[0])
10000 ← accounts[0]に10000トークン発行されたことがわかる
> browser_kraitatoken_sol_kraitatoken2.maxToken()
9990000 ← また在庫からも減っている
> web3.fromWei(eth.getBalance("0xc7201abfa08e90f3aebe62f32f723e931dbb2ffa"), 'ether')
1 ← 1etherがコントラクトのアドレスに支払われている事が確認できる
このように簡単な自作トークンをスマートコントラクト(プログラム)で実現できた
※ 本来はこのあと実際にテストネットやライブネットなどにデプロイする
まとめ
実際にICOや自作コインなどを作ろうとした場合はもう少し機能追加などをしないといけなし、もっとちゃんとやろうとすると色々Ethereum特有のはまりどころとかもでてくるだろうが思っていたより簡単であることがわかった。今回軽く触れてみてある程度Solidityのコードを読めるようになったので、今後は実際にデプロイされているコードなどを読んで勉強したい