Alert Source Discuss
🚧 Stagnant Standards Track: Core

EIP-2938: 账户抽象

Authors Vitalik Buterin (@vbuterin), Ansgar Dietrichs (@adietrichs), Matt Garnett (@lightclient), Will Villanueva (@villanuevawill), Sam Wilson (@SamWilsn)
Created 2020-09-04
Discussion Link https://ethereum-magicians.org/t/eip-2938-account-abstraction/4630
Requires EIP-2718

简述

账户抽象 (AA) 允许合约成为支付费用并启动交易执行的顶级账户。

摘要

另请参阅:https://ethereum-magicians.org/t/implementing-account-abstraction-as-part-of-eth1-x/4020 及其中链接,了解历史工作和动机。

截至穆尔冰川升级,交易有效性由协议严格定义:ECDSA 签名、一个简单的 nonce 和账户余额。账户抽象通过执行任意 EVM 字节码(对可以访问的状态有一些限制)来扩展交易的有效性条件。为了表示有效性,我们提出了一个新的 EVM 操作码 PAYGAS,它也设置了合约愿意支付的 gas 价格和 gas 限制。

我们将账户抽象分为两个层级:单租户 AA,旨在支持参与者较少的钱包或其他用例,以及多租户 AA,旨在支持有许多参与者的应用程序(例如 tornado.cash、Uniswap)。

动机

现有的限制阻碍了许多重要领域的创新,特别是:

  1. 使用 ECDSA 以外的签名验证的智能合约钱包(例如 Schnorr、BLS、后量子…)
  2. 包含多重签名验证或社交恢复等功能的智能合约钱包,从而降低了资金丢失或被盗的高度普遍风险
  3. 隐私保护系统,如 tornado.cash
  4. 尝试通过防止不满足高级条件(例如,存在匹配的订单)的交易被包含在链上来提高 DeFi 协议的 gas 效率
  5. 用户能够用 ETH 以外的代币支付交易手续费(例如,通过实时地将该代币转换成交易内部所需的 ETH 手续费)

以上大多数用例目前都可以使用中介来实现,最值得注意的是 Gas Station Network 和特定于应用程序的替代方案。这些实现的(i)技术效率低下,因为需要额外 21000 gas 来支付给中继者,(ii)经济效率低下,因为中继者需要在他们支付的 gas 费用之上获利。此外,使用中介协议意味着这些应用程序不能简单地依赖于基础以太坊基础设施,而需要依赖于用户群较小且未来不再可用的风险较高的额外协议。

在上述五个用例中,单租户 AA 大致支持 (1) 和 (2),多租户 AA 大致支持 (3) 和 (4)。我们将在下面的规范和原理部分讨论这两个层级之间的差异。

规范

单租户

FORK_BLOCK 之后,协议将识别以下更改。

常量

常量 数值
AA_ENTRY_POINT 0xffffffffffffffffffffffffffffffffffffffff
AA_TX_TYPE 2
FORK_BLOCK 待定
AA_BASE_GAS_COST 15000

新的交易类型

引入了一种新的 EIP-2718 类型的交易,类型为 AA_TX_TYPE。这种类型的交易被称为“AA 交易”。它们的 payload 应该被解释为 rlp([nonce, target, data])

此交易的基本 gas 成本设置为 AA_BASE_GAS_COST 而不是 21000,以反映缺少“内在的” ECDSA 和签名。

Nonce 的处理方式与现有交易类似(检查 tx.nonce == tx.target.nonce,如果失败则交易无效,否则继续并立即设置 tx.nonce += 1)。

请注意,此交易类型没有内在的 gas 限制;在开始执行时,gas 限制仅设置为区块中剩余的 gas(即 block.gas_limit 减去之前交易花费的 gas),并且 PAYGAS 操作码(见下文)可以向下调整 gas 限制。

交易范围内的全局变量

引入一些新的交易范围内的全局变量。这些变量的工作方式与 SSTORE 退款计数器类似(特别地,具有类似的还原逻辑)。

变量 类型 初始值
globals.transaction_fee_paid bool False if type(tx) == AA_TX_TYPE else True
globals.gas_price int 0 if type(tx) == AA_TX_TYPE else tx.gas_price
globals.gas_limit int 0 if type(tx) == AA_TX_TYPE else tx.gas_limit

NONCE (0x48) 操作码

引入了一个新的操作码 NONCE (0x48),gas 成本为 G_base,它将调用的 nonce 推送到堆栈上。

PAYGAS (0x49) 操作码

引入了一个新的操作码 PAYGAS (0x49),gas 成本为 G_base。它从堆栈中取出两个参数:(顶部)version_number,(顶部第二个)memory_start。在初始实现中,它将 assert version_number == 0 并读取:

  • gas_price = bytes_to_int(vm.memory[memory_start: memory_start + 32])
  • gas_limit = bytes_to_int(vm.memory[memory_start + 32: memory_start + 64])

两次读取都使用类似于 MLOAD 和 CALL 的机制,因此如果需要,内存会扩展。

未来的硬分叉可能会添加对不同版本号的支持,在这种情况下,操作码可能会采用不同大小的内存片段并以不同的方式解释它们。两个特别潜在的用例是 EIP 1559电梯机制

操作码的工作方式如下。如果以下三个条件(除了上面的版本号检查)都满足:

  1. 账户余额 >= gas_price * gas_limit
  2. globals.transaction_fee_paid == False
  3. 我们处于顶层 AA 执行帧中(即,如果当前运行的 EVM 执行退出或还原,则整个交易的 EVM 执行完成)

然后执行以下操作:

  • 从合约的余额中减去 gas_price * gas_limit
  • globals.transaction_fee_paid 设置为 True
  • globals.gas_price 设置为 gas_price,并将 globals.gas_limit 设置为 gas_limit
  • 将当前执行上下文中剩余的 gas 设置为等于 gas_limit 减去已消耗的 gas

如果上述三个条件中的任何一个不满足,则抛出异常。

在 AA 交易执行结束时,必须 globals.transaction_fee_paid == True;如果不是,则交易无效。执行结束时,合约将获得剩余 gas 的 globals.gas_price * remaining_gas 的退款,并且 (globals.gas_limit - remaining_gas) * globals.gas_price 将转移给矿工。

PAYGAS 也可用作 EVM 执行_检查点_:如果在调用 PAYGAS 后顶层执行帧还原,则执行仅还原到调用 PAYGAS 之后的位置,并在那里退出。在这种情况下,合约不收到退款,并且 globals.gas_limit * globals.gas_price 将转移给矿工。

重放保护

必须实施以下两种方法之一,以防止重放攻击。

需要 SET_INDESTRUCTIBLE

要求 AA 交易针对的合约以 EIP-2937’s SET_INDESTRUCTIBLE 操作码开头。以不以 SET_INDESTRUCTIBLE 开头的合约为目标的 AA 交易无效,并且不能包含在区块中。

需要修改 AA_PREFIX 以包含此操作码。

SELFDESTRUCT 上保留 Nonce

另一种选择是在 SELFDESTRUCT 调用中保留合约 nonce,而不是将 nonce 设置为零。

其他

  • 如果在由 AA 交易启动的调用的第一个执行帧中调用 CALLER (0x33),则它必须返回 AA_ENTRY_POINT
  • 如果在 AA 交易的任何执行帧中调用 ORIGIN (0x32),它必须返回 AA_ENTRY_POINT
  • GASPRICE (0x3A) 操作码现在推送值 globals.gas_price

请注意,GASPRICE 的新定义不会导致非 AA 交易的行为发生任何变化,因为 globals.gas_price 初始化为 tx.gas_price,并且由于无法调用 PAYGAS,因此无法更改。

挖矿和广播策略

账户抽象中的大部分复杂性源于矿工和验证节点用来确定是否接受和重新广播交易的策略。矿工需要确定,如果他们只进行少量处理后包含交易,该交易是否真的会支付费用,以避免 DoS 攻击。验证节点需要执行基本相同的验证,以确定是否重新广播交易。

通过保持共识更改的最小化,此 EIP 允许矿工和验证节点逐步引入 AA mempool 支持。初始支持将侧重于启用简单的、单租户用例,而后续步骤还将支持更复杂的多租户用例。早期阶段比后期阶段的描述更为详细,因为后期阶段的实施还需要更多时间。

具有固定 Nonce 的交易
常量 数值
VERIFICATION_GAS_MULTIPLIER 6
VERIFICATION_GAS_CAP = VERIFICATION_GAS_MULTIPLIER * AA_BASE_GAS_COST = 90000
AA_PREFIX if(msg.sender != shr(-1, 12)) { LOG1(msg.sender, msg.value); return }; 编译为 EVM 待定

当节点收到 AA 交易时,它们会对其进行处理(即尝试针对当前链头的后状态执行它)以确定其有效性,并继续执行直到发生以下事件之一:

  • 如果 target 的代码没有以 AA_PREFIX 作为前缀,则以失败退出
  • 如果执行遇到以下任何一项,则以失败退出:
    • 环境操作码 (BLOCKHASHCOINBASETIMESTAMPNUMBERDIFFICULTYGASLIMIT)
    • BALANCE(任何账户,包括 target 本身)
    • callee 更改为除 target 或预编译合约之外的任何内容的外部调用/创建 (CALLCALLCODESTATICCALLCREATECREATE2)。
    • 读取代码的外部状态访问 (EXTCODESIZEEXTCODEHASHEXTCODECOPY,但也包括 CALLCODEDELEGATECALL),除非读取的代码的地址是 target
  • 如果执行消耗的 gas 超过 VERIFICATION_GAS_CAP(如上指定),或超过区块中可用的 gas,则以失败退出
  • 如果执行到达 PAYGAS,则根据余额是否充足(例如 balance >= gas_price * gas_limit)以成功或失败退出。

节点不会在 mempool 中保留 nonce 高于当前有效 nonce 的交易。如果 mempool 已经包含具有当前有效 nonce 的交易,则另一个发送到同一合约且具有相同 nonce 的传入交易要么替换现有交易(如果其 gas 价格足够高),要么被丢弃。因此,mempool 每个账户最多保留一个待处理交易。

在处理新区块时,请注意哪些账户是 AA 交易的 target(每个区块目前有 12500000 gas,并且 AA 交易的成本 >= 15000,因此最多有 12500000 // 15000 = 833 个被针对的账户)。删除所有以这些账户为目标的待处理交易。所有其他交易都保留在 mempool 中。

单租户+

如果添加了 不可破坏合约 EIP,则可以调整单租户 AA 以允许在交易验证期间进行 DELEGATECALL:在执行新的 AA 交易期间,读取任何合约的代码的外部状态访问 (EXTCODESIZEEXTCODEHASHEXTCODECOPYCALLCODEDELEGATECALL),并且该合约的第一个字节是 SET_INDESTRUCTIBLE 操作码不再被禁止。但是,仍然不允许调用除 target 或预编译合约之外的任何内容,并且该调用会更改 callee (即,CALLCODEDELEGATECALL 以外的调用)。

如果添加了 IS_STATIC EIP,则可以扩展允许的前缀列表,以允许启用传入的静态调用但不启用更改状态的调用的前缀。

还可以扩展允许的前缀列表以启用其他良性用例(例如,记录传入的付款)。

允许_进入_ AA 账户的外部调用,方式如下。我们可以添加一个操作码 RESERVE_GAS,该操作码以值 N 作为参数,并且具有简单的行为:立即燃烧 N gas 并将 N gas 添加到退款中。然后,我们添加一个允许的 AA_PREFIX,它保留 >= AA_BASE_GAS_COST * 2 gas。这确保至少必须花费 AA_BASE_GAS_COST gas(因为退款最多可以退还总消耗量的 50%),才能调用到账户中并使 mempool 中以该账户为目标的交易无效,从而保持了该不变性。

请注意,账户也可以选择设置更高的 RESERVE_GAS 值,以便安全地拥有更高的 VERIFICATION_GAS_CAP;目标是在编辑账户的最低 gas 成本(即其 RESERVE_GAS 的一半)与允许该账户使用的 VERIFICATION_GAS_CAP 之间保持 VERIFICATION_GAS_MULTIPLIER 与 1 的比率。这也将保持之前部分暗示的关于最大重新验证 gas 消耗的不变性。

多租户及更高版本

在后续阶段,我们可以在 mempool 中添加对每个账户多个待处理交易的支持。这里的主要挑战是,单个交易可能会导致状态更改,从而使发送到同一账户的所有其他交易无效。此外,如果我们简单地按 gas 价格对交易进行优先级排序,则存在一种攻击媒介,即愿意支付最高 gas 价格的用户会发布许多(互斥的)略有改动的交易版本,从而将其他所有人的交易推出 mempool。

以下是缓解此问题的一种策略的草图。我们将要求传入的交易包含一个 EIP-2930 样式的访问列表,详细说明交易读取或修改的存储槽,并使其具有约束力;也就是说,访问访问列表之外的内容将无效。只有当交易的访问列表与 mempool 中其他交易的访问列表不相交(或者其 gas 价格更高)时,该交易才会被包含在 mempool 中。考虑此问题的另一种方法是拥有每个存储槽的 mempool,而不是仅仅拥有每个账户的 mempool,除非交易可以是多个每个存储槽的 mempool 的一部分(如果需要,可以将其上限设置为例如 5 个存储槽)。

另请注意,多租户 AA 几乎肯定需要允许矿工编辑传入交易的 nonce 以按顺序排列它们,从而导致交易的最终哈希在发布时是不可预测的。客户端需要明确地解决这个问题。

需要更多研究来完善这些想法,这留待以后的工作。

原理

账户抽象设置中的核心问题始终是,矿工和网络节点需要能够验证他们尝试包含或重新广播的交易是否真的会支付费用。目前,这非常简单,因为只要签名和 nonce 有效,并且余额和 gas 价格充足,就可以保证交易可以包含并支付费用。这些检查可以快速完成。

在账户抽象设置中,目标是允许账户指定 EVM 代码,该代码可以为交易的有效性建立更灵活的条件,但要求是可以快速验证此 EVM 代码,并具有与现有设置相同的安全属性。

在普通交易中,顶层调用从 tx.sendertx.to,并携带 tx.value。在 AA 交易中,顶层调用从_入口点地址_ (0xFFFF...FF) 到 tx.target

顶层代码执行预计分为两个阶段:较短的验证阶段PAYGAS 之前)和较长的执行阶段PAYGAS 之后)。如果执行在验证阶段抛出异常,则该交易无效,就像当前系统中具有无效签名的交易一样。如果执行在验证阶段之后抛出异常,则该交易会支付费用,因此矿工仍然可以包含它。

AA 不同阶段之间的转换完全通过矿工策略的更改来完成。第一阶段支持单租户 AA,在这种情况下,可以轻松实现的用例仅限于 tx.target 是代表用户账户的合约(即智能合约钱包,例如多重签名)。后续阶段改进了对例如日志和库的支持,并且还朝着支持多租户 AA 发展,在这种情况下,目标是尝试支持 tx.target 代表处理来自多个用户的传入活动的 应用程序 的情况。

Nonce 仍然体现在单租户 AA 中

Nonce 仍然在单租户 AA 中强制执行,以确保单目标 AA 不会破坏每个交易(因此每个交易哈希)只能在链中包含一次的不变性。虽然在单租户 AA 中允许任意顺序的交易包含有一些有限的价值,但没有足够的价值来证明破坏该不变性是合理的。

请注意,AA 账户中的 nonce 最终具有双重用途:它们既用于重放保护,也用于在使用 CREATE 操作码时生成合约地址。这意味着单个交易可以将 nonce 增加 1 以上。这被认为是可接受的,因为 AA 引入的其他机制已经破坏了轻松验证可以处理一条以上交易的链的能力。但是,我们强烈建议 AA 合约使用 CREATE2 而不是 CREATE

如上所述,在多租户 AA 中,nonce 预计会变得易于修改,并且使用多租户 AA 系统的应用程序需要管理这一点。

Nonce 暴露给 EVM

这样做是为了允许在验证代码中完成的签名检查来验证 nonce。

重放保护

必须实施上述两种方法之一(要求 SET_INDESTRUCTIBLE 或修改 SELFDESTRUCT 行为),以便不能重复使用 nonce。它必须是共识变更,而不仅仅是 AA_PREFIX 的一部分,以便保持交易哈希的唯一性。

矿工拒绝在 PAYGAS 之前访问外部数据或目标自身余额的交易

传统交易的一个重要属性是,作为源自给定账户 X 之外的交易的一部分发生的活动不能使发送者为 X 的交易无效。外部交易可以对 X 施加的唯一状态变化是增加其余额,这不会使交易无效。

允许 AA 合约在调用 PAYGAS 之前(即在验证阶段期间)访问外部数据(包括其他账户和诸如 GASPRICE、DIFFICULTY 等环境变量)会破坏此不变性。例如,假设有人发送了数千个执行外部调用的 AA 交易 if FOO.get_number() != 5: throw()。当所有这些交易都被发送时,FOO.number 可能设置为 5,但是单个发送到 FOO 的交易可能会将 number 设置为其他值,从而使_所有数千个依赖它的 AA 交易_无效。这将是一个严重的 DoS 向量。

唯一允许的例外是不可销毁的合约(即,第一个字节是 此 EIP 中定义的 SET_INDESTRUCTIBLE 操作码)。这是一个安全的例外,因为正在读取的数据无法更改。

不允许读取 BALANCE 会阻止较温和的攻击媒介:攻击者可以仅以 6700 gas 的成本(而不是 15000 或 21000)强制重新处理交易,在最坏的情况下,重新处理的交易数量会增加一倍以上。

从长远来看,可以扩展 AA 以允许读取外部数据,尽管需要诸如强制访问列表之类的保护措施。

AA 交易必须调用带有前缀的合约

前奏用于确保只有 AA 交易才能调用合约。这是为确保上述不变性而采取的另一项措施。如果未进行此检查,则有可能来自 AA 账户 X 之外的交易调用进入 X 并进行存储更改,从而强制以仅 5000 gas 的成本重新处理以该账户为目标的交易。

多租户 AA

多租户 AA 通过更好地处理不同的且不协调的用户尝试发送用于/发送到同一账户的交易,并且这些交易可能会相互干扰的情况来扩展单租户 AA。

我们可以通过检查两个示例用例来理解多租户 AA 的价值:(i) tornado.cash 和 (ii) Uniswap。在这两种情况下,都有一个代表应用程序的单个中心合约,而不是任何特定用户。然而,使用抽象来对交易进行特定于应用程序的验证仍然具有重要的价值。

Tornado Cash

tornado.cash 工作流程如下:

  1. 用户向 TC 合约发送一笔交易,存入一些标准数量的代币(例如 1 ETH)。包含用户已知的秘密哈希的存款记录被添加到 Merkle 树中,该 Merkle 树的根存储在 TC 合约中。
  2. 当用户稍后想要提款时,他们会生成并发送一个 ZK-SNARK,证明他们知道一个秘密,该秘密的哈希在存款树中的某个叶子中(而不透露它在哪儿)。TC 合约验证 ZK-SNARK,并且还验证了一个 nullifier 值(也可以从秘密派生)尚未被花费。该合约将 1 ETH 发送到用户所需的地址,并保存一条记录,表明用户的 nullifier 已被花费。

TC 提供的隐私来自于当用户进行提款时,他们可以证明它来自_某些_唯一的存款,但除了用户之外,没有人知道它来自哪个存款。然而,天真地实现 TC 存在一个致命的缺陷:用户通常尚未在他们的提款地址中存入 ETH,并且如果用户使用他们的存款地址来支付 gas,则会在他们的存款地址和他们的提款地址之间创建一个链上链接。

目前,这可以通过中继者来解决;第三方中继者验证 ZK-SNARK 和 nullifier 的未花费状态,使用他们自己的 ETH 发布该交易来支付 gas,并从 TC 合约收回用户的费用。

AA 允许在没有中继者的情况下执行此操作:用户可以简单地发送一个以 TC 合约为目标的 AA 交易,ZK-SNARK 验证和 nullifier 检查可以在验证步骤中完成,并且可以直接在此之后调用 PAYGAS。这允许提款人直接从发送到其提款地址的代币中支付 gas,从而避免了对中继者的需求或与提款地址的链上链接。

请注意,完全实现此功能需要以一种支持多个用户同时发送提款的方式构建 AA(需要 nonce 会使这变得困难),并且允许单个账户同时支持 AA 交易(提款)和外部发起的调用(存款)。

Uniswap

可以构建Uniswap的新版本,该版本允许发送直接针对Uniswap合约的交易。 用户可以提前将代币存入Uniswap,Uniswap 将存储他们的余额以及一个公钥,可以针对该公钥验证花费这些余额的交易。 AA发起的Uniswap交易将只能花费这些内部余额。

对于普通交易者来说,这毫无用处,因为普通交易者在Uniswap合约之外拥有他们的代币,但对于套利者来说,这将是一个强大的福音。 套利者会将他们的代币存入Uniswap,并且只要外部市场条件发生变化,他们就能够发送执行套利的交易,并且可以在验证步骤中执行诸如价格限制之类的逻辑。 因此,没有进入的交易(例如,因为其他套利者首先进行了交易)将不会包含在链上,从而允许套利者不支付gas,并减少链上包含的“垃圾”交易的数量。 这可以显着提高实际的区块链可扩展性和市场效率,因为套利者将能够更精细地纠正交易所价格之间的差异。

请注意,这里Uniswap 还需要支持 AA 交易和外部发起的调用。

向后兼容性

此 AA 实现保留了现有的交易类型。使用 assert origin == caller 来验证账户是否为 EOA 的做法仍然有效,但不适用于 AA 账户;AA 交易将始终具有 origin == AA_ENTRY_POINT

设计不佳的单租户 AA 合约将破坏交易不可延展性不变性。也就是说,可以获取正在进行的 AA 交易,修改它,并使修改后的版本仍然有效;可以设计 AA 账户合约以使其不可能,但这是它们的责任。多租户 AA 将更彻底地破坏交易不可延展性不变性,即使对于使用多租户 AA 功能的合法应用程序而言,交易哈希也是不可预测的(但对于之前存在的应用程序而言,不变性不会进一步破坏)。

除非 AA 合约显式地内置了重放保护功能,否则它们可能不具有重放保护功能;这可以使用 EIP 1344 中引入的 CHAINID (0x46) 操作码来完成。

测试用例

参见:https://github.com/quilt/tests/tree/account-abstraction

实现

参见:https://github.com/quilt/go-ethereum/tree/account-abstraction

安全注意事项

请参阅 https://ethresear.ch/t/dos-vectors-in-account-abstraction-aa-or-validation-generalization-a-case-study-in-geth/7937 以获取对 DoS 问题的分析。

重新验证

当交易进入 mempool 时,客户端能够快速确定交易是否有效。一旦确定了这一点,它就可以确信该交易将继续有效,除非来自同一账户的交易使其失效。

但是,在某些情况下,攻击者可以发布使现有交易失效的交易,并要求网络执行比交易本身计算量更多的重新计算。EIP 保持了重新计算在单个区块中被限制为理论上的最大值为区块 gas 限制的六倍的不变性;这比以前稍微贵一些,但没有贵那么多。

对等网络拒绝服务

由于难以识别对等列表中的 sybil,因此难以防御拒绝服务攻击。在任何时候,某人都可以决定(或被贿赂)发起攻击。这不是账户抽象引入的问题。今天,它可以通过用签名无效的交易淹没目标来针对现有客户端来完成。但是,由于 AA 允许增加验证工作的分配量,因此必须限制对手可以使用无效交易强迫客户端花费的计算量。因此,矿工最好遵循推荐的挖矿策略。

版权

版权和相关权利通过 CC0 放弃。

Citation

Please cite this document as:

Vitalik Buterin (@vbuterin), Ansgar Dietrichs (@adietrichs), Matt Garnett (@lightclient), Will Villanueva (@villanuevawill), Sam Wilson (@SamWilsn), "EIP-2938: 账户抽象 [DRAFT]," Ethereum Improvement Proposals, no. 2938, September 2020. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-2938.