Skip to main content

hopr_protocol_hopr/codec/
decoder.rs

1use std::{ops::Mul, sync::atomic::AtomicU64, time::Duration};
2
3use bytes::{BufMut, Bytes, BytesMut};
4use hopr_api::{
5    chain::*,
6    types::{crypto::prelude::*, internal::prelude::*, primitive::prelude::*},
7};
8use hopr_crypto_packet::prelude::*;
9use hopr_utils::trace_timed;
10
11use crate::{
12    AuxiliaryPacketInfo, HoprCodecConfig, IncomingAcknowledgementPacket, IncomingFinalPacket, IncomingForwardedPacket,
13    IncomingPacket, IncomingPacketError, PacketDecoder, SurbStore, errors::HoprProtocolError, tbf::TagBloomFilter,
14};
15
16/// How often a sustained SURB-buffer overflow is allowed to log.
17///
18/// A full buffer stays full, so once it is at capacity *every* subsequent insert evicts — and this
19/// runs per received packet. Under poor network conditions eviction is the steady state rather than
20/// the exception, so warning on each one would turn the signal into thousands of lines a second.
21/// Warn once per interval and carry the running totals instead.
22const SURB_EVICTION_WARN_INTERVAL: u64 = 256;
23
24/// Inserts that had to evict, process-wide; drives [`SURB_EVICTION_WARN_INTERVAL`].
25///
26/// Process-wide rather than per-pseudonym: the totals are an operator signal about this node, and a
27/// per-pseudonym breakdown would need a map on the packet hot path to say something the balancer
28/// estimate already implies.
29static SURB_EVICTING_INSERTS: AtomicU64 = AtomicU64::new(0);
30
31/// SURBs destroyed by those evictions, process-wide — the loss magnitude the interval would hide.
32static SURB_EVICTED_TOTAL: AtomicU64 = AtomicU64::new(0);
33
34/// Default [decoder](PacketDecoder) implementation for HOPR packets.
35pub struct HoprDecoder<Chain, S, T> {
36    chain_api: Chain,
37    surb_store: std::sync::Arc<S>,
38    ticket_factory: T,
39    packet_key: OffchainKeypair,
40    chain_key: ChainKeypair,
41    channels_dst: Hash,
42    cfg: HoprCodecConfig,
43    tbf: parking_lot::Mutex<TagBloomFilter>,
44    peer_id_cache: moka::sync::Cache<PeerId, OffchainPublicKey>,
45}
46
47impl<Chain, S, T> HoprDecoder<Chain, S, T>
48where
49    Chain: ChainReadChannelOperations + ChainKeyOperations + ChainReadTicketOperations + ChainValues + Send + Sync,
50    S: SurbStore + Send + Sync,
51    T: hopr_api::tickets::TicketFactory + Send + Sync,
52{
53    /// Creates a new instance of the decoder.
54    pub fn new(
55        (packet_key, chain_key): (OffchainKeypair, ChainKeypair),
56        chain_api: Chain,
57        surb_store: S,
58        ticket_factory: T,
59        channels_dst: Hash,
60        cfg: HoprCodecConfig,
61    ) -> Self {
62        Self {
63            chain_api,
64            surb_store: std::sync::Arc::new(surb_store),
65            packet_key,
66            chain_key,
67            channels_dst,
68            cfg,
69            ticket_factory,
70            tbf: parking_lot::Mutex::new(Default::default()),
71            peer_id_cache: moka::sync::Cache::builder()
72                .time_to_idle(Duration::from_secs(600))
73                .max_capacity(100_000)
74                .build(),
75        }
76    }
77
78    #[tracing::instrument(skip(self, fwd), level = "debug", fields(path_pos = fwd.path_pos))]
79    fn validate_and_replace_ticket(
80        &self,
81        mut fwd: HoprForwardedPacket,
82    ) -> Result<(HoprForwardedPacket, UnacknowledgedTicket), HoprProtocolError> {
83        let previous_hop_addr = trace_timed!("previous_hop_addr lookup", {
84            self.chain_api
85                .packet_key_to_chain_key(&fwd.previous_hop)
86                .map_err(HoprProtocolError::resolver)?
87                .ok_or(HoprProtocolError::KeyNotFound)?
88        });
89
90        let next_hop_addr = trace_timed!("next_hop_addr lookup", {
91            self.chain_api
92                .packet_key_to_chain_key(&fwd.outgoing.next_hop)
93                .map_err(HoprProtocolError::resolver)?
94                .ok_or(HoprProtocolError::KeyNotFound)?
95        });
96
97        let incoming_channel = trace_timed!("incoming_channel lookup", {
98            self.chain_api
99                .channel_by_parties(&previous_hop_addr, self.chain_key.as_ref())
100                .map_err(HoprProtocolError::resolver)?
101                .ok_or_else(|| HoprProtocolError::ChannelNotFound(previous_hop_addr, *self.chain_key.as_ref()))?
102        });
103
104        // The ticket price from the oracle times my node's position on the
105        // path is the acceptable minimum
106        let (win_prob, minimum_ticket_price) = self
107            .chain_api
108            .incoming_ticket_values()
109            .map_err(HoprProtocolError::resolver)?;
110
111        let minimum_ticket_price = minimum_ticket_price
112            .mul(U256::from(fwd.path_pos))
113            .max(self.cfg.min_incoming_ticket_price.unwrap_or_default());
114
115        let remaining_balance = trace_timed!("unrealized_balance lookup", {
116            self.ticket_factory
117                .remaining_incoming_channel_stake(&incoming_channel)
118                .map_err(HoprProtocolError::ticket_factory)?
119        });
120
121        // Here also the signature on the ticket gets validated,
122        // so afterward we are sure the source of the `channel`
123        // (which is equal to `previous_hop_addr`) has issued this
124        // ticket.
125
126        let verified_incoming_ticket = trace_timed!("ticket_signature_verification", {
127            validate_unacknowledged_ticket(
128                fwd.outgoing.ticket,
129                &incoming_channel,
130                minimum_ticket_price,
131                win_prob,
132                remaining_balance,
133                &self.channels_dst,
134            )
135        })?;
136
137        // The ticket is now validated:
138        tracing::trace!(%verified_incoming_ticket, "successfully verified incoming ticket");
139
140        // NOTE: that the path position according to the ticket value
141        // may no longer match the path position from the packet header,
142        // because the ticket issuer may set the price of the ticket higher.
143
144        // Create the new ticket for the new packet
145        let ticket_builder = if fwd.path_pos > 1 {
146            // There must be a channel to the next node if it's not the final hop.
147            // If the channel does not exist, the ticket we extracted before cannot be saved,
148            // as there would be no way to acknowledge it without the channel.
149            let outgoing_channel = self
150                .chain_api
151                .channel_by_parties(self.chain_key.as_ref(), &next_hop_addr)
152                .map_err(HoprProtocolError::resolver)?
153                .ok_or_else(|| HoprProtocolError::ChannelNotFound(*self.chain_key.as_ref(), next_hop_addr))?;
154
155            let (outgoing_ticket_win_prob, outgoing_ticket_price) = self
156                .chain_api
157                .outgoing_ticket_values(self.cfg.outgoing_win_prob, self.cfg.outgoing_ticket_price)
158                .map_err(HoprProtocolError::resolver)?;
159
160            // We currently take the maximum of the win prob from the incoming ticket
161            // and the one configured on this node.
162            // Therefore, the winning probability can only increase along the path.
163            let outgoing_ticket_win_prob = outgoing_ticket_win_prob.max(&verified_incoming_ticket.win_prob());
164
165            // The following operation fails if there's not enough balance on the channel to the next hop.
166            // Again, in this case, we cannot save the ticket we previously extracted because there is no way it gets
167            // acknowledged without enough balance.
168            self.ticket_factory
169                .new_multihop_ticket(
170                    &outgoing_channel,
171                    fwd.path_pos.try_into().expect("path position is always > 1"),
172                    outgoing_ticket_win_prob,
173                    outgoing_ticket_price,
174                )
175                .map_err(HoprProtocolError::ticket_factory)?
176        } else {
177            TicketBuilder::zero_hop().counterparty(next_hop_addr)
178        };
179
180        // Finally, replace the ticket in the outgoing packet with a new one
181        let ticket_builder = ticket_builder.eth_challenge(fwd.next_challenge);
182        fwd.outgoing.ticket = trace_timed!("ticket_signing", {
183            ticket_builder.build_signed(&self.chain_key, &self.channels_dst)?.leak()
184        });
185
186        let unack_ticket = verified_incoming_ticket.into_unacknowledged(fwd.own_key);
187        Ok((fwd, unack_ticket))
188    }
189}
190
191impl<Chain, S, T> PacketDecoder for HoprDecoder<Chain, S, T>
192where
193    Chain: ChainReadChannelOperations + ChainKeyOperations + ChainReadTicketOperations + ChainValues + Send + Sync,
194    S: SurbStore + Send + Sync + 'static,
195    T: hopr_api::tickets::TicketFactory + Send + Sync,
196{
197    type Error = HoprProtocolError;
198
199    #[tracing::instrument(skip(self, sender, data), level = "trace", fields(%sender))]
200    fn decode(&self, sender: PeerId, data: Bytes) -> Result<IncomingPacket, IncomingPacketError<Self::Error>> {
201        #[cfg(feature = "trace-timing")]
202        let decode_start = std::time::Instant::now();
203        tracing::trace!(data_len = data.len(), "decoding packet");
204
205        // Phase 1: Peer ID conversion
206        // Try to retrieve the peer's public key from the cache or compute it if it does not exist yet.
207        // The async block ensures the Rayon task is only submitted on cache miss.
208        let previous_hop = trace_timed!("peer_id_conversion complete", {
209            match self
210                .peer_id_cache
211                .try_get_with_by_ref(&sender, || OffchainPublicKey::from_peerid(&sender))
212            {
213                Ok(peer) => Ok(peer),
214                Err(error) => {
215                    tracing::error!(%sender, %error, "dropping packet - cannot convert peer id");
216                    Err(IncomingPacketError::Undecodable(HoprProtocolError::InvalidSender))
217                }
218            }
219        })?;
220
221        // Phase 2: Sphinx packet decoding
222
223        // If the following operation fails, it means that the packet is not a valid Hopr packet,
224        // and as such should not be acknowledged later.
225        // Which of our SURBs this packet came back on, if any. Captured here because resolving the
226        // opener is the only point that knows it; by the time the decoded packet exists the sender
227        // id is gone.
228        let replied_on_surb = std::cell::Cell::new(None);
229        let packet = trace_timed!("sphinx_decode complete", {
230            HoprPacket::from_incoming(
231                &data,
232                &self.packet_key,
233                previous_hop,
234                self.chain_api.key_id_mapper_ref(),
235                |p| {
236                    let opener = self.surb_store.find_reply_opener(p);
237                    if opener.is_some() {
238                        replied_on_surb.set(Some(p.surb_id()));
239                    }
240                    opener
241                },
242            )
243        })
244        .map_err(IncomingPacketError::undecodable)?;
245
246        // This is checked on both Final and Forwarded packets,
247        // Outgoing packets are not allowed to pass and are later reported as invalid state.
248        if let Some(tag) = packet.packet_tag() {
249            // This operation has run-time of ~10 nanoseconds,
250            // and therefore does not need to be invoked via spawn_blocking
251            if self.tbf.lock().check_and_set(tag) {
252                return Err(IncomingPacketError::ProcessingError(
253                    previous_hop.into(),
254                    HoprProtocolError::Replay,
255                ));
256            }
257        }
258
259        match packet {
260            HoprPacket::Final(incoming) => {
261                // Extract additional information from the packet that will be passed upwards
262                let mut info = AuxiliaryPacketInfo {
263                    packet_signals: incoming.signals,
264                    num_surbs: incoming.surbs.len(),
265                    num_evicted_surbs: 0,
266                };
267
268                // Store all incoming SURBs if any
269                if !incoming.surbs.is_empty() {
270                    let outcome = self.surb_store.insert_surbs(incoming.sender, incoming.surbs);
271                    info.num_evicted_surbs = outcome.evicted;
272                    tracing::trace!(pseudonym = %incoming.sender, num_surbs = info.num_surbs, retained = outcome.retained, packet_type = "final", "stored incoming surbs for pseudonym");
273
274                    // Warn rather than trace: an overflow is silent everywhere else. Without this
275                    // line the only way to learn that a buffer is overflowing is to infer it from a
276                    // balancer estimate that outgrew the store it describes, which is how it was
277                    // found the first time. Rate-limited, because a full buffer evicts on every
278                    // insert from then on -- see `SURB_EVICTION_WARN_INTERVAL`.
279                    if outcome.evicted > 0 {
280                        let total_evicted = SURB_EVICTED_TOTAL
281                            .fetch_add(outcome.evicted as u64, std::sync::atomic::Ordering::Relaxed)
282                            + outcome.evicted as u64;
283                        let prev_inserts = SURB_EVICTING_INSERTS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
284
285                        // `prev` is a multiple of the interval on the very first overflow, so the
286                        // onset is reported immediately and only the sustained condition is thinned.
287                        if prev_inserts.is_multiple_of(SURB_EVICTION_WARN_INTERVAL) {
288                            tracing::warn!(
289                                pseudonym = %incoming.sender,
290                                evicted = outcome.evicted,
291                                retained = outcome.retained,
292                                total_evicted,
293                                overflowing_inserts = prev_inserts + 1,
294                                "SURB buffer full; dropping the oldest SURBs"
295                            );
296                        }
297                    }
298                }
299
300                let result = match incoming.ack_key {
301                    None => {
302                        if incoming.plain_text.len() < size_of::<u16>() {
303                            return Err(IncomingPacketError::Undecodable(
304                                GeneralError::ParseError("invalid acknowledgement packet size".into()).into(),
305                            ));
306                        }
307
308                        let num_acks =
309                            u16::from_be_bytes(incoming.plain_text[..size_of::<u16>()].try_into().map_err(|_| {
310                                IncomingPacketError::Undecodable(
311                                    GeneralError::ParseError("invalid num acks".into()).into(),
312                                )
313                            })?);
314
315                        if incoming.plain_text.len() < size_of::<u16>() + (num_acks as usize) * Acknowledgement::SIZE {
316                            return Err(IncomingPacketError::Undecodable(
317                                GeneralError::ParseError("invalid number of acknowledgements in packet".into()).into(),
318                            ));
319                        }
320                        tracing::trace!(num_acks, packet_type = "final", "received acknowledgement packet");
321
322                        // The contained payload represents an Acknowledgement
323                        IncomingPacket::Acknowledgement(
324                            IncomingAcknowledgementPacket {
325                                packet_tag: incoming.packet_tag,
326                                previous_hop: incoming.previous_hop,
327                                received_acks: incoming.plain_text
328                                    [size_of::<u16>()..size_of::<u16>() + num_acks as usize * Acknowledgement::SIZE]
329                                    .chunks_exact(Acknowledgement::SIZE)
330                                    .map(Acknowledgement::try_from)
331                                    .collect::<Result<Vec<_>, _>>()
332                                    .map_err(|e: GeneralError| IncomingPacketError::Undecodable(e.into()))?,
333                            }
334                            .into(),
335                        )
336                    }
337                    Some(ack_key) => IncomingPacket::Final(
338                        IncomingFinalPacket {
339                            packet_tag: incoming.packet_tag,
340                            previous_hop: incoming.previous_hop,
341                            sender: incoming.sender,
342                            replied_on_surb: replied_on_surb.get(),
343                            plain_text: incoming.plain_text,
344                            ack_key,
345                            info,
346                        }
347                        .into(),
348                    ),
349                };
350                #[cfg(feature = "trace-timing")]
351                tracing::trace!(
352                    total_ms = decode_start.elapsed().as_millis() as u64,
353                    packet_type = "final",
354                    "decode complete"
355                );
356                Ok(result)
357            }
358            HoprPacket::Forwarded(fwd) => {
359                // Phase 3: Ticket validation and replacement for forwarded packets
360                // Transform the ticket so it can be sent to the next hop
361                let (fwd, verified_unack_ticket) = trace_timed!("ticket_validation complete", {
362                    self.validate_and_replace_ticket(*fwd).map_err(|error| match error {
363                        // Distinguish ticket validation errors so that they can get extra treatment later
364                        HoprProtocolError::TicketValidationError(e) => {
365                            IncomingPacketError::InvalidTicket(previous_hop.into(), e)
366                        }
367                        e => IncomingPacketError::ProcessingError(previous_hop.into(), e),
368                    })?
369                });
370
371                let mut payload = BytesMut::with_capacity(HoprPacket::SIZE);
372                payload.put_slice(fwd.outgoing.packet.as_ref());
373                payload.put_slice(&fwd.outgoing.ticket.into_encoded());
374
375                #[cfg(feature = "trace-timing")]
376                tracing::trace!(
377                    total_ms = decode_start.elapsed().as_millis() as u64,
378                    packet_type = "forwarded",
379                    "decode complete"
380                );
381                Ok(IncomingPacket::Forwarded(
382                    IncomingForwardedPacket {
383                        packet_tag: fwd.packet_tag,
384                        previous_hop: fwd.previous_hop,
385                        next_hop: fwd.outgoing.next_hop,
386                        data: payload.freeze(),
387                        ack_challenge: fwd.outgoing.ack_challenge,
388                        received_ticket: verified_unack_ticket,
389                        ack_key_prev_hop: fwd.ack_key,
390                    }
391                    .into(),
392                ))
393            }
394            HoprPacket::Outgoing(_) => {
395                #[cfg(feature = "trace-timing")]
396                tracing::trace!(
397                    total_ms = decode_start.elapsed().as_millis() as u64,
398                    packet_type = "outgoing",
399                    "decode complete"
400                );
401                Err(IncomingPacketError::ProcessingError(
402                    previous_hop.into(),
403                    HoprProtocolError::InvalidState("cannot be outgoing packet"),
404                ))
405            }
406        }
407    }
408}