Skip to content

Commit 3f6b570

Browse files
authored
Fix stateroot mismatch caused by stale db cache when using checkpoints (#3527)
* Fix state root mismatch. * Add tests which reproduce the issue. * Fix storage slots test. * Update comments.
1 parent 9ccffee commit 3f6b570

3 files changed

Lines changed: 204 additions & 0 deletions

File tree

execution_chain/db/aristo/aristo_tx_frame.nim

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ proc buildSnapshot(txFrame: AristoTxRef, minLevel: int) =
3737
# `frame` has a snapshot only in the first iteration of the for loop
3838
txFrame.snapshot = move(frame.snapshot)
3939

40+
# Copy cached values that are not present in the newer txFrame.
41+
# These are needed to update the main caches in the AristoDbRef instance.
42+
for k, v in frame.accLeaves:
43+
discard txFrame.accLeaves.hasKeyOrPut(k, v)
44+
for k, v in frame.stoLeaves:
45+
discard txFrame.stoLeaves.hasKeyOrPut(k, v)
46+
4047
# Verify that https://github.com/nim-lang/Nim/issues/23759 is not present
4148
assert frame.snapshot.vtx.len == 0 and frame.snapshot.level.isNone()
4249

tests/all_tests.nim

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import
2525
test_jwt_auth,
2626
test_kvt,
2727
test_ledger,
28+
test_stateroot_mismatch,
2829
test_op_arith,
2930
test_op_bit,
3031
test_op_custom,

tests/test_stateroot_mismatch.nim

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
# Nimbus
2+
# Copyright (c) 2025 Status Research & Development GmbH
3+
# Licensed under either of
4+
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
5+
# http://www.apache.org/licenses/LICENSE-2.0)
6+
# * MIT license ([LICENSE-MIT](LICENSE-MIT) or
7+
# http://opensource.org/licenses/MIT)
8+
# at your option. This file may not be copied, modified, or
9+
# distributed except according to those terms.
10+
11+
{.used.}
12+
13+
import
14+
stew/byteutils,
15+
unittest2,
16+
../execution_chain/db/ledger,
17+
../execution_chain/common/common
18+
19+
suite "Stateroot Mismatch Checks":
20+
21+
# The purpose of this test is to reproduce an issue where a state root mismatch
22+
# was encountered when using multiple snapshots before committing to the database.
23+
# See here for the full context of the problem: https://github.com/status-im/nimbus-eth1/issues/3381#issuecomment-3131162957
24+
25+
# Problem was related to having stale account and storage slot caches in the main
26+
# database because the required values were not being updated in the call to persist.
27+
# This was because when we persist changes to the database we stop iterating over the txFrames
28+
# once we hit a snapshot but the account and storage caches were not copying over any needed
29+
# cache values when moving a snapshot.
30+
# See the fix here: https://github.com/status-im/nimbus-eth1/pull/3527
31+
32+
# A number of steps are required to reproduce the problem which is what these
33+
# tests cover. These steps are as follows:
34+
# 1. Create a txFrame, create the first checkpoint and persist some state to the database.
35+
# 2. Create a new txFrame and then read a value that doesn't exist and then set the value
36+
# and then create the second checkpoint (the state read and write here triggers the issue
37+
# because the write will be lost but the read causes a nil value to be set in the db
38+
# cache imediately).
39+
# 3. Create a new txFrame and then set some more state then create the third checkpoint
40+
# and then persist the changes to the database (we just do this step so that
41+
# we have multiple checkpoints created before the call to persist which is required
42+
# to reproduce the issue).
43+
# 4. Create a new txFrame and check that the state (account/slot) which was read
44+
# in step 2 is as expected before and after persisting to the database.
45+
46+
setup:
47+
let
48+
memDB = DefaultDbMemory.newCoreDbRef()
49+
addr1 = address"0x0f572e5295c57f15886f9b263e2f6d2d6c7b5ec6"
50+
addr2 = address"0x1f572e5295c57f15886f9b263e2f6d2d6c7b5ec7"
51+
addr3 = address"0x2f572e5295c57f15886f9b263e2f6d2d6c7b5ec8"
52+
code = hexToSeqByte("0x010203")
53+
slot1 = 1.u256
54+
slot2 = 2.u256
55+
slot3 = 3.u256
56+
57+
test "Stateroot check - Accounts":
58+
59+
# Persist first update to database - changes from a single checkpoint
60+
let txFrame0 = memDB.baseTxFrame().txFrameBegin()
61+
block:
62+
let ac0 = LedgerRef.init(txFrame0, false)
63+
ac0.setBalance(addr3, 300.u256)
64+
ac0.persist(clearCache = true)
65+
txFrame0.checkpoint(1.BlockNumber, skipSnapshot = false)
66+
memDB.persist(txFrame0, Opt.none(Hash32))
67+
68+
69+
# Persist second update to database - changes from two checkpoints
70+
let txFrame1 = memDB.baseTxFrame().txFrameBegin()
71+
block:
72+
let
73+
ac1 = LedgerRef.init(txFrame1, false)
74+
preStateRoot = ac1.getStateRoot()
75+
76+
ac1.addBalance(addr1, 50.u256)
77+
ac1.persist(clearCache = true)
78+
txFrame1.checkpoint(2.BlockNumber, skipSnapshot = false)
79+
80+
let postStateRoot = ac1.getStateRoot()
81+
check preStateRoot != postStateRoot
82+
83+
let txFrame2 = txFrame1.txFrameBegin()
84+
block:
85+
let
86+
ac2 = LedgerRef.init(txFrame2, false)
87+
preStateRoot = ac2.getStateRoot()
88+
89+
ac2.addBalance(addr2, 200.u256)
90+
ac2.persist(clearCache = true)
91+
txFrame2.checkpoint(3.BlockNumber, skipSnapshot = false)
92+
memDB.persist(txFrame2, Opt.none(Hash32))
93+
94+
let postStateRoot = ac2.getStateRoot()
95+
check preStateRoot != postStateRoot
96+
97+
98+
# Persist third update to database - changes from a single checkpoint
99+
let txFrame3 = memDB.baseTxFrame().txFrameBegin()
100+
block:
101+
let
102+
ac3 = LedgerRef.init(txFrame3, false)
103+
preStateRoot = ac3.getStateRoot()
104+
105+
ac3.addBalance(addr1, 50.u256)
106+
ac3.persist(clearCache = true)
107+
108+
check:
109+
# Check balances
110+
ac3.getBalance(addr1) == 100.u256
111+
ac3.getBalance(addr2) == 200.u256
112+
ac3.getBalance(addr3) == 300.u256
113+
114+
txFrame3.checkpoint(4.BlockNumber, skipSnapshot = false)
115+
memDB.persist(txFrame3, Opt.none(Hash32))
116+
117+
let postStateRoot = ac3.getStateRoot()
118+
check:
119+
preStateRoot != postStateRoot
120+
121+
# Check balances
122+
ac3.getBalance(addr1) == 100.u256
123+
ac3.getBalance(addr2) == 200.u256
124+
ac3.getBalance(addr3) == 300.u256
125+
126+
test "Stateroot check - Storage Slots":
127+
128+
# Persist first update to database - changes from a single checkpoint
129+
let txFrame0 = memDB.baseTxFrame().txFrameBegin()
130+
block:
131+
let ac0 = LedgerRef.init(txFrame0, false)
132+
ac0.setBalance(addr1, 0.u256)
133+
ac0.setCode(addr1, code)
134+
ac0.setStorage(addr1, slot3, 300.u256)
135+
ac0.persist(clearCache = true)
136+
txFrame0.checkpoint(1.BlockNumber, skipSnapshot = false)
137+
memDB.persist(txFrame0, Opt.none(Hash32))
138+
139+
140+
# Persist second update to database - changes from two checkpoints
141+
let txFrame1 = memDB.baseTxFrame().txFrameBegin()
142+
block:
143+
let
144+
ac1 = LedgerRef.init(txFrame1, false)
145+
preStateRoot = ac1.getStateRoot()
146+
147+
discard ac1.getStorage(addr1, slot1)
148+
ac1.setStorage(addr1, slot1, 50.u256)
149+
ac1.persist(clearCache = true)
150+
txFrame1.checkpoint(2.BlockNumber, skipSnapshot = false)
151+
152+
let postStateRoot = ac1.getStateRoot()
153+
check preStateRoot != postStateRoot
154+
155+
let txFrame2 = txFrame1.txFrameBegin()
156+
block:
157+
let
158+
ac2 = LedgerRef.init(txFrame2, false)
159+
preStateRoot = ac2.getStateRoot()
160+
161+
ac2.setStorage(addr1, slot2, 200.u256)
162+
ac2.persist(clearCache = true)
163+
txFrame2.checkpoint(3.BlockNumber, skipSnapshot = false)
164+
memDB.persist(txFrame2, Opt.none(Hash32))
165+
166+
let postStateRoot = ac2.getStateRoot()
167+
check preStateRoot != postStateRoot
168+
169+
170+
# Persist third update to database - changes from a single checkpoint
171+
let txFrame3 = memDB.baseTxFrame().txFrameBegin()
172+
block:
173+
let
174+
ac3 = LedgerRef.init(txFrame3, false)
175+
preStateRoot = ac3.getStateRoot()
176+
177+
let slotValue = ac3.getStorage(addr1, slot1)
178+
ac3.setStorage(addr1, slot1, slotValue + 50.u256)
179+
ac3.persist(clearCache = true)
180+
181+
check:
182+
# Check slots
183+
ac3.getStorage(addr1, slot1) == 100.u256
184+
ac3.getStorage(addr1, slot2) == 200.u256
185+
ac3.getStorage(addr1, slot3) == 300.u256
186+
187+
txFrame3.checkpoint(4.BlockNumber, skipSnapshot = false)
188+
memDB.persist(txFrame3, Opt.none(Hash32))
189+
190+
let postStateRoot = ac3.getStateRoot()
191+
check:
192+
preStateRoot != postStateRoot
193+
# Check slots
194+
ac3.getStorage(addr1, slot1) == 100.u256
195+
ac3.getStorage(addr1, slot2) == 200.u256
196+
ac3.getStorage(addr1, slot3) == 300.u256

0 commit comments

Comments
 (0)