Account Abstraction 抽象帳戶:EIP-2938 簡介

ChiHaoLu
Taipei Ethereum Meetup
19 min readSep 8, 2022

本文主要由 EIP-2938 介紹 Account Abstraction (AA) 的相關內容。

Table of Contents

  • Introduction
  • Cast an eye over Account Abstraction
  • Transaction Validation
  • Examples Quick Look
  • Closing

Introduction

在 Ethereum 中有兩種 accounts,分別為 EOA 和 Contract Account,而 EOA 的所有權簽核權理論上是同一個個體單位持有,簡單來說便是:持有私鑰的人不只擁有這個 Account 的「所有權」,同時還可以「任意轉移所有資產」。這個定義是被刻在以太坊上的,然而現今的 EOA 設計可能會衍伸出一些值得討論的問題。

  1. 失去私鑰(遺失、遭駭、密碼學上的被破解)意味著毫無疑問地失去所有資產
  2. 原生協議在驗證交易上只能使用 ECDSA 這個簽章演算法
  3. 一筆交易無法透過多簽授權(只能透過智能合約實作)
  4. 手續費只能透過 ether 支付
  5. 一對一的交易容易對應出帳戶持有者的隱私資訊,或在隱私應用(Privacy Solution)上需要依賴 relayer。

目前有許多新興的帳戶概念,例如使用 Relay Server 的 Meta Transaction、多簽錢包,以及將要介紹的 Account Abstraction 抽象帳戶。

Account Abstraction 能夠將所有權(Owner)簽核權(Signer)解耦(decouple)。

Account Abstraction 很像是擁有 EOA 特性的 Contract Account,它到底哪裡比較優勢呢?實際上讓交易和帳戶從底層脫離成為 High-Level 智能合約的角色,能夠做到:

  1. 多簽錢包或 Social recovery
  2. 使用其他簽章演算法:過往只能使用以 Address 作為 Verification Data 的 ECDSA ,但在 AA 中可以使用其他演算法,例如: Multisig verification、Schnorr sigs、BLS sigs、Quantum-resistant sigs (e.g. Lamport, Winternitz)。
  3. 將一連串的 operations 包裝成一個不可分割的(atomic)交易行為
  4. 可升級

Background

這篇文章會希望讀者已經對 EOA、Contract Account 有一定的了解,以及熟悉 Public/Private Key、Signature、nonce,尤其是 Transaction & State(e.g. Balance)、EVM 等相關概念。

Cast an eye over Account Abstraction

Account Abstraction 希望將帳戶的概念移植到智能合約之中,就和我們時常看見的 Multi-Sig. Contract 很像,只是 Multi-Sig. Contract 仍然是由一個 EOA 作為 Gas Account 的角色來發起交易和支付 Gas,交易驗證方式也和往常相差無幾,而 AA 希望可以透過智能合約來作為唯一支付者。

AA Contract 要做的事情是有個 相同的介面標準(Interface) 供大家驗證或執行交易。

相關概念歷經了以下的提案慢慢完整中:

EIP-2938

EIP-2938: Account Abstraction 首先提出了一個完整的 AA 概念,可以說是 AA 的一種 specification。提案中表示:像 AA 這樣一個特殊的 Contract Account 可以接收和發送「指定型態的交易」、以各種方式支付交易手續費,以及能夠使用程式碼去定義交易的 maximum gas 和 verification method。

New Type Transaction

在這樣一個「新的帳戶 & 交易」設計裡與「現今的 EOA & 交易」有不少的差別:

  1. 新的交易型態名稱為:AA_TX_TYPE
  2. 需要新的 OpCodes PAYGAS (0x49)NONCE (0x48)。前者待下文詳述;後者能夠 push 交易物件中的 nonce field 到 EVM stack 中(未來能與 contract 中儲存的 nocne 變數進行驗證 :tx.nonce == contract.nonce,文末有相關程式碼範例)。
  3. 需要新的 Global Variable:globals.transaction_fee_paidglobals.gas_priceglobals.gas_limit
  4. 交易物件也和我們原本認識的 fromtogasgaslimitvaluev, r, s 等 fields 不同,AA 的交易物件主要由三個 fields 構成:target(為 AA contract’s address)、noncedata
  5. 交易的 payload 為 rlp([nonce, target, data])
  6. 這個特殊的交易形式會把 tx.origin 設為常數 AA_ENTRY_POINT = 0xffff...ff,此地址是既定的 AA entry point。

由於所有的帳戶都將變成「合約」,每一筆交易都會變成一個「Call」,「這個 Call」的起源(tx.origin)都需要是定義好的 entry point address(eg. 0xffff…ff)。換句話說 AA contracts 需要檢查每次交易的 tx.origin 是否是 AA_ENTRY_POINT 這個環境變數的地址。

Classification

我們可以把 Abstract Acoount 分成兩種類別,差別在於這個 AA 用來支援少數用戶或大量用戶:

  1. Single-Tenant AA
  2. Multi-Tenant AA

Multi-Tenant AA 會被 Tornado.Cash 或 Uniswap 這樣的應用程式使用,設計中會有一個主合約來代表這個應用程式,當然根據需求實作的時候要設計一個專屬於此 Dapp 的交易驗證方式。在文末我會提到 Tornado.Cash 使用 AA 的範例介紹。

One Account One Pending Transaction

在 EIP-2938 裡的節點 mempool 中,每一個 AA 帳戶僅能有一個 pending transaction。

提出者也計畫未來滿足 Multi-Tenant AA 的功能時,會對單一個 account 支援多個 pending transactions,這部分的主要挑戰在於「單一筆交易可能使其他所有交易失效」,以及「單純以 gas price 作為交易的優先次序,可能會導致惡意交易排擠其他交易」。

在 AA 的交易型態下,交易驗證變得比現在彈性許多,從節點的 tx mempool 角度來看,要驗證交易就跟在合約中驗證一樣,需要 EVM 執行之後才能知道結果,這使得惡意交易可以排擠在 tx mempool 中的其他交易。

EIP-2938 針對多個 pending tx 提出來的一種減輕方案(尚未完成)為:

  1. 使用 EIP-2930-style 的 access list。這一個 List 會詳述每一筆交易需要使用(read / write)到的 storage slot,因此如果踩到 list 以外範圍的交易就會被拒絕。衍伸閱讀:Account read/write lists
  2. 在 mempool 中,每筆交易的 Access List 都必須與其他交易的 list 互相獨立(因為交易彼此之間有相依性容易排擠其他交易),除非兩筆交易中有 gasprice 的差距(能夠排出優先次序)。

然而在 multi-tenant AA 的應用應該很難避免去讓交易彼此不重疊,因此 access list 可能比較適合 single-tenant AA 使用。

在 multi-tenant AA 的情況中 miner 仍然可以去修改每一筆接收到的 transaction 的 nonce,因此最後交易的次序是無法被預測的,用戶需要特別去注意這件事情。

Transaction Validation

過往節點對於交易的驗證只需要確認「簽章合法、nonce 合法、gas 與 balance 足夠」,在 AA 中這些交易驗證都需要移動到合約裡(以 EVM 運算驗證),成為 top-level code execution。而節點還是需要有能力去判斷這些交易是否合法與真的付了 fee,也就是待會提到的 Node 驗證內容

驗證階段主要分為兩個部分:PAYGAS 以前的驗證階段(verification phase),與 PAYGAS 以後的執行階段(execution phase)。

  • 前者限制交易只能閱讀合約的 storage(e.g. public keys, anti-replay nonces, Merkle roots) 以及不能更改任何狀態,這個階段失敗了礦工就不會收入這筆交易
  • 後者因為已經支付了 fee,如果交易在這個階段失敗,礦工仍然可以選擇要不要加入這筆交易。
Source: Implementing account abstraction as part of eth1.x

PAYGAS & Verification Phase

當交易發起時,第一步會從 EVM Stack 獲得 gas_pricegas_limit ,接下來進行一連串的檢查:

  1. Contract’s balance >= gas_price * gas_limit
  2. globals.transaction_fee_paid == False:此條件等同於 type(tx) == AA_TX_TYPE
  3. 仍然在執行合約(EVM 運作)階段。

如果以上三點條件都滿足,則執行下列步驟:

  1. 從 Contract’s balance 扣掉 gas_price * gas_limit
  2. globals.transaction_fee_paid 設為 true
  3. globals.gas_price 設為 gas_price,將 globals.gas_limit 設為 gas_limit
  4. 設定 remaining gas = gas_limit - gas_consumed

以上交易步驟結束之後:

  1. 會將 globals.gas_price * remaining_gas 退款回 contract 中
  2. 並把 (globals.gas_limit - remaining_gas) * globals.gas_price 作為獎勵給予礦工

透過以上的檢查點與步驟,PAYGAS (0x49) 能夠建立一個不可逆的(irreversible)檢查點來確保 PAYGAS 之前的狀態改變不能夠被 reverse,且交易只查看了合約內部的狀態(不包含 pre-compile)

Nodes Policy

傳統的交易中,節點會去檢驗 nonce 的合法性,還原出簽章地址之後查看是否有足夠的 balance 以及 gas fees。

在 AA 的新形態交易中:節點一樣會去驗證 nonce 的合法性(tx.nonce == tx.target.nonce),也就是不能有高過於當前已驗證 nonce 的 transaction。但因為面對的是新型態的交易,節點不會驗證既往的簽章而是驗證其他內容,步驟如下:

  1. 檢查 entry point:如果 target 的程式碼(bytecode)沒有 AA_PREFIX: if(msg.sender != shr(-1, 12)) { LOG1(msg.sender, msg.value); return } 作為 prefix 則終止並回傳錯誤;這個檢查等價於在合約最前方加上 require(msg.sender == ENTRY_POINT)
Nit: ENTRY_POINT = shr(-1, 12) = 2**160 - 1 = 1.4615016e+48

2. 在 PAYGAS 之前使用到以下 OpCodes 則終止並回傳錯誤:

  • Environment Opcodes: BLOCKHASH COINBASE, TIMESTAMP, NUMBER, DIFFICULTY, GASLIMIT
  • 任何帳戶的 BALANCE(包含 target
  • 任何會將 callee 轉為除了 target 以外角色的 external call/create:CALL, CALLCODE, STATICCALL, CREATE, CREATE2
  • 任何存取外部狀態的行為:EXTCODESIZE, EXTCODEHASH, EXTCODECOPY, CALLCODEDELEGATECALL

3. 執行期間消耗的 gas 多於 VERIFICATION_GAS_CAP = VERIFICATION_GAS_MULTIPLIER * AA_BASE_GAS_COST = 90000 則終止並回傳錯誤;這個上限是為了防止節點花費一堆 gas 結果最後驗證失敗,釀成 DoS 風險。

DoS 延伸閱讀:DoS Vectors in Account Abstraction (AA) or Validation Generalization, a Case Study in Geth

4. 遇見 PAYGAS 後會檢查是否有足夠的 balance,如果是的話則接受該交易驗證,否則拒絕此筆交易

以上的限制可以確保所有的狀態存取只能在合約之中,也只有合約本身可以去改動這些狀態。

Source: A recap of where we are at on account abstraction

回到我們之前提過的:One Account One Pending Transaction。

如果現在 mempool 中 AA account 已經存在一個 valid nonce 的 transaction,當一個相同 nonce 的新 transaction 進入此 contract 時,只會有兩種情況:

  1. 去取代當前的 valid transaction(如果新加入者 gas price 比較高)
  2. 新加入的 transaction 被拋棄

因此 mempool 中每個 account 最多只會保留一個 pending transaction。

Examples Quick Look

Example 1 — AA Playground

我們可以看一下 Account Abstraction Playground 中的 wallet.sol,這是一個模擬 EIP-2938 的 AA Wallet 例子,需要注意這個模擬範例並不是定案,也跟 EIP-4337EIP-3074 的實作方式不同!

首先引入套件跟宣告版本:

// SPDX-License-Identifier: MITpragma solidity 0.6.10;
pragma experimental AccountAbstraction;
import { ECDSA } from "./ECDSA.sol";

接下來開始宣告合約物件:

  1. 能看見跟往常不一樣,在最前方出現了一個新的關鍵字 account 能夠讓合約接收 AA Transaction,以及 emit 特定事件。
  2. 宣告 nonce 讓 AA contract 可以達到 replay protection。
  3. 從這個 AA Contract 要進行交易時需要 owner 的 signature,如果我們今天要使用多簽功能,則這個部分的 owner 可以有複數個(當然後面的 transfer() 等相關函式都要針對這種情況處理)。
account contract Wallet is ECDSA {
uint256 nonce;
address owner;

constructor() public payable {
owner = msg.sender;
}

使用上 assembly { paygas(gasPrice)} 能利用 Opcode PAYGAS 達到「告知交易是合法」以及「檢查點」的作用,在 transfer() 中我們也能看到同樣的作用。

AA 合約在執行時,無論是對狀態 write 或 read 都需要 PAYGAS 作為 signal,因此下方程式碼的 Get Function 會加上這個部分的 modifier。傳 0 是因為發起 read function 時不需要支付 gas,但跟我們一開始說的一樣,仍須要有一個 signal 存在。

    modifier paygasZero {
assembly { paygas(0) }
_;
}
function getNonce() public view paygasZero returns (uint256) {
return nonce;
}

function getOwner() public view paygasZero returns (address) {
return owner;
}
}

transfer() 會在 high-level 的 AA Contract 中模擬 low-level 運作的交易。

以下的重點在於:

  1. 這裡傳入了 signature 表示此筆交易需要 owner 的簽核
  2. AA Contract 可以不必使用 ECDSA 作為簽章演算法
  3. 交易物件是彈性的,每個 AA Contract 可以自己決定交易物件的內容與需求
    function transfer(
uint256 txNonce,
uint256 gasPrice,
address payable to,
uint256 amount,
bytes calldata signature
) public {
assert(nonce == txNonce);
bytes32 hash = keccak256(abi.encodePacked(this, txNonce, gasPrice, to, amount));
bytes32 messageHash = toEthSignedMessageHash(hash);
address signer = recover(messageHash, signature);
require(signer == owner);
nonce = txNonce + 1;
assembly { paygas(gasPrice) }
to.transfer(amount);
}

Example 2 — Privacy Solution: Tornado.Cash

Source: Ethereum Account Abstraction by Vitalik Buterin

另外一個能夠套用 AA 的例子是 Tornado.Cash,現今的 Tornado.Cash 運作方式為我們將資金存入 TC Contract 中並得到一串密鑰,且這筆存款紀錄會存至合約中的 Merkle Root。

當 User 想要提取資金時,需要根據之前的密鑰產生他們真的在 deposit-tree 中的 ZK-SNARK,而 TC Contract 會在成功驗證這個 ZKP 以及資金未被花用後匯款給 User。

然而在 TC 中有一個問題,那就是當收款者(withdrawal address)還沒有任何的 ETH,那他就沒辦法啟動這筆提款的 proving transaction。如果他用存款的帳戶(deposit address)來支付這筆 gas,那就能在鏈上串連起這兩個地址,隱蔽性也就蕩然無存了。

現行的解決方案是需要有一個第三方的 Relayer 來接收 Off-Chain Message ,驗證 ZK-SNARK 以及確認這筆資金未被使用。驗證完畢後提交交易物件給 TC Contract(並獲得 Gas refund 和 fee),最後 TC Contract 會匯出款項。

如果套用 AA 的使用情境就不需要 Relayer 了,交易物件(AA Transaction)可以直接被送到 TC Contract,而 TC Contract 能夠在 verification step(也就是之前提到 PAYGAS 前的第一階段)驗證 Snarks 與確認資金未被花用後,直接送出匯款同時支付 Gas Fee。

這樣 withdrawal address 就能用將要獲得的這筆資金來支付提款交易的手續費。

Closing

關於 AA 比較廣為人知的討論是在 EIP-2938 之後的 EIP-4337EIP-3074,先了解 EIP-2938 可以很大幅度的幫助了解之後的各種延伸 Proposal。以上這些提案大部分都還在研擬、review 甚至是 draft 階段,但 AA 其實在 L2 上有很大的發展性值得我們深入研究。

本文省略了很多討論問題,例如 Replay Protection & Transaction Hash Uniqueness,Protocol-enshrined Nonce, Create2 & Proxy, Bottleneck & Security 等,對這些討論有興趣可以見下方的 Reference 處和提案內文。

這篇文章非常感謝 Chang-Wu ChenNIC Lin 兩位老師提供 review!

Reference

Link Tree

--

--

ChiHaoLu
Taipei Ethereum Meetup

Multitasking Master & Mr.MurMur | Blockchain Dev. @ imToken Labs | chihaolu.me | Advisory Services - https://forms.gle/mVGKQwPQEUP37fLYA