👏

Ethereumでのスマートコントラクト開発

2022/01/13に公開

Web3.0が各所から聞こえるようになってきて、以前に軽く触った程度の知識でしたが、もう一度掘り起こしてみようと思い、その備忘録として残します。

当時読んでいた本の知識を実際にコードに起こしながらやっているので、書いている内容が古い場合があります。

作ったもの

https://github.com/akihokurino/ethereum-truffle

Etereumに関して

暗号通貨の用途として作られたビットコインに対して、汎用的なアプリケーションを動かすために作られたプラットフォーム。「ワールドコンピューター」とも呼ばれます。ブロックチェーンの構成要素としてはビットコインのものをいくつか踏襲しているが、残高に関するデータ構造が違ったりします。今はビットコインと同じく PoW のコンセンサスアルゴリズムを採用しているが、PoS へ移行中です。

イーサリアムの中で発行されている通貨がEtherで、この通貨自体が他の暗号通貨と同じように価値を持っており、これから説明するスマートコントラクトを動かすためのGasとしても使用されます。

ブロックチェーンに関して

2009年に開始したビットコインで使われている仕組みです。
P2Pプロトコルで非中央集権な分散システムであるにも関わらず、各参加者が非同期的な合意形成を作ることができており(コンセンサスアルゴリズム)、データの改竄なども起こりにくい仕組みになっています。

チェーンという言葉だが、最初のブロック(Genesisブロック)から上にどんどん積み上げているイメージが僕はしっくりきます。(ブロックの位置をブロックHeightとしているので、一般的にもそうなはず)
各ブロックは1つ前のブロックの一方向性の暗号学的ハッシュ値を持っており、過去のブロックが改竄されるとハッシュ値が変わってしまい、改竄された次のブロックも不正になるという感じです。

ブロックの中には複数の検証済みトランザクションが含まれており、この1つ1つのトランザクションが価値の移転を表していたり、イーサリアムだとスマートコントラクトを呼び出す命令になっていたりします。

イーサリアムの場合はトランザクションとは別にStateTreeと呼ばれるアカウントベースの残高管理機構が存在し、そのStateTreeをマークルパトリシアツリーにしたときのマークルルートのハッシュをブロックヘッダに含めています。
つまり、StateTreeの状態が変わればブロックのハッシュ値が変わるので、改竄できないようになっています。
さらに、StorageTreeと呼ばれる状態管理の領域が存在し、これも同じくマークルルートのハッシュをブロックヘッダに含めているので、改竄されないようになっています。

スマートコントラクトとは基本的に関数を通してStateTreeとStorageTreeを更新していくものになります。

ちなみに、ビットコインは残高をUTXOで表現しており、なにか残高を保持している領域があるわけではなく、トランザクションのうち自身がunlocking scriptで解除可能な未使用アウトプット(UTXO)の合算値が残高になります。

コンセンサスアルゴリズムに関して

P2Pの分散システムにおいて、各ノードが有効なブロックチェーン(今後上にブロックを積んでいくべき)を合意できるアルゴリズム。
PoWは「Proof of Work」でそのブロックを作るために正当な量のリソースを消費したという証明です。
具体的には、ブロックのヘッダとnonceと呼ばれる変数を入力に、暗号学的ハッシュ関数に通して結果を取得し、それが特定の値より小さいものを目指す行為です。小さくならなければnonceを変更して再度試行します。この時特定の値より小さくなったnonceが解となり、正当なブロックということになります。(マイニング成功)
これはnonceさえわかれば正当なブロックである検証は1回の計算でできるのに対して、nonceを見つけ出すには多大なリソースを消費しないといけない非対称性なものになっております。

分散システムなので、このマイニングを各参加者が同時に行い、同時に2つ以上のブロックがマイニングされる場合もあります。さらにその2つ以上のブロックが同じブロックを親として(親ブロックのハッシュ値を持つ)ネットワークを流れてくる場合があります。ブロックのフォークといいます。
この場合でもビットコインだと最長ブロック高、イーサリアムだと最も重いブロック(ビットコインとどう違う?)が採用されるルールになっているので、フォークされても最終的には1つのブロックチェーンに収束していきます。その時に一度ブロックチェーンに含まれたものの、自身とは違うブランチが最長になったため破棄されてしまったトランザクションはまた別のブロックに入って最長チェーンに繋がっていくことになります。

チェーンが長くなれば長くなるほど、過去のブロックを改竄することは難しくなります。過去のブロックを改竄するとハッシュ値が変わるので、そのブロックより上に存在する全てのブロックのハッシュ値を再計算する必要があり、現実的ではないリソースが必要になります。さらにそれを頑張っている間に他のノードは今の最長チェーンにブロックを繋げていくので、差を埋めることができず、現実的に最長ブロックになるまで計算しなおすことは確率的に不可能とされています。これによって、P2Pの分散システムにおいて、中央集権が存在しない状態で全参加者が合意をとれ、さらにデータの改竄ができないようになっています。

このPoWですが、問題もあります。単純なハッシュ計算の試行回数がマイニングにとって重要になるので、その行為に特化したハードウェア(ASIC)がたくさん出てきており、電気をかなり消費します。イーサリアムではEthashが採用されており多くのメモリを必要とする計算になっているので、それ用のハードウェアを作ろうとすると高額になりマイニング報酬と見合わなくなる(ASIC耐性)仕組みがありますが、それでも通貨の価値が上がるにつれて競争が激化しています。
そこでイーサリアムではPoSというアルゴリズムに移行予定で、これは競争を激化させることなく、多くのイーサを持っているノードがマイニングしやすくなります。(イーサをたくさん持っている人間がイーサリアムを壊そうとはしないよねという考え?)ノードごとに難易度が可変であるため、マシンパワーに依存した競争になりにくいようですね。

ウォレットに関して

ビットコインやイーサリアムに参加するには公開鍵暗号方式に従って自分自身の秘密鍵と公開鍵を準備するだけです。その鍵を管理するのがウォレットの1つの役割になります。
公開鍵からアドレスを生成します。
HDウォレットなど、秘密鍵から別の秘密鍵or公開鍵を、公開鍵から別の公開鍵を導出できるので、トランザクションごとに1つの公開鍵を使っていくことが推奨されています。(プライバシーの観点から)

ウォレットはさらに自分の秘密鍵で署名済みの正当なトランザクションを生成してネットワークにブロードキャストすることも役割の1つになります。イーサリアムの場合この時に実際にかかるGasやGasLimitを設定します。
GasLimitはチューリング完全なEVMに対して一定の防御の役割があります。
(無限ループなどを中に仕込むことができ、なにも対策がないとシステムが検証中に止まってしまう。そこでGasLimitを設けることで無限ループになっても消費GasがGasLimitを超えたら処理に失敗するようになっている)

まとめると、ウォレットは秘密鍵を管理して自身の資産の管理、トランザクションを発行して資産の移動をおこなったり、スマートコントラクトに対して命令を行ったりします。

スマートコントラクトに関して

この記事の文脈では、イーサリアム上で動作する自動執行プログラムです。
主にSolidityという言語で記述し、EVM(イーサリアムVM)上で動作します。
言語自体は簡単ですが、ブロックチェーン由来の様々なセキュリティや障害時の対応を考える必要があり、Web2.0のアプリケーション開発とはまた違った視点が必要になります。
(例えば、一度デプロイしたコントラクトは変えられないなど)

スマートコントラクトを通して、Etherの送金をしたり独自のトークンを発行してその管理を行ったりできます。

イーサリアムクライアント

Geth (Go)

https://geth.ethereum.org/

Parity (Rust)

https://www.parity.io/technologies/ethereum/

あたりが有名で、今回はGethを利用します。

プライベートネットワークを作ってみる

Gethをインストール

Macの場合はBrewからインストール可能です。

brew tap ethereum/ethereum
brew install ethereum

Genesisブロックを作成

genesis.json

{
  "config": {
    "chainId": 15,
    "homesteadBlock": 0,
    "eip150Block": 0,
    "eip155Block": 0,
    "eip158Block": 0,
    "byzantiumBlock": 0,
    "constantinopleBlock": 0,
    "petersburgBlock": 0,
    "istanbulBlock": 0,
    "berlinBlock": 0
  },
  "nonce": "0x0000000000000042",
  "timestamp": "0x0",
  "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
  "extraData": "",
  "gasLimit": "0x8000000",
  "difficulty": "0x4000",
  "mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000",
  "coinbase": "0x3333333333333333333333333333333333333333",
  "alloc": {}
}

Gethの初期化

geth --datadir ./ init genesis.json

INFO [01-13|15:47:16.981] Maximum peer count                       ETH=50 LES=0 total=50
INFO [01-13|15:47:16.984] Set global gas cap                       cap=50,000,000
INFO [01-13|15:47:16.984] Allocated cache and file handles         database=/Users/akiho/Applications/ethereum-truffle/private-net-geth/geth/chaindata cache=16.00MiB handles=16
INFO [01-13|15:47:17.100] Persisted trie from memory database      nodes=0 size=0.00B time="19.31µs" gcnodes=0 gcsize=0.00B gctime=0s livenodes=1 livesize=0.00B
INFO [01-13|15:47:17.102] Successfully wrote genesis state         database=chaindata hash=7b2e8b..7e0432
INFO [01-13|15:47:17.102] Allocated cache and file handles         database=/Users/akiho/Applications/ethereum-truffle/private-net-geth/geth/lightchaindata cache=16.00MiB handles=16
INFO [01-13|15:47:17.218] Persisted trie from memory database      nodes=0 size=0.00B time="3.673µs" gcnodes=0 gcsize=0.00B gctime=0s livenodes=1 livesize=0.00B
INFO [01-13|15:47:17.219] Successfully wrote genesis state         database=lightchaindata hash=7b2e8b..7e0432

ネットワークの起動

geth --networkid "10" --port 8545 --http --nodiscover --datadir ./ --allow-insecure-unlock console

INFO [01-13|15:47:16.981] Maximum peer count                       ETH=50 LES=0 total=50
INFO [01-13|15:47:16.984] Set global gas cap                       cap=50,000,000
INFO [01-13|15:47:16.984] Allocated cache and file handles         database=/Users/akiho/Applications/ethereum-truffle/private-net-geth/geth/chaindata cache=16.00MiB handles=16
INFO [01-13|15:47:17.100] Persisted trie from memory database      nodes=0 size=0.00B time="19.31µs" gcnodes=0 gcsize=0.00B gctime=0s livenodes=1 livesize=0.00B
INFO [01-13|15:47:17.102] Successfully wrote genesis state         database=chaindata hash=7b2e8b..7e0432
INFO [01-13|15:47:17.102] Allocated cache and file handles         database=/Users/akiho/Applications/ethereum-truffle/private-net-geth/geth/lightchaindata cache=16.00MiB handles=16
INFO [01-13|15:47:17.218] Persisted trie from memory database      nodes=0 size=0.00B time="3.673µs" gcnodes=0 gcsize=0.00B gctime=0s livenodes=1 livesize=0.00B
INFO [01-13|15:47:17.219] Successfully wrote genesis state         database=lightchaindata hash=7b2e8b..7e0432
➜  private-net-geth git:(master)make console
geth --networkid "10" --port 8545 --http --nodiscover --datadir ./ --allow-insecure-unlock console
INFO [01-13|15:52:02.702] Maximum peer count                       ETH=50 LES=0 total=50
INFO [01-13|15:52:02.703] Set global gas cap                       cap=50,000,000
INFO [01-13|15:52:02.703] Allocated trie memory caches             clean=154.00MiB dirty=256.00MiB
INFO [01-13|15:52:02.704] Allocated cache and file handles         database=/Users/akiho/Applications/ethereum-truffle/private-net-geth/geth/chaindata cache=512.00MiB handles=5120
INFO [01-13|15:52:02.845] Opened ancient database                  database=/Users/akiho/Applications/ethereum-truffle/private-net-geth/geth/chaindata/ancient readonly=false
INFO [01-13|15:52:02.847] Initialised chain configuration          config="{ChainID: 15 Homestead: 0 DAO: <nil> DAOSupport: false EIP150: 0 EIP155: 0 EIP158: 0 Byzantium: 0 Constantinople: 0 Petersburg: 0 Istanbul: 0, Muir Glacier: <nil>, Berlin: 0, London: <nil>, Arrow Glacier: <nil>, MergeFork: <nil>, Engine: unknown}"
INFO [01-13|15:52:02.848] Disk storage enabled for ethash caches   dir=/Users/akiho/Applications/ethereum-truffle/private-net-geth/geth/ethash count=3
INFO [01-13|15:52:02.849] Disk storage enabled for ethash DAGs     dir=/Users/akiho/Library/Ethash count=2
INFO [01-13|15:52:02.849] Initialising Ethereum protocol           network=10 dbversion=8
INFO [01-13|15:52:02.854] Loaded most recent local header          number=45 hash=259d95..69585a td=5,930,112 age=3d50m38s
INFO [01-13|15:52:02.855] Loaded most recent local full block      number=45 hash=259d95..69585a td=5,930,112 age=3d50m38s
INFO [01-13|15:52:02.855] Loaded most recent local fast block      number=45 hash=259d95..69585a td=5,930,112 age=3d50m38s
INFO [01-13|15:52:02.862] Loaded local transaction journal         transactions=4 dropped=4
INFO [01-13|15:52:02.863] Regenerated local transaction journal    transactions=0 accounts=0
WARN [01-13|15:52:02.863] Switch sync mode from snap sync to full sync
INFO [01-13|15:52:02.865] Gasprice oracle is ignoring threshold set threshold=2
INFO [01-13|15:52:02.867] Starting peer-to-peer node               instance=Geth/v1.10.15-stable/darwin-amd64/go1.17.5
INFO [01-13|15:52:02.984] New local node record                    seq=1,641,741,809,340 id=fafd437f042803dd ip=127.0.0.1 udp=0 tcp=8545
INFO [01-13|15:52:02.984] Started P2P networking                   self="enode://86a951a016b118f4cb73e5fe7868cb50a733fa886d8663096fcf3f1bf122e272b894b7fb2642e2552234e01ea9a78a7f8f6bcef2e728ccbaa334345965ddf358@127.0.0.1:8545?discport=0"
INFO [01-13|15:52:03.005] IPC endpoint opened                      url=/Users/akiho/Applications/ethereum-truffle/private-net-geth/geth.ipc
INFO [01-13|15:52:03.005] HTTP server started                      endpoint=127.0.0.1:8545 prefix= cors= vhosts=localhost
INFO [01-13|15:52:03.059] Etherbase automatically configured       address=0x9C261FCaf4EF5a73F1b1b09cd1d2274E9bcB84e0
Welcome to the Geth JavaScript console!

instance: Geth/v1.10.15-stable/darwin-amd64/go1.17.5
coinbase: 0x9c261fcaf4ef5a73f1b1b09cd1d2274e9bcb84e0
at block: 45 (Mon Jan 10 2022 15:01:24 GMT+0900 (JST))
 datadir: /Users/akiho/Applications/ethereum-truffle/private-net-geth
 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 or type exit
> INFO [01-13|15:52:05.913] Mapped network port                      proto=tcp extport=8545 intport=8545 interface="UPNP IGDv1-IP1"
  • networkid=自分で決めた数字
  • port=デフォルトで8545になる
  • http=外部からhttpアクセスを許可する
  • nodiscover=起動時にピアを探しにいかない
  • allow-insecure-unlock=アカウントをRPC経由でアンロック状態にする

アカウントを作成する

personal.newAccount("1234")

1234はパスワードです。

アカウントを確認する

eth.accounts
eth.accounts[0]

コインベースアカウントを確認+変更する

eth.coinbase
miner.setEtherbase(eth.accounts[1])

コインベースアカウントとはマイニング報酬を受け取るアカウントです。
デフォルトで最初のアカウントになります。

ブロックの確認

eth.getBlock(1)

{
  difficulty: 131072,
  extraData: "0xd983010a0f846765746888676f312e31372e358664617277696e",
  gasLimit: 134086657,
  gasUsed: 0,
  hash: "0xaef7f171f6cb77f0bad07b91eb20e6acb78a835a85c4361987e0ea760dac355b",
  logsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
  miner: "0x9c261fcaf4ef5a73f1b1b09cd1d2274e9bcb84e0",
  mixHash: "0x9db011d5b7112a0b97f98e6b62d705daa47012a309e47b590991c4df0971b1d7",
  nonce: "0x7d0600c89e8e5d7f",
  number: 1,
  parentHash: "0x7b2e8be699df0d329cc74a99271ff7720e2875cd2c4dd0b419ec60d1fe7e0432",
  receiptsRoot: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
  sha3Uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
  size: 538,
  stateRoot: "0xf79442e1d5d0afadd309c024444aa53d3babfd52d2367306738939887f37bb57",
  timestamp: 1641742394,
  totalDifficulty: 147456,
  transactions: [],
  transactionsRoot: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
  uncles: []
}

マイニングの開始+停止+確認

miner.start(1)
miner.stop()
eth.mining

startに渡している数字はスレッド数です。

残高確認

eth.getBalance(eth.accounts[0])
web3.fromWei(eth.getBalance(eth.accounts[0]), "ether")

扱う通貨の単位はweiで表現されます。かなりの桁数になってわかりにくいので、etherの単位に変換しています。

送金

personal.unlockAccount(eth.accounts[0])
eth.sendTransaction({from: eth.accounts[0], to: eth.accounts[2], value: web3.toWei(5, "ether")})
web3.fromWei(eth.getBalance(eth.accounts[0]), "ether")
web3.fromWei(eth.getBalance(eth.accounts[2]), "ether")

accounts[0]から送金するのでアカウントのロックを外しています。その後、accounts[2]へ5etherを送金します。これでトランザクションは生成されましたがマイニング経由でブロックチェーンの一部にはなっていないので、残高は変わりません。

上記マイニングコマンドを実行後、少し時間が経ってから残高が更新されます。

トランザクション確認

eth.getTransaction("0x9f1df3cd75e273cafb4fc4e6ae774ebc4a4349246737f26d3b40319a657041cc")

{
  blockHash: "0x5bab9872aac39a6a901bad9077d97d141354db14e5925921c17fd5f7410023ad",
  blockNumber: 6,
  from: "0x9c261fcaf4ef5a73f1b1b09cd1d2274e9bcb84e0",
  gas: 21000,
  gasPrice: 1000000000,
  hash: "0x9f1df3cd75e273cafb4fc4e6ae774ebc4a4349246737f26d3b40319a657041cc",
  input: "0x",
  nonce: 0,
  r: "0x1854ef3ac4a37651f7095e5cf32bba3958e5dbc973dd8a6e7b9de04925c31b72",
  s: "0x63b61c42da53f854b0f84f1aff6cc014c30bc0bbc38ff5efd0e8ac91236e12a9",
  to: "0x258c6b909f7cbffa4c121e10309dc0dff9a43309",
  transactionIndex: 0,
  type: "0x0",
  v: "0x41",
  value: 5000000000000000000
}

引数にはトランザクション生成時に作成されるハッシュ値を指定します。
まだブロックに含まれていない場合はblockHashやblockNumberはnullになります。
nullではないので、このトランザクションはすでにブロックチェーンに含まれています。

トランザクションレシートの確認

eth.getTransactionReceipt("0x9f1df3cd75e273cafb4fc4e6ae774ebc4a4349246737f26d3b40319a657041cc")

{
  blockHash: "0x5bab9872aac39a6a901bad9077d97d141354db14e5925921c17fd5f7410023ad",
  blockNumber: 6,
  contractAddress: null,
  cumulativeGasUsed: 21000,
  effectiveGasPrice: 1000000000,
  from: "0x9c261fcaf4ef5a73f1b1b09cd1d2274e9bcb84e0",
  gasUsed: 21000,
  logs: [],
  logsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
  status: "0x1",
  to: "0x258c6b909f7cbffa4c121e10309dc0dff9a43309",
  transactionHash: "0x9f1df3cd75e273cafb4fc4e6ae774ebc4a4349246737f26d3b40319a657041cc",
  transactionIndex: 0,
  type: "0x0"
}

引数にはトランザクション生成時に作成されるハッシュ値を指定します。
詳細な実行結果を表示します。

スマートコントラクトを作ってみる

truffleをいうフレームワークを使います。これでSolidityで書いたコントラクトのコンパイル、テスト、マイグレーション、デプロイ等を行います。コントラクトが実行可能な対話式コンソールも提供されているので迅速に動作確認しながらコントラクトを作成していくことが可能です。
https://trufflesuite.com/

truffleのインストール

npm install -g truffle

npm経由でインストールします。

truffleの初期化

truffle init

雛形のプロジェクトが出来上がります。

コントラクトを作成する

pragma solidity ^0.8.0;

import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol";

contract Token is ERC20 {
    constructor(uint256 initialSupply) ERC20("MyToken", "MT") {
        _mint(msg.sender, initialSupply);
    }
}

今回はERC20に準拠したトークンを作るのですが、様々な雛形を用意してくれているopenzeppelinを利用しているため、ほとんど書くことがありません。openzeppelinは様々なケースに対するスタンダードな方針を提案してくれているので、基本的にはこれに従って(継承して)作っていくほうがよいとされています。
https://openzeppelin.com/

このコード上でやっているのは初期化時にトークンの供給量を決定しているのみです。
あとは送金などにはERC20にあるメソッドをそのまま利用します。

コントラクトをコンパイルする

truffle compile

Compiling your contracts...
===========================
> Compiling ./contracts/Migrations.sol
> Compiling ./contracts/Token.sol
> Compiling openzeppelin-solidity/contracts/token/ERC20/ERC20.sol
> Compiling openzeppelin-solidity/contracts/token/ERC20/IERC20.sol
> Compiling openzeppelin-solidity/contracts/token/ERC20/extensions/IERC20Metadata.sol
> Compiling openzeppelin-solidity/contracts/utils/Context.sol
> Compilation warnings encountered:

    Warning: SPDX license identifier not provided in source file. Before publishing, consider adding a comment containing "SPDX-License-Identifier: <SPDX-License>" to each source file. Use "SPDX-License-Identifier: UNLICENSED" for non-open-source code. Please see https://spdx.org for more information.
--> project:/contracts/Token.sol


> Artifacts written to /Users/akiho/Applications/ethereum-truffle/erc20-token/build/contracts
> Compiled successfully using:
   - solc: 0.8.11+commit.d7f03943.Emscripten.clang

コントラクトのテストを記述する

const Token = artifacts.require("Token");

contract("Token", (accounts) => {
  it("should put 1000 Token in the first account", async () => {
    const tokenInstance = await Token.deployed();
    const balance = await tokenInstance.balanceOf.call(accounts[0]);

    assert.equal(balance.valueOf(), 1000, "1000 wasn't in the first account");
  });
});

コントラクトをデプロイするためのマイグレーションを用意する

const Token = artifacts.require("Token");

module.exports = function (deployer) {
  deployer.deploy(Token, 1000, {
    gas: 2000000,
  });
};

ファイル名を数字から初めて順番を表現します。デフォルトだと 1_initial_migration.js のみが存在するので2からファイル名を定義します。

対話式コンソール上でコントラクトを動作させる

truffle develop

Truffle Develop started at http://127.0.0.1:9545/

Accounts:
(0) 0x290ef15cbfff84fd591b454a4c06d8b487c73321
(1) 0x805c7b08195ed8812dbdbd686f0c85985b4efb15
(2) 0x0867988a97698f0270c6b980964ae5924f2b3099
(3) 0xec4b3d448b873bc3dd4b7b97a6d2822b7ff4490c
(4) 0x2d709c603d4f920409d0a226f5e098a5535b80f9
(5) 0xb1114e0bf0b226ef5db630ec224961fbe34af690
(6) 0xa02d70e7985720e12e65b7ae00d0ffa8d94d5b2f
(7) 0xf310bbd8bd90420ba107f8e3c5b59628d7fb850c
(8) 0xe0d70de2c5b287408e0f093a143abaf58cac1717
(9) 0x11d4472abac38b2e22629cdcdd10d7acabb46ce1

Private Keys:
(0) 4efa31aa6eec89879f0a45ef70997e53c6ca44f77755f78039a52b9ad2e81c1f
(1) a50db2aecd5192093a087b5c1e20829886495afaff5afd74295c9a17f4f4300f
(2) c193065a918334b1d88a6574e44f0be99eff7cd6fc8a1bd574dad87f048dd4d4
(3) d813fb47f8c4924ef5248b4d8b3e44ebecf18a439f8c5942c17828e1ee493e38
(4) f775415c5a5ca4ac46118de196c754e52102f3400afc30d01dbd1e9394db4ed8
(5) 63bd025b92d62cbbda8e7de7863ec3b7771d3cde423d4ef96bade4f0e8097c1b
(6) 359bb1a1a76d94c2a87964b406f40e6d2c0568247759cada425c7848d5434409
(7) 4c8b5f0a69c12c98bc0813e082823979268e18d16f7ad5667d5b3a1832516642
(8) 6ecb1bd5058b8c185f6f135446e4a4f0a9a35193de104ba0cef31005d41f17ff
(9) 206c22ec5b4991ddd106c0b45a2156f95283f0ff25752e514e90ca2072c157d8

Mnemonic: mercy toast perfect best chief key pave matrix picture worth baby coconut

テストを実行する

上記コンソール上で

truffle(develop)> test

Using network 'develop'.


Compiling your contracts...
===========================
> Everything is up to date, there is nothing to compile.



  Contract: Token
    ✓ should put 1000 Token in the first account (42ms)


  1 passing (127ms)

マイグレーションを実行する

上記コンソールの中で

truffle(develop)> migrate

Starting migrations...
======================
> Network name:    'develop'
> Network id:      5777
> Block gas limit: 6721975 (0x6691b7)


1_initial_migration.js
======================

   Deploying 'Migrations'
   ----------------------
   > transaction hash:    0x771796b5776bafb3b2af9b2ccab74df704a1a91790659c9f6a44ee86da90be21
   > Blocks: 0            Seconds: 0
   > contract address:    0x341DbEb118F537049B11E57DA6325cc67e56CD48
   > block number:        1
   > block timestamp:     1642058736
   > account:             0x290Ef15cbfFf84Fd591b454a4c06D8b487C73321
   > balance:             99.999502316
   > gas used:            248842 (0x3cc0a)
   > gas price:           2 gwei
   > value sent:          0 ETH
   > total cost:          0.000497684 ETH


   > Saving migration to chain.
   > Saving artifacts
   -------------------------------------
   > Total cost:         0.000497684 ETH


2_deploy_token.js
=================

   Deploying 'Token'
   -----------------
   > transaction hash:    0x2b43a74c93dc7479275e449b7ebdada97eb5bc39bb09e673e4a583cdd2c26d33
   > Blocks: 0            Seconds: 0
   > contract address:    0xb0cAe6F605c840F35361Fdb70daB50377D94E224
   > block number:        3
   > block timestamp:     1642058737
   > account:             0x290Ef15cbfFf84Fd591b454a4c06D8b487C73321
   > balance:             99.99693663
   > gas used:            1240330 (0x12ed0a)
   > gas price:           2 gwei
   > value sent:          0 ETH
   > total cost:          0.00248066 ETH


   > Saving migration to chain.
   > Saving artifacts
   -------------------------------------
   > Total cost:          0.00248066 ETH


Summary
=======

デプロイされたコントラクトを確認する

truffle(develop)> let t = await Token.deployed()
undefined
truffle(develop)> t.name()
'MyToken'
truffle(develop)> t.totalSupply()
BN {
  negative: 0,
  words: [ 1000, <1 empty item> ],
  length: 1,
  red: null
}

無事にデプロイされているようです。

トークン上の残高を確認する

truffle(develop)> let t = await Token.deployed()
undefined
truffle(develop)> let accounts = await web3.eth.getAccounts()
undefined
truffle(develop)> let balance = await t.balanceOf(accounts[0])
undefined
truffle(develop)> balance.toNumber()
1000

トークン上で送金する

truffle(develop)> let t = await Token.deployed()
undefined
truffle(develop)> let accounts = await web3.eth.getAccounts()
undefined
truffle(develop)> await t.transfer(accounts[1], 100)
{
  tx: '0xb879850b6d8b62ea1d4d786cb78c17793e5c76029ba4366135674acc33b0e9e7',
  receipt: {
    transactionHash: '0xb879850b6d8b62ea1d4d786cb78c17793e5c76029ba4366135674acc33b0e9e7',
    transactionIndex: 0,
    blockHash: '0x86f022f882ba03133858533c92b61fcecae1635422656eebcf36007986adf4cb',
    blockNumber: 5,
    from: '0x290ef15cbfff84fd591b454a4c06d8b487c73321',
    to: '0xb0cae6f605c840f35361fdb70dab50377d94e224',
    gasUsed: 51820,
    cumulativeGasUsed: 51820,
    contractAddress: null,
    logs: [ [Object] ],
    status: true,
    logsBloom: '0x00000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000002800000000000020000000000000000000008000000000000000000008000000000000000000000000000000000000000000010000000020000002000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
    rawLogs: [ [Object] ]
  },
  logs: [
    {
      logIndex: 0,
      transactionIndex: 0,
      transactionHash: '0xb879850b6d8b62ea1d4d786cb78c17793e5c76029ba4366135674acc33b0e9e7',
      blockHash: '0x86f022f882ba03133858533c92b61fcecae1635422656eebcf36007986adf4cb',
      blockNumber: 5,
      address: '0xb0cAe6F605c840F35361Fdb70daB50377D94E224',
      type: 'mined',
      id: 'log_d0096be4',
      event: 'Transfer',
      args: [Result]
    }
  ]
}

先程のGethを使ったプライベートネットワークと違い、開発用のコンソールなので、トランザクションは即座にブロックに取り込まれます。

truffleで作成したコントラクトをGethで作成したプライベートネットワーク上で稼働させる

trrufle-config.js

module.exports = {
  networks: {
    development: {
      host: "127.0.0.1",
      port: 8545,
      network_id: "10",
    },
  },
  mocha: {},
  compilers: {
    solc: {
      version: "0.8.11",
    },
  },
};

必要なところのみ抜粋していますが、networkの設定で自身のGethのネットワークを指定します。この時にGeth側でhttpのパラメータを指定しておかないと接続できないので注意です。
あとは、

geth --networkid "10" --port 8545 --http --nodiscover --datadir ./ --allow-insecure-unlock console

で別プロセスでGethを起動させておき、

truffle migrate --network development
truffle console --network development

こうすることで先ほどと同じ対話式コンソールの中でコントラクトを操作できます。今度はGeth側でマイニングをしておかないと生成したトランザクションはいつまでもブロックに含まれないです。

truffleで作成したコントラクトをテストネットワーク上で稼働させる

イーサリアムブロックチェーンは大きく3つのネットワークがあります

  • メインネット
    本番環境
  • テストネット
    世界中の人が使えるステージング環境
    いくつかの種類がある
    ここでのetherは価値がなく、気軽にfaucetからetherをもらうことができる
  • プライベートネット
    先ほど作成した自身のローカルに閉じたネットワーク

ここではテストネットの1つであるRopstenにコントラクトをデプロイしようと思います。

Metamaskを利用してRopstenに接続し、そこでのetherを手に入れる

https://chrome.google.com/webstore/detail/metamask/nkbihfbeogaeaoehlefnkodbefgpgknn?hl=ja

今回はChrome拡張のウォレットを利用します。

上記の通り、自身のMetamaskをRopstenに接続しておきます。この状態で

ここからfaucetの1つに飛びそこでetherを送ってもらいます。

Ropstenにコントラクトをデプロイして動作確認

https://infura.io/

簡単にデプロイできるサービスがあるのでこれを利用します。
このサービス上でプロジェクトを作成し、そこのプロジェクトIDを控えておきます。
先ほどプライベートネットにデプロイした時と同じように

trrufle-config.js

const HDWalletProvider = require("@truffle/hdwallet-provider");

const fs = require("fs");
const mnemonic = fs.readFileSync(".secret").toString().trim();

module.exports = {
  networks: {
    ropsten: {
      provider: () =>
        new HDWalletProvider(
          mnemonic,
          `https://ropsten.infura.io/v3/{PROJECT_ID}`
        ),
      network_id: 3,
      gas: 5500000,
      confirmations: 2,
      timeoutBlocks: 200,
      skipDryRun: true,
    },
  },
  mocha: {},
  compilers: {
    solc: {
      version: "0.8.11",
    },
  },
};

このようにRopsten接続用の設定を追加し、

truffle migrate --network ropsten
truffle console --network ropsten

同じように操作することが可能です。

この時、コード上でも書いていますが、Metamaskで設定したニーモニックが必要になります。
ニーモニックは他人と共有してはいけないものなので、 .secret から読み込むようにしてこれ自身はGitなどで管理しないように気をつけましょう。

まとめ

以上で簡単にはなりますが、基本的なブロックチェーンの仕組みからGethを使ったプライベートチェーンの作成、truffleを使ったスマートコントラクトの管理、metamaskやinfuraを使ったテストネットワークへのデプロイを説明しました。

上記は下記に載せる本に書いてある内容を最新版で動くように書き直したものになりますので、さらに詳しく学びたい方は本を読むことをお勧めします。

個人的にはweb3.jsのRust版のようなrust-web3が気になっていて、これで遊んでみようかなと思っています。
https://github.com/tomusdrw/rust-web3

参考資料

マスタリングビットコイン
https://www.amazon.co.jp/ビットコインとブロックチェーン-暗号通貨を支える技術-アンドレアス・M・アントノプロス/dp/4757103670

ブロックチェーンアプリケーション開発の教科書
https://www.amazon.co.jp/ブロックチェーンアプリケーション開発の教科書-加嵜-長門/dp/4839965137

仮想通貨の教科書
https://www.amazon.co.jp/仮想通貨の教科書-アーヴィンド・ナラヤナン/dp/4822285456/ref=sr_1_1?adgrpid=52987812413&gclid=Cj0KCQiAuP-OBhDqARIsAD4XHpcQHirWecKxf-OO0payCFmxOwov8Oehabe8S51_6lOG2Q3V7xBzyT4aAnrtEALw_wcB&hvadid=553962270221&hvdev=c&hvlocphy=1009300&hvnetw=g&hvqmt=e&hvrand=12089394769359419971&hvtargid=kwd-335137990548&hydadcr=16038_13486739&jp-ad-ap=0&keywords=仮想通貨の教科書&qid=1642061356&sr=8-1

マスタリングイーサリアム
https://www.amazon.co.jp/マスタリング・イーサリアム-―スマートコントラクトとDAppの構築-Andreas-M-Antonopoulos/dp/4873118964

Discussion