摘要该EIP引入了两个EVM指令AUTH和AUTHCALL。第一个基于ECDSA签名设置上下文变量authorized。第二个以authorized账户发送调用。
该 EIP 引入了两个 EVM 指令AUTH
和AUTHCALL
。第一个基于 ECDSA 签名设置上下文变量authorized
。第二个以authorized
账户发送调用。这实质上将外部拥有账户(EOA)的控制权委托给智能合约。
向 EOA 添加更多功能一直是一个长期存在的功能请求。这些请求涵盖了从实现批处理功能、允许进行 gas 赞助、到过期、脚本等等的功能。这些变化通常意味着协议的复杂性和刚性增加。在某些情况下,这也意味着增加了攻击面。
该 EIP 采取了一种不同的方法。与其将这些功能作为交易有效性要求固化在协议中,不如允许用户委托他们的 EOA 控制权给一个合约。这为开发人员提供了一个灵活的框架,用于为 EOA 开发新颖的交易方案。该 EIP 的一个激励用例是,它允许任何 EOA 像一个智能合约钱包一样操作,而无需部署合约。
尽管该 EIP 为个人用户提供了巨大的好处,但该 EIP 的主要动机是“赞助交易”。这是指交易的费用由发起调用的账户不同的账户提供。
随着以太坊上代币的异常增长,EOA 持有有价值的资产而根本不持有任何以太币已经变得很常见。今天,这些资产必须在使用以太币支付 gas 费用之前转换为以太币。然而,如果没有以太币支付转换费用,就无法将它们转换。赞助交易打破了这种循环依赖。
top - N
- EVM 堆栈上第N
个最近推送的值,其中top - 0
是最近的。||
- 字节连接运算符。常量 | 值 |
---|---|
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 |
最后两个堆栈参数(offset
和length
)描述了一个内存范围。该范围的内容格式如下:
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
是生成签名的账户的地址。
参数(yParity
、r
、s
)被解释为 secp256k1 曲线上对消息keccak256(MAGIC || chainId || nonce || invokerAddress || commit)
的 ECDSA 签名,其中:
chainId
是当前链的 EIP-155 唯一标识符,填充到 32 字节。nonce
是签名者的当前 nonce,左填充到 32 字节。任何其他值都被视为无效。invokerAddress
是执行AUTH
的合约地址(或在CALLCODE
或DELEGATECALL
上下文中的活动状态地址),左填充为 32 字节(例如0x000000000000000000000000AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
)。commit
,传递给AUTH
的参数之一,是一个 32 字节的值,可用于承诺在调用者的预处理逻辑中满足特定的附加有效条件。签名的有效性和签名者还原(recovery)类似于交易签名,包括用于防止 ECDSA 可变性的更严格的s
范围。请注意,预期yParity
为0
或1
。
如果签名有效且签名者地址等于authority
,则上下文变量authorized
将设置为authority
。特别地,如果authority == tx.origin
,这在该 EIP 早期版本中曾单独处理(请参阅安全注意事项)。如果签名无效或签名者地址不等于authority
,则authorized
将重置为未设置值。
AUTH
如果authorized
已设置,则返回1
,否则返回0
。
AUTH
的 gas 成本等于:
3100
。auth_memory_expansion_fee
)。authority
是热的,则为100
,如果是冷的,则为2600
(根据 EIP-2929)。固定费用等于ecrecover
预编译的成本,再加上一点额外费用以覆盖 keccak256 哈希和一些额外逻辑。
内存扩展 gas 成本(auth_memory_expansion_fee
)应按照RETURN
的方式计算,其中如果指定范围超出当前分配,则会扩展内存。
AUTHCALL
(0xf7
)应在0xf7
处创建一个新的操作码AUTHCALL
。它应接受七个堆栈元素,并返回一个堆栈元素。它与现有的CALL
(0xF1
)指令的行为相匹配,除非另有说明。
堆栈 | 值 |
---|---|
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
操作数等于 0
,指令将根据 EIP-150 发送所有可用的 gas。gas
,执行无效。value
,也没有 gas 铺贴。value
从 authorized
的余额中扣除。如果 value
高于 authorized
的余额,则执行无效。AUTHCALL
必须将调用深度增加一。AUTHCALL
不能将调用深度增加两,因为如果首先调用授权账户,然后调用目标账户,它将增加两个。
使用 RETURNDATASIZE
(0x3d
) 和 RETURNDATACOPY
(0x3e
) 访问的返回数据区域必须与 CALL
指令设置方式相同。
重要的是,AUTHCALL
不会重置 authorized
,而是保持不变。
AUTHCALL
的 gas 成本应为:
warm_storage_read
)memory_expansion_fee
)dynamic_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 成本会立即收取,而不管实际执行调用。
签名格式(yParity
、r
和 s
)是固定的,因此 auth
接受动态内存范围可能看起来有些奇怪。签名放置在内存中,以便将来可以升级 auth
以与合约账户一起使用(可能使用非 ECDSA 签名),而不仅仅是 EOA。
auth
参数的理解将 authority
(签名地址)包含为 auth
的参数允许将来升级指令以与合约账户一起使用,而不仅仅是 EOA。
如果不包括 authority
并允许多个签名方案,则无法仅通过签名本身计算授权账户的地址。
AUTHCALL
不会传递超过可用 gas 的 63/64,原因在 EIP-150 中列举。
AUTHCALL
期间对未设置的 authorized
抛出异常一个行为良好的合约在未成功设置 authorized
的情况下永远不应该到达 AUTHCALL
。因此,最安全的行为是立即退出当前执行堆栈。这在交易赞助/中继的背景下尤为重要,这被期望是该 EIP 的主要用例之一。在赞助交易中,无法区分被赞助方可归因的故障(如失败的子调用)还是赞助方不可归因的故障(如失败的 AUTH
)尤为危险,应予以防止,因为这会对赞助方收取不公平的费用。
将“付费方”与“操作发起方”分离有两种一般方法。
第一种是引入新的交易类型。这需要对客户端进行重大更改以支持,并且通常比其他解决方案(例如此 EIP)更不易升级。这种方法也不立即与账户抽象(AA)兼容。这些提案需要赞助方账户的签名交易,而 AA 合约无法进行这种签名,因为它没有私钥可用于签名。引入新交易类型的主要优势在于,有效性要求由协议强制执行,因此无效交易不会污染区块空间。
另一种主要方法是在 EVM 中引入一种伪装成其他账户的新机制。此 EIP 引入 AUTH
和 AUTHCALL
以作为 EOA 进行调用。这种机制有许多不同的变体。另一种替代机制是添加一个可以根据类似于 CREATE2
的地址创建方案进行任意调用的操作码。尽管这种机制今天不会使用户受益,但它将立即允许这些账户发送和接收以太币——使其感觉更像是一种更高级的原语。
除了与 AA 更好的兼容性外,将新机制引入 EVM 比引入新交易类型要少得多侵入性。这种方法不需要对现有钱包进行任何更改,并且对其他工具的更改也很少。
AUTHCALL
与 CALL
的唯一偏差是设置 CALLER
。它实现了使赞助交易的发送方抽象化的最小功能。这种单一性使 AUTHCALL
与现有以太坊功能显著更具组合性。
可以围绕 AUTHCALL
指令实现更多逻辑,为调用者和赞助方提供更多控制,而不会牺牲赞助方的安全性或用户体验。
最初的提案规定了一个带有存储的预编译来跟踪 nonce。由于带有存储的预编译是前所未有的,因此修订版将重放保护移至调用者合约,这需要用户对调用者的一定程度信任。在扩展对受信任调用者的这一想法时,其他签名字段逐渐被逐一淘汰,最终只剩下 invoker
和 commit
。
invoker
将特定的签名消息绑定到单个调用者合约。如果调用者不是消息的一部分,任何调用者都可以重用签名来完全破坏 EOA。这使用户可以相信他们的消息将按照他们期望的方式进行验证,特别是在 commit
中承诺的值。
commit
此 EIP 的早期版本包括用于重放保护的机制,并且还对 AUTHCALL
的值、gas 和其他参数进行了签名。经过进一步调查,我们将此 EIP 修订为其当前状态:明确将这些责任委托给调用者合约。
用户将与他们信任的调用者合约专门交互。因为他们信任这个合约会忠实执行,所以他们将通过计算调用值的哈希来“commit”到他们希望进行的调用的某些属性。可以确信,只有在调用者能够验证承诺的值(例如用于防止重放攻击的 nonce)时,调用才会继续进行。这种确定性来自用户签名的 commit
值。这是用户签名的内容的哈希,调用者将验证的值。一个安全的调用者应该接受用户的内容并自行计算 commit 哈希。这确保了调用者对用户授权的相同输入进行操作。
使用 commit
作为内容的哈希允许调用者实现任意约束。例如,他们可以允许账户具有 N
个并行 nonce。或者,他们可以允许用户承诺多个调用与单个签名验证。这将允许多个调用的流程,例如 ERC-20 approve
-transfer
操作,被压缩为单个交易并进行单个签名验证。对多个调用的承诺将类似于下面的图表。
调用者(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)
检查的智能合约。这个检查可以用于至少三个目的:
msg.sender
是一个 EOA(鉴于 tx.origin
必须始终是一个 EOA)。这个不变性不依赖于执行层深度,因此不受影响。tx.origin
这种方式被认为是不良实践,矿工可以有条件地将交易包含在一个区块中,从而可以绕过这种保护。可以在以太坊主网上部署的合约中找到(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
。这将使简单的批处理用例变得不可能,但可以在将来放宽。AUTHCALL
比 CALL
更便宜使用 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
的逻辑,实际上调用账户并允许账户确定签名/见证是否有效。需要进一步研究如何在这种情况下调用者要进行怎样的更改,以及如何以符合未来兼容性的方式编写它们。
以下是调用者应该警惕的一些检查/陷阱/条件的非穷尽列表:
commit
中。没有它,恶意行为者可以重复使用签名,重复其效果。value
应该包含在 commit
中。没有它,一个恶意赞助商可能会导致被调用者产生意外效果。gas
应该包含在 commit
中。没有它,一个恶意赞助商可能会导致被调用者耗尽 gas 并失败,使受赞助者受到伤害。addr
和 calldata
应该包含在 commit
中。没有它们,一个恶意行为者可能会在任意合约中调用任意函数。一个实现不良的调用者可以允许恶意行为者几乎完全控制签名者的 EOA。
tx.origin
作为签名者允许 authorized
等于 tx.origin
有可能:
tx.origin
的原子三明治保护;require(tx.origin == msg.sender)
风格的递归保护。这个 EIP 的作者认为允许 authorized
等于 tx.origin
的风险是可以接受的,原因在 Rationale 部分有概述。
authorized
账户有可能导致赞助交易中继花费 gas 而不被任何人偿还,方法是使授权无效(增加账户的 nonce)或将相关资产从账户中转出。中继应该考虑这些情况,可能需要要求存入保证金或实施声誉系统。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!