@@ -26,6 +26,10 @@ pub struct ProofCache {
2626 tree : Pubkey ,
2727 base_root : [ u8 ; 32 ] ,
2828 proofs : VecDeque < CachedProof > ,
29+ /// Proofs that completed after the cache warm-up deadline. These are kept
30+ /// separately because a newer warm-up must not discard potentially useful
31+ /// work from an older eligibility window.
32+ late_proofs : VecDeque < CachedProof > ,
2933 warming_proofs : BTreeMap < u64 , CachedProof > ,
3034 is_warming : bool ,
3135 warming_generation : u64 ,
@@ -38,6 +42,7 @@ impl ProofCache {
3842 tree,
3943 base_root : [ 0u8 ; 32 ] ,
4044 proofs : VecDeque :: new ( ) ,
45+ late_proofs : VecDeque :: new ( ) ,
4146 warming_proofs : BTreeMap :: new ( ) ,
4247 is_warming : false ,
4348 warming_generation : 0 ,
@@ -160,62 +165,103 @@ impl ProofCache {
160165 ) ;
161166 }
162167
163- pub fn take_if_valid ( & mut self , current_root : & [ u8 ; 32 ] ) -> Option < Vec < CachedProof > > {
164- if self . proofs . is_empty ( ) || self . is_warming {
165- return None ;
168+ pub fn add_late_proof (
169+ & mut self ,
170+ seq : u64 ,
171+ old_root : [ u8 ; 32 ] ,
172+ new_root : [ u8 ; 32 ] ,
173+ instruction : BatchInstruction ,
174+ ) {
175+ let duplicate = self
176+ . proofs
177+ . iter ( )
178+ . chain ( self . warming_proofs . values ( ) )
179+ . chain ( self . late_proofs . iter ( ) )
180+ . any ( |proof| proof. old_root == old_root && proof. new_root == new_root) ;
181+ if duplicate {
182+ debug ! (
183+ tree = %self . tree,
184+ seq,
185+ "Ignoring duplicate late proof"
186+ ) ;
187+ return ;
166188 }
167189
168- let mut skipped = 0 ;
169- while let Some ( proof) = self . proofs . front ( ) {
170- if proof. old_root == * current_root {
171- break ;
172- }
173- if proof. new_root == * current_root {
174- self . proofs . pop_front ( ) ;
175- skipped += 1 ;
176- continue ;
190+ let items = instruction. items_count ( ) ;
191+ self . late_proofs . push_back ( CachedProof {
192+ seq,
193+ old_root,
194+ new_root,
195+ instruction,
196+ items,
197+ } ) ;
198+
199+ while self . late_proofs . len ( ) > self . max_proofs {
200+ if let Some ( dropped) = self . late_proofs . pop_front ( ) {
201+ warn ! (
202+ tree = %self . tree,
203+ seq = dropped. seq,
204+ max = self . max_proofs,
205+ "Late proof cache limit reached; dropping oldest candidate"
206+ ) ;
177207 }
178- self . proofs . pop_front ( ) ;
179- skipped += 1 ;
180208 }
181209
210+ debug ! (
211+ tree = %self . tree,
212+ seq,
213+ late_proofs_cached = self . late_proofs. len( ) ,
214+ "Retained proof that completed after cache warm-up deadline"
215+ ) ;
216+ }
217+
218+ pub fn take_if_valid ( & mut self , current_root : & [ u8 ; 32 ] ) -> Option < Vec < CachedProof > > {
219+ if self . is_warming || ( self . proofs . is_empty ( ) && self . late_proofs . is_empty ( ) ) {
220+ return None ;
221+ }
222+
223+ // Treat both synchronously warmed and later results as candidates. Proof
224+ // completion order is not guaranteed, so build the usable chain by roots
225+ // instead of sequence number. Unmatched candidates stay cached: a missing
226+ // predecessor may still arrive, or the on-chain root may advance to them.
227+ let mut candidates = VecDeque :: with_capacity ( self . proofs . len ( ) + self . late_proofs . len ( ) ) ;
228+ candidates. append ( & mut self . proofs ) ;
229+ candidates. append ( & mut self . late_proofs ) ;
230+
231+ let before = candidates. len ( ) ;
232+ candidates. retain ( |proof| proof. new_root != * current_root) ;
233+ let skipped = before - candidates. len ( ) ;
234+
182235 if skipped > 0 {
183236 debug ! (
184237 "Skipped {} stale cached proofs for tree {} (on-chain already advanced)" ,
185238 skipped, self . tree
186239 ) ;
187240 }
188241
189- if self . proofs . is_empty ( ) {
190- debug ! (
191- "Cache empty after skipping stale proofs for tree {} (current_root {:?})" ,
192- self . tree,
193- & current_root[ ..4 ]
194- ) ;
195- return None ;
196- }
197-
198242 let mut expected = * current_root;
199243 let mut taken: Vec < CachedProof > = Vec :: new ( ) ;
200244
201- while let Some ( proof) = self . proofs . pop_front ( ) {
202- if proof. old_root != expected {
203- warn ! (
204- "Cache chain broken for tree {} at seq {}: expected root {:?}, got {:?}. Dropping remaining {} proofs." ,
205- self . tree,
206- proof. seq,
207- & expected[ ..4 ] ,
208- & proof. old_root[ ..4 ] ,
209- self . proofs. len( )
210- ) ;
211- self . proofs . clear ( ) ;
212- break ;
213- }
245+ while let Some ( position) = candidates
246+ . iter ( )
247+ . position ( |proof| proof. old_root == expected)
248+ {
249+ let proof = candidates
250+ . remove ( position)
251+ . expect ( "candidate position was found" ) ;
214252 expected = proof. new_root ;
215253 taken. push ( proof) ;
216254 }
217255
256+ self . late_proofs = candidates;
257+
218258 if taken. is_empty ( ) {
259+ debug ! (
260+ tree = %self . tree,
261+ current_root = ?& current_root[ ..4 ] ,
262+ retained_candidates = self . late_proofs. len( ) ,
263+ "No cached proof currently links to the on-chain root"
264+ ) ;
219265 return None ;
220266 }
221267
@@ -235,11 +281,11 @@ impl ProofCache {
235281 }
236282
237283 pub fn len ( & self ) -> usize {
238- self . proofs . len ( )
284+ self . proofs . len ( ) + self . late_proofs . len ( )
239285 }
240286
241287 pub fn is_empty ( & self ) -> bool {
242- self . proofs . is_empty ( )
288+ self . proofs . is_empty ( ) && self . late_proofs . is_empty ( )
243289 }
244290
245291 pub fn is_warming ( & self ) -> bool {
@@ -252,6 +298,7 @@ impl ProofCache {
252298
253299 pub fn clear ( & mut self ) {
254300 self . proofs . clear ( ) ;
301+ self . late_proofs . clear ( ) ;
255302 self . warming_proofs . clear ( ) ;
256303 self . is_warming = false ;
257304 }
@@ -301,6 +348,19 @@ impl SharedProofCache {
301348 self . inner . lock ( ) . await . take_if_valid ( current_root)
302349 }
303350
351+ pub async fn add_late_proof (
352+ & self ,
353+ seq : u64 ,
354+ old_root : [ u8 ; 32 ] ,
355+ new_root : [ u8 ; 32 ] ,
356+ instruction : BatchInstruction ,
357+ ) {
358+ self . inner
359+ . lock ( )
360+ . await
361+ . add_late_proof ( seq, old_root, new_root, instruction) ;
362+ }
363+
304364 pub async fn is_warming ( & self ) -> bool {
305365 self . inner . lock ( ) . await . is_warming ( )
306366 }
@@ -413,4 +473,27 @@ mod tests {
413473 new_warmup. finish ( ) . await ;
414474 assert ! ( !cache. is_warming( ) . await ) ;
415475 }
476+
477+ #[ tokio:: test]
478+ async fn late_proofs_are_linked_by_root_when_they_arrive_out_of_order ( ) {
479+ let cache = Arc :: new ( SharedProofCache :: new ( Pubkey :: new_unique ( ) ) ) ;
480+ let root_1 = [ 1u8 ; 32 ] ;
481+ let root_2 = [ 2u8 ; 32 ] ;
482+ let root_3 = [ 3u8 ; 32 ] ;
483+
484+ cache
485+ . add_late_proof ( 1 , root_2, root_3, BatchInstruction :: Append ( Vec :: new ( ) ) )
486+ . await ;
487+ cache
488+ . add_late_proof ( 0 , root_1, root_2, BatchInstruction :: Append ( Vec :: new ( ) ) )
489+ . await ;
490+
491+ let proofs = cache. take_if_valid ( & root_1) . await . unwrap ( ) ;
492+ assert_eq ! ( proofs. len( ) , 2 ) ;
493+ assert_eq ! ( proofs[ 0 ] . old_root, root_1) ;
494+ assert_eq ! ( proofs[ 0 ] . new_root, root_2) ;
495+ assert_eq ! ( proofs[ 1 ] . old_root, root_2) ;
496+ assert_eq ! ( proofs[ 1 ] . new_root, root_3) ;
497+ assert ! ( cache. is_empty( ) . await ) ;
498+ }
416499}
0 commit comments