これは 電通国際情報サービス アドベントカレンダー の3日目の記事です。 こんにちは。 電通国際情報サービス (ISID) イノベーション ラボの比嘉です。 うちの会社が、テックブログを始めるということなので、僕もブログを再開します。以前は個人のブログだったので、技術以外のエントリもいろいろありましたが、今後は会社のテックブログとしてやっていきます。 ブログでは基本的にEthereumのプログラミングについて書いていく予定です。僕自身、Ethereumについてほとんど知らないので、学んでいく過程をブログに書いていきます。また、できるだけ意図的にエラーを起こし、それを考察していこうと思います。失敗から学べることは多いからね。 Gethとは Gethのインストール MacへのGethのインストール WindowsへのGethのインストール プライベートネットワークへの接続 データディレクトリの作成 Genesisブロックの作成 Gethの起動 Geth attachサブコマンド etherの採掘 etherの送金 Gethとは Ethereumを利用する場合、まずはEthereumのネットワークに参加する必要があります。ネットワークへの参加には、Ethereumクライアントを使います。 Ethereumクライアントの中で公式に推奨されているクライアントがGethです。Gethは プログラミング言語 Goにより実装された CUI クライアントです。Gethを使うことで次のようなことができます。 etherの採掘 etherの送金 スマート・ コントラ クトの生成 Gethのインストール Gethのインストールの公式サイトはこちら Mac と Windows については、載せておきます。 Mac しか試していません。ごめんなさい。 Mac へのGethのインストール Homebrewを使うのが簡単です。Homebrewをインストールしてから次のコマンドを実行してください。 $ brew tap ethereum/ethereum $ brew install ethereum Windows へのGethのインストール インストーラ ーをダウンロードして実行しましょう。 ダウンロードサイトはこちら プライベートネットワークへの接続 Ethereum 本番環境ネットワークにいきなり接続するのは怖いですよね。まだ、Ethereumのこともよく知らないし。まずは、自分だけの閉じたネットワーク(プライベートネットワーク)を作成してそこに接続しましょう。 データ ディレクト リの作成 データを格納する ディレクト リを作って、そこに移動します。例として、private_netという ディレクト リを作ります。 $ mkdir private_net $ cd private_net Genesis ブロックの作成 ブロックチェーン 上の最初のブロックを Genesis ブロックと言います。プライベートネットワークでは、 Genesis ブロックは自分で作成します。 Genesis ブロックを作成するための元情報として、 Genesis ファイルを作成しましょう。 公式サイトのgenesis.json の中身をコピーして、chainIdを任意の正数にします。今回は、1203にします。 いくつかのchainIdは既に使われているので避けましょう。 { " config ": { " chainId ": 1203 , " homesteadBlock ": 0 , " eip150Block ": 0 , " eip155Block ": 0 , " eip158Block ": 0 , " byzantiumBlock ": 0 , " constantinopleBlock ": 0 , " petersburgBlock ": 0 , " istanbulBlock ": 0 , " berlinBlock ": 0 , " londonBlock ": 0 } , " alloc ": {} , " coinbase ": " 0x0000000000000000000000000000000000000000 ", " difficulty ": " 0x20000 ", " extraData ": "", " gasLimit ": " 0x2fefd8 ", " nonce ": " 0x0000000000000042 ", " mixhash ": " 0x0000000000000000000000000000000000000000000000000000000000000000 ", " parentHash ": " 0x0000000000000000000000000000000000000000000000000000000000000000 ", " timestamp ": " 0x00 " } 作成した genesis . json は、データ ディレクト リに保存します。 次のコマンドで、 Genesis ブロックを作成します。 $ geth init genesis.json INFO [11-29|16:32:24.485] Maximum peer count ETH=50 LES=0 total=50 INFO [11-29|16:32:24.499] Set global gas cap cap=25000000 INFO [11-29|16:32:24.499] Allocated cache and file handles database=/Users/higayasuo/Library/Ethereum/geth/chaindata cache=16.00MiB handles=16 INFO [11-29|16:32:24.576] Persisted trie from memory database nodes=0 size=0.00B time="6.87µs" gcnodes=0 gcsize=0.00B gctime=0s livenodes=1 livesize=0.00B Fatal: Failed to write genesis block: database contains incompatible genesis (have d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3, new 5e1fc79cb4ffa4739177b5408045cd5d51c6cf766133f23f7cd72ee1f8d790e0) ログを見ると、 Failed to write genesis block: と処理に失敗しています。--datadirオプションでデータのための ディレクト リを指定していないので、ライブラリがインストールされている方の ディレクト リに書き込んで失敗しているのでしょう。 今度は、--datadirを指定して、geth initを呼び出します。--datadirで指定した ディレクト リにgethとkeystoreの ディレクト リがあれば、初期化はうまくいっています。 $ geth --datadir . init genesis.json 実は、gethを開発モード(--dev)で起動するといろんなものをあらかじめ準備してくれるので、すぐに送金などを試すことができます。しかし、Ethereumをきちんと理解するには、最初は自分で全部やってみるのが良いと思います。 Gethの起動 先ほど、指定したchainIdをnetworkidに指定して、Gethを起動します。 Gethはデフォルトで自動的に同じネットワークIDのノードを探し接続を試みます。今回、そのような動作は必要ありません。--nodiscoverを指定し無効にします。 $ geth --networkid 1203 --nodiscover ログを見てください。 IPC endpoint opened のログが下のほうにあるはずです。 INFO [11-29|17:03:04.231] IPC endpoint opened url=/Users/higayasuo/Library/Ethereum/geth.ipc geth.ipcが先ほど作成したデータ ディレクト リ(private_net)ではなく、$HOME/Library/Ethereumに作成されています。これは、--datadirオプションを指定し忘れたためです。CTRL-Cでプロセスをいったん止め、次のコマンドで再度起動しましょう。 $ geth --networkid 1203 --nodiscover --datadir . IPC endpoint opened のログを探して、データ ディレクト リ(private_net)にgeth.ipcが作成されたことを確認してください。geth.ipcは、今起動したプロセスに、別のGethがコンソールでアクセスするときに使います。 Gethのconsoleサブコマンドでコンソールとして起動することもできますが、お勧めしません。なぜかというと、Gethのいろんなログがコンソールに表示され使いにくいためです。また、コンソールを終了させるとGeth自身も終了してしまいます。Gethはconsoleなしで起動し、Gethを対話的に操作したい時に別のターミナル(プロセス)から geth attach を使って既に起動したGethのコンソールを立ち上げるのがおすすめです。 Geth attachサブコマンド 別のターミナルを立ち上げ、データ ディレクト リ(private_net)に移動しましょう。Gethのattachサブコマンドで、既に立ち上げたGethプロセスのコンソールを立ち上げることができます。 $ geth attach geth.ipc Welcome to the Geth JavaScript console! instance: Geth/v1.9.25-stable/darwin-amd64/go1.15.6 at block: 0 (Thu Jan 01 1970 09:00:00 GMT+0900 (JST)) datadir: /Users/higayasuo/dev/ethereum/private_net5 modules: admin:1.0 debug:1.0 eth:1.0 ethash:1.0 miner:1.0 net:1.0 personal:1.0 rpc:1.0 txpool:1.0 web3:1.0 To exit, press ctrl-d > Gethは プログラミング言語 Goで作られた CUI ですが、コンソールは JavaScript で出来ているようですね。 geth attach に--datadirをつけ忘れているのではと思う方もいらっしゃるかもしれません。これまでつけ忘れていつも失敗していましたからね。でも、たぶん大丈夫。 geth attach は既に起動したGethプロセスのコンソールなので、そちらに--datadirが付いていれば特に問題はないはず。 etherの採掘 何をやるにしてもまずは元手(ether)が必要です。採掘をしてみましょう。まずは、採掘の状態を確認します。 > eth.mining false まだ、採掘は始まっていませんね。minerモジュールを使って採掘を始めましょう。採掘は非同期に行われます。 > miner.start() Error: etherbase missing: etherbase must be explicitly specified at web3.js:6347:37(47) at web3.js:5081:62(37) at <eval>:1:12(3) etherbaseとは採掘の報酬を受け取るアカウントのことです。現在のアカウントの状態を調べてみましょう。 > eth.accounts [] まだ空っぽです。アカウントを作成しましょう。personalモジュールを使います。 > personal.newAccount() Passphrase: Repeat passphrase: "0x29faad1bb68151278c47df617766bf045c9b2b00" 最後に表示された16進数がアカウントのアドレスです。 eth.accounts をチェックしてみましょう。 > eth.accounts ["0x29faad1bb68151278c47df617766bf045c9b2b00"] もう一つ確認しておきたいのが、 eth.coinbase です。coinbaseはetherbaseと同じものです。 > eth.coinbase "0x29faad1bb68151278c47df617766bf045c9b2b00" coinbaseは eth.accounts[0] が自動で設定されます。 miner.setEtherbase() で eth.accounts[0] 以外のアカウントを設定することもできます。試してみましょう。 > personal.newAccount() Passphrase: Repeat passphrase: "0x0577d82aa10fea504320a087f822d8f68899f980" > eth.accounts ["0x29faad1bb68151278c47df617766bf045c9b2b00", "0x0577d82aa10fea504320a087f822d8f68899f980"] > miner.setEtherbase(eth.accounts[1]) true > eth.coinbase "0x0577d82aa10fea504320a087f822d8f68899f980" 二番目のアカウントが eth.coinbase になっていますね。 etherbaseを変更することが確認できたので、元に戻しておきましょう。 > miner.setEtherbase(eth.accounts[0]) true > eth.coinbase "0x29faad1bb68151278c47df617766bf045c9b2b00" etherbaseが設定できたので、採掘を開始しましょうと言いたいところですが、その前にいくつか確認しておきたいことがあります。一つ目は、 eth.blockNumber 。 > eth.blockNumber 0 0といってもブロックがないわけではなく、 Genesis ブロックのblockNumberが0だということです。blockNumberは採掘が進めば増えていきます。ブロックの中身も eth.getBlock() で確認しておきましょう。logsBloomだけ長いの省略しています。 > eth.getBlock(0) { difficulty: 131072, extraData: "0x", gasLimit: 3141592, gasUsed: 0, hash: "0x5e1fc79cb4ffa4739177b5408045cd5d51c6cf766133f23f7cd72ee1f8d790e0", logsBloom: "0x000000000000000000000000000000省略", miner: "0x0000000000000000000000000000000000000000", mixHash: "0x0000000000000000000000000000000000000000000000000000000000000000", nonce: "0x0000000000000042", number: 0, parentHash: "0x0000000000000000000000000000000000000000000000000000000000000000", receiptsRoot: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", sha3Uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", size: 507, stateRoot: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", timestamp: 0, totalDifficulty: 131072, transactions: [], transactionsRoot: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", uncles: [] } これをみても「これが Genesis ブロックなのか」という実感は湧かないと思いますが、こんなものだと思ってください。アカウントの残高も確認しておきましょう。 > eth.getBalance(eth.accounts[0]) 0 > eth.getBalance(eth.accounts[1]) 0 お待たせしました。採掘を開始しましょう。 > miner.start() null > eth.mining true Gethのメインプロセスのログも見てみましょう。モリモリ採掘しているのが分かります。 eth.blockNumber も確認しましょう。 > eth.blockNumber 127 数が増えているので、きちんと採掘されていることが確認できます。アカウントの残高も確認しておきましょう。 > eth.getBalance(eth.accounts[0]) 2.27e+21 > eth.getBalance(eth.accounts[1]) 0 残高の単位はweiといって、1 ether = 1e+18 weiです。今、1 etherは50万くらいなので、 eth.accounts[0] の残高は1億円超えてますね。 etherの送金 etherの送金は、 eth.sendTransaction() を使います。 from に送金元アカウント、 to に送金先アカウント、 value に送金額をweiで指定します。weiよりetherの方が分かりやすいという場合には、 web3.toWei() を使うといいです。次の例では、2 etherをweiに変換しています。 > web3.toWei(2, 'ether') "2000000000000000000" それでは、 eth.accounts[0] から eth.accounts[1] に 2 ether 送金してみましょう。 > tx = {from: eth.accounts[0], to: eth.accounts[1], value: web3.toWei(2, 'ether')} { from: "0x29faad1bb68151278c47df617766bf045c9b2b00", to: "0x0577d82aa10fea504320a087f822d8f68899f980", value: "2000000000000000000" } > eth.sendTransaction(tx) Error: authentication needed: password or unlock at web3.js:6347:37(47) at web3.js:5081:62(37) at <eval>:1:20(4) おや、 Error: authentication needed: password or unlock のエラーが出てしまいました。送金元は、アンロックしておく必要があります。アンロックはデフォルトで5分間持続します。アンロック後、5分以上経過した場合、送金したいなら再度アンロックする必要があります。 personal.unlockAccount() の三番目の引数で、アンロックが持続する時間を秒で指定することもできます。二番目の引数はパスワードなので、パスワードが、見えてしまう問題が生じいまいちですね。 > personal.unlockAccount(eth.accounts[0]) Unlock account 0x29faad1bb68151278c47df617766bf045c9b2b00 Passphrase: true > eth.sendTransaction(tx) "0x5d1a835279c0050037fffce65597afed3dd26ca0f7155662e2239746b08621f0" 最後に表示されたのは、 トランザクション IDです。 eth.getTransaction() で中身を確認してみましょう。 > eth.getTransaction("0x5d1a835279c0050037fffce65597afed3dd26ca0f7155662e2239746b08621f0") { blockHash: "0xe510ee2dc063657b6506b85c78ec361c77d6acc4755b664f2b94faee34b934dd", blockNumber: 4702, from: "0x29faad1bb68151278c47df617766bf045c9b2b00", gas: 21000, gasPrice: 1000000000, hash: "0x5d1a835279c0050037fffce65597afed3dd26ca0f7155662e2239746b08621f0", input: "0x", nonce: 0, r: "0x407f7a5709fc864a8d485cde854c880329475539b773d0e429de0e324f0241c9", s: "0x4998ca73871d2a4dd05374abeb7352f9e8a3c48c1bc288cfe9dbca0ba6206300", to: "0x0577d82aa10fea504320a087f822d8f68899f980", transactionIndex: 0, v: "0x989", value: 2000000000000000000 } from, to, value がきちんと設定されていますね。blockHash, blockNumberが設定されているので、この トランザクション は採掘者によって採掘済みであることがわかります。いつまでたっても、blockHash, blockNumberが設定されない場合は、採掘処理が動いていないかもしれません。 eth.mining で確認して false の場合は、 miner.start() させましょう。 送金先の残高が増えていることを確認しましょう。 web3.fromWei() でweiからetherに単位を変換できます。 > web3.fromWei(eth.getBalance(eth.accounts[1]), 'ether') 2 トランザクション を処理するとき、採掘者に支払われる手数料をgas代といいます。 トランザクション 情報にあったgasの値は、gas代ではありません。 トランザクション 処理時におけるgas使用量の最大値を示しています。 それでは、gas代を知るにはどうしたら良いのでしょうか。送金の時に、送金元の残高から、送金額とgas代が引かれます。そこから計算する必要があります。 eth.accounts[0] は、採掘で常に残高が増え続けているので、gas代を計算するのに向いていません。 eth.accounts[1] から eth.accounts[0] に 1 ether 送金してみましょう。 eth.accounts[1] の現在の残高は、 2 ether ですから 1 ether より少なくなった分がgas代になります。 eth.accounts[1] が送金元になるので、アカウントをアンロックしておく必要があります。失敗から学ぶと言っても同じ失敗を繰り返してはダメですよ。 > personal.unlockAccount(eth.accounts[1]) Unlock account 0x0577d82aa10fea504320a087f822d8f68899f980 Passphrase: true > tx2 = {from: eth.accounts[1], to: eth.accounts[0], value: web3.toWei(1, 'ether')} { from: "0x0577d82aa10fea504320a087f822d8f68899f980", to: "0x29faad1bb68151278c47df617766bf045c9b2b00", value: "1000000000000000000" } > txId2 = eth.sendTransaction(tx2) "0xf9c427c253bfd6980c236a691393e1cc501b1d3d71deaf5ddcf17f2d3fca206f" > txInfo2 = eth.getTransaction(txId2) { blockHash: "0xbf4c0d1ccc2325ad2ada70148b818724119ba582d4f0f765b3f56edc36cabc4f", blockNumber: 6378, from: "0x0577d82aa10fea504320a087f822d8f68899f980", gas: 21000, gasPrice: 1000000000, hash: "0xf9c427c253bfd6980c236a691393e1cc501b1d3d71deaf5ddcf17f2d3fca206f", input: "0x", nonce: 0, r: "0x2703788aaad9dd2ae6d08802ae555cdbdbdd1a6dfc432aa94fd1ccc5f4efe8a0", s: "0x413213c0447df661f8ef204e21654ff41894d219a3181f17cad30895aea4034e", to: "0x29faad1bb68151278c47df617766bf045c9b2b00", transactionIndex: 0, v: "0x98a", value: 1000000000000000000 } > gasFee2 = web3.toWei(1, 'ether') - eth.getBalance(eth.accounts[1]) 21000000000000 > gas2 = gasFee2 / txInfo2.gasPrice 21000 gasFee2が今回の トランザクション のgas代です。gasPriceは1gasあたりの手数料です。gas代をgasPriceで割ると今回、何gas使用したかが分かります。 今回はここまで。Gethを止めておきましょう。コンソールはCTL-D。メインプロセスはCTL-Cで止めます。 次回は、スマート コントラ クトを扱います。 僕の書いたNFT関連の記事 NFT入門 スマートコントラクト入門 執筆: @higa 、レビュー: @sato.taichi ( Shodo で執筆されました )