はじめに
Pythonからイーサリアムのスマートコントラクトを実行する方法について書きます。
少しでもWeb3に興味を持ってくれるPythonistaが増えると嬉しいです。
※今回使用したソースコードはこちらにあります。
スマートコントラクトの開発手順
スマートコントラクトのプログラムを開発するステップはコーディング、コンパイル、テスト環境へのデプロイ、テストという流れになります。本番へのデプロイは分散したノードにそれぞれデプロイされてしまうため、コントラクトは一般的には修正が行えません。そのためにも開発時は何度も修正して品質が上げれる効率のよい開発環境があるとベストです。
スマートコントラクト用の開発環境「Remix」
イーサリアムのスマートコントラクトで一番有名なのはSolidityプログラミング言語です。ここではSolidityの開発環境を紹介します。いくつかある中で一番簡単なのものはブラウザから開発が行えるRemixです。Remixの中にもいくつか開発環境の種類があります。その中のブラウザ版(Remix Online IDE)もかなり機能が充実しています。このブラウザ版を使えばパソコンには何もインストールする必要がありません。それでコーディングからコンパイル、デプロイ、テストの全て行うことができます。さらにデフォルトのワークスペースには簡単なコントラクトのサンプルコードが3つ入っています。
今回はこのサンプルのスマートコントラクトを使って、Pythonから呼びだしたいと思います。
まずブラウザ版のRemix を開くと、以下の様な画面が表示されます。
コーディング方法と3種類のサンプルコード
スマートコントラクトをコーディングするにはRemixの左メニューの上から2番目にあるマークの「File Exploror」をクリックします。そうするとファイル一覧リストが表示されるので、早速サンプルコードを一つ開いてみましょう。「contracts」→「1_Storage.sol」の順にクリックすると該当のファイルが開きます。
3つのスマートコントラクトのサンプルコードを覗いてみましょう。各ファイルには、それぞれ以下の処理が書かれています。
スマートコントラクトの詳しい解説はここではしませんが、プログラミング自体は、クラスに近いので、オブジェクト指向に慣れてるエンジニアなら理解できる思います。
1.Storageコントラクト(1_Storage.solファイル)
数値データの格納を行うだけの非常にシンプルなコントラクトです。
- データを格納する(store)
- データを取得する(retrieve)
pragma solidity >=0.7.0 <0.9.0;
contract Storage {
uint256 number;
// データを格納する(store)
function store(uint256 num) public {
number = num;
}
// データを取得する(retrieve)
function retrieve() public view returns (uint256){
return number;
}
}
Ownerコントラクト(2_Owner.solファイル)
オーナーを管理するコントラクトです。
- 初期処理:コントラクトのインスタンスを作成した人がオーナーとなります。
- オーナーを変更する(changeOwner)
- 現在オーナー以外が変更することはできません。
- オーナーが変更されるとイベント(OwnerSet)を発行します。
- オーナーを確認する(getOwner)
pragma solidity >=0.7.0 <0.9.0;
import "hardhat/console.sol";
contract Owner {
address private owner;
// オーナー変更通知のイベント
event OwnerSet(address indexed oldOwner, address indexed newOwner);
modifier isOwner() {
require(msg.sender == owner, "Caller is not owner");
_;
}
// コンストラクタ(初期処理)
constructor() {
console.log("Owner contract deployed by:", msg.sender);
owner = msg.sender;
emit OwnerSet(address(0), owner);
}
// オーナーを変更する
function changeOwner(address newOwner) public isOwner {
emit OwnerSet(owner, newOwner);
owner = newOwner;
}
// オーナーを確認する
function getOwner() external view returns (address) {
return owner;
}
}
Ballotコントラクト(3_Ballot.solファイル)
投票を管理するコントラクトです。
- 初期処理:コントラクトのインスタンスを作成した人には最初から投票権が付与されます。
- 投票権を付与する(giveRightToVote)
- 投票する(vote)
- 一人一票しか投票(委任投票込み)できません
- 委任投票する(delegate)
スマートコントラクトのコンパイル方法
先程の「File Exploror」でいずれかのスマートコントラクトファイルを選択している状況で、今度は左にある上から4番目のマークの「Solidity Compiler」をクリックします。するとコンパイルメニューが表示され。該当のファイルをコンパイルできるようになります。早速「Compile 1_Storage.sol」ボタンをクリックして下さい。
もしコンパイルエラーがあると、赤字でエラー内容が表示されますが、エラーがなければ特になにも表示されずに「Solidity Compiler」メニューにチェックがマークが付きます。さらにコンパイル設定のした方法に「ABI」をいうマークが表示されており、ここをコピーするとスマートコントラクトのインタフェース定義のjsonがクリップボードにコピーされます。あとでPyhonにこの内容を貼り付ける必要があるので、ここでコピーできることを覚えていて下さい。
スマートコントラクトのデプロイ・動作確認
続いてデプロイですが、今度は左メニューにある上から5番目のマークの「Deploy」をクリックします。するとデプロイメニューが表示されます。一番上のENVIREMENTに「Remix VM(London)」がデフォルトで選択されていますが、これはRemix社が提供しているサンドボックステストネット環境です。自分でブロックチェーンネットワークを準備しなくても使えるので非常に便利ですが、ブラウザを立ち上げ直すたびに初期化されるのできちんとしたテストを行うには若干不便です。でもまずはRemixVM環境を選んだままで「デプロイ」ボタンをクリックしてみて下さい。
正常にデプロイが行われると下にある「Deployed Contracts」にコントラクトが追加されます。
デプロイされたコントラクトはコントラクタインスタンスと呼ばれそれぞれにインスタンアドレスが付与されます。ここでは「STORAGE」コントラクタインスタンスが生成され0X08B...33FA8のコントラクトアドレスが表示されたことを表示しています。右にコントラクトアドレスをコピーできるボタンがついてますが、Pythonコードにこのアドレスを貼り付ける必要があるので、ここも覚えておいて下さい。
続いては、実際にコントラクトインスタンスをテストしてみましょう。
"retrieve"ボタンをクリックすると0が表示されます。まだ何も設定していないので初期値0のままでこの動きで正常です。
続いて、"store"の右になる入力ボックスに123456789を設定して、"store"ボタンをクリックします。
スマートコントラクトは読み込みはコストがかかりませんが、先程のインスタンスのデプロイ
"retrieve"ボタンをクリックすると、先程設定された123456789が表示されます。
上記のような流れて、Remixを使うと簡単にSolidityのコーディングからコンパイル、デプロイ、動作確認までを行うことができます。
Ethereumのローカルブロックチェーン環境「Ganache」
Ganacheとは
Ganacheは、Ethereumのブロックチェーンをローカル環境に構築するためのツールです。 Ganacheを使用することで、開発者は簡単にEthereumのブロックチェーンを構築し、スマートコントラクトを実行したり、トランザクションを実行したりすることができます。 Ganacheは、簡単に使用できるWebインターフェースを提供するほか、コマンドラインインターフェースも提供しています。 Ganacheを使用することで、Ethereumブロックチェーンを使用するアプリケーションを簡単に開発し、テストすることができます。
Ganacheのインストール
Ganacheのインストールはインストーラーを動かすだけなので非常に簡単です。
Ganacheは、Windows、Mac、およびLinux用に提供されています。Ganacheをインストールするには、次の手順に従います。
Ganacheの公式サイトから、Ganacheの最新版をダウンロードします。
ダウンロードしたインストーラーを実行し、Ganacheをインストールします。
Ganacheを起動します。初回起動時は、Ganacheの設定画面が表示されますので、必要に応じて設定を変更します。
Ganacheが起動したら、「Quickstart」タブをクリックします。
Ganacheのインストールは以上です。詳細な手順や使い方については、Ganacheの公式ドキュメントをご覧ください。
RemixからGanacheに接続する
Remixは自体非常に便利ですが、Remixによって提供されるイーサリアムブロックチェーンネットワークのサンドボックスが毎回初期化されるため、テストなどでアカウントアドレスやコントラクトアドレスを更新するのが大変です。なのでGanache を起動して、RemixからGanacheへ接続したいのです。Ganacheのワークスペースにはデフォルトで10ユーザー分のアカウントアドレスが用意されています。それぞれのユーザーは100ETHづつ保有しています。これらはオプション設定で変更することも可能です。
続いてRemixのデプロイ画面のENVIREMENTのコンボボックスをドロップダウンします。すると、その中にはGanache Providerがあるのでこれをクリックします。
Ganacheの画面上の中央付近にはRPC SERVERと書かれたhttpのアドレスとポートが表示されています。
おそらくデフォルトでは"HTTP://120.0.0.1:7545"と書かれています。これを控えておきましょう。
Remix画面でGanashe Provideを設定する画面があり、一番下に「Ganache JSON-PRC Endpoint:」を設定する入力ボックスがあるので、先程の設定に変更します。8545のところを7545に変更することになります。
ここでの注意としては、Ganashe側の表示ではhttpの部分が大文字で"HTTP"と書かれているため、そのままコピペするとエラーになってしまうのでご注意下さい。
OKボタンを押してエラーにならなければGanacheに接続されている状態になります。
アカウントアドレスの一覧を見るとGanacheのものと同じものが表示されています。
これでRemixとGanacheの連携は完了です。
Web3.py
Web3.pyは、Python用のEthereum Web3 APIのライブラリです。このライブラリを使用すると、PythonプログラムからEthereumのブロックチェーンと通信し、スマートコントラクトを実行することができます。また、Web3.pyを使用することで、Ethereum上のトランザクションやアカウントの情報を取得したり、スマートコントラクトをデプロイしたりすることができます。 Web3.pyを使用するには、PythonプログラムとEthereumノードの間でJSON-RPC接続を構築する必要があります。
ライブラリのインストール
Web3.pyのインストール方法は以下の通りです。
$ pip install web3
Web3への接続に関する情報を書く。
Web3.HTTPProviderでGanacheによってローカル上に起動しているブロックチェーンネットワークに接続します。PRC NETWORKの設定を指定します。
また、テストに使うユーザーのアカウントアドレスと秘密鍵(private key)の情報も取得しましょう。Ganacheはデフォルトで10ユーザーのアカウントを作成し、それぞれ100.00イーサづつ持つ状態になっています。それぞれの秘密鍵を確認するには各ユーザーの右側にある「show keys」ボタンをクリックします。
アカウントアドレスと秘密鍵の情報をコピーしてプログラムに書きましょう。本体はconfigファイルでプログラム外に持つのが望ましいですが、ここでは説明のために直接かいています。
上記の2つを書くと以下の様なコードになります。
from web3 import Web3
account_1 = '0x2044E11CeeBA8dbeaFaA37f61c9F7EeCa994b34b'
account_2 = '0xD70C23F6f2b174d714c52399AbE7C86331dfdBe8'
account_3 = '0x350d426eA41051ab33e543a5826490e0DdE3c89a'
private_key_1 = '0x636dad935aca38288fb3f6262d945ab0a0802d7afb902774ace597024f2fa9c8'
private_key_2 = '0x32981336a053501949a5415a5f31fc67ff71772bc9f10855ac5c3ccc53c169a3'
private_key_3 = '0xc4aeb0559cc91d6f1ec5477b388569d839db98b9ea05baded4b53b9b99a3409f'
# Ganacheに接続する
w3 = Web3(Web3.HTTPProvider('http://127.0.0.1:7545'))
w3.isConnected()
スマートコントラクトへ接続する。
続いてはスマートコントラクトへ接続するための情報です。
まずはRemixのABIをコピーしてPythonコードに貼り付けます。
コンパイラ画面の下の方に、小さく「ABI」というボタンがあるのでクリックするとjson情報がコピーされるので、コードに貼り付けます。
続いて、デプロイ先がGanacheに変更されたので、Remixのデプロイメニューで新しい方(下に追加されていきます)のコントラクトアドレスをコピーします。
Pythonコードには、以下の様にabiとcontract_addressへ貼り付けます。
# ここにRemixでコンパイルしたABIのコピーを貼り付ける
abi='''
[
{
"inputs": [
:
(省略)
:
"type": "function"
}
]
'''
# ここにRemixでデプロイしたコントラクトのアドレスを貼り付ける
contract_address='0x3fc44cd135BD12c4627085C4f6674A668187F5A2'
contract_instace = w3.eth.contract(address=contract_address,abi=abi)
ここまで書いた時点で動かしてみてエラーが出なければ、Web3に接続され、ノードからスマートコントラクトにも接続ができています。
サンプルコード1の動作確認
続いては、サンプルコードを順番に動かしていきましょう。
こちらではデータを参照したり、格納するだけのスマートコントラクトを動かしています。
参照だけしてるretrieve(取り出す)メソッドの呼び出しは簡単ですが、スマートコントラクトにデータを書き込むstore(格納する)メソッドを動かすにはガス代という手数料がかかるので、トランザクションとして実行する必要があります。これはパターンとして決まっているので、そういうものだとさえ覚えてさえいただければ問題ないかと思います。実行するスマートコントラクトのコード量や、ブロックチェーンネットワークの負荷が高い場合には、より多くのガス代が必要となるので、本番では事前にガス代を見積もったり、リトライを入れるなどの処理などが必要な場合があります。
# Storageコントラクトのretrieve(取り出す)メソッドの呼びだし
print("初期値:retrieve : {}".format(contract_instace.functions.retrieve().call()))
# Storageコントラクトのstore(格納する)メソッドの呼びだし
txn = contract_instace.functions.store(123456789).buildTransaction({
'gas': 70000,
'gasPrice': w3.toWei('1', 'gwei'),
'from': account_1,
'nonce' : w3.eth.getTransactionCount(account_1),
})
signed_txn = w3.eth.account.signTransaction(txn, private_key=private_key_1)
w3.eth.sendRawTransaction(signed_txn.rawTransaction)
# Storageコントラクトのretrieve(取り出す)メソッドの呼びだし
print("変更後:retrieve : {}".format(contract_instace.functions.retrieve().call()))
【結果】
初期値:retrieve : 0
変更後:retrieve : 123456789
サンプルコード2の動作確認
続いてサンプルコードはオーナーを管理するスマートコントラクトを動かします。
自分がオーナーの場合は、別のアカウントをオーナに変更することができます。
# 現在のオーナーのアカウントアドレスを確認する。
print("オーナー : {}".format(contract_instace.functions.getOwner().call()))
# 現在のオーナーを変更する。
try:
txn = contract_instace.functions.changeOwner(account_2).buildTransaction({
'gas': 70000,
'gasPrice': w3.toWei('1', 'gwei'),
'from': account_1,
'nonce': w3.eth.getTransactionCount(account_1),
})
signed_txn = w3.eth.account.signTransaction(txn, private_key=private_key_1)
w3.eth.sendRawTransaction(signed_txn.rawTransaction)
except Exception as e:
print('例外です')
print(e)
# 新しいオーナーのアカウントアドレスを確認する。
print("オーナー : {}".format(contract_instace.functions.getOwner().call()))
【結果】
最初のオーナー (アカウントアドレス): 0xb50dbED82B48F3Db24976C584183f9e4Aebe4235
新しいのオーナー (アカウントアドレス): 0x1367961482526AE5504441ee5e247B0EAe6AD21D
今度は自分がオーナーではないのにオーナーを変更するとエラー(revert Caller is not owner)になることを確認しましょう。
期待通りのエラー処理をしました。
# 前のオーナーが、オーナーを自分に変更する。
try:
txn = contract_instace.functions.changeOwner(account_1).buildTransaction({
'gas': 70000,
'gasPrice': w3.toWei('1', 'gwei'),
'from': account_1,
'nonce': w3.eth.getTransactionCount(account_1),
})
signed_txn = w3.eth.account.signTransaction(txn, private_key=private_key_1)
w3.eth.sendRawTransaction(signed_txn.rawTransaction)
except Exception as e:
print('例外です')
print(e)
【結果】
例外です
{'message': 'VM Exception while processing transaction: revert Caller is not owner', 'code': -32000, 'data': {'0xf6014328e448ee200c4ab69cc1d7faa5d65ab77809c9f964fe0b066052fd2131': {'error': 'revert', 'program_counter': 300, 'return': '0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001343616c6c6572206973206e6f74206f776e657200000000000000000000000000', 'reason': 'Caller is not owner'}, 'stack': 'RuntimeError: VM Exception while processing transaction: revert Caller is not owner\n at Function.RuntimeError.fromResults (/Applications/Ganache.app/Contents/Resources/static/node/node_modules/ganache-core/lib/utils/runtimeerror.js:94:13)\n at BlockchainDouble.processBlock (/Applications/Ganache.app/Contents/Resources/static/node/node_modules/ganache-core/lib/blockchain_double.js:627:24)\n at runMicrotasks (<anonymous>)\n at processTicksAndRejections (internal/process/task_queues.js:93:5)', 'name': 'RuntimeError'}}
現在のオーナー(アカウントアドレス) : 0x2044E11CeeBA8dbeaFaA37f61c9F7EeCa994b34b
サンプルコード3の動作確認
最後のサンプルは投票になります。スマートコントラクトのインスタンスを生成したアカウントが議長となっています。この議長だけが、アカウントに投票権を付与することができます。
また、Ballotコントラクトのコンストラクタには複数の提案を配列として渡します。
/**
* @dev Create a new ballot to choose one of 'proposalNames'.
* @param proposalNames names of proposals
*/
constructor(bytes32[] memory proposalNames) {
chairperson = msg.sender;
voters[chairperson].weight = 1;
for (uint i = 0; i < proposalNames.length; i++) {
// 'Proposal({...})' creates a temporary
// Proposal object and 'proposals.push(...)'
// appends it to the end of 'proposals'.
proposals.push(Proposal({
name: proposalNames[i],
voteCount: 0
}));
}
}
今回の提案は以下となります。
["0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000002","0x0000000000000000000000000000000000000000000000000000000000000003"]
以下の様にRemixのデプロイ時に引数として与える必要があります。
投票権の付与
投票するには、議長によって投票権を付与してもらう必要があります。議長は、giveRightToVoteメソッドに付与するアカウントアドレスを指定してトランザクションを実行します。
try:
# 2人目のアカウントに投票権を付与
txn = contract_instace.functions.giveRightToVote(account_2).buildTransaction({
'gas': 170000,
'gasPrice': w3.toWei('1', 'gwei'),
'from': account_1,
'nonce': w3.eth.getTransactionCount(account_1),
})
signed_txn = w3.eth.account.signTransaction(txn, private_key=private_key_1)
w3.eth.sendRawTransaction(signed_txn.rawTransaction)
except Exception as e:
print('例外です(5)')
print(e)
投票する
投票権を付与されると、投票(voteメソッド)ができます。どの当選案にするかはインデックスで指定します。
try:
# 2人目のアカウントの投票
txn = contract_instace.functions.vote(0).buildTransaction({
'gas': 170000,
'gasPrice': w3.toWei('1', 'gwei'),
'from': account_2,
'nonce': w3.eth.getTransactionCount(account_2),
})
signed_txn = w3.eth.account.signTransaction(txn, private_key=private_key_2)
w3.eth.sendRawTransaction(signed_txn.rawTransaction)
except Exception as e:
print('例外です(6)')
print(e)
委任投票する
自分が選ぶのではなく、他の人の選んだ提案に投票したい場合は、委任投票ができます。delegateメソッドに委任したいアカウントアドレスを指定してトランザクションを実行します。
try:
txn = contract_instace.functions.delegate(account_1).buildTransaction({
'gas': 170000,
'gasPrice': w3.toWei('1', 'gwei'),
'from': account_3,
'nonce': w3.eth.getTransactionCount(account_3),
})
signed_txn = w3.eth.account.signTransaction(txn, private_key=private_key_3)
w3.eth.sendRawTransaction(signed_txn.rawTransaction)
except Exception as e:
print('例外です(8)')
print(e)
当選結果を見る
当選結果を見るには winningProposal() を先に呼び出し当選したインデックスを取得します。
続いて、winnerName() を呼び出して当選案を確認します。
今回のサンプルでは
- 1人目(議長)はindex 2番に投票
- 2人目はindex 0番に投票
- 3人目はindex 1人目(議長)への委任投票(=index 2番)
なのでindex 2番が当選されました。
print(contract_instace.functions.winningProposal().call()) # 当選したインデックス
print(contract_instace.functions.winnerName().call()) # 当選案
【結果】
2
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03'
投票(ballot)コントラクタは、投票権が付与されないと投票ができなかったり、議長には最初から投票権が付与されてたり、二重投票を防ぐなどのようなチェックが入っています。GitHubには一通りのチェックも書いているので是非参考にしてみて下さい。
まとめ
今回はWeb3をPythonからまずは触ってみることについて、できるだけ簡潔にまとめてみましたがイメージが伝わったでしょうか。最近はWeb3.py以外にもノーコード・ローコードで提供される ThirdWebなどがあり、これもPythonで動かすことができます(次回余力があればまとめてみます💦)。
ここではビギナーレベルの開発を紹介しましたが、実際の開発や運用には実に様々なツールやサービスを利用します。
代表的なものにはInfuraやAlchemyなどがあります。コード監査やトランザクション解析などもあります。
スマートコントラクト自体はオブジェクト指向のクラス・インスタンスに近いイメージですが、このインスタンスが分散したワールドコンピューター上にあって誰からも見れたり、利用できることに大きな意義があります。自分でもうまく解釈できませんがOSSの様なコードを共有する世界から、実際に動いているインスタンスまでをも共有しあおうとする世界感になっている気もしております。