EIP-3074 提案翻译

摘要该EIP引入了两个EVM指令AUTH和AUTHCALL。第一个基于ECDSA签名设置上下文变量authorized。第二个以authorized账户发送调用。

摘要

该 EIP 引入了两个 EVM 指令AUTHAUTHCALL。第一个基于 ECDSA 签名设置上下文变量authorized。第二个以authorized账户发送调用。这实质上将外部拥有账户(EOA)的控制权委托给智能合约。

动机

向 EOA 添加更多功能一直是一个长期存在的功能请求。这些请求涵盖了从实现批处理功能、允许进行 gas 赞助、到过期、脚本等等的功能。这些变化通常意味着协议的复杂性和刚性增加。在某些情况下,这也意味着增加了攻击面。

该 EIP 采取了一种不同的方法。与其将这些功能作为交易有效性要求固化在协议中,不如允许用户委托他们的 EOA 控制权给一个合约。这为开发人员提供了一个灵活的框架,用于为 EOA 开发新颖的交易方案。该 EIP 的一个激励用例是,它允许任何 EOA 像一个智能合约钱包一样操作,而无需部署合约。

尽管该 EIP 为个人用户提供了巨大的好处,但该 EIP 的主要动机是“赞助交易”。这是指交易的费用由发起调用的账户不同的账户提供。

随着以太坊上代币的异常增长,EOA 持有有价值的资产而根本不持有任何以太币已经变得很常见。今天,这些资产必须在使用以太币支付 gas 费用之前转换为以太币。然而,如果没有以太币支付转换费用,就无法将它们转换。赞助交易打破了这种循环依赖。

规范

约定

  • top - N - EVM 堆栈上第N个最近推送的值,其中top - 0是最近的。
  • || - 字节连接运算符。
  • invalid execution - 无效的执行,必须立即退出当前执行上下文,消耗所有剩余的 gas(与堆栈下溢或无效跳转的方式相同)。

常量

常量
MAGIC 0x04

MAGIC用于 EIP-3074 签名,以防止与其他签名格式的冲突。

上下文变量

变量 类型 初始值
authorized address 未设置

上下文变量authorized应指示当前执行上下文中AUTHCALL指令的活动账户。如果设置,authorized应仅包含已授权合约代表其行事的账户。未设置的值应表示尚未设置此类账户,并且在当前执行上下文中的AUTHCALL指令中尚无活动账户。

该变量的作用域与程序计数器相同--authorized在合约的单个执行上下文中持续存在,但不会通过任何调用(包括DELEGATECALL)传递。如果在单独的执行上下文中执行相同的合约(例如CALL到自身),则这两个执行上下文的authorized值将是独立的。在每个执行上下文的初始阶段,authorized始终未设置,即使之前的同一合约的执行上下文具有值。

AUTH (0xf6)

0xf6 表示创建的一个新的操作码AUTH。它应接受三个堆栈元素输入(最后两个描述内存范围),并返回一个堆栈元素。

输入

堆栈
堆栈
top - 0 authority
top - 1 offset
top - 2 length
内存

最后两个堆栈参数(offsetlength)描述了一个内存范围。该范围的内容格式如下:

  • memory[offset : offset+1 ] - yParity
  • memory[offset+1 : offset+33] - r
  • memory[offset+33 : offset+65] - s
  • memory[offset+65 : offset+97] - commit

输出

堆栈
堆栈
top - 0 success
内存

此指令不会修改内存。

行为

如果length大于 97,则额外字节将被忽略以进行签名验证(它们仍会产生后续定义的 gas 成本)。如果范围之外的字节(如果length小于 97)将被视为零处理。

authority是生成签名的账户的地址。

参数(yParityrs)被解释为 secp256k1 曲线上对消息keccak256(MAGIC || chainId || nonce || invokerAddress || commit)的 ECDSA 签名,其中:

  • chainId是当前链的 EIP-155 唯一标识符,填充到 32 字节。
  • nonce是签名者的当前 nonce,左填充到 32 字节。任何其他值都被视为无效。
  • invokerAddress是执行AUTH的合约地址(或在CALLCODEDELEGATECALL上下文中的活动状态地址),左填充为 32 字节(例如0x000000000000000000000000AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA)。
  • commit,传递给AUTH的参数之一,是一个 32 字节的值,可用于承诺在调用者的预处理逻辑中满足特定的附加有效条件。

签名的有效性和签名者还原(recovery)类似于交易签名,包括用于防止 ECDSA 可变性的更严格的s范围。请注意,预期yParity01

如果签名有效且签名者地址等于authority,则上下文变量authorized将设置为authority。特别地,如果authority == tx.origin,这在该 EIP 早期版本中曾单独处理(请参阅安全注意事项)。如果签名无效或签名者地址不等于authority,则authorized将重置为未设置值。

AUTH如果authorized已设置,则返回1,否则返回0

Gas 成本

AUTH的 gas 成本等于:

  • 固定费用3100
  • 内存扩展 gas 成本(auth_memory_expansion_fee)。
  • 如果authority是热的,则为100,如果是冷的,则为2600(根据 EIP-2929)。

固定费用等于ecrecover预编译的成本,再加上一点额外费用以覆盖 keccak256 哈希和一些额外逻辑。

内存扩展 gas 成本(auth_memory_expansion_fee)应按照RETURN的方式计算,其中如果指定范围超出当前分配,则会扩展内存。

AUTHCALL (0xf7)

应在0xf7处创建一个新的操作码AUTHCALL。它应接受七个堆栈元素,并返回一个堆栈元素。它与现有的CALL0xF1)指令的行为相匹配,除非另有说明。

输入

堆栈
top - 0 gas
top - 1 addr
top - 2 value
top - 3 argsOffset
top - 4 argsLength
top - 5 retOffset
top - 6 retLength

输出

top - 0 success

行为

AUTHCALL 被解释为与 CALL 相同,除了(注意:此列表也是逻辑检查的优先顺序):

  • 如果 authorized 未设置,则执行无效(如上所定义)。否则,调用的地址(msg.sender)将设置为 authorized
  • 包括子调用可用的 gas 在内的 gas 成本在 Gas 成本部分中指定。
  • 如果 gas 操作数等于 0,指令将根据 EIP-150 发送所有可用的 gas。
  • 如果子调用的可用 gas 少于 gas,执行无效。
  • 即使对于非零 value,也没有 gas 铺贴。
  • valueauthorized 的余额中扣除。如果 value 高于 authorized 的余额,则执行无效。

AUTHCALL 必须将调用深度增加一。AUTHCALL 不能将调用深度增加两,因为如果首先调用授权账户,然后调用目标账户,它将增加两个。

使用 RETURNDATASIZE (0x3d) 和 RETURNDATACOPY (0x3e) 访问的返回数据区域必须与 CALL 指令设置方式相同。

重要的是,AUTHCALL 不会重置 authorized,而是保持不变。

Gas 成本

AUTHCALL 的 gas 成本应为:

  • 静态 gas 成本(warm_storage_read
  • 内存扩展 gas 成本(memory_expansion_fee
  • 动态 gas 成本(dynamic_gas
  • 子调用中可用于执行的 gas(subcall_gas

内存扩展 gas 成本(memory_expansion_fee)应与 CALL 相同方式计算。

动态 gas 部分(dynamic_gas)和子调用中可用于执行的 gas(subcall_gas)应计算为:

dynamic_gas = 0

if addr not in accessed_addresses:
    dynamic_gas += 2500         # cold_account_access - warm_storage_read

if value > 0:
    dynamic_gas += 6700         # NB: Not 9000, like in `CALL`
    if is_empty(addr):
        dynamic_gas += 25000

remaining_gas = available_gas - dynamic_gas
all_but_one_64th = remaining_gas - (remaining_gas // 64)

if gas == 0:
    subcall_gas = all_but_one_64th
elif all_but_one_64th < gas:
    raise                       # Execution is invalid.
else:
    subcall_gas = gas

CALL 一样,完整的 gas 成本会立即收取,而不管实际执行调用。

原理

内存中的签名

签名格式(yParityrs)是固定的,因此 auth 接受动态内存范围可能看起来有些奇怪。签名放置在内存中,以便将来可以升级 auth 以与合约账户一起使用(可能使用非 ECDSA 签名),而不仅仅是 EOA。

对签名地址 auth 参数的理解

authority(签名地址)包含为 auth 的参数允许将来升级指令以与合约账户一起使用,而不仅仅是 EOA。

如果不包括 authority 并允许多个签名方案,则无法仅通过签名本身计算授权账户的地址。

保留可用 gas 的六十四分之一

AUTHCALL 不会传递超过可用 gas 的 63/64,原因在 EIP-150 中列举。

AUTHCALL 期间对未设置的 authorized 抛出异常

一个行为良好的合约在未成功设置 authorized 的情况下永远不应该到达 AUTHCALL。因此,最安全的行为是立即退出当前执行堆栈。这在交易赞助/中继的背景下尤为重要,这被期望是该 EIP 的主要用例之一。在赞助交易中,无法区分被赞助方可归因的故障(如失败的子调用)还是赞助方不可归因的故障(如失败的 AUTH)尤为危险,应予以防止,因为这会对赞助方收取不公平的费用。

另一个赞助交易 EIP

将“付费方”与“操作发起方”分离有两种一般方法。

第一种是引入新的交易类型。这需要对客户端进行重大更改以支持,并且通常比其他解决方案(例如此 EIP)更不易升级。这种方法也不立即与账户抽象(AA)兼容。这些提案需要赞助方账户的签名交易,而 AA 合约无法进行这种签名,因为它没有私钥可用于签名。引入新交易类型的主要优势在于,有效性要求由协议强制执行,因此无效交易不会污染区块空间。

另一种主要方法是在 EVM 中引入一种伪装成其他账户的新机制。此 EIP 引入 AUTHAUTHCALL 以作为 EOA 进行调用。这种机制有许多不同的变体。另一种替代机制是添加一个可以根据类似于 CREATE2 的地址创建方案进行任意调用的操作码。尽管这种机制今天不会使用户受益,但它将立即允许这些账户发送和接收以太币——使其感觉更像是一种更高级的原语。

除了与 AA 更好的兼容性外,将新机制引入 EVM 比引入新交易类型要少得多侵入性。这种方法不需要对现有钱包进行任何更改,并且对其他工具的更改也很少。

AUTHCALLCALL 的唯一偏差是设置 CALLER。它实现了使赞助交易的发送方抽象化的最小功能。这种单一性使 AUTHCALL 与现有以太坊功能显著更具组合性。

可以围绕 AUTHCALL 指令实现更多逻辑,为调用者和赞助方提供更多控制,而不会牺牲赞助方的安全性或用户体验。

签名什么内容?

最初的提案规定了一个带有存储的预编译来跟踪 nonce。由于带有存储的预编译是前所未有的,因此修订版将重放保护移至调用者合约,这需要用户对调用者的一定程度信任。在扩展对受信任调用者的这一想法时,其他签名字段逐渐被逐一淘汰,最终只剩下 invokercommit

invoker 将特定的签名消息绑定到单个调用者合约。如果调用者不是消息的一部分,任何调用者都可以重用签名来完全破坏 EOA。这使用户可以相信他们的消息将按照他们期望的方式进行验证,特别是在 commit 中承诺的值。

理解 commit

此 EIP 的早期版本包括用于重放保护的机制,并且还对 AUTHCALL 的值、gas 和其他参数进行了签名。经过进一步调查,我们将此 EIP 修订为其当前状态:明确将这些责任委托给调用者合约。

用户将与他们信任的调用者合约专门交互。因为他们信任这个合约会忠实执行,所以他们将通过计算调用值的哈希来“commit”到他们希望进行的调用的某些属性。可以确信,只有在调用者能够验证承诺的值(例如用于防止重放攻击的 nonce)时,调用才会继续进行。这种确定性来自用户签名的 commit 值。这是用户签名的内容的哈希,调用者将验证的值。一个安全的调用者应该接受用户的内容并自行计算 commit 哈希。这确保了调用者对用户授权的相同输入进行操作。

auth 消息格式

使用 commit 作为内容的哈希允许调用者实现任意约束。例如,他们可以允许账户具有 N 个并行 nonce。或者,他们可以允许用户承诺多个调用与单个签名验证。这将允许多个调用的流程,例如 ERC-20 approve-transfer 操作,被压缩为单个交易并进行单个签名验证。对多个调用的承诺将类似于下面的图表。

多次调用授权消息

调用者(invoker)合约

调用者(invoker)合约是赞助方和受赞助方之间的无信任中介。受赞助方在 invoker 的签名,来实现他们要求的交易只能由他们信任的合约处理。这使他们能够与赞助方交互而无需信任他们。

选择调用者类似于选择智能合约钱包实现。重要的是选择一个经过彻底审查、测试并被社区接受,认为是安全的调用者。我们预计大多数主要交易中继提供商将使用一些调用者(invoker)设计,还会有一些提供更新颖机制的外围层。

一个重要的注意事项是调用者合约不能是可升级的。如果可以将调用者重新部署到相同地址并使用不同代码,那么可能会重新部署调用者并使用未正确验证 commit 的代码,从而危及签署了关于该调用者(invoker)的消息的任何账户。尽管听起来很可怕,但这与通过 DELEGATECALL 使用智能合约钱包没有什么不同。如果钱包使用不同逻辑重新部署,所有使用其代码的钱包都可能会受到威胁。

调用深度

EVM 限制了最大嵌套调用次数,如果允许赞助方在到达调用者(invoker)之前操纵调用深度,将引入对受赞助方的一种破坏性攻击。也就是说,通过 63/64 Gas规则和 AUTHCALL 的成本,堆栈实际上受到 gas 参数的限制,比最大深度硬顶要小得多。

因此,调用者(invoker)保证一定数量的 gas 是足够的,因为没有办法用任何合理的(即少于数十亿)Gas达到最大调用深度硬顶。

value 的来源

在这个 EIP 的之前版本中,在执行中间扣除 EOA 的 value 被认为是有问题的。这是由于待处理交易的不变性,允许在交易池静态确定给定交易的有效性。

然而,经过进一步调查,我们发现破坏不变性是安全的。这主要是因为最坏情况在这两种情况下是相似的。

目前,攻击者可以在交易池中排队许多交易,跨多个账户,并在一个区块中使它们全部无效,其中每个排队的账户发送一个转账,转走其整个余额的在交易。这种攻击在这个 EIP 之后将变得更容易和更便宜,因为它将不再需要直接访问区块构建者,并且不需要花费完整的 21000 gas 来发起每个在交易。然而,这种攻击对网络没有实质性影响,因此降低难度和成本并不是一个问题。

允许 tx.origin 作为签名者

允许 authorized 等于 tx.origin 可以实现简单的交易批处理,其中外部交易的发送方将成为签名账户。ERC-20 授权后转账模式,目前需要两个单独的交易,可以通过这个提案在单个交易中完成。

AUTH 允许使用 tx.origin 签名。对于任何这样的签名,随后的 AUTHCALL 在其执行的第一层中具有 msg.sender == tx.origin。没有 EIP-3074,这种情况只能出现在交易的最顶层执行层中。这个 EIP 打破了这个不变性,因此影响包含 require(msg.sender == tx.origin) 检查的智能合约。这个检查可以用于至少三个目的:

  1. 确保 msg.sender 是一个 EOA(鉴于 tx.origin 必须始终是一个 EOA)。这个不变性不依赖于执行层深度,因此不受影响。
  2. 保护免受原子夹击攻击的影响,比如闪电贷,这些攻击依赖于在同一个原子交易的目标合约执行之前和之后修改状态的能力。这种保护将被这个 EIP 打破。然而,依赖 tx.origin 这种方式被认为是不良实践,矿工可以有条件地将交易包含在一个区块中,从而可以绕过这种保护。
  3. 防止递归调用。

可以在以太坊主网上部署的合约中找到(1)和(2)的示例,(1)更常见(并且不受此提案影响)。另一方面,用例(3)受到此提案的影响更严重,但本 EIP 的作者没有找到这种形式的递归保护的任何示例,尽管搜索并不是穷尽的。

这种出现情况的分布——许多(1),一些(2),没有(3)——正是这个 EIP 的作者所期望的,因为:

  • 在没有 tx.origin 的情况下确定 msg.sender 是否是 EOA 是困难的(如果不是不可能的)。
  • 唯一安全免受原子夹击攻击的执行上下文是最顶层上下文,而 tx.origin == msg.sender 是检测该上下文的唯一方法。
  • 相比之下,有许多直接和灵活的方法来防止递归调用(例如使用存储变量)。由于 msg.sender == tx.origin 只在最顶层上下文中为真,因此它将成为一种防止递归调用的晦涩工具,而不是其他更常见的方法。

有其他方法可以减轻这种限制而不破坏不变性:

  • tx.origin 设置为常量 ENTRY_POINT 地址,用于 AUTHCALL
  • tx.origin 设置为调用者地址,用于 AUTHCALL
  • tx.origin 设置为从发送方、调用者和/或签名者地址派生的特殊地址。
  • 不允许 authorized == tx.origin。这将使简单的批处理用例变得不可能,但可以在将来放宽。

发送value 时 AUTHCALLCALL 更便宜

使用 CALL 发送非零值会使其成本增加 9,000。其中,6,700 用于覆盖余额转移的增加开销,2,300 用作补助金进入子调用以初始化其Gas计数器。AUTHCALL 不提供补助金,因此只收取基本的 6,700。

协议内撤销

这个 EIP 在如何处理 AUTH 消息撤销方面来回摇摆。没有撤销,这个 EIP 对于开发人员来说是一个极其强大和灵活的原语。然而,对于使用不安全和/或积极恶意的调用者(invoker)的用户来说,它确实存在风险。

很大一部分风险是由于用户可以在单个交易中批处理许多操作的新能力。账户被耗尽变得更容易。这种风险将继续增长,无论是否采用这个 EIP,因为人们非常渴望这个功能,并试图在协议级别和应用级别支持它。

对于不安全和有缺陷的调用者(invoker)引入了一种新的风险类别。如果调用者已经按照作者的建议实现了重放保护,这应该大大限制爆炸半径。然而,如果 bug 允许对手规避重放保护机制,它可能使他们完全访问任何与易受攻击的调用者交互的 EOA。

尽管这是一个真正灾难性的事件,不太可能通过声誉良好的钱包实现,但这是一个严肃的考虑。没有协议内吊销,用户无法从易受攻击的调用者中移除他们的账户。

因此,AUTH 要求消息中的 nonce 必须等于签名者的当前 nonce。这样,来自 EOA 的单个 tx 将导致 nonce 增加,使所有未完成的授权无效。

向后兼容性

尽管这个 EIP 对向后兼容性没有问题,但有人担心它通过进一步确立 ECDSA 签名来限制未来对账户的更改。例如,彻底消除 EOA 的概念,并用模拟相同行为的智能合约钱包替换它们可能是可取的。按照目前的 EIP 编写,这是完全兼容的,但是,如果用户可以选择“升级”他们的智能合约钱包以使用其他身份验证方法,比如转换为多重签名,情况就会变得棘手。没有任何更改,AUTH将不会遵守这种新逻辑,并继续允许旧私钥代表账户执行操作。

解决这个问题的方法是在移除 EOA 的同时,修改AUTH的逻辑,实际上调用账户并允许账户确定签名/见证是否有效。需要进一步研究如何在这种情况下调用者要进行怎样的更改,以及如何以符合未来兼容性的方式编写它们。

安全考虑

安全调用者

以下是调用者应该警惕的一些检查/陷阱/条件的非穷尽列表:

  • 重放保护(例如 nonce)应该由调用者(invoke了)实现,并包含在commit中。没有它,恶意行为者可以重复使用签名,重复其效果。
  • value 应该包含在 commit 中。没有它,一个恶意赞助商可能会导致被调用者产生意外效果。
  • gas 应该包含在 commit 中。没有它,一个恶意赞助商可能会导致被调用者耗尽 gas 并失败,使受赞助者受到伤害。
  • addrcalldata 应该包含在 commit 中。没有它们,一个恶意行为者可能会在任意合约中调用任意函数。

一个实现不良的调用者可以允许恶意行为者几乎完全控制签名者的 EOA。

允许 tx.origin 作为签名者

允许 authorized 等于 tx.origin 有可能:

  • 打破依赖 tx.origin 的原子三明治保护;
  • 打破 require(tx.origin == msg.sender) 风格的递归保护。

这个 EIP 的作者认为允许 authorized 等于 tx.origin 的风险是可以接受的,原因在 Rationale 部分有概述。

赞助交易中继

authorized 账户有可能导致赞助交易中继花费 gas 而不被任何人偿还,方法是使授权无效(增加账户的 nonce)或将相关资产从账户中转出。中继应该考虑这些情况,可能需要要求存入保证金或实施声誉系统。

点赞 1
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

1 条评论

请先 登录 后评论
翻译小组
翻译小组
0x9e64...7c84
大家看到好的文章可以在 GitHub 提 Issue: https://github.com/lbc-team/Pioneer/issues 欢迎关注我的 Twitter: https://twitter.com/UpchainDAO