1- use anyhow:: { Result , anyhow} ;
1+ use anyhow:: { Result , anyhow, ensure } ;
22use ethexe_common:: {
33 Announce , HashOf , SimpleBlockData ,
44 db:: { AnnounceStorageRW , BlockMetaStorageRW , LatestDataStorageRO , OnChainStorageRO } ,
@@ -46,7 +46,7 @@ impl<DB: AnnounceStorageRW + BlockMetaStorageRW + OnChainStorageRO + LatestDataS
4646 }
4747
4848 fn include_announce ( & self , announce : Announce ) -> Result < HashOf < Announce > > {
49- tracing:: trace!( announce = %announce. to_hash( ) , "Including announce" ) ;
49+ tracing:: trace!( announce = %announce. to_hash( ) , "Including announce... " ) ;
5050
5151 let block_hash = announce. block_hash ;
5252 let announce_hash = self . set_announce ( announce) ;
@@ -114,7 +114,9 @@ pub fn propagate_announces(
114114
115115 announces_chain_recovery_if_needed (
116116 db,
117+ & block,
117118 last_committed_announce_hash,
119+ commitment_delay_limit,
118120 & mut missing_announces,
119121 ) ?;
120122
@@ -136,31 +138,118 @@ pub fn propagate_announces(
136138 commitment_delay_limit,
137139 ) ?;
138140 }
141+
142+ debug_assert ! (
143+ db. block_meta( block. hash)
144+ . announces
145+ . into_iter( )
146+ . flatten( )
147+ . next( )
148+ . is_some( ) ,
149+ "at least one announce must be propagated for block({})" ,
150+ block. hash
151+ ) ;
139152 }
140153
141154 Ok ( ( ) )
142155}
143156
157+ /// Recover announces chain if it was committed but not included yet by this node.
158+ /// For example node has following chain:
159+ /// ```text
160+ /// [B1] <-- [B2] <-- [B3] <-- [B4] <-- [B5] (blocks)
161+ /// | | | |
162+ /// (A1) <-- (A2) <-- (A3) <-- (A4) (announces)
163+ /// ```
164+ /// Then node checks events that unknown announce `(A3')` was committed at block `B5`.
165+ /// Then node have to recover the chain of announces to include `(A3')` and its predecessors:
166+ /// ```text
167+ /// [B1] <-- [B2] <-- [B3] <-- [B4] <-- [B5] (blocks)
168+ /// | | | |
169+ /// (A1) <-- (A2) <-- (A3) <-- (A4) (announces)
170+ /// \
171+ /// ---- (A2') <- (A3') <- (A4') (recovered announces)
172+ /// ```
173+ /// where `(A3')` and `(A2')` are committed
174+ /// and must be presented in `missing_announces` if they are not included yet,
175+ /// and `(A4')` is base announce propagated from `(A3')`.
144176fn announces_chain_recovery_if_needed (
145177 db : & impl DBExt ,
178+ block : & SimpleBlockData ,
146179 last_committed_announce_hash : HashOf < Announce > ,
180+ commitment_delay_limit : u32 ,
147181 missing_announces : & mut HashMap < HashOf < Announce > , Announce > ,
148182) -> Result < ( ) > {
149- let mut announce_hash = last_committed_announce_hash;
150- while !db. announce_is_included ( announce_hash) {
151- tracing:: debug!( announce = %announce_hash, "Committed announces was not included yet, including..." ) ;
152-
153- let announce = missing_announces. remove ( & announce_hash) . ok_or_else ( || {
183+ // TODO +_+_+: append recovery from rejected announces
184+ // if node received announce, which was rejected because of incorrect parent,
185+ // but later we receive event from ethereum that parent announce was committed,
186+ // than node should use previously rejected announce to recover the chain.
187+
188+ // Recover backwards the chain of committed announces till last included one
189+ // According to T1, this chain must not be longer than commitment_delay_limit
190+ let mut last_committed_announce_block_hash = None ;
191+ let mut current_announce_hash = last_committed_announce_hash;
192+ let mut count = 0 ;
193+ while count < commitment_delay_limit && !db. announce_is_included ( current_announce_hash) {
194+ tracing:: debug!( announce = %current_announce_hash, "Committed announces was not included yet, try to recover..." ) ;
195+
196+ let announce = missing_announces. remove ( & current_announce_hash) . ok_or_else ( || {
154197 anyhow ! (
155- "Committed announce {announce_hash } is missing, but not found in missing announces"
198+ "Committed announce {current_announce_hash } is missing, but not found in missing announces"
156199 )
157200 } ) ?;
158201
159- announce_hash = announce. parent ;
202+ last_committed_announce_block_hash. get_or_insert ( announce. block_hash ) ;
203+
204+ current_announce_hash = announce. parent ;
205+ count += 1 ;
160206
161207 db. include_announce ( announce) ?;
162208 }
163209
210+ let Some ( last_committed_announce_block_hash) = last_committed_announce_block_hash else {
211+ // No committed announces were missing, no need to recover
212+ return Ok ( ( ) ) ;
213+ } ;
214+
215+ // If error: DB is corrupted, or incorrect commitment detected (have not base announce committed after commitment delay limit)
216+ ensure ! (
217+ db. announce_is_included( current_announce_hash) ,
218+ "{current_announce_hash} is not included after checking {commitment_delay_limit} announces" ,
219+ ) ;
220+
221+ // Recover forward the chain filling with base announces
222+
223+ // First collect a chain of blocks from `last_committed_announce_block_hash` to `block` (exclusive)
224+ // According to T1, this chain must not be longer than commitment_delay_limit
225+ let mut current_block_hash = block. header . parent_hash ;
226+ let mut chain = VecDeque :: new ( ) ;
227+ let mut count = 0 ;
228+ while count < commitment_delay_limit && current_block_hash != last_committed_announce_block_hash
229+ {
230+ chain. push_front ( current_block_hash) ;
231+ current_block_hash = db
232+ . block_header ( current_block_hash)
233+ . ok_or_else ( || anyhow ! ( "header not found for block({current_block_hash})" ) ) ?
234+ . parent_hash ;
235+ count += 1 ;
236+ }
237+
238+ // If error: DB is corrupted, or incorrect commitment detected (have not base announce committed after commitment delay limit)
239+ ensure ! (
240+ current_block_hash == last_committed_announce_block_hash,
241+ "last committed announce block {last_committed_announce_block_hash} not found \
242+ in parent chain of block {} within {commitment_delay_limit} blocks",
243+ block. hash
244+ ) ;
245+
246+ // Now propagate base announces along the chain
247+ let mut parent_announce_hash = last_committed_announce_hash;
248+ for block_hash in chain {
249+ let new_base_announce = Announce :: base ( block_hash, parent_announce_hash) ;
250+ parent_announce_hash = db. include_announce ( new_base_announce) ?;
251+ }
252+
164253 Ok ( ( ) )
165254}
166255
@@ -224,6 +313,12 @@ fn propagate_one_base_announce(
224313 {
225314 // We found last committed announce in the neighbor branch, until commitment delay limit
226315 // that means this branch is already expired.
316+ tracing:: trace!(
317+ predecessor = %predecessor,
318+ parent_announce = %parent_announce_hash,
319+ latest_committed_announce = %last_committed_announce_hash,
320+ "neighbor announce branch contains last committed announce, so parent announce branch is expired" ,
321+ ) ;
227322 return Ok ( ( ) ) ;
228323 } ;
229324
@@ -332,10 +427,7 @@ fn find_announces_common_predecessor(
332427 . announces
333428 . ok_or_else ( || anyhow ! ( "announces not found for block {block_hash}" ) ) ?;
334429
335- for _ in 0 ..commitment_delay_limit
336- . checked_sub ( 1 )
337- . ok_or_else ( || anyhow ! ( "unsupported 0 commitment delay limit" ) ) ?
338- {
430+ for _ in 0 ..commitment_delay_limit {
339431 if announces. contains ( & start_announce_hash) {
340432 if announces. len ( ) != 1 {
341433 return Err ( anyhow ! (
0 commit comments