🧬

zkSync Python SDKを触ってみよう!

2023/01/21に公開約14,600字

はじめに

zk-rollupを用いたEthereum Layer2であるzkSyncのPython SDKを触ってみようという日本語記事です。

https://zksync.io/

初学者のため、指摘・補足あれば遠慮なくTwitterなどにて連絡をください。

答えられるかどうかわかりませんが、質問も歓迎です。一緒に勉強していきましょう。

https://v2-docs.zksync.io/api/python/getting-started.html

また、サンプルコードはすべてGithubにて公開しています。

https://github.com/mio256/zkSync-python

インストール

下記で普通にインストールできる方は普通にインストールして頂いて構いません。

$ pip install zksync2

ただ、python3.10-3.11などで以下のエラーが発生すると思います。

  error: subprocess-exited-with-error

  × Running setup.py install for pysha3 did not run successfully.
  │ exit code: 1
  ╰─> [20 lines of output]
      running install
      
...省略...

  note: This error originates from a subprocess, and is likely not a problem with pip.
error: legacy-install-failure

× Encountered error while trying to install package.
╰─> pysha3

note: This is an issue with the package mentioned above, not pip.
hint: See above for output from the failure.

これは、おそらくzksync2の依存ライブラリであるpysha3がもう長期間更新されていないからです。

pysha3はpython3.5までしかサポートしていませんが(python3.6も想定してはいる)、python3.5はもうサポートを終了しています。

逆にzksync2はpython3.8以降しかサポートしていないようなので、とりあえずpython3.8にしてみましょう。

以下はbrewを用いてpython3.8をインストールし、仮想環境を立ててzksync2をインストールする方法です。

$ brew install python@3.8
$ python3.8 --version
# python3.8.* と表示されるはず(表示されない場合やエラー時はパスが通っていない)
$ python3.8 -m venv venv
$ . ./venv/bin/activate
(venv) $ python3.8 -m pip install --upgrade pip
(venv) $ python --version
# python3.8.* と表示されるはず(表示されない場合は仮想環境を立てれていないか入れていない)
$ pip install zksync2

これで基本的にうまく動くはずです。

おそらく、pysha3がpython3.8までは問題なく対応しており、それ以降でなにか破壊的変更があるのではないかと考えています。

なにかわかりましたらご連絡ください。

流儀を押し付けるつもりはありませんが、特にバージョン互換性で詰まりやすい場面なので仮想環境を使うことをおすすめします。(僕のように意味のわからないエラーで数時間頭を抱えたくなければ笑)

Instantiating the SDK

instance.py
from web3 import Web3
from zksync2.module.module_builder import ZkSyncBuilder

URL_TO_ETH_NETWORK = "https://rpc.ankr.com/eth_goerli"
ZKSYNC_NETWORK_URL = "https://zksync2-testnet.zksync.dev"

eth_web3 = Web3(Web3.HTTPProvider(URL_TO_ETH_NETWORK))
zkSync_web3 = ZkSyncBuilder.build(ZKSYNC_NETWORK_URL)

Ethereum signer

Metamaskでテスト用秘密鍵を生成してコピペで使うのもいいですが、せっかくなのでPythonで生成しましょう。

こちらから秘密鍵生成のPythonコードを拝借しました。(もしなにか足りないパッケージがあればご自分でpip installしてください)

https://www.arthurkoziel.com/generating-ethereum-addresses-in-python/

key.py
from secrets import token_bytes
from coincurve import PublicKey
from sha3 import keccak_256

private_key = keccak_256(token_bytes(32)).digest()
public_key = PublicKey.from_valid_secret(private_key).format(compressed=False)[1:]
addr = keccak_256(public_key).digest()[-20:]

print('private_key:', private_key.hex())
print('eth addr: 0x' + addr.hex())

上記コードを実行すると、秘密鍵とEthereumAddressが表示されるはずです。

(venv) $ python key.py
private_key: 98b05edb1a99312202a8c2629e4bef2570d764a59791a60a32d654c95eadefa6
eth addr: 0x075d7029c699acf1ccb637611051074e57e7475a

この秘密鍵を使ってzksyncのsignerを生成します。

chainidはなんでもいいですが、とりあえずGoerliにしておきました。

https://zenn.dev/watson_sei/articles/0bf87f4bb70207

signer.py
from eth_account import Account
from eth_account.signers.local import LocalAccount
from zksync2.signer.eth_signer import PrivateKeyEthSigner

PRIVATE_KEY = '生成した秘密鍵'

chain_id = 5

account: LocalAccount = Account.from_key(PRIVATE_KEY)
signer = PrivateKeyEthSigner(account, chain_id)

print(signer)

実行結果として、ちゃんとsignerが生成されたことがわかります。

(venv) $ python signer.py
<zksync2.signer.eth_signer.PrivateKeyEthSigner object at 0x102b23f40>

Depositing funds

ここからはGOERLI ETHを用いてzkSyncを扱うことになるので、FaucetにてETHを獲得しておきましょう。

Faucet探しにはFaucet Link Directoryがとても便利です。

https://ethresear.ch/t/faucet-link-directory/12670

送金用のETHが用意できたら、instance.pyとkey.pyを組み合わせてdepositを実行してみましょう。

deposit.py
from web3 import Web3
from web3.middleware import geth_poa_middleware
from eth_account import Account
from eth_account.signers.local import LocalAccount
from zksync2.signer.eth_signer import PrivateKeyEthSigner
from zksync2.manage_contracts.gas_provider import StaticGasProvider
from zksync2.core.types import Token
from zksync2.provider.eth_provider import EthereumProvider
from zksync2.module.module_builder import ZkSyncBuilder

URL_TO_ETH_NETWORK = "https://rpc.ankr.com/eth_goerli"
ZKSYNC_NETWORK_URL = "https://zksync2-testnet.zksync.dev"

eth_web3 = Web3(Web3.HTTPProvider(URL_TO_ETH_NETWORK))
zkSync_web3 = ZkSyncBuilder.build(ZKSYNC_NETWORK_URL)

PRIVATE_KEY = '生成した秘密鍵'
chain_id = 5

account: LocalAccount = Account.from_key(PRIVATE_KEY)
signer = PrivateKeyEthSigner(account, chain_id)

def deposit():
    #geth_poa_middleware is used to connect to geth --dev.
    eth_web3.middleware_onion.inject(geth_poa_middleware, layer=0)

    #calculate  gas fees
    gas_provider = StaticGasProvider(Web3.toWei(1, "gwei"), 555000)

    #Create the ethereum provider for interacting with ethereum node, initialize zkSync signer and deposit funds.
    eth_provider = EthereumProvider.build_ethereum_provider(zksync=zkSync_web3,
                                                            eth=eth_web3,
                                                            account=account,
                                                            gas_provider=gas_provider)
    tx_receipt = eth_provider.deposit(Token.create_eth(),
                                    eth_web3.toWei("depositしたい量", "ether"),
                                    account.address)
    # Show the output of the transaction details.
    print(f"tx status: {tx_receipt['status']}")


if __name__ == "__main__":
    deposit()

実行後にGoerli Scanでプログラム通りに実行されていることが確認できます。

ただ、このままだと非常に助長的なのでimportを使って書き換えます。

importできるように、instance.pyとsinger.pyを同一ディレクトリに用意しておいてください。

これ以降はimportを用いて実行を行っていきます。

deposit_2.py
from web3 import Web3
from web3.middleware import geth_poa_middleware
from zksync2.manage_contracts.gas_provider import StaticGasProvider
from zksync2.core.types import Token
from zksync2.provider.eth_provider import EthereumProvider

from instance import eth_web3, zkSync_web3
from signer import account

def deposit():
    #geth_poa_middleware is used to connect to geth --dev.
    eth_web3.middleware_onion.inject(geth_poa_middleware, layer=0)

    #calculate  gas fees
    gas_provider = StaticGasProvider(Web3.toWei(1, "gwei"), 555000)

    #Create the ethereum provider for interacting with ethereum node, initialize zkSync signer and deposit funds.
    eth_provider = EthereumProvider.build_ethereum_provider(zksync=zkSync_web3,
                                                            eth=eth_web3,
                                                            account=account,
                                                            gas_provider=gas_provider)
    tx_receipt = eth_provider.deposit(Token.create_eth(),
                                    eth_web3.toWei("depositしたい量", "ether"),
                                    account.address)
    # Show the output of the transaction details.
    print(f"tx status: {tx_receipt['status']}")


if __name__ == "__main__":
    deposit()

Checking Balance

残高の確認は以下のように実行できます。

balance.py
from zksync2.core.types import EthBlockParams

from instance import zkSync_web3
from signer import account

def get_account_balance():
    zk_balance = zkSync_web3.zksync.get_balance(account.address, EthBlockParams.LATEST.value)
    print(f"zkSync balance: {zk_balance}")


if __name__ == "__main__":
    get_account_balance()

Perfoming for transfer

トランザクションを生成し、ガス代を確認して、実際にトランザクションを実行するコードです。

自分から自分宛てにトランザクションを起こしているので、ガス代だけ払われることになります。

実行後にbalance.pyを実行すると、しっかりガス代だけ払われていることがわかります。

from eth_typing import HexStr
from web3 import Web3
from zksync2.module.request_types import EIP712Meta, Transaction
from zksync2.core.types import ZkBlockParams

from zksync2.signer.eth_signer import PrivateKeyEthSigner
from zksync2.transaction.transaction712 import Transaction712

from instance import zkSync_web3
from signer import account


def transfer_to_self():
    chain_id = zkSync_web3.zksync.chain_id
    signer = PrivateKeyEthSigner(account, chain_id)

    nonce = zkSync_web3.zksync.get_transaction_count(account.address, ZkBlockParams.COMMITTED.value)
    tx = Transaction(from_=account.address,
                     to=account.address,
                     ergs_price=0,
                     ergs_limit=0,
                     data=HexStr("0x"),
                     eip712Meta=EIP712Meta)
    estimate_gas = int(zkSync_web3.zksync.eth_estimate_gas(tx))
    gas_price = zkSync_web3.zksync.gas_price

    print(f"Fee for transaction is: {estimate_gas * gas_price}")

    tx_712 = Transaction712(chain_id=chain_id,
                            nonce=nonce,
                            gas_limit=estimate_gas,
                            to=tx["to"],
                            value=Web3.toWei(送金する量, 'ether'),
                            data=tx["data"],
                            maxPriorityFeePerGas=100000000,
                            maxFeePerGas=gas_price,
                            from_=account.address,
                            meta=tx["eip712Meta"])

    singed_message = signer.sign_typed_data(tx_712.to_eip712_struct())
    msg = tx_712.encode(singed_message)
    tx_hash = zkSync_web3.zksync.send_raw_transaction(msg)
    tx_receipt = zkSync_web3.zksync.wait_for_transaction_receipt(tx_hash, timeout=240, poll_latency=0.5)
    print(f"tx status: {tx_receipt['status']}")


if __name__ == "__main__":
    transfer_to_self()

Transfer funds (ERC20 tokens)

公式Docsのままだと動かなかったため、推測で修正を入れています。

from web3 import Web3
from zksync2.module.request_types import EIP712Meta, Transaction
from zksync2.manage_contracts.erc20_contract import ERC20FunctionEncoder
from zksync2.core.types import ZkBlockParams
from zksync2.signer.eth_signer import PrivateKeyEthSigner
from zksync2.transaction.transaction712 import Transaction712

from instance import zkSync_web3
from signer import account


def transfer_erc20_token():
    chain_id = zkSync_web3.zksync.chain_id
    signer = PrivateKeyEthSigner(account, chain_id)

    nonce = zkSync_web3.zksync.get_transaction_count(account.address, ZkBlockParams.COMMITTED.value)

    erc20_encoder = ERC20FunctionEncoder(zkSync_web3)
    transfer_params = [account.address, 0]
    call_data = erc20_encoder.encode_method("transfer", args=transfer_params)

    tx = Transaction(from_=account.address,
                     to=account.address,
                     value=Web3.toWei(0.01, 'ether'),
                     ergs_price=0,
                     ergs_limit=0,
                     data=call_data,
                     eip712Meta=EIP712Meta)
    estimate_gas = int(zkSync_web3.zksync.eth_estimate_gas(tx))
    gas_price = zkSync_web3.zksync.gas_price

    print(f"Fee for transaction is: {estimate_gas * gas_price}")

    tx_712 = Transaction712(chain_id=chain_id,
                            nonce=nonce,
                            gas_limit=estimate_gas,
                            to=tx["to"],
                            value=tx["value"],
                            data=tx["data"],
                            maxPriorityFeePerGas=100000000,
                            maxFeePerGas=gas_price,
                            from_=tx["from_"],
                            meta=tx["eip712Meta"])
    singed_message = signer.sign_typed_data(tx_712.to_eip712_struct())
    msg = tx_712.encode(singed_message)
    tx_hash = zkSync_web3.zksync.send_raw_transaction(msg)
    tx_receipt = zkSync_web3.zksync.wait_for_transaction_receipt(tx_hash, timeout=240, poll_latency=0.5)
    print(f"tx status: {tx_receipt['status']}")


if __name__ == "__main__":
    transfer_erc20_token()

Withdrawing funds

公式Docsのままだと動かなかったため、推測で修正を入れています。

from web3 import Web3
from decimal import Decimal
from eth_typing import HexStr
from zksync2.module.request_types import EIP712Meta, Transaction
from zksync2.manage_contracts.l2_bridge import L2BridgeEncoder
from zksync2.core.types import Token, ZkBlockParams, BridgeAddresses

from zksync2.signer.eth_signer import PrivateKeyEthSigner
from zksync2.transaction.transaction712 import Transaction712

from instance import zkSync_web3
from signer import account


def withdraw():
    chain_id = zkSync_web3.zksync.chain_id
    signer = PrivateKeyEthSigner(account, chain_id)
    ETH_TOKEN = Token.create_eth()

    nonce = zkSync_web3.zksync.get_transaction_count(account.address, ZkBlockParams.COMMITTED.value)
    bridges: BridgeAddresses = zkSync_web3.zksync.zks_get_bridge_contracts()

    l2_func_encoder = L2BridgeEncoder(zkSync_web3)
    call_data = l2_func_encoder.encode_function(fn_name="withdraw", args=[
        account.address,
        ETH_TOKEN.l2_address,
        ETH_TOKEN.to_int(Decimal("0.001"))
    ])

    tx = Transaction(from_=account.address,
                     to=bridges.l2_eth_default_bridge,
                     ergs_limit=0,
                     ergs_price=0,
                     data=HexStr(call_data),
                     eip712Meta=EIP712Meta)
    estimate_gas = int(zkSync_web3.zksync.eth_estimate_gas(tx))
    gas_price = zkSync_web3.zksync.gas_price

    print(f"Fee for transaction is: {estimate_gas * gas_price}")

    tx_712 = Transaction712(chain_id=chain_id,
                            nonce=nonce,
                            gas_limit=estimate_gas,
                            to=tx["to"],
                            value=Web3.toWei(0.01, 'ether'),
                            data=tx["data"],
                            maxPriorityFeePerGas=100000000,
                            maxFeePerGas=gas_price,
                            from_=tx["from_"],
                            meta=tx["eip712Meta"])

    singed_message = signer.sign_typed_data(tx_712.to_eip712_struct())
    msg = tx_712.encode(singed_message)
    tx_hash = zkSync_web3.zksync.send_raw_transaction(msg)
    tx_receipt = zkSync_web3.zksync.wait_for_transaction_receipt(tx_hash, timeout=240, poll_latency=0.5)
    print(f"tx status: {tx_receipt['status']}")


if __name__ == "__main__":
    withdraw()

Smart Contract

スマートコントラクトのデプロイについては、公式Docsを参考にしてください。

https://v2-docs.zksync.io/api/python/getting-started.html#deploy-a-smart-contract

Discussion

ログインするとコメントできます