電通総研 テックブログ

電通総研が運営する技術ブログ

Geth(ゲス)はじめました

これは電通国際情報サービス アドベントカレンダーの3日目の記事です。 こんにちは。電通国際情報サービス(ISID) イノベーションラボの比嘉です。 うちの会社が、テックブログを始めるということなので、僕もブログを再開します。以前は個人のブログだったので、技術以外のエントリもいろいろありましたが、今後は会社のテックブログとしてやっていきます。

ブログでは基本的にEthereumのプログラミングについて書いていく予定です。僕自身、Ethereumについてほとんど知らないので、学んでいく過程をブログに書いていきます。また、できるだけ意図的にエラーを起こし、それを考察していこうと思います。失敗から学べることは多いからね。

Gethとは

Ethereumを利用する場合、まずはEthereumのネットワークに参加する必要があります。ネットワークへの参加には、Ethereumクライアントを使います。

Ethereumクライアントの中で公式に推奨されているクライアントがGethです。Gethはプログラミング言語Goにより実装されたCUIクライアントです。Gethを使うことで次のようなことができます。

  • etherの採掘
  • etherの送金
  • スマート・コントラクトの生成

Gethのインストール

Gethのインストールの公式サイトはこちら MacWindowsについては、載せておきます。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関連の記事

執筆:@higa、レビュー:@sato.taichiShodoで執筆されました