Image Source: zkSync Tweets
本文主要介紹 Account Abstraction(AA、抽象帳戶)在 zkSync 這個 Layer2 Solution 的發展與相關內容。而重點會放在三個部分:
帳戶合約:帳戶型態,帳戶合約的重要 Entry Point 和相關重點
交易:AA 交易的驗證方式和執行方式、流程
手續費:交易手續費、Paymaster
Author: ChiHaoLu(chihaolu.eth) @ imToken Labs
Special thanks to NIC Lin & Cyan Ho for reviewing this post.
Intro
Quick Look at the zkSync AA Contract
Fee Model and Paymaster in zkSync Era
Summary & Comparison
Closing
熟悉智能合約錢包與其常見的 features
大致了解 Ethereum 的交易如何運作
大致了解 EIP-4337 運作模式
大致了解 ZK(Validity)Rollup 運作模式
這邊為了幫助大家在不需要深入理解 zkSync 的情況下閱讀,快速的 recap 一下 zkSync 的基本資訊,zkSync 主要有分兩個版本,1.0(zkSync Lite)與 2.0(zkSync Era)。
zkSync 1.0 僅能使用 EOA 且不支援智能合約(只支援 token transfer 和 swap),而 zkSync 2.0 也就是 zkSync Era,屬於 Native AA(原生帳戶型態都是合約,沒有 EOA,也就是沒有 Ethereum 中的 EOA 和 Contract Account 的差別),同時為 EVM-Compatible,支援使用 Rust、Yul、Vyper、Solidity 開發智能合約。
下文提到的 zkSync 若無特別指稱,都指的是 zkSync 2.0,也就是 zkSync Era。
在 zkSync Era 中還有多個 System Contract,可以理解成是他們把 zkSync 一些重要 OS 功能實作在智能合約中。這些 System Contract 都是 precompiled contract,從未被部署(直接跑在節點裡),但它們都具有一個正式地址。
執行 AA Protocol 時 zkSync 會透過一些 System Contract 來做邏輯運算和判斷,例如驗證 nonce 時是由 NonceHolder 來判斷,而執行抽象帳戶的機制和收取手續費是由 bootloader 來判斷,下文會一一介紹他們。
Account Abstraction 的核心概念可以總結為兩個關鍵點:Signature Abstraction(簽章抽象)和 Payment Abstraction(支付抽象)。
Signature Abstraction 的目標是使各種帳戶合約能夠使用不同的驗證方案。這意味著使用者不受限於只能使用特定曲線的數位簽章演算法,而可以選擇任何他們喜好的驗證機制。
而 Payment Abstraction 旨在為使用者提供多種交易支付選項。例如,可以使用 ERC-20 代幣進行支付,而不是使用原生代幣,或者可以由第三方贊助交易,甚至是其他更特別的 payment model。
zkSync 2.0 中的帳戶可以發起交易,就像 EOA 一樣,但也可以在其中利用可編程性實現任意邏輯,如 Contract Account。這就是我們所說的 Account Abstraction,它融合 Ethereum 中兩種帳戶型態的優勢,使 AA 帳戶的使用者體驗更加靈活,進而達到上述的兩種目標:Signature Abstraction 和 Payment Abstraction。
zkSync AA 中最重要的角色分別為 bootloader,他是一個 System Contract,他主要用來處理交易以及執行 AA 的機制,對應著 4337 的 EntryPoint Contract。bootloader 無法被使用者呼叫(僅能被 Operator 觸發),也從未被部署(直接跑在節點裡),但它具有一個正式地址(可以用來收款)。
Operator 是 ZK rollup 中的一位重要角色,是中心化的 Off-Chain Server,與大家可能會看過的 Sequencer 同義,負責從外部觸發 bootloader 等 System Contract。
原生的 Account Abstraction Protocol(e.g. StarkNet、zkSync)基本上都是參考 EIP-4337 進行設計,zkSync 的實現上用戶會將交易送給 Operator,Operator 就會開始把交易送給 bootloader,並開始一系列的處理。
從區塊的角度看:
當 bootloader 接收到來自 Operator 的輸入,bootloader 會先替該區塊定義一些環境變數(e.g. gas price、 block.number
、block.timestamp
等)。之後 bootloader 會依序讀取交易列表,先詢問該帳戶合約是否同意該交易(也就是 AA 機制中的呼叫 validate function),再把他們放進區塊中。
每一筆交易驗證通過之後 Operator 會驗證該區塊是否足夠大以便發送給證明者(或是否已超時)。如果夠大或超時 Operator 就會關閉該區塊,停止向 bootloader 添加新的交易,並完成交易執行。
從交易的角度看,bootloader 被 Operator 觸發後會依序對每一筆交易:
確認用戶 Account Contract Address 對應的 nonce 是否合法
呼叫用戶 Account Contract 上的 validate function 進行驗證
驗證通過之後 Account Contract 會把 gas fee 匯進 bootloader 的地址(或透過 Paymaster,下文會介紹),bootloader 會檢查自己是否有收到足夠的款項。
呼叫用戶 Account Contract 上的 execute function 執行交易
以上的前三步就是對應著 4337 的 Verification Loop,第四步則對應著 4337 的 Execution Loop。
這邊主要做一個 overview 的介紹,每一步細節跟角色都會在之後詳細說明。
zkSync 的帳戶 nonce 是記錄在一個 System Contract - NonceHolder 裡面,以 mapping 記住一組又一組的 (account_address, nonce)
pair 是否有使用過來判斷 nonce 是否合法。
從上文我們知道 bootloader 被 Operator 觸發後第一步是檢查 nonce,所以在每筆交易開始之前,NonceHolder 會被用來確認當前使用的這組 nonce 是否合法(目前只檢查有沒有用過)。如果 nonce 合法,則進入到 Verification Phase,此時 nonce 就已經會被標記為 used;不合法則交易(驗證)失敗。
關於 zkSync 當前的 nonce 的重點:
雖然當前使用者可以同時送出多筆不同 nonce 的交易到 Account 執行,但因為 zkSync 不支援平行化處理,所以不同 nonce 的交易還是會依序被處理。
理論上用戶可以使用任何 256-bit 的非 0 整數作為 nonce,但 zkSync 還是推薦使用 incrementNonceIfEquals
來作為管理 nonce 的方式,以確認他是依序遞增的(當前 zkSync 的 AA 機制並不會確認依序遞增,只確認沒用過,但官方文件表示未來會會要求依序遞增)。
在 zkSync 中的 Account Contract 有以下四個必要的 Entry Point,分別為:
validateTransaction
在 Verification Phase 被呼叫,以確認這次操作是被帳戶的擁有者授權的,用戶可以在這邊客製自己的驗證邏輯(e.g. 各種簽章演算法、多簽等)。
payForTransaction
,當交易手續費由這個帳戶支付(而非使用 paymaster)時,Operator 會呼叫這個函式來向 bootloader
address 支付至少 tx.gasprice * tx.gasLimit
的 ETH。
prepareForPaymaster
當交易手續費會由 Paymaster 支付,Operator 會呼叫這個函式來完成一些與 paymaster 互動的前置動作。zkSync 提供的範例是 approve Paymaster 的 ERC-20 token。
executeTransaction
,在 Verification Phase 成功通過且成功收取手續費之後,此函式會用來完成用戶想要達到的 operation(e.g. 跟合約互動、匯款等行為)。
關於 Paymaster、手續費數量(
tx.gasprice * tx.gasLimit
)等會在後面的章節解釋。
在 zkSync 的 Account 中還有一個非必要(optional)的保險函式 executeTransactionFromOutside
,當沒有辦法執行函式時(例如 sequencer 沒有反應或發現 zkSync 有監管風險時),可以有一個「逃跑機制」來提取資金到 L1。這個部分與 AA Protocol 沒有太大的關係所以不耗費篇幅敘述,有興趣的人可以查看官方文件與 zkSync 的 spec.。
在 validateTransaction
中可以實作各種客製化邏輯,例如如果 Account 有實作 1271 的話,可以直接把 1271 裡的驗證邏輯也套用在 validateTransaction
中,或參考 zkSync 官方文件的實作 MultiSig Account Contract。
同時,在 4337 中的 Verification Phase 為了避免 DoS 威脅有一些限制(不能觸及外部的 Opcodes 還有 Limited Depth 等),在 zkSync 也有同樣的限制,例如:
合約邏輯只可以碰到自己的 slots(Account Contract 的地址若為地址 A):
屬於地址 A 的 Slots
任何其他地址的 Slots A
任何其他地址的 Slot keccak256(A || X)
:同意直接使用地址作為 mapping 的 key(例如 mapping(address => value)
),等同於同意碰觸 slot keccak256(A || X)
,以此方式進行擴增。例如 ERC-20 上的 token balance。
合約邏輯不可以使用到 Global 變數,例如 block.number
executeTransaction
需要注意的是如果要執行 System Call,需要確認有 isSystem
flag。因為這些 System Contract 對帳戶系統的影響非常大,例如增加 nonce 的唯一方式是與 NonceHolder 互動,要部署合約必須與 ContractDeployer 互動,藉由這個 isSystem
flag 可以確保帳戶開發者是有意識地要與 System Contract 互動。
然而實作上建議可以引用 SystemContractsCaller
這個 zkSync 提供的 Library 以避免自己處理 isSystem
flag,並使用其中的 systemCallWithPropagatedRevert
完成 System Call。
function _executeTransaction(Transaction calldata _transaction) internal {
address to = address(uint160(_transaction.to));
uint128 value = Utils.safeCastToU128(_transaction.value);
bytes memory data = _transaction.data;
if (to == address(DEPLOYER_SYSTEM_CONTRACT)) {
uint32 gas = Utils.safeCastToU32(gasleft());
// Note, that the deployer contract can only be called
// with a "systemCall" flag.
SystemContractsCaller.systemCallWithPropagatedRevert(gas, to, value, data);
} else {
bool success;
assembly {
success := call(gas(), to, value, add(data, 0x20), mload(data), 0, 0)
}
require(success);
}
}
上面程式碼的例子是與
DEPLOYER_SYSTEM_CONTRACT
互動,帳戶開發者最常碰到 System Contract 的情況就是我們要用帳戶去部署一個合約,此時必須要跟 ContractDeployer 這個 System Contract 互動。
zkSync 的費用模型與以太坊非常類似,Fee Token 也為 ETH,然而 zkSync 除了基本運算和寫入 slot 的成本之外,和其他的 Layer2(Arbitrum、Optimism)一樣需要考量發布到 L1 的額外成本(安全費用)。由於發布資料到 L1 上的 Gas Price 非常不穩定,所以在每個區塊開啟(開始收錄交易)時,zkSync 的 Operator 會定義以下動態參數:
gasPrice:以 gwei 為單位的 gas 價格,也就是前文提到的 transaction object 中的 tx.gasprice
gasPerPubdata:在以太坊上發布一個 byte 的資料所需的 gas 數量
此外,zkSync 不像 4337 一樣需要定義三種 Gas Limits:verificationGas
、executionGas
和 preVerificationGas
,只單由一個 gasLimit
就包含以上所有的 fee cost,所以用戶需要自行確認 gasLimit
足以涵蓋 Verification Phase、Execution Phase 還有上傳資料到 L1 的安全費用等全部 fee cost。這個就是前文提到的 transaction object 中的 tx.gasLimit
。
將兩者相乘(tx.gasprice * tx.gasLimit
)就能得到這筆交易所需要支付給 bootloader 的手續費數量。
Paymaster 主要在用戶交易的支付手續費階段時,代替 User 的 Account Contract 支付 ETH 給 bootloader,用戶可以選擇他們想要的 Paymaster 以及不同的 Payment Model 進行支付手續費,例如(但不限於):
在交易發起前或交易執行後支付 ERC-20 給 Paymaster
使用 Credit Card 儲值進入 Paymaster Contract
Paymaster 會不斷為 User 免費支付部分或全額手續費
用戶要如何與 Paymaster 互動需要根據不同的協議而定,可以是中心化也可以是去中心化;可以在交易前,也可以在交易後;可以使用 ERC-20 也可以使用法幣,甚至可以免費。
zkSync 的 Paymaster Contract 主要由兩個函式構成,分別為 validateAndPayForPaymasterTransaction
(required) 與 postTransaction
(optional),兩者都只能被 bootloader 呼叫:
validateAndPayForPaymasterTransaction
是整個 Paymaster Contract 中唯一必須實作的 function,當 Operator 收到的交易有附帶 paymaster params 時,代表手續費不由 User 的 Account Contract 支付,而是由 Paymaster 支付。此時 Operator 就會呼叫 validateAndPayForPaymasterTransaction
來判斷這個 Paymaster 是否願意支付這筆交易。如果 Paymaster 願意,這個函式會 send 至少 tx.gasprice * tx.gasLimit
給 bootloader。
postTransaction
是一個 optional 的函式,通常用於 refund(將未使用完的 gas 退還給發送者),但當前 zkSync 還不支援此操作。
zkSync 中的 Paymaster 有實作
postTransaction
才會執行postTransaction
,這個部分有別於 4337,4337 如果在validatePaymasterUserOp
沒有回傳 context 的話就不會呼叫postOp
,反之則會。
綜合以上,舉例來說用戶現在想要發送一筆手續費由 Paymaster 支付的交易,那流程如下:
藉由 NonceHolder 確認 nonce 是否合法
呼叫用戶 Account Contract 上的 validateTransaction
進行驗證,確認交易由帳戶擁有者授權
呼叫用戶 Account Contract 上的 prepareForPaymaster
,裡面可能會執行例如 approve 一定數量的 ERC-20 Token 給 Paymaster 或是不做任何事
呼叫 Paymaster Contract 上的 validateAndPayForPaymasterTransaction
確認 Paymaster 願意支付並且收取手續費,同時 Paymaster 向用戶收取一定數量的 ERC-20(前面 approve 的)
確認 bootloader 收到正確數量(至少 tx.gasprice * tx.gasLimit
)的 ETH 手續費
呼叫用戶 Account Contract 上的 executeTransaction
執行用戶想要的交易
如果 Paymaster Contract 有實作 postTransaction
且 gas 仍然足夠(沒有 out of gas error),那就執行 postTransaction
最後一步即便 out of gas error 導致不能執行
postTransaction
,這筆 AA 交易也算是成功,只是省略掉呼叫postTransaction
的動作而已。
更深入探究 zkSync 的 Paymaster 會發現它的 Verification Rules 和 4337 稍有不同(zkSync Paymaster 可以踩任何其他合約的 slot)、同時也有各種不同的 type(e.g. Approval-based),這部分由於比較細節所以有興趣深入的人可以參考官方文件或我之前的實作。
經過上文的解釋我們已經知道一個帳戶合約有哪些重點 Entry Point,以及他們的作用、相關限制,同時也知道 System Contract 是什麼,接下來我們重新梳理一次一筆 AA 交易在 zkSync 中怎麼從建置到完成,同時我也會附上更細節的 ref 讓有興趣深入了解的人可以參考:
用戶在本地端使用 SDK 或錢包建置 Transaction Object(e.g. from, to, data, value...)
用戶對這個 Transaction 簽章,這裡的簽章不一定真的是一個 EIP-712 格式且 ECDSA 曲線的簽章,zkSync 也支援 2718 和 1559,且選擇何種曲線或驗證方式的重點是要通過 Account Contract 中的 validate function
將這個已簽的 Transaction 透過 RPC API 送去給 Operator,這時會進入 Pending 狀態,Operator 把這筆交易送給 bootloader(呼叫 bootloader contract 上的 processL2Tx)執行一連串的 AA Protocol
booteloader 會檢查 Nonce 是否合法(利用 NonceHolder)
booteloader 會呼叫 Account Contract 上的 validateTransaction
確認此筆交易是經過帳戶的擁有者授權的
booteloader 獲取手續費的方式有兩種,藉由何種方式來 collect fee 依照交易參數(組建 transaction object 時是否有附帶上 paymaster params)而定:
呼叫 payForTransaction
來跟 Account Contract 收取手續費
呼叫 prepareForPaymaster
和 validateAndPayForPaymasterTransaction
來跟 Paymaster Contract 收取手續費
檢查 bootloader 有收到至少 tx.gasPrice * tx.gasLimit
數量的交易手續費
booteloader 會呼叫 Account Contract 上的 executeTransaction
來執行交易
(optional)如果利用 Paymaster 來支付手續費,booteloader 會呼叫 postTransaction
,如果 Paymaster 沒有實作 postTransaction
或 gas 已經耗盡就不會有這一步
以上的 4.~7. 步為 Verification Phase(定義在 bootloader 的 l2TxValidation),第 8.~9. 步 Execution Phase(定義在 bootloader 的 l2TxExecution)。
基本上這三者的 AA 機制流程都相仿,皆為 Verification Phase → 手續費機制(由 Account Contract 支付或者 Paymaster)→ Execution Phase,主要差別有:
執行 AA 機制的角色是誰:在 zkSync Era 中最主要與其他兩者 AA 的差別在於 Operator 需要和 bootloader(System Contract)一起配合,例如 bootloader 會開啟一個新區塊並且定義該區塊的相關參數,接收 Operator 送來的交易們並且進行驗證。在 4337 中這部分由 Bundler 與 EntryPoint 協作,而在 StarkNet 中這部分全部都由 Sequencer 負責。
Gas Cost 是否需要考量到 L1 安全費用:L2 的 AA 都需要考慮這個上傳資料到 L1 的費用,不只是這邊提到的 ZK(Validity)Rollups Native AA,在 Optimistic Rollups 實作 4337 時也需要算入 L1 安全費用(算在 preVerificationGas
中,細節可見 Alchemy 相關文件)。
是否可以在 Account Contract 部署前送出交易:在 StarkNet 和 zkSync Era 中都沒有像 4337 的 EntryPoint 有 initCode
這個 field 能讓其替用戶部署 Account Contract,所以都不可以在部署帳戶前送出交易。
由於 StarkNet 尚無已實現的 Paymaster 機制、zkSync 也尚未完成 gas refund 機制的設計,所以一些比較細節的比較在這裡就沒有列出。
此外,目前的 4337 bundler 們並未完成 P2P mempool,且 zkRollups 的 Sequencer 和 Operator 也都還是唯一的官方 server,所以都有一定中心化的成分存在。
在開發流程上 zkSync 由於沒有與各家 bundler 串接的問題(只需要與 Operator API 互動),所以使用起來相較 4337 容易,開發帳戶合約(SDK)的體驗也會更好;同時 zkSync 可以使用 Solidity 作為合約開發語言,所以也不像在 StarkNet 開發需要跨過 Cairo 的門檻。
因為 StarkNet 以及 zkSync 都屬於 Native AA,所以大家也可以參考我之前寫過的 StarkNet AA 介紹文章 - Introduction of StarkNet Account Abstraction,或者是其他跟 4337 有關的文章們: