bolt4

Onion routing protocol, per BOLT #4 (docs.ppad.tech/bolt4).
git clone git://git.ppad.tech/bolt4.git
Log | Files | Refs | README | LICENSE

Types.hs (16365B)


      1 {-# OPTIONS_HADDOCK hide #-}
      2 {-# LANGUAGE DeriveGeneric #-}
      3 
      4 -- |
      5 -- Module: Lightning.Protocol.BOLT4.Types
      6 -- Copyright: (c) 2025 Jared Tobin
      7 -- License: MIT
      8 -- Maintainer: Jared Tobin <jared@ppad.tech>
      9 --
     10 -- Data types for BOLT #4.
     11 
     12 module Lightning.Protocol.BOLT4.Types (
     13   -- * Onion packets
     14     OnionPacket(..)
     15   , HopPayloads(..)
     16   , hop_payloads
     17   , un_hop_payloads
     18   , Hmac32(..)
     19   , hmac32
     20   , un_hmac32
     21 
     22   -- * Hop payloads
     23   , HopPayload(..)
     24   , empty_hop_payload
     25   , PaymentData(..)
     26   , PaymentSecret(..)
     27   , payment_secret
     28   , un_payment_secret
     29 
     30   -- * Route blinding
     31   , BlindedPath(..)
     32   , BlindedHop(..)
     33   , BlindedHopData(..)
     34   , empty_blinded_hop_data
     35   , PaymentRelay(..)
     36   , PaymentConstraints(..)
     37   , BlindedInfo(..)
     38 
     39   -- * Failure messages
     40   , FailureMessage(..)
     41   , Failure(..)
     42   , OnionHash(..)
     43   , onion_hash
     44   , un_onion_hash
     45   , failure_code
     46   , is_badonion
     47   , is_perm
     48   , is_node
     49   , is_update
     50 
     51   -- * Errors
     52   , DecodeError(..)
     53   , EncodeError(..)
     54   , ProcessError(..)
     55   ) where
     56 
     57 import Control.DeepSeq (NFData(..))
     58 import Data.Bits ((.&.))
     59 import qualified Data.ByteString as BS
     60 import Data.Word (Word8, Word16, Word32, Word64)
     61 import GHC.Generics (Generic)
     62 import qualified Lightning.Protocol.BOLT1 as BOLT1
     63 import qualified Lightning.Protocol.BOLT4.Prim as P
     64 import qualified Lightning.Protocol.BOLT9 as BOLT9
     65 
     66 -- onion packets --------------------------------------------------------------
     67 
     68 -- | An onion packet (@onion_packet@): 1366 bytes on the wire.
     69 data OnionPacket = OnionPacket
     70   { onion_version      :: {-# UNPACK #-} !Word8
     71     -- ^ the version byte; 0 in this version of the protocol
     72   , onion_public_key   :: !BOLT1.Point
     73     -- ^ the ephemeral public key
     74   , onion_hop_payloads :: !HopPayloads
     75     -- ^ the obfuscated hop payloads
     76   , onion_hmac         :: !Hmac32
     77     -- ^ the HMAC authenticating the packet
     78   } deriving (Eq, Show, Generic)
     79 
     80 instance NFData OnionPacket
     81 
     82 -- | The 1300-byte @hop_payloads@ field of an onion packet.
     83 newtype HopPayloads = HopPayloads BS.ByteString
     84   deriving (Eq, Show, Generic)
     85 
     86 instance NFData HopPayloads
     87 
     88 -- | Construct t'HopPayloads' from exactly 1300 bytes.
     89 --
     90 --   >>> let Just hp = hop_payloads (BS.replicate 1300 0)
     91 --   >>> BS.length (un_hop_payloads hp)
     92 --   1300
     93 --   >>> hop_payloads (BS.replicate 1299 0)
     94 --   Nothing
     95 hop_payloads :: BS.ByteString -> Maybe HopPayloads
     96 hop_payloads bs
     97   | BS.length bs == 1300 = Just (HopPayloads bs)
     98   | otherwise            = Nothing
     99 {-# INLINE hop_payloads #-}
    100 
    101 -- | The bytes of t'HopPayloads'.
    102 un_hop_payloads :: HopPayloads -> BS.ByteString
    103 un_hop_payloads (HopPayloads bs) = bs
    104 {-# INLINE un_hop_payloads #-}
    105 
    106 -- | A 32-byte HMAC.
    107 newtype Hmac32 = Hmac32 BS.ByteString
    108   deriving (Eq, Show, Generic)
    109 
    110 instance NFData Hmac32
    111 
    112 -- | Construct an t'Hmac32' from exactly 32 bytes.
    113 --
    114 --   >>> fmap (BS.length . un_hmac32) (hmac32 (BS.replicate 32 0))
    115 --   Just 32
    116 --   >>> hmac32 "too short"
    117 --   Nothing
    118 hmac32 :: BS.ByteString -> Maybe Hmac32
    119 hmac32 bs
    120   | BS.length bs == 32 = Just (Hmac32 bs)
    121   | otherwise          = Nothing
    122 {-# INLINE hmac32 #-}
    123 
    124 -- | The bytes of an t'Hmac32'.
    125 un_hmac32 :: Hmac32 -> BS.ByteString
    126 un_hmac32 (Hmac32 bs) = bs
    127 {-# INLINE un_hmac32 #-}
    128 
    129 -- hop payloads ---------------------------------------------------------------
    130 
    131 -- | A hop's @payload@ TLV stream.
    132 --
    133 --   Each known record has a typed field. 'hp_extra' holds the remaining
    134 --   records: decoding fills it with the unknown odd records (unknown even
    135 --   records are rejected), and encoding accepts any record in it whose
    136 --   type isn't one of the known ones.
    137 data HopPayload = HopPayload
    138   { hp_amt_to_forward      :: !(Maybe BOLT1.MilliSatoshi)
    139     -- ^ type 2: @amt_to_forward@
    140   , hp_outgoing_cltv_value :: !(Maybe Word32)
    141     -- ^ type 4: @outgoing_cltv_value@
    142   , hp_short_channel_id    :: !(Maybe BOLT1.ShortChannelId)
    143     -- ^ type 6: @short_channel_id@
    144   , hp_payment_data        :: !(Maybe PaymentData)
    145     -- ^ type 8: @payment_data@
    146   , hp_encrypted_data      :: !(Maybe BS.ByteString)
    147     -- ^ type 10: @encrypted_recipient_data@
    148   , hp_current_path_key    :: !(Maybe BOLT1.Point)
    149     -- ^ type 12: @current_path_key@
    150   , hp_payment_metadata    :: !(Maybe BS.ByteString)
    151     -- ^ type 16: @payment_metadata@
    152   , hp_total_amount_msat   :: !(Maybe BOLT1.MilliSatoshi)
    153     -- ^ type 18: @total_amount_msat@
    154   , hp_extra               :: !BOLT1.TlvStream
    155     -- ^ other records
    156   } deriving (Eq, Show, Generic)
    157 
    158 instance NFData HopPayload
    159 
    160 -- | The empty t'HopPayload', to be updated with record syntax.
    161 --
    162 --   >>> let Just amt = BOLT1.milli_satoshi 1000
    163 --   >>> let hp = empty_hop_payload { hp_amt_to_forward = Just amt }
    164 --   >>> encode_hop_payload hp
    165 --   Right "\STX\STX\ETX\232"
    166 empty_hop_payload :: HopPayload
    167 empty_hop_payload = HopPayload
    168   Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing
    169   BOLT1.empty_tlv_stream
    170 
    171 -- | The @payment_data@ record of a final hop's payload.
    172 data PaymentData = PaymentData
    173   { pd_payment_secret :: !PaymentSecret
    174   , pd_total_msat     :: !BOLT1.MilliSatoshi
    175   } deriving (Eq, Show, Generic)
    176 
    177 instance NFData PaymentData
    178 
    179 -- | A 32-byte payment secret.
    180 --
    181 --   Its 'Show' instance is redacted and its 'Eq' instance runs in
    182 --   constant time.
    183 newtype PaymentSecret = PaymentSecret BS.ByteString
    184 
    185 instance Eq PaymentSecret where
    186   PaymentSecret a == PaymentSecret b = P.ct_eq a b
    187 
    188 instance Show PaymentSecret where
    189   showsPrec d _ = showParen (d > 10) $
    190     showString "PaymentSecret <redacted>"
    191 
    192 instance NFData PaymentSecret where
    193   rnf (PaymentSecret bs) = rnf bs
    194 
    195 -- | Construct a t'PaymentSecret' from exactly 32 bytes.
    196 --
    197 --   >>> payment_secret (BS.replicate 32 0x01)
    198 --   Just (PaymentSecret <redacted>)
    199 --   >>> payment_secret (BS.replicate 16 0x01)
    200 --   Nothing
    201 payment_secret :: BS.ByteString -> Maybe PaymentSecret
    202 payment_secret bs
    203   | BS.length bs == 32 = Just (PaymentSecret bs)
    204   | otherwise          = Nothing
    205 {-# INLINE payment_secret #-}
    206 
    207 -- | The bytes of a t'PaymentSecret'.
    208 un_payment_secret :: PaymentSecret -> BS.ByteString
    209 un_payment_secret (PaymentSecret bs) = bs
    210 {-# INLINE un_payment_secret #-}
    211 
    212 -- route blinding -------------------------------------------------------------
    213 
    214 -- | A blinded route (@blinded_path@), as created by a recipient.
    215 data BlindedPath = BlindedPath
    216   { bp_first_node_id  :: !BOLT1.Point
    217     -- ^ the (unblinded) introduction node
    218   , bp_first_path_key :: !BOLT1.Point
    219     -- ^ the path key for the introduction node
    220   , bp_hops           :: ![BlindedHop]
    221     -- ^ the blinded hops, starting with the introduction node
    222   } deriving (Eq, Show, Generic)
    223 
    224 instance NFData BlindedPath
    225 
    226 -- | A hop of a blinded route (@blinded_path_hop@).
    227 data BlindedHop = BlindedHop
    228   { bh_blinded_node_id :: !BOLT1.Point
    229     -- ^ the hop's blinded node id
    230   , bh_encrypted_data  :: !BS.ByteString
    231     -- ^ the hop's @encrypted_recipient_data@
    232   } deriving (Eq, Show, Generic)
    233 
    234 instance NFData BlindedHop
    235 
    236 -- | The @encrypted_data_tlv@ stream carried, encrypted, in a hop's
    237 --   @encrypted_recipient_data@.
    238 --
    239 --   'bhd_extra' plays the same role as 'hp_extra' in t'HopPayload'.
    240 data BlindedHopData = BlindedHopData
    241   { bhd_padding                :: !(Maybe BS.ByteString)
    242     -- ^ type 1: @padding@
    243   , bhd_short_channel_id       :: !(Maybe BOLT1.ShortChannelId)
    244     -- ^ type 2: @short_channel_id@
    245   , bhd_next_node_id           :: !(Maybe BOLT1.Point)
    246     -- ^ type 4: @next_node_id@
    247   , bhd_path_id                :: !(Maybe BS.ByteString)
    248     -- ^ type 6: @path_id@
    249   , bhd_next_path_key_override :: !(Maybe BOLT1.Point)
    250     -- ^ type 8: @next_path_key_override@
    251   , bhd_payment_relay          :: !(Maybe PaymentRelay)
    252     -- ^ type 10: @payment_relay@
    253   , bhd_payment_constraints    :: !(Maybe PaymentConstraints)
    254     -- ^ type 12: @payment_constraints@
    255   , bhd_allowed_features       :: !(Maybe BOLT9.FeatureVector)
    256     -- ^ type 14: @allowed_features@
    257   , bhd_extra                  :: !BOLT1.TlvStream
    258     -- ^ other records
    259   } deriving (Eq, Show, Generic)
    260 
    261 instance NFData BlindedHopData
    262 
    263 -- | The empty t'BlindedHopData', to be updated with record syntax.
    264 --
    265 --   >>> let bhd = empty_blinded_hop_data { bhd_path_id = Just "path id" }
    266 --   >>> encode_blinded_hop_data bhd
    267 --   Right "\ACK\apath id"
    268 empty_blinded_hop_data :: BlindedHopData
    269 empty_blinded_hop_data = BlindedHopData
    270   Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing
    271   BOLT1.empty_tlv_stream
    272 
    273 -- | The @payment_relay@ record of t'BlindedHopData'.
    274 data PaymentRelay = PaymentRelay
    275   { pr_cltv_expiry_delta           :: {-# UNPACK #-} !Word16
    276   , pr_fee_proportional_millionths :: {-# UNPACK #-} !Word32
    277   , pr_fee_base_msat               :: {-# UNPACK #-} !Word32
    278   } deriving (Eq, Show, Generic)
    279 
    280 instance NFData PaymentRelay
    281 
    282 -- | The @payment_constraints@ record of t'BlindedHopData'.
    283 data PaymentConstraints = PaymentConstraints
    284   { pc_max_cltv_expiry   :: {-# UNPACK #-} !Word32
    285   , pc_htlc_minimum_msat :: !BOLT1.MilliSatoshi
    286   } deriving (Eq, Show, Generic)
    287 
    288 instance NFData PaymentConstraints
    289 
    290 -- | What a node in a blinded route learns from its
    291 --   @encrypted_recipient_data@.
    292 data BlindedInfo = BlindedInfo
    293   { bi_data          :: !BlindedHopData
    294     -- ^ the decrypted @encrypted_data_tlv@
    295   , bi_next_path_key :: !BOLT1.Point
    296     -- ^ the @path_key@ to send to the next node
    297   } deriving (Eq, Show, Generic)
    298 
    299 instance NFData BlindedInfo
    300 
    301 -- failure messages -----------------------------------------------------------
    302 
    303 -- | A failure message (@failuremsg@): a failure, followed by an optional
    304 --   TLV stream.
    305 data FailureMessage = FailureMessage
    306   { fm_failure :: !Failure
    307   , fm_tlvs    :: !BOLT1.TlvStream
    308   } deriving (Eq, Show, Generic)
    309 
    310 instance NFData FailureMessage
    311 
    312 -- | A failure: a failure code and its data.
    313 --
    314 --   A @channel_update@ field holds the raw bytes of the update; it is
    315 --   empty when the update is omitted.
    316 data Failure
    317   = TemporaryNodeFailure
    318     -- ^ NODE|2
    319   | PermanentNodeFailure
    320     -- ^ PERM|NODE|2
    321   | RequiredNodeFeatureMissing
    322     -- ^ PERM|NODE|3
    323   | InvalidOnionVersion !OnionHash
    324     -- ^ BADONION|PERM|4: @sha256_of_onion@
    325   | InvalidOnionHmac !OnionHash
    326     -- ^ BADONION|PERM|5: @sha256_of_onion@
    327   | InvalidOnionKey !OnionHash
    328     -- ^ BADONION|PERM|6: @sha256_of_onion@
    329   | TemporaryChannelFailure !BS.ByteString
    330     -- ^ UPDATE|7: @channel_update@
    331   | PermanentChannelFailure
    332     -- ^ PERM|8
    333   | RequiredChannelFeatureMissing
    334     -- ^ PERM|9
    335   | UnknownNextPeer
    336     -- ^ PERM|10
    337   | AmountBelowMinimum !BOLT1.MilliSatoshi !BS.ByteString
    338     -- ^ UPDATE|11: @htlc_msat@, @channel_update@
    339   | FeeInsufficient !BOLT1.MilliSatoshi !BS.ByteString
    340     -- ^ UPDATE|12: @htlc_msat@, @channel_update@
    341   | IncorrectCltvExpiry {-# UNPACK #-} !Word32 !BS.ByteString
    342     -- ^ UPDATE|13: @cltv_expiry@, @channel_update@
    343   | ExpiryTooSoon !BS.ByteString
    344     -- ^ UPDATE|14: @channel_update@
    345   | IncorrectOrUnknownPaymentDetails !BOLT1.MilliSatoshi
    346                                      {-# UNPACK #-} !Word32
    347     -- ^ PERM|15: @htlc_msat@, @height@
    348   | FinalIncorrectCltvExpiry {-# UNPACK #-} !Word32
    349     -- ^ 18: @cltv_expiry@
    350   | FinalIncorrectHtlcAmount !BOLT1.MilliSatoshi
    351     -- ^ 19: @incoming_htlc_amt@
    352   | ChannelDisabled {-# UNPACK #-} !Word16 !BS.ByteString
    353     -- ^ UPDATE|20: @disabled_flags@, @channel_update@
    354   | ExpiryTooFar
    355     -- ^ 21
    356   | InvalidOnionPayload !(Maybe (Word64, Word16))
    357     -- ^ PERM|22: the optional @type@ and @offset@
    358   | MppTimeout
    359     -- ^ 23
    360   | InvalidOnionBlinding !OnionHash
    361     -- ^ BADONION|PERM|24: @sha256_of_onion@
    362   | UnknownFailure {-# UNPACK #-} !Word16 !BS.ByteString
    363     -- ^ a failure code not defined above, and the bytes following it
    364   deriving (Eq, Show, Generic)
    365 
    366 instance NFData Failure
    367 
    368 -- | A 32-byte @sha256_of_onion@.
    369 newtype OnionHash = OnionHash BS.ByteString
    370   deriving (Eq, Show, Generic)
    371 
    372 instance NFData OnionHash
    373 
    374 -- | Construct an t'OnionHash' from exactly 32 bytes.
    375 --
    376 --   >>> fmap (BS.length . un_onion_hash) (onion_hash (BS.replicate 32 0))
    377 --   Just 32
    378 onion_hash :: BS.ByteString -> Maybe OnionHash
    379 onion_hash bs
    380   | BS.length bs == 32 = Just (OnionHash bs)
    381   | otherwise          = Nothing
    382 {-# INLINE onion_hash #-}
    383 
    384 -- | The bytes of an t'OnionHash'.
    385 un_onion_hash :: OnionHash -> BS.ByteString
    386 un_onion_hash (OnionHash bs) = bs
    387 {-# INLINE un_onion_hash #-}
    388 
    389 -- | The @failure_code@ of a 'Failure'.
    390 --
    391 --   >>> failure_code TemporaryNodeFailure
    392 --   8194
    393 --   >>> failure_code (UnknownFailure 0x1234 mempty)
    394 --   4660
    395 failure_code :: Failure -> Word16
    396 failure_code f = case f of
    397   TemporaryNodeFailure                 -> 0x2002
    398   PermanentNodeFailure                 -> 0x6002
    399   RequiredNodeFeatureMissing           -> 0x6003
    400   InvalidOnionVersion _                -> 0xc004
    401   InvalidOnionHmac _                   -> 0xc005
    402   InvalidOnionKey _                    -> 0xc006
    403   TemporaryChannelFailure _            -> 0x1007
    404   PermanentChannelFailure              -> 0x4008
    405   RequiredChannelFeatureMissing        -> 0x4009
    406   UnknownNextPeer                      -> 0x400a
    407   AmountBelowMinimum _ _               -> 0x100b
    408   FeeInsufficient _ _                  -> 0x100c
    409   IncorrectCltvExpiry _ _              -> 0x100d
    410   ExpiryTooSoon _                      -> 0x100e
    411   IncorrectOrUnknownPaymentDetails _ _ -> 0x400f
    412   FinalIncorrectCltvExpiry _           -> 0x0012
    413   FinalIncorrectHtlcAmount _           -> 0x0013
    414   ChannelDisabled _ _                  -> 0x1014
    415   ExpiryTooFar                         -> 0x0015
    416   InvalidOnionPayload _                -> 0x4016
    417   MppTimeout                           -> 0x0017
    418   InvalidOnionBlinding _               -> 0xc018
    419   UnknownFailure c _                   -> c
    420 
    421 -- | Is the BADONION flag (0x8000) set: was the onion unparsable?
    422 --
    423 --   >>> is_badonion TemporaryNodeFailure
    424 --   False
    425 is_badonion :: Failure -> Bool
    426 is_badonion f = failure_code f .&. 0x8000 /= 0
    427 {-# INLINE is_badonion #-}
    428 
    429 -- | Is the PERM flag (0x4000) set: is the failure permanent?
    430 --
    431 --   >>> is_perm PermanentNodeFailure
    432 --   True
    433 is_perm :: Failure -> Bool
    434 is_perm f = failure_code f .&. 0x4000 /= 0
    435 {-# INLINE is_perm #-}
    436 
    437 -- | Is the NODE flag (0x2000) set: is it a node (not channel) failure?
    438 --
    439 --   >>> is_node TemporaryNodeFailure
    440 --   True
    441 is_node :: Failure -> Bool
    442 is_node f = failure_code f .&. 0x2000 /= 0
    443 {-# INLINE is_node #-}
    444 
    445 -- | Is the UPDATE flag (0x1000) set: was a channel forwarding parameter
    446 --   violated?
    447 --
    448 --   >>> is_update (ExpiryTooSoon mempty)
    449 --   True
    450 is_update :: Failure -> Bool
    451 is_update f = failure_code f .&. 0x1000 /= 0
    452 {-# INLINE is_update #-}
    453 
    454 -- errors ---------------------------------------------------------------------
    455 
    456 -- | Why a decoder failed.
    457 data DecodeError
    458   = InvalidLength
    459     -- ^ the input has the wrong length
    460   | InvalidPoint
    461     -- ^ a point lacks a compressed-encoding prefix
    462   | InvalidTlvStream !BOLT1.TlvError
    463     -- ^ the TLV stream is malformed, or has an unknown even type
    464   | InvalidTlvValue {-# UNPACK #-} !Word64
    465     -- ^ the value of the record of this type is malformed
    466   | InvalidFailureData {-# UNPACK #-} !Word16
    467     -- ^ the data of a failure with this code is malformed
    468   deriving (Eq, Show, Generic)
    469 
    470 instance NFData DecodeError
    471 
    472 -- | Why an encoder failed.
    473 data EncodeError
    474   = ConflictingTlv
    475     -- ^ an extra TLV record has the type of a known record
    476   | FieldTooLong
    477     -- ^ a length-prefixed field exceeds its maximum length
    478   deriving (Eq, Show, Generic)
    479 
    480 instance NFData EncodeError
    481 
    482 -- | Why an onion packet was rejected.
    483 data ProcessError
    484   = InvalidVersion {-# UNPACK #-} !Word8
    485     -- ^ the version byte is not 0
    486   | InvalidPublicKey
    487     -- ^ the ephemeral public key is not a valid point
    488   | HmacMismatch
    489     -- ^ the packet's HMAC is wrong
    490   | InvalidPayloadLength
    491     -- ^ the payload length is malformed, below 2, or past the end of
    492     --   @hop_payloads@
    493   | InvalidPayload !DecodeError
    494     -- ^ the payload is not a valid @payload@ TLV stream
    495   | InvalidPathKey
    496     -- ^ the path key is not a valid point
    497   | UnexpectedPathKey
    498     -- ^ a path key was given with no @encrypted_recipient_data@, or
    499     --   both a @path_key@ and a @current_path_key@ were given
    500   | MissingPathKey
    501     -- ^ @encrypted_recipient_data@ was given with no path key
    502   | InvalidRecipientData
    503     -- ^ the @encrypted_recipient_data@ doesn't decrypt to a valid
    504     --   @encrypted_data_tlv@ stream
    505   deriving (Eq, Show, Generic)
    506 
    507 instance NFData ProcessError