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.

Replay a Nested EVM Call: Restore Storage and Reconstruct Surviving Logs

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.

中文版 · Blockchain learning guide · Previous article

Blockchain learning · Extension BCX016

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.
Study photograph for illustration; it does not establish that the pictured students are studying blockchain.

A failed nested call does not tell you to restore every account to the transaction’s starting values. You need the failed frame’s entry boundary, the earlier values recorded for each write, and the order in which effects occurred. Otherwise, a replay can erase a valid earlier change or preserve a change that should have disappeared.

This lesson gives you a complete synthetic action stream and asks for a concrete answer: reconstruct four final storage fields and the ordered list of surviving receipt logs. The central calculation is B.count moving from 10 to 12 to 99, back to 12, and finally to 15. Each value has a specific reason.

The broader idea of provisional nested work already appears in Additional Mathematics Solution Transactions. Here we apply a different procedure: machine-style reverse journal replay with supplied frame outcomes. We are calculating exact storage and log survivors, rather than judging a mathematical derivation’s validity.

Choose your reading route

In this guide

  • For the complete worked replay, follow sections 1–7 in order.
  • To check a suspicious result, compare the wrong algorithms in section 8.
  • To distinguish local return status from final receipt evidence, read sections 6, 7 and 9.
  • To reproduce the calculation independently, use section 10, then answer section 11 before reading its model answers.

Back to contents · Next section

1. Fix the accounts, fields and execution assumptions

The fictional existing contracts are R, A and B. Calls use ordinary CALL with zero transferred value and sufficient resources. The account names are distinct; repeated visits to B refer to the same account. Each ordinary call writes its own account’s supplied storage fields.

Initial tracked storage is R.phase = 0, R.handled = 0, A.stage = 0 and B.count = 10. These are symbolic account-qualified fields, not raw EVM storage slots. Log labels are already decoded teaching names, not actual topics or ABI bytes.

R is the root frame F0. It first calls B in frame F1, then calls A in frame F2. A calls B in frame F3. After F2 fails, R explicitly handles that failed call and continues to call B again in frame F4. There is no callback into an active ancestor and no reentrancy exercise.

Only the listed storage writes and symbolic log emissions affect our tracked application state. We exclude transaction fees, sender nonce, contract creation, DELEGATECALL, precompiles, transient storage, chain forks, finality and reorganizations. The supplied outcomes determine which frames return and which revert; no actual bytecode is executed.

Keep account identity and frame identity in separate columns. F1, F3 and F4 all execute at B, but occupy different positions in the call tree. An account-wide rule such as “keep B’s changes because B succeeded” cannot describe their different rollback scopes.

Back to contents · Next section

2. Build the teaching journal before attempting an undo

Our small model keeps a journal of reversible tracked effects. Before each write, append the account-qualified key and its previous value. Then apply the new value. For each symbolic log, append a journal entry and add the log to the current ordered log list.

At frame entry, record the current journal length as that frame’s cursor. On a successful return, leave its journal effects available to any still-active ancestor’s rollback scope. On a revert, remove entries from the end until reaching the failing frame’s cursor, restoring each recorded previous value or removing the matching last log.

The journal is a classroom representation of the tracked effects, not a claim about every field in an implementation’s real journal. We retain an external worksheet of attempted emissions and frame outcomes even when modeled receipt logs are removed. That worksheet is evidence for the exercise, not persistent contract storage.

Number attempted emissions from 1 as they occur. Never reuse an attempt number after an undo. Later, the surviving list receives new zero-based positions within this transaction’s modeled receipt. These two numbering systems answer different questions.

A useful hand calculation records journal length, active frame, changed key, previous value, new value and log label after every action. Recording previous values at the moment of each write is essential. The transaction-start value is not a substitute when an earlier sibling has already changed the same account.

Back to contents · Next section

3. First establish the effects outside the failing subtree

Enter F0 at journal cursor 0. R writes R.phase from 0 to 1, creating journal entry 1. R emits R.begin, creating entry 2 and attempted emission 1.

Enter F1 at cursor 2. B writes B.count from 10 to 12, creating entry 3. B emits B.pre, creating entry 4 and attempted emission 2. F1 returns successfully. Its effects remain in the journal and the current application state.

At this checkpoint, the storage is R.phase 1, R.handled 0, A.stage 0 and B.count 12. The current log list is R.begin, B.pre. The journal length is 4. This is the state that exists when R next enters F2.

The value 12 is not an estimate or a value inferred from a log. It is the result of an explicit supplied write in F1. The log is a separate tracked effect. Our replay uses the action stream to calculate both, without assuming that an arbitrary real log necessarily proves a storage update.

Record F2’s entry cursor as 4. The earlier F1 is a sibling of F2, not a descendant. That structural fact is why a later F2 revert will preserve the first four journal entries.

Back to contents · Next section

4. Append the failing subtree, including its successful child

Inside F2, A writes A.stage from 0 to 1. This is entry 5. A emits A.begin, entry 6 and attempted emission 3.

A then enters F3 at journal cursor 6. B currently reads count 12, not 10. F3 writes B.count from 12 to 99, producing entry 7 whose stored previous value is 12. It emits B.temporary, entry 8 and attempted emission 4. F3 returns successfully.

Back in F2, A writes A.stage from 1 to 2, producing entry 9 whose stored previous value is 1. A emits A.ready, entry 10 and attempted emission 5. F2 then executes the supplied revert outcome.

Immediately before that revert, tracked storage is R.phase 1, R.handled 0, A.stage 2 and B.count 99. The current log list contains five labels: R.begin, B.pre, A.begin, B.temporary, A.ready. The journal length is 10.

F3’s local success is a recorded execution outcome. It does not move entries 7 and 8 outside F2’s entry cursor. F2’s rollback interval includes all six entries appended after cursor 4, including the successful descendant’s effects on B.

Do not confuse “A is the failing account” with “only A’s fields are in scope.” The cursor marks a portion of the nested execution history, not a filter on account names. B appears on both sides of that boundary through different frames.

Back to contents · Next section

5. Undo exactly six entries, in reverse order

Start from journal length 10 and stop at F2’s cursor 4. The complete undo sequence is:

  1. Remove the A.ready log associated with entry 10.
  2. Undo entry 9, restoring A.stage from 2 to its recorded previous value 1.
  3. Remove the B.temporary log associated with entry 8.
  4. Undo entry 7, restoring B.count from 99 to its recorded previous value 12.
  5. Remove the A.begin log associated with entry 6.
  6. Undo entry 5, restoring A.stage from 1 to its recorded previous value 0.

The resulting storage is R.phase 1, R.handled 0, A.stage 0 and B.count 12. The remaining logs are R.begin and B.pre. Journal length is again 4. F2’s local status is failure, while F3’s local status remains success in the diagnostic worksheet, with its effects identified as rolled back by F2.

This calculation explains the whole A.stage sequence: 0 to 1 to 2 during execution, then 2 to 1 to 0 during undo. Restoring overwritten values in forward order would be wrong. If you first restored the older A.stage entry to 0 and then restored the later entry to 1, you would finish at 1, not the frame-entry value 0.

B.count similarly returns to 12, not 10. The journal entry being undone remembers the value immediately before F3’s write. Restoring transaction-start B.count would erase F1, even though F1 lies outside F2’s failed subtree.

The intermediate restoration to A.stage 1 is a step in the replay algorithm. We do not claim a separate externally observable transaction occurs between undo entries. The final restored snapshot, rather than an invented interleaving, is what R sees when its handled call resumes.

Back to contents · Next section

6. Continue the root and calculate the final storage

R explicitly handles F2’s failure. It writes R.handled from 0 to 1 and emits R.caught, attempted emission 6. These are new root-frame effects after the rollback; they are not restoration of the removed A logs.

R enters F4, another ordinary call to B. This frame reads restored B.count 12 and increments it by 3, producing 15. It emits B.post, attempted emission 7, and returns successfully.

Finally, R writes R.phase from 1 to 2 and emits R.done, attempted emission 8. F0 returns successfully. The final tracked storage is R.phase = 2, R.handled = 1, A.stage = 0 and B.count = 15.

Write B’s complete value path on one line: 10 → 12 → 99 → 12 → 15. The transitions are respectively F1’s write, F3’s write, F2’s undo of its descendant, and F4’s increment. Calling the final result “99 plus 3” would ignore the rollback that happened before F4 began.

The local frame outcomes are F0 success, F1 success, F2 failure, F3 success and F4 success. They are not five independent transaction receipt statuses. In the supplied successful-root case, the modeled top-level receipt status is 1.

Back to contents · Next section

7. Reconstruct the surviving log order

Eight emissions were attempted during the action stream. Attempts 3, 4 and 5 occurred in the F2 rollback interval and are removed from the modeled receipt log list. Attempts 1 and 2 preceded that interval; attempts 6, 7 and 8 followed the handled failure.

The surviving labels, in order, are R.begin, B.pre, R.caught, B.post and R.done. Their worksheet attempt numbers are 1, 2, 6, 7 and 8. Their positions within this transaction’s surviving receipt list are 0, 1, 2, 3 and 4 respectively.

In particular, R.caught moves into receipt-list position 2, but its worksheet attempt number remains 6. There are no empty receipt-list positions reserved for the removed A.begin, B.temporary or A.ready labels.

Do not group survivors by emitting account. A list containing all R labels first and B labels afterwards would contain the right names in the wrong order. The correct sequence preserves execution emission order among surviving entries, even though R and B alternate.

These positions are local to the transaction’s log list. They are not asserted to equal the block-wide logIndex values returned by a real JSON-RPC service. No actual receipt or block is supplied, so the exercise does not calculate that wider index.

Back to contents · Next section

8. Diagnose five wrong replay algorithms

The first wrong algorithm resets the whole transaction whenever any inner frame fails. At F2’s failure it would restore B.count to 10 and erase R.begin and B.pre, although they precede F2’s cursor. If execution then continued with an increment of 3, it would calculate 13 instead of 15. That numerical difference exposes the overly broad rollback boundary.

The second algorithm undoes only the failing account. It might correctly restore A.stage while leaving F3’s B.count at 99. F4 would then produce 102. It could also incorrectly retain B.temporary. The error is an account filter where a nested execution boundary is required.

The third algorithm keeps all effects from frames that locally returned successfully. It would preserve F3’s temporary write and log despite F2’s later failure. A local success label cannot serve as the sole retention rule; the ancestor rollback scope must also be applied.

The fourth algorithm restores overwritten values in forward order. The two A.stage entries make its defect visible: restoring 0 and then 1 leaves the wrong final value. Reverse order unwinds the latest write before the earlier write that it replaced.

The fifth algorithm groups surviving logs by account. It may get every storage value and survivor count right yet still produce a receipt list with the wrong ordering. Storage equality and a set of log names are insufficient checks for an ordered receipt-log reconstruction.

These failures should be tested separately. A checker that only asserts final B.count can miss a corrupted A.stage or misordered logs. A checker that only asserts five surviving logs can accept five incorrect labels. The task requires exact state, exact labels and exact order.

Back to contents · Next section

9. Control experiment: change only the root outcome

Run the same supplied actions, but replace F0’s final successful return with REVERT. Keep the earlier caught F2 failure and all subsequent actions unchanged. The root’s entry cursor is 0, so its failure undoes all remaining tracked effects back to that cursor.

The control result is R.phase 0, R.handled 0, A.stage 0 and B.count 10, with no modeled receipt logs and top-level status 0. Logs removed earlier by F2 do not reappear. The root undo removes the remaining survivors; it does not resurrect a previous failed subtree.

This is a control for the tracked application state only. It does not say that a real transaction’s sender nonce, fees or every surrounding system effect rolls back. Those components are explicitly outside this fixture.

The contrast separates two questions cleanly: which effects survived the handled nested failure, and which of those survived the eventual root outcome? A correct implementation of our tiny replay must answer both without changing the original action stream’s intermediate read expectations.

Back to contents · Next section

10. Verify with two different model representations

The journal checker stores previous values and reverses entries back to frame cursors. It checks the exact six-entry F2 undo, all five local outcomes, final storage, survivor labels, attempt numbers and the root-revert control.

An independent checker takes complete copies of tracked application state and the current log list at frame entry. A failure restores the relevant copy. That method does not need to implement reverse prior-value replay internally, so agreement helps detect an error specific to the journal representation.

Both approaches must reproduce the supplied action stream, including B reading 12 before both F3 and F4. The first read follows F1’s success; the second follows F2’s rollback. Those read assertions test the intermediate state, rather than only comparing final answers.

Agreement establishes internal consistency of this synthetic teaching fixture. Neither checker runs Geth, interprets EVM bytecode or validates a real transaction. It is not client conformance testing or a formal proof of an arbitrary contract. More opcodes or account effects would require a larger model and new evidence.

Back to contents · Next section

11. Exercises and model answers

  1. Why is F2’s cursor 4 rather than 0 or 6?
  2. What previous value must F3 record before writing B.count 99?
  3. Which three attempted logs disappear when F2 fails?
  4. What is the zero-based receipt-list position of R.caught?
  5. Does F3’s local success mean B.temporary survives?
  6. What changes in the final answer if the root also reverts?

Answers: 1. Four tracked journal effects occurred before F2 entered; cursor 0 belongs to the root and cursor 6 to F3. 2. It records 12, the value left by F1. 3. A.begin, B.temporary and A.ready. Their removal is applied in reverse journal order. 4. Position 2, although its worksheet attempted-emission number is 6. 5. No; F3’s effects remain inside F2’s rollback interval. 6. All tracked fields return to their initial values, the modeled receipt logs are empty and top-level status is 0.

For an additional check, list A.stage’s two undo values in order. The answer is 1 then 0. A response containing only “restore A” is too vague to demonstrate the reverse-journal procedure.

Back to contents · Next section

Sources and scope notes

Sources were checked on 12 October 2026. EIP-140, created 6 February 2017 and marked Final, specifies REVERT behavior. EIP-658, created 30 June 2017 and marked Final, describes the top-level receipt status distinction.

Pinned implementation references are Geth v1.16.5 ordinary CALL handling, journal reversal, StateDB snapshots and logs and receipt construction. They support the implementation comparison; the example is an original model, not a captured client trace or a claim about every future fork.

The final deliverable is a reproducible worksheet: account-qualified storage, frame cursors, reverse undo rows and ordered surviving logs. Keep those concrete records together. They show why the answer is 15, why A.stage is 0, and why exactly five log labels survive in that particular order.

Back to contents