Skip to content

Commit 8fdf590

Browse files
Merge pull request #134 from StabilityNexus/feature/state-rollback
feat: Implement StateJournal and in-memory snapshots for state rollback
2 parents 9bace4c + f1384db commit 8fdf590

3 files changed

Lines changed: 119 additions & 14 deletions

File tree

minichain/chain.py

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ def __init__(self, genesis_path="genesis.json"):
5151
self.state = State()
5252
self.chain_id = "minichain-default"
5353
self._lock = threading.RLock()
54+
import collections
55+
from .node_config import MAX_STATE_SNAPSHOTS
56+
self._state_snapshots = collections.deque(maxlen=MAX_STATE_SNAPSHOTS)
5457
self._create_genesis_block(genesis_path)
5558

5659
def _create_genesis_block(self, genesis_path):
@@ -131,6 +134,7 @@ def _create_genesis_block(self, genesis_path):
131134

132135
# Snapshot the state exactly after genesis allocation for clean reorg rebuilds
133136
self._genesis_state_snapshot = self.state.snapshot()
137+
self._state_snapshots.append((genesis_block.hash, self.state.snapshot()))
134138

135139
@property
136140
def last_block(self):
@@ -237,7 +241,9 @@ def add_block(self, block):
237241
self.current_target = new_target
238242
self.avg_block_time = new_avg
239243
self.chain.append(block)
240-
244+
245+
self._state_snapshots.append((block.hash, self.state.snapshot()))
246+
241247
return ValidationStatus.VALID
242248

243249
def resolve_conflicts(self, new_chain_list) -> tuple[bool, list]:
@@ -293,12 +299,35 @@ def resolve_conflicts(self, new_chain_list) -> tuple[bool, list]:
293299

294300
temp_state = State()
295301
temp_state.chain_id = self.chain_id
296-
temp_state.restore(self._genesis_state_snapshot)
302+
303+
fork_base_hash = self.chain[fork_idx - 1].hash if fork_idx > 0 else None
297304

298305
temp_target = proposed_chain[0].target
299306
temp_avg_block_time = self.target_block_time
307+
308+
snapshot_found = None
309+
if fork_base_hash:
310+
for h, snap in self._state_snapshots:
311+
if h == fork_base_hash:
312+
snapshot_found = snap
313+
break
314+
315+
if snapshot_found is not None:
316+
logger.info("Reorg optimization: Restoring state from in-memory snapshot at block %s", fork_idx - 1)
317+
temp_state.restore(snapshot_found)
318+
319+
# Fast forward target and avg_block_time without executing state
320+
for i in range(1, fork_idx):
321+
block_time = proposed_chain[i].timestamp - proposed_chain[i-1].timestamp
322+
temp_avg_block_time = self.alpha * block_time + (1 - self.alpha) * temp_avg_block_time
323+
temp_target = self._next_target(temp_target, temp_avg_block_time)
324+
325+
start_idx = fork_idx
326+
else:
327+
temp_state.restore(self._genesis_state_snapshot)
328+
start_idx = 1
300329

301-
for i in range(1, len(proposed_chain)):
330+
for i in range(start_idx, len(proposed_chain)):
302331
status, temp_target, temp_avg_block_time = self._apply_block(
303332
proposed_chain[i - 1], proposed_chain[i], temp_state, temp_target, temp_avg_block_time
304333
)
@@ -317,6 +346,9 @@ def resolve_conflicts(self, new_chain_list) -> tuple[bool, list]:
317346
self.state = temp_state
318347
self.current_target = temp_target
319348
self.avg_block_time = temp_avg_block_time
320-
349+
350+
# Repopulate snapshots for the new chain tip
351+
self._state_snapshots.append((self.last_block.hash, self.state.snapshot()))
352+
321353
logger.info("Reorg successful! Switched to new chain tip: Block %s", self.last_block.index)
322354
return True, orphans

minichain/node_config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,6 @@
2323
# Keeping the upper limit around 32-bits ensures the nonce string in the JSON block
2424
# doesn't become unnecessarily large, and avoids cross-language serialization issues.
2525
MINING_INITIAL_NONCE_MAX = 2**32 - 1
26+
27+
# State Config
28+
MAX_STATE_SNAPSHOTS = 10 # Number of recent block states to keep in memory for reorg optimization

minichain/state.py

Lines changed: 80 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,66 @@
1010

1111
logger = logging.getLogger(__name__)
1212

13+
class StateJournal:
14+
"""
15+
An in-memory proxy dictionary that caches reads and writes to avoid
16+
expensive deep copies of the entire state dictionary during transactions.
17+
"""
18+
def __init__(self, backing_dict):
19+
self.backing = backing_dict
20+
self.cache = {}
21+
22+
def __getitem__(self, key):
23+
if key not in self.cache:
24+
if key in self.backing:
25+
import copy
26+
self.cache[key] = copy.deepcopy(self.backing[key])
27+
else:
28+
raise KeyError(key)
29+
return self.cache[key]
30+
31+
def __setitem__(self, key, value):
32+
self.cache[key] = value
33+
34+
def __delitem__(self, key):
35+
raise NotImplementedError("Account deletion not supported in StateJournal")
36+
37+
def __contains__(self, key):
38+
return key in self.cache or key in self.backing
39+
40+
def get(self, key, default=None):
41+
try:
42+
return self.__getitem__(key)
43+
except KeyError:
44+
return default
45+
46+
def items(self):
47+
res = self.backing.copy()
48+
res.update(self.cache)
49+
return res.items()
50+
51+
def update(self, other_dict):
52+
if hasattr(other_dict, 'items'):
53+
for k, v in other_dict.items():
54+
self[k] = v
55+
else:
56+
for k, v in other_dict:
57+
self[k] = v
58+
59+
def copy(self):
60+
res = self.backing.copy()
61+
res.update(self.cache)
62+
return res
63+
64+
def commit(self):
65+
"""Flushes cached modifications to the backing dictionary."""
66+
self.backing.update(self.cache)
67+
self.cache.clear()
68+
69+
def rollback(self):
70+
"""Discards modifications."""
71+
self.cache.clear()
72+
1373

1474
class State:
1575
def __init__(self):
@@ -69,9 +129,11 @@ def verify_transaction_logic(self, tx):
69129
def copy(self):
70130
"""
71131
Return an independent copy of state for transactional validation.
132+
Uses StateJournal for O(1) cloning instead of deepcopy.
72133
"""
73-
new_state = copy.deepcopy(self)
74-
new_state.contract_machine = ContractMachine(new_state) # Reinitialize contract_machine
134+
new_state = State()
135+
new_state.accounts = StateJournal(self.accounts)
136+
new_state.contract_machine = ContractMachine(new_state)
75137
new_state.chain_id = self.chain_id
76138
return new_state
77139

@@ -124,22 +186,22 @@ def apply_transaction(self, tx):
124186

125187

126188
def _apply_validated_tx(self, tx):
189+
original_accounts = self.accounts
190+
journal = StateJournal(original_accounts)
191+
self.accounts = journal
192+
127193
sender = self.accounts[tx.sender]
128194
total_cost = tx.amount + (getattr(tx, 'gas_limit', 0) * getattr(tx, 'fee_per_gas', 0))
129195

130196
sender['balance'] -= total_cost
131197
sender['nonce'] += 1
132198

133-
import copy
134-
state_snapshot = copy.deepcopy(self.accounts)
135-
136199
def rollback_and_refund(error_message, gas_used):
137-
self.accounts = copy.deepcopy(state_snapshot)
200+
journal.rollback()
201+
self.accounts = original_accounts
138202
refund_acc = self.accounts[tx.sender]
139-
refund_acc['balance'] += tx.amount
140-
gas_refund = getattr(tx, 'gas_limit', 0) - gas_used
141-
if gas_refund > 0:
142-
refund_acc['balance'] += (gas_refund * getattr(tx, 'fee_per_gas', 0))
203+
refund_acc['balance'] -= (gas_used * getattr(tx, 'fee_per_gas', 0))
204+
refund_acc['nonce'] += 1
143205
return Receipt(tx.tx_id, status=0, error_message=error_message, gas_used=gas_used)
144206

145207
# LOGIC BRANCH 1: Contract Deployment
@@ -162,6 +224,9 @@ def rollback_and_refund(error_message, gas_used):
162224
gas_refund = gas_used - code_gas
163225
if gas_refund > 0:
164226
self.accounts[tx.sender]['balance'] += (gas_refund * getattr(tx, 'fee_per_gas', 0))
227+
228+
journal.commit()
229+
self.accounts = original_accounts
165230
return Receipt(tx.tx_id, status=1, contract_address=contract_address, gas_used=code_gas)
166231

167232
# LOGIC BRANCH 2: Contract Call
@@ -187,12 +252,17 @@ def rollback_and_refund(error_message, gas_used):
187252
if gas_refund > 0:
188253
self.accounts[tx.sender]['balance'] += (gas_refund * getattr(tx, 'fee_per_gas', 0))
189254

255+
journal.commit()
256+
self.accounts = original_accounts
190257
return Receipt(tx.tx_id, status=1, gas_used=gas_used)
191258

192259
# LOGIC BRANCH 3: Regular Transfer
193260
receiver = self.get_account(tx.receiver)
194261
receiver['balance'] += tx.amount
195262
gas_used = getattr(tx, 'gas_limit', 0)
263+
264+
journal.commit()
265+
self.accounts = original_accounts
196266
return Receipt(tx.tx_id, status=1, gas_used=gas_used)
197267

198268
def execute_internal_call(self, sender, receiver_address, amount, payload, gas_limit, depth, is_top_level=False):

0 commit comments

Comments
 (0)