Social Recovery Wallet 社交恢復錢包
過往我們在 Account Abstraction 抽象帳戶:EIP-2938 簡介 及 Account Abstraction 抽象帳戶:EIP-3074 與 EIP-4337 簡介 中提到過:「Account Abstraction 能夠做到多簽錢包或 Social recovery,以及使用其他簽章演算法。」,由於 Social Recovery 乍看之下比較難從字面上理解,所以這邊特別撰文介紹。
Author: ChiHaoLu(chihaolu.eth) @ imToken Labs
Table of Contents
- Introduction of Social Recovery
- Overview the System Design
- Discussion
- Closing
Background
這篇文章會希望讀者已經對 EOA、Contract Account 有一定的了解,以及熟悉 Public/Private Key、Mnemonic、Signature 和 Multi-Sig Contract 等概念。
由於 Social Recovery 並不僅限於 AA,只要是 Contract Wallet 都能夠實作,因此看這篇文章前實際上不需要先閱讀過前兩篇介紹 AA 的文章。
但還是推薦各位去看 AA in EIP-2938 這篇,裡面提到的許多概念會對為什麼新的帳戶設計們會想要脫離原始 EOA 有更深的了解。
給已經知道 AA 是什麼的人:AA 能夠做到 Social Recovery,但 Social Recovery 不一定要 AA。
Introduction of Social Recovery
在以太坊上如何維護私鑰的安全是一個重要的議題,例如怎麼安全存放私鑰,被偷了怎麼辦,忘記了怎麼辦等等的。畢竟在 Web2 的世界通常都有「忘記密碼」這個選項,這也導致以太坊變得難以親近。
因此這些新興帳戶或功能的討論其實都圍繞在一個重點上,那就是如何把 Signer 與 Owner 解耦(或至少不要讓私鑰是一個單點錯誤)。如果我們能找到一個辦法確保「帳戶主人」的身分正確,那即便私鑰失竊了也能進行停用、即便忘記了也能進行恢復(Recovery)。
The First Concept of Recovery
第一個 Recovery 的概念來自於我們最初的錢包設計三巨頭,也就是 BIP-32、BIP-39 以及 BIP-44。
因為任誰都很難正確無誤的記住私鑰,且保存這樣一個無法理解的字串實在過於危險,因此這些 BIP 提出了私鑰種子以及註記詞(mnemonic phrase)的概念。當用戶忘記(非失竊)了自己的私鑰,他們可以透過種子或助記詞這樣的 Root 來 Recover。
然而我們面對的問題只是減輕而非解決,我們依然要面對失去私鑰(或助記詞)就失去一切的情況。
What is Social Recovery Wallet
Social recovery — your wallet supports guardians and a user can recover their wallet if they lose their seed phrase using these guardians. — Cited From Ethereum.org: Adding wallets
言簡意賅地說:Social Recovery 是一個 Contract Account 的 Features,當用戶開了一個新錢包,可以讓他信任的人當 Guardians。當用戶丟失他們的 Contract Account(失竊或遺忘私鑰)時,Guardians 就能夠出面替他們恢復錢包的擁有權與使用權。
更深入一點講:Social Recovery 所要做的就是當 Contract Account 紀錄的 Signing Key 失去帳戶擁有者(Owner)掌控的時候(失竊或遺忘等),能夠透過與預先設定的 Guardians 溝通,去更改合約中紀錄的擁有者地址(一個狀態變數)。 Guardians 將這個變數指向新的公鑰地址,且這個新地址是 Owner 能掌控的(持有對應的私鑰)。
Social Recovery 要 Recover 的是 Account 而不是 Private Key,由於資產是存放在 Contract 中的,更換新的公私鑰 Pair 並不會影響帳戶擁有者的資產。
這邊是有實作 Social Recovery 的合約錢包(Contract Account)與 EOA 錢包一個很大的不同,Social Recovery 的合約錢包不會產出私鑰給用戶,因為他是一個「合約」。而控制這個合約(錢包)的方法就是用戶會使用一個公私鑰對去作為合約內部紀錄的擁有者(用一個變數儲存 Owner 地址)。
以下是兩種帳戶的比較圖表,Contract Account 我們假設有實作 Social Recovery 這個功能,同時 Guardians 能夠正常運作。
另外補充說明兩個點:
- EOA Wallet 的註記詞跟私鑰都遺失的話就:Goodbye My Love ~ 我的愛人,再見
- EOA 無法透過販賣私鑰轉移帳戶擁有權,除非你有消除記憶棒把對方腦袋裡的記憶刪除。
Overview the System Design
支援 Social Recovery 這個 Feature 的 Contract Account 有一些角色,一開始我會先介紹 Signing Key,依序介紹 Guardians 以及 Recover 等機制和特殊功能。同時我也會附上一些 pseudocode 幫助大家理解。
文章中的系統設計並不是一個標準,大家還是要思考自己的需求而定。
Signing Key
首先,整個 Contract Account 有一把唯一的 Signing Key,用於批准交易(鏈下簽章並以合約送出交易)。「正常情況下」持有這把 Signing Key 的人就視為是這個 Contract Account 的 Owner。
這裡要先講到區塊鏈世界與現實世界的區別,在銀行體系中,擁有一個帳戶的提款卡密碼或印鑑不代表能夠臨櫃領出那個帳戶所有的錢,因為銀行會針對你的身分進行辨認。
但在區塊鏈的世界中通常持有私鑰就等於持有那個帳戶的所有資產,沒有人有辦法辨認當前操作私鑰的人,到底是不是這個帳戶的擁有者,更無法阻止他進行任何交易。
因此,我們可以期盼在合約錢包中有一個狀態變數能夠紀錄 Signing Key 對應的公鑰(以地址方式記錄),此時我們就能夠利用這個 owner
變數來決定函式訪問權限(onlyOwner
)。
address public owner;function sendTransaction(
address _to,
uint _value,
bytes memory _data
) external onlyOwner returns (bytes memory) {
// 這裡還需要先確認並不處在 Recovering 中
(bool success, ) = _to.call{value: _value}(_data);
require(success, "external call reverted");
return result;
}
有了這個 owner
來代表合約的擁有者之後,我們就能藉由改變這個變數對應的地址,來改變合約的擁有權。
- 只有 Signing Key 有權力可以轉移 Signing Key(
owner
),以及增加、刪除或轉移 Guardians。 - 當 Signing Key 發生失竊、遺忘等情況時,就會需要 Guardians 的幫助,來把
owner
改成合約擁有者新的地址。這也是 Social Recovery 的核心概念。
這邊依然需要一個 EOA(代表 Signing Key 的公私鑰對)從外部作為 Contract Account 函式(交易)的發起者(文末討論處我會講手續費機制)。只不過當資產都是放在 Contract Account 上,且能夠正常 Recovery,EOA(當前的 Signing Key)就是可以說換就換的。
然而在以上的設計中,Signing Key 失竊依然會導致原始 EOA Private Key 失竊一樣的問題,於是乎我們可以在這邊加上一些設計讓交易「不要馬上執行」。
或者是 Vitalik 在文中提到的 Vault 概念,例如我們讓這個 Social Recovery Wallet 的錢送到一個我們部署的 Vault Contract 做保管,而在這個 Vault 裡面的資產必須經過一段時間才能被使用。同時 Signing Key 或 Guardian 可以去取消 Vault Contract 中正在 Pending(Delay)的交易。
Guardian
- 我們可以設定一定數量的 Guardians(最少三位,數量根據合約設計而定,但必須是奇數,才能確保表態時不會平手),在合約中會有一個資料結構儲存他們的公鑰地址。
- 當 Signing Key 失竊時會由 Guardians 來負責執行 Recover,運作流程下一個小節我會提到。
- Guardians 的挑選通常有幾種選擇,例如親友、Owner 的另外一個設備錢包、機構等。他們不一定要彼此認識,甚至他們不應該彼此認識(身處 Owner 不同的社交圈),這樣可以避免他們串通起來竊走 Owner 的資產(更改新的 Signing Key)。
- Guardians 還可以執行一些特殊功能,例如凍結及解凍這個合約帳戶,授權及取消交易等,一切都可以視合約設計而定。
- 當我們想要加入或刪除一個 Guardian 時,都是將這個 Operation 加入排程,而不是一呼叫函式就執行(加入排程且無法馬上執行,可以形成一種 Delay 的效果)。這個排程通常要經過 1 到 3 天,Operation 才會真的被執行(更改合約中的狀態)。
- 第五點提到的設計中,會需要數天的 Delay 是為了避免駭客直接把 Guardians 轉移了,可以試想兩種情況:
- Signing Key 失竊:Guardians 可以在這幾天集結在一起,並且更改 Signing Key。
- Guardian Key 失竊:Signing Key 可以做出反應,將 Guardian 權力從失竊的 Guardian 地址轉給新的地址。
合約中,我們可以期待有一個資料結構來儲存每一個 Guardians 的狀態,更細節一點的可以宣告一個 Guardian Struct,包含每個 Guardian 的 Status(removing
, kicked-out
, working 等)或更多細節:
/*
如果此 guardian 已經被刪除,那 value 則為 false
如果此 guardian 已經被加入,那 value 則為 true
*/
mapping(address => bool) isGuardian;
分別會有兩種 Operations 對應著對 isGuardian
這個資料結構的加入和刪除,其實也就是管理 Guardian 的名單。但這邊設計上由於 Guardians 數量必須是奇數,導致 add
實作起來可能會有點複雜(也許需要一次增加偶數個)。
因此更常見的設計不是無上限的增加 Guardians 數量,而是一開始(利用 Constructor)就決定 Guardians 只能有幾個,想要增加新的 Guardian 就必須「轉移舊 Guardian 的權力」給新地址。
mapping(address => uint256) public guardianRemovalPeriod;function removeGuardian(address removingGuardian) external onlyOwner {
// 將 Removal Period 設定為當前 timestamp + 3 Days
}function executeGuardianRemoval(
address removingGuardian,
address newGuardian
) external onlyOwner {
// 確認 oldGuardian 正在移除的排程中
// 確認 Removal Period 已經過了
// 重置 Removal Period Timestamp 為 0 (means not in Removal Period)
// 設定 Guardian Data Structures
}function cancelGuardianRemoval(
address removingGuardian
) onlyOwner external {
// 重置 Removal Period Timestamp 為 0 (means not in Removal Period)
}
function transferGuardian(
address newGuardian
) external onlyGuardian {
// 確認當前帳戶狀態並非正在 Recovery Period
// 確認 msg.sender(被轉移的 Guardian) 並非處在 Removal Period
// 設定 Guardian Data Structures
}
Guardians 的挑選對象是一門學問,前面有稍微提到可以使用 Owner 擁有的另一個冷錢包來作為其中一個 Guardian,或是找一些不同社交圈的親友,甚至也可以選擇機構來作為 Guardian。當我們能挑選三個以上的 Guardians 時就可以是由這些人構成的組合。
機構來作為 Guardian 是一個不錯的選擇,因為機構通常沒有理由也無法串通 Guardian 竊走你的錢,此外他們還能夠使用生物辨識、信箱認證、電話或視訊等方式(例如 Argent 的 two-factor authentication),判斷當前申請 Recovery 的人是不是真的 Contract Account 的擁有者。
但這也是一個 Trade-Off,面對舒適使用體驗的同時,中心化機構會需要對用戶進行 KYC,並且在政府介入的情況下可能會有監管問題。這個部份就要看用戶是否在意,能夠選擇符合自己需求的錢包商。
就跟忘記密碼或進行 OTP 驗證時,機構會寄簡訊驗證碼到用戶手機同理。
Recovery
當 Signing Key 被偷竊或者忘記時,Guardians 就可以操作 Recovery 函式,使得遺失的那把 Signing Key 無效化,並將 Owner 設定為新的 Owner 能掌握的公鑰地址。
關於 Recovery 的過程我們會需要一些資料結構:
- 首先是 Constructor 中要決定這個 Contract Account Recovery 需要的 Guardian 數量,定義在
threshold
中(e.g. 五個裡面要有三個表態同意)。 - 我們必須要有一個變數
isRecovering
確認當前的帳戶狀態,當現在帳戶正在 Recovering 時,就不可以進行一些動作(e.g.sendTransaction
,transferGuardian
)或才能進行一些動作(e.g.cancelRecovery
,executeRecovery
)。 - 另外我們需要
recoveryRound
與RecoverInfo
搭配,來記錄每一輪 Recovery 的資訊。recoveryRound
表示當前這輪 Recovery 的編號,RecoverInfo
會記錄每輪每位 Guardians 的意見。 - 最後每一個 Guardian 都會對每一輪 Recovery 表達他的意見,如果今天 Guardian 願意表態(呼叫
supportRecovery
)則視為同意,不同意就不動作。此後guardianToRecovery
這個 mapping 就會出現該同意 Recovery 的 Guardian 的 Key-Value Pair(address
->RecoverInfo
)。
uint public threshold;
bool public isRecovering;
uint public currRecoveryRound;struct RecoverInfo {
uint recoveryRound; // 當前是第幾輪
address newOwnerAddr; // 將要轉移給哪個新 Owner 的地址
bool used;
// 這位 Guardian 對這輪 Recover 之同意已經被使用過了,則設為 true
// 也就是當 executeRecovery 執行後,無論該輪 Recovery 成功與否都會設為 true
// 此次表態之後便不能再用了
}mapping(address => RecoverInfo) public guardianToRecovery;
在發起新一輪 Recovery(initiateRecovery
)或表態支持當前 Recovery(supportRecovery
)時,設定該 Guardian 對該輪 RecoverInfo 表態的程式碼如下:
guardianToRecovery[msg.sender] = Recovery(
recoveryRound=currRecoveryRound,
newOwnerAddr=_newOwnerAddr,
used=false
);
// 這邊的 msg.sender 就是願意表態支持 Recover 的 Guardians
接下來可以看每一個函式的執行內容:
// Guardian 發起一輪新的 Recovery
function initiateRecovery(
address _newOwnerAddr
) onlyGuardian external {
// 檢查當前並不在 Recovering(check isRecovering == false)
// currRecoveryRound += 1
// isRecovering = true;
// 設定 guardianToRecovery[msg.sender] 的資料內容
}
// Guardian 對當前的 Recovery 表態支持
function supportRecovery(
address _newOwnerAddr
) onlyGuardian onlyInRecovery external {
// 設定 guardianToRecovery[msg.sender] 的資料內容
}// Owner 擁有絕對的帳戶控制權,因此他可以隨時取消 Recovery 的行為
function cancelRecovery() onlyOwner onlyInRecovery external {
// isRecovering = false;
}// 執行 Recovery
function executeRecovery(
address newOwner,
address[] calldata guardianList
) onlyGuardian onlyInRecovery external {
// 確認 guardianList 長度大於 threshold
for (uint i = 0; i < guardianList.length; i++) {
// 確認每一個傳入的地址真的是 Guardian
// 確認該 Guardian 的當前表態尚未被使用過,檢查 used == false
// 確認該 Guardian 有對「這輪」進行表態,檢查 RecoveryRound == currRecoveryRound
// 確認該 Guardian 有對「這個新擁有者地址」進行表態,檢查 newOwnerAddr == newOwner // 設定該 Guardian 的當前表態為已經使用過,設 used = true
}
// 以上遍歷過程如果出現錯誤就會 Revert,如果能執行至此表示通過該輪 Recovery // inRecovery = false;
// owner = newOwner;
}
More Features
- 暫時凍結帳戶:可以利用
isFreezed
作為 modifier,在重要函式例如sendTransaction
加上,就可以暫停當前帳戶的運作。那當然就會需要有lock()
與unlock()
等 Guardians 才能使用的 Operations。 - 設置每天的轉賬限制:設定一個上限值,搭配 Timestamp 的運算決定期限內可以匯出的總量
- 可以設定這個帳戶不能向某些地址的轉賬:設定一個 Black List,轉帳前檢查
to
不在此 List 中。 - 可以設定這個帳戶只能向特定地址轉賬:設定一個 Trust Lists,轉帳前檢查
to
要在此 List 中。
Discussion
Wait a Minute, Who Pay the TX Fee Firstly?
大家看到這裡可能會有一個疑惑,那就是既然是個 Contract Account,那免不了要有一個 EOA 來發起交易並且支付手續費(在非 AA 的情況下)。那我想要用 Contract Account 送出交易的時候,不就代表我還要去充值 Signing Key 在以太坊上的對應公鑰地址這個 EOA 再去戳 Contract Account 嗎?
舉 Argent 為例,當我們在操作 Argent 錢包介面時,有一個 Relayer 會聆聽我們在錢包上簽核的訊息,當我們使用錢包簽核完交易要送出時,Relayer 會以 EOA 的身份作為交易發起者,把我們已簽的交易物件送到 Contract Account 觸發 sendTransaction
,這樣就能夠正常送出這筆交易了。Contract Account 會支付一定手續費(e.g. ERC-20)給 Relayer 作為給 Argent 的手續費(此手續費不是 Network Gas Fee)。
Argent 是自行運作一個 Relayer,他們自己建立了一個 queue system 以及 low gas transaction 的 pool,首先會模擬交易(失敗則 revert 給錢包用戶看錯誤訊息),確認成功後會在 gas 比較低時送出交易。用戶不能自定義 Gas Fee 來加速交易,因此也常常出現用戶送出交易後等了好幾天依然不見 Confirmed。
可想而知這是一個不是很政治正確(Where is Decentralization !!??)的做法哈哈哈
一是隱私問題(錢包商或 Relayer 會記錄我們所有的交易),二是監管問題(如果政府介入,那中心化設施可能無法正常運作),三是安全問題(如果 Relayer 作惡或者崩潰且錢包商無法解決,那有機率導致錢包毀損)。
但這是一個 Trade-Off,中心化的設施可能可以讓交易體驗大幅的優化,開發者必須斟酌他們想要吸引的客群,而使用者必須自己判斷怎麼樣的錢包適合自己。
We need guardians’ approve everytime?
當閱讀到 Vault 或者 Guardians Approve 才能使得一筆交易真正送出時,大家也不免有一個疑惑就是如果今天是要搶 NFT 或者操作 DeFi 項目,這種分秒必爭的行為怎麼可能乖乖的等大家同意?或甚至我今天要玩一個 GameFi 時,也不可能每一個動作都要等那麼久。這也是 Multi-Sig 的一個大弊病。
首先針對 Vault:Vault 的概念是能夠根據合約內容再擴充的,例如 Vitalik 也有提到 Vault Contract 可以設定一些 financial operations,讓像是在 Uniswap 換兌我們設定的 Whitelist Token 時不需要經過 Delay。
另外 Argent 的錢包設計中也有一個功能叫做 Trusted Session,能夠讓帳戶在一定時間內不需要 Guardians 同意就可以送出交易。
How About some Special Recovery?
在 Argent 中還提供了 iOS/Android 用戶使用他們的 iCloud / Google Drive 作為 Recovery 的新途徑,運作過程為 Argent 會產生一個隨機數稱作 key-encryption-key(KEK)並用其將你的私鑰加密,加密結果(encrypted private key)會存在雲端硬碟中,而 KEK 會送到 Argent 公司的資料庫保存。
- 有權限訪問雲端硬碟的人如果沒有 Argent 公司回傳的 KEK,就無法解密出你的私鑰。當任何人向 Argent Request KEK 時,會需要 two-factor authentication 的認證通過(簡訊或電子郵件認證)才會收到 KEK。
- 另外一個角度來看 Argent 公司(或駭入 Argent 公司資料庫的人)也無法獲得你的私鑰,因為他們沒有權限可以訪問你的雲端硬碟。
Closing
智能合約錢包的 Social Recovery 並不是去拆解私鑰,這也是為什麼我在一開始會說這邊的 Social Recovery 是 Recover Account 而不是 Recover (Private) Key。
Special thanks to Vitalik Buterin and NIC Lin for reviewing this post.