Skip to content

feat: Implement StateJournal and in-memory snapshots for state rollback - #134

Merged
Zahnentferner merged 3 commits into
mainfrom
feature/state-rollback
Aug 10, 2026
Merged

feat: Implement StateJournal and in-memory snapshots for state rollback#134
Zahnentferner merged 3 commits into
mainfrom
feature/state-rollback

Conversation

@SIDDHANTCOOKIE

@SIDDHANTCOOKIE SIDDHANTCOOKIE commented Aug 3, 2026

Copy link
Copy Markdown
Member

Addressed Issues:

Previously, the node relied heavily on full copy.deepcopy() operations of the entire state dictionary. This PR replaces that with a highly optimized two-tier rollback architecture:

  1. Transaction-level Undo Log (StateJournal)
    Problem: When validating transactions inside a block, the node was making an expensive deep copy of the state before every transaction to allow for gas refunds/reverts upon failure.
    Solution: Introduced a StateJournal proxy dictionary in minichain/state.py. It acts as an in-memory cache that tracks diffs during transaction execution in O(1) time.
    Impact: If a transaction fails mid-execution (e.g., out of gas), the journal simply discards the cache (rollback()). If it succeeds, the diff is flushed to the backing dictionary (commit()), completely eliminating the need for transaction-level deep copies.
  2. Block-level In-Memory Snapshots
    Problem: During a chain reorganization (resolve_conflicts), the node had to fetch the genesis state and sequentially re-apply every single transaction in history up to the fork point.
    Solution: Implemented _state_snapshots in minichain/chain.py (an OrderedDict limited to the 10 most recent blocks).
    Impact: When a shallow reorg occurs, the node now fast-forwards the difficulty/timestamp calculations and restores the exact state at the fork point directly from memory. Replaying from genesis is now strictly a fallback mechanism for incredibly deep reorgs.

Screenshots/Recordings:

TODO: If applicable, add screenshots or recordings that demonstrate the interface before and after the changes.

Additional Notes:

AI Usage Disclosure:

We encourage contributors to use AI tools responsibly when creating Pull Requests. While AI can be a valuable aid, it is essential to ensure that your contributions meet the task requirements, build successfully, include relevant tests, and pass all linters. Submissions that do not meet these standards may be closed without warning to maintain the quality and integrity of the project. Please take the time to understand the changes you are proposing and their impact. AI slop is strongly discouraged and may lead to banning and blocking. Do not spam our repos with AI slop.

Check one of the checkboxes below:

  • This PR does not contain AI-generated code at all.
  • This PR contains AI-generated code. I have read the AI Usage Policy and this PR complies with this policy. I have tested the code locally and I am responsible for it.

I have used the following AI models and tools: TODO

Checklist

  • My PR addresses a single issue, fixes a single bug or makes a single improvement.
  • My code follows the project's code style and conventions
  • If applicable, I have made corresponding changes or additions to the documentation
  • If applicable, I have made corresponding changes or additions to tests
  • My changes generate no new warnings or errors
  • I have joined the Discord server and I will share a link to this PR with the project maintainers there
  • I have read the Contribution Guidelines
  • Once I submit my PR, CodeRabbit AI will automatically review it and I will address CodeRabbit's comments.
  • I have filled this PR template completely and carefully, and I understand that my PR may be closed without review otherwise.

Summary by CodeRabbit

  • Bug Fixes

    • Improved blockchain recovery during competing-chain updates, preserving the correct account state when possible.
    • Fixed transaction failure handling so state changes are rolled back consistently.
    • Improved refund calculations after unsuccessful transactions.
    • Prevented accidental account deletion during state updates.
  • Performance

    • Reduced unnecessary state copying for faster transaction processing.
    • Retained recent chain states in memory to speed up recovery.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The PR adds journaled account updates and bounded blockchain state snapshots. Transactions now commit or roll back through StateJournal. Block commits and reorganizations store or restore state snapshots.

Changes

State consistency and snapshot management

Layer / File(s) Summary
Journal-backed state copying
minichain/state.py
StateJournal lazily copies account reads, tracks writes, and supports commit or rollback. State.copy() now uses the journal.
Transactional account updates
minichain/state.py
Transaction paths commit successful journal changes, roll back failures, restore the original account mapping, and charge only used gas on failure.
Block state snapshots and reorganization
minichain/node_config.py, minichain/chain.py
The blockchain retains up to 10 state snapshots. Block commits and reorganizations record snapshots, and conflict resolution restores cached fork-point state when available.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Blockchain
  participant State
  participant StateJournal
  participant SnapshotDeque
  Blockchain->>State: apply validated transaction
  State->>StateJournal: track account mutations
  StateJournal-->>State: commit or rollback changes
  Blockchain->>State: finalize committed account state
  Blockchain->>SnapshotDeque: store block state snapshot
  Blockchain->>SnapshotDeque: restore cached fork-point snapshot during reorganization
Loading

Possibly related PRs

Suggested labels: Python Lang

Suggested reviewers: zahnentferner

Poem

A rabbit journals state with care,
Commits good changes, rolls back there.
Ten snapshots wait in memory,
Forks restore their history.
Blocks hop onward, clean and bright.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the two main changes: StateJournal and in-memory snapshots for state rollback.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/state-rollback

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

Comment thread minichain/chain.py Outdated
self._lock = threading.RLock()
import collections
self._state_snapshots = collections.OrderedDict()
self._max_snapshots = 10

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make this a node-level configuration parameter.

Comment thread minichain/chain.py Outdated
self.chain_id = "minichain-default"
self._lock = threading.RLock()
import collections
self._state_snapshots = collections.OrderedDict()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wondered if OrderedDict is really the best data structure for this purpose.

Fror example, deque (https://realpython.com/python-deque/) could be a good alternative, because it automatically evicts old items from the queue.

Could you investigate this?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, I investigated it and you're right that deque(maxlen=MAX_STATE_SNAPSHOTS) handles eviction much cleaner, so I've updated it to use that

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
minichain/chain.py (1)

303-324: 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Rebuild the snapshot cache after a successful reorganization.

Line 324 appends only the new tip snapshot. Snapshots for orphaned block hashes remain in _state_snapshots. Snapshots for newly adopted intermediate blocks are not recorded.

A later shallow reorganization at one of those intermediate blocks cannot find snapshot_found. It falls back to replay even when the fork is within MAX_STATE_SNAPSHOTS.

Capture a snapshot after each successfully replayed block. Then replace _state_snapshots with snapshots for the recent canonical blocks in proposed_chain only.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@minichain/chain.py` around lines 303 - 324, The reorganization flow must
rebuild _state_snapshots for the canonical chain instead of appending only the
new tip. In the block replay loop within the reorganization method, capture a
snapshot after each successfully applied block, then replace _state_snapshots
with snapshots for only the recent blocks from proposed_chain, bounded by
MAX_STATE_SNAPSHOTS; remove orphaned entries while retaining intermediate
adopted-block snapshots for future shallow reorganizations.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@minichain/chain.py`:
- Around line 303-324: The reorganization flow must rebuild _state_snapshots for
the canonical chain instead of appending only the new tip. In the block replay
loop within the reorganization method, capture a snapshot after each
successfully applied block, then replace _state_snapshots with snapshots for
only the recent blocks from proposed_chain, bounded by MAX_STATE_SNAPSHOTS;
remove orphaned entries while retaining intermediate adopted-block snapshots for
future shallow reorganizations.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 920b0513-a7f2-4896-bd65-8446a6291426

📥 Commits

Reviewing files that changed from the base of the PR and between 20d148e and 91a0e87.

📒 Files selected for processing (3)
  • minichain/chain.py
  • minichain/node_config.py
  • minichain/state.py

@github-actions

Copy link
Copy Markdown

⚠️ This PR has merge conflicts.

Please resolve the merge conflicts before review.

Your PR will only be reviewed by a maintainer after all conflicts have been resolved.

📺 Watch this video to understand why conflicts occur and how to resolve them:
https://www.youtube.com/watch?v=Sqsz1-o7nXk

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
minichain/chain.py (3)

152-158: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Copy self.chain while holding _lock.

When chain_list is None, this code stores the mutable self.chain and releases the lock before summing it. A concurrent add_block can change the sequence during iteration. Copy the chain inside the lock, for example with tuple(self.chain).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@minichain/chain.py` around lines 152 - 158, Update the chain_list
initialization in the work-calculation method to copy self.chain while holding
self._lock, using an immutable snapshot such as tuple(self.chain), before
releasing the lock; preserve the existing summation over chain_list.

278-287: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Validate incoming block hashes before calculating chain work.

Line 285 can raise ValueError or TypeError for malformed incoming data, and resolve_conflicts does not convert that failure into (False, []). A fabricated hexadecimal hash can also pass this numeric check and inflate new_work; _apply_block rejects it only after the work comparison. Run header validation before get_total_work, or reject malformed hashes in this fast path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@minichain/chain.py` around lines 278 - 287, Update the fast PoW validation in
resolve_conflicts to validate each incoming block hash before get_total_work
calculates new_work. Catch malformed or non-hex hash values and reject them with
(False, []) instead of allowing ValueError or TypeError to escape; ensure
fabricated hashes cannot pass the numeric target comparison and inflate chain
work, while preserving existing invalid-target rejection.

96-109: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Validate the genesis consensus parameters before use.

int(raw_target) silently converts JSON floats and booleans. An invalid hex string raises before the configuration error path logs the problem. target_block_time and alpha are also accepted without constraints. A zero target_block_time causes division by zero at Line 167. Validate the target format, require a positive integer target_block_time, and require alpha to be between 0 and 1.

Also applies to: 160-169

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@minichain/chain.py` around lines 96 - 109, Validate all genesis consensus
parameters before assigning or using them in the chain initialization flow:
accept target only as a valid positive integer or hexadecimal string, reject
booleans, floats, and malformed hex through the existing configuration error
path; require target_block_time to be a positive integer and alpha to be within
0 to 1. Ensure these checks occur before the difficulty adjustment logic that
uses them, including the flow around avg_block_time and target recalculation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@minichain/chain.py`:
- Around line 303-305: Authenticate the incoming genesis block before reading
its target in the fork_idx == 0 path. Update the proposed_chain/new_chain_list
handling so the genesis header is recomputed and matched against the local
genesis snapshot, or replace the incoming genesis with self.chain[0] before
temp_target and subsequent validation use it; do not rely only on the mutable
hash field.
- Around line 350-351: Update the reorganization flow around snapshot validation
and the “Repopulate snapshots for the new chain tip” block to collect snapshots
for the retained portion of proposed_chain while validating it, then replace the
bounded snapshot deque only after the reorganization succeeds. Do not retain
old-branch snapshots or append only the new tip; preserve the deque’s configured
maximum length and ensure fork-point snapshots remain available for later
reorganizations.

---

Outside diff comments:
In `@minichain/chain.py`:
- Around line 152-158: Update the chain_list initialization in the
work-calculation method to copy self.chain while holding self._lock, using an
immutable snapshot such as tuple(self.chain), before releasing the lock;
preserve the existing summation over chain_list.
- Around line 278-287: Update the fast PoW validation in resolve_conflicts to
validate each incoming block hash before get_total_work calculates new_work.
Catch malformed or non-hex hash values and reject them with (False, []) instead
of allowing ValueError or TypeError to escape; ensure fabricated hashes cannot
pass the numeric target comparison and inflate chain work, while preserving
existing invalid-target rejection.
- Around line 96-109: Validate all genesis consensus parameters before assigning
or using them in the chain initialization flow: accept target only as a valid
positive integer or hexadecimal string, reject booleans, floats, and malformed
hex through the existing configuration error path; require target_block_time to
be a positive integer and alpha to be within 0 to 1. Ensure these checks occur
before the difficulty adjustment logic that uses them, including the flow around
avg_block_time and target recalculation.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 99717d9e-abb2-449c-aec5-6d0f1fc47f0c

📥 Commits

Reviewing files that changed from the base of the PR and between 91a0e87 and f1384db.

📒 Files selected for processing (2)
  • minichain/chain.py
  • minichain/node_config.py

Comment thread minichain/chain.py
Comment on lines +303 to 305
fork_base_hash = self.chain[fork_idx - 1].hash if fork_idx > 0 else None

temp_target = proposed_chain[0].target

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Authenticate the incoming genesis block before using its target.

When fork_idx == 0, proposed_chain[0] comes from new_chain_list, but the validation loop starts at block 1. The code checks only the incoming object's mutable hash field. It can therefore use a different genesis target or state root with the local genesis snapshot. Recompute and compare the complete genesis header, or replace the incoming genesis object with self.chain[0] before using it.

Also applies to: 325-330

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@minichain/chain.py` around lines 303 - 305, Authenticate the incoming genesis
block before reading its target in the fork_idx == 0 path. Update the
proposed_chain/new_chain_list handling so the genesis header is recomputed and
matched against the local genesis snapshot, or replace the incoming genesis with
self.chain[0] before temp_target and subsequent validation use it; do not rely
only on the mutable hash field.

Comment thread minichain/chain.py
Comment on lines +350 to +351
# Repopulate snapshots for the new chain tip
self._state_snapshots.append((self.last_block.hash, self.state.snapshot()))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Rebuild the bounded snapshot deque after a reorganization.

This code appends only the new tip and retains snapshots from the old branch. After a deep reorganization, a later shallow reorganization on the new branch cannot find its fork-point snapshot and falls back to replay from genesis. Collect snapshots for the retained portion of proposed_chain while validating it, then replace the deque only after the reorganization succeeds.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@minichain/chain.py` around lines 350 - 351, Update the reorganization flow
around snapshot validation and the “Repopulate snapshots for the new chain tip”
block to collect snapshots for the retained portion of proposed_chain while
validating it, then replace the bounded snapshot deque only after the
reorganization succeeds. Do not retain old-branch snapshots or append only the
new tip; preserve the deque’s configured maximum length and ensure fork-point
snapshots remain available for later reorganizations.

Comment thread minichain/chain.py
snapshot_found = snap
break

if snapshot_found is not None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't if snapshot_found: suffice here?

@Zahnentferner
Zahnentferner merged commit 8fdf590 into main Aug 10, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants