Skip to main content

fiber_types/
onion.rs

1//! Onion routing types and Sphinx packet processing.
2//!
3//! This module contains the core types and functions for creating, peeling, and
4//! processing Sphinx onion packets used in payment routing and error propagation.
5
6use crate::gen::fiber as molecule_fiber;
7use crate::payment::{
8    BasicMppPaymentData, CurrentPaymentHopData, PaymentCustomRecords, PaymentHopData, TlcErr,
9    USER_CUSTOM_RECORDS_MAX_INDEX,
10};
11use ckb_types::prelude::{Pack, Unpack};
12use fiber_sphinx::OnionErrorPacket;
13use molecule::prelude::{Builder, Entity};
14use serde::{Deserialize, Serialize};
15
16/// Onion packet version with u64 BE length header for hop data.
17pub const ONION_PACKET_VERSION_V0: u8 = 0;
18/// Onion packet version with molecule's native u32 LE length for hop data.
19pub const ONION_PACKET_VERSION_V1: u8 = 1;
20
21const PACKET_DATA_LEN: usize = 6500;
22
23/// Length of the u64 BE header used in v0 hop data format.
24const HOP_DATA_HEAD_LEN: usize = std::mem::size_of::<u64>();
25
26/// An encrypted error packet for TLC failures.
27/// The sender should decode it and then decide what to do with the error.
28/// Note: this is supposed to be only accessible by the sender, and it's not reliable since it
29/// is not placed on-chain due to the possibility of hop failure.
30#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
31pub struct TlcErrPacket {
32    pub onion_packet: Vec<u8>,
33}
34
35/// Shared secret indicating no encryption (for origin node).
36pub const NO_SHARED_SECRET: [u8; 32] = [0u8; 32];
37const NO_ERROR_PACKET_HMAC: [u8; 32] = [0u8; 32];
38
39impl TlcErrPacket {
40    /// Create a new TlcErrPacket from raw payload bytes.
41    /// Erring node creates the error packet using the shared secret used in forwarding onion packet.
42    /// Use all zeros (NO_SHARED_SECRET) for the origin node.
43    pub fn from_payload(payload: Vec<u8>, shared_secret: &[u8; 32]) -> Self {
44        let onion_packet = if shared_secret != &NO_SHARED_SECRET {
45            OnionErrorPacket::create(shared_secret, payload)
46        } else {
47            OnionErrorPacket::concat(NO_ERROR_PACKET_HMAC, payload)
48        }
49        .into_bytes();
50        TlcErrPacket { onion_packet }
51    }
52
53    /// Check if this packet is plaintext (not encrypted).
54    pub fn is_plaintext(&self) -> bool {
55        self.onion_packet.len() >= 32 && self.onion_packet[0..32] == NO_ERROR_PACKET_HMAC
56    }
57
58    /// Intermediate node backwards the error to the previous hop using the shared secret
59    /// used in forwarding the onion packet.
60    pub fn backward(self, shared_secret: &[u8; 32]) -> Self {
61        if !self.is_plaintext() {
62            let onion_packet = OnionErrorPacket::from_bytes(self.onion_packet)
63                .xor_cipher_stream(shared_secret)
64                .into_bytes();
65            TlcErrPacket { onion_packet }
66        } else {
67            // If it is not encrypted, just send back as it is.
68            self
69        }
70    }
71}
72
73impl From<TlcErrPacket> for molecule_fiber::TlcErrPacket {
74    fn from(tlc_err_packet: TlcErrPacket) -> Self {
75        molecule_fiber::TlcErrPacket::new_builder()
76            .onion_packet(tlc_err_packet.onion_packet.pack())
77            .build()
78    }
79}
80
81impl From<molecule_fiber::TlcErrPacket> for TlcErrPacket {
82    fn from(tlc_err_packet: molecule_fiber::TlcErrPacket) -> Self {
83        TlcErrPacket {
84            onion_packet: tlc_err_packet.onion_packet().unpack(),
85        }
86    }
87}
88
89impl std::fmt::Display for TlcErrPacket {
90    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91        write!(f, "TlcErrPacket")
92    }
93}
94
95/// Always decrypting 27 times so the erroring node cannot learn its relative position in the route
96/// by performing a timing analysis if the sender were to retry the same route multiple times.
97const ERROR_DECODING_PASSES: usize = 27;
98
99impl TlcErrPacket {
100    /// Erring node creates the error packet using the shared secret used in forwarding onion packet.
101    /// Use all zeros for the origin node. Takes a structured `TlcErr` and serializes it.
102    pub fn new(tlc_fail: TlcErr, shared_secret: &[u8; 32]) -> Self {
103        let payload = tlc_fail.serialize();
104        Self::from_payload(payload, shared_secret)
105    }
106
107    /// Decode the onion error packet using the session key and hop public keys.
108    pub fn decode(
109        &self,
110        session_key: &[u8; 32],
111        hops_public_keys: Vec<crate::Pubkey>,
112    ) -> Option<TlcErr> {
113        use secp256k1::{PublicKey, SecretKey};
114
115        if self.is_plaintext() {
116            let error = TlcErr::deserialize(&self.onion_packet[32..]);
117            if error.is_some() {
118                return error;
119            }
120        }
121
122        let hops_public_keys: Vec<PublicKey> = hops_public_keys
123            .iter()
124            .map(|k| PublicKey::from_slice(&k.0).expect("valid pubkey"))
125            .collect();
126        let session_key = SecretKey::from_slice(session_key)
127            .inspect_err(|err| {
128                tracing::error!(
129                    target: "fnn::fiber::types::TlcErrPacket",
130                    "decode session_key error={} key={}",
131                    err,
132                    hex::encode(session_key)
133                )
134            })
135            .ok()?;
136        OnionErrorPacket::from_bytes(self.onion_packet.clone())
137            .parse(hops_public_keys, session_key, TlcErr::deserialize)
138            .map(|(error, hop_index)| {
139                for _ in hop_index..ERROR_DECODING_PASSES {
140                    OnionErrorPacket::from_bytes(self.onion_packet.clone())
141                        .xor_cipher_stream(&NO_SHARED_SECRET);
142                }
143                error
144            })
145    }
146}
147
148/// Errors that can occur when processing an onion packet.
149#[derive(thiserror::Error, Debug)]
150pub enum OnionPacketError {
151    #[error("Fail to deserialize the hop data")]
152    InvalidHopData,
153
154    #[error("Unknown onion packet version: {0}")]
155    UnknownVersion(u8),
156
157    #[error("Sphinx protocol error")]
158    Sphinx(#[from] fiber_sphinx::SphinxError),
159}
160
161/// An encrypted onion packet for payment routing.
162#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
163pub struct PaymentOnionPacket {
164    /// The encrypted packet data.
165    data: Vec<u8>,
166}
167
168impl PaymentOnionPacket {
169    /// Create a new PaymentOnionPacket from raw data.
170    pub fn new(data: Vec<u8>) -> Self {
171        Self { data }
172    }
173
174    /// Get the raw packet data.
175    pub fn data(&self) -> &[u8] {
176        &self.data
177    }
178
179    /// Get the raw packet data as bytes slice (alias for `data()`).
180    pub fn as_bytes(&self) -> &[u8] {
181        &self.data
182    }
183
184    /// Consume self and return the raw data.
185    pub fn into_data(self) -> Vec<u8> {
186        self.data
187    }
188
189    /// Consume self and return the raw data as bytes (alias for `into_data()`).
190    pub fn into_bytes(self) -> Vec<u8> {
191        self.data
192    }
193
194    /// Convert into the raw Sphinx onion packet.
195    pub fn into_sphinx_onion_packet(self) -> Result<fiber_sphinx::OnionPacket, OnionPacketError> {
196        fiber_sphinx::OnionPacket::from_bytes(self.data).map_err(OnionPacketError::Sphinx)
197    }
198
199    /// Peels the next layer of the onion packet using the privkey of the current node.
200    ///
201    /// Returns errors when:
202    /// - This is the packet for the last hop.
203    /// - Fail to peel the packet using the given private key.
204    pub fn peel<C: secp256k1::Verification>(
205        self,
206        peeler: &crate::Privkey,
207        assoc_data: Option<&[u8]>,
208        secp_ctx: &secp256k1::Secp256k1<C>,
209    ) -> Result<PeeledPaymentOnionPacket, OnionPacketError> {
210        let peeled =
211            peel_sphinx_onion::<C, PaymentSphinxCodec>(self.data, peeler, assoc_data, secp_ctx)?;
212        Ok(PeeledPaymentOnionPacket {
213            current: peeled.current,
214            next: peeled.next.map(PaymentOnionPacket::new),
215            shared_secret: peeled.shared_secret,
216        })
217    }
218}
219
220/// A peeled payment onion packet, containing the current hop data and the packet for the next hop.
221#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
222pub struct PeeledPaymentOnionPacket {
223    /// The decrypted hop data for the current hop.
224    pub current: CurrentPaymentHopData,
225    /// The shared secret for `current` used for returning error. Set to all zeros for the origin node
226    /// who has no shared secret.
227    pub shared_secret: [u8; 32],
228    /// The packet for the next hop.
229    pub next: Option<PaymentOnionPacket>,
230}
231
232impl PeeledPaymentOnionPacket {
233    /// - `hops_info`: the first is the instruction for the origin node itself.
234    ///   Remaining elements are for each node to receive the packet.
235    pub fn create<C: secp256k1::Signing>(
236        session_key: crate::Privkey,
237        mut hops_infos: Vec<PaymentHopData>,
238        assoc_data: Option<Vec<u8>>,
239        secp_ctx: &secp256k1::Secp256k1<C>,
240    ) -> Result<Self, OnionPacketError> {
241        if hops_infos.is_empty() {
242            return Err(OnionPacketError::Sphinx(
243                fiber_sphinx::SphinxError::HopsIsEmpty,
244            ));
245        }
246
247        let hops_path: Vec<crate::Pubkey> = hops_infos
248            .iter()
249            .map(|h| h.next_hop())
250            .take_while(Option::is_some)
251            .map(|opt| opt.expect("must be some"))
252            .collect();
253
254        // Keep the original hop ordering for payloads.
255        let current = hops_infos.remove(0);
256        let payloads = hops_infos;
257
258        let next = if !hops_path.is_empty() {
259            Some(PaymentOnionPacket::new(create_sphinx_onion::<
260                C,
261                PaymentSphinxCodec,
262            >(
263                session_key,
264                hops_path,
265                payloads,
266                assoc_data,
267                secp_ctx,
268            )?))
269        } else {
270            None
271        };
272
273        Ok(PeeledPaymentOnionPacket {
274            current: current.into(),
275            next,
276            // Use all zeros for the sender
277            shared_secret: NO_SHARED_SECRET,
278        })
279    }
280
281    /// Returns true if this is the peeled packet for the last destination.
282    pub fn is_last(&self) -> bool {
283        self.next.is_none()
284    }
285
286    /// Returns the MPP custom records from the current hop data, if present.
287    pub fn mpp_custom_records(&self) -> Option<BasicMppPaymentData> {
288        self.current
289            .custom_records
290            .as_ref()
291            .and_then(BasicMppPaymentData::read)
292    }
293}
294
295/// Trait for encoding/decoding Sphinx onion packet hop data.
296///
297/// This trait abstracts over different onion packet formats (payment vs trampoline),
298/// allowing the generic `peel_sphinx_onion` and `create_sphinx_onion` functions to
299/// work with any format.
300pub trait SphinxOnionCodec {
301    type Decoded;
302    type Current;
303
304    const PACKET_DATA_LEN: usize;
305    /// The onion packet version used when creating new packets.
306    const CURRENT_VERSION: u8;
307
308    /// Packs the decoded data for transmission. Must use `CURRENT_VERSION` format.
309    fn pack(decoded: &Self::Decoded) -> Vec<u8>;
310    /// Unpacks data received from the network. Must handle all versions allowed by `is_version_allowed`.
311    fn unpack(version: u8, buf: &[u8]) -> Option<Self::Decoded>;
312    fn to_current(decoded: Self::Decoded) -> Self::Current;
313    /// Returns true if the given version is allowed for this codec.
314    fn is_version_allowed(version: u8) -> bool;
315    /// Returns the total length of hop data (including any headers) for the specified version.
316    fn hop_data_len(version: u8, buf: &[u8]) -> Option<usize>;
317}
318
319/// Internal result of peeling a Sphinx onion layer.
320pub struct SphinxPeeled<Current> {
321    pub current: Current,
322    pub shared_secret: [u8; 32],
323    pub next: Option<Vec<u8>>,
324}
325
326/// Peels one layer of a Sphinx onion packet.
327pub fn peel_sphinx_onion<C: secp256k1::Verification, Codec: SphinxOnionCodec>(
328    packet_bytes: Vec<u8>,
329    peeler: &crate::Privkey,
330    assoc_data: Option<&[u8]>,
331    secp_ctx: &secp256k1::Secp256k1<C>,
332) -> Result<SphinxPeeled<Codec::Current>, OnionPacketError> {
333    let sphinx_packet =
334        fiber_sphinx::OnionPacket::from_bytes(packet_bytes).map_err(OnionPacketError::Sphinx)?;
335    let version = sphinx_packet.version;
336    if !Codec::is_version_allowed(version) {
337        return Err(OnionPacketError::UnknownVersion(version));
338    }
339    let shared_secret = sphinx_packet.shared_secret(&peeler.0);
340
341    let (new_current, new_next) = sphinx_packet
342        .peel(&peeler.0, assoc_data, secp_ctx, |buf| {
343            Codec::hop_data_len(version, buf)
344        })
345        .map_err(OnionPacketError::Sphinx)?;
346
347    let decoded = Codec::unpack(version, &new_current).ok_or(OnionPacketError::InvalidHopData)?;
348    let current = Codec::to_current(decoded);
349
350    // All zeros hmac indicates the last hop.
351    let next = new_next
352        .hmac
353        .iter()
354        .any(|b| *b != 0)
355        .then(|| new_next.into_bytes());
356
357    Ok(SphinxPeeled {
358        current,
359        shared_secret,
360        next,
361    })
362}
363
364/// Creates a Sphinx onion packet from hop data.
365pub fn create_sphinx_onion<C: secp256k1::Signing, Codec: SphinxOnionCodec>(
366    session_key: crate::Privkey,
367    hops_path: Vec<crate::Pubkey>,
368    payloads: Vec<Codec::Decoded>,
369    assoc_data: Option<Vec<u8>>,
370    secp_ctx: &secp256k1::Secp256k1<C>,
371) -> Result<Vec<u8>, OnionPacketError> {
372    let hops_path: Vec<secp256k1::PublicKey> = hops_path
373        .into_iter()
374        .map(|pk| secp256k1::PublicKey::from_slice(&pk.0).expect("valid public key"))
375        .collect();
376    let hops_data: Vec<Vec<u8>> = payloads.iter().map(|p| Codec::pack(p)).collect();
377    let mut packet = fiber_sphinx::OnionPacket::create(
378        session_key.0,
379        hops_path,
380        hops_data,
381        assoc_data,
382        Codec::PACKET_DATA_LEN,
383        secp_ctx,
384    )
385    .map_err(OnionPacketError::Sphinx)?;
386    // Set the version to indicate which hop data format is used
387    packet.version = Codec::CURRENT_VERSION;
388    Ok(packet.into_bytes())
389}
390
391/// Codec for payment onion packets (used by the outer payment onion layer).
392pub struct PaymentSphinxCodec;
393
394impl PaymentSphinxCodec {
395    /// Packs hop data according to the specified onion packet version.
396    /// - Version 0: Prepends u64 BE length header before molecule data.
397    /// - Version 1: Returns molecule-serialized data directly (uses molecule's native u32 LE length).
398    pub fn pack_hop_data(version: u8, hop_data: &PaymentHopData) -> Vec<u8> {
399        match version {
400            ONION_PACKET_VERSION_V0 => pack_len_prefixed(hop_data.serialize()),
401            ONION_PACKET_VERSION_V1 => hop_data.serialize(),
402            other => {
403                debug_assert!(
404                    false,
405                    "Unknown onion packet version {} passed to pack_hop_data; defaulting to v1",
406                    other
407                );
408                hop_data.serialize()
409            }
410        }
411    }
412
413    /// Unpacks hop data according to the specified onion packet version.
414    /// - Version 0: Skips u64 BE length header, deserializes molecule data.
415    /// - Version 1: Deserializes molecule data directly (using molecule's u32 LE length).
416    /// - Unknown versions: Returns None to fail fast and avoid silent misparsing.
417    pub fn unpack_hop_data(version: u8, buf: &[u8]) -> Option<PaymentHopData> {
418        match version {
419            ONION_PACKET_VERSION_V0 => {
420                let payload = unpack_len_prefixed_payload(buf)?;
421                PaymentHopData::deserialize(payload)
422            }
423            ONION_PACKET_VERSION_V1 => {
424                let len = molecule_table_data_len(buf)?;
425                if buf.len() < len {
426                    return None;
427                }
428                PaymentHopData::deserialize(&buf[..len])
429            }
430            _ => None,
431        }
432    }
433}
434
435impl SphinxOnionCodec for PaymentSphinxCodec {
436    type Decoded = PaymentHopData;
437    type Current = CurrentPaymentHopData;
438
439    const PACKET_DATA_LEN: usize = PACKET_DATA_LEN;
440    // Send v1, accept both v0 and v1 for backward compatibility
441    const CURRENT_VERSION: u8 = ONION_PACKET_VERSION_V1;
442
443    fn pack(decoded: &Self::Decoded) -> Vec<u8> {
444        Self::pack_hop_data(Self::CURRENT_VERSION, decoded)
445    }
446
447    fn unpack(version: u8, buf: &[u8]) -> Option<Self::Decoded> {
448        Self::unpack_hop_data(version, buf)
449    }
450
451    fn to_current(decoded: Self::Decoded) -> Self::Current {
452        decoded.into()
453    }
454
455    fn is_version_allowed(version: u8) -> bool {
456        // Accept both v0 and v1 for backward compatibility
457        version <= ONION_PACKET_VERSION_V1
458    }
459
460    fn hop_data_len(version: u8, buf: &[u8]) -> Option<usize> {
461        match version {
462            ONION_PACKET_VERSION_V0 => len_with_u64_header(buf),
463            ONION_PACKET_VERSION_V1 => molecule_table_data_len(buf),
464            _ => None,
465        }
466    }
467}
468
469/// Packs data with u64 BE length header (v0 format).
470/// Used by Trampoline (bincode serialization) and v0 payment hop data.
471pub fn pack_len_prefixed(mut payload: Vec<u8>) -> Vec<u8> {
472    let mut packed = (payload.len() as u64).to_be_bytes().to_vec();
473    packed.append(&mut payload);
474    packed
475}
476
477/// Unpacks length-prefixed payload (v0 format): [u64 BE length][data].
478pub fn unpack_len_prefixed_payload(buf: &[u8]) -> Option<&[u8]> {
479    let len = len_with_u64_header(buf)?;
480    if buf.len() < len {
481        return None;
482    }
483    buf.get(HOP_DATA_HEAD_LEN..len)
484}
485
486/// Returns the total length with u64 BE header: [u64 BE length] + data.
487/// Used by v0 format (Trampoline and legacy payment hop data).
488pub fn len_with_u64_header(buf: &[u8]) -> Option<usize> {
489    if buf.len() < HOP_DATA_HEAD_LEN {
490        return None;
491    }
492    let len = u64::from_be_bytes(
493        buf[0..HOP_DATA_HEAD_LEN]
494            .try_into()
495            .expect("u64 from slice"),
496    );
497    // Safe conversion: check value fits in usize and addition won't overflow.
498    // Note: Caller (fiber-sphinx) is responsible for validating len against packet bounds.
499    usize::try_from(len).ok()?.checked_add(HOP_DATA_HEAD_LEN)
500}
501
502/// Returns the total length from molecule's native u32 LE header.
503/// Used by v1 format (current payment hop data).
504pub fn molecule_table_data_len(buf: &[u8]) -> Option<usize> {
505    if buf.len() < molecule::NUMBER_SIZE {
506        return None;
507    }
508    let len = molecule::unpack_number(buf) as usize;
509    // Molecule size must be at least NUMBER_SIZE (4 bytes for the length header itself).
510    // Reject malformed data claiming a smaller size.
511    if len < molecule::NUMBER_SIZE {
512        return None;
513    }
514    Some(len)
515}
516
517/// Helper to store the trampoline onion packet inside `custom_records`.
518///
519/// This embeds the trampoline onion bytes as a custom record entry so that the molecule
520/// `PaymentHopData` schema stays at 7 fields — matching v0.6.1 — and old onion packets
521/// created before trampoline support can still be deserialized.
522pub struct TrampolineOnionData;
523
524impl TrampolineOnionData {
525    /// Custom record key for embedded trampoline onion data.
526    /// `BasicMppPaymentData` uses `USER_CUSTOM_RECORDS_MAX_INDEX + 1` (65536).
527    pub const CUSTOM_RECORD_KEY: u32 = USER_CUSTOM_RECORDS_MAX_INDEX + 2;
528
529    pub fn write(data: Vec<u8>, custom_records: &mut PaymentCustomRecords) {
530        custom_records.data.insert(Self::CUSTOM_RECORD_KEY, data);
531    }
532
533    pub fn read(custom_records: &PaymentCustomRecords) -> Option<Vec<u8>> {
534        custom_records.data.get(&Self::CUSTOM_RECORD_KEY).cloned()
535    }
536}