Skip to main content

tempo_transaction_pool/
amm.rs

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