VIEW THIS AS

Auto mode follows the Route Engine until you choose a viewpoint.

YOU ARE HERE

ROUTE CHECK

CONNECTED TO

WHAT NEXT

Use the canonical route for this room, or HELP if you are unsure.

重放嵌套EVM调用:恢复存储并重建保留日志

Three students in school uniforms work through open books at a classroom table, with textbooks and stationery nearby and study notes on the whiteboard behind them.

English version · 区块链学习导航 · 上一篇

区块链学习 · 延伸文章 BCX016

三名学生在教室桌旁一起阅读书本,身边放有课本和文具。
学习场景配图;不代表照片中的学生正在学习区块链。

嵌套调用失败,并不意味着要把每个账户都恢复到交易开始时的值。你需要知道失败调用帧的入口边界、每次写入记录的旧值,以及效果发生的顺序。否则,重放可能抹去本应保留的早期变更,或者留下本应消失的变更。

本文给出一段完整的合成动作流,要求得到具体答案:重建四个最终存储字段,以及回执中保留日志的准确顺序。核心计算是B.count从10变为12、再变为99,恢复到12,最后变为15。每个值都有明确原因。

关于嵌套工作暂时成功的更广泛思想,已有附加数学解题事务课程讨论。本文采用不同的具体程序:根据给定调用帧结果,进行机器式逆序撤销日志重放。任务是计算准确的存储值和保留日志,而不是判断数学推导是否有效。

选择适合你的阅读路线

本篇目录

  • 要完成整个重放,依次阅读第1—7节。
  • 要检查可疑结果,对照第8节的错误算法。
  • 要区分局部返回状态与最终回执证据,阅读第6、7、9节。
  • 要独立复现计算,使用第10节,再先回答第11节问题,最后查看参考答案。

返回目录 · 下一节

1. 固定账户、字段与执行假设

虚构的已存在合约为R、A、B。所有调用使用普通CALL,转移价值为零,资源充足。账户名称各不相同;多次调用B指向同一个账户。每个普通调用写入其自身账户中给定的存储字段。

初始跟踪存储为R.phase=0、R.handled=0、A.stage=0、B.count=10。这些是带账户限定的符号字段,不是原始EVM存储槽。日志标签是已经解码的教学名称,不是真实主题或ABI字节。

R是根调用帧F0。它先在F1调用B,再在F2调用A。A在F3调用B。F2失败后,R明确处理该失败,并继续在F4再次调用B。没有回调进入仍活跃的祖先调用帧,也没有重入练习。

只有列出的存储写入和符号日志发送会影响所跟踪的应用状态。模型不包括交易费用、发送者nonce、合约创建、DELEGATECALL、预编译合约、瞬态存储、链分叉、最终性或重组。哪些帧正常返回、哪些回退,均由给定结果决定;不执行真实字节码。

请把账户身份与调用帧身份分列记录。F1、F3、F4都在B上执行,却处在调用树的不同位置。“B成功了,所以保留B的变更”这种账户级规则,不能描述它们不同的回退范围。

返回目录 · 下一节

2. 撤销前先建立教学日志

小模型维护一份可撤销效果日志。每次写入之前,先追加带账户限定的键和旧值,再应用新值。每次发出符号日志时,追加一项撤销记录,并把该日志加入当前有序日志列表。

进入调用帧时,记录当前撤销日志长度,作为该帧的游标。正常返回时,保留其撤销记录,让仍活跃的祖先帧仍能在自己的回退范围内撤销它们。发生回退时,从末尾逐项移除,直到失败帧的入口游标;每一步恢复已记录的旧值,或移除对应的最后一条日志。

这里的撤销日志是跟踪效果的课堂表示,不声称覆盖真实实现日志中的每一种字段。即使模型回执中的日志被移除,外部工作表仍保留尝试发送记录和调用帧结果。该工作表是练习证据,不是持久化合约存储。

按发生顺序,从1开始给每次日志发送尝试编号。撤销后不要复用尝试编号。最终保留列表另行取得从0开始的交易内回执位置。这两种编号回答的是不同问题。

手工计算时,可在每一步后记录撤销日志长度、活跃调用帧、变更键、旧值、新值和日志标签。必须在每次写入发生时记录旧值。如果较早的同级调用已改变同一账户,就不能拿交易初始值来代替。

返回目录 · 下一节

3. 先建立失败子树之外的效果

F0进入时游标为0。R把R.phase从0写成1,形成撤销日志第1项。R发出R.begin,形成第2项,同时也是日志发送尝试1。

F1进入时游标为2。B把B.count从10写成12,形成第3项。B发出B.pre,形成第4项,也是发送尝试2。F1正常返回,其效果仍留在撤销日志和当前应用状态中。

此时检查点的存储是R.phase为1、R.handled为0、A.stage为0、B.count为12。当前日志列表是R.begin、B.pre。撤销日志长度为4。R随后进入F2时,看到的就是这个状态。

数值12不是估计,也不是仅从一条事件日志推断出来的值,而是F1中明确给定写入的结果。日志本身是另一项跟踪效果。重放依据动作流分别计算二者,不假设任意真实日志都能证明某项存储更新。

记录F2入口游标为4。较早的F1是F2的同级帧,不是其后代。正是这种结构,使之后的F2回退保留最前面的四项撤销记录。

返回目录 · 下一节

4. 追加失败子树,包括其中成功的子调用

在F2中,A把A.stage从0写成1,这是第5项。A发出A.begin,形成第6项和发送尝试3。

A随后进入F3,入口游标为6。B此时读取count为12,不是10。F3把B.count从12写成99,形成第7项;该项保存的旧值是12。然后发出B.temporary,形成第8项和发送尝试4。F3正常返回。

回到F2,A把A.stage从1写成2,形成第9项,保存的旧值是1。A发出A.ready,形成第10项和发送尝试5。随后F2按给定结果执行回退。

回退之前,跟踪存储为R.phase 1、R.handled 0、A.stage 2、B.count 99。当前列表有五个标签:R.begin、B.pre、A.begin、B.temporary、A.ready。撤销日志长度为10。

F3的局部成功是一项已记录的执行结果,不会把第7、8项移动到F2入口游标之外。F2的回退区间覆盖游标4之后追加的全部六项,包括成功后代对B产生的效果。

不要把“A是失败账户”理解成“只有A的字段需要撤销”。游标标记的是嵌套执行历史中的一段范围,不是账户名称过滤器。B通过不同调用帧出现在这条边界的两侧。

返回目录 · 下一节

5. 准确逆序撤销六项

从撤销日志长度10开始,停在F2的游标4。完整撤销顺序如下:

  1. 移除第10项对应的A.ready日志。
  2. 撤销第9项,把A.stage从2恢复为该项记录的旧值1。
  3. 移除第8项对应的B.temporary日志。
  4. 撤销第7项,把B.count从99恢复为该项记录的旧值12。
  5. 移除第6项对应的A.begin日志。
  6. 撤销第5项,把A.stage从1恢复为该项记录的旧值0。

结果存储为R.phase 1、R.handled 0、A.stage 0、B.count 12。保留日志是R.begin和B.pre,撤销日志长度重新变为4。F2局部状态为失败;F3在诊断工作表中的局部状态仍为成功,但其效果注明已被F2回退撤销。

这解释了A.stage的完整路径:执行时从0到1到2,撤销时从2到1到0。按正序恢复被覆盖的值会出错。如果先用较早项把A.stage恢复为0,再用较晚项恢复为1,最终就会停在1,而不是调用帧入口时的0。

B.count同样恢复为12,不是10。正在撤销的记录保存的是F3写入前一刻的值。如果恢复成交易初始B.count,就会抹去F1,尽管F1位于F2失败子树之外。

A.stage中间恢复为1,是重放算法的一步。本文不声称撤销项之间存在另一笔可从外部观察的交易。R处理失败调用并恢复执行时看到的,是最终恢复后的快照,而不是虚构的交错执行。

返回目录 · 下一节

6. 根调用继续,计算最终存储

R明确处理F2失败,把R.handled从0写成1,并发出R.caught,即发送尝试6。这些是回退之后新增的根帧效果,不是恢复先前被移除的A日志。

R进入F4,再次普通调用B。这个帧读到恢复后的B.count为12,再增加3,得到15。它发出B.post,即发送尝试7,然后正常返回。

最后,R把R.phase从1写成2,发出R.done,即发送尝试8。F0正常返回。最终跟踪存储为R.phase=2、R.handled=1、A.stage=0、B.count=15。

把B的完整数值路径写成一行:10→12→99→12→15。依次对应F1写入、F3写入、F2撤销后代效果,以及F4增加。如果把最终结果算成“99加3”,就忽略了F4开始之前已经发生的回退。

各帧局部结果为:F0成功、F1成功、F2失败、F3成功、F4成功。它们不是五笔独立交易的回执状态。在给定的根成功案例中,模型顶层回执状态为1。

返回目录 · 下一节

7. 重建保留日志顺序

动作流中共发生八次日志发送尝试。尝试3、4、5位于F2回退区间,被从模型回执日志列表移除。尝试1、2发生在该区间之前;尝试6、7、8发生在处理失败之后。

保留标签按顺序为R.begin、B.pre、R.caught、B.post、R.done。对应的工作表尝试编号为1、2、6、7、8;对应的本交易保留回执列表位置则依次为0、1、2、3、4。

尤其要注意,R.caught在回执列表中位于2,但其工作表尝试编号仍为6。列表不会为已移除的A.begin、B.temporary、A.ready保留空位置。

不要按发送账户给保留日志分组。如果先列全部R标签,再列B标签,就会得到名称正确但顺序错误的列表。正确结果保留所有幸存项之间的发送顺序,即使R和B交替出现。

这些位置只针对当前交易的日志列表,不声称等于真实JSON-RPC服务返回的区块范围logIndex。练习没有提供真实回执或区块,因此不计算那个更大范围的索引。

返回目录 · 下一节

8. 诊断五种错误重放算法

第一种错误算法在任何内部帧失败时重置整笔交易。F2失败时,它会把B.count恢复为10,抹去R.begin和B.pre,尽管这些效果早于F2游标。如果之后继续增加3,就会得到13而不是15。这个数值差异暴露了过大的回退范围。

第二种算法只撤销失败账户。它可能正确恢复A.stage,却把F3的B.count保留在99,使F4算出102;还可能错误保留B.temporary。其问题是把需要嵌套执行边界的地方,换成了账户过滤。

第三种算法保留所有局部正常返回帧的效果。它会保留F3的临时写入和日志,无视F2后来失败。局部成功标签不能成为唯一保留规则,还必须应用祖先回退范围。

第四种算法按正序恢复被覆盖的旧值。两项A.stage记录正好揭示缺陷:先恢复0,再恢复1,最终值错误。逆序则先撤销最新写入,再撤销被它覆盖的较早写入。

第五种算法按账户整理保留日志。它可能把所有存储值和保留数量都算对,却给出错误的回执列表顺序。存储相等和日志名称集合相等,都不足以验证有序回执日志重建。

这些错误应分别测试。只检查最终B.count,会漏掉错误的A.stage或日志顺序;只检查保留五条日志,也可能接受五个错误标签。任务要求状态、标签和顺序都准确。

返回目录 · 下一节

9. 对照实验:只改变根调用结果

重新运行同一动作流,但把F0最后的正常返回替换为REVERT。此前处理F2失败的逻辑及之后全部动作保持不变。根帧入口游标为0,所以其失败会把剩余跟踪效果一直撤销到该游标。

对照结果为R.phase 0、R.handled 0、A.stage 0、B.count 10,没有模型回执日志,顶层状态为0。此前被F2移除的日志不会重新出现。根撤销移除的是剩余幸存项,不会复活先前失败的子树。

该对照只针对所跟踪的应用状态,不表示真实交易的发送者nonce、费用或所有周边系统效果都会回退。这些内容已明确排除在设定之外。

这一区别把两个问题分开:哪些效果在被处理的嵌套失败后保留?其中哪些又在最终根结果之后保留?正确的小型重放应同时回答两者,并保持原动作流的中间读取预期不变。

返回目录 · 下一节

10. 用两种不同模型表示来复核

撤销日志检查器保存旧值,按帧游标逆序恢复。它检查F2准确的六项撤销、五个局部结果、最终存储、保留标签、尝试编号,以及根回退对照。

独立检查器则在每个帧入口复制完整的跟踪应用状态和当前日志列表。失败时恢复相应副本。该方法不必在内部实现旧值逆序重放,因此两种结果一致,有助于发现仅由某一种日志表示方式引入的错误。

两种方法都必须复现给定动作流,包括F3和F4之前B都读到12。第一次读取来自F1成功后的状态;第二次读取来自F2回退后的状态。这些读取断言检查中间状态,而不是只比较最终答案。

一致性证明的是这个合成教学设定的内部相容性。两个检查器都不运行Geth,不解释EVM字节码,也不验证真实交易。它们不是客户端一致性测试,更不是任意合约的形式化证明。若增加操作码或账户效果,就需要更大模型和新证据。

返回目录 · 下一节

11. 练习与参考答案

  1. 为什么F2游标是4,而不是0或6?
  2. F3写入B.count 99之前必须记录哪个旧值?
  3. F2失败时,哪三次日志发送从最终列表消失?
  4. R.caught在从0开始的回执列表中位于哪里?
  5. F3局部成功是否意味着B.temporary保留?
  6. 如果根调用也回退,最终答案怎样改变?

答案:1. F2进入之前已有四项跟踪效果;游标0属于根帧,6属于F3。2. 记录F1留下的12。3. A.begin、B.temporary、A.ready;实际移除按逆序撤销日志进行。4. 位置2,尽管其工作表发送尝试编号是6。5. 不会;F3效果仍位于F2回退区间。6. 全部跟踪字段恢复初始值,模型回执日志为空,顶层状态为0。

再做一个检查:依次列出A.stage的两个撤销恢复值。答案是1,然后0。只写“恢复A”过于含糊,不能证明掌握了逆序撤销程序。

返回目录 · 下一节

来源与范围说明

来源于2026年10月12日核对。EIP-140创建于2017年2月6日,状态为Final,规定REVERT行为。EIP-658创建于2017年6月30日,状态为Final,描述顶层回执状态的区别。

固定版本实现参考为Geth v1.16.5的普通CALL处理、撤销日志逆序恢复、StateDB快照与日志,以及回执构造。这些来源支持实现对照;例子是原创模型,不是捕获的客户端轨迹,也不声称覆盖所有未来分叉。

最终成果应是一份可复现工作表:带账户限定的存储、调用帧游标、逆序撤销行和有序保留日志。把这些具体记录放在一起,才能解释为什么答案是15、为什么A.stage为0,以及为什么恰好有五个标签以这个顺序保留。

返回目录