zkSync JavaScript SDKをrinkebyテストネットで利用する
ZK Rollupを元にしたL2プロトコルであるzkSyncについて調査し、そのJavaScript SDKの利用方法について軽く確認する。
以下のチュートリアルの内容について、実際に確認する際に必要な作業を追加で説明として加えている。
Getting started | zkSync: secure, scalable crypto payments
zkRollup
zkSyncはZK rollupに基づくL2プロトコル。
全ての資産はメインチェーンであるEthereum上で保持され、計算とストレージはオフチェーンで処理されます。 全てのトランザクションを個別に検証するのではなく、複数のトランザクションを1つのトランザクションにロールアップすることで、全ての検証と証明を同時におこおなうことができるというのがZK Rollupの考え。
- ユーザーはトランザクションに署名し、バリデータに送信する
- バリデーターは千個のトランザクションを1つのトランザクションにまとめ上げ、新しい状態のルートハッシュの暗号学的なコミットメントをメインネットのスマートコントラクトにSNARKと共に提出する。これはいくつかの正確なトランザクションを古い状態に適用した結果。
- SNARKに加え、状態の差分は安価なcalldataとしてメインチェーンネットワークに提出されます。これにより、いかなる時も誰でも状態を再構築することが可能になります。
- SNARKと状態の差分はスマートコントラクトによって検証され、全てのトランザクションの検証と妥当性はブロックの中に含まれます。
この時、各トランザクションを個別に検証するのと、オフチェーンでまとめて検証するのとでは100~200倍程度のスケーラビリティの差があり、トランザクションのコストも節約されます。
この時、ZK Rollupは以下の保証を提供します。
- バリデータが状態を破壊したり、資産を盗むことはできない。(サイドチェインとは異なり)
- バリデータが停止している状態でさえ、ユーザーは常にzkSyncスマートコントラクトから資産を取得することができます。(Plasmaとは異なり)
- ユーザーも単一の信頼できるサードパーティもZK Rollupブロックを詐欺から守るために監視を行う必要がありません。(ペイメントチャネルやOptimistic Rollupとは異なり)
環境構築
ウォレットの作成と実行環境Repl用意
npmとtruffleを先にインストールしてください。
$ truffle init $ npm install zksync $ npm install ethers
$ truffle console truffle(ganache)> const zksync = require('zksync'); undefined truffle(ganache)> zksync { Wallet: [Getter], Transaction: [Getter], ETHOperation: [Getter], submitSignedTransaction: [Getter], submitSignedTransactionsBatch: [Getter], ... } truffle(ganache)> const ethers = require('ethers'); // create provider for rinkeby test net truffle(ganache)> const syncProvider = await zksync.getDefault('rinkeby'); truffle(ganache)> const ethersProvider = ethers.getDefaultProvider('rinkeby'); // create wallet for test use truffle(ganache)> let wallet = ethers.Wallet.createRandom(); truffle(ganache)> wallet Wallet { _isSigner: true, _signingKey: [Function], address: '0x...', _mnemonic: [Function], provider: null } // attach rinkeby provider to randomly created wallet truffle(ganache)> wallet = wallet.connect(ethersProvider); Wallet { _isSigner: true, _signingKey: [Function], address: '0x...', _mnemonic: [Function], provider: FallbackProvider { _isProvider: true, _events: [], _emitted: { block: -2 }, formatter: Formatter { formats: [Object] }, anyNetwork: false, _network: { name: 'rinkeby', chainId: 4, ensAddress: '0x...', _defaultProvider: [Function] }, .. } ... } // Currenly, The balance of wallet is 0 eth. truffle(ganache)> wallet.getBalance(); BigNumber { _hex: '0x00', _isBigNumber: true }
Rinkeby Faucetでテスト用Ethの発行
EthereumテストネットワークであるRinkebyネットワークでテスト利用するためにFaucetからRinkeby用のEthereumを取得します。
(Rinkeby Authenticated Faucet)https://faucet.rinkeby.io/
「3 Ethers / 8 hours」を選択し、3 Ethersを取得します。
fundedの表示がされたタイミングで、再度 wallet.getBalance()
を呼び出してみると、3 Ether近くが残高として追加されていることが確認できます。
truffle(ganache)> wallet.getBalance(); BigNumber { _hex: '0x29a2241af62c0000', _isBigNumber: true }
- (Etherscan - Rinkeby Testnet Network)https://rinkeby.etherscan.io/ Etherscanで作成したWalletのアドレスを調べてみると、fundsのトランザクションが発行されているのが確認できます。
zkSyncウォレットの作成
zkSyncウォレットで利用される秘密鍵は特別なメッセージに対してEthereumウォレットで署名した結果から暗黙的に導出される、とのことで、以下のようにしてethersのウォレットと、zkSyncのproviderを引数にzkSyncウォレットを導出します。
truffle(ganache)> const syncWallet = await zksync.Wallet.fromEthSigner(wallet, syncProvider); undefined truffle(ganache)> syncWallet Wallet { cachedAddress: '0x...', accountId: undefined, _ethSigner: Wallet { _isSigner: true, _signingKey: [Function], address: '0x...', _mnemonic: [Function], provider: FallbackProvider { _isProvider: true, _events: [], _emitted: [Object], formatter: [Formatter], anyNetwork: false, _network: [Object], _maxInternalBlockNumber: 10275589, _lastBlockNumber: -2, _pollingInterval: 4000, _fastQueryDate: 1646481989319, providerConfigs: [Array], quorum: 1, _highestBlockNumber: 10275589, _internalBlockNumber: [Promise], _fastBlockNumber: 10275589, _fastBlockNumberPromise: [Promise] } }, _ethMessageSigner: EthMessageSigner { ethSigner: Wallet { _isSigner: true, _signingKey: [Function], address: '0x...', _mnemonic: [Function], provider: [FallbackProvider] }, ethSignerType: { verificationMethod: 'ECDSA', isSignedMsgPrefixed: true } }, signer: Signer {}, ethSignerType: { verificationMethod: 'ECDSA', isSignedMsgPrefixed: true }, provider: Provider { pollIntervalMilliSecs: 1000, transport: HTTPTransport { address: 'https://rinkeby-api.zksync.io/jsrpc' }, providerType: 'RPC', contractAddress: { mainContract: '0x82F67958A5474e40E1485742d648C0b0686b6e5D', govContract: '0xC8568F373484Cd51FDc1FE3675E46D8C0dc7D246' }, tokenSet: TokenSet { tokensBySymbol: [Object] }, network: 'rinkeby' } }
zkSyncのコントラクトコードはmainContract
の https://rinkeby.etherscan.io/address/0x82f67958a5474e40e1485742d648c0b0686b6e5d#code から確認することができ、このコントラクトはOpenZeppelinのUpgradableなどのコントラクトを継承しており実際には https://rinkeby.etherscan.io/address/0x59fac1d75ecf61193c7fa897035235f850076931#code へのプロキシーであることやzkSyncの実際のコントラクトコードが確認できます。
EthereumからzkSyncに資産を移動する
以下のコードでEthからzkSyncに1.0 ETHをデポジットします。この時、内部的にはRinkebyネットワークのzkSyncのコントラクトのDeposit ETHメソッドが呼び出されています。
truffle(ganache)> const deposit = await syncWallet.depositToSyncFromEthereum({ depositTo: syncWallet.address(), token: 'ETH', amount: ethers.utils.parseEther('1.0') }); undefined truffle(ganache)> deposit.awaitReceipt() { executed: true, block: { blockNumber: 108890, committed: true, verified: false } }
この場合でもetherscanでトランザクションを確認することができます。
ここで、zkSyncで資産を管理するにはzkSyncのアカウントを登録する必要があり、別の公開鍵が必要です。 zkSyncのアドレスはEthereumの資産をデポジットするか、zkSync内で資産を移動するがのどちらかにより作成されます。 しかし、作成するだけでは資産をコントロールすることができないので、zkSyncで署名を行うための署名鍵を事前に登録しておく必要があります。
// By default, the signing key isn't set. truffle(ganache)> syncWallet.isSigningKeySet() false
初期状態だと、署名鍵がセットされていない状態であり、これはアカウントが所有されていないということを示しています。別途zkSyncでのアカウント有効化手順が必要で、有効化されていない場合所有されていないアカウントとして作成されるのは、
- Ethereumでの資産の転送が正しければ、それはzkSync内でも有効だから
- 全てのアドレスが秘密鍵を持つわけではないから (コントラクトなど)
- 資産の転送は、そのアドレスの実際の所有者がzkSyncを利用し始めるまでに行われる可能性があるから
そこで、アカウントの初期化を行うために、ChangePubKeyトランザクションを介して署名鍵をセットする必要があります。
このトランザクションでは通常のEthereumトランザクションとは異なり、トランザクションデータに対するzkSync署名と、アカウントの所有権を証明するEthereum署名の2つが必要になります。
実際に署名鍵をセットするには以下の呼び出しを行い、この時かかる費用は、zkSyncノードに問い合わせが行われ可能な限り小さい費用が設定されるようになります。
truffle(ganache)> await syncWallet.setSigningKey({ feeToken: 'ETH', ethAuthType: 'ECDSA' }); Transaction { txData: { tx: { accountId: 1287821, account: '0x...', newPkHash: 'sync:...', nonce: 0, feeTokenId: 0, fee: '47200000000000', ethAuthData: [Object], ethSignature: undefined, validFrom: 0, validUntil: 4294967295, type: 'ChangePubKey', feeToken: 0, signature: [Object] } }, txHash: 'sync-tx:...', sidechainProvider: Provider { pollIntervalMilliSecs: 1000, transport: HTTPTransport { address: 'https://rinkeby-api.zksync.io/jsrpc' }, providerType: 'RPC', contractAddress: { mainContract: '0x82F67958A5474e40E1485742d648C0b0686b6e5D', govContract: '0xC8568F373484Cd51FDc1FE3675E46D8C0dc7D246' }, tokenSet: TokenSet { tokensBySymbol: [Object] }, network: 'rinkeby' }, state: 'Sent' }
このChangePubKeyトランザクションは、Rinkebyネットワークに直接送られるものではなく、zkSyncノードに送信され、後でオペレータによりまとめてRinkebyネットワークに送信されます。
そのため、zkSyncのレベルでのトランザクションはetherscanで確認するのではなくzkscanというサイトを利用します。
- (zkscan - Rinkeby)https://rinkeby.zkscan.io
ここでzkSyncウォレットで残高を確認すると、転送したEthが反映されているのを確認することができます。
truffle(ganache)> syncWallet.getBalance('ETH'); BigNumber { _hex: '0x0de08bc60c284000', _isBigNumber: true } truffle(ganache)> let zkSyncBalance = await syncWallet.getBalance('ETH') undefined truffle(ganache)> ethers.utils.formatEther(zkSyncBalance); '0.9999528'
getAccountState
を呼び出すことで、アカウントが所有する全てのトークンを確認することができます。
truffle(ganache)> await syncWallet.getAccountState() { address: '0x...', id: 1287821, depositing: { balances: {} }, committed: { balances: { ETH: '999952800000000000' }, nfts: {}, mintedNfts: {}, nonce: 1, pubKeyHash: 'sync:...' }, verified: { balances: { ETH: '1000000000000000000' }, nfts: {}, mintedNfts: {}, nonce: 0, pubKeyHash: 'sync:...' }, accountType: 'Owned' }
zkSyncからEthereumに資産を移動する
withdrawFromSyncToEthereum
によって、zkSyncからEthereumに引き出しを行うことができます。
truffle(ganache)> let withdraw = await syncWallet.withdrawFromSyncToEthereum({ ethAddress: wallet.address, token: 'ETH', amount: ethers.utils.parseEther('0.5') }); truffle(ganache)> zkSyncBalance = await syncWallet.getBalance('ETH') undefined truffle(ganache)> ethers.utils.formatEther(zkSyncBalance); '0.4998541' // This is not immediately reflected as the withdrawal from L2 to L1 needs to get finality at L1. truffle(ganache)> ethers.utils.formatEther(ethBalance); '1.999937400998935817' // After a while, ETH tx will be submitted and confirmed. After that, eth balance is reflected. truffle(ganache)> ethBalance = await wallet.getBalance() undefined truffle(ganache)> ethers.utils.formatEther(ethBalance); '2.499937400998935817'
このとき、zkSyncのバリデータによってrollupされたトランザクションでまとめて引き出しが実行されます。
zkscanからETH tx hashを確認することで、EthereumのどのトランザクションによってzkSyncコントラクトから自分のアドレスに資産が転送されたかを確認することができます。