
EXEC_TX:為帳戶功能設計的可擴展執行交易
EXEC_TX 引入了一種基於鉤子(hook)模型的新型交易類型,將身份驗證和託管等邏輯委託給智能合約,從而減少對協議層頻繁變動的需求。它具備多階段執行流程與二維 Nonce 設計,以實現並行交易處理並為帳戶功能提供更大的靈活性。
EXEC_TX 引入了一種基於 Hook 執行(hook-based execution) 的新型交易類型。我們不再為帳戶抽象、隱私、代贊助以及明年的新功能添加新的 EIP,而是提供一個協議層的封裝(envelope),由 Hook 來實現各項功能的邏輯。協議負責處理枯燥的部分(Nonce、Gas、多階段路由);Hook 則負責處理有趣的部分(驗證、託管、證明、策略)。
問題所在
目前,每一項新的帳戶功能都需要:
-
新的交易類型或獨立的基礎設施(EIP-4337 捆綁器 bundlers、中繼器 relayers)
-
新的錢包簽名邏輯
-
新的客戶端驗證規則
-
新的內存池(mempool)邏輯
-
新的 dApp 集成
這造成了摩擦、碎片化,並減緩了功能開發的速度。
解決方案:一種可擴展的交易類型,其功能邏輯存在於合約中,而非共識層。
Hook 模型:核心設計
什麼是 Hook?
Hook 是一個智能合約,協議會在 EXEC_TX 執行流程中的特定點調用它。協議不再由自身代碼處理驗證、託管、證明,而是將其委託給 Hook。
Hook 接口(概念性):
interface IExecHook {
// 在 PRE_VALIDATION(驗證模式)期間調用
// 必須是確定性的(無狀態寫入,無依賴區塊的讀取)
// 返回值:0 = 失敗,1 = 成功
function preValidation(bytes calldata txData, bytes calldata hookData)
external view returns (uint256);
// 在 PRE_EXECUTION 期間調用
// 可以自由讀寫狀態
// 返回值:0 = 失敗,1 = 成功
function preExecution(bytes calldata txData, bytes calldata hookData)
external returns (uint256);
// 在 POST_EXECUTION 期間調用(僅在核心調用成功時)
// 可以自由讀寫狀態
// 返回值:0 = 失敗,1 = 成功
function postExecution(bytes calldata txData, bytes calldata hookData)
external returns (uint256);
}
Hook 生命周期
EXEC_TX 執行流程:
-
基礎驗證
├─ 解碼 EXEC_TX
├─ 驗證簽名 (EOA) 或調用 isAuthorizedExecHook (合約帳戶)
└─ 加載 Hook 合約 -
PRE_VALIDATION(強制執行驗證模式)
├─ 協議調用 hook.preValidation(txData, hookData)
├─ Hook 邏輯:驗證簽名、檢查證明、驗證策略
├─ 限制:無狀態寫入,無依賴區塊的讀取
└─ Hook 返回 0(失敗,交易被拒絕)或 1(成功,繼續) -
PRE_EXECUTION(若 phaseMask 第 1 位已設置)
├─ 協議調用 hook.preExecution(txData, hookData)
├─ Hook 邏輯:鎖定資金、預留容量、發出中間狀態
├─ 限制:無(完整的 EVM 訪問權限)
└─ Hook 返回 0(失敗,交易回滾)或 1(成功,繼續) -
核心調用 (Core CALL)
├─ 協議執行 CALL(to, data, value)
├─ 無 Hook 參與
└─ 若回滾,則整筆交易回滾(跳過 POST_EXECUTION) -
POST_EXECUTION(若 phaseMask 第 2 位已設置且核心調用成功)
├─ 協議調用 hook.postExecution(txData, hookData)
├─ Hook 邏輯:釋放託管資金、發出作廢標誌(nullifiers)、結算帳戶
├─ 限制:無(完整的 EVM 訪問權限)
└─ 若 Hook 回滾,則整筆交易回滾(核心調用的結果也會撤銷) -
結算
└─ 向支付者收取實際消耗的 Gas(或回退至 from 地址)
為什麼只用單個 Hook?
每個 EXEC_TX 僅限一個 Hook,而非多個。原因如下:
-
極簡共識規則:協議不定義 Hook 的順序、依賴關係或衝突解決。由應用程序決定。
-
成熟模式:EIP-4337 的捆綁器邏輯證明了調度器(dispatcher)模式是成熟且經過測試的。
-
不限制組合性:單個 Hook 可以調度內部模塊。下方的示例展示了如何通過一個 Hook 實現「3 分之 2 驗證 + 額度限制策略」。
Hook 授權
對於合約帳戶:協議在 PRE_VALIDATION 之前調用 isAuthorizedExecHook(hook.target)。
// 多簽合約必須實現此接口
contract MultisigAccount {
mapping(address => bool) authorizedHooks; // 允許的 Hook 白名單
function isAuthorizedExecHook(address hookTarget)
external view returns (bool) {
return authorizedHooks[hookTarget];
}
}
對於 EOA:簽名本身即為授權。不需要調用 isAuthorizedExecHook。
Hook 數據編碼
Hook 邏輯傳遞於 hook.data(任意字節)。編碼方式由應用程序特定,而非協議強制規定。常見慣例:
// 簡單:單一用途 Hook
hook.data = abi.encode(signature1, signature2) // 例如:3 分之 2 簽名
// 複雜:調度器路由
hook.data = abi.encode(
phase0Data, // 用於 PRE_VALIDATION
phase1Data, // 用於 PRE_EXECUTION
phase2Data // 用於 POST_EXECUTION
)
Hook 負責解碼 hook.data 並理解其當前運行的階段(通過 phaseMask)。
執行模型:多階段設計
什麼是 phaseMask?
一個控制哪些階段運行的位掩碼:
- phaseMask = 1 (二進制 001): 僅 PRE_VALIDATION
- phaseMask = 3 (二進制 011): PRE_VALIDATION + PRE_EXECUTION
- phaseMask = 5 (二進制 101): PRE_VALIDATION + POST_EXECUTION
- phaseMask = 7 (二進制 111): 所有三個階段
- phaseMask = 0: 無 Hook,僅核心調用(類似傳統交易)
為什麼分為四個階段?
| 階段 | 執行時機 | Hook 可執行的操作 | 協議保證 |
|---|---|---|---|
| PRE_VALIDATION | 核心調用前 | 驗證簽名、檢查證明、驗證策略 | 無狀態寫入;確定性;內存池安全 |
| PRE_EXECUTION | 核心調用前 | 鎖定託管、預留容量 | 完整 EVM 訪問;與核心調用原子化 |
| Core CALL | 用戶意圖 | (無 Hook) | 與傳統交易一致;可執行任何操作 |
| POST_EXECUTION | 核心調用後(若成功) | 釋放託管、發出作廢標誌 | 完整 EVM 訪問;與核心調用原子化;若回滾則全部回滾 |
為什麼採用這種結構?
-
PRE_VALIDATION:在觸碰狀態前進行確定性檢查。內存池節點可以在本地驗證而不會產生分歧。
-
PRE_EXECUTION:核心調用前的準備(鎖定資金、預留)。核心調用可以假設資源已就緒。
-
Core CALL:與傳統交易相同。用戶實際的交易內容。
-
POST_EXECUTION:核心調用成功後的清理。託管會計是原子化的:要麼核心調用成功且資金釋放,要麼兩者都不發生。
驗證模式:為什麼限制 PRE_VALIDATION?
內存池節點在將交易納入區塊前會在本地進行驗證。如果 PRE_VALIDATION 代碼讀取區塊狀態:
- 節點 A 在區塊 1000:BALANCE 檢查通過(支付者有 1 ETH)
- 節點 B 在區塊 1001:支付者資金被抽乾,BALANCE 檢查失敗
- 結果:同一筆交易在 A 有效,在 B 無效 → 導致內存池分區。
解決方案:驗證模式禁止:
-
SLOAD, SSTORE(狀態讀/寫)
-
TIMESTAMP, BLOCKHASH, BASEFEE, BLOBBASEFEE, PREVRANDAO, COINBASE, GASLIMIT(依賴區塊的指令)
-
例外:BALANCE, SELFBALANCE(支付者流動性檢查至關重要;TOCTOU 風險通過 PRE_EXECUTION 資金鎖定來緩解)
這確保了 PRE_VALIDATION 是確定性的:無論區塊高度如何,同一筆交易在所有節點上的結果始終一致。
二維 Nonce:並行化設計
線性 Nonce 的問題
傳統 Nonce 強制執行線性順序:nonce=100, 101, 102, ...。一次失敗會阻塞所有後續交易。
- 發送 nonce=100 的交易:待處理
- 發送 nonce=101 的交易:待處理(等待 100)
- 發送 nonce=102 的交易:待處理(等待 100 和 101)
- nonce=100 的交易失敗
- 結果:101 和 102 被永久阻塞
解決方案:帶有獨立通道的二維 Nonce
struct Nonce {
uint64 nonceKey; // 通道標識符
uint64 nonceSeq; // 通道內的序列號
}
通道是相互獨立的:
- 通道 0: nonce_seq=100, 101, 102, ... (支付交易)
- 通道 1: nonce_seq=100, 101, 102, ... (治理投票)
- 通道 2: nonce_seq=100, 101, 102, ... (意圖響應)
所有三個通道並行執行。通道 0 的失敗不會阻塞通道 1 或 2。
為什麼在協議層實現?
Hook 能否自行管理 Nonce 序列?不能,因為:
-
共識分歧:兩個擁有相同 Hook 代碼但狀態不同的節點可能會對衝突交易產生不同的接受/拒絕結果。
-
內存池攻擊:攻擊者利用 Hook 狀態差異進行雙重支出或死鎖。
-
協議不變量:重放保護必須是共識保證,而非應用邏輯。
協議強制執行:「在每個通道內,nonceSeq 必須等於下一個預期序列。」應用程序可以在此之上疊加策略(例如,「通道 0 僅用於支付」)。
EOA 與合約帳戶的 Nonce 處理
| 帳戶類型 | 協議強制執行 | 應用程序責任 |
|---|---|---|
| EOA | 嚴格:所有通道 nonceSeq = expected[lane] | 無;協議保證 |
| 合約帳戶 | 僅限通道內的順序 | Hook 必須防止重複 Nonce 以避免重放攻擊 |
風險:如果合約帳戶 Hook 不檢查 Nonce 的唯一性,帳戶將面臨重放風險。同一筆已簽名的 EXEC_TX(nonceSeq=100)可能會執行兩次。
實例演練:Hook 運作中
示例 1:簡單授權(僅 PRE_VALIDATION)
// 3 分之 2 多簽合約帳戶發送代幣
EXEC_TX {
from: safe_multisig_account,
to: token_contract,
data: transfer(recipient, amount),
hook: {
target: multisig_validator, // 部署一次,由所有 3 分之 2 帳戶複用
phaseMask: 1, // 僅 PRE_VALIDATION
gasLimit: 100_000,
data: abi.encode([sig1, sig2]) // 3 分之 2 簽名
}
}
Hook 執行:
contract MultisigValidator {
function preValidation(bytes calldata txData, bytes calldata hookData)
external view returns (uint256) {
// 解碼簽名
(bytes memory sig1, bytes memory sig2) = abi.decode(hookData, (bytes, bytes));
// 從 EXEC_TX 簽名哈希中恢復簽名者
address signer1 = recoverFromSignature(txData, sig1);
address signer2 = recoverFromSignature(txData, sig2);
// 驗證兩位簽名者是否都在此帳戶的 3 分之 2 集合中
Safe safe = Safe(msg.sender); // msg.sender 是 EXEC_TX 的 from 地址
require(safe.isOwner(signer1) && safe.isOwner(signer2), "Invalid signers");
return 1; // 成功
}
}
為什麼這很重要:這是合約帳戶首次擁有原生的基礎層簽名驗證。不需要捆綁器。同一個驗證器合約可供 100,000 個 Safe 錢包複用。
示例 2:帶有 ZK 的隱私(僅 PRE_VALIDATION)
// 帶有 ZK 證明的隱私代幣轉帳
EXEC_TX {
from: user,
to: privacy_token_contract,
data: [commitment_hash, encrypted_transfer_params],
hook: {
target: zk_verifier,
phaseMask: 1, // 僅 PRE_VALIDATION
gasLimit: 200_000,
data: zk_proof // 公開證明
}
}
Hook 執行:
contract ZKVerifier {
function preValidation(bytes calldata txData, bytes calldata hookData)
external view returns (uint256) {
// 解碼 calldata 以提取 commitment_hash
(bytes32 commitment, ) = abi.decode(txData, (bytes32, bytes));
// 從 hook.data 解碼證明
bytes memory proof = hookData;
// 驗證證明(確定性,無狀態讀取)
require(verifyProof(proof, commitment), "Invalid proof");
return 1; // 成功
}
}
執行後續:
-
核心:隱私代幣合約接收加密參數,更新屏蔽池狀態。
-
用戶在鏈下保留私鑰;未來交易提款時可進行解密。
為什麼這有效:證明驗證(公開、確定性)與狀態變更(加密)解耦。隱私邏輯可以在合約中演進,無需更改共識。
示例 3:帶有調度器的多模塊組合(所有階段)
// 3 分之 2 多簽 + 額度限制 + 贊助 Gas
EXEC_TX {
from: safe_multisig_account,
to: uniswap,
data: swap_calldata,
payer: relayer_sponsor,
hook: {
target: dispatcher, // 路由至 AuthModule, PolicyModule, EscrowModule
phaseMask: 7, // 所有三個階段
gasLimit: 500_000,
data: abi.encode(
[sig1, sig2], // 用於 AuthModule (階段 0)
{amount: 100e18, recipient: "0x..."}, // 用於 PolicyModule (階段 1)
relayer_address // 用於 EscrowModule (階段 2)
)
}
}
Hook 執行:
contract Dispatcher {
AuthModule authModule;
PolicyModule policyModule;
EscrowModule escrowModule;
function preValidation(bytes calldata txData, bytes calldata hookData)
external view returns (uint256) {
(bytes[] memory sigs, , ) = abi.decode(hookData, (bytes[], bytes32, address));
return authModule.verify(txData, sigs) ? 1 : 0;
}
function preExecution(bytes calldata txData, bytes calldata hookData)
external returns (uint256) {
(, bytes32 constraints, address relayer) = abi.decode(hookData, (bytes[], bytes32, address));
// 驗證策略
require(policyModule.validateConstraints(constraints, txData), "Policy violation");
// 為 Gas 贊助鎖定託管
require(escrowModule.lockEscrow(relayer, tx.gasprice * 500_000), "Escrow lock failed");
return 1;
}
function postExecution(bytes calldata txData, bytes calldata hookData)
external returns (uint256) {
(, , address relayer) = abi.decode(hookData, (bytes[], bytes32, address));
// 釋放託管資金(計算實際消耗的 Gas,退回差額)
uint256 actualGasCost = (initialGas - gasleft()) * tx.gasprice;
escrowModule.releaseEscrow(relayer, actualGasCost);
return 1;
}
}
執行時間線:
-
PRE_VALIDATION:Dispatcher.preValidation → AuthModule.verify → 3 分之 2 簽名檢查(約 8K Gas)
-
PRE_EXECUTION:Dispatcher.preExecution → PolicyModule.validate (額度限制) → EscrowModule.lockEscrow(約 10K Gas)
-
核心:Uniswap 兌換執行(約 100K Gas,取決於市場)
-
POST_EXECUTION(僅因核心成功):釋放託管,退款給中繼器(約 5K Gas)
-
結算:向中繼器收取實際 Gas;若中繼器資金耗盡則回退至 Safe 帳戶。
為什麼採用此架構:單個 Hook(調度器)路由至 3 個內部模塊。協議不在乎存在多少模塊;調度器負責處理組合。路由開銷約為 1K Gas(可忽略不計)。
Hook 設計如何解決問題
從「為每個功能升級」到「部署一個 Hook」
舊模式(EIP-4337,捆綁中繼器):
-
需要新功能 → 新 EIP 提案 → 客戶端共識 → 主網升級 → 錢包集成 → Dapp 集成
-
每個功能耗時數月至數年
-
每個功能都會使基礎設施碎片化
新模式(EXEC_TX + Hook):
-
需要新功能 → 編寫 Hook 合約 → 部署 → Dapp 集成
-
每個功能耗時數週
-
單一統一的基礎設施 (EXEC_TX)
未來 Hook 示例(無需更改協議)
-
風險管理:風險 Hook 根據用戶的風險概況驗證交易。
-
意圖求解:意圖 Hook 驗證鏈上狀態是否符合用戶意圖。
-
隱私:隱私 Hook 根據承諾驗證證明。
-
抗量子驗證:Hook 實現後量子簽名驗證。
-
社交恢復:Hook 實現社交恢復機制。
-
授權委託:Hook 驗證授權委託證明。
-
批量操作:Hook 驗證批量結構和依賴關係。
-
抗 MEV:Hook 實現加密 calldata + 揭示機制。
由於 Hook 接口是通用的,所有這些都可以在不對協議進行任何更改的情況下實現。
Hook 模型的關鍵屬性
使其運作的屬性
-
確定性 PRE_VALIDATION:不同節點對有效性始終達成一致。內存池不會產生分區。
-
原子化階段:PRE_EXECUTION 和 POST_EXECUTION 與核心調用原子化。託管不變量得以維持。
-
授權被委託:協議不定義何為「有效」。由合約定義 (isAuthorizedExecHook)。
-
應用層可擴展:新的 Hook 不需要共識。可以快速部署和迭代。
-
通過調度器組合:單個 Hook 邊界不防礙多功能交易。調度器模式已獲證實。
權衡
| 權衡點 | 我們的選擇 | 原因 |
|---|---|---|
| 協議 vs 應用 | 協議:Nonce, 階段, Gas | 協議強制執行不變量(重放保護、原子性、確定性)。應用程序執行策略。 |
| 單個 vs 多個 Hook | 單個 Hook | 極簡共識規則;調度器模式已獲證實。未來可擴展。 |
| Nonce 唯一性 | EOA:強制,合約:委託 | EOA 默認安全。合約帳戶獲得靈活性;需對自身安全負責。 |
| 驗證模式嚴格性 | 嚴格(禁止 9 個指令) | 內存池一致性。為支付者檢查保留 BALANCE 例外。 |
| 支付者回退 | 耗盡時回退 | 核心原子性。風險:靜默回退。緩解措施:錢包 UI。 |
與替代方案的比較
我們並非試圖取代 EIP-4337、EIP-8130 或 EIP-7702。不同的工具用於不同的目的。
-
EIP-4337(捆綁器,UserOps):適用於鏈下基礎設施。將繼續存在。EXEC_TX 是基礎層原生的替代方案。
-
EIP-8130(帳戶配置):規範性的帳戶管理(多所有者、權限範圍)。EXEC_TX 是一個極簡封裝,由合約實現策略。
-
EIP-7702(EOA 委託):讓 EOA 為每筆交易附加代碼。EXEC_TX 適用於希望獲得原生支持的合約帳戶。
所有方案可以共存。不同的用戶會選擇不同的工具。
社區開放問題
-
Hook 接口 ABI:協議是否應該定義標準的 Hook 函數簽名,還是應用程序特定的編碼即可接受?
-
驗證模式一致性:我們如何確保 geth、erigon、reth 都以完全相同的方式執行驗證模式?存在客戶端分歧的風險。
-
階段上下文傳遞:Hook 應該如何知道當前正在執行哪個階段?通過 phaseMask 參數?還是從上下文中隱式獲取?
-
支付者回退 UX:帶有錢包警告的靜默回退是否可以接受,還是我們應該嚴格執行失敗?
-
合約帳戶 Nonce 安全:委託給 Hook 是否足夠,還是我們應該添加協議層的合理性檢查?
-
Hook 回滾數據:協議是否應該捕獲回滾原因以用於錢包錯誤消息?
-
Gas 計量:當階段是可選時,
MAX_HOOK_GAS = gasLimit - MIN_CORE_GAS應該如何運作?MIN_CORE_GAS是否應隨階段而變化? -
二維 Nonce 錢包 UX:如何向非技術用戶展示「通道」概念而不讓他們感到困惑?
更新日誌
- 2026-04-08: 初始 RFC 草案,PR 待處理
外部審查
截至 2026-04-08 尚無。
待解決問題
- Hook 合約 ABI 與接口標準化
- 各 EVM 客戶端間驗證模式實現的一致性
- Hook 的階段上下文傳遞機制
- 支付者回退語義及 UX 緩解
- 合約帳戶 Nonce 安全保證
- Hook 回滾數據捕獲與錯誤消息傳遞
- 可選階段的 Gas 計量
- 二維 Nonce 錢包 UX 設計
- 調度器模式數據編碼標準化
作者:Louis Liu (@louisliu2048) 及貢獻者
相關項目:EIP-155, EIP-1559, EIP-1153, EIP-2718, EIP-2929, EIP-4844, EIP-6780
1 則貼文 - 1 位參與者
[閱讀完整主題](https://ethereum-magicians.org/t/exec-tx-extensible-execution-transaction-for-account-capabilities/28168)