Skip to content

Commit 4e3387a

Browse files
feat: Implement StateJournal and in-memory snapshots for state rollback
1 parent 20d148e commit 4e3387a

2 files changed

Lines changed: 120 additions & 13 deletions

File tree

minichain/chain.py

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ def __init__(self, genesis_path="genesis.json"):
4949
self.state = State()
5050
self.chain_id = "minichain-default"
5151
self._lock = threading.RLock()
52+
import collections
53+
self._state_snapshots = collections.OrderedDict()
54+
self._max_snapshots = 10
5255
self._create_genesis_block(genesis_path)
5356

5457
def _create_genesis_block(self, genesis_path):
@@ -121,6 +124,7 @@ def _create_genesis_block(self, genesis_path):
121124

122125
# Snapshot the state exactly after genesis allocation for clean reorg rebuilds
123126
self._genesis_state_snapshot = self.state.snapshot()
127+
self._state_snapshots[genesis_block.hash] = self.state.snapshot()
124128

125129
@property
126130
def last_block(self):
@@ -214,10 +218,18 @@ def add_block(self, block):
214218
return status
215219

216220
# All transactions valid → commit state and append block
221+
if hasattr(temp_state.accounts, 'commit'):
222+
temp_state.accounts.commit()
223+
temp_state.accounts = temp_state.accounts.backing
217224
self.state = temp_state
218225
self.current_difficulty = new_difficulty
219226
self.avg_block_time = new_avg
220227
self.chain.append(block)
228+
229+
self._state_snapshots[block.hash] = self.state.snapshot()
230+
while len(self._state_snapshots) > self._max_snapshots:
231+
self._state_snapshots.popitem(last=False)
232+
221233
return ValidationStatus.VALID
222234

223235
def resolve_conflicts(self, new_chain_list) -> tuple[bool, list]:
@@ -262,15 +274,34 @@ def resolve_conflicts(self, new_chain_list) -> tuple[bool, list]:
262274

263275
temp_state = State()
264276
temp_state.chain_id = self.chain_id
265-
temp_state.restore(self._genesis_state_snapshot)
266-
277+
278+
fork_base_hash = self.chain[fork_idx - 1].hash if fork_idx > 0 else None
279+
267280
temp_difficulty = proposed_chain[0].difficulty
268281
temp_avg_block_time = self.target_block_time
282+
283+
if fork_base_hash and fork_base_hash in self._state_snapshots:
284+
logger.info("Reorg optimization: Restoring state from in-memory snapshot at block %s", fork_idx - 1)
285+
temp_state.restore(self._state_snapshots[fork_base_hash])
286+
287+
# Fast forward difficulty and avg_block_time without executing state
288+
for i in range(1, fork_idx):
289+
block_time = proposed_chain[i].timestamp - proposed_chain[i-1].timestamp
290+
temp_avg_block_time = self.alpha * block_time + (1 - self.alpha) * temp_avg_block_time
291+
temp_difficulty = self._next_difficulty(temp_difficulty, temp_avg_block_time)
292+
293+
start_idx = fork_idx
294+
else:
295+
temp_state.restore(self._genesis_state_snapshot)
296+
start_idx = 1
269297

270-
for i in range(1, len(proposed_chain)):
298+
for i in range(start_idx, len(proposed_chain)):
271299
status, temp_difficulty, temp_avg_block_time = self._apply_block(
272300
proposed_chain[i - 1], proposed_chain[i], temp_state, temp_difficulty, temp_avg_block_time
273301
)
302+
if hasattr(temp_state.accounts, 'commit'):
303+
temp_state.accounts.commit()
304+
temp_state.accounts = temp_state.accounts.backing
274305
if status != ValidationStatus.VALID:
275306
logger.warning("Reorg failed at block %s", proposed_chain[i].index)
276307
return False, []
@@ -283,5 +314,11 @@ def resolve_conflicts(self, new_chain_list) -> tuple[bool, list]:
283314
self.state = temp_state
284315
self.current_difficulty = temp_difficulty
285316
self.avg_block_time = temp_avg_block_time
317+
318+
# Repopulate snapshots for the new chain tip
319+
self._state_snapshots[self.last_block.hash] = self.state.snapshot()
320+
while len(self._state_snapshots) > self._max_snapshots:
321+
self._state_snapshots.popitem(last=False)
322+
286323
logger.info("Reorg successful! Switched to new chain tip: Block %s", self.last_block.index)
287324
return True, orphans

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)