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    TlcErrData, TlcErrorCode, 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/// A TLC error decoded from an onion error packet.
36#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
37pub struct DecodedTlcErr {
38    pub error: TlcErr,
39    /// Index into the hop public keys passed to [`TlcErrPacket::decode`].
40    pub hop_index: usize,
41}
42
43/// Shared secret indicating no encryption (for origin node).
44pub const NO_SHARED_SECRET: [u8; 32] = [0u8; 32];
45const NO_ERROR_PACKET_HMAC: [u8; 32] = [0u8; 32];
46
47/// Maximum number of times to retry generating an onion packet with a fresh
48/// session key when fiber-sphinx returns [`fiber_sphinx::SphinxError::InvalidBlindingFactor`].
49///
50/// This event occurs when the SHA-256 derived blinding factor reduces to zero
51/// modulo the secp256k1 curve order. It is cryptographically improbable
52/// (probability ≈ 2⁻¹²⁸ per attempt), so 7 retries is effectively unreachable
53/// in practice while bounding the loop.
54pub const ONION_SESSION_KEY_MAX_ATTEMPTS: usize = 7;
55
56impl TlcErrPacket {
57    /// Create a new TlcErrPacket from raw payload bytes.
58    /// Erring node creates the error packet using the shared secret used in forwarding onion packet.
59    pub fn from_payload(payload: Vec<u8>, shared_secret: &[u8; 32]) -> Self {
60        let onion_packet = OnionErrorPacket::create(shared_secret, payload).into_bytes();
61        TlcErrPacket { onion_packet }
62    }
63
64    /// Check if this packet is plaintext (not encrypted).
65    pub fn is_plaintext(&self) -> bool {
66        self.onion_packet.len() >= 32 && self.onion_packet[0..32] == NO_ERROR_PACKET_HMAC
67    }
68
69    /// Intermediate node backwards the error to the previous hop using the shared secret
70    /// used in forwarding the onion packet.
71    pub fn backward(self, shared_secret: &[u8; 32]) -> Result<Self, TlcErrPacketError> {
72        if self.is_plaintext() {
73            return Err(TlcErrPacketError::PlaintextForward);
74        }
75
76        let onion_packet = OnionErrorPacket::from_bytes(self.onion_packet)
77            .xor_cipher_stream(shared_secret)
78            .into_bytes();
79        Ok(TlcErrPacket { onion_packet })
80    }
81}
82
83impl From<TlcErrPacket> for molecule_fiber::TlcErrPacket {
84    fn from(tlc_err_packet: TlcErrPacket) -> Self {
85        molecule_fiber::TlcErrPacket::new_builder()
86            .onion_packet(tlc_err_packet.onion_packet.pack())
87            .build()
88    }
89}
90
91impl From<molecule_fiber::TlcErrPacket> for TlcErrPacket {
92    fn from(tlc_err_packet: molecule_fiber::TlcErrPacket) -> Self {
93        TlcErrPacket {
94            onion_packet: tlc_err_packet.onion_packet().unpack(),
95        }
96    }
97}
98
99impl std::fmt::Display for TlcErrPacket {
100    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101        write!(f, "TlcErrPacket")
102    }
103}
104
105/// Always decrypting 27 times so the erroring node cannot learn its relative position in the route
106/// by performing a timing analysis if the sender were to retry the same route multiple times.
107const ERROR_DECODING_PASSES: usize = 27;
108
109impl TlcErrPacket {
110    /// Erring node creates the error packet using the shared secret used in forwarding onion packet.
111    /// Takes a structured `TlcErr` and serializes it.
112    pub fn new(tlc_fail: TlcErr, shared_secret: &[u8; 32]) -> Self {
113        let payload = tlc_fail.serialize();
114        Self::from_payload(payload, shared_secret)
115    }
116
117    /// Decode the onion error packet using the session key and hop public keys.
118    pub fn decode(
119        &self,
120        session_key: &[u8; 32],
121        hops_public_keys: Vec<crate::Pubkey>,
122    ) -> Option<DecodedTlcErr> {
123        use secp256k1::{PublicKey, SecretKey};
124
125        let hops_public_keys: Vec<PublicKey> = hops_public_keys
126            .iter()
127            .map(|k| PublicKey::from_slice(&k.0).expect("valid pubkey"))
128            .collect();
129        let session_key = SecretKey::from_slice(session_key)
130            .inspect_err(|err| {
131                tracing::error!(
132                    target: "fnn::fiber::types::TlcErrPacket",
133                    "decode session_key error={} key={}",
134                    err,
135                    hex::encode(session_key)
136                )
137            })
138            .ok()?;
139        OnionErrorPacket::from_bytes(self.onion_packet.clone())
140            .parse(hops_public_keys, session_key, TlcErr::deserialize)
141            .map(|(error, hop_index)| {
142                for _ in hop_index..ERROR_DECODING_PASSES {
143                    OnionErrorPacket::from_bytes(self.onion_packet.clone())
144                        .xor_cipher_stream(&NO_SHARED_SECRET);
145                }
146                DecodedTlcErr { error, hop_index }
147            })
148    }
149
150    /// Create a trampoline failure wrapper encrypted with the upstream outer shared secret.
151    /// Returning `None` means the caller would have produced a plaintext trampoline wrapper.
152    pub fn new_trampoline_failed(
153        error_code: TlcErrorCode,
154        node_id: crate::Pubkey,
155        inner_error_packet: Vec<u8>,
156        shared_secret: &[u8; 32],
157    ) -> Self {
158        let mut tlc_err = TlcErr::new(error_code);
159        tlc_err.set_extra_data(TlcErrData::TrampolineFailed {
160            node_id,
161            inner_error_packet,
162        });
163        Self::new(tlc_err, shared_secret)
164    }
165}
166
167#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
168pub enum TlcErrPacketError {
169    #[error("Refusing to forward plaintext TLC error packet")]
170    PlaintextForward,
171}
172
173/// Errors that can occur when processing an onion packet.
174#[derive(thiserror::Error, Debug)]
175pub enum OnionPacketError {
176    #[error("Fail to deserialize the hop data")]
177    InvalidHopData,
178
179    #[error("Unknown onion packet version: {0}")]
180    UnknownVersion(u8),
181
182    #[error("Sphinx protocol error")]
183    Sphinx(#[from] fiber_sphinx::SphinxError),
184}
185
186/// An encrypted onion packet for payment routing.
187#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
188pub struct PaymentOnionPacket {
189    /// The encrypted packet data.
190    data: Vec<u8>,
191}
192
193impl PaymentOnionPacket {
194    /// Create a new PaymentOnionPacket from raw data.
195    pub fn new(data: Vec<u8>) -> Self {
196        Self { data }
197    }
198
199    /// Get the raw packet data.
200    pub fn data(&self) -> &[u8] {
201        &self.data
202    }
203
204    /// Get the raw packet data as bytes slice (alias for `data()`).
205    pub fn as_bytes(&self) -> &[u8] {
206        &self.data
207    }
208
209    /// Consume self and return the raw data.
210    pub fn into_data(self) -> Vec<u8> {
211        self.data
212    }
213
214    /// Consume self and return the raw data as bytes (alias for `into_data()`).
215    pub fn into_bytes(self) -> Vec<u8> {
216        self.data
217    }
218
219    /// Convert into the raw Sphinx onion packet.
220    pub fn into_sphinx_onion_packet(self) -> Result<fiber_sphinx::OnionPacket, OnionPacketError> {
221        fiber_sphinx::OnionPacket::from_bytes_with_packet_data_len(
222            self.data,
223            PaymentSphinxCodec::PACKET_DATA_LEN,
224        )
225        .map_err(OnionPacketError::Sphinx)
226    }
227
228    /// Peels the next layer of the onion packet using the privkey of the current node.
229    ///
230    /// Returns errors when:
231    /// - This is the packet for the last hop.
232    /// - Fail to peel the packet using the given private key.
233    pub fn peel<C: secp256k1::Verification>(
234        self,
235        peeler: &crate::Privkey,
236        assoc_data: Option<&[u8]>,
237        secp_ctx: &secp256k1::Secp256k1<C>,
238    ) -> Result<PeeledPaymentOnionPacket, OnionPacketError> {
239        let peeled =
240            peel_sphinx_onion::<C, PaymentSphinxCodec>(self.data, peeler, assoc_data, secp_ctx)?;
241        Ok(PeeledPaymentOnionPacket {
242            current: peeled.current,
243            next: peeled.next.map(PaymentOnionPacket::new),
244            shared_secret: peeled.shared_secret,
245        })
246    }
247}
248
249/// A peeled payment onion packet, containing the current hop data and the packet for the next hop.
250#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
251pub struct PeeledPaymentOnionPacket {
252    /// The decrypted hop data for the current hop.
253    pub current: CurrentPaymentHopData,
254    /// The shared secret for `current` used for returning error. Set to all zeros for the origin node
255    /// who has no shared secret.
256    pub shared_secret: [u8; 32],
257    /// The packet for the next hop.
258    pub next: Option<PaymentOnionPacket>,
259}
260
261impl PeeledPaymentOnionPacket {
262    /// - `hops_info`: the first is the instruction for the origin node itself.
263    ///   Remaining elements are for each node to receive the packet.
264    pub fn create<C: secp256k1::Signing>(
265        session_key: crate::Privkey,
266        mut hops_infos: Vec<PaymentHopData>,
267        assoc_data: Option<Vec<u8>>,
268        secp_ctx: &secp256k1::Secp256k1<C>,
269    ) -> Result<Self, OnionPacketError> {
270        if hops_infos.is_empty() {
271            return Err(OnionPacketError::Sphinx(
272                fiber_sphinx::SphinxError::HopsIsEmpty,
273            ));
274        }
275
276        let hops_path = peeled_payment_hops_path(&hops_infos);
277        let current = hops_infos.remove(0);
278        let payloads = hops_infos;
279
280        let next = if !hops_path.is_empty() {
281            let bytes = create_sphinx_onion::<C, PaymentSphinxCodec>(
282                session_key,
283                hops_path,
284                payloads,
285                assoc_data,
286                secp_ctx,
287            )?;
288            Some(PaymentOnionPacket::new(bytes))
289        } else {
290            None
291        };
292
293        Ok(PeeledPaymentOnionPacket {
294            current: current.into(),
295            next,
296            // Use all zeros for the sender
297            shared_secret: NO_SHARED_SECRET,
298        })
299    }
300
301    /// Like [`Self::create`], but obtains the session key from `gen_session_key`.
302    ///
303    /// Retries with a fresh session key from `gen_session_key` on the
304    /// cryptographically improbable
305    /// [`fiber_sphinx::SphinxError::InvalidBlindingFactor`] event, up to
306    /// [`ONION_SESSION_KEY_MAX_ATTEMPTS`] times. Returns the chosen session key
307    /// alongside the packet on success so the caller can record which key was
308    /// used.
309    ///
310    /// Requires `hops_infos` to describe at least one downstream hop (so that
311    /// a sphinx onion is actually generated and a session key can be returned).
312    pub fn create_with_session_key_fn<C, F>(
313        gen_session_key: F,
314        mut hops_infos: Vec<PaymentHopData>,
315        assoc_data: Option<Vec<u8>>,
316        secp_ctx: &secp256k1::Secp256k1<C>,
317    ) -> Result<(Self, crate::Privkey), OnionPacketError>
318    where
319        C: secp256k1::Signing,
320        F: FnMut() -> crate::Privkey,
321    {
322        if hops_infos.is_empty() {
323            return Err(OnionPacketError::Sphinx(
324                fiber_sphinx::SphinxError::HopsIsEmpty,
325            ));
326        }
327
328        let hops_path = peeled_payment_hops_path(&hops_infos);
329        if hops_path.is_empty() {
330            return Err(OnionPacketError::Sphinx(
331                fiber_sphinx::SphinxError::HopsIsEmpty,
332            ));
333        }
334        let current = hops_infos.remove(0);
335        let payloads = hops_infos;
336
337        let (bytes, session_key) = create_sphinx_onion_with_session_key_fn::<
338            C,
339            PaymentSphinxCodec,
340            _,
341        >(
342            gen_session_key, hops_path, payloads, assoc_data, secp_ctx
343        )?;
344
345        Ok((
346            PeeledPaymentOnionPacket {
347                current: current.into(),
348                next: Some(PaymentOnionPacket::new(bytes)),
349                // Use all zeros for the sender
350                shared_secret: NO_SHARED_SECRET,
351            },
352            session_key,
353        ))
354    }
355
356    /// Returns true if this is the peeled packet for the last destination.
357    pub fn is_last(&self) -> bool {
358        self.next.is_none()
359    }
360
361    /// Returns the MPP custom records from the current hop data, if present.
362    pub fn mpp_custom_records(&self) -> Option<BasicMppPaymentData> {
363        self.current
364            .custom_records
365            .as_ref()
366            .and_then(BasicMppPaymentData::read)
367    }
368}
369
370fn peeled_payment_hops_path(hops_infos: &[PaymentHopData]) -> Vec<crate::Pubkey> {
371    hops_infos
372        .iter()
373        .map(|h| h.next_hop())
374        .take_while(Option::is_some)
375        .map(|opt| opt.expect("must be some"))
376        .collect()
377}
378
379/// Trait for encoding/decoding Sphinx onion packet hop data.
380///
381/// This trait abstracts over different onion packet formats (payment vs trampoline),
382/// allowing the generic `peel_sphinx_onion` and `create_sphinx_onion` functions to
383/// work with any format.
384pub trait SphinxOnionCodec {
385    type Decoded;
386    type Current;
387
388    const PACKET_DATA_LEN: usize;
389    /// The onion packet version used when creating new packets.
390    const CURRENT_VERSION: u8;
391
392    /// Packs the decoded data for transmission. Must use `CURRENT_VERSION` format.
393    fn pack(decoded: &Self::Decoded) -> Vec<u8>;
394    /// Unpacks data received from the network. Must handle all versions allowed by `is_version_allowed`.
395    fn unpack(version: u8, buf: &[u8]) -> Option<Self::Decoded>;
396    fn to_current(decoded: Self::Decoded) -> Self::Current;
397    /// Returns true if the given version is allowed for this codec.
398    fn is_version_allowed(version: u8) -> bool;
399    /// Returns the total length of hop data (including any headers) for the specified version.
400    fn hop_data_len(version: u8, buf: &[u8]) -> Option<usize>;
401}
402
403/// Internal result of peeling a Sphinx onion layer.
404pub struct SphinxPeeled<Current> {
405    pub current: Current,
406    pub shared_secret: [u8; 32],
407    pub next: Option<Vec<u8>>,
408}
409
410/// Peels one layer of a Sphinx onion packet.
411pub fn peel_sphinx_onion<C: secp256k1::Verification, Codec: SphinxOnionCodec>(
412    packet_bytes: Vec<u8>,
413    peeler: &crate::Privkey,
414    assoc_data: Option<&[u8]>,
415    secp_ctx: &secp256k1::Secp256k1<C>,
416) -> Result<SphinxPeeled<Codec::Current>, OnionPacketError> {
417    let sphinx_packet = fiber_sphinx::OnionPacket::from_bytes_with_packet_data_len(
418        packet_bytes,
419        Codec::PACKET_DATA_LEN,
420    )
421    .map_err(OnionPacketError::Sphinx)?;
422    let version = sphinx_packet.version;
423    if !Codec::is_version_allowed(version) {
424        return Err(OnionPacketError::UnknownVersion(version));
425    }
426    let shared_secret = sphinx_packet.shared_secret(&peeler.0);
427
428    let (new_current, new_next) = sphinx_packet
429        .peel(&peeler.0, assoc_data, secp_ctx, |buf| {
430            Codec::hop_data_len(version, buf)
431        })
432        .map_err(OnionPacketError::Sphinx)?;
433
434    let decoded = Codec::unpack(version, &new_current).ok_or(OnionPacketError::InvalidHopData)?;
435    let current = Codec::to_current(decoded);
436
437    // All zeros hmac indicates the last hop.
438    let next = new_next
439        .hmac
440        .iter()
441        .any(|b| *b != 0)
442        .then(|| new_next.into_bytes());
443
444    Ok(SphinxPeeled {
445        current,
446        shared_secret,
447        next,
448    })
449}
450
451/// Creates a Sphinx onion packet from hop data.
452pub fn create_sphinx_onion<C: secp256k1::Signing, Codec: SphinxOnionCodec>(
453    session_key: crate::Privkey,
454    hops_path: Vec<crate::Pubkey>,
455    payloads: Vec<Codec::Decoded>,
456    assoc_data: Option<Vec<u8>>,
457    secp_ctx: &secp256k1::Secp256k1<C>,
458) -> Result<Vec<u8>, OnionPacketError> {
459    if hops_path.is_empty() {
460        return Err(OnionPacketError::Sphinx(
461            fiber_sphinx::SphinxError::HopsIsEmpty,
462        ));
463    }
464    if hops_path.len() != payloads.len() {
465        return Err(OnionPacketError::InvalidHopData);
466    }
467
468    let hops_path: Vec<secp256k1::PublicKey> = hops_path
469        .into_iter()
470        .map(|pk| secp256k1::PublicKey::from_slice(&pk.0).expect("valid public key"))
471        .collect();
472    let hops_data: Vec<Vec<u8>> = payloads.iter().map(|p| Codec::pack(p)).collect();
473    let mut packet = fiber_sphinx::OnionPacket::create(
474        session_key.0,
475        hops_path,
476        hops_data,
477        assoc_data,
478        Codec::PACKET_DATA_LEN,
479        secp_ctx,
480    )
481    .map_err(OnionPacketError::Sphinx)?;
482    // Set the version to indicate which hop data format is used
483    packet.version = Codec::CURRENT_VERSION;
484    Ok(packet.into_bytes())
485}
486
487/// Creates a Sphinx onion packet, retrying with fresh session keys on
488/// [`fiber_sphinx::SphinxError::InvalidBlindingFactor`].
489///
490/// Calls `gen_session_key` to obtain each candidate session key. Retries are
491/// bounded by [`ONION_SESSION_KEY_MAX_ATTEMPTS`]; on exhaustion the last
492/// `InvalidBlindingFactor` error is returned. Any other error short-circuits.
493/// On success returns the encoded packet bytes together with the session key
494/// that produced it.
495pub fn create_sphinx_onion_with_session_key_fn<C, Codec, F>(
496    mut gen_session_key: F,
497    hops_path: Vec<crate::Pubkey>,
498    payloads: Vec<Codec::Decoded>,
499    assoc_data: Option<Vec<u8>>,
500    secp_ctx: &secp256k1::Secp256k1<C>,
501) -> Result<(Vec<u8>, crate::Privkey), OnionPacketError>
502where
503    C: secp256k1::Signing,
504    Codec: SphinxOnionCodec,
505    Codec::Decoded: Clone,
506    F: FnMut() -> crate::Privkey,
507{
508    let mut last_err: Option<fiber_sphinx::SphinxError> = None;
509    for _ in 0..ONION_SESSION_KEY_MAX_ATTEMPTS {
510        let session_key = gen_session_key();
511        match create_sphinx_onion::<C, Codec>(
512            session_key.clone(),
513            hops_path.clone(),
514            payloads.clone(),
515            assoc_data.clone(),
516            secp_ctx,
517        ) {
518            Ok(bytes) => return Ok((bytes, session_key)),
519            Err(OnionPacketError::Sphinx(
520                err @ fiber_sphinx::SphinxError::InvalidBlindingFactor,
521            )) => {
522                last_err = Some(err);
523                continue;
524            }
525            Err(other) => return Err(other),
526        }
527    }
528    Err(OnionPacketError::Sphinx(last_err.expect(
529        "ONION_SESSION_KEY_MAX_ATTEMPTS is non-zero, so at least one attempt ran",
530    )))
531}
532
533/// Codec for payment onion packets (used by the outer payment onion layer).
534pub struct PaymentSphinxCodec;
535
536impl PaymentSphinxCodec {
537    /// Packs hop data according to the specified onion packet version.
538    /// - Version 0: Prepends u64 BE length header before molecule data.
539    /// - Version 1: Returns molecule-serialized data directly (uses molecule's native u32 LE length).
540    pub fn pack_hop_data(version: u8, hop_data: &PaymentHopData) -> Vec<u8> {
541        match version {
542            ONION_PACKET_VERSION_V0 => pack_len_prefixed(hop_data.serialize()),
543            ONION_PACKET_VERSION_V1 => hop_data.serialize(),
544            other => {
545                debug_assert!(
546                    false,
547                    "Unknown onion packet version {} passed to pack_hop_data; defaulting to v1",
548                    other
549                );
550                hop_data.serialize()
551            }
552        }
553    }
554
555    /// Unpacks hop data according to the specified onion packet version.
556    /// - Version 0: Skips u64 BE length header, deserializes molecule data.
557    /// - Version 1: Deserializes molecule data directly (using molecule's u32 LE length).
558    /// - Unknown versions: Returns None to fail fast and avoid silent misparsing.
559    pub fn unpack_hop_data(version: u8, buf: &[u8]) -> Option<PaymentHopData> {
560        match version {
561            ONION_PACKET_VERSION_V0 => {
562                let payload = unpack_len_prefixed_payload(buf)?;
563                PaymentHopData::deserialize(payload)
564            }
565            ONION_PACKET_VERSION_V1 => {
566                let len = molecule_table_data_len(buf)?;
567                if buf.len() < len {
568                    return None;
569                }
570                PaymentHopData::deserialize(&buf[..len])
571            }
572            _ => None,
573        }
574    }
575}
576
577impl SphinxOnionCodec for PaymentSphinxCodec {
578    type Decoded = PaymentHopData;
579    type Current = CurrentPaymentHopData;
580
581    const PACKET_DATA_LEN: usize = PACKET_DATA_LEN;
582    // Send v1, accept both v0 and v1 for backward compatibility
583    const CURRENT_VERSION: u8 = ONION_PACKET_VERSION_V1;
584
585    fn pack(decoded: &Self::Decoded) -> Vec<u8> {
586        Self::pack_hop_data(Self::CURRENT_VERSION, decoded)
587    }
588
589    fn unpack(version: u8, buf: &[u8]) -> Option<Self::Decoded> {
590        Self::unpack_hop_data(version, buf)
591    }
592
593    fn to_current(decoded: Self::Decoded) -> Self::Current {
594        decoded.into()
595    }
596
597    fn is_version_allowed(version: u8) -> bool {
598        // Accept both v0 and v1 for backward compatibility
599        version <= ONION_PACKET_VERSION_V1
600    }
601
602    fn hop_data_len(version: u8, buf: &[u8]) -> Option<usize> {
603        match version {
604            ONION_PACKET_VERSION_V0 => len_with_u64_header(buf),
605            ONION_PACKET_VERSION_V1 => molecule_table_data_len(buf),
606            _ => None,
607        }
608    }
609}
610
611/// Packs data with u64 BE length header (v0 format).
612/// Used by Trampoline (bincode serialization) and v0 payment hop data.
613pub fn pack_len_prefixed(mut payload: Vec<u8>) -> Vec<u8> {
614    let mut packed = (payload.len() as u64).to_be_bytes().to_vec();
615    packed.append(&mut payload);
616    packed
617}
618
619/// Unpacks length-prefixed payload (v0 format): [u64 BE length][data].
620pub fn unpack_len_prefixed_payload(buf: &[u8]) -> Option<&[u8]> {
621    let len = len_with_u64_header(buf)?;
622    if buf.len() < len {
623        return None;
624    }
625    buf.get(HOP_DATA_HEAD_LEN..len)
626}
627
628/// Returns the total length with u64 BE header: [u64 BE length] + data.
629/// Used by v0 format (Trampoline and legacy payment hop data).
630pub fn len_with_u64_header(buf: &[u8]) -> Option<usize> {
631    if buf.len() < HOP_DATA_HEAD_LEN {
632        return None;
633    }
634    let len = u64::from_be_bytes(
635        buf[0..HOP_DATA_HEAD_LEN]
636            .try_into()
637            .expect("u64 from slice"),
638    );
639    // Safe conversion: check value fits in usize and addition won't overflow.
640    // Note: Caller (fiber-sphinx) is responsible for validating len against packet bounds.
641    usize::try_from(len).ok()?.checked_add(HOP_DATA_HEAD_LEN)
642}
643
644/// Returns the total length from molecule's native u32 LE header.
645/// Used by v1 format (current payment hop data).
646pub fn molecule_table_data_len(buf: &[u8]) -> Option<usize> {
647    if buf.len() < molecule::NUMBER_SIZE {
648        return None;
649    }
650    let len = molecule::unpack_number(buf) as usize;
651    // Molecule size must be at least NUMBER_SIZE (4 bytes for the length header itself).
652    // Reject malformed data claiming a smaller size.
653    if len < molecule::NUMBER_SIZE {
654        return None;
655    }
656    Some(len)
657}
658
659/// Helper to store the trampoline onion packet inside `custom_records`.
660///
661/// This embeds the trampoline onion bytes as a custom record entry so that the molecule
662/// `PaymentHopData` schema stays at 7 fields — matching v0.6.1 — and old onion packets
663/// created before trampoline support can still be deserialized.
664pub struct TrampolineOnionData;
665
666impl TrampolineOnionData {
667    /// Custom record key for embedded trampoline onion data.
668    /// `BasicMppPaymentData` uses `USER_CUSTOM_RECORDS_MAX_INDEX + 1` (65536).
669    pub const CUSTOM_RECORD_KEY: u32 = USER_CUSTOM_RECORDS_MAX_INDEX + 2;
670
671    pub fn write(data: Vec<u8>, custom_records: &mut PaymentCustomRecords) {
672        custom_records.data.insert(Self::CUSTOM_RECORD_KEY, data);
673    }
674
675    pub fn read(custom_records: &PaymentCustomRecords) -> Option<Vec<u8>> {
676        custom_records.data.get(&Self::CUSTOM_RECORD_KEY).cloned()
677    }
678}