Skip to main content

tempo_transaction_pool/
amm.rs

1use std::{collections::VecDeque, sync::Arc};
2
3use alloy_consensus::BlockHeader;
4use alloy_primitives::{
5    Address, U256,
6    map::{AddressMap, HashMap, U256Map},
7};
8use itertools::Itertools;
9use parking_lot::RwLock;
10use reth_primitives_traits::SealedHeader;
11use reth_provider::{
12    ChainSpecProvider, ExecutionOutcome, HeaderProvider, ProviderError, ProviderResult,
13    StateProvider, StateProviderFactory,
14};
15use tempo_chainspec::{
16    TempoChainSpec,
17    hardfork::{TempoHardfork, TempoHardforks},
18};
19use tempo_evm::TempoStateAccess;
20use tempo_precompiles::{
21    DEFAULT_FEE_TOKEN, TIP_FEE_MANAGER_ADDRESS,
22    error::Result as TempoResult,
23    storage::Handler,
24    tip_fee_manager::{
25        TipFeeManager,
26        amm::{Pool, PoolKey, compute_amount_out},
27    },
28};
29use tempo_primitives::{TempoHeader, TempoReceipt};
30use tempo_revm::IntoAddress;
31
32/// Number of recent validators/tokens to track.
33const LAST_SEEN_WINDOW: usize = 10;
34
35#[derive(Debug, Clone)]
36pub struct AmmLiquidityCache {
37    inner: Arc<RwLock<AmmLiquidityCacheInner>>,
38}
39
40impl AmmLiquidityCache {
41    /// Creates a new [`AmmLiquidityCache`] and pre-populates the cache with
42    /// validator fee tokens of the latest blocks.
43    pub fn new<Client>(client: Client) -> ProviderResult<Self>
44    where
45        Client: StateProviderFactory
46            + HeaderProvider<Header = TempoHeader>
47            + ChainSpecProvider<ChainSpec = TempoChainSpec>,
48    {
49        let this = Self {
50            inner: Default::default(),
51        };
52        this.repopulate(&client)?;
53
54        Ok(this)
55    }
56
57    /// Checks whether there's enough liquidity in at least one of the AMM pools
58    /// used by recent validators for the given fee token and fee amount
59    pub fn has_enough_liquidity(
60        &self,
61        user_token: Address,
62        fee: U256,
63        state_provider: &mut impl StateProvider,
64    ) -> Result<bool, ProviderError> {
65        let amount_out = compute_amount_out(fee).map_err(ProviderError::other)?;
66
67        let mut missing_in_cache = Vec::new();
68
69        // search through latest observed validator tokens and find any cached pools that have enough liquidity
70        {
71            let inner = self.inner.read();
72            for validator_token in &inner.unique_tokens {
73                // If user token matches one of the recently seen validator tokens,
74                // short circuit and return true. We assume that validators are willing to
75                // accept transactions that pay fees in their token directly.
76                if validator_token == &user_token {
77                    return Ok(true);
78                }
79
80                if let Some(validator_reserve) = inner.cache.get(&(user_token, *validator_token)) {
81                    if *validator_reserve >= amount_out {
82                        return Ok(true);
83                    }
84                } else {
85                    missing_in_cache.push(*validator_token);
86                }
87            }
88        }
89
90        // If no cache misses were hit, just return false
91        if missing_in_cache.is_empty() {
92            return Ok(false);
93        }
94
95        // Spec doesn't affect raw storage reads (sload), so default is safe here.
96        state_provider
97            .with_read_only_storage_ctx(TempoHardfork::default(), || -> TempoResult<bool> {
98                // Otherwise, load pools that weren't found in cache and check if they have enough liquidity
99                for validator_token in missing_in_cache {
100                    // This might race other fetches but we're OK with it.
101                    let pool_key = PoolKey::new(user_token, validator_token).get_id();
102                    let manager = TipFeeManager::new();
103                    let (slot, pool) = (
104                        manager.pools[pool_key].base_slot(),
105                        manager.pools[pool_key].read()?,
106                    );
107                    let reserve = U256::from(pool.reserve_validator_token);
108
109                    let mut inner = self.inner.write();
110                    inner.cache.insert((user_token, validator_token), reserve);
111                    inner
112                        .slot_to_pool
113                        .insert(slot, (user_token, validator_token));
114
115                    // If the pool has enough liquidity, short circuit and return true
116                    if reserve >= amount_out {
117                        return Ok(true);
118                    }
119                }
120
121                Ok(false)
122            })
123            .map_err(ProviderError::other)
124    }
125
126    /// Clears all cached state. Used on reorg to invalidate stale entries
127    /// from orphaned blocks.
128    pub fn clear(&self) {
129        *self.inner.write() = AmmLiquidityCacheInner::default();
130    }
131
132    /// Clears all cached state and repopulates from the current canonical chain.
133    ///
134    /// This should be called on reorg to ensure stale entries from orphaned
135    /// blocks are replaced with data from the new canonical chain.
136    pub fn repopulate<Client>(&self, client: &Client) -> ProviderResult<()>
137    where
138        Client: StateProviderFactory
139            + HeaderProvider<Header = TempoHeader>
140            + ChainSpecProvider<ChainSpec = TempoChainSpec>,
141    {
142        self.clear();
143        let tip = client.best_block_number()?;
144        let headers =
145            client.sealed_headers_range(tip.saturating_sub(LAST_SEEN_WINDOW as u64 + 1)..=tip)?;
146        self.on_new_blocks(headers.iter(), client)
147    }
148
149    /// Processes a new [`ExecutionOutcome`] and caches new validator
150    /// fee token preferences and AMM pool liquidity changes.
151    pub fn on_new_state(&self, execution_outcome: &ExecutionOutcome<TempoReceipt>) {
152        let Some(storage) = execution_outcome
153            .account_state(&TIP_FEE_MANAGER_ADDRESS)
154            .map(|acc| &acc.storage)
155        else {
156            return;
157        };
158
159        let mut inner = self.inner.write();
160
161        // Process all FeeManager slot changes and update the cache.
162        for (slot, value) in storage.iter() {
163            if let Some(pool) = inner.slot_to_pool.get(slot).copied() {
164                // Update AMM pools
165                let validator_reserve =
166                    U256::from(Pool::decode_from_slot(value.present_value).reserve_validator_token);
167                inner.cache.insert(pool, validator_reserve);
168            } else if let Some(validator) = inner.slot_to_validator.get(slot).copied() {
169                // Update validator fee token preferences
170                inner
171                    .validator_preferences
172                    .insert(validator, value.present_value().into_address());
173            }
174        }
175    }
176
177    /// Processes new blocks and records recent validators and their fee token preferences in the cache.
178    pub fn on_new_blocks<'a, P>(
179        &self,
180        headers: impl IntoIterator<Item = &'a SealedHeader<TempoHeader>>,
181        client: P,
182    ) -> ProviderResult<()>
183    where
184        P: StateProviderFactory + ChainSpecProvider<ChainSpec: TempoHardforks>,
185    {
186        let headers = headers.into_iter().collect::<Vec<_>>();
187        let latest_hash = if let Some(header) = headers.last() {
188            header.hash()
189        } else {
190            return Ok(());
191        };
192
193        let mut state = None;
194
195        for header in headers {
196            let beneficiary = header.beneficiary();
197            let validator_token_slot = TipFeeManager::new().validator_tokens[beneficiary].slot();
198
199            let cached_preference = self
200                .inner
201                .read()
202                .validator_preferences
203                .get(&beneficiary)
204                .copied();
205
206            let preference = if let Some(cached) = cached_preference {
207                cached
208            } else {
209                // If no cached preference, load from state
210
211                // Lazily initialize the state provider for the latest block in the set
212                if state.is_none() {
213                    state = Some(client.state_by_block_hash(latest_hash)?);
214                }
215
216                state
217                    .as_mut()
218                    .expect("initialized above")
219                    .storage(TIP_FEE_MANAGER_ADDRESS, validator_token_slot.into())?
220                    .unwrap_or_default()
221                    .into_address()
222            };
223
224            // Get the actual fee token, accounting for defaults.
225            let fee_token = if preference.is_zero() {
226                DEFAULT_FEE_TOKEN
227            } else {
228                preference
229            };
230
231            let mut inner = self.inner.write();
232
233            // Track the new fee token preference, if any
234            if cached_preference.is_none() {
235                inner.validator_preferences.insert(beneficiary, preference);
236                inner
237                    .slot_to_validator
238                    .insert(validator_token_slot, beneficiary);
239            }
240
241            // Track the new observed fee token
242            inner.last_seen_tokens.push_back(fee_token);
243            if inner.last_seen_tokens.len() > LAST_SEEN_WINDOW {
244                inner.last_seen_tokens.pop_front();
245            }
246            inner.unique_tokens = inner.last_seen_tokens.iter().copied().unique().collect();
247
248            // Track the new observed validator (block producer)
249            inner.last_seen_validators.push_back(beneficiary);
250            if inner.last_seen_validators.len() > LAST_SEEN_WINDOW {
251                inner.last_seen_validators.pop_front();
252            }
253            inner.unique_validators = inner
254                .last_seen_validators
255                .iter()
256                .copied()
257                .unique()
258                .collect();
259        }
260
261        Ok(())
262    }
263}
264
265#[derive(Debug, Default)]
266struct AmmLiquidityCacheInner {
267    /// Cache for (user_token, validator_token) -> liquidity
268    cache: HashMap<(Address, Address), U256>,
269
270    /// Reverse index for mapping AMM slot to a pool.
271    slot_to_pool: U256Map<(Address, Address)>,
272
273    /// Latest observed validator tokens.
274    last_seen_tokens: VecDeque<Address>,
275
276    /// Unique tokens that have been seen in the last_seen_tokens.
277    unique_tokens: Vec<Address>,
278
279    /// Latest observed validators (block producers).
280    last_seen_validators: VecDeque<Address>,
281
282    /// Unique validators that have produced recent blocks.
283    unique_validators: Vec<Address>,
284
285    /// cache for validator fee token preferences configured in the fee manager
286    validator_preferences: AddressMap<Address>,
287
288    /// Reverse index for mapping validator preference slot to validator address.
289    slot_to_validator: U256Map<Address>,
290}
291
292impl AmmLiquidityCache {
293    /// Returns `true` if the given address is a validator that has produced recent blocks.
294    ///
295    /// Use this to filter validator token change events: only process changes from
296    /// validators who actually produce blocks. This prevents permissionless
297    /// `setValidatorToken` calls from triggering mass pending transaction eviction.
298    pub fn is_active_validator(&self, validator: &Address) -> bool {
299        self.inner.read().unique_validators.contains(validator)
300    }
301
302    /// Returns `true` if the given token is in the `unique_tokens` list (tokens used
303    /// by recent block producers as their preferred fee token).
304    pub fn is_active_validator_token(&self, token: &Address) -> bool {
305        self.inner.read().unique_tokens.contains(token)
306    }
307
308    /// Injects tokens into `unique_tokens` so `has_enough_liquidity` sees them.
309    /// Returns `true` if any of the input tokens is added to the `unique_tokens` list.
310    ///
311    /// NOTE: Bridges the gap between `setValidatorToken` events and the next block
312    /// produced by that validator. Cleaned up on the next `on_new_block` call.
313    pub fn track_tokens(&self, tokens: &[Address]) -> bool {
314        let mut updated = false;
315        if tokens.is_empty() {
316            return updated;
317        }
318
319        let mut inner = self.inner.write();
320        for &token in tokens {
321            if !inner.unique_tokens.contains(&token) {
322                inner.unique_tokens.push(token);
323                updated = true;
324            }
325        }
326        updated
327    }
328}
329
330#[cfg(any(test, feature = "test-utils"))]
331impl AmmLiquidityCache {
332    /// Creates a new [`AmmLiquidityCache`] with pre-populated unique tokens for testing.
333    pub fn with_unique_tokens(unique_tokens: Vec<Address>) -> Self {
334        Self {
335            inner: Arc::new(RwLock::new(AmmLiquidityCacheInner {
336                unique_tokens,
337                ..Default::default()
338            })),
339        }
340    }
341
342    /// Creates a new [`AmmLiquidityCache`] with pre-populated unique validators for testing.
343    pub fn with_unique_validators(unique_validators: Vec<Address>) -> Self {
344        Self {
345            inner: Arc::new(RwLock::new(AmmLiquidityCacheInner {
346                unique_validators,
347                ..Default::default()
348            })),
349        }
350    }
351}
352
353#[cfg(test)]
354mod tests {
355    use super::*;
356    use crate::test_utils::create_mock_provider;
357    use alloy_primitives::address;
358
359    // ============================================
360    // has_enough_liquidity tests (using MockEthProvider)
361    // ============================================
362
363    #[test]
364    fn test_has_enough_liquidity_user_token_matches_validator_token() {
365        let cache = AmmLiquidityCache {
366            inner: Arc::new(RwLock::new(AmmLiquidityCacheInner {
367                unique_tokens: vec![address!("1111111111111111111111111111111111111111")],
368                ..Default::default()
369            })),
370        };
371
372        let provider = create_mock_provider();
373        let mut state = provider.latest().unwrap();
374
375        let user_token = address!("1111111111111111111111111111111111111111");
376        let result = cache.has_enough_liquidity(user_token, U256::from(100), &mut state);
377
378        assert!(result.is_ok());
379        assert!(
380            result.unwrap(),
381            "Should return true when user token matches validator token"
382        );
383    }
384
385    #[test]
386    fn test_has_enough_liquidity_cached_pool_sufficient() {
387        let user_token = address!("2222222222222222222222222222222222222222");
388        let validator_token = address!("3333333333333333333333333333333333333333");
389
390        let cache = AmmLiquidityCache {
391            inner: Arc::new(RwLock::new(AmmLiquidityCacheInner {
392                unique_tokens: vec![validator_token],
393                cache: {
394                    let mut m = HashMap::default();
395                    m.insert((user_token, validator_token), U256::MAX);
396                    m
397                },
398                ..Default::default()
399            })),
400        };
401
402        let provider = create_mock_provider();
403        let mut state = provider.latest().unwrap();
404
405        let result = cache.has_enough_liquidity(user_token, U256::from(1000), &mut state);
406        assert!(result.is_ok());
407        assert!(
408            result.unwrap(),
409            "Should return true for sufficient cached reserve"
410        );
411    }
412
413    #[test]
414    fn test_has_enough_liquidity_cached_pool_insufficient() {
415        let user_token = address!("2222222222222222222222222222222222222222");
416        let validator_token = address!("3333333333333333333333333333333333333333");
417
418        let cache = AmmLiquidityCache {
419            inner: Arc::new(RwLock::new(AmmLiquidityCacheInner {
420                unique_tokens: vec![validator_token],
421                cache: {
422                    let mut m = HashMap::default();
423                    m.insert((user_token, validator_token), U256::ZERO);
424                    m
425                },
426                ..Default::default()
427            })),
428        };
429
430        let provider = create_mock_provider();
431        let mut state = provider.latest().unwrap();
432
433        let result = cache.has_enough_liquidity(user_token, U256::from(1000), &mut state);
434        assert!(result.is_ok());
435        assert!(
436            !result.unwrap(),
437            "Should return false for insufficient cached reserve"
438        );
439    }
440
441    #[test]
442    fn test_has_enough_liquidity_no_unique_tokens() {
443        let cache = AmmLiquidityCache {
444            inner: Arc::new(RwLock::new(AmmLiquidityCacheInner::default())),
445        };
446
447        let provider = create_mock_provider();
448        let mut state = provider.latest().unwrap();
449
450        let user_token = address!("1111111111111111111111111111111111111111");
451        let result = cache.has_enough_liquidity(user_token, U256::from(1000), &mut state);
452        assert!(result.is_ok());
453        assert!(
454            !result.unwrap(),
455            "Should return false when no unique tokens"
456        );
457    }
458
459    #[test]
460    fn test_has_enough_liquidity_cache_miss_insufficient() {
461        let user_token = address!("2222222222222222222222222222222222222222");
462        let validator_token = address!("3333333333333333333333333333333333333333");
463
464        let cache = AmmLiquidityCache {
465            inner: Arc::new(RwLock::new(AmmLiquidityCacheInner {
466                unique_tokens: vec![validator_token],
467                cache: HashMap::default(),
468                ..Default::default()
469            })),
470        };
471
472        let provider = create_mock_provider();
473        let mut state = provider.latest().unwrap();
474
475        // Provider returns default (zero) storage values
476        let result = cache.has_enough_liquidity(user_token, U256::from(1000), &mut state);
477        assert!(result.is_ok());
478        assert!(
479            !result.unwrap(),
480            "Should return false for insufficient reserve"
481        );
482    }
483
484    // ============================================
485    // on_new_state tests
486    // ============================================
487
488    #[test]
489    fn test_on_new_state_early_return_no_fee_manager_account() {
490        use reth_provider::ExecutionOutcome;
491        use tempo_primitives::TempoReceipt;
492
493        let cache = AmmLiquidityCache {
494            inner: Arc::new(RwLock::new(AmmLiquidityCacheInner::default())),
495        };
496
497        let execution_outcome: ExecutionOutcome<TempoReceipt> = ExecutionOutcome::default();
498        cache.on_new_state(&execution_outcome);
499
500        let inner = cache.inner.read();
501        assert!(inner.cache.is_empty());
502        assert!(inner.validator_preferences.is_empty());
503    }
504
505    // ============================================
506    // Sliding window tests
507    // ============================================
508
509    #[test]
510    fn test_sliding_window_max_size() {
511        let mut inner = AmmLiquidityCacheInner::default();
512
513        for i in 0..LAST_SEEN_WINDOW {
514            let token = Address::new([i as u8; 20]);
515            inner.last_seen_tokens.push_back(token);
516        }
517
518        assert_eq!(inner.last_seen_tokens.len(), LAST_SEEN_WINDOW);
519
520        let new_token = Address::new([0xFF; 20]);
521        inner.last_seen_tokens.push_back(new_token);
522        if inner.last_seen_tokens.len() > LAST_SEEN_WINDOW {
523            inner.last_seen_tokens.pop_front();
524        }
525
526        assert_eq!(inner.last_seen_tokens.len(), LAST_SEEN_WINDOW);
527        assert_eq!(inner.last_seen_tokens.back(), Some(&new_token));
528        assert_eq!(inner.last_seen_tokens.front(), Some(&Address::new([1; 20])));
529    }
530
531    #[test]
532    fn test_sliding_window_validators() {
533        let mut inner = AmmLiquidityCacheInner::default();
534
535        for i in 0..LAST_SEEN_WINDOW {
536            let validator = Address::new([i as u8; 20]);
537            inner.last_seen_validators.push_back(validator);
538        }
539
540        assert_eq!(inner.last_seen_validators.len(), LAST_SEEN_WINDOW);
541
542        let new_validator = Address::new([0xFF; 20]);
543        inner.last_seen_validators.push_back(new_validator);
544        if inner.last_seen_validators.len() > LAST_SEEN_WINDOW {
545            inner.last_seen_validators.pop_front();
546        }
547
548        assert_eq!(inner.last_seen_validators.len(), LAST_SEEN_WINDOW);
549        assert_eq!(inner.last_seen_validators.back(), Some(&new_validator));
550        assert_eq!(
551            inner.last_seen_validators.front(),
552            Some(&Address::new([1; 20]))
553        );
554
555        inner.unique_validators = inner
556            .last_seen_validators
557            .iter()
558            .copied()
559            .unique()
560            .collect();
561        assert!(inner.unique_validators.contains(&new_validator));
562    }
563
564    #[test]
565    fn test_unique_tokens_deduplication() {
566        let mut inner = AmmLiquidityCacheInner::default();
567
568        let token_a = address!("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
569        let token_b = address!("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB");
570
571        inner.last_seen_tokens.push_back(token_a);
572        inner.last_seen_tokens.push_back(token_b);
573        inner.last_seen_tokens.push_back(token_b);
574        inner.last_seen_tokens.push_back(token_b);
575
576        inner.unique_tokens = inner.last_seen_tokens.iter().copied().unique().collect();
577
578        assert_eq!(inner.unique_tokens.len(), 2, "duplicates must be removed");
579        assert_eq!(inner.unique_tokens[0], token_a);
580        assert_eq!(inner.unique_tokens[1], token_b);
581    }
582
583    // ============================================
584    // AmmLiquidityCacheInner direct manipulation tests
585    // ============================================
586
587    #[test]
588    fn test_cache_insert_and_lookup() {
589        let mut inner = AmmLiquidityCacheInner::default();
590
591        let user_token = address!("1111111111111111111111111111111111111111");
592        let validator_token = address!("2222222222222222222222222222222222222222");
593        let reserve = U256::from(5000);
594
595        inner.cache.insert((user_token, validator_token), reserve);
596
597        assert_eq!(
598            inner.cache.get(&(user_token, validator_token)),
599            Some(&reserve)
600        );
601    }
602
603    #[test]
604    fn test_slot_to_pool_mapping() {
605        let mut inner = AmmLiquidityCacheInner::default();
606
607        let user_token = address!("1111111111111111111111111111111111111111");
608        let validator_token = address!("2222222222222222222222222222222222222222");
609        let slot = U256::from(12345);
610
611        inner
612            .slot_to_pool
613            .insert(slot, (user_token, validator_token));
614
615        assert_eq!(
616            inner.slot_to_pool.get(&slot),
617            Some(&(user_token, validator_token))
618        );
619    }
620
621    #[test]
622    fn test_validator_preferences_mapping() {
623        let mut inner = AmmLiquidityCacheInner::default();
624
625        let validator = address!("3333333333333333333333333333333333333333");
626        let fee_token = address!("4444444444444444444444444444444444444444");
627
628        inner.validator_preferences.insert(validator, fee_token);
629
630        assert_eq!(
631            inner.validator_preferences.get(&validator),
632            Some(&fee_token)
633        );
634    }
635
636    #[test]
637    fn test_slot_to_validator_mapping() {
638        let mut inner = AmmLiquidityCacheInner::default();
639
640        let validator = address!("3333333333333333333333333333333333333333");
641        let slot = U256::from(67890);
642
643        inner.slot_to_validator.insert(slot, validator);
644
645        assert_eq!(inner.slot_to_validator.get(&slot), Some(&validator));
646    }
647
648    #[test]
649    fn test_clear_resets_all_state() {
650        let user_token = Address::random();
651        let validator_token = Address::random();
652        let validator = Address::random();
653
654        let cache = AmmLiquidityCache {
655            inner: Arc::new(RwLock::new(AmmLiquidityCacheInner {
656                cache: {
657                    let mut m = HashMap::default();
658                    m.insert((user_token, validator_token), U256::from(1000));
659                    m
660                },
661                slot_to_pool: {
662                    let mut m = U256Map::default();
663                    m.insert(U256::from(1), (user_token, validator_token));
664                    m
665                },
666                last_seen_tokens: VecDeque::from(vec![validator_token]),
667                unique_tokens: vec![validator_token],
668                last_seen_validators: VecDeque::from(vec![validator]),
669                unique_validators: vec![validator],
670                validator_preferences: {
671                    let mut m = AddressMap::default();
672                    m.insert(validator, validator_token);
673                    m
674                },
675                slot_to_validator: {
676                    let mut m = U256Map::default();
677                    m.insert(U256::from(2), validator);
678                    m
679                },
680            })),
681        };
682
683        cache.clear();
684
685        let inner = cache.inner.read();
686        assert!(inner.cache.is_empty(), "cache should be empty after clear");
687        assert!(
688            inner.slot_to_pool.is_empty(),
689            "slot_to_pool should be empty after clear"
690        );
691        assert!(
692            inner.last_seen_tokens.is_empty(),
693            "last_seen_tokens should be empty after clear"
694        );
695        assert!(
696            inner.unique_tokens.is_empty(),
697            "unique_tokens should be empty after clear"
698        );
699        assert!(
700            inner.last_seen_validators.is_empty(),
701            "last_seen_validators should be empty after clear"
702        );
703        assert!(
704            inner.unique_validators.is_empty(),
705            "unique_validators should be empty after clear"
706        );
707        assert!(
708            inner.validator_preferences.is_empty(),
709            "validator_preferences should be empty after clear"
710        );
711        assert!(
712            inner.slot_to_validator.is_empty(),
713            "slot_to_validator should be empty after clear"
714        );
715    }
716
717    #[test]
718    fn test_repopulate_clears_stale_data_and_rebuilds_from_canonical_chain() {
719        use alloy_consensus::Header;
720
721        let stale_validator = Address::random();
722        let stale_token = Address::random();
723        let stale_user_token = Address::random();
724
725        let cache = AmmLiquidityCache {
726            inner: Arc::new(RwLock::new(AmmLiquidityCacheInner {
727                cache: {
728                    let mut m = HashMap::default();
729                    m.insert((stale_user_token, stale_token), U256::from(9999));
730                    m
731                },
732                slot_to_pool: {
733                    let mut m = U256Map::default();
734                    m.insert(U256::from(42), (stale_user_token, stale_token));
735                    m
736                },
737                last_seen_tokens: VecDeque::from(vec![stale_token]),
738                unique_tokens: vec![stale_token],
739                last_seen_validators: VecDeque::from(vec![stale_validator]),
740                unique_validators: vec![stale_validator],
741                validator_preferences: {
742                    let mut m = AddressMap::default();
743                    m.insert(stale_validator, stale_token);
744                    m
745                },
746                slot_to_validator: {
747                    let mut m = U256Map::default();
748                    m.insert(U256::from(99), stale_validator);
749                    m
750                },
751            })),
752        };
753
754        {
755            let inner = cache.inner.read();
756            assert!(inner.unique_validators.contains(&stale_validator));
757            assert!(inner.unique_tokens.contains(&stale_token));
758            assert_eq!(
759                inner.cache.get(&(stale_user_token, stale_token)),
760                Some(&U256::from(9999))
761            );
762        }
763
764        let new_validator = Address::random();
765        let provider = create_mock_provider();
766        for i in 0..3u64 {
767            let header = TempoHeader {
768                inner: Header {
769                    number: i,
770                    beneficiary: new_validator,
771                    ..Default::default()
772                },
773                ..Default::default()
774            };
775            provider.add_header(alloy_primitives::B256::random(), header);
776        }
777
778        cache
779            .repopulate(&provider)
780            .expect("repopulate should succeed");
781
782        let inner = cache.inner.read();
783
784        assert!(
785            !inner.unique_validators.contains(&stale_validator),
786            "stale validator should be gone after repopulate"
787        );
788        assert!(
789            !inner.unique_tokens.contains(&stale_token),
790            "stale token should be gone after repopulate"
791        );
792        assert!(
793            !inner.cache.contains_key(&(stale_user_token, stale_token)),
794            "stale liquidity entry should be gone after repopulate"
795        );
796        assert!(
797            inner.slot_to_pool.is_empty(),
798            "stale slot_to_pool should be gone after repopulate"
799        );
800
801        assert!(
802            inner.unique_validators.contains(&new_validator),
803            "new canonical validator should be present after repopulate"
804        );
805        assert_eq!(
806            inner.last_seen_validators.len(),
807            3,
808            "should have 3 validators from new canonical headers"
809        );
810    }
811
812    #[test]
813    fn test_is_active_validator() {
814        let active = address!("1111111111111111111111111111111111111111");
815        let inactive = address!("DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF");
816
817        let cases = [
818            (vec![active], active, true, "active validator in set"),
819            (
820                vec![active],
821                inactive,
822                false,
823                "inactive validator not in set",
824            ),
825            (vec![], active, false, "empty set"),
826        ];
827
828        for (unique_validators, query, expected, desc) in cases {
829            let cache = AmmLiquidityCache::with_unique_validators(unique_validators);
830            assert_eq!(cache.is_active_validator(&query), expected, "{desc}");
831        }
832    }
833
834    #[test]
835    fn test_track_tokens() {
836        let token_a = address!("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
837        let token_b = address!("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB");
838
839        // Empty slice is a no-op
840        let cache = AmmLiquidityCache::with_unique_tokens(vec![]);
841        assert!(!cache.track_tokens(&[]));
842        assert!(cache.inner.read().unique_tokens.is_empty());
843
844        // New token is inserted
845        let cache = AmmLiquidityCache::with_unique_tokens(vec![token_a]);
846        assert!(cache.track_tokens(&[token_b]));
847        assert_eq!(cache.inner.read().unique_tokens, vec![token_a, token_b]);
848
849        // Already-tracked token returns false
850        let cache = AmmLiquidityCache::with_unique_tokens(vec![token_a]);
851        assert!(!cache.track_tokens(&[token_a]));
852        assert_eq!(cache.inner.read().unique_tokens.len(), 1);
853
854        // Duplicate input is deduplicated
855        let cache = AmmLiquidityCache::with_unique_tokens(vec![token_a]);
856        assert!(cache.track_tokens(&[token_b, token_b]));
857        assert_eq!(cache.inner.read().unique_tokens.len(), 2);
858    }
859}