1use super::{
29 config::TransactionFetcherConfig,
30 constants::{tx_fetcher::*, SOFT_LIMIT_COUNT_HASHES_IN_GET_POOLED_TRANSACTIONS_REQUEST},
31 PeerMetadata, PooledTransactions, SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESPONSE,
32};
33use crate::{
34 cache::{LruCache, LruMap},
35 duration_metered_exec,
36 metrics::TransactionFetcherMetrics,
37};
38use alloy_consensus::transaction::PooledTransaction;
39use alloy_primitives::{
40 map::{FbBuildHasher, HashMap},
41 TxHash,
42};
43use derive_more::{Constructor, Deref};
44use futures::{stream::FuturesUnordered, Future, FutureExt, Stream, StreamExt};
45use pin_project::pin_project;
46use reth_eth_wire::{
47 DedupPayload, GetPooledTransactions, HandleMempoolData, HandleVersionedMempoolData,
48 PartiallyValidData, RequestTxHashes, ValidAnnouncementData,
49};
50use reth_eth_wire_types::{EthNetworkPrimitives, NetworkPrimitives};
51use reth_network_api::PeerRequest;
52use reth_network_p2p::error::{RequestError, RequestResult};
53use reth_network_peers::PeerId;
54use reth_primitives_traits::SignedTransaction;
55use schnellru::ByLength;
56use std::{
57 pin::Pin,
58 task::{ready, Context, Poll},
59 time::Duration,
60};
61use tokio::sync::{mpsc::error::TrySendError, oneshot, oneshot::error::RecvError};
62use tracing::trace;
63
64#[derive(Debug)]
69#[pin_project]
70pub struct TransactionFetcher<N: NetworkPrimitives = EthNetworkPrimitives> {
71 pub active_peers: LruMap<PeerId, u8, ByLength, FbBuildHasher<64>>,
73 #[pin]
79 pub inflight_requests: FuturesUnordered<GetPooledTxRequestFut<N::PooledTransaction>>,
80 pub hashes_pending_fetch: LruCache<TxHash, FbBuildHasher<32>>,
85 pub hashes_fetch_inflight_and_pending_fetch:
87 LruMap<TxHash, TxFetchMetadata, ByLength, FbBuildHasher<32>>,
88 pub info: TransactionFetcherInfo,
90 #[doc(hidden)]
91 metrics: TransactionFetcherMetrics,
92}
93
94impl<N: NetworkPrimitives> TransactionFetcher<N> {
95 pub(crate) fn remove_peer(&mut self, peer_id: &PeerId) {
97 self.active_peers.remove(peer_id);
98 }
99
100 #[inline]
102 pub fn update_metrics(&self) {
103 let metrics = &self.metrics;
104
105 metrics.inflight_transaction_requests.set(self.inflight_requests.len() as f64);
106
107 let hashes_pending_fetch = self.hashes_pending_fetch.len() as f64;
108 let total_hashes = self.hashes_fetch_inflight_and_pending_fetch.len() as f64;
109
110 metrics.hashes_pending_fetch.set(hashes_pending_fetch);
111 metrics.hashes_inflight_transaction_requests.set(total_hashes - hashes_pending_fetch);
112 }
113
114 #[inline]
115 fn update_pending_fetch_cache_search_metrics(&self, durations: TxFetcherSearchDurations) {
116 let metrics = &self.metrics;
117
118 let TxFetcherSearchDurations { find_idle_peer, fill_request } = durations;
119 metrics
120 .duration_find_idle_fallback_peer_for_any_pending_hash
121 .set(find_idle_peer.as_secs_f64());
122 metrics.duration_fill_request_from_hashes_pending_fetch.set(fill_request.as_secs_f64());
123 }
124
125 pub fn with_transaction_fetcher_config(config: &TransactionFetcherConfig) -> Self {
127 let TransactionFetcherConfig {
128 max_inflight_requests,
129 max_capacity_cache_txns_pending_fetch,
130 ..
131 } = *config;
132
133 let info = config.clone().into();
134
135 let metrics = TransactionFetcherMetrics::default();
136 metrics.capacity_inflight_requests.increment(max_inflight_requests as u64);
137
138 Self {
139 active_peers: LruMap::with_hasher(max_inflight_requests, Default::default()),
140 hashes_pending_fetch: LruCache::with_hasher(
141 max_capacity_cache_txns_pending_fetch,
142 Default::default(),
143 ),
144 hashes_fetch_inflight_and_pending_fetch: LruMap::with_hasher(
145 max_inflight_requests + max_capacity_cache_txns_pending_fetch,
146 Default::default(),
147 ),
148 info,
149 metrics,
150 ..Default::default()
151 }
152 }
153
154 #[inline]
156 pub fn remove_hashes_from_transaction_fetcher<'a, I>(&mut self, hashes: I)
157 where
158 I: IntoIterator<Item = &'a TxHash>,
159 {
160 for hash in hashes {
161 self.hashes_fetch_inflight_and_pending_fetch.remove(hash);
162 self.hashes_pending_fetch.remove(hash);
163 }
164 }
165
166 fn decrement_inflight_request_count_for(&mut self, peer_id: &PeerId) {
168 let remove = || -> bool {
169 if let Some(inflight_count) = self.active_peers.get(peer_id) {
170 *inflight_count = inflight_count.saturating_sub(1);
171 if *inflight_count == 0 {
172 return true
173 }
174 }
175 false
176 }();
177
178 if remove {
179 self.active_peers.remove(peer_id);
180 }
181 }
182
183 #[inline]
185 pub fn is_idle(&self, peer_id: &PeerId) -> bool {
186 let Some(inflight_count) = self.active_peers.peek(peer_id) else { return true };
187 *inflight_count < self.info.max_inflight_requests_per_peer
188 }
189
190 pub fn get_idle_peer_for(&self, hash: TxHash) -> Option<&PeerId> {
192 let TxFetchMetadata { fallback_peers, .. } =
193 self.hashes_fetch_inflight_and_pending_fetch.peek(&hash)?;
194
195 fallback_peers.iter().find(|peer_id| self.is_idle(peer_id))
196 }
197
198 pub fn find_any_idle_fallback_peer_for_any_pending_hash(
204 &mut self,
205 hashes_to_request: &mut RequestTxHashes,
206 mut budget: Option<usize>, ) -> Option<PeerId> {
208 let mut hashes_pending_fetch_iter = self.hashes_pending_fetch.iter();
209
210 let idle_peer = loop {
211 let &hash = hashes_pending_fetch_iter.next()?;
212
213 let idle_peer = self.get_idle_peer_for(hash);
214
215 if idle_peer.is_some() {
216 hashes_to_request.insert(hash);
217 break idle_peer.copied()
218 }
219
220 if let Some(ref mut bud) = budget {
221 *bud = bud.saturating_sub(1);
222 if *bud == 0 {
223 return None
224 }
225 }
226 };
227 let hash = hashes_to_request.iter().next()?;
228
229 drop(hashes_pending_fetch_iter);
231 _ = self.hashes_pending_fetch.remove(hash);
232
233 idle_peer
234 }
235
236 pub fn pack_request(
241 &self,
242 hashes_to_request: &mut RequestTxHashes,
243 hashes_from_announcement: ValidAnnouncementData,
244 ) -> RequestTxHashes {
245 if hashes_from_announcement.msg_version().has_eth68_metadata() {
246 return self.pack_request_eth68(hashes_to_request, hashes_from_announcement)
247 }
248 self.pack_request_eth66(hashes_to_request, hashes_from_announcement)
249 }
250
251 pub fn pack_request_eth68(
262 &self,
263 hashes_to_request: &mut RequestTxHashes,
264 hashes_from_announcement: impl HandleMempoolData
265 + IntoIterator<Item = (TxHash, Option<(u8, usize)>)>,
266 ) -> RequestTxHashes {
267 let mut acc_size_response = 0;
268
269 let mut hashes_from_announcement_iter = hashes_from_announcement.into_iter();
270
271 if let Some((hash, Some((_ty, size)))) = hashes_from_announcement_iter.next() {
272 hashes_to_request.insert(hash);
273
274 if size >= self.info.soft_limit_byte_size_pooled_transactions_response_on_pack_request {
276 return hashes_from_announcement_iter.collect()
277 }
278 acc_size_response = size;
279 }
280
281 let mut surplus_hashes = RequestTxHashes::default();
282
283 for (hash, metadata) in hashes_from_announcement_iter.by_ref() {
286 let Some((_ty, size)) = metadata else {
287 unreachable!("this method is called upon reception of an eth68 announcement")
288 };
289
290 let next_acc_size = acc_size_response.checked_add(size).filter(|next_acc_size| {
291 *next_acc_size <=
292 self.info.soft_limit_byte_size_pooled_transactions_response_on_pack_request
293 });
294
295 if let Some(next_acc_size) = next_acc_size {
296 acc_size_response = next_acc_size;
299 _ = hashes_to_request.insert(hash)
300 } else {
301 _ = surplus_hashes.insert(hash)
302 }
303
304 let free_space =
305 self.info.soft_limit_byte_size_pooled_transactions_response_on_pack_request -
306 acc_size_response;
307
308 if free_space < MEDIAN_BYTE_SIZE_SMALL_LEGACY_TX_ENCODED {
309 break
310 }
311 }
312
313 surplus_hashes.extend(hashes_from_announcement_iter.map(|(hash, _metadata)| hash));
314
315 surplus_hashes
316 }
317
318 pub fn pack_request_eth66(
325 &self,
326 hashes_to_request: &mut RequestTxHashes,
327 hashes_from_announcement: ValidAnnouncementData,
328 ) -> RequestTxHashes {
329 let (mut hashes, _version) = hashes_from_announcement.into_request_hashes();
330 if hashes.len() <= SOFT_LIMIT_COUNT_HASHES_IN_GET_POOLED_TRANSACTIONS_REQUEST {
331 *hashes_to_request = hashes;
332 hashes_to_request.shrink_to_fit();
333
334 RequestTxHashes::default()
335 } else {
336 let surplus_hashes =
337 hashes.retain_count(SOFT_LIMIT_COUNT_HASHES_IN_GET_POOLED_TRANSACTIONS_REQUEST);
338 *hashes_to_request = hashes;
339 hashes_to_request.shrink_to_fit();
340
341 surplus_hashes
342 }
343 }
344
345 pub fn try_buffer_hashes_for_retry(
347 &mut self,
348 mut hashes: RequestTxHashes,
349 peer_failed_to_serve: &PeerId,
350 ) {
351 hashes.retain(|hash| {
354 if let Some(entry) = self.hashes_fetch_inflight_and_pending_fetch.get(hash) {
355 entry.fallback_peers_mut().remove(peer_failed_to_serve);
356 return true
357 }
358 false
360 });
361
362 self.buffer_hashes(hashes, None)
363 }
364
365 pub fn num_pending_hashes(&self) -> usize {
367 self.hashes_pending_fetch.len()
368 }
369
370 pub fn num_all_hashes(&self) -> usize {
372 self.hashes_fetch_inflight_and_pending_fetch.len()
373 }
374
375 pub fn buffer_hashes(&mut self, hashes: RequestTxHashes, fallback_peer: Option<PeerId>) {
380 for hash in hashes {
381 let Some(TxFetchMetadata { retries, fallback_peers, .. }) =
383 self.hashes_fetch_inflight_and_pending_fetch.get(&hash)
384 else {
385 continue
386 };
387
388 if let Some(peer_id) = fallback_peer {
389 fallback_peers.insert(peer_id);
391 } else {
392 if *retries >= DEFAULT_MAX_RETRIES {
393 trace!(target: "net::tx",
394 %hash,
395 retries,
396 "retry limit for `GetPooledTransactions` requests reached for hash, dropping hash"
397 );
398
399 self.hashes_fetch_inflight_and_pending_fetch.remove(&hash);
400 self.hashes_pending_fetch.remove(&hash);
401 continue
402 }
403 *retries += 1;
404 }
405
406 if let (_, Some(evicted_hash)) = self.hashes_pending_fetch.insert_and_get_evicted(hash)
407 {
408 self.hashes_fetch_inflight_and_pending_fetch.remove(&evicted_hash);
409 }
410 }
411 }
412
413 pub fn on_fetch_pending_hashes(
418 &mut self,
419 peers: &HashMap<PeerId, PeerMetadata<N>, FbBuildHasher<64>>,
420 has_capacity_wrt_pending_pool_imports: impl Fn(usize) -> bool,
421 ) -> bool {
422 let mut hashes_to_request = RequestTxHashes::with_capacity(
423 DEFAULT_MARGINAL_COUNT_HASHES_GET_POOLED_TRANSACTIONS_REQUEST,
424 );
425 let mut search_durations = TxFetcherSearchDurations::default();
426
427 let budget_find_idle_fallback_peer = self
429 .search_breadth_budget_find_idle_fallback_peer(&has_capacity_wrt_pending_pool_imports);
430
431 let peer_id = duration_metered_exec!(
432 {
433 let Some(peer_id) = self.find_any_idle_fallback_peer_for_any_pending_hash(
434 &mut hashes_to_request,
435 budget_find_idle_fallback_peer,
436 ) else {
437 return false
439 };
440
441 peer_id
442 },
443 search_durations.find_idle_peer
444 );
445
446 let Some(peer) = peers.get(&peer_id) else {
449 self.buffer_hashes(hashes_to_request, None);
450 return false
451 };
452 let conn_eth_version = peer.version;
453
454 let budget_fill_request = self
459 .search_breadth_budget_find_intersection_pending_hashes_and_hashes_seen_by_peer(
460 &has_capacity_wrt_pending_pool_imports,
461 );
462
463 duration_metered_exec!(
464 {
465 self.fill_request_from_hashes_pending_fetch(
466 &mut hashes_to_request,
467 &peer.seen_transactions,
468 budget_fill_request,
469 )
470 },
471 search_durations.fill_request
472 );
473
474 self.update_pending_fetch_cache_search_metrics(search_durations);
475
476 trace!(target: "net::tx",
477 peer_id=format!("{peer_id:#}"),
478 hashes=?*hashes_to_request,
479 %conn_eth_version,
480 "requesting hashes that were stored pending fetch from peer"
481 );
482
483 if let Some(failed_to_request_hashes) =
485 self.request_transactions_from_peer(hashes_to_request, peer)
486 {
487 trace!(target: "net::tx",
488 peer_id=format!("{peer_id:#}"),
489 ?failed_to_request_hashes,
490 %conn_eth_version,
491 "failed sending request to peer's session, buffering hashes"
492 );
493
494 self.buffer_hashes(failed_to_request_hashes, Some(peer_id));
495 return false
496 }
497
498 true
499 }
500
501 pub fn filter_unseen_and_pending_hashes(
504 &mut self,
505 new_announced_hashes: &mut ValidAnnouncementData,
506 is_tx_bad_import: impl Fn(&TxHash) -> bool,
507 peer_id: &PeerId,
508 client_version: &str,
509 ) {
510 let mut previously_unseen_hashes_count = 0;
511
512 let msg_version = new_announced_hashes.msg_version();
513
514 new_announced_hashes.retain(|hash, metadata| {
516
517 if let Some(TxFetchMetadata{ tx_encoded_length: previously_seen_size, ..}) = self.hashes_fetch_inflight_and_pending_fetch.peek_mut(hash) {
519 if let Some((_ty, size)) = metadata {
521 if let Some(prev_size) = previously_seen_size {
522 if size != prev_size {
524 trace!(target: "net::tx",
525 peer_id=format!("{peer_id:#}"),
526 %hash,
527 size,
528 previously_seen_size,
529 %client_version,
530 "peer announced a different size for tx, this is especially worrying if one size is much bigger..."
531 );
532 }
533 }
534 *previously_seen_size = Some(*size);
536 }
537
538 if self.hashes_pending_fetch.remove(hash) {
540 return true
541 }
542
543 return false
544 }
545
546 if is_tx_bad_import(hash) {
549 return false
550 }
551
552 previously_unseen_hashes_count += 1;
553
554 if self
555 .hashes_fetch_inflight_and_pending_fetch
556 .get_or_insert(*hash, || TxFetchMetadata {
557 retries: 0,
558 fallback_peers: LruCache::with_hasher(
559 DEFAULT_MAX_COUNT_FALLBACK_PEERS as u32,
560 Default::default(),
561 ),
562 tx_encoded_length: None,
563 })
564 .is_none()
565 {
566
567 trace!(target: "net::tx",
568 peer_id=format!("{peer_id:#}"),
569 %hash,
570 ?msg_version,
571 %client_version,
572 "failed to cache new announced hash from peer in schnellru::LruMap, dropping hash"
573 );
574
575 return false
576 }
577 true
578 });
579
580 trace!(target: "net::tx",
581 peer_id=format!("{peer_id:#}"),
582 previously_unseen_hashes_count=previously_unseen_hashes_count,
583 msg_version=?msg_version,
584 client_version=%client_version,
585 "received previously unseen hashes in announcement from peer"
586 );
587 }
588
589 pub fn request_transactions_from_peer(
597 &mut self,
598 new_announced_hashes: RequestTxHashes,
599 peer: &PeerMetadata<N>,
600 ) -> Option<RequestTxHashes> {
601 let peer_id: PeerId = peer.request_tx.peer_id;
602 let conn_eth_version = peer.version;
603
604 if self.active_peers.len() >= self.info.max_inflight_requests {
605 trace!(target: "net::tx",
606 peer_id=format!("{peer_id:#}"),
607 hashes=?*new_announced_hashes,
608 %conn_eth_version,
609 max_inflight_transaction_requests=self.info.max_inflight_requests,
610 "limit for concurrent `GetPooledTransactions` requests reached, dropping request for hashes to peer"
611 );
612 return Some(new_announced_hashes)
613 }
614
615 let Some(inflight_count) = self.active_peers.get_or_insert(peer_id, || 0) else {
616 trace!(target: "net::tx",
617 peer_id=format!("{peer_id:#}"),
618 hashes=?*new_announced_hashes,
619 conn_eth_version=%conn_eth_version,
620 "failed to cache active peer in schnellru::LruMap, dropping request to peer"
621 );
622 return Some(new_announced_hashes)
623 };
624
625 if *inflight_count >= self.info.max_inflight_requests_per_peer {
626 trace!(target: "net::tx",
627 peer_id=format!("{peer_id:#}"),
628 hashes=?*new_announced_hashes,
629 %conn_eth_version,
630 max_concurrent_tx_reqs_per_peer=self.info.max_inflight_requests_per_peer,
631 "limit for concurrent `GetPooledTransactions` requests per peer reached"
632 );
633 return Some(new_announced_hashes)
634 }
635
636 #[cfg(debug_assertions)]
637 {
638 for hash in &new_announced_hashes {
639 if self.hashes_pending_fetch.contains(hash) {
640 tracing::debug!(target: "net::tx", "`{}` should have been taken out of buffer before packing in a request, breaks invariant `@hashes_pending_fetch` and `@inflight_requests`, `@hashes_fetch_inflight_and_pending_fetch` for `{}`: {:?}",
641 format!("{:?}", new_announced_hashes), format!("{:?}", new_announced_hashes),
643 new_announced_hashes.iter().map(|hash| {
644 let metadata = self.hashes_fetch_inflight_and_pending_fetch.get(hash);
645 (*hash, metadata.map(|m| (m.retries, m.tx_encoded_length)))
647 }).collect::<Vec<(TxHash, Option<(u8, Option<usize>)>)>>())
648 }
649 }
650 }
651
652 let (response, rx) = oneshot::channel();
653 let req = PeerRequest::GetPooledTransactions {
654 request: GetPooledTransactions(new_announced_hashes.iter().copied().collect()),
655 response,
656 };
657
658 if let Err(err) = peer.request_tx.try_send(req) {
660 return match err {
662 TrySendError::Full(_) | TrySendError::Closed(_) => {
663 self.metrics.egress_peer_channel_full.increment(1);
664 Some(new_announced_hashes)
665 }
666 }
667 }
668
669 *inflight_count += 1;
670 self.inflight_requests.push(GetPooledTxRequestFut::new(peer_id, new_announced_hashes, rx));
672
673 None
674 }
675
676 pub fn fill_request_from_hashes_pending_fetch(
696 &mut self,
697 hashes_to_request: &mut RequestTxHashes,
698 seen_hashes: &LruCache<TxHash, FbBuildHasher<32>>,
699 mut budget_fill_request: Option<usize>, ) {
701 let Some(hash) = hashes_to_request.iter().next() else { return };
702
703 let mut acc_size_response = self
704 .hashes_fetch_inflight_and_pending_fetch
705 .get(hash)
706 .and_then(|entry| entry.tx_encoded_len())
707 .unwrap_or(AVERAGE_BYTE_SIZE_TX_ENCODED);
708
709 if acc_size_response >=
711 DEFAULT_SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESPONSE_ON_FETCH_PENDING_HASHES
712 {
713 return
714 }
715
716 for hash in self.hashes_pending_fetch.iter() {
719 if !seen_hashes.contains(hash) {
721 continue
722 };
723
724 hashes_to_request.insert(*hash);
726
727 let size = self
729 .hashes_fetch_inflight_and_pending_fetch
730 .get(hash)
731 .and_then(|entry| entry.tx_encoded_len())
732 .unwrap_or(AVERAGE_BYTE_SIZE_TX_ENCODED);
733
734 acc_size_response = acc_size_response.saturating_add(size);
735
736 if acc_size_response >=
740 DEFAULT_SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESPONSE_ON_FETCH_PENDING_HASHES ||
741 hashes_to_request.len() >
742 DEFAULT_SOFT_LIMIT_COUNT_HASHES_IN_GET_POOLED_TRANSACTIONS_REQUEST_ON_FETCH_PENDING_HASHES
743 {
744 break
745 }
746
747 if let Some(ref mut bud) = budget_fill_request {
748 *bud -= 1;
749 if *bud == 0 {
750 break
751 }
752 }
753 }
754
755 for hash in hashes_to_request.iter() {
757 self.hashes_pending_fetch.remove(hash);
758 }
759 }
760
761 pub fn has_capacity_for_fetching_pending_hashes(&self) -> bool {
764 let info = &self.info;
765
766 self.has_capacity(info.max_inflight_requests)
767 }
768
769 fn has_capacity(&self, max_inflight_requests: usize) -> bool {
771 self.inflight_requests.len() <= max_inflight_requests
772 }
773
774 pub fn search_breadth_budget_find_idle_fallback_peer(
780 &self,
781 has_capacity_wrt_pending_pool_imports: impl Fn(usize) -> bool,
782 ) -> Option<usize> {
783 let info = &self.info;
784
785 let tx_fetcher_has_capacity = self.has_capacity(
786 info.max_inflight_requests /
787 DEFAULT_DIVISOR_MAX_COUNT_INFLIGHT_REQUESTS_ON_FIND_IDLE_PEER,
788 );
789 let tx_pool_has_capacity = has_capacity_wrt_pending_pool_imports(
790 DEFAULT_DIVISOR_MAX_COUNT_PENDING_POOL_IMPORTS_ON_FIND_IDLE_PEER,
791 );
792
793 if tx_fetcher_has_capacity && tx_pool_has_capacity {
794 None
796 } else {
797 let limit = DEFAULT_BUDGET_FIND_IDLE_FALLBACK_PEER;
799
800 trace!(target: "net::tx",
801 inflight_requests=self.inflight_requests.len(),
802 max_inflight_transaction_requests=info.max_inflight_requests,
803 hashes_pending_fetch=self.hashes_pending_fetch.len(),
804 limit,
805 "search breadth limited in search for idle fallback peer for some hash pending fetch"
806 );
807
808 Some(limit)
809 }
810 }
811
812 pub fn search_breadth_budget_find_intersection_pending_hashes_and_hashes_seen_by_peer(
819 &self,
820 has_capacity_wrt_pending_pool_imports: impl Fn(usize) -> bool,
821 ) -> Option<usize> {
822 let info = &self.info;
823
824 let tx_fetcher_has_capacity = self.has_capacity(
825 info.max_inflight_requests /
826 DEFAULT_DIVISOR_MAX_COUNT_INFLIGHT_REQUESTS_ON_FIND_INTERSECTION,
827 );
828 let tx_pool_has_capacity = has_capacity_wrt_pending_pool_imports(
829 DEFAULT_DIVISOR_MAX_COUNT_PENDING_POOL_IMPORTS_ON_FIND_INTERSECTION,
830 );
831
832 if tx_fetcher_has_capacity && tx_pool_has_capacity {
833 None
835 } else {
836 let limit = DEFAULT_BUDGET_FIND_INTERSECTION_ANNOUNCED_BY_PEER_AND_PENDING_FETCH;
838
839 trace!(target: "net::tx",
840 inflight_requests=self.inflight_requests.len(),
841 max_inflight_transaction_requests=self.info.max_inflight_requests,
842 hashes_pending_fetch=self.hashes_pending_fetch.len(),
843 limit=limit,
844 "search breadth limited in search for intersection of hashes announced by peer and hashes pending fetch"
845 );
846
847 Some(limit)
848 }
849 }
850
851 pub fn on_resolved_get_pooled_transactions_request_fut(
855 &mut self,
856 response: GetPooledTxResponse<N::PooledTransaction>,
857 ) -> FetchEvent<N::PooledTransaction> {
858 let GetPooledTxResponse { peer_id, mut requested_hashes, result } = response;
861
862 self.decrement_inflight_request_count_for(&peer_id);
863
864 match result {
865 Ok(Ok(transactions)) => {
866 if transactions.is_empty() {
871 trace!(target: "net::tx",
872 peer_id=format!("{peer_id:#}"),
873 requested_hashes_len=requested_hashes.len(),
874 "received empty `PooledTransactions` response from peer, peer failed to serve hashes it announced"
875 );
876
877 return FetchEvent::EmptyResponse { peer_id }
878 }
879
880 let payload = UnverifiedPooledTransactions::new(transactions);
884
885 let unverified_len = payload.len();
886 let (verification_outcome, verified_payload) =
887 payload.verify(&requested_hashes, &peer_id);
888
889 let unsolicited = unverified_len - verified_payload.len();
890 if unsolicited > 0 {
891 self.metrics.unsolicited_transactions.increment(unsolicited as u64);
892 }
893
894 let report_peer = if verification_outcome == VerificationOutcome::ReportPeer {
895 trace!(target: "net::tx",
896 peer_id=format!("{peer_id:#}"),
897 unverified_len,
898 verified_payload_len=verified_payload.len(),
899 "received `PooledTransactions` response from peer with entries that didn't verify against request, filtered out transactions"
900 );
901 true
902 } else {
903 false
904 };
905
906 if verified_payload.is_empty() {
908 return FetchEvent::FetchError { peer_id, error: RequestError::BadResponse }
909 }
910
911 let unvalidated_payload_len = verified_payload.len();
915
916 let valid_payload = verified_payload.dedup();
917
918 if valid_payload.len() != unvalidated_payload_len {
924 trace!(target: "net::tx",
925 peer_id=format!("{peer_id:#}"),
926 unvalidated_payload_len,
927 valid_payload_len=valid_payload.len(),
928 "received `PooledTransactions` response from peer with duplicate entries, filtered them out"
929 );
930 }
931 let requested_hashes_len = requested_hashes.len();
939 let mut fetched = Vec::with_capacity(valid_payload.len());
940 requested_hashes.retain(|requested_hash| {
941 if valid_payload.contains_key(requested_hash) {
942 fetched.push(*requested_hash);
944 return false
945 }
946 true
947 });
948 fetched.shrink_to_fit();
949 self.metrics.fetched_transactions.increment(fetched.len() as u64);
950
951 if fetched.len() < requested_hashes_len {
952 trace!(target: "net::tx",
953 peer_id=format!("{peer_id:#}"),
954 requested_hashes_len=requested_hashes_len,
955 fetched_len=fetched.len(),
956 "peer failed to serve hashes it announced"
957 );
958 }
959
960 self.try_buffer_hashes_for_retry(requested_hashes, &peer_id);
964
965 let transactions = valid_payload.into_data().into_values().collect();
966
967 FetchEvent::TransactionsFetched { peer_id, transactions, report_peer }
968 }
969 Ok(Err(req_err)) => {
970 self.try_buffer_hashes_for_retry(requested_hashes, &peer_id);
971 FetchEvent::FetchError { peer_id, error: req_err }
972 }
973 Err(_) => {
974 self.try_buffer_hashes_for_retry(requested_hashes, &peer_id);
975 FetchEvent::FetchError { peer_id, error: RequestError::ChannelClosed }
977 }
978 }
979 }
980}
981
982impl<N: NetworkPrimitives> Stream for TransactionFetcher<N> {
983 type Item = FetchEvent<N::PooledTransaction>;
984
985 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
987 if self.inflight_requests.is_empty() {
990 return Poll::Pending
991 }
992
993 if let Some(resp) = ready!(self.inflight_requests.poll_next_unpin(cx)) {
994 return Poll::Ready(Some(self.on_resolved_get_pooled_transactions_request_fut(resp)))
995 }
996
997 Poll::Pending
998 }
999}
1000
1001impl<T: NetworkPrimitives> Default for TransactionFetcher<T> {
1002 fn default() -> Self {
1003 Self {
1004 active_peers: LruMap::with_hasher(
1005 DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS,
1006 Default::default(),
1007 ),
1008 inflight_requests: Default::default(),
1009 hashes_pending_fetch: LruCache::with_hasher(
1010 DEFAULT_MAX_CAPACITY_CACHE_PENDING_FETCH,
1011 Default::default(),
1012 ),
1013 hashes_fetch_inflight_and_pending_fetch: LruMap::with_hasher(
1014 DEFAULT_MAX_CAPACITY_CACHE_INFLIGHT_AND_PENDING_FETCH,
1015 Default::default(),
1016 ),
1017 info: TransactionFetcherInfo::default(),
1018 metrics: Default::default(),
1019 }
1020 }
1021}
1022
1023#[derive(Debug, Constructor)]
1025pub struct TxFetchMetadata {
1026 retries: u8,
1028 fallback_peers: LruCache<PeerId, FbBuildHasher<64>>,
1030 tx_encoded_length: Option<usize>,
1035}
1036
1037impl TxFetchMetadata {
1038 pub const fn fallback_peers_mut(&mut self) -> &mut LruCache<PeerId, FbBuildHasher<64>> {
1040 &mut self.fallback_peers
1041 }
1042
1043 pub const fn tx_encoded_len(&self) -> Option<usize> {
1048 self.tx_encoded_length
1049 }
1050}
1051
1052#[derive(Debug)]
1054pub enum FetchEvent<T = PooledTransaction> {
1055 TransactionsFetched {
1057 peer_id: PeerId,
1059 transactions: PooledTransactions<T>,
1061 report_peer: bool,
1064 },
1065 FetchError {
1067 peer_id: PeerId,
1069 error: RequestError,
1071 },
1072 EmptyResponse {
1074 peer_id: PeerId,
1076 },
1077}
1078
1079#[derive(Debug)]
1081pub struct GetPooledTxRequest<T = PooledTransaction> {
1082 peer_id: PeerId,
1083 requested_hashes: RequestTxHashes,
1085 response: oneshot::Receiver<RequestResult<PooledTransactions<T>>>,
1086}
1087
1088#[derive(Debug)]
1091pub struct GetPooledTxResponse<T = PooledTransaction> {
1092 peer_id: PeerId,
1093 requested_hashes: RequestTxHashes,
1096 result: Result<RequestResult<PooledTransactions<T>>, RecvError>,
1097}
1098
1099#[must_use = "futures do nothing unless polled"]
1102#[pin_project::pin_project]
1103#[derive(Debug)]
1104pub struct GetPooledTxRequestFut<T = PooledTransaction> {
1105 #[pin]
1106 inner: Option<GetPooledTxRequest<T>>,
1107}
1108
1109impl<T> GetPooledTxRequestFut<T> {
1110 #[inline]
1111 const fn new(
1112 peer_id: PeerId,
1113 requested_hashes: RequestTxHashes,
1114 response: oneshot::Receiver<RequestResult<PooledTransactions<T>>>,
1115 ) -> Self {
1116 Self { inner: Some(GetPooledTxRequest { peer_id, requested_hashes, response }) }
1117 }
1118}
1119
1120impl<T> Future for GetPooledTxRequestFut<T> {
1121 type Output = GetPooledTxResponse<T>;
1122
1123 fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
1124 let mut req = self.as_mut().project().inner.take().expect("polled after completion");
1125 match req.response.poll_unpin(cx) {
1126 Poll::Ready(result) => Poll::Ready(GetPooledTxResponse {
1127 peer_id: req.peer_id,
1128 requested_hashes: req.requested_hashes,
1129 result,
1130 }),
1131 Poll::Pending => {
1132 self.project().inner.set(Some(req));
1133 Poll::Pending
1134 }
1135 }
1136 }
1137}
1138
1139#[derive(Debug, Constructor, Deref)]
1141pub struct UnverifiedPooledTransactions<T> {
1142 txns: PooledTransactions<T>,
1143}
1144
1145#[derive(Debug, Constructor, Deref)]
1147pub struct VerifiedPooledTransactions<T> {
1148 txns: PooledTransactions<T>,
1149}
1150
1151impl<T: SignedTransaction> DedupPayload for VerifiedPooledTransactions<T> {
1152 type Value = T;
1153
1154 fn is_empty(&self) -> bool {
1155 self.txns.is_empty()
1156 }
1157
1158 fn len(&self) -> usize {
1159 self.txns.len()
1160 }
1161
1162 fn dedup(self) -> PartiallyValidData<Self::Value> {
1163 PartiallyValidData::from_raw_data(
1164 self.txns.into_iter().map(|tx| (*tx.tx_hash(), tx)).collect(),
1165 None,
1166 )
1167 }
1168}
1169
1170trait VerifyPooledTransactionsResponse {
1171 type Transaction: SignedTransaction;
1172
1173 fn verify(
1174 self,
1175 requested_hashes: &RequestTxHashes,
1176 peer_id: &PeerId,
1177 ) -> (VerificationOutcome, VerifiedPooledTransactions<Self::Transaction>);
1178}
1179
1180impl<T: SignedTransaction> VerifyPooledTransactionsResponse for UnverifiedPooledTransactions<T> {
1181 type Transaction = T;
1182
1183 fn verify(
1184 self,
1185 requested_hashes: &RequestTxHashes,
1186 _peer_id: &PeerId,
1187 ) -> (VerificationOutcome, VerifiedPooledTransactions<T>) {
1188 let mut verification_outcome = VerificationOutcome::Ok;
1189
1190 let Self { mut txns } = self;
1191
1192 #[cfg(debug_assertions)]
1193 let mut tx_hashes_not_requested: smallvec::SmallVec<[TxHash; 16]> = smallvec::smallvec!();
1194 #[cfg(not(debug_assertions))]
1195 let mut tx_hashes_not_requested_count = 0;
1196
1197 txns.0.retain(|tx| {
1198 if !requested_hashes.contains(tx.tx_hash()) {
1199 verification_outcome = VerificationOutcome::ReportPeer;
1200
1201 #[cfg(debug_assertions)]
1202 tx_hashes_not_requested.push(*tx.tx_hash());
1203 #[cfg(not(debug_assertions))]
1204 {
1205 tx_hashes_not_requested_count += 1;
1206 }
1207
1208 return false
1209 }
1210 true
1211 });
1212
1213 #[cfg(debug_assertions)]
1214 if !tx_hashes_not_requested.is_empty() {
1215 trace!(target: "net::tx",
1216 peer_id=format!("{_peer_id:#}"),
1217 ?tx_hashes_not_requested,
1218 "transactions in `PooledTransactions` response from peer were not requested"
1219 );
1220 }
1221 #[cfg(not(debug_assertions))]
1222 if tx_hashes_not_requested_count != 0 {
1223 trace!(target: "net::tx",
1224 peer_id=format!("{_peer_id:#}"),
1225 tx_hashes_not_requested_count,
1226 "transactions in `PooledTransactions` response from peer were not requested"
1227 );
1228 }
1229
1230 (verification_outcome, VerifiedPooledTransactions::new(txns))
1231 }
1232}
1233
1234#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1237pub enum VerificationOutcome {
1238 Ok,
1240 ReportPeer,
1243}
1244
1245#[derive(Debug, Constructor)]
1247pub struct TransactionFetcherInfo {
1248 pub max_inflight_requests: usize,
1250 pub max_inflight_requests_per_peer: u8,
1252 pub soft_limit_byte_size_pooled_transactions_response_on_pack_request: usize,
1256 pub soft_limit_byte_size_pooled_transactions_response: usize,
1259 pub max_capacity_cache_txns_pending_fetch: u32,
1263}
1264
1265impl Default for TransactionFetcherInfo {
1266 fn default() -> Self {
1267 Self::new(
1268 DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS as usize,
1269 DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS_PER_PEER,
1270 DEFAULT_SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESP_ON_PACK_GET_POOLED_TRANSACTIONS_REQ,
1271 SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESPONSE,
1272 DEFAULT_MAX_CAPACITY_CACHE_PENDING_FETCH,
1273 )
1274 }
1275}
1276
1277impl From<TransactionFetcherConfig> for TransactionFetcherInfo {
1278 fn from(config: TransactionFetcherConfig) -> Self {
1279 let TransactionFetcherConfig {
1280 max_inflight_requests,
1281 max_inflight_requests_per_peer,
1282 soft_limit_byte_size_pooled_transactions_response,
1283 soft_limit_byte_size_pooled_transactions_response_on_pack_request,
1284 max_capacity_cache_txns_pending_fetch,
1285 } = config;
1286
1287 Self::new(
1288 max_inflight_requests as usize,
1289 max_inflight_requests_per_peer,
1290 soft_limit_byte_size_pooled_transactions_response_on_pack_request,
1291 soft_limit_byte_size_pooled_transactions_response,
1292 max_capacity_cache_txns_pending_fetch,
1293 )
1294 }
1295}
1296
1297#[derive(Debug, Default)]
1298struct TxFetcherSearchDurations {
1299 find_idle_peer: Duration,
1300 fill_request: Duration,
1301}
1302
1303#[cfg(test)]
1304mod test {
1305 use super::*;
1306 use crate::test_utils::transactions::{buffer_hash_to_tx_fetcher, new_mock_session};
1307 use alloy_primitives::{
1308 hex,
1309 map::{B256Map, B256Set, HashMap},
1310 B256,
1311 };
1312 use alloy_rlp::Decodable;
1313 use derive_more::IntoIterator;
1314 use reth_eth_wire_types::EthVersion;
1315 use reth_ethereum_primitives::TransactionSigned;
1316 use std::str::FromStr;
1317
1318 #[derive(IntoIterator)]
1319 struct TestValidAnnouncementData(Vec<(TxHash, Option<(u8, usize)>)>);
1320
1321 impl HandleMempoolData for TestValidAnnouncementData {
1322 fn is_empty(&self) -> bool {
1323 self.0.is_empty()
1324 }
1325
1326 fn len(&self) -> usize {
1327 self.0.len()
1328 }
1329
1330 fn retain_by_hash(&mut self, mut f: impl FnMut(&TxHash) -> bool) {
1331 self.0.retain(|(hash, _)| f(hash))
1332 }
1333 }
1334
1335 impl HandleVersionedMempoolData for TestValidAnnouncementData {
1336 fn msg_version(&self) -> EthVersion {
1337 EthVersion::Eth68
1338 }
1339 }
1340
1341 #[test]
1342 fn pack_eth68_request() {
1343 reth_tracing::init_test_tracing();
1344
1345 let tx_fetcher = &mut TransactionFetcher::<EthNetworkPrimitives>::default();
1348
1349 let eth68_hashes = [
1350 B256::from_slice(&[1; 32]),
1351 B256::from_slice(&[2; 32]),
1352 B256::from_slice(&[3; 32]),
1353 B256::from_slice(&[4; 32]),
1354 B256::from_slice(&[5; 32]),
1355 ];
1356 let eth68_sizes = [
1357 DEFAULT_SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESP_ON_PACK_GET_POOLED_TRANSACTIONS_REQ - MEDIAN_BYTE_SIZE_SMALL_LEGACY_TX_ENCODED - 1, DEFAULT_SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESP_ON_PACK_GET_POOLED_TRANSACTIONS_REQ, 2, 9,
1361 0,
1362 ];
1363
1364 let expected_request_hashes =
1365 [eth68_hashes[0], eth68_hashes[2]].into_iter().collect::<B256Set>();
1366
1367 let expected_surplus_hashes =
1368 [eth68_hashes[1], eth68_hashes[3], eth68_hashes[4]].into_iter().collect::<B256Set>();
1369
1370 let mut eth68_hashes_to_request = RequestTxHashes::with_capacity(3);
1371
1372 let valid_announcement_data = TestValidAnnouncementData(
1373 eth68_hashes
1374 .into_iter()
1375 .zip(eth68_sizes)
1376 .map(|(hash, size)| (hash, Some((0u8, size))))
1377 .collect::<Vec<_>>(),
1378 );
1379
1380 let surplus_eth68_hashes =
1383 tx_fetcher.pack_request_eth68(&mut eth68_hashes_to_request, valid_announcement_data);
1384
1385 let eth68_hashes_to_request = eth68_hashes_to_request.into_iter().collect::<B256Set>();
1386 let surplus_eth68_hashes = surplus_eth68_hashes.into_iter().collect::<B256Set>();
1387
1388 assert_eq!(expected_request_hashes, eth68_hashes_to_request);
1389 assert_eq!(expected_surplus_hashes, surplus_eth68_hashes);
1390 }
1391
1392 #[test]
1393 fn pack_eth68_request_does_not_overflow_announced_size() {
1394 reth_tracing::init_test_tracing();
1395
1396 let tx_fetcher = &mut TransactionFetcher::<EthNetworkPrimitives>::default();
1397
1398 let eth68_hashes =
1399 [B256::from_slice(&[1; 32]), B256::from_slice(&[2; 32]), B256::from_slice(&[3; 32])];
1400 let eth68_sizes = [
1401 DEFAULT_SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESP_ON_PACK_GET_POOLED_TRANSACTIONS_REQ - MEDIAN_BYTE_SIZE_SMALL_LEGACY_TX_ENCODED - 1,
1402 usize::MAX,
1403 2,
1404 ];
1405
1406 let expected_request_hashes =
1407 [eth68_hashes[0], eth68_hashes[2]].into_iter().collect::<B256Set>();
1408 let expected_surplus_hashes = std::iter::once(eth68_hashes[1]).collect::<B256Set>();
1409
1410 let mut eth68_hashes_to_request = RequestTxHashes::with_capacity(3);
1411 let valid_announcement_data = TestValidAnnouncementData(
1412 eth68_hashes
1413 .into_iter()
1414 .zip(eth68_sizes)
1415 .map(|(hash, size)| (hash, Some((0u8, size))))
1416 .collect::<Vec<_>>(),
1417 );
1418
1419 let surplus_eth68_hashes =
1420 tx_fetcher.pack_request_eth68(&mut eth68_hashes_to_request, valid_announcement_data);
1421
1422 let eth68_hashes_to_request = eth68_hashes_to_request.into_iter().collect::<B256Set>();
1423 let surplus_eth68_hashes = surplus_eth68_hashes.into_iter().collect::<B256Set>();
1424
1425 assert_eq!(expected_request_hashes, eth68_hashes_to_request);
1426 assert_eq!(expected_surplus_hashes, surplus_eth68_hashes);
1427 }
1428
1429 #[test]
1430 fn pack_eth72_request_uses_metadata_size_limit() {
1431 reth_tracing::init_test_tracing();
1432
1433 let tx_fetcher = &mut TransactionFetcher::<EthNetworkPrimitives>::default();
1434
1435 let hashes =
1436 [B256::from_slice(&[1; 32]), B256::from_slice(&[2; 32]), B256::from_slice(&[3; 32])];
1437 let announced_size =
1438 DEFAULT_SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESP_ON_PACK_GET_POOLED_TRANSACTIONS_REQ;
1439 let announcement_data = hashes
1440 .into_iter()
1441 .map(|hash| (hash, Some((0u8, announced_size))))
1442 .collect::<B256Map<_>>();
1443 let valid_announcement_data = ValidAnnouncementData::from_partially_valid_data(
1444 PartiallyValidData::from_raw_data_eth72(announcement_data),
1445 );
1446
1447 let mut hashes_to_request = RequestTxHashes::with_capacity(3);
1448 let surplus_hashes =
1449 tx_fetcher.pack_request(&mut hashes_to_request, valid_announcement_data);
1450
1451 assert_eq!(1, hashes_to_request.len());
1452 assert_eq!(2, surplus_hashes.len());
1453 }
1454
1455 #[tokio::test]
1456 async fn test_on_fetch_pending_hashes() {
1457 reth_tracing::init_test_tracing();
1458
1459 let tx_fetcher = &mut TransactionFetcher::default();
1460
1461 let seen_hashes = [
1465 B256::from_slice(&[1; 32]),
1466 B256::from_slice(&[2; 32]),
1467 B256::from_slice(&[3; 32]),
1468 B256::from_slice(&[4; 32]),
1469 ];
1470 let seen_eth68_hashes_sizes = [120, 158, 116];
1475
1476 let peer_1 = PeerId::new([1; 64]);
1478 let peer_2 = PeerId::new([2; 64]);
1480
1481 let (mut peer_1_data, mut peer_1_mock_session_rx) =
1485 new_mock_session(peer_1, EthVersion::Eth66);
1486 for hash in &seen_hashes {
1487 peer_1_data.seen_transactions.insert(*hash);
1488 }
1489 let (mut peer_2_data, _) = new_mock_session(peer_2, EthVersion::Eth66);
1490 for hash in &seen_hashes {
1491 peer_2_data.seen_transactions.insert(*hash);
1492 }
1493 let mut peers: HashMap<PeerId, _, FbBuildHasher<64>> = HashMap::default();
1494 peers.insert(peer_1, peer_1_data);
1495 peers.insert(peer_2, peer_2_data);
1496
1497 for i in 0..3 {
1499 buffer_hash_to_tx_fetcher(
1501 tx_fetcher,
1502 seen_hashes[i],
1503 peer_2,
1504 0,
1505 Some(seen_eth68_hashes_sizes[i]),
1506 );
1507 }
1508 buffer_hash_to_tx_fetcher(tx_fetcher, seen_hashes[3], peer_2, 0, None);
1509
1510 let hash_other = B256::from_slice(&[5; 32]);
1512 buffer_hash_to_tx_fetcher(tx_fetcher, hash_other, peer_2, 0, None);
1513
1514 for hash in &seen_hashes {
1516 buffer_hash_to_tx_fetcher(tx_fetcher, *hash, peer_1, 0, None);
1517 }
1518
1519 assert_eq!(tx_fetcher.num_pending_hashes(), 5);
1521
1522 tx_fetcher.on_fetch_pending_hashes(&peers, |_| true);
1525
1526 let req = peer_1_mock_session_rx
1528 .recv()
1529 .await
1530 .expect("peer session should receive request with buffered hashes");
1531 let PeerRequest::GetPooledTransactions { request, .. } = req else { unreachable!() };
1532 let GetPooledTransactions(requested_hashes) = request;
1533
1534 assert_eq!(
1535 requested_hashes.into_iter().collect::<B256Set>(),
1536 seen_hashes.into_iter().collect::<B256Set>()
1537 )
1538 }
1539
1540 #[test]
1541 fn on_fetch_pending_hashes_rebuffers_on_disconnected_peer() {
1542 let tx_fetcher = &mut TransactionFetcher::default();
1543 let peer_1 = PeerId::new([1; 64]);
1544 let peer_2 = PeerId::new([2; 64]);
1545 let hash_1 = B256::from_slice(&[1; 32]);
1546
1547 buffer_hash_to_tx_fetcher(tx_fetcher, hash_1, peer_1, 0, Some(128));
1548 buffer_hash_to_tx_fetcher(tx_fetcher, hash_1, peer_2, 0, Some(128));
1549
1550 assert_eq!(tx_fetcher.num_pending_hashes(), 1);
1551
1552 let peers: HashMap<PeerId, PeerMetadata, FbBuildHasher<64>> = HashMap::default();
1554 tx_fetcher.on_fetch_pending_hashes(&peers, |_| true);
1555
1556 assert_eq!(tx_fetcher.num_pending_hashes(), 1);
1558 }
1559
1560 #[test]
1561 fn verify_response_hashes() {
1562 let input = hex!(
1563 "02f871018302a90f808504890aef60826b6c94ddf4c5025d1a5742cf12f74eec246d4432c295e487e09c3bbcc12b2b80c080a0f21a4eacd0bf8fea9c5105c543be5a1d8c796516875710fafafdf16d16d8ee23a001280915021bb446d1973501a67f93d2b38894a514b976e7b46dc2fe54598daa"
1564 );
1565 let signed_tx_1: PooledTransaction =
1566 TransactionSigned::decode(&mut &input[..]).unwrap().try_into().unwrap();
1567 let input = hex!(
1568 "02f871018302a90f808504890aef60826b6c94ddf4c5025d1a5742cf12f74eec246d4432c295e487e09c3bbcc12b2b80c080a0f21a4eacd0bf8fea9c5105c543be5a1d8c796516875710fafafdf16d16d8ee23a001280915021bb446d1973501a67f93d2b38894a514b976e7b46dc2fe54598d76"
1569 );
1570 let signed_tx_2: PooledTransaction =
1571 TransactionSigned::decode(&mut &input[..]).unwrap().try_into().unwrap();
1572
1573 let request_hashes = [
1575 B256::from_str("0x3b9aca00f0671c9a2a1b817a0a78d3fe0c0f776cccb2a8c3c1b412a4f4e67890")
1576 .unwrap(),
1577 *signed_tx_1.hash(),
1578 B256::from_str("0x3b9aca00f0671c9a2a1b817a0a78d3fe0c0f776cccb2a8c3c1b412a4f4e12345")
1579 .unwrap(),
1580 B256::from_str("0x3b9aca00f0671c9a2a1b817a0a78d3fe0c0f776cccb2a8c3c1b412a4f4edabe3")
1581 .unwrap(),
1582 ];
1583
1584 for hash in &request_hashes {
1585 assert_ne!(hash, signed_tx_2.hash())
1586 }
1587
1588 let request_hashes = RequestTxHashes::new(request_hashes.into_iter().collect());
1589
1590 let response_txns = PooledTransactions(vec![signed_tx_1.clone(), signed_tx_2]);
1592 let payload = UnverifiedPooledTransactions::new(response_txns);
1593
1594 let (outcome, verified_payload) = payload.verify(&request_hashes, &PeerId::ZERO);
1595
1596 assert_eq!(VerificationOutcome::ReportPeer, outcome);
1597 assert_eq!(1, verified_payload.len());
1598 assert!(verified_payload.contains(&signed_tx_1));
1599 }
1600}