@@ -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}
0 commit comments