Skip to content

Commit 5439643

Browse files
arturgontijomanuelmauro
authored andcommitted
Improve "latest" block resolution on pruned nodes (polkadot-evm#1856)
* Improve "latest" block resolution on pruned nodes * Use config.state_pruning as state_pruning_blocks in node template * Coderabbit: better logs + use finalized block * Retain in-window tips when skipping pruned blocks * Skip target lookup failure explicitly * Backfill BLOCK_NUMBER_MAPPING for the full live window * Add max_blocks to backfill_number_mappings() * Add pruning_skip_retains_in_window_tips() test * Use find_post_log() + test-pruning-skip.ts * Make latest_block_hash() read-only + rpc fast path * Validate eth hash from digest before writing BLOCK_MAPPING * Repair missing TRANSACTION_MAPPING when state becomes readable * Add integration test for block and tx consistency after a reorg * Check all txns when deciding tx mapping repair * Restore a background cursor-based sweep * Remove unnecessary comment/report * coderabbit review * coderabbit: propagate client.hash() failures * coderabbit: no more ok().flatten() * Remove unnecesasry reconcile #0..#0
1 parent 8ff2482 commit 5439643

12 files changed

Lines changed: 1385 additions & 71 deletions

File tree

client/db/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ fp-storage = { workspace = true, features = ["default"] }
4141
futures = { workspace = true }
4242
maplit = "1.0.2"
4343
tempfile = "3.21.0"
44+
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
4445
# Substrate
4546
sc-block-builder = { workspace = true }
4647
sp-consensus = { workspace = true }

client/db/src/kv/mod.rs

Lines changed: 68 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -138,12 +138,16 @@ impl<Block: BlockT, C: HeaderBackend<Block>> fc_api::Backend<Block> for Backend<
138138
// where eth_getBlockByNumber("latest") returns block 0 during initial sync.
139139
// Users can check sync status via eth_syncing to determine if the node is
140140
// still catching up.
141+
//
142+
// IMPORTANT: This function is intentionally read-only. The persisted pointer
143+
// (LATEST_CANONICAL_INDEXED_BLOCK) is maintained exclusively by the reconciler
144+
// in mapping-sync via advance_latest_pointer(). Writing the pointer here from
145+
// a reader caused a race condition where every failed fast path would lower the
146+
// pointer, racing against the reconciler trying to advance it.
141147
let best_number: u64 = self.client.info().best_number.unique_saturated_into();
142148

143149
// Fast path: if best is already indexed and canonical, use it directly.
144150
if let Some(canonical_hash) = self.indexed_canonical_hash_at(best_number)? {
145-
self.mapping
146-
.set_latest_canonical_indexed_block(best_number)?;
147151
return Ok(canonical_hash);
148152
}
149153

@@ -153,11 +157,9 @@ impl<Block: BlockT, C: HeaderBackend<Block>> fc_api::Backend<Block> for Backend<
153157
let bounded_start = best_number.saturating_sub(1);
154158

155159
// Layer 1 — bounded scan: [best-1 .. best-8k]
156-
if let Some((found_number, found_hash)) =
160+
if let Some((_found_number, found_hash)) =
157161
self.find_latest_indexed_canonical_block(bounded_start, INDEXED_RECOVERY_SCAN_LIMIT)?
158162
{
159-
self.mapping
160-
.set_latest_canonical_indexed_block(found_number)?;
161163
return Ok(found_hash);
162164
}
163165

@@ -183,11 +185,9 @@ impl<Block: BlockT, C: HeaderBackend<Block>> fc_api::Backend<Block> for Backend<
183185
// 8k bounded + 24k exhaustive = 32k total non-overlapping coverage.
184186
let exhaustive_start = bounded_start.saturating_sub(INDEXED_RECOVERY_SCAN_LIMIT);
185187
let exhaustive_limit = INDEXED_RECOVERY_SCAN_LIMIT * 3;
186-
if let Some((found_number, found_hash)) =
188+
if let Some((_found_number, found_hash)) =
187189
self.find_latest_indexed_canonical_block(exhaustive_start, exhaustive_limit)?
188190
{
189-
self.mapping
190-
.set_latest_canonical_indexed_block(found_number)?;
191191
return Ok(found_hash);
192192
}
193193
}
@@ -199,11 +199,9 @@ impl<Block: BlockT, C: HeaderBackend<Block>> fc_api::Backend<Block> for Backend<
199199
let deep_start = bounded_start
200200
.saturating_sub(INDEXED_RECOVERY_SCAN_LIMIT)
201201
.saturating_sub(INDEXED_RECOVERY_SCAN_LIMIT * 3);
202-
if let Some((found_number, found_hash)) = self
202+
if let Some((_found_number, found_hash)) = self
203203
.find_latest_indexed_canonical_block(deep_start, INDEXED_DEEP_RECOVERY_SCAN_LIMIT)?
204204
{
205-
self.mapping
206-
.set_latest_canonical_indexed_block(found_number)?;
207205
return Ok(found_hash);
208206
}
209207
}
@@ -212,8 +210,10 @@ impl<Block: BlockT, C: HeaderBackend<Block>> fc_api::Backend<Block> for Backend<
212210
// Checked after deep recovery so we never return an older block when a newer one
213211
// exists in the deep window.
214212
//
215-
// When the pointer target is stale (e.g. reorg), walk backward from it to
216-
// find the latest valid indexed canonical block instead of falling to genesis.
213+
// The pointer is maintained exclusively by the reconciler (advance_latest_pointer),
214+
// so it is always monotonically increasing and safe to trust here as a fallback.
215+
// When the pointer target is stale (e.g. reorg not yet reconciled), walk backward
216+
// from it to find the latest valid indexed canonical block.
217217
if let Some(persisted_number) = self.mapping.latest_canonical_indexed_block_number()? {
218218
if persisted_number <= best_number {
219219
if let Some(canonical_hash) = self.indexed_canonical_hash_at(persisted_number)? {
@@ -222,13 +222,11 @@ impl<Block: BlockT, C: HeaderBackend<Block>> fc_api::Backend<Block> for Backend<
222222
// Pointer target is stale; backtrack from pointer-1 to find a valid block.
223223
if persisted_number > 0 {
224224
let backtrack_start = persisted_number.saturating_sub(1);
225-
if let Some((found_number, found_hash)) = self
225+
if let Some((_found_number, found_hash)) = self
226226
.find_latest_indexed_canonical_block(
227227
backtrack_start,
228228
INDEXED_RECOVERY_SCAN_LIMIT,
229229
)? {
230-
self.mapping
231-
.set_latest_canonical_indexed_block(found_number)?;
232230
return Ok(found_hash);
233231
}
234232
}
@@ -868,6 +866,34 @@ mod tests {
868866
);
869867
}
870868

869+
/// latest_block_hash() is read-only: it must never write the pointer. Otherwise RPC
870+
/// calls would lower the pointer when the fast path fails and a scan finds an older
871+
/// block, racing the reconciler and causing "latest" to stick.
872+
#[tokio::test]
873+
async fn latest_block_hash_never_lowers_pointer() {
874+
let env = TestEnv::new(5).await;
875+
for n in 1u64..=3 {
876+
env.index_block(n);
877+
}
878+
// Pointer at 5 (e.g. from a previous reconciler tick); blocks 4 and 5 are not indexed.
879+
env.set_pointer(5);
880+
881+
let _ = env.latest().await;
882+
// Call again to simulate multiple RPC requests between reconciler ticks.
883+
let _ = env.latest().await;
884+
885+
let pointer_after = env
886+
.backend
887+
.mapping()
888+
.latest_canonical_indexed_block_number()
889+
.expect("read pointer")
890+
.expect("pointer set");
891+
assert_eq!(
892+
pointer_after, 5,
893+
"reader must not write the pointer; it must remain 5 and never be lowered to 3"
894+
);
895+
}
896+
871897
#[tokio::test]
872898
async fn exhaustive_scan_finds_indexed_block_beyond_bounded_range() {
873899
// With the test scan limit (8), best=20 yields:
@@ -981,48 +1007,59 @@ mod tests {
9811007
}
9821008

9831009
#[tokio::test]
984-
async fn pointer_updates_after_stale_pointer_backtrack_recovery() {
985-
// After backtrack from a stale pointer, the persisted pointer should be
986-
// updated to the block we found.
1010+
async fn pointer_unchanged_after_stale_pointer_backtrack_recovery() {
1011+
// latest_block_hash() is read-only: even when backtracking from a stale
1012+
// pointer, it must not modify the persisted pointer. The reconciler is
1013+
// the sole writer.
9871014
let env = TestEnv::new(40).await;
9881015
env.index_block(2);
9891016
env.write_stale_mapping(3);
9901017
env.set_pointer(3);
9911018

992-
let _ = env.latest().await;
1019+
let result = env.latest().await;
1020+
assert_eq!(
1021+
result, env.substrate_hashes[2],
1022+
"backtrack must still find block 2"
1023+
);
9931024

994-
let updated = env
1025+
let pointer = env
9951026
.backend
9961027
.mapping()
9971028
.latest_canonical_indexed_block_number()
9981029
.expect("read pointer");
9991030
assert_eq!(
1000-
updated,
1001-
Some(2),
1002-
"pointer should be updated to block 2 found by backtrack"
1031+
pointer,
1032+
Some(3),
1033+
"read-only: pointer must stay at 3, not be lowered to 2"
10031034
);
10041035
}
10051036

10061037
#[tokio::test]
1007-
async fn pointer_updates_after_bounded_scan_recovery() {
1038+
async fn pointer_unchanged_after_bounded_scan_recovery() {
1039+
// latest_block_hash() is read-only: even when the bounded scan finds a
1040+
// higher indexed block, the pointer must not be updated. The reconciler
1041+
// is the sole writer.
10081042
let env = TestEnv::new(10).await;
10091043
for n in 1u64..=6 {
10101044
env.index_block(n);
10111045
}
10121046
env.set_pointer(3);
10131047

1014-
let _ = env.latest().await;
1048+
let result = env.latest().await;
1049+
assert_eq!(
1050+
result, env.substrate_hashes[6],
1051+
"bounded scan must find block 6"
1052+
);
10151053

1016-
// After the call, the pointer should have been updated to 6.
1017-
let updated = env
1054+
let pointer = env
10181055
.backend
10191056
.mapping()
10201057
.latest_canonical_indexed_block_number()
10211058
.expect("read pointer");
10221059
assert_eq!(
1023-
updated,
1024-
Some(6),
1025-
"pointer should be updated to the block found by bounded scan"
1060+
pointer,
1061+
Some(3),
1062+
"read-only: pointer must stay at 3, not be advanced to 6"
10261063
);
10271064
}
10281065
}

client/mapping-sync/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ repository = { workspace = true }
1111
targets = ["x86_64-unknown-linux-gnu"]
1212

1313
[dependencies]
14+
ethereum-types = { workspace = true }
1415
futures = { workspace = true }
1516
futures-timer = "3.0.3"
1617
log = { workspace = true }
@@ -32,7 +33,6 @@ fp-rpc = { workspace = true, features = ["default"] }
3233

3334
[dev-dependencies]
3435
ethereum = { workspace = true }
35-
ethereum-types = { workspace = true }
3636
scale-codec = { workspace = true }
3737
sqlx = { workspace = true, features = ["runtime-tokio-native-tls", "sqlite"] }
3838
tempfile = "3.21.0"

0 commit comments

Comments
 (0)