1use std::{
2 collections::{HashMap, HashSet},
3 sync::Arc,
4};
5
6use thiserror::Error;
7use tokio::sync::{mpsc::UnboundedReceiver, watch};
8use tycho_client::feed::{synchronizer::Snapshot, BlockHeader, FeedMessage};
9use tycho_common::{
10 models::{
11 blockchain::{Block, BlockAggregatedChanges, PendingBlock},
12 protocol::{ComponentBalance, ProtocolComponent, ProtocolComponentStateDelta},
13 Chain,
14 },
15 traits::TxDeltaIndexer,
16 Bytes,
17};
18
19use crate::{
20 evm::decoder::{StreamDecodeError, TychoStreamDecoder},
21 protocol::models::Update,
22};
23
24pub struct PendingUpdate {
30 pub label: String,
31 pub update: Update,
32}
33
34#[derive(Debug, Error)]
35pub enum PendingError {
36 #[error("parent block {needed} not yet confirmed (current: {current})")]
40 ParentNotYetConfirmed { needed: u64, current: u64 },
41 #[error("decoder error: {0}")]
42 Decoder(#[from] StreamDecodeError),
43 #[error("indexer error for extractor '{extractor}': {message}")]
44 Indexer { extractor: String, message: String },
45}
46
47pub struct PendingBlockProcessor {
82 indexers: HashMap<String, Box<dyn TxDeltaIndexer>>,
83 decoder: Arc<TychoStreamDecoder<BlockHeader>>,
84 chain: Chain,
85 current_confirmed_block: u64,
87 confirmed_block_tx: watch::Sender<u64>,
89 block_rx: UnboundedReceiver<FeedMessage<BlockHeader>>,
91}
92
93impl PendingBlockProcessor {
94 pub(crate) fn new(
95 indexers: HashMap<String, Box<dyn TxDeltaIndexer>>,
96 decoder: Arc<TychoStreamDecoder<BlockHeader>>,
97 chain: Chain,
98 block_rx: UnboundedReceiver<FeedMessage<BlockHeader>>,
99 ) -> Self {
100 let (confirmed_block_tx, _) = watch::channel(0u64);
101 Self { indexers, decoder, chain, current_confirmed_block: 0, confirmed_block_tx, block_rx }
102 }
103
104 pub fn subscribe_confirmed_block(&self) -> watch::Receiver<u64> {
110 self.confirmed_block_tx.subscribe()
111 }
112
113 pub fn current_confirmed_block(&self) -> u64 {
115 self.current_confirmed_block
116 }
117
118 pub fn advance(&mut self, msg: &FeedMessage<BlockHeader>) -> Result<(), PendingError> {
124 self.advance_inner(msg)
125 }
126
127 pub async fn generate_pending_update(
145 &mut self,
146 pending: &PendingBlock,
147 label: String,
148 ) -> Result<PendingUpdate, PendingError> {
149 while let Ok(msg) = self.block_rx.try_recv() {
151 self.advance_inner(&msg)?;
152 }
153
154 let target_block = pending.block();
155 let parent = target_block.number.saturating_sub(1);
156 if self.current_confirmed_block < parent {
157 return Err(PendingError::ParentNotYetConfirmed {
158 needed: parent,
159 current: self.current_confirmed_block,
160 });
161 }
162 let target_header = BlockHeader::from(target_block);
163
164 let mut pending_deltas: HashMap<String, BlockAggregatedChanges> = HashMap::new();
165 for (extractor, indexer) in &self.indexers {
166 let changes = indexer.generate_deltas(pending);
167 pending_deltas.insert(extractor.clone(), changes);
168 }
169
170 let update = self
171 .decoder
172 .apply_deltas_ephemeral(&pending_deltas, target_header)
173 .await?;
174 Ok(PendingUpdate { label, update })
175 }
176
177 fn advance_inner(&mut self, msg: &FeedMessage<BlockHeader>) -> Result<(), PendingError> {
178 let msg_block = msg
179 .state_msgs
180 .values()
181 .map(|s| s.header.number)
182 .max()
183 .unwrap_or(0);
184
185 for (extractor, state_msg) in &msg.state_msgs {
186 let Some(indexer) = self.indexers.get_mut(extractor) else {
187 continue;
188 };
189
190 if !state_msg.snapshots.states.is_empty() {
191 let block_changes = snapshot_to_block_changes(
192 extractor,
193 &state_msg.snapshots,
194 &state_msg.header,
195 self.chain,
196 );
197 indexer
198 .apply_block(&block_changes)
199 .map_err(|e| PendingError::Indexer {
200 extractor: extractor.clone(),
201 message: format!("{e:#}"),
202 })?;
203 }
204
205 if let Some(deltas) = &state_msg.deltas {
206 indexer
207 .apply_block(deltas)
208 .map_err(|e| PendingError::Indexer {
209 extractor: extractor.clone(),
210 message: format!("{e:#}"),
211 })?;
212 }
213 }
214
215 if msg_block > self.current_confirmed_block {
216 self.current_confirmed_block = msg_block;
217 let _ = self.confirmed_block_tx.send(msg_block);
219 }
220 Ok(())
221 }
222}
223
224fn snapshot_to_block_changes(
227 extractor: &str,
228 snapshot: &Snapshot,
229 header: &BlockHeader,
230 chain: Chain,
231) -> BlockAggregatedChanges {
232 let ts = chrono::DateTime::from_timestamp(header.timestamp as i64, 0)
233 .unwrap_or_default()
234 .naive_utc();
235 let block = Block {
236 number: header.number,
237 chain,
238 hash: header.hash.clone(),
239 parent_hash: header.parent_hash.clone(),
240 ts,
241 };
242
243 let mut new_protocol_components: HashMap<String, ProtocolComponent> = HashMap::new();
244 let mut state_deltas: HashMap<String, ProtocolComponentStateDelta> = HashMap::new();
245 let mut component_balances: HashMap<String, HashMap<Bytes, ComponentBalance>> = HashMap::new();
246
247 for (id, comp_with_state) in &snapshot.states {
248 new_protocol_components.insert(id.clone(), comp_with_state.component.clone());
249
250 state_deltas.insert(
251 id.clone(),
252 ProtocolComponentStateDelta {
253 component_id: id.clone(),
254 updated_attributes: comp_with_state.state.attributes.clone(),
255 deleted_attributes: HashSet::new(),
256 created_attributes: HashSet::new(),
257 },
258 );
259
260 let token_balances: HashMap<Bytes, ComponentBalance> = comp_with_state
261 .state
262 .balances
263 .iter()
264 .map(|(token, balance)| {
265 (
266 token.clone(),
267 ComponentBalance {
268 token: token.clone(),
269 balance: balance.clone(),
270 balance_float: 0.0,
271 modify_tx: Bytes::default(),
272 component_id: id.clone(),
273 },
274 )
275 })
276 .collect();
277 component_balances.insert(id.clone(), token_balances);
278 }
279
280 BlockAggregatedChanges {
281 extractor: extractor.to_string(),
282 chain,
283 block,
284 finalized_block_height: header.number,
285 new_protocol_components,
286 state_deltas,
287 component_balances,
288 ..Default::default()
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use std::sync::Mutex;
295
296 use tycho_common::models::blockchain::{Block, PendingBlock};
297
298 use super::*;
299
300 struct RecordingIndexer {
302 seen: Arc<Mutex<Vec<Block>>>,
303 }
304
305 impl TxDeltaIndexer for RecordingIndexer {
306 fn apply_block(&mut self, _block: &BlockAggregatedChanges) -> anyhow::Result<()> {
307 Ok(())
308 }
309
310 fn generate_deltas(&self, pending: &PendingBlock) -> BlockAggregatedChanges {
311 self.seen
312 .lock()
313 .unwrap()
314 .push(pending.block().clone());
315 BlockAggregatedChanges::default()
316 }
317 }
318
319 fn target_block(number: u64, timestamp: i64) -> Block {
320 Block {
321 number,
322 chain: Chain::Ethereum,
323 hash: Bytes::from([1u8; 32]),
324 parent_hash: Bytes::from([2u8; 32]),
325 ts: chrono::DateTime::from_timestamp(timestamp, 0)
326 .unwrap()
327 .naive_utc(),
328 }
329 }
330
331 fn processor(seen: Arc<Mutex<Vec<Block>>>) -> PendingBlockProcessor {
332 let indexers: HashMap<String, Box<dyn TxDeltaIndexer>> =
333 HashMap::from([("fluid".to_string(), Box::new(RecordingIndexer { seen }) as _)]);
334 let (_tx, rx) = tokio::sync::mpsc::unbounded_channel();
335 PendingBlockProcessor::new(
336 indexers,
337 Arc::new(TychoStreamDecoder::<BlockHeader>::new()),
338 Chain::Ethereum,
339 rx,
340 )
341 }
342
343 #[tokio::test]
344 async fn test_indexer_receives_the_callers_target_block() {
345 let seen = Arc::new(Mutex::new(Vec::new()));
346 let mut pending_processor = processor(seen.clone());
347 let block = target_block(1, 1_759_842_947);
349
350 pending_processor
351 .generate_pending_update(
352 &PendingBlock::new(block.clone(), vec![], HashMap::new()),
353 "bundle-1".to_string(),
354 )
355 .await
356 .expect("pending update failed");
357
358 let seen = seen.lock().unwrap();
359 assert_eq!(
360 seen.as_slice(),
361 [block],
362 "The indexer must be handed the caller's block, not one derived from the parent."
363 );
364 }
365
366 fn confirmed_at(number: u64) -> FeedMessage<BlockHeader> {
368 FeedMessage {
369 state_msgs: HashMap::from([(
370 "fluid".to_string(),
371 tycho_client::feed::synchronizer::StateSyncMessage {
372 header: BlockHeader { number, ..Default::default() },
373 ..Default::default()
374 },
375 )]),
376 sync_states: HashMap::new(),
377 }
378 }
379
380 #[tokio::test]
383 async fn test_stamped_header_comes_from_the_pending_block() {
384 let seen = Arc::new(Mutex::new(Vec::new()));
385 let mut pending_processor = processor(seen);
386 pending_processor
387 .advance(&confirmed_at(5))
388 .expect("advance failed");
389
390 let update = pending_processor
391 .generate_pending_update(
392 &PendingBlock::new(target_block(3, 1_759_842_947), vec![], HashMap::new()),
393 "bundle-1".to_string(),
394 )
395 .await
396 .expect("pending update failed");
397
398 assert_eq!(
399 update.update.block_number_or_timestamp, 3,
400 "The update must be stamped with the pending block, not the confirmed tip."
401 );
402 }
403
404 #[tokio::test]
405 async fn test_parent_guard_reads_the_pending_blocks_number() {
406 let seen = Arc::new(Mutex::new(Vec::new()));
407 let mut pending_processor = processor(seen.clone());
408 let block = target_block(23_526_115, 1_759_842_947);
409
410 let result = pending_processor
411 .generate_pending_update(
412 &PendingBlock::new(block, vec![], HashMap::new()),
413 "bundle-1".to_string(),
414 )
415 .await;
416
417 match result {
418 Err(PendingError::ParentNotYetConfirmed { needed, current }) => {
419 assert_eq!(needed, 23_526_114);
420 assert_eq!(current, 0);
421 }
422 Err(other) => panic!("expected ParentNotYetConfirmed, got {other:?}"),
423 Ok(_) => panic!("expected ParentNotYetConfirmed, got a successful update"),
424 }
425 assert!(
426 seen.lock().unwrap().is_empty(),
427 "No indexer should run when the parent is not confirmed."
428 );
429 }
430}