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. ユーザーはトランザクションに署名し、バリデータに送信する
  2. バリデーターは千個のトランザクションを1つのトランザクションにまとめ上げ、新しい状態のルートハッシュの暗号学的なコミットメントをメインネットのスマートコントラクトにSNARKと共に提出する。これはいくつかの正確なトランザクションを古い状態に適用した結果。
  3. SNARKに加え、状態の差分は安価なcalldataとしてメインチェーンネットワークに提出されます。これにより、いかなる時も誰でも状態を再構築することが可能になります。
  4. 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 }

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のコントラクトコードはmainContracthttps://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というサイトを利用します。

f:id:dasshshsd:20220305230537p:plain

ここで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コントラクトから自分のアドレスに資産が転送されたかを確認することができます。

f:id:dasshshsd:20220305230833p:plain