iT邦幫忙

2022 iThome 鐵人賽

DAY 21
1
Web 3

從以太坊白皮書理解 web 3 概念系列 第 22

從以太坊白皮書理解 web 3 概念 - Day21

  • 分享至 

  • xImage
  •  

從以太坊白皮書理解 web 3 概念 - Day21

Learn Solidity - Day 13 - Intro to zkSync

今天將會透過 Lession 17 - Intro to zkSync 來了解

何謂零知識證明與其應用

透過 zkSync 做到 off-chain 的 token 對換。

零知識證明簡介

在密碼學,所謂的零知識證明是指一種驗證方法可以透過不直接讀取需要被驗證資料本身的資訊來驗證論述是否正確,不需要額外關於驗證資料本身的資訊。

所謂的零知識是指不需要關於關於被驗證資料的資訊。

舉例來說:

想象一下,您與一個蒙著眼睛的人共處一室。在您面前的桌子上有兩個球:白色和黑色。您需要在不透露顏色的情況下向第二個人(驗證者)證明這些球確實是不同的顏色。

透過一個方式,讓蒙著眼睛的人先把球藏在桌下,然後讓他隨機選出一個展示給您看。
然後再讓他已同樣的動作展示一次。這時您可以透漏球是否與上次是同一個。

為了避免協助驗證的人作弊,因此蒙著眼睛的人隨機選球。在做過多次驗證後,透過知道會有一顆球與另一顆不同。

蒙眼的本身不需要知道太多關於球顏色的內容,卻可以透過其他觀察者回答球是不是同一顆來驗證球顏色是不同的。

在這個範例裡驗證者是蒙眼的人,而驗證資料是球的顏色

zkSync

zkSync 是一個實作零知識證明的框架,可用來加強不同鏈上的 token 交換的安全性,可以透過鏈下的運算來實作,也就是相對有效率。

zkSync 運作流程

使用者會透過傳輸 ERC20Token 到 zkSync Smart Contract

zkSync Smart Contrat 則會給予使用者等價的 Asset

設定

zkSync 與 Ethereum 節點都是透過 JSON-RPC 格式欄做溝通

這邊將會使用 nodejs 套件來處理與節點溝通的細節

使用的套件有 ethers 與 zksync

建立一個資料夾然後透過以下指令初始化專案目錄

npm init -y

或是

yarn init -y

透過以下指令做套件安裝

npm i ethers zksync -S

或是

yarn add ethers zksync

與 zkSync 節點溝通

zkSync 使用了 zk-SNARKs 標準代表 "Zero-Knowledge Succinct Non-interactive Arguments of Knowledge"

零知識證明

簡單來說,零知識證明是一種方法能夠讓 Alice(受驗證者)能夠讓不需要額外透露其他資訊就可以向 Bob(驗證者)證明 Alice 所知到的某些事實。

舉例來說: 使用 zk-SNARK, Bob 可以不查詢 Alice 帳號就可以知道 Alice 的帳戶內資產大於 0.5 ETH

zkSync 是透過一個發佈在 Ethereum 鏈上的 Smart Contrat 來持有所有資產,而大部分的運算都是使用鏈下處理。每次簽入一筆交易,這個協定就會送出一個對應的運算來處理多筆交易進入一個區塊,並且計算以下資料:

  • 密碼學簽章(root hash)
  • 密碼學證明(the SNARK)
  • 狀態,代表每筆交易的一小部份資料

這些資料接著會送到一個 Smart Contract,這可以讓所有人都可以重建這些狀態。

SNARK 的驗證比單獨驗證每比交易還要便宜,並且會儲存狀態再鏈下也比鏈上便宜。

建立一個 Provider

當要開始與 zkSync 節點互動時,需要透過 Provider 元件來處理。

Provider 如同 zkSync 節點與前端 app 的一個溝通橋梁。

實作

建立兩個 javascript 應用 alice.js 與 bob.js

並且把共用的部份都放到 util.js

  1. 建立 async function getZkSyncProvider(zksync, networkName)
  2. getZkSyncProvider function 第一行需要宣告一個變數如下
let zkSyncProvider;
  1. 使用 try catch 來處理 provider 建立失敗的狀況如下
try {
// Initialize the provider
} catch (error) {
// Display an error message
}

  1. 在 try block 加入以下邏輯
await zksync.getDefaultProvider(networkName);
  1. 在 catch block 加入以下邏輯
console.log('Unable to connect to zkSync.')
console.log(error)
  1. 最後 return zkSyncProvider

更新如下

async function getZkSyncProvider (zksync, networkName) {
    let zkSyncProvider
    try {
        zkSyncProvider = await zksync.getDefaultProvider(networkName)
    } catch (error) {
        console.log('Unable to connect to zkSync.')
        console.log(error)
    }
    return zkSyncProvider
}

建立一個 zkSync 帳號

如同其他錢包一樣,為了要能在鏈上做交易,必須建立一個帳號

帳號也等同於需要產生一把簽章密鑰

並且把簽章密鑰存放在錢包內

在做交易時,需要使用密鑰做簽章

這邊透過 zksync 與 ethers 函式庫整合了一個錢包物件 zksync.Wallet 可以來做上述的產生密鑰還有保管密鑰與做簽章的邏輯

為了要初始化 zksync.Wallet 必須要呼叫 zksync.Wallet.fromEthSigner

並且傳入兩個參數:

  1. ethers.Wallet 的物件
  2. zkSync provider

實作邏輯

  1. 宣告 async function initAccount(rinkebyWallet, zkSyncProvider, zksync)
  2. 宣告 const zkSyncWallet = await zksync.Wallet.fromEthSigner(rinkebyWallet, zkSyncProvider)
  3. 最後 return zkSyncWallet
    更新如下:
async function getZkSyncProvider (zksync, networkName) {
  let zkSyncProvider
  try {
    zkSyncProvider = await zksync.getDefaultProvider(networkName)
  } catch (error) {
    console.log('Unable to connect to zkSync.')
    console.log(error)
  }
  return zkSyncProvider
}

async function getEthereumProvider (ethers, networkName) {
  let ethersProvider
  try {
    // eslint-disable-next-line new-cap
    ethersProvider = new ethers.getDefaultProvider(networkName)
  } catch (error) {
    console.log('Could not connect to Rinkeby')
    console.log(error)
  }
  return ethersProvider
}

// Start here
async function initAccount(rinkebyWallet, zkSyncProvider, zksync) {
    const zkSyncWallet = await zksync.Wallet.fromEthSigner(rinkebyWallet, zkSyncProvider)
    return zkSyncWallet
}

驗證 signing key

前面提到可以使用 zkSync Wallet 來轉換資產

然而,要做簽章必須先檢查是否已經產生簽章密鑰

流程如下

  • 驗證是否有 signing key 如下
if (!await wallet.isSigningKeySet()) {
  // Your signing keys have not been set. You'll place the logic for setting it here.
}
  • 檢查是否具有 zkSync 帳號透過呼叫 getAccountId 來做驗證如下
if (await wallet.getAccountId() === undefined) {
  throw new Error('Unknown account')
}
  • 假設 siging key 還沒產生則會透過以下程式碼產生
const changePubkey = await wallet.setSigningKey()
  • 因為以上功能都是需要上鏈,所以會需要花一些時間,並且需要透過以下程式碼來確認交易已經 commit
await changePubkey.awaitReceipt()

實作

宣告一個 registerAccount function

  1. 驗證 signing key 已經有產生
  2. 確認是否有產生 zkSync 帳號
  3. 實作當 signing key 沒設定時,透過 wallet.setSigningKey 來設定
  4. 最後需要加入 await changePubkey.awaitReceipt 來等待交易完成

更新如下

async function getZkSyncProvider (zksync, networkName) {
  let zkSyncProvider
  try {
    zkSyncProvider = await zksync.getDefaultProvider(networkName)
  } catch (error) {
    console.log('Unable to connect to zkSync.')
    console.log(error)
  }
  return zkSyncProvider
}

async function getEthereumProvider (ethers, networkName) {
  let ethersProvider
  try {
    // eslint-disable-next-line new-cap
    ethersProvider = new ethers.getDefaultProvider(networkName)
  } catch (error) {
    console.log('Could not connect to Rinkeby')
    console.log(error)
  }
  return ethersProvider
}

async function initAccount (rinkebyWallet, zkSyncProvider, zksync) {
  const zkSyncWallet = await zksync.Wallet.fromEthSigner(rinkebyWallet, zkSyncProvider)
  return zkSyncWallet
}

async function registerAccount (wallet) {
  console.log(`Registering the ${wallet.address()} account on zkSync`)
  // Start here
  if (!await wallet.isSigningKeySet()) {
	if (await wallet.getAccountId() === undefined) {
		throw new Error('Unknown account')
	}  
    const changePubkey = await wallet.setSigningKey()
    await changePubkey.awaitReceipt()
  }
  
}

把 asset 存入 zkSync

使用者可以對 zkSync 執行以下類型操作

  • 具有優先順序的操作: 這類操作使用者會發起在 Ethereum 上。舉例來說:把資產存入

  • 交易: 這類型操作使用者會發起來 zkSync 上。舉例來說: 建立一個付款單

    zkSync 的交易具有以下特性:

    • 當送出一筆交易到 zkSync 時,交易會馬上被確認。
      但這個確認只是代表確認這比交易會被加入下一個區塊而已。當你信任這比交易一定會完成就不需要去等待交易實際完成
    • 當 SNARK 證明被 Ethereum Smart Contract 接受, zkSync 的交易才會完成。大約會需要 10 分鐘完成這個流程

Deposity assets to zkSync

要把 assets 存入 zkSync 只需要呼叫 depositToSyncFromEthereum 如下

onst deposit = await zkSyncWallet.depositToSyncFromEthereum({
    depositTo: recipient,
    token: token,
    amount: amountInWei
  })

在呼叫之後,交易會被 Ethereum 上的礦工節點處理。

在這個階段,交易還不會顯示在 zkSync。

而這時通常會透過 awaitEthereumTxCommit 來等待交易處理結果如下

await deposit.awaitEthereumTxCommit()

然後,交易完成後就會推送到 zkSync 。

當這個發生時, 用戶的 balance 就會被更新

為了確認結果可以使用 awaitReceipt 處理如下

await deposit.awaitVerifyReceipt()

實作步驟

  1. 建立一個 async function depositToZkSync(zkSyncWallet, token, amountToDeposit, ethers)
  2. function 第一行會呼叫 depositToSyncFromEthereum ,基本上會跟上面範例類似,但需要做以下一些修正
    • depositTo 需要設定為 zkSyncWallet.address
    • amountToDeposit 是以 wei 為單位
      所以需要把 amount 帶入 ethers.utils.parseEther(amountToDeposit)
  3. 呼叫 deposit.awaitReceipt() 並且需要使用 try/catch 處理如下
try {
	await deposit.awaitReceipt()
} catch (error) {
	
}
  1. 在 catch block 加入以下
console.log('Error while awaiting confirmation from the zkSync operators.')
console.log(error)

更新如下

async function getZkSyncProvider (zksync, networkName) {
  let zkSyncProvider
  try {
    zkSyncProvider = await zksync.getDefaultProvider(networkName)
  } catch (error) {
    console.log('Unable to connect to zkSync.')
    console.log(error)
  }
  return zkSyncProvider
}

async function getEthereumProvider (ethers, networkName) {
  let ethersProvider
  try {
    // eslint-disable-next-line new-cap
    ethersProvider = new ethers.getDefaultProvider(networkName)
  } catch (error) {
    console.log('Could not connect to Rinkeby')
    console.log(error)
  }
  return ethersProvider
}

async function initAccount (rinkebyWallet, zkSyncProvider, zksync) {
  const zkSyncWallet = await zksync.Wallet.fromEthSigner(rinkebyWallet, zkSyncProvider)
  return zkSyncWallet
}

async function registerAccount (wallet) {
  console.log(`Registering the ${wallet.address()} account on zkSync`)
  if (!await wallet.isSigningKeySet()) {
    if (await wallet.getAccountId() === undefined) {
      throw new Error('Unknown account')
    }
    const changePubkey = await wallet.setSigningKey()
    await changePubkey.awaitReceipt()
  }
  console.log(`Account ${wallet.address()} registered`)
}

// Start here
async function depositToZkSync (zkSyncWallet, token, amountToDeposit, ethers) {
    const deposit = await zkSyncWallet.depositToSyncFromEthereum({
        depositTo: zkSyncWallet.address(),
        token: token,
        amount: ethers.utils.parseEther(amountToDeposit)
    })
    try {
        await  deposit.awaitReceipt()
    } catch (error) {
        console.log('Error while awaiting confirmation from the zkSync operators.')
        console.log(error)
    }
}

在 zkSync 轉移 assets

要在 zkSync 轉移 assets 需要以下兩個步驟:

  1. 呼叫 wallet.syncTransfer 如下
const transfer = await wallet.syncTransfer({
 to: toAddress,
 token: token,
 amount: closestPackableAmount,
 fee: closestPackableFee
})
  1. 利用上一步的回傳值物件 transfer 執行 transfer.awaitReceipt 如下
const transferReceipt = await transfer.awaitReceipt()

特別要注意的是 zkSync 做 Tranfer 的精準度是有限的

其中,轉移量的精準度是 5-byte long floating-point

而 fee 的精準度是 2-byte long floating-point

需要使用以下方式做精準度轉換

const closestPackableAmount = zksync.utils.closestPackableTransactionAmount(amountToTransferInWei)
const closestPackableFee = zksync.utils.closestPackableTransactionFee(transferFeeInWei)

實作 transfer function

  1. 呼叫 closestPackableTransactionAmount 把轉移量做精準度轉換
  2. 呼叫 closestPackableTransactionFee 把 fee 做精準度轉換
  3. 呼叫 from.syncTransfer 並且把結果存入 const transfer
  4. 呼叫 await transfer.awaitReceipt 把結果存入 const transferReceipt
  5. 最後使用以下來做 log
console.log('Got transfer receipt.')
console.log(transferReceipt)

更新如下:

async function getZkSyncProvider (zksync, networkName) {
  let zkSyncProvider
  try {
    zkSyncProvider = await zksync.getDefaultProvider(networkName)
  } catch (error) {
    console.log('Unable to connect to zkSync.')
    console.log(error)
  }
  return zkSyncProvider
}

async function getEthereumProvider (ethers, networkName) {
  let ethersProvider
  try {
    // eslint-disable-next-line new-cap
    ethersProvider = new ethers.getDefaultProvider(networkName)
  } catch (error) {
    console.log('Could not connect to Rinkeby')
    console.log(error)
  }
  return ethersProvider
}

async function initAccount (rinkebyWallet, zkSyncProvider, zksync) {
  const zkSyncWallet = await zksync.Wallet.fromEthSigner(rinkebyWallet, zkSyncProvider)
  return zkSyncWallet
}

async function registerAccount (wallet) {
  console.log(`Registering the ${wallet.address()} account on zkSync`)
  if (!await wallet.isSigningKeySet()) {
    if (await wallet.getAccountId() === undefined) {
      throw new Error('Unknown account')
    }
    const changePubkey = await wallet.setSigningKey()
    await changePubkey.awaitReceipt()
  }
  console.log(`Account ${wallet.address()} registered`)
}

async function depositToZkSync (zkSyncWallet, token, amountToDeposit, ethers) {
  const deposit = await zkSyncWallet.depositToSyncFromEthereum({
    depositTo: zkSyncWallet.address(),
    token: token,
    amount: ethers.utils.parseEther(amountToDeposit)
  })
  try {
    await deposit.awaitReceipt()
  } catch (error) {
    console.log('Error while awaiting confirmation from the zkSync operators.')
    console.log(error)
  }
}

async function transfer (from, toAddress, amountToTransfer, transferFee, token, zksync, ethers) {
  // Start here
  const closestPackableAmount = zksync.utils.closestPackableTransactionAmount(
	  ethers.utils.parseEther(amountToTransfer))
  const closestPackableFee = zksync.utils.closestPackableTransactionFee(
	  ethers.utils.parseEther(transferFee))
  const transfer = await from.syncTransfer({
    to: toAddress,
    token: token,
    amount: closestPackableAmount,
    fee: closestPackableFee
  })
  const transferReceipt = await transfer.awaitReceipt()
  console.log('Got transfer receipt.')
  console.log(transferReceipt)
}

Transfer Fee

在 zkSync , 運算需要費用雖然便宜

一共有兩種 fee:

  • off-chain fee: 代表鏈下運算與存儲的花費。這個費用是固定的
  • on-chain fee: 代表鏈上用來執行驗證 SNARK 的費用。這個費用會根據不用的驗證而有所變動。

以下計算 fee 的範例

const feeInWei = await zkSyncProvider.getTransactionFee(transactionType, address, token)

getTransactionFee 需要 3 個參數

  • transactionType: 用來指定是 "Withdraw" 或 "Transfer"
  • address: 代表接收者的 address
  • token: 代表要轉移的 token 類型舉例來說 "ETH"

回傳值的型別會如下

export interface Fee {
    // Operation type (amount of chunks in operation differs and impacts the total fee).
    feeType: "Withdraw" | "Transfer" | "TransferToNew",
    // Amount of gas used by transaction
    gasTxAmount: utils.BigNumber,
    // Gas price (in wei)
    gasPriceWei: utils.BigNumber,
    // Ethereum gas part of fee (in wei)
    gasFee: utils.BigNumber,
    // Zero-knowledge proof part of fee (in wei)
    zkpFee: utils.BigNumber,
    // Total fee amount (in wei)
    // This value represents the summarized fee components, and it should be used as a fee
    // for the actual operation.
    totalFee: utils.BigNumber,
}

可以發現回傳值會是 BigNumber

如果要轉回可讀的格式就必須使用以下範例

const fee = ethers.utils.formatEther(feeInWei.toString())

實作

  1. 宣告 async function getFee(transactionType, address, token, zkSyncProvider, ethers)
  2. 宣告 const feeInWei = await zkSyncProvider.getTransactionFee(transactionType, address, token)
  3. 回傳 ethers.utils.formatEther(feeInWei.totalEee.toString())

更新如下:

async function getZkSyncProvider (zksync, networkName) {
  let zkSyncProvider
  try {
    zkSyncProvider = await zksync.getDefaultProvider(networkName)
  } catch (error) {
    console.log('Unable to connect to zkSync.')
    console.log(error)
  }
  return zkSyncProvider
}

async function getEthereumProvider (ethers, networkName) {
  let ethersProvider
  try {
    // eslint-disable-next-line new-cap
    ethersProvider = new ethers.getDefaultProvider(networkName)
  } catch (error) {
    console.log('Could not connect to Rinkeby')
    console.log(error)
  }
  return ethersProvider
}

async function initAccount (rinkebyWallet, zkSyncProvider, zksync) {
  const zkSyncWallet = await zksync.Wallet.fromEthSigner(rinkebyWallet, zkSyncProvider)
  return zkSyncWallet
}

async function registerAccount (wallet) {
  console.log(`Registering the ${wallet.address()} account on zkSync`)
  if (!await wallet.isSigningKeySet()) {
    if (await wallet.getAccountId() === undefined) {
      throw new Error('Unknown account')
    }
    const changePubkey = await wallet.setSigningKey()
    await changePubkey.awaitReceipt()
  }
  console.log(`Account ${wallet.address()} registered`)
}

async function depositToZkSync (zkSyncWallet, token, amountToDeposit, ethers) {
  const deposit = await zkSyncWallet.depositToSyncFromEthereum({
    depositTo: zkSyncWallet.address(),
    token: token,
    amount: ethers.utils.parseEther(amountToDeposit)
  })
  try {
    await deposit.awaitReceipt()
  } catch (error) {
    console.log('Error while awaiting confirmation from the zkSync operators.')
    console.log(error)
  }
}

async function transfer (from, toAddress, amountToTransfer, transferFee, token, zksync, ethers) {
  const closestPackableAmount = zksync.utils.closestPackableTransactionAmount(
    ethers.utils.parseEther(amountToTransfer))
  const closestPackableFee = zksync.utils.closestPackableTransactionFee(
    ethers.utils.parseEther(transferFee))

  const transfer = await from.syncTransfer({
    to: toAddress,
    token: token,
    amount: closestPackableAmount,
    fee: closestPackableFee
  })
  const transferReceipt = await transfer.awaitReceipt()
  console.log('Got transfer receipt.')
  console.log(transferReceipt)
}

// Start here
async function getFee (transactionType, address, token, zkSyncProvider, ethers) {
    const feeInWei = await zkSyncProvider.getTransactionFee(transactionType, address, token)
    return ethers.utils.formatEther(feeInWei.totalFee.toString())
}

Withdraw to Ethereum

要從 zkSync 把 asset 提出到 Ethereum 步驟如下:

  1. 需要計算多少量要提出
  2. 呼叫 wallet.withdrawFromSyncToEthereum 如下
const withdraw = await wallet.withdrawFromSyncToEthereum({
 ethAddress: recipientEthereumAddress,
 token: token,
 amount: closestPackableAmount,
 fee: closestPackableFee
})
  1. 最後呼叫 awaitVerifyReceipt 來確認 withdraw 結果

特別要注意以下幾點:

  • wallet.withdrawFromSyncToEthereum 的接收者需要傳入 Ethereum 的地址
  • 需要使用 awaitVerifyReceipt 來察看交易狀態

實作 withdrawToEthereum

  1. 呼叫 zksync.utils.closestPackableTransactionAmount 來計算 closestPackableAmount
  2. 呼叫 zksync.utils.closestPackableTransactionFee 來計算 closestPackableFee
  3. 宣告 const withdraw 來接收呼叫 await wallet.withdrawFromSyncToEthereum 的結果
  4. 呼叫 withdraw.awaitVerifyReceipt

更新如下

async function getZkSyncProvider (zksync, networkName) {
  let zkSyncProvider
  try {
    zkSyncProvider = await zksync.getDefaultProvider(networkName)
  } catch (error) {
    console.log('Unable to connect to zkSync.')
    console.log(error)
  }
  return zkSyncProvider
}

async function getEthereumProvider (ethers, networkName) {
  let ethersProvider
  try {
    // eslint-disable-next-line new-cap
    ethersProvider = new ethers.getDefaultProvider(networkName)
  } catch (error) {
    console.log('Could not connect to Rinkeby')
    console.log(error)
  }
  return ethersProvider
}

async function initAccount (rinkebyWallet, zkSyncProvider, zksync) {
  const zkSyncWallet = await zksync.Wallet.fromEthSigner(rinkebyWallet, zkSyncProvider)
  return zkSyncWallet
}

async function registerAccount (wallet) {
  console.log(`Registering the ${wallet.address()} account on zkSync`)
  if (!await wallet.isSigningKeySet()) {
    if (await wallet.getAccountId() === undefined) {
      throw new Error('Unknown account')
    }
    const changePubkey = await wallet.setSigningKey()
    await changePubkey.awaitReceipt()
  }
  console.log(`Account ${wallet.address()} registered`)
}

async function depositToZkSync (zkSyncWallet, token, amountToDeposit, ethers) {
  const deposit = await zkSyncWallet.depositToSyncFromEthereum({
    depositTo: zkSyncWallet.address(),
    token: token,
    amount: ethers.utils.parseEther(amountToDeposit)
  })
  try {
    await deposit.awaitReceipt()
  } catch (error) {
    console.log('Error while awaiting confirmation from the zkSync operators.')
    console.log(error)
  }
}

async function transfer (from, toAddress, amountToTransfer, transferFee, token, zksync, ethers) {
  const closestPackableAmount = zksync.utils.closestPackableTransactionAmount(
    ethers.utils.parseEther(amountToTransfer))
  const closestPackableFee = zksync.utils.closestPackableTransactionFee(
    ethers.utils.parseEther(transferFee))

  const transfer = await from.syncTransfer({
    to: toAddress,
    token: token,
    amount: closestPackableAmount,
    fee: closestPackableFee
  })
  const transferReceipt = await transfer.awaitReceipt()
  console.log('Got transfer receipt.')
  console.log(transferReceipt)
}

async function getFee(transactionType, address, token, zkSyncProvider, ethers) {
  const feeInWei = await zkSyncProvider.getTransactionFee(transactionType, address, token)
  return ethers.utils.formatEther(feeInWei.totalFee.toString())
}

async function withdrawToEthereum (wallet, amountToWithdraw, withdrawalFee, token, zksync, ethers) {

  // Start here
  const closestPackableAmount = zksync.utils.closestPackableTransactionAmount(
    ethers.utils.parseEther(amountToWithdraw))
  const closestPackableFee = zksync.utils.closestPackableTransactionFee(
    ethers.utils.parseEther(withdrawalFee))
  const withdraw =  await wallet.withdrawFromSyncToEthereum({
    ethAddress: wallet.address(),
    token: token,
    amount: closestPackableAmount,
    fee: closestPackableFee
  })
  await withdraw.awaitVerifyReceipt()
  console.log('ZKP verification is complete')

}

account balance

Committed and Verified Balance

當使用者在 zkSync 轉移 asset , 曠工節點會把這個交易加入下一個區塊並且新增這新區塊到 Ethereum 上的 zkSync Smart Contract 透過一個 Commit 交易。

接著, SNARK 在驗證完 block 內的每個交易後會把 Verify 交易放到 Ethereum 上。一旦 Verify 交易被曠工寫入 Ethereum ,整個轉移流程就完成了

在以上過程中,基本上會把 balance 分為兩種類型

  • committed balance: 包含 verified 與 commited 的交易
  • verified balance: 只包含 verified 的交易

可以透過以下語法取得 balance

  1. 使用 await wallet.getBalance 語法如下
const committedETHBalance = await wallet.getBalance('ETH')
const verifiedETHBalance = await wallet.getBalance('ETH', 'verified')
  1. 透過取得 account state 的物件,來察看如下
const state = await wallet.getAccountState()

state 物件內容格式如下

{ address: '0xc26f2adeeebbad73f25329ffa12cd3889429b5b6',
 committed:
 { balances: { ETH: '100000000000000000' },
   nonce: 1,
   pubKeyHash: 'sync:de9de11bdad08aa1cdc2beb5b2b7c7f29c10f079' },
 depositing: { balances: {} },
 id: 138,
 verified:
 { balances: { ETH: '100000000000000000' },
   nonce: 1,
   pubKeyHash: 'sync:de9de11bdad08aa1cdc2beb5b2b7c7f29c10f079' } }

特別要注意的是當 account balance 的值是 0, 則 state.committed.balances.ETH 會是 undefined

特別要小心處理這塊邏輯

實作

  1. 宣告 async function displayZkSyncBalance(wallet, ethers)
  2. 第一行宣告 cosnt state = await wallet.getAccountState()
  3. 加入 if else 來判斷 state.committed.balances.ETH 是否 defined
  4. 在 if 成立加入以下邏輯
console.log(`Commited ETH balance for ${wallet.address()}: ${ethers.utils.formatEther(state.committed.balances.ETH)}`)
  1. 在 else 加入以下邏輯
console.log(`Commited ETH balance for ${wallet.address()}: 0`)
  1. 加入 if /else 判斷 state.verified.balances.ETH 是否 defined
  2. 如果 if 成立,則印出 verified balance
  3. 在 else 加入以下
console.log(`Verified ETH balance for ${wallet.address()}: 0`)

更新如下

async function getZkSyncProvider (zksync, networkName) {
  let zkSyncProvider
  try {
    zkSyncProvider = await zksync.getDefaultProvider(networkName)
  } catch (error) {
    console.log('Unable to connect to zkSync.')
    console.log(error)
  }
  return zkSyncProvider
}

async function getEthereumProvider (ethers, networkName) {
  let ethersProvider
  try {
    // eslint-disable-next-line new-cap
    ethersProvider = new ethers.getDefaultProvider(networkName)
  } catch (error) {
    console.log('Could not connect to Rinkeby')
    console.log(error)
  }
  return ethersProvider
}

async function initAccount (rinkebyWallet, zkSyncProvider, zksync) {
  const zkSyncWallet = await zksync.Wallet.fromEthSigner(rinkebyWallet, zkSyncProvider)
  return zkSyncWallet
}

async function registerAccount (wallet) {
  console.log(`Registering the ${wallet.address()} account on zkSync`)
  if (!await wallet.isSigningKeySet()) {
    if (await wallet.getAccountId() === undefined) {
      throw new Error('Unknown account')
    }
    const changePubkey = await wallet.setSigningKey()
    await changePubkey.awaitReceipt()
  }
  console.log(`Account ${wallet.address()} registered`)
}

async function depositToZkSync (zkSyncWallet, token, amountToDeposit, ethers) {
  const deposit = await zkSyncWallet.depositToSyncFromEthereum({
    depositTo: zkSyncWallet.address(),
    token: token,
    amount: ethers.utils.parseEther(amountToDeposit)
  })
  try {
    await deposit.awaitReceipt()
  } catch (error) {
    console.log('Error while awaiting confirmation from the zkSync operators.')
    console.log(error)
  }
}

async function transfer (from, toAddress, amountToTransfer, transferFee, token, zksync, ethers) {
  const closestPackableAmount = zksync.utils.closestPackableTransactionAmount(
    ethers.utils.parseEther(amountToTransfer))
  const closestPackableFee = zksync.utils.closestPackableTransactionFee(
    ethers.utils.parseEther(transferFee))

  const transfer = await from.syncTransfer({
    to: toAddress,
    token: token,
    amount: closestPackableAmount,
    fee: closestPackableFee
  })
  const transferReceipt = await transfer.awaitReceipt()
  console.log('Got transfer receipt.')
  console.log(transferReceipt)
}

async function getFee(transactionType, address, token, zkSyncProvider, ethers) {
  const feeInWei = await zkSyncProvider.getTransactionFee(transactionType, address, token)
  return ethers.utils.formatEther(feeInWei.totalFee.toString())
}

async function withdrawToEthereum (wallet, amountToWithdraw, withdrawalFee, token, zksync, ethers) {
  const closestPackableAmount = zksync.utils.closestPackableTransactionAmount(ethers.utils.parseEther(amountToWithdraw))
  const closestPackableFee = zksync.utils.closestPackableTransactionFee(ethers.utils.parseEther(withdrawalFee))
  const withdraw = await wallet.withdrawFromSyncToEthereum({
    ethAddress: wallet.address(),
    token: token,
    amount: closestPackableAmount,
    fee: closestPackableFee
    })
    await withdraw.awaitVerifyReceipt()
    console.log('ZKP verification is complete')
}

// Start here
async function displayZkSyncBalance (wallet, ethers) {
   const state = await wallet.getAccountState()
   if (state.committed.balances.ETH) {
      console.log(`Commited ETH balance for ${wallet.address()}: ${ethers.utils.formatEther(state.committed.balances.ETH)}`) 
   } else {
      console.log(`Commited ETH balance for ${wallet.address()}: 0`) 
   }
   if (state.verified.balances.ETH) {
      console.log(`Verified ETH balance for ${wallet.address()}: ${ethers.utils.formatEther(state.verified.balances.ETH)}`) 
   } else {
      console.log(`Verified ETH balance for ${wallet.address()}: 0`) 
   }
}

shopkeeper - Talking to the blockchain

接下來實作與 zkSync 互動的功能

這邊使用兩個 nodejs 的應用個別代表 shopkeeper(Bob) 以及 customer(Alice)

實作 bob.js(shopkeeper)

  1. 建立一個 bob.js
  2. 產生一個 zkSync Provider 來與 zkSync 做連線
  3. 建立一個 ethereum Provider 來與 ethereum 做連線
    更新如下
(async () => {
  const ethers = require('ethers')
  const zksync = require('zksync')
  const utils = require('./utils')

  // Start here
  const zkSyncProvider = await utils.getZkSyncProvider(zksync, process.env.NETWORK_NAME)
  const ethersProvider = await utils.getEthereumProvider(ethers, process.env.NETWORK_NAME)
})()

建立 shopkeeper 的 Wallet

為了要讓 bob 可以簽章

這裡需要建立 Rinkeby Wallet

建立語法如下

const rinkebyWallet = new ethers.Wallet(process.env.YOUR_PRIVATE_KEY, ethersProvider)

ethers.Wallet 提供以下語法可以讓使用者察看 address 與 balance

console.log(`Bob's Rinkeby address is: ${rinkebyWallet.address}`)
console.log(`Bob's initial balance on Rinkeby is: ${ethers.utils.formatEther(await rinkebyWallet.getBalance())}`)

實作建立 Rinkeby wallet 邏輯

  1. 建立一個 rinkeby wallet 給 bob
  2. 顯示 bob 在 rinkeby 的 address 與初始的 balance
  3. 建立一個 zkSync wallet 給 bob

更新如下

(async () => {
  const ethers = require('ethers')
  const zksync = require('zksync')
  const utils = require('./utils')

  const zkSyncProvider = await utils.getZkSyncProvider(zksync, process.env.NETWORK_NAME)
  const ethersProvider = await utils.getEthereumProvider(ethers, process.env.NETWORK_NAME)

  // Start here
  const bobRinkebyWallet = new ethers.Wallet(process.env.BOB_PRIVATE_KEY, ethersProvider)
  console.log(`Bob's Rinkeby address is: ${bobRinkebyWallet.address}`)
  console.log(`Bob's initial balance on Rinkeby is: ${ethers.utils.formatEther(await bobRinkebyWallet.getBalance())}`)
  const bobZkSyncWallet =  await utils.initAccount(bobRinkebyWallet, zkSyncProvider, zksync)
})()

實作更新 balance

當轉移 asset 發生後,則需要更新 balance

因為 balance 需要等 blockchain 同步完 transaction 才能更新

因此需要透過 setInterval 來不斷監聽變化

且為了避免應用關閉時, setInterval 沒有釋放掉

所以需要使用 process.on('SIGINT') 語法來監聽 app 關閉來做釋放資源的邏輯

實作步驟

  1. 呼叫 utils.displayZkSyncBalance(bobZkSyncWallet, ethers) 來更新 balance

更新如下

(async () => {
  const ethers = require('ethers')
  const zksync = require('zksync')
  const utils = require('./utils')
  const SLEEP_INTERVAL = process.env.SLEEP_INTERVAL || 5000

  const zkSyncProvider = await utils.getZkSyncProvider(zksync, process.env.NETWORK_NAME)
  const ethersProvider = await utils.getEthereumProvider(ethers, process.env.NETWORK_NAME)

  const bobRinkebyWallet = new ethers.Wallet(process.env.BOB_PRIVATE_KEY, ethersProvider)
  console.log(`Bob's Rinkeby address is: ${bobRinkebyWallet.address}`)
  console.log(`Bob's initial balance on Rinkeby is: ${ethers.utils.formatEther(await bobRinkebyWallet.getBalance())}`)
  const bobZkSyncWallet = await utils.initAccount(bobRinkebyWallet, zkSyncProvider, zksync)

  process.on('SIGINT', () => {
    console.log('Disconnecting')
    // Disconnect
    process.exit()
  })
  setInterval(async () => {
    // Call the `utils.displayZkSyncBalance` function
    await utils.displayZkSyncBalance(bobZkSyncWallet, ethers)
    console.log('---')
  }, SLEEP_INTERVAL)

})()

實作 alice.js 邏輯

接下來需要實作與 bob.js 互動的 alice.js

實作流程如下

  • 建立 alice.js
  • 引入所需要函式庫
  • 定義一些需要 const 如下
const token = 'ETH'
const amountToDeposit = '0.05'
const amountToTransfer = '0.02'
const amountToWithdraw = '0.002'
  • 實作連接到 Rinkeby 與 zkSync 的邏輯

以下是 alice 得操作流程

  1. alice 存 ETH 到 zkSync 帳號
  2. alice 註冊公鑰到 zkSync
  3. alice 在 zkSync 建立一個 payment 給 bob
  4. alice 從 Ethereum 提出 ETH 到 zkSync

實作步驟

  1. 呼叫 utils.depositToZkSync
  2. 顯示 alice 在 zkSync 的 balance
  3. alice 呼叫 utils.registerAccount 在 zkSync 註冊公鑰

更新如下:

(async () => {
  const ethers = require('ethers')
  const zksync = require('zksync')
  const utils = require('./utils')
  const token = 'ETH'
  const amountToDeposit = '0.05'
  const amountToTransfer = '0.02'
  const amountToWithdraw = '0.002'

  const zkSyncProvider = await utils.getZkSyncProvider(zksync, process.env.NETWORK_NAME)
  const ethersProvider = await utils.getEthereumProvider(ethers, process.env.NETWORK_NAME)
  console.log('Creating a new Rinkeby wallet for Alice')
  const aliceRinkebyWallet = new ethers.Wallet(process.env.ALICE_PRIVATE_KEY, ethersProvider) // Account #78
  console.log(`Alice's Rinkeby address is: ${aliceRinkebyWallet.address}`)
  const aliceInitialRinkebyBalance = await aliceRinkebyWallet.getBalance()
  console.log(`Alice's initial balance on Rinkeby is: ${ethers.utils.formatEther(aliceInitialRinkebyBalance)}`)

  console.log('Creating a zkSync wallet for Alice')
  const aliceZkSyncWallet = await utils.initAccount(aliceRinkebyWallet, zkSyncProvider, zksync)

  console.log('Depositing')
  // Start here
  await utils.depositToZkSync(aliceZkSyncWallet, token, amountToDeposit, ethers)
  await utils.displayZkSyncBalance(aliceZkSyncWallet, ethers)
  await utils.registerAccount(aliceZkSyncWallet)
})()

實作在 zkSync 建立 payment

  1. 呼叫 utils.getFee 來讀取轉移需要的 fee 並且存入 const transferFee
  2. 執行 utils.transfer

更新如下:

(async () => {
  const ethers = require('ethers')
  const zksync = require('zksync')
  const utils = require('./utils')
  const token = 'ETH'
  const amountToDeposit = '0.05'
  const amountToTransfer = '0.02'
  const amountToWithdraw = '0.002'

  const zkSyncProvider = await utils.getZkSyncProvider(zksync, process.env.NETWORK_NAME)
  const ethersProvider = await utils.getEthereumProvider(ethers, process.env.NETWORK_NAME)
  console.log('Creating a new Rinkeby wallet for Alice')
  const aliceRinkebyWallet = new ethers.Wallet(process.env.ALICE_PRIVATE_KEY, ethersProvider) // Account #78
  console.log(`Alice's Rinkeby address is: ${aliceRinkebyWallet.address}`)
  const aliceInitialRinkebyBalance = await aliceRinkebyWallet.getBalance()
  console.log(`Alice's initial balance on Rinkeby is: ${ethers.utils.formatEther(aliceInitialRinkebyBalance)}`)

  console.log('Creating a zkSync wallet for Alice')
  const aliceZkSyncWallet = await utils.initAccount(aliceRinkebyWallet, zkSyncProvider, zksync)

  console.log('Depositing')
  await utils.depositToZkSync(aliceZkSyncWallet, token, amountToDeposit, ethers)
  await utils.displayZkSyncBalance(aliceZkSyncWallet, ethers)
  await utils.registerAccount(aliceZkSyncWallet)

  console.log('Transferring')
  // Start here
  const transferFee = await utils.getFee('Transfer', aliceRinkebyWallet.address, token, zkSyncProvider, ethers)
  await utils.transfer(aliceZkSyncWallet, process.env.BOB_ADDRESS, amountToTransfer, transferFee, token, zksync, ethers)
})()

實作 withdraw token 到 ethereum

  1. 宣告 const withdrawalFee 用來存放執行 utils.getFee 的結果
  2. 執行 utils.withdrawToEthereum

更新如下

(async () => {
  const ethers = require('ethers')
  const zksync = require('zksync')
  const utils = require('./utils')
  const token = 'ETH'
  const amountToDeposit = '0.05'
  const amountToTransfer = '0.02'
  const amountToWithdraw = '0.002'

  const zkSyncProvider = await utils.getZkSyncProvider(zksync, process.env.NETWORK_NAME)
  const ethersProvider = await utils.getEthereumProvider(ethers, process.env.NETWORK_NAME)
  console.log('Creating a new Rinkeby wallet for Alice')
  const aliceRinkebyWallet = new ethers.Wallet(process.env.ALICE_PRIVATE_KEY, ethersProvider) // Account #78
  console.log(`Alice's Rinkeby address is: ${aliceRinkebyWallet.address}`)
  const aliceInitialRinkebyBalance = await aliceRinkebyWallet.getBalance()
  console.log(`Alice's initial balance on Rinkeby is: ${ethers.utils.formatEther(aliceInitialRinkebyBalance)}`)

  console.log('Creating a zkSync wallet for Alice')
  const aliceZkSyncWallet = await utils.initAccount(aliceRinkebyWallet, zkSyncProvider, zksync)

  console.log('Depositing')
  await utils.depositToZkSync(aliceZkSyncWallet, token, amountToDeposit, ethers)
  await utils.displayZkSyncBalance(aliceZkSyncWallet, ethers)
  await utils.registerAccount(aliceZkSyncWallet)

  console.log('Transferring')
  const transferFee = await utils.getFee('Transfer', aliceRinkebyWallet.address, token, zkSyncProvider, ethers)
  await utils.transfer(aliceZkSyncWallet, process.env.BOB_ADDRESS, amountToTransfer, transferFee, token, zksync, ethers)

  console.log('Withdrawing')
  // Start here
  const withdrawalFee = await utils.getFee('Withdraw', aliceRinkebyWallet.address, token, zkSyncProvider, ethers)
  await utils.withdrawToEthereum(aliceZkSyncWallet, amountToWithdraw, withdrawalFee, token, zksync, ethers)
})()

參考文獻

https://www.blocktempo.com/zero-knowledge-proof-zkp-chagelly-column/


上一篇
從以太坊白皮書理解 web 3 概念 - Day20
下一篇
從以太坊白皮書理解 web 3 概念 - Day22
系列文
從以太坊白皮書理解 web 3 概念32
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言