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 if *inflight_count < self.info.max_inflight_requests_per_peer {
188 return true
189 }
190 false
191 }
192
193 pub fn get_idle_peer_for(&self, hash: TxHash) -> Option<&PeerId> {
195 let TxFetchMetadata { fallback_peers, .. } =
196 self.hashes_fetch_inflight_and_pending_fetch.peek(&hash)?;
197
198 fallback_peers.iter().find(|peer_id| self.is_idle(peer_id))
199 }
200
201 pub fn find_any_idle_fallback_peer_for_any_pending_hash(
207 &mut self,
208 hashes_to_request: &mut RequestTxHashes,
209 mut budget: Option<usize>, ) -> Option<PeerId> {
211 let mut hashes_pending_fetch_iter = self.hashes_pending_fetch.iter();
212
213 let idle_peer = loop {
214 let &hash = hashes_pending_fetch_iter.next()?;
215
216 let idle_peer = self.get_idle_peer_for(hash);
217
218 if idle_peer.is_some() {
219 hashes_to_request.insert(hash);
220 break idle_peer.copied()
221 }
222
223 if let Some(ref mut bud) = budget {
224 *bud = bud.saturating_sub(1);
225 if *bud == 0 {
226 return None
227 }
228 }
229 };
230 let hash = hashes_to_request.iter().next()?;
231
232 drop(hashes_pending_fetch_iter);
234 _ = self.hashes_pending_fetch.remove(hash);
235
236 idle_peer
237 }
238
239 pub fn pack_request(
244 &self,
245 hashes_to_request: &mut RequestTxHashes,
246 hashes_from_announcement: ValidAnnouncementData,
247 ) -> RequestTxHashes {
248 if hashes_from_announcement.msg_version().has_eth68_metadata() {
249 return self.pack_request_eth68(hashes_to_request, hashes_from_announcement)
250 }
251 self.pack_request_eth66(hashes_to_request, hashes_from_announcement)
252 }
253
254 pub fn pack_request_eth68(
265 &self,
266 hashes_to_request: &mut RequestTxHashes,
267 hashes_from_announcement: impl HandleMempoolData
268 + IntoIterator<Item = (TxHash, Option<(u8, usize)>)>,
269 ) -> RequestTxHashes {
270 let mut acc_size_response = 0;
271
272 let mut hashes_from_announcement_iter = hashes_from_announcement.into_iter();
273
274 if let Some((hash, Some((_ty, size)))) = hashes_from_announcement_iter.next() {
275 hashes_to_request.insert(hash);
276
277 if size >= self.info.soft_limit_byte_size_pooled_transactions_response_on_pack_request {
279 return hashes_from_announcement_iter.collect()
280 }
281 acc_size_response = size;
282 }
283
284 let mut surplus_hashes = RequestTxHashes::default();
285
286 for (hash, metadata) in hashes_from_announcement_iter.by_ref() {
289 let Some((_ty, size)) = metadata else {
290 unreachable!("this method is called upon reception of an eth68 announcement")
291 };
292
293 let next_acc_size = acc_size_response.checked_add(size).filter(|next_acc_size| {
294 *next_acc_size <=
295 self.info.soft_limit_byte_size_pooled_transactions_response_on_pack_request
296 });
297
298 if let Some(next_acc_size) = next_acc_size {
299 acc_size_response = next_acc_size;
302 _ = hashes_to_request.insert(hash)
303 } else {
304 _ = surplus_hashes.insert(hash)
305 }
306
307 let free_space =
308 self.info.soft_limit_byte_size_pooled_transactions_response_on_pack_request -
309 acc_size_response;
310
311 if free_space < MEDIAN_BYTE_SIZE_SMALL_LEGACY_TX_ENCODED {
312 break
313 }
314 }
315
316 surplus_hashes.extend(hashes_from_announcement_iter.map(|(hash, _metadata)| hash));
317
318 surplus_hashes
319 }
320
321 pub fn pack_request_eth66(
328 &self,
329 hashes_to_request: &mut RequestTxHashes,
330 hashes_from_announcement: ValidAnnouncementData,
331 ) -> RequestTxHashes {
332 let (mut hashes, _version) = hashes_from_announcement.into_request_hashes();
333 if hashes.len() <= SOFT_LIMIT_COUNT_HASHES_IN_GET_POOLED_TRANSACTIONS_REQUEST {
334 *hashes_to_request = hashes;
335 hashes_to_request.shrink_to_fit();
336
337 RequestTxHashes::default()
338 } else {
339 let surplus_hashes =
340 hashes.retain_count(SOFT_LIMIT_COUNT_HASHES_IN_GET_POOLED_TRANSACTIONS_REQUEST);
341 *hashes_to_request = hashes;
342 hashes_to_request.shrink_to_fit();
343
344 surplus_hashes
345 }
346 }
347
348 pub fn try_buffer_hashes_for_retry(
350 &mut self,
351 mut hashes: RequestTxHashes,
352 peer_failed_to_serve: &PeerId,
353 ) {
354 hashes.retain(|hash| {
357 if let Some(entry) = self.hashes_fetch_inflight_and_pending_fetch.get(hash) {
358 entry.fallback_peers_mut().remove(peer_failed_to_serve);
359 return true
360 }
361 false
363 });
364
365 self.buffer_hashes(hashes, None)
366 }
367
368 pub fn num_pending_hashes(&self) -> usize {
370 self.hashes_pending_fetch.len()
371 }
372
373 pub fn num_all_hashes(&self) -> usize {
375 self.hashes_fetch_inflight_and_pending_fetch.len()
376 }
377
378 pub fn buffer_hashes(&mut self, hashes: RequestTxHashes, fallback_peer: Option<PeerId>) {
383 for hash in hashes {
384 let Some(TxFetchMetadata { retries, fallback_peers, .. }) =
386 self.hashes_fetch_inflight_and_pending_fetch.get(&hash)
387 else {
388 continue
389 };
390
391 if let Some(peer_id) = fallback_peer {
392 fallback_peers.insert(peer_id);
394 } else {
395 if *retries >= DEFAULT_MAX_RETRIES {
396 trace!(target: "net::tx",
397 %hash,
398 retries,
399 "retry limit for `GetPooledTransactions` requests reached for hash, dropping hash"
400 );
401
402 self.hashes_fetch_inflight_and_pending_fetch.remove(&hash);
403 self.hashes_pending_fetch.remove(&hash);
404 continue
405 }
406 *retries += 1;
407 }
408
409 if let (_, Some(evicted_hash)) = self.hashes_pending_fetch.insert_and_get_evicted(hash)
410 {
411 self.hashes_fetch_inflight_and_pending_fetch.remove(&evicted_hash);
412 }
413 }
414 }
415
416 pub fn on_fetch_pending_hashes(
421 &mut self,
422 peers: &HashMap<PeerId, PeerMetadata<N>, FbBuildHasher<64>>,
423 has_capacity_wrt_pending_pool_imports: impl Fn(usize) -> bool,
424 ) -> bool {
425 let mut hashes_to_request = RequestTxHashes::with_capacity(
426 DEFAULT_MARGINAL_COUNT_HASHES_GET_POOLED_TRANSACTIONS_REQUEST,
427 );
428 let mut search_durations = TxFetcherSearchDurations::default();
429
430 let budget_find_idle_fallback_peer = self
432 .search_breadth_budget_find_idle_fallback_peer(&has_capacity_wrt_pending_pool_imports);
433
434 let peer_id = duration_metered_exec!(
435 {
436 let Some(peer_id) = self.find_any_idle_fallback_peer_for_any_pending_hash(
437 &mut hashes_to_request,
438 budget_find_idle_fallback_peer,
439 ) else {
440 return false
442 };
443
444 peer_id
445 },
446 search_durations.find_idle_peer
447 );
448
449 let Some(peer) = peers.get(&peer_id) else {
452 self.buffer_hashes(hashes_to_request, None);
453 return false
454 };
455 let conn_eth_version = peer.version;
456
457 let budget_fill_request = self
462 .search_breadth_budget_find_intersection_pending_hashes_and_hashes_seen_by_peer(
463 &has_capacity_wrt_pending_pool_imports,
464 );
465
466 duration_metered_exec!(
467 {
468 self.fill_request_from_hashes_pending_fetch(
469 &mut hashes_to_request,
470 &peer.seen_transactions,
471 budget_fill_request,
472 )
473 },
474 search_durations.fill_request
475 );
476
477 self.update_pending_fetch_cache_search_metrics(search_durations);
478
479 trace!(target: "net::tx",
480 peer_id=format!("{peer_id:#}"),
481 hashes=?*hashes_to_request,
482 %conn_eth_version,
483 "requesting hashes that were stored pending fetch from peer"
484 );
485
486 if let Some(failed_to_request_hashes) =
488 self.request_transactions_from_peer(hashes_to_request, peer)
489 {
490 trace!(target: "net::tx",
491 peer_id=format!("{peer_id:#}"),
492 ?failed_to_request_hashes,
493 %conn_eth_version,
494 "failed sending request to peer's session, buffering hashes"
495 );
496
497 self.buffer_hashes(failed_to_request_hashes, Some(peer_id));
498 return false
499 }
500
501 true
502 }
503
504 pub fn filter_unseen_and_pending_hashes(
507 &mut self,
508 new_announced_hashes: &mut ValidAnnouncementData,
509 is_tx_bad_import: impl Fn(&TxHash) -> bool,
510 peer_id: &PeerId,
511 client_version: &str,
512 ) {
513 let mut previously_unseen_hashes_count = 0;
514
515 let msg_version = new_announced_hashes.msg_version();
516
517 new_announced_hashes.retain(|hash, metadata| {
519
520 if let Some(TxFetchMetadata{ tx_encoded_length: previously_seen_size, ..}) = self.hashes_fetch_inflight_and_pending_fetch.peek_mut(hash) {
522 if let Some((_ty, size)) = metadata {
524 if let Some(prev_size) = previously_seen_size {
525 if size != prev_size {
527 trace!(target: "net::tx",
528 peer_id=format!("{peer_id:#}"),
529 %hash,
530 size,
531 previously_seen_size,
532 %client_version,
533 "peer announced a different size for tx, this is especially worrying if one size is much bigger..."
534 );
535 }
536 }
537 *previously_seen_size = Some(*size);
539 }
540
541 if self.hashes_pending_fetch.remove(hash) {
543 return true
544 }
545
546 return false
547 }
548
549 if is_tx_bad_import(hash) {
552 return false
553 }
554
555 previously_unseen_hashes_count += 1;
556
557 if self
558 .hashes_fetch_inflight_and_pending_fetch
559 .get_or_insert(*hash, || TxFetchMetadata {
560 retries: 0,
561 fallback_peers: LruCache::with_hasher(
562 DEFAULT_MAX_COUNT_FALLBACK_PEERS as u32,
563 Default::default(),
564 ),
565 tx_encoded_length: None,
566 })
567 .is_none()
568 {
569
570 trace!(target: "net::tx",
571 peer_id=format!("{peer_id:#}"),
572 %hash,
573 ?msg_version,
574 %client_version,
575 "failed to cache new announced hash from peer in schnellru::LruMap, dropping hash"
576 );
577
578 return false
579 }
580 true
581 });
582
583 trace!(target: "net::tx",
584 peer_id=format!("{peer_id:#}"),
585 previously_unseen_hashes_count=previously_unseen_hashes_count,
586 msg_version=?msg_version,
587 client_version=%client_version,
588 "received previously unseen hashes in announcement from peer"
589 );
590 }
591
592 pub fn request_transactions_from_peer(
600 &mut self,
601 new_announced_hashes: RequestTxHashes,
602 peer: &PeerMetadata<N>,
603 ) -> Option<RequestTxHashes> {
604 let peer_id: PeerId = peer.request_tx.peer_id;
605 let conn_eth_version = peer.version;
606
607 if self.active_peers.len() >= self.info.max_inflight_requests {
608 trace!(target: "net::tx",
609 peer_id=format!("{peer_id:#}"),
610 hashes=?*new_announced_hashes,
611 %conn_eth_version,
612 max_inflight_transaction_requests=self.info.max_inflight_requests,
613 "limit for concurrent `GetPooledTransactions` requests reached, dropping request for hashes to peer"
614 );
615 return Some(new_announced_hashes)
616 }
617
618 let Some(inflight_count) = self.active_peers.get_or_insert(peer_id, || 0) else {
619 trace!(target: "net::tx",
620 peer_id=format!("{peer_id:#}"),
621 hashes=?*new_announced_hashes,
622 conn_eth_version=%conn_eth_version,
623 "failed to cache active peer in schnellru::LruMap, dropping request to peer"
624 );
625 return Some(new_announced_hashes)
626 };
627
628 if *inflight_count >= self.info.max_inflight_requests_per_peer {
629 trace!(target: "net::tx",
630 peer_id=format!("{peer_id:#}"),
631 hashes=?*new_announced_hashes,
632 %conn_eth_version,
633 max_concurrent_tx_reqs_per_peer=self.info.max_inflight_requests_per_peer,
634 "limit for concurrent `GetPooledTransactions` requests per peer reached"
635 );
636 return Some(new_announced_hashes)
637 }
638
639 #[cfg(debug_assertions)]
640 {
641 for hash in &new_announced_hashes {
642 if self.hashes_pending_fetch.contains(hash) {
643 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 `{}`: {:?}",
644 format!("{:?}", new_announced_hashes), format!("{:?}", new_announced_hashes),
646 new_announced_hashes.iter().map(|hash| {
647 let metadata = self.hashes_fetch_inflight_and_pending_fetch.get(hash);
648 (*hash, metadata.map(|m| (m.retries, m.tx_encoded_length)))
650 }).collect::<Vec<(TxHash, Option<(u8, Option<usize>)>)>>())
651 }
652 }
653 }
654
655 let (response, rx) = oneshot::channel();
656 let req = PeerRequest::GetPooledTransactions {
657 request: GetPooledTransactions(new_announced_hashes.iter().copied().collect()),
658 response,
659 };
660
661 if let Err(err) = peer.request_tx.try_send(req) {
663 return match err {
665 TrySendError::Full(_) | TrySendError::Closed(_) => {
666 self.metrics.egress_peer_channel_full.increment(1);
667 Some(new_announced_hashes)
668 }
669 }
670 }
671
672 *inflight_count += 1;
673 self.inflight_requests.push(GetPooledTxRequestFut::new(peer_id, new_announced_hashes, rx));
675
676 None
677 }
678
679 pub fn fill_request_from_hashes_pending_fetch(
699 &mut self,
700 hashes_to_request: &mut RequestTxHashes,
701 seen_hashes: &LruCache<TxHash, FbBuildHasher<32>>,
702 mut budget_fill_request: Option<usize>, ) {
704 let Some(hash) = hashes_to_request.iter().next() else { return };
705
706 let mut acc_size_response = self
707 .hashes_fetch_inflight_and_pending_fetch
708 .get(hash)
709 .and_then(|entry| entry.tx_encoded_len())
710 .unwrap_or(AVERAGE_BYTE_SIZE_TX_ENCODED);
711
712 if acc_size_response >=
714 DEFAULT_SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESPONSE_ON_FETCH_PENDING_HASHES
715 {
716 return
717 }
718
719 for hash in self.hashes_pending_fetch.iter() {
722 if !seen_hashes.contains(hash) {
724 continue
725 };
726
727 hashes_to_request.insert(*hash);
729
730 let size = self
732 .hashes_fetch_inflight_and_pending_fetch
733 .get(hash)
734 .and_then(|entry| entry.tx_encoded_len())
735 .unwrap_or(AVERAGE_BYTE_SIZE_TX_ENCODED);
736
737 acc_size_response = acc_size_response.saturating_add(size);
738
739 if acc_size_response >=
743 DEFAULT_SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESPONSE_ON_FETCH_PENDING_HASHES ||
744 hashes_to_request.len() >
745 DEFAULT_SOFT_LIMIT_COUNT_HASHES_IN_GET_POOLED_TRANSACTIONS_REQUEST_ON_FETCH_PENDING_HASHES
746 {
747 break
748 }
749
750 if let Some(ref mut bud) = budget_fill_request {
751 *bud -= 1;
752 if *bud == 0 {
753 break
754 }
755 }
756 }
757
758 for hash in hashes_to_request.iter() {
760 self.hashes_pending_fetch.remove(hash);
761 }
762 }
763
764 pub fn has_capacity_for_fetching_pending_hashes(&self) -> bool {
767 let info = &self.info;
768
769 self.has_capacity(info.max_inflight_requests)
770 }
771
772 fn has_capacity(&self, max_inflight_requests: usize) -> bool {
774 self.inflight_requests.len() <= max_inflight_requests
775 }
776
777 pub fn search_breadth_budget_find_idle_fallback_peer(
783 &self,
784 has_capacity_wrt_pending_pool_imports: impl Fn(usize) -> bool,
785 ) -> Option<usize> {
786 let info = &self.info;
787
788 let tx_fetcher_has_capacity = self.has_capacity(
789 info.max_inflight_requests /
790 DEFAULT_DIVISOR_MAX_COUNT_INFLIGHT_REQUESTS_ON_FIND_IDLE_PEER,
791 );
792 let tx_pool_has_capacity = has_capacity_wrt_pending_pool_imports(
793 DEFAULT_DIVISOR_MAX_COUNT_PENDING_POOL_IMPORTS_ON_FIND_IDLE_PEER,
794 );
795
796 if tx_fetcher_has_capacity && tx_pool_has_capacity {
797 None
799 } else {
800 let limit = DEFAULT_BUDGET_FIND_IDLE_FALLBACK_PEER;
802
803 trace!(target: "net::tx",
804 inflight_requests=self.inflight_requests.len(),
805 max_inflight_transaction_requests=info.max_inflight_requests,
806 hashes_pending_fetch=self.hashes_pending_fetch.len(),
807 limit,
808 "search breadth limited in search for idle fallback peer for some hash pending fetch"
809 );
810
811 Some(limit)
812 }
813 }
814
815 pub fn search_breadth_budget_find_intersection_pending_hashes_and_hashes_seen_by_peer(
822 &self,
823 has_capacity_wrt_pending_pool_imports: impl Fn(usize) -> bool,
824 ) -> Option<usize> {
825 let info = &self.info;
826
827 let tx_fetcher_has_capacity = self.has_capacity(
828 info.max_inflight_requests /
829 DEFAULT_DIVISOR_MAX_COUNT_INFLIGHT_REQUESTS_ON_FIND_INTERSECTION,
830 );
831 let tx_pool_has_capacity = has_capacity_wrt_pending_pool_imports(
832 DEFAULT_DIVISOR_MAX_COUNT_PENDING_POOL_IMPORTS_ON_FIND_INTERSECTION,
833 );
834
835 if tx_fetcher_has_capacity && tx_pool_has_capacity {
836 None
838 } else {
839 let limit = DEFAULT_BUDGET_FIND_INTERSECTION_ANNOUNCED_BY_PEER_AND_PENDING_FETCH;
841
842 trace!(target: "net::tx",
843 inflight_requests=self.inflight_requests.len(),
844 max_inflight_transaction_requests=self.info.max_inflight_requests,
845 hashes_pending_fetch=self.hashes_pending_fetch.len(),
846 limit=limit,
847 "search breadth limited in search for intersection of hashes announced by peer and hashes pending fetch"
848 );
849
850 Some(limit)
851 }
852 }
853
854 pub fn on_resolved_get_pooled_transactions_request_fut(
858 &mut self,
859 response: GetPooledTxResponse<N::PooledTransaction>,
860 ) -> FetchEvent<N::PooledTransaction> {
861 let GetPooledTxResponse { peer_id, mut requested_hashes, result } = response;
864
865 self.decrement_inflight_request_count_for(&peer_id);
866
867 match result {
868 Ok(Ok(transactions)) => {
869 if transactions.is_empty() {
874 trace!(target: "net::tx",
875 peer_id=format!("{peer_id:#}"),
876 requested_hashes_len=requested_hashes.len(),
877 "received empty `PooledTransactions` response from peer, peer failed to serve hashes it announced"
878 );
879
880 return FetchEvent::EmptyResponse { peer_id }
881 }
882
883 let payload = UnverifiedPooledTransactions::new(transactions);
887
888 let unverified_len = payload.len();
889 let (verification_outcome, verified_payload) =
890 payload.verify(&requested_hashes, &peer_id);
891
892 let unsolicited = unverified_len - verified_payload.len();
893 if unsolicited > 0 {
894 self.metrics.unsolicited_transactions.increment(unsolicited as u64);
895 }
896
897 let report_peer = if verification_outcome == VerificationOutcome::ReportPeer {
898 trace!(target: "net::tx",
899 peer_id=format!("{peer_id:#}"),
900 unverified_len,
901 verified_payload_len=verified_payload.len(),
902 "received `PooledTransactions` response from peer with entries that didn't verify against request, filtered out transactions"
903 );
904 true
905 } else {
906 false
907 };
908
909 if verified_payload.is_empty() {
911 return FetchEvent::FetchError { peer_id, error: RequestError::BadResponse }
912 }
913
914 let unvalidated_payload_len = verified_payload.len();
918
919 let valid_payload = verified_payload.dedup();
920
921 if valid_payload.len() != unvalidated_payload_len {
927 trace!(target: "net::tx",
928 peer_id=format!("{peer_id:#}"),
929 unvalidated_payload_len,
930 valid_payload_len=valid_payload.len(),
931 "received `PooledTransactions` response from peer with duplicate entries, filtered them out"
932 );
933 }
934 let requested_hashes_len = requested_hashes.len();
942 let mut fetched = Vec::with_capacity(valid_payload.len());
943 requested_hashes.retain(|requested_hash| {
944 if valid_payload.contains_key(requested_hash) {
945 fetched.push(*requested_hash);
947 return false
948 }
949 true
950 });
951 fetched.shrink_to_fit();
952 self.metrics.fetched_transactions.increment(fetched.len() as u64);
953
954 if fetched.len() < requested_hashes_len {
955 trace!(target: "net::tx",
956 peer_id=format!("{peer_id:#}"),
957 requested_hashes_len=requested_hashes_len,
958 fetched_len=fetched.len(),
959 "peer failed to serve hashes it announced"
960 );
961 }
962
963 self.try_buffer_hashes_for_retry(requested_hashes, &peer_id);
967
968 let transactions = valid_payload.into_data().into_values().collect();
969
970 FetchEvent::TransactionsFetched { peer_id, transactions, report_peer }
971 }
972 Ok(Err(req_err)) => {
973 self.try_buffer_hashes_for_retry(requested_hashes, &peer_id);
974 FetchEvent::FetchError { peer_id, error: req_err }
975 }
976 Err(_) => {
977 self.try_buffer_hashes_for_retry(requested_hashes, &peer_id);
978 FetchEvent::FetchError { peer_id, error: RequestError::ChannelClosed }
980 }
981 }
982 }
983}
984
985impl<N: NetworkPrimitives> Stream for TransactionFetcher<N> {
986 type Item = FetchEvent<N::PooledTransaction>;
987
988 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
990 if self.inflight_requests.is_empty() {
993 return Poll::Pending
994 }
995
996 if let Some(resp) = ready!(self.inflight_requests.poll_next_unpin(cx)) {
997 return Poll::Ready(Some(self.on_resolved_get_pooled_transactions_request_fut(resp)))
998 }
999
1000 Poll::Pending
1001 }
1002}
1003
1004impl<T: NetworkPrimitives> Default for TransactionFetcher<T> {
1005 fn default() -> Self {
1006 Self {
1007 active_peers: LruMap::with_hasher(
1008 DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS,
1009 Default::default(),
1010 ),
1011 inflight_requests: Default::default(),
1012 hashes_pending_fetch: LruCache::with_hasher(
1013 DEFAULT_MAX_CAPACITY_CACHE_PENDING_FETCH,
1014 Default::default(),
1015 ),
1016 hashes_fetch_inflight_and_pending_fetch: LruMap::with_hasher(
1017 DEFAULT_MAX_CAPACITY_CACHE_INFLIGHT_AND_PENDING_FETCH,
1018 Default::default(),
1019 ),
1020 info: TransactionFetcherInfo::default(),
1021 metrics: Default::default(),
1022 }
1023 }
1024}
1025
1026#[derive(Debug, Constructor)]
1028pub struct TxFetchMetadata {
1029 retries: u8,
1031 fallback_peers: LruCache<PeerId, FbBuildHasher<64>>,
1033 tx_encoded_length: Option<usize>,
1038}
1039
1040impl TxFetchMetadata {
1041 pub const fn fallback_peers_mut(&mut self) -> &mut LruCache<PeerId, FbBuildHasher<64>> {
1043 &mut self.fallback_peers
1044 }
1045
1046 pub const fn tx_encoded_len(&self) -> Option<usize> {
1051 self.tx_encoded_length
1052 }
1053}
1054
1055#[derive(Debug)]
1057pub enum FetchEvent<T = PooledTransaction> {
1058 TransactionsFetched {
1060 peer_id: PeerId,
1062 transactions: PooledTransactions<T>,
1064 report_peer: bool,
1067 },
1068 FetchError {
1070 peer_id: PeerId,
1072 error: RequestError,
1074 },
1075 EmptyResponse {
1077 peer_id: PeerId,
1079 },
1080}
1081
1082#[derive(Debug)]
1084pub struct GetPooledTxRequest<T = PooledTransaction> {
1085 peer_id: PeerId,
1086 requested_hashes: RequestTxHashes,
1088 response: oneshot::Receiver<RequestResult<PooledTransactions<T>>>,
1089}
1090
1091#[derive(Debug)]
1094pub struct GetPooledTxResponse<T = PooledTransaction> {
1095 peer_id: PeerId,
1096 requested_hashes: RequestTxHashes,
1099 result: Result<RequestResult<PooledTransactions<T>>, RecvError>,
1100}
1101
1102#[must_use = "futures do nothing unless polled"]
1105#[pin_project::pin_project]
1106#[derive(Debug)]
1107pub struct GetPooledTxRequestFut<T = PooledTransaction> {
1108 #[pin]
1109 inner: Option<GetPooledTxRequest<T>>,
1110}
1111
1112impl<T> GetPooledTxRequestFut<T> {
1113 #[inline]
1114 const fn new(
1115 peer_id: PeerId,
1116 requested_hashes: RequestTxHashes,
1117 response: oneshot::Receiver<RequestResult<PooledTransactions<T>>>,
1118 ) -> Self {
1119 Self { inner: Some(GetPooledTxRequest { peer_id, requested_hashes, response }) }
1120 }
1121}
1122
1123impl<T> Future for GetPooledTxRequestFut<T> {
1124 type Output = GetPooledTxResponse<T>;
1125
1126 fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
1127 let mut req = self.as_mut().project().inner.take().expect("polled after completion");
1128 match req.response.poll_unpin(cx) {
1129 Poll::Ready(result) => Poll::Ready(GetPooledTxResponse {
1130 peer_id: req.peer_id,
1131 requested_hashes: req.requested_hashes,
1132 result,
1133 }),
1134 Poll::Pending => {
1135 self.project().inner.set(Some(req));
1136 Poll::Pending
1137 }
1138 }
1139 }
1140}
1141
1142#[derive(Debug, Constructor, Deref)]
1144pub struct UnverifiedPooledTransactions<T> {
1145 txns: PooledTransactions<T>,
1146}
1147
1148#[derive(Debug, Constructor, Deref)]
1150pub struct VerifiedPooledTransactions<T> {
1151 txns: PooledTransactions<T>,
1152}
1153
1154impl<T: SignedTransaction> DedupPayload for VerifiedPooledTransactions<T> {
1155 type Value = T;
1156
1157 fn is_empty(&self) -> bool {
1158 self.txns.is_empty()
1159 }
1160
1161 fn len(&self) -> usize {
1162 self.txns.len()
1163 }
1164
1165 fn dedup(self) -> PartiallyValidData<Self::Value> {
1166 PartiallyValidData::from_raw_data(
1167 self.txns.into_iter().map(|tx| (*tx.tx_hash(), tx)).collect(),
1168 None,
1169 )
1170 }
1171}
1172
1173trait VerifyPooledTransactionsResponse {
1174 type Transaction: SignedTransaction;
1175
1176 fn verify(
1177 self,
1178 requested_hashes: &RequestTxHashes,
1179 peer_id: &PeerId,
1180 ) -> (VerificationOutcome, VerifiedPooledTransactions<Self::Transaction>);
1181}
1182
1183impl<T: SignedTransaction> VerifyPooledTransactionsResponse for UnverifiedPooledTransactions<T> {
1184 type Transaction = T;
1185
1186 fn verify(
1187 self,
1188 requested_hashes: &RequestTxHashes,
1189 _peer_id: &PeerId,
1190 ) -> (VerificationOutcome, VerifiedPooledTransactions<T>) {
1191 let mut verification_outcome = VerificationOutcome::Ok;
1192
1193 let Self { mut txns } = self;
1194
1195 #[cfg(debug_assertions)]
1196 let mut tx_hashes_not_requested: smallvec::SmallVec<[TxHash; 16]> = smallvec::smallvec!();
1197 #[cfg(not(debug_assertions))]
1198 let mut tx_hashes_not_requested_count = 0;
1199
1200 txns.0.retain(|tx| {
1201 if !requested_hashes.contains(tx.tx_hash()) {
1202 verification_outcome = VerificationOutcome::ReportPeer;
1203
1204 #[cfg(debug_assertions)]
1205 tx_hashes_not_requested.push(*tx.tx_hash());
1206 #[cfg(not(debug_assertions))]
1207 {
1208 tx_hashes_not_requested_count += 1;
1209 }
1210
1211 return false
1212 }
1213 true
1214 });
1215
1216 #[cfg(debug_assertions)]
1217 if !tx_hashes_not_requested.is_empty() {
1218 trace!(target: "net::tx",
1219 peer_id=format!("{_peer_id:#}"),
1220 ?tx_hashes_not_requested,
1221 "transactions in `PooledTransactions` response from peer were not requested"
1222 );
1223 }
1224 #[cfg(not(debug_assertions))]
1225 if tx_hashes_not_requested_count != 0 {
1226 trace!(target: "net::tx",
1227 peer_id=format!("{_peer_id:#}"),
1228 tx_hashes_not_requested_count,
1229 "transactions in `PooledTransactions` response from peer were not requested"
1230 );
1231 }
1232
1233 (verification_outcome, VerifiedPooledTransactions::new(txns))
1234 }
1235}
1236
1237#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1240pub enum VerificationOutcome {
1241 Ok,
1243 ReportPeer,
1246}
1247
1248#[derive(Debug, Constructor)]
1250pub struct TransactionFetcherInfo {
1251 pub max_inflight_requests: usize,
1253 pub max_inflight_requests_per_peer: u8,
1255 pub soft_limit_byte_size_pooled_transactions_response_on_pack_request: usize,
1259 pub soft_limit_byte_size_pooled_transactions_response: usize,
1262 pub max_capacity_cache_txns_pending_fetch: u32,
1266}
1267
1268impl Default for TransactionFetcherInfo {
1269 fn default() -> Self {
1270 Self::new(
1271 DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS as usize,
1272 DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS_PER_PEER,
1273 DEFAULT_SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESP_ON_PACK_GET_POOLED_TRANSACTIONS_REQ,
1274 SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESPONSE,
1275 DEFAULT_MAX_CAPACITY_CACHE_PENDING_FETCH,
1276 )
1277 }
1278}
1279
1280impl From<TransactionFetcherConfig> for TransactionFetcherInfo {
1281 fn from(config: TransactionFetcherConfig) -> Self {
1282 let TransactionFetcherConfig {
1283 max_inflight_requests,
1284 max_inflight_requests_per_peer,
1285 soft_limit_byte_size_pooled_transactions_response,
1286 soft_limit_byte_size_pooled_transactions_response_on_pack_request,
1287 max_capacity_cache_txns_pending_fetch,
1288 } = config;
1289
1290 Self::new(
1291 max_inflight_requests as usize,
1292 max_inflight_requests_per_peer,
1293 soft_limit_byte_size_pooled_transactions_response_on_pack_request,
1294 soft_limit_byte_size_pooled_transactions_response,
1295 max_capacity_cache_txns_pending_fetch,
1296 )
1297 }
1298}
1299
1300#[derive(Debug, Default)]
1301struct TxFetcherSearchDurations {
1302 find_idle_peer: Duration,
1303 fill_request: Duration,
1304}
1305
1306#[cfg(test)]
1307mod test {
1308 use super::*;
1309 use crate::test_utils::transactions::{buffer_hash_to_tx_fetcher, new_mock_session};
1310 use alloy_primitives::{
1311 hex,
1312 map::{B256Map, B256Set, HashMap},
1313 B256,
1314 };
1315 use alloy_rlp::Decodable;
1316 use derive_more::IntoIterator;
1317 use reth_eth_wire_types::EthVersion;
1318 use reth_ethereum_primitives::TransactionSigned;
1319 use std::str::FromStr;
1320
1321 #[derive(IntoIterator)]
1322 struct TestValidAnnouncementData(Vec<(TxHash, Option<(u8, usize)>)>);
1323
1324 impl HandleMempoolData for TestValidAnnouncementData {
1325 fn is_empty(&self) -> bool {
1326 self.0.is_empty()
1327 }
1328
1329 fn len(&self) -> usize {
1330 self.0.len()
1331 }
1332
1333 fn retain_by_hash(&mut self, mut f: impl FnMut(&TxHash) -> bool) {
1334 self.0.retain(|(hash, _)| f(hash))
1335 }
1336 }
1337
1338 impl HandleVersionedMempoolData for TestValidAnnouncementData {
1339 fn msg_version(&self) -> EthVersion {
1340 EthVersion::Eth68
1341 }
1342 }
1343
1344 #[test]
1345 fn pack_eth68_request() {
1346 reth_tracing::init_test_tracing();
1347
1348 let tx_fetcher = &mut TransactionFetcher::<EthNetworkPrimitives>::default();
1351
1352 let eth68_hashes = [
1353 B256::from_slice(&[1; 32]),
1354 B256::from_slice(&[2; 32]),
1355 B256::from_slice(&[3; 32]),
1356 B256::from_slice(&[4; 32]),
1357 B256::from_slice(&[5; 32]),
1358 ];
1359 let eth68_sizes = [
1360 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,
1364 0,
1365 ];
1366
1367 let expected_request_hashes =
1368 [eth68_hashes[0], eth68_hashes[2]].into_iter().collect::<B256Set>();
1369
1370 let expected_surplus_hashes =
1371 [eth68_hashes[1], eth68_hashes[3], eth68_hashes[4]].into_iter().collect::<B256Set>();
1372
1373 let mut eth68_hashes_to_request = RequestTxHashes::with_capacity(3);
1374
1375 let valid_announcement_data = TestValidAnnouncementData(
1376 eth68_hashes
1377 .into_iter()
1378 .zip(eth68_sizes)
1379 .map(|(hash, size)| (hash, Some((0u8, size))))
1380 .collect::<Vec<_>>(),
1381 );
1382
1383 let surplus_eth68_hashes =
1386 tx_fetcher.pack_request_eth68(&mut eth68_hashes_to_request, valid_announcement_data);
1387
1388 let eth68_hashes_to_request = eth68_hashes_to_request.into_iter().collect::<B256Set>();
1389 let surplus_eth68_hashes = surplus_eth68_hashes.into_iter().collect::<B256Set>();
1390
1391 assert_eq!(expected_request_hashes, eth68_hashes_to_request);
1392 assert_eq!(expected_surplus_hashes, surplus_eth68_hashes);
1393 }
1394
1395 #[test]
1396 fn pack_eth68_request_does_not_overflow_announced_size() {
1397 reth_tracing::init_test_tracing();
1398
1399 let tx_fetcher = &mut TransactionFetcher::<EthNetworkPrimitives>::default();
1400
1401 let eth68_hashes =
1402 [B256::from_slice(&[1; 32]), B256::from_slice(&[2; 32]), B256::from_slice(&[3; 32])];
1403 let eth68_sizes = [
1404 DEFAULT_SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESP_ON_PACK_GET_POOLED_TRANSACTIONS_REQ - MEDIAN_BYTE_SIZE_SMALL_LEGACY_TX_ENCODED - 1,
1405 usize::MAX,
1406 2,
1407 ];
1408
1409 let expected_request_hashes =
1410 [eth68_hashes[0], eth68_hashes[2]].into_iter().collect::<B256Set>();
1411 let expected_surplus_hashes = std::iter::once(eth68_hashes[1]).collect::<B256Set>();
1412
1413 let mut eth68_hashes_to_request = RequestTxHashes::with_capacity(3);
1414 let valid_announcement_data = TestValidAnnouncementData(
1415 eth68_hashes
1416 .into_iter()
1417 .zip(eth68_sizes)
1418 .map(|(hash, size)| (hash, Some((0u8, size))))
1419 .collect::<Vec<_>>(),
1420 );
1421
1422 let surplus_eth68_hashes =
1423 tx_fetcher.pack_request_eth68(&mut eth68_hashes_to_request, valid_announcement_data);
1424
1425 let eth68_hashes_to_request = eth68_hashes_to_request.into_iter().collect::<B256Set>();
1426 let surplus_eth68_hashes = surplus_eth68_hashes.into_iter().collect::<B256Set>();
1427
1428 assert_eq!(expected_request_hashes, eth68_hashes_to_request);
1429 assert_eq!(expected_surplus_hashes, surplus_eth68_hashes);
1430 }
1431
1432 #[test]
1433 fn pack_eth72_request_uses_metadata_size_limit() {
1434 reth_tracing::init_test_tracing();
1435
1436 let tx_fetcher = &mut TransactionFetcher::<EthNetworkPrimitives>::default();
1437
1438 let hashes =
1439 [B256::from_slice(&[1; 32]), B256::from_slice(&[2; 32]), B256::from_slice(&[3; 32])];
1440 let announced_size =
1441 DEFAULT_SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESP_ON_PACK_GET_POOLED_TRANSACTIONS_REQ;
1442 let announcement_data = hashes
1443 .into_iter()
1444 .map(|hash| (hash, Some((0u8, announced_size))))
1445 .collect::<B256Map<_>>();
1446 let valid_announcement_data = ValidAnnouncementData::from_partially_valid_data(
1447 PartiallyValidData::from_raw_data_eth72(announcement_data),
1448 );
1449
1450 let mut hashes_to_request = RequestTxHashes::with_capacity(3);
1451 let surplus_hashes =
1452 tx_fetcher.pack_request(&mut hashes_to_request, valid_announcement_data);
1453
1454 assert_eq!(1, hashes_to_request.len());
1455 assert_eq!(2, surplus_hashes.len());
1456 }
1457
1458 #[tokio::test]
1459 async fn test_on_fetch_pending_hashes() {
1460 reth_tracing::init_test_tracing();
1461
1462 let tx_fetcher = &mut TransactionFetcher::default();
1463
1464 let seen_hashes = [
1468 B256::from_slice(&[1; 32]),
1469 B256::from_slice(&[2; 32]),
1470 B256::from_slice(&[3; 32]),
1471 B256::from_slice(&[4; 32]),
1472 ];
1473 let seen_eth68_hashes_sizes = [120, 158, 116];
1478
1479 let peer_1 = PeerId::new([1; 64]);
1481 let peer_2 = PeerId::new([2; 64]);
1483
1484 let (mut peer_1_data, mut peer_1_mock_session_rx) =
1488 new_mock_session(peer_1, EthVersion::Eth66);
1489 for hash in &seen_hashes {
1490 peer_1_data.seen_transactions.insert(*hash);
1491 }
1492 let (mut peer_2_data, _) = new_mock_session(peer_2, EthVersion::Eth66);
1493 for hash in &seen_hashes {
1494 peer_2_data.seen_transactions.insert(*hash);
1495 }
1496 let mut peers: HashMap<PeerId, _, FbBuildHasher<64>> = HashMap::default();
1497 peers.insert(peer_1, peer_1_data);
1498 peers.insert(peer_2, peer_2_data);
1499
1500 for i in 0..3 {
1502 buffer_hash_to_tx_fetcher(
1504 tx_fetcher,
1505 seen_hashes[i],
1506 peer_2,
1507 0,
1508 Some(seen_eth68_hashes_sizes[i]),
1509 );
1510 }
1511 buffer_hash_to_tx_fetcher(tx_fetcher, seen_hashes[3], peer_2, 0, None);
1512
1513 let hash_other = B256::from_slice(&[5; 32]);
1515 buffer_hash_to_tx_fetcher(tx_fetcher, hash_other, peer_2, 0, None);
1516
1517 for hash in &seen_hashes {
1519 buffer_hash_to_tx_fetcher(tx_fetcher, *hash, peer_1, 0, None);
1520 }
1521
1522 assert_eq!(tx_fetcher.num_pending_hashes(), 5);
1524
1525 tx_fetcher.on_fetch_pending_hashes(&peers, |_| true);
1528
1529 let req = peer_1_mock_session_rx
1531 .recv()
1532 .await
1533 .expect("peer session should receive request with buffered hashes");
1534 let PeerRequest::GetPooledTransactions { request, .. } = req else { unreachable!() };
1535 let GetPooledTransactions(requested_hashes) = request;
1536
1537 assert_eq!(
1538 requested_hashes.into_iter().collect::<B256Set>(),
1539 seen_hashes.into_iter().collect::<B256Set>()
1540 )
1541 }
1542
1543 #[test]
1544 fn on_fetch_pending_hashes_rebuffers_on_disconnected_peer() {
1545 let tx_fetcher = &mut TransactionFetcher::default();
1546 let peer_1 = PeerId::new([1; 64]);
1547 let peer_2 = PeerId::new([2; 64]);
1548 let hash_1 = B256::from_slice(&[1; 32]);
1549
1550 buffer_hash_to_tx_fetcher(tx_fetcher, hash_1, peer_1, 0, Some(128));
1551 buffer_hash_to_tx_fetcher(tx_fetcher, hash_1, peer_2, 0, Some(128));
1552
1553 assert_eq!(tx_fetcher.num_pending_hashes(), 1);
1554
1555 let peers: HashMap<PeerId, PeerMetadata, FbBuildHasher<64>> = HashMap::default();
1557 tx_fetcher.on_fetch_pending_hashes(&peers, |_| true);
1558
1559 assert_eq!(tx_fetcher.num_pending_hashes(), 1);
1561 }
1562
1563 #[test]
1564 fn verify_response_hashes() {
1565 let input = hex!(
1566 "02f871018302a90f808504890aef60826b6c94ddf4c5025d1a5742cf12f74eec246d4432c295e487e09c3bbcc12b2b80c080a0f21a4eacd0bf8fea9c5105c543be5a1d8c796516875710fafafdf16d16d8ee23a001280915021bb446d1973501a67f93d2b38894a514b976e7b46dc2fe54598daa"
1567 );
1568 let signed_tx_1: PooledTransaction =
1569 TransactionSigned::decode(&mut &input[..]).unwrap().try_into().unwrap();
1570 let input = hex!(
1571 "02f871018302a90f808504890aef60826b6c94ddf4c5025d1a5742cf12f74eec246d4432c295e487e09c3bbcc12b2b80c080a0f21a4eacd0bf8fea9c5105c543be5a1d8c796516875710fafafdf16d16d8ee23a001280915021bb446d1973501a67f93d2b38894a514b976e7b46dc2fe54598d76"
1572 );
1573 let signed_tx_2: PooledTransaction =
1574 TransactionSigned::decode(&mut &input[..]).unwrap().try_into().unwrap();
1575
1576 let request_hashes = [
1578 B256::from_str("0x3b9aca00f0671c9a2a1b817a0a78d3fe0c0f776cccb2a8c3c1b412a4f4e67890")
1579 .unwrap(),
1580 *signed_tx_1.hash(),
1581 B256::from_str("0x3b9aca00f0671c9a2a1b817a0a78d3fe0c0f776cccb2a8c3c1b412a4f4e12345")
1582 .unwrap(),
1583 B256::from_str("0x3b9aca00f0671c9a2a1b817a0a78d3fe0c0f776cccb2a8c3c1b412a4f4edabe3")
1584 .unwrap(),
1585 ];
1586
1587 for hash in &request_hashes {
1588 assert_ne!(hash, signed_tx_2.hash())
1589 }
1590
1591 let request_hashes = RequestTxHashes::new(request_hashes.into_iter().collect());
1592
1593 let response_txns = PooledTransactions(vec![signed_tx_1.clone(), signed_tx_2]);
1595 let payload = UnverifiedPooledTransactions::new(response_txns);
1596
1597 let (outcome, verified_payload) = payload.verify(&request_hashes, &PeerId::ZERO);
1598
1599 assert_eq!(VerificationOutcome::ReportPeer, outcome);
1600 assert_eq!(1, verified_payload.len());
1601 assert!(verified_payload.contains(&signed_tx_1));
1602 }
1603}