Skip to main content

ethrex_p2p/
tx_broadcaster.rs

1use std::{
2    collections::HashMap,
3    sync::Arc,
4    time::{Duration, Instant},
5};
6
7use ethrex_blockchain::Blockchain;
8use ethrex_common::H256;
9use ethrex_common::types::{MempoolTransaction, Transaction};
10use ethrex_crypto::NativeCrypto;
11use ethrex_storage::error::StoreError;
12use rand::{seq::SliceRandom, thread_rng};
13use spawned_concurrency::{
14    actor,
15    error::ActorError,
16    protocol,
17    tasks::{Actor, ActorRef, ActorStart as _, Context, Handler, send_interval},
18};
19use tracing::{debug, error, trace};
20
21use crate::{
22    peer_table::{PeerTable, PeerTableServerProtocol as _},
23    rlpx::{
24        Message,
25        connection::server::PeerConnection,
26        eth::transactions::{NewPooledTransactionHashes, Transactions},
27        p2p::{Capability, SUPPORTED_ETH_CAPABILITIES},
28    },
29};
30
31// Soft limit for the number of transaction hashes sent in a single NewPooledTransactionHashes message as per [the spec](https://github.com/ethereum/devp2p/blob/master/caps/eth.md#newpooledtransactionhashes-0x080)
32const NEW_POOLED_TRANSACTION_HASHES_SOFT_LIMIT: usize = 4096;
33
34// Amount of seconds after which we prune broadcast records (We should fine tune this)
35const PRUNE_WAIT_TIME_SECS: u64 = 600; // 10 minutes
36
37// Amount of seconds between each prune
38const PRUNE_INTERVAL_SECS: u64 = 360; // 6 minutes
39
40// Amount of milliseconds between each broadcast
41pub const BROADCAST_INTERVAL_MS: u64 = 1000; // 1 second
42
43#[protocol]
44pub trait TxBroadcasterProtocol: Send + Sync {
45    fn broadcast_txs(&self) -> Result<(), ActorError>;
46    fn add_txs(&self, tx_hashes: Vec<H256>, peer_id: H256) -> Result<(), ActorError>;
47    fn prune_txs(&self) -> Result<(), ActorError>;
48    fn remove_peer(&self, peer_id: H256) -> Result<(), ActorError>;
49}
50
51#[derive(Debug, Clone, Default)]
52struct PeerMask {
53    bits: Vec<u64>,
54}
55
56impl PeerMask {
57    #[inline]
58    // Ensure that the internal bit vector can hold the given index
59    // If not, resize the vector.
60    fn ensure(&mut self, idx: u32) {
61        let word = (idx as usize) / 64;
62        if self.bits.len() <= word {
63            self.bits.resize(word + 1, 0);
64        }
65    }
66
67    #[inline]
68    fn is_set(&self, idx: u32) -> bool {
69        let word = (idx as usize) / 64;
70        if word >= self.bits.len() {
71            return false;
72        }
73        let bit = (idx as usize) % 64;
74        (self.bits[word] >> bit) & 1 == 1
75    }
76
77    #[inline]
78    fn set(&mut self, idx: u32) {
79        self.ensure(idx);
80        let word = (idx as usize) / 64;
81        let bit = (idx as usize) % 64;
82        self.bits[word] |= 1u64 << bit;
83    }
84
85    #[inline]
86    // Clear bit `idx`. Used when a peer's index is freed so a future peer that reuses the
87    // index does not inherit a stale "already-sent" bit (which would suppress a real broadcast).
88    fn clear(&mut self, idx: u32) {
89        let word = (idx as usize) / 64;
90        if word < self.bits.len() {
91            let bit = (idx as usize) % 64;
92            self.bits[word] &= !(1u64 << bit);
93        }
94    }
95}
96
97#[derive(Debug, Clone)]
98struct BroadcastRecord {
99    peers: PeerMask,
100    last_sent: Instant,
101}
102
103impl Default for BroadcastRecord {
104    fn default() -> Self {
105        Self {
106            peers: PeerMask::default(),
107            last_sent: Instant::now(),
108        }
109    }
110}
111
112#[derive(Debug, Clone)]
113pub struct TxBroadcaster {
114    peer_table: PeerTable,
115    blockchain: Arc<Blockchain>,
116    // tx_hash -> broadcast record (which peers know it and when it was last sent)
117    known_txs: HashMap<H256, BroadcastRecord>,
118    // Assign each peer_id (H256) a u32 index used by PeerMask entries
119    peer_indexer: HashMap<H256, u32>,
120    // Next index to assign to a new peer
121    next_peer_idx: u32,
122    // Indices freed by disconnected peers, reused before allocating new ones. Without this,
123    // next_peer_idx (and therefore every PeerMask's width) grows unbounded with peer churn.
124    free_indices: Vec<u32>,
125    tx_broadcasting_time_interval: u64,
126}
127
128pub async fn send_tx_hashes(
129    txs: Vec<MempoolTransaction>,
130    capabilities: Vec<Capability>,
131    connection: &mut PeerConnection,
132    peer_id: H256,
133    blockchain: &Arc<Blockchain>,
134) -> Result<(), TxBroadcasterError> {
135    if SUPPORTED_ETH_CAPABILITIES
136        .iter()
137        .any(|cap| capabilities.contains(cap))
138    {
139        for tx_chunk in txs.chunks(NEW_POOLED_TRANSACTION_HASHES_SOFT_LIMIT) {
140            let tx_count = tx_chunk.len();
141            let mut txs_to_send = Vec::with_capacity(tx_count);
142            for tx in tx_chunk {
143                txs_to_send.push((**tx).clone());
144            }
145            let hashes_message = Message::NewPooledTransactionHashes(
146                NewPooledTransactionHashes::new(txs_to_send, blockchain)?,
147            );
148            connection.outgoing_message(hashes_message.clone()).await.unwrap_or_else(|err| {
149                debug!(peer_id = %format!("{:#x}", peer_id), err = ?err, "Failed to send transaction hashes");
150            });
151        }
152    }
153    Ok(())
154}
155
156#[actor(protocol = TxBroadcasterProtocol)]
157impl TxBroadcaster {
158    pub fn spawn(
159        kademlia: PeerTable,
160        blockchain: Arc<Blockchain>,
161        tx_broadcasting_time_interval: u64,
162    ) -> Result<ActorRef<TxBroadcaster>, TxBroadcasterError> {
163        debug!("Starting transaction broadcaster");
164
165        let state = TxBroadcaster {
166            peer_table: kademlia,
167            blockchain,
168            known_txs: HashMap::new(),
169            peer_indexer: HashMap::new(),
170            next_peer_idx: 0,
171            free_indices: Vec::new(),
172            tx_broadcasting_time_interval,
173        };
174
175        Ok(state.start())
176    }
177
178    #[started]
179    async fn started(&mut self, ctx: &Context<Self>) {
180        send_interval(
181            Duration::from_millis(self.tx_broadcasting_time_interval),
182            ctx.clone(),
183            tx_broadcaster_protocol::BroadcastTxs,
184        );
185
186        send_interval(
187            Duration::from_secs(PRUNE_INTERVAL_SECS),
188            ctx.clone(),
189            tx_broadcaster_protocol::PruneTxs,
190        );
191    }
192
193    #[send_handler]
194    async fn handle_broadcast_txs(
195        &mut self,
196        _msg: tx_broadcaster_protocol::BroadcastTxs,
197        _ctx: &Context<Self>,
198    ) {
199        trace!(received = "BroadcastTxs");
200
201        let _ = self.do_broadcast_txs().await.inspect_err(|err| {
202            error!(err = ?err, "Failed to broadcast transactions");
203        });
204    }
205
206    #[send_handler]
207    async fn handle_add_txs(&mut self, msg: tx_broadcaster_protocol::AddTxs, _ctx: &Context<Self>) {
208        debug!(received = "AddTxs", tx_count = msg.tx_hashes.len());
209        self.do_add_txs(msg.tx_hashes, msg.peer_id);
210    }
211
212    #[send_handler]
213    async fn handle_prune_txs(
214        &mut self,
215        _msg: tx_broadcaster_protocol::PruneTxs,
216        _ctx: &Context<Self>,
217    ) {
218        debug!(received = "PruneTxs");
219        let now = Instant::now();
220        let before = self.known_txs.len();
221        let prune_window = Duration::from_secs(PRUNE_WAIT_TIME_SECS);
222
223        self.known_txs
224            .retain(|_, record| now.duration_since(record.last_sent) < prune_window);
225        debug!(
226            before = before,
227            after = self.known_txs.len(),
228            "Pruned old broadcasted transactions"
229        );
230
231        // Piggyback the alternates-map sweep on this same tick.
232        let _ = self.blockchain.mempool.prune_alternates(prune_window);
233    }
234
235    #[send_handler]
236    async fn handle_remove_peer(
237        &mut self,
238        msg: tx_broadcaster_protocol::RemovePeer,
239        _ctx: &Context<Self>,
240    ) {
241        self.do_remove_peer(msg.peer_id);
242    }
243
244    // Remove a disconnected peer: drop its index mapping, clear its bit from every known_txs
245    // record (so a future peer reusing the index isn't treated as already-notified), and return
246    // the index to the free-list — bounding peer_indexer and every PeerMask's width to live peers.
247    fn do_remove_peer(&mut self, peer_id: H256) {
248        if let Some(idx) = self.peer_indexer.remove(&peer_id) {
249            for record in self.known_txs.values_mut() {
250                record.peers.clear(idx);
251            }
252            self.free_indices.push(idx);
253        }
254    }
255
256    // Get or assign a unique index to the peer_id
257    #[inline]
258    fn peer_index(&mut self, peer_id: H256) -> u32 {
259        if let Some(&idx) = self.peer_indexer.get(&peer_id) {
260            idx
261        } else {
262            // We are assigning indexes sequentially, so next_peer_idx is always the next available one.
263            // self.peer_indexer.len() could be used instead of next_peer_idx but avoided here if we ever
264            // remove entries from peer_indexer in the future.
265            // Reuse a freed index if available, otherwise allocate the next one.
266            let idx = self.free_indices.pop().unwrap_or_else(|| {
267                let idx = self.next_peer_idx;
268                // In practice we won't exceed u32::MAX (~4.29 Billion) peers.
269                self.next_peer_idx += 1;
270                idx
271            });
272            self.peer_indexer.insert(peer_id, idx);
273            idx
274        }
275    }
276
277    fn do_add_txs(&mut self, txs: Vec<H256>, peer_id: H256) {
278        debug!(total = self.known_txs.len(), adding = txs.len(), peer_id = %format!("{:#x}", peer_id), "Adding transactions to known list");
279
280        if txs.is_empty() {
281            return;
282        }
283
284        let now = Instant::now();
285        let peer_idx = self.peer_index(peer_id);
286        for tx in txs {
287            let record = self.known_txs.entry(tx).or_default();
288            record.peers.set(peer_idx);
289            record.last_sent = now;
290        }
291    }
292
293    async fn do_broadcast_txs(&mut self) -> Result<(), TxBroadcasterError> {
294        let txs_to_broadcast = self
295            .blockchain
296            .mempool
297            .get_txs_for_broadcast()
298            .map_err(|_| TxBroadcasterError::Broadcast)?;
299        if txs_to_broadcast.is_empty() {
300            return Ok(());
301        }
302        let peers = self.peer_table.get_peers_with_capabilities().await?;
303        let peer_sqrt = (peers.len() as f64).sqrt();
304
305        let full_txs = txs_to_broadcast
306            .iter()
307            .map(|tx| tx.transaction().clone())
308            .filter(|tx| {
309                !matches!(tx, Transaction::EIP4844Transaction { .. }) && !tx.is_privileged()
310            })
311            .collect::<Vec<Transaction>>();
312
313        let blob_txs = txs_to_broadcast
314            .iter()
315            .filter(|tx| matches!(tx.transaction(), Transaction::EIP4844Transaction { .. }))
316            .cloned()
317            .collect::<Vec<MempoolTransaction>>();
318
319        let mut shuffled_peers = peers.clone();
320        shuffled_peers.shuffle(&mut thread_rng());
321
322        let (peers_to_send_full_txs, peers_to_send_hashes) =
323            shuffled_peers.split_at(peer_sqrt.ceil() as usize);
324
325        for (peer_id, mut connection, capabilities) in peers_to_send_full_txs.iter().cloned() {
326            let peer_idx = self.peer_index(peer_id);
327            let txs_to_send = full_txs
328                .iter()
329                .filter(|tx| {
330                    let hash = tx.hash(&NativeCrypto);
331                    !self
332                        .known_txs
333                        .get(&hash)
334                        .is_some_and(|record| record.peers.is_set(peer_idx))
335                })
336                .cloned()
337                .collect::<Vec<Transaction>>();
338            self.do_add_txs(
339                txs_to_send
340                    .iter()
341                    .map(|tx| tx.hash(&NativeCrypto))
342                    .collect(),
343                peer_id,
344            );
345            // If a peer is selected to receive the full transactions, we don't send the blob transactions, since they only require to send the hashes
346            let txs_message = Message::Transactions(Transactions {
347                transactions: txs_to_send,
348            });
349            connection.outgoing_message(txs_message).await.unwrap_or_else(|err| {
350                debug!(peer_id = %format!("{:#x}", peer_id), err = ?err, "Failed to send transactions");
351            });
352            self.send_tx_hashes_internal(blob_txs.clone(), capabilities, &mut connection, peer_id)
353                .await?;
354        }
355        for (peer_id, mut connection, capabilities) in peers_to_send_hashes.iter().cloned() {
356            // If a peer is not selected to receive the full transactions, we only send the hashes of all transactions (including blob transactions)
357            self.send_tx_hashes_internal(
358                txs_to_broadcast.clone(),
359                capabilities,
360                &mut connection,
361                peer_id,
362            )
363            .await?;
364        }
365        let broadcasted_hashes: Vec<H256> = txs_to_broadcast
366            .iter()
367            .map(|tx| tx.hash(&NativeCrypto))
368            .collect();
369        self.blockchain
370            .mempool
371            .remove_broadcasted_txs(&broadcasted_hashes)?;
372        Ok(())
373    }
374
375    async fn send_tx_hashes_internal(
376        &mut self,
377        txs: Vec<MempoolTransaction>,
378        capabilities: Vec<Capability>,
379        connection: &mut PeerConnection,
380        peer_id: H256,
381    ) -> Result<(), TxBroadcasterError> {
382        let peer_idx = self.peer_index(peer_id);
383        let txs_to_send = txs
384            .iter()
385            .filter(|tx| {
386                let hash = tx.hash(&NativeCrypto);
387                !self
388                    .known_txs
389                    .get(&hash)
390                    .is_some_and(|record| record.peers.is_set(peer_idx))
391                    && !tx.is_privileged()
392            })
393            .cloned()
394            .collect::<Vec<MempoolTransaction>>();
395        self.do_add_txs(
396            txs_to_send
397                .iter()
398                .map(|tx| tx.hash(&NativeCrypto))
399                .collect(),
400            peer_id,
401        );
402        send_tx_hashes(
403            txs_to_send,
404            capabilities,
405            connection,
406            peer_id,
407            &self.blockchain,
408        )
409        .await
410    }
411}
412
413#[derive(Debug, thiserror::Error)]
414pub enum TxBroadcasterError {
415    #[error("Failed to broadcast transactions")]
416    Broadcast,
417    #[error(transparent)]
418    StoreError(#[from] StoreError),
419    #[error(transparent)]
420    PeerTableError(#[from] ActorError),
421}