bolt9

Lightning feature flags, per BOLT #9 (docs.ppad.tech/bolt9).
git clone git://git.ppad.tech/bolt9.git
Log | Files | Refs | README | LICENSE

commit 5c2e239fdd5d5ccce99572aad742bef4cdd3e70f
parent 39e9cd5da9db0cc676d1c55fb3aa78de779d7c7c
Author: Jared Tobin <jared@jtobin.io>
Date:   Sat, 10 Oct 2026 13:28:20 +0400

lib: redesign around a closed feature enum and spec contexts

Collapses the library into the single exposed module
Lightning.Protocol.BOLT9 with a snake_case API.

- FeatureVector is abstract. parse keeps the received bytes and render
  returns them unchanged; every other operation encodes minimally.
  Equality ignores leading zero bytes. Adds union.
- Bit indices are Int throughout; negative indices are ignored.
- Feature is a closed enumeration of the BOLT #9 table at
  lightning/bolts@1aadb719, adding zero_fee_commitments,
  option_splice and option_onion_messages_only_channels. Dependencies
  are Features, not names. Drops the containers dependency.
- Context has one constructor per spec context (I, N, C, 9, B, T).
  C- and C+ are not contexts.
- validate_local rejects unknown bits, both bits set, features outside
  their contexts and missing dependencies. validate_remote applies
  each context's receiver rules, including BOLT #4's rejection of any
  unknown bit in allowed_features. A dependency on an ASSUMED feature
  counts as met.
- ChannelType models BOLT #2 channel types; the channel_type context
  is validated against them rather than the generic rules.
- Removes RequiredBit, OptionalBit, BitIndex, KnownFeature, the
  name-based lookups, the duplicate bit operations and the aliases.

Diffstat:
Mlib/Lightning/Protocol/BOLT9.hs | 797++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Dlib/Lightning/Protocol/BOLT9/Codec.hs | 193-------------------------------------------------------------------------------
Dlib/Lightning/Protocol/BOLT9/Features.hs | 161-------------------------------------------------------------------------------
Dlib/Lightning/Protocol/BOLT9/Types.hs | 282-------------------------------------------------------------------------------
Dlib/Lightning/Protocol/BOLT9/Validate.hs | 308-------------------------------------------------------------------------------
Mppad-bolt9.cabal | 21++++++++++-----------
6 files changed, 695 insertions(+), 1067 deletions(-)

diff --git a/lib/Lightning/Protocol/BOLT9.hs b/lib/Lightning/Protocol/BOLT9.hs @@ -11,132 +11,705 @@ -- Feature flags for the Lightning Network, per -- [BOLT #9](https://github.com/lightning/bolts/blob/master/09-features.md). -- --- == Overview +-- A feature vector is a big-endian bit field. Features are assigned +-- pairs of bits: setting the even bit means the feature is required, +-- setting the odd bit means it is optional (/it's ok to be odd/). -- --- BOLT #9 defines feature flags that Lightning nodes advertise to indicate --- support for optional protocol features. Features are represented as bit --- positions in a variable-length bit vector, where even bits indicate --- required (compulsory) support and odd bits indicate optional support. +-- The examples below assume: -- --- This library provides: +-- >>> :set -XOverloadedStrings +-- >>> import Lightning.Protocol.BOLT9 -- --- * Type-safe feature vectors with efficient bit manipulation --- * A complete table of known features from the BOLT #9 specification --- * Validation for both locally-created and remotely-received vectors --- * Context-aware validation (init, node_announcement, invoice, etc.) +-- Build a vector, check it before sending it, and render it: -- --- == Quick Start +-- >>> let fv = set_feature_with_deps OptionZeroconf Optional empty +-- >>> list_features fv +-- [(OptionScidAlias,Optional),(OptionZeroconf,Optional)] +-- >>> validate_local InitContext fv +-- Right () +-- >>> render fv +-- "\b\128\NUL\NUL\NUL\NUL\NUL" -- --- Create a feature vector and set some features: +-- Check a vector received from a peer: -- --- >>> import Lightning.Protocol.BOLT9 --- >>> let Just mpp = featureByName "basic_mpp" --- >>> let fv = setFeature mpp Optional empty --- >>> hasFeature mpp fv --- Just Optional +-- >>> validate_remote InitContext (parse "\DLE\NUL\NUL") +-- Left (UnknownBit 20 :| []) +-- >>> validate_remote InitContext (parse "\STX\NUL\NUL") +-- Right () + +module Lightning.Protocol.BOLT9 ( + -- * Feature vectors + FeatureVector + , parse + , render + , empty + , union + + -- * Bits + , set_bit + , clear_bit + , test_bit + , set_bits + + -- * Known features + , Feature(..) + , known_features + , feature_by_bit + , feature_bit + , feature_name + , feature_contexts + , feature_dependencies + , feature_assumed + + -- * Feature operations + , FeatureLevel(..) + , set_feature + , set_feature_with_deps + , clear_feature + , test_feature + , list_features + + -- * Validation + , Context(..) + , ValidationError(..) + , validate_local + , validate_remote + + -- * Channel types + , ChannelType(..) + , BasicChannelType(..) + , channel_type + , channel_type_features + ) where + +import Control.DeepSeq (NFData) +import qualified Data.Bits as B +import Data.ByteString (ByteString) +import qualified Data.ByteString as BS +import Data.List (find) +import Data.List.NonEmpty (NonEmpty(..)) +import Data.Maybe (isNothing) +import Data.Word (Word8) +import GHC.Generics (Generic) + +-- feature vectors ----------------------------------------------------------- + +-- | A feature vector. +-- +-- Bit 0 is the least significant bit of the last byte. A vector +-- produced by 'parse' keeps its bytes exactly, so 'render' gives +-- them back unchanged (signatures over gossip messages cover them). +-- Every other operation produces a minimally-encoded vector, i.e. +-- one without leading zero bytes. -- --- Validate a feature vector for a specific context: +-- Equality ignores leading zero bytes. -- --- >>> validateLocal Init fv --- Left [MissingDependency "basic_mpp" "payment_secret"] +-- >>> parse "\NUL\STX" == parse "\STX" +-- True +-- >>> set_bit 1 empty +-- parse "\STX" +newtype FeatureVector = FeatureVector ByteString + deriving Generic + +instance Eq FeatureVector where + FeatureVector a == FeatureVector b = strip a == strip b + +instance Show FeatureVector where + showsPrec d (FeatureVector bs) = showParen (d > 10) $ + showString "parse " . showsPrec 11 bs + +instance NFData FeatureVector + +-- | Parse a feature vector from its wire bytes. Every byte string is a +-- valid feature vector. -- --- Fix by adding the dependency: +-- >>> test_bit 9 (parse "\STX\NUL") +-- True +parse :: ByteString -> FeatureVector +parse = FeatureVector +{-# INLINE parse #-} + +-- | The wire bytes of a feature vector: the original bytes of a vector +-- produced by 'parse', and the minimal encoding of any other. -- --- >>> let Just ps = featureByName "payment_secret" --- >>> let fv' = setFeature ps Optional (setFeature mpp Optional empty) --- >>> validateLocal Init fv' --- Right () +-- >>> render (parse "\NUL\STX") +-- "\NUL\STX" +-- >>> render (union (parse "\NUL\STX") empty) +-- "\STX" +render :: FeatureVector -> ByteString +render (FeatureVector bs) = bs +{-# INLINE render #-} + +-- | The empty feature vector. -- --- == Bit Numbering +-- >>> render empty +-- "" +empty :: FeatureVector +empty = FeatureVector BS.empty + +-- | The bitwise OR of two feature vectors. -- --- Features use paired bits: even bits (0, 2, 4, ...) indicate required --- support, while odd bits (1, 3, 5, ...) indicate optional support. --- For example, @basic_mpp@ uses bit 16 (required) and 17 (optional). +-- BOLT #1 requires the receiver of an @init@ message to combine its +-- @globalfeatures@ and @features@ fields this way. -- --- A node setting bit 16 requires all peers to support @basic_mpp@. --- A node setting bit 17 indicates optional support (peers without it --- may still connect). +-- >>> set_bits (union (parse "\STX") (parse "\SOH\NUL")) +-- [1,8] +union :: FeatureVector -> FeatureVector -> FeatureVector +union (FeatureVector a) (FeatureVector b) = + FeatureVector (combine (B..|.) a b) -module Lightning.Protocol.BOLT9 ( - -- * Context - -- | Contexts specify where feature flags appear in the protocol. - Context(..) - , isChannelContext - , channelParity - - -- * Bit indices - -- | Low-level bit index types for direct bit manipulation. - , BitIndex - , unBitIndex - , bitIndex - - -- * Required/optional level - -- | Whether a feature is set as required or optional. - , FeatureLevel(..) +-- bits ---------------------------------------------------------------------- - -- * Required/optional bits - -- | Type-safe wrappers ensuring correct parity. - , RequiredBit - , unRequiredBit - , requiredBit - , requiredFromBitIndex - - , OptionalBit - , unOptionalBit - , optionalBit - , optionalFromBitIndex - - -- * Feature vectors - -- | The core feature vector type and basic operations. - , FeatureVector - , unFeatureVector - , FV.empty - , fromByteString - , set - , clear - , member - - -- * Known features - -- | The BOLT #9 feature table and lookup functions. - , Feature(..) - , KnownFeature(..) - , knownFeatureByBit - , knownFeatureByName - , featureByBit - , featureByName - , knownFeatures - - -- * Parsing and rendering - -- | Wire format conversion. - , parse - , render +-- | Set the bit at the given index. Negative indices are ignored. +-- +-- >>> render (set_bit 9 empty) +-- "\STX\NUL" +set_bit :: Int -> FeatureVector -> FeatureVector +set_bit !i (FeatureVector bs) + | i < 0 = FeatureVector (strip bs) + | otherwise = FeatureVector (combine (B..|.) bs (single i)) - -- * Low-level bit operations - -- | Direct bit manipulation by index. - , Codec.setBit - , Codec.clearBit - , Codec.testBit - - -- * Feature operations - -- | High-level operations using 'Feature' values. - , setFeature - , setFeatureWithDeps - , hasFeature - , isFeatureSet - , listFeatures - - -- * Validation - -- | Validate feature vectors for correctness. - , ValidationError(..) - , validateLocal - , validateRemote - , setFeatureForContext - , validateNoBothBits - , highestSetBit - , Validate.setBits - ) where +-- | Clear the bit at the given index. Negative indices are ignored. +-- +-- >>> render (clear_bit 9 (parse "\STX\SOH")) +-- "\SOH" +clear_bit :: Int -> FeatureVector -> FeatureVector +clear_bit !i (FeatureVector bs) + | i < 0 || i `quot` 8 >= BS.length s = FeatureVector s + | otherwise = FeatureVector (combine clr s (single i)) + where + s = strip bs + clr x y = x B..&. B.complement y + +-- | Test the bit at the given index. Negative indices are never set. +-- +-- >>> test_bit 9 (parse "\STX\NUL") +-- True +-- >>> test_bit 8 (parse "\STX\NUL") +-- False +test_bit :: Int -> FeatureVector -> Bool +test_bit !i (FeatureVector bs) + | i < 0 || q >= len = False + | otherwise = B.testBit (BS.index bs (len - 1 - q)) r + where + len = BS.length bs + (q, r) = i `quotRem` 8 + +-- | The indices of all set bits, in ascending order. +-- +-- >>> set_bits (parse "\STX\NUL\SOH") +-- [0,17] +set_bits :: FeatureVector -> [Int] +set_bits (FeatureVector bs) = go 0 + where + !len = BS.length bs + go !q + | q >= len = [] + | otherwise = + let !w = BS.index bs (len - 1 - q) + in [q * 8 + r | r <- [0 .. 7], B.testBit w r] ++ go (q + 1) -import Lightning.Protocol.BOLT9.Codec as Codec -import Lightning.Protocol.BOLT9.Features -import Lightning.Protocol.BOLT9.Types as FV -import Lightning.Protocol.BOLT9.Validate as Validate +-- Drop leading zero bytes. +strip :: ByteString -> ByteString +strip = BS.dropWhile (== 0) +{-# INLINE strip #-} + +-- The minimal encoding of a vector with only bit i set (i >= 0). +single :: Int -> ByteString +single i = BS.cons (B.bit r) (BS.replicate q 0) + where + (q, r) = i `quotRem` 8 + +-- Combine two encodings bytewise, aligned at their last bytes (the +-- shorter one padded with leading zeros), and strip the result. +combine + :: (Word8 -> Word8 -> Word8) -> ByteString -> ByteString -> ByteString +combine f a b = strip (fst (BS.unfoldrN n step 0)) + where + !la = BS.length a + !lb = BS.length b + !n = max la lb + byte s ls k + | k < n - ls = 0 + | otherwise = BS.index s (k - n + ls) + step !k = Just (f (byte a la k) (byte b lb k), k + 1) + +-- known features ------------------------------------------------------------ + +-- | The features assigned in the BOLT #9 table. +data Feature + = OptionDataLossProtect -- ^ 0/1 @option_data_loss_protect@ + | OptionUpfrontShutdownScript -- ^ 4/5 @option_upfront_shutdown_script@ + | GossipQueries -- ^ 6/7 @gossip_queries@ + | VarOnionOptin -- ^ 8/9 @var_onion_optin@ + | GossipQueriesEx -- ^ 10/11 @gossip_queries_ex@ + | OptionStaticRemotekey -- ^ 12/13 @option_static_remotekey@ + | PaymentSecret -- ^ 14/15 @payment_secret@ + | BasicMpp -- ^ 16/17 @basic_mpp@ + | OptionSupportLargeChannel -- ^ 18/19 @option_support_large_channel@ + | OptionAnchors -- ^ 22/23 @option_anchors@ + | OptionRouteBlinding -- ^ 24/25 @option_route_blinding@ + | OptionShutdownAnysegwit -- ^ 26/27 @option_shutdown_anysegwit@ + | OptionDualFund -- ^ 28/29 @option_dual_fund@ + | OptionQuiesce -- ^ 34/35 @option_quiesce@ + | OptionAttributionData -- ^ 36/37 @option_attribution_data@ + | OptionOnionMessages -- ^ 38/39 @option_onion_messages@ + | ZeroFeeCommitments -- ^ 40/41 @zero_fee_commitments@ + | OptionProvideStorage -- ^ 42/43 @option_provide_storage@ + | OptionChannelType -- ^ 44/45 @option_channel_type@ + | OptionScidAlias -- ^ 46/47 @option_scid_alias@ + | OptionPaymentMetadata -- ^ 48/49 @option_payment_metadata@ + | OptionZeroconf -- ^ 50/51 @option_zeroconf@ + | OptionSimpleClose -- ^ 60/61 @option_simple_close@ + | OptionSplice -- ^ 62/63 @option_splice@ + | OptionOnionMessagesOnlyChannels + -- ^ 66/67 @option_onion_messages_only_channels@ + deriving (Eq, Ord, Show, Generic) + +instance NFData Feature + +-- | All known features, in bit order. +-- +-- >>> length known_features +-- 25 +known_features :: [Feature] +known_features = [ + OptionDataLossProtect + , OptionUpfrontShutdownScript + , GossipQueries + , VarOnionOptin + , GossipQueriesEx + , OptionStaticRemotekey + , PaymentSecret + , BasicMpp + , OptionSupportLargeChannel + , OptionAnchors + , OptionRouteBlinding + , OptionShutdownAnysegwit + , OptionDualFund + , OptionQuiesce + , OptionAttributionData + , OptionOnionMessages + , ZeroFeeCommitments + , OptionProvideStorage + , OptionChannelType + , OptionScidAlias + , OptionPaymentMetadata + , OptionZeroconf + , OptionSimpleClose + , OptionSplice + , OptionOnionMessagesOnlyChannels + ] + +-- A row of the BOLT #9 table. +data Entry = Entry { + e_bit :: !Int + , e_name :: !String + , e_contexts :: ![Context] + , e_deps :: ![Feature] + , e_assumed :: !Bool + } + +entry :: Feature -> Entry +entry f = case f of + OptionDataLossProtect -> + Entry 0 "option_data_loss_protect" [] [] True + OptionUpfrontShutdownScript -> + Entry 4 "option_upfront_shutdown_script" _IN [] False + GossipQueries -> + Entry 6 "gossip_queries" [] [] False + VarOnionOptin -> + Entry 8 "var_onion_optin" [] [] True + GossipQueriesEx -> + Entry 10 "gossip_queries_ex" _IN [] False + OptionStaticRemotekey -> + Entry 12 "option_static_remotekey" [] [] True + PaymentSecret -> + Entry 14 "payment_secret" [] [] True + BasicMpp -> + Entry 16 "basic_mpp" _IN9 [PaymentSecret] False + OptionSupportLargeChannel -> + Entry 18 "option_support_large_channel" _IN [] False + OptionAnchors -> + Entry 22 "option_anchors" _INT [] False + OptionRouteBlinding -> + Entry 24 "option_route_blinding" _IN9 [] False + OptionShutdownAnysegwit -> + Entry 26 "option_shutdown_anysegwit" _IN [] False + OptionDualFund -> + Entry 28 "option_dual_fund" _IN [] False + OptionQuiesce -> + Entry 34 "option_quiesce" _IN [] False + OptionAttributionData -> + Entry 36 "option_attribution_data" _IN9 [] False + OptionOnionMessages -> + Entry 38 "option_onion_messages" _IN [] False + ZeroFeeCommitments -> + Entry 40 "zero_fee_commitments" _IN [OptionChannelType] False + OptionProvideStorage -> + Entry 42 "option_provide_storage" _IN [] False + OptionChannelType -> + Entry 44 "option_channel_type" [] [] True + OptionScidAlias -> + Entry 46 "option_scid_alias" _INT [] False + OptionPaymentMetadata -> + Entry 48 "option_payment_metadata" [InvoiceContext] [] False + OptionZeroconf -> + Entry 50 "option_zeroconf" _INT [OptionScidAlias] False + OptionSimpleClose -> + Entry 60 "option_simple_close" _IN [OptionShutdownAnysegwit] False + OptionSplice -> + Entry 62 "option_splice" _IN [] False + OptionOnionMessagesOnlyChannels -> + Entry 66 "option_onion_messages_only_channels" _IN + [OptionOnionMessages] False + where + _IN = [InitContext, NodeContext] + _IN9 = [InitContext, NodeContext, InvoiceContext] + _INT = [InitContext, NodeContext, ChannelTypeContext] + +-- | The feature that a bit (even or odd) belongs to, if any. +-- +-- >>> feature_by_bit 16 +-- Just BasicMpp +-- >>> feature_by_bit 17 +-- Just BasicMpp +-- >>> feature_by_bit 20 +-- Nothing +feature_by_bit :: Int -> Maybe Feature +feature_by_bit !i + | i < 0 || i > max_known_bit = Nothing + | otherwise = find ((== base) . feature_bit) known_features + where + base = i - i `rem` 2 + +-- The highest bit assigned to any known feature. +max_known_bit :: Int +max_known_bit = foldr (max . (+ 1) . feature_bit) 0 known_features + +-- | A feature's even (required) bit. Its odd (optional) bit is the +-- next one up. +-- +-- >>> feature_bit BasicMpp +-- 16 +feature_bit :: Feature -> Int +feature_bit = e_bit . entry + +-- | A feature's name in the BOLT #9 table. +-- +-- >>> feature_name BasicMpp +-- "basic_mpp" +feature_name :: Feature -> String +feature_name = e_name . entry + +-- | The contexts the BOLT #9 table lists for a feature, in table order. +-- The list is empty for ASSUMED features and for 'GossipQueries'. +-- +-- >>> feature_contexts BasicMpp +-- [InitContext,NodeContext,InvoiceContext] +-- >>> feature_contexts PaymentSecret +-- [] +feature_contexts :: Feature -> [Context] +feature_contexts = e_contexts . entry + +-- | A feature's direct dependencies. +-- +-- >>> feature_dependencies OptionZeroconf +-- [OptionScidAlias] +feature_dependencies :: Feature -> [Feature] +feature_dependencies = e_deps . entry + +-- | Whether BOLT #9 marks a feature ASSUMED, i.e. supported by all +-- nodes. +-- +-- >>> feature_assumed PaymentSecret +-- True +feature_assumed :: Feature -> Bool +feature_assumed = e_assumed . entry + +-- feature operations -------------------------------------------------------- + +-- | The level at which a feature is set. +data FeatureLevel + = Required -- ^ the even bit is set + | Optional -- ^ the odd bit is set + deriving (Eq, Ord, Show, Generic) + +instance NFData FeatureLevel + +level_bit :: Feature -> FeatureLevel -> Int +level_bit f Required = feature_bit f +level_bit f Optional = feature_bit f + 1 +{-# INLINE level_bit #-} + +-- | Set a feature at the given level, clearing the other bit of its +-- pair. +-- +-- >>> set_bits (set_feature BasicMpp Optional empty) +-- [17] +-- >>> set_bits (set_feature BasicMpp Required (set_bit 17 empty)) +-- [16] +set_feature :: Feature -> FeatureLevel -> FeatureVector -> FeatureVector +set_feature f l = set_bit (level_bit f l) . clear_bit (level_bit f other) + where + other = case l of + Required -> Optional + Optional -> Required + +-- | Set a feature at the given level, together with its transitive +-- dependencies (ASSUMED ones included). Dependencies that are not +-- yet set are set at the same level; those already set keep theirs. +-- +-- >>> list_features (set_feature_with_deps BasicMpp Optional empty) +-- [(PaymentSecret,Optional),(BasicMpp,Optional)] +set_feature_with_deps + :: Feature -> FeatureLevel -> FeatureVector -> FeatureVector +set_feature_with_deps f l fv = + foldr ensure (set_feature f l fv) (feature_dependencies f) + where + ensure d acc = + let acc' = case test_feature d acc of + Just _ -> acc + Nothing -> set_feature d l acc + in foldr ensure acc' (feature_dependencies d) + +-- | Clear both bits of a feature. +-- +-- >>> clear_feature BasicMpp (set_feature BasicMpp Optional empty) +-- parse "" +clear_feature :: Feature -> FeatureVector -> FeatureVector +clear_feature f = + clear_bit (level_bit f Required) . clear_bit (level_bit f Optional) + +-- | The level at which a feature is set, if it is. +-- +-- A feature with both bits set is 'Required', as BOLT #9 directs the +-- receiver to treat it. +-- +-- >>> test_feature BasicMpp (set_bit 17 empty) +-- Just Optional +-- >>> test_feature BasicMpp (set_bit 16 (set_bit 17 empty)) +-- Just Required +-- >>> test_feature BasicMpp empty +-- Nothing +test_feature :: Feature -> FeatureVector -> Maybe FeatureLevel +test_feature f fv + | test_bit (level_bit f Required) fv = Just Required + | test_bit (level_bit f Optional) fv = Just Optional + | otherwise = Nothing + +-- | The known features set in a vector, in bit order. +-- +-- >>> list_features (parse "\SOH\128\NUL") +-- [(PaymentSecret,Optional),(BasicMpp,Required)] +list_features :: FeatureVector -> [(Feature, FeatureLevel)] +list_features fv = + [(f, l) | f <- known_features, Just l <- [test_feature f fv]] + +-- validation ---------------------------------------------------------------- + +-- | A field in which feature bits are presented: the values of the +-- Context column of the BOLT #9 table. +-- +-- The table's @C-@ and @C+@ are not further contexts. They mark a +-- feature presented in @channel_announcement@ as always optional or +-- always required. No feature at the implemented spec revision is +-- presented in @channel_announcement@ in any form. +data Context + = InitContext -- ^ @I@: the @init@ message + | NodeContext -- ^ @N@: @node_announcement@ + | ChannelContext -- ^ @C@: @channel_announcement@ + | InvoiceContext -- ^ @9@: BOLT #11 invoices + | BlindedContext -- ^ @B@: @allowed_features@ of a blinded path + | ChannelTypeContext -- ^ @T@: the @channel_type@ field + deriving (Eq, Ord, Show, Generic) + +instance NFData Context + +-- | A rule a feature vector breaks. +data ValidationError + = UnknownBit {-# UNPACK #-} !Int + -- ^ a bit belonging to no known feature is set + | BothBitsSet !Feature + -- ^ both bits of a feature are set + | ContextNotAllowed !Feature !Context + -- ^ a feature is set in a context that doesn't present it + | MissingDependency !Feature !Feature + -- ^ a set feature (first) lacks a dependency (second) + | InvalidChannelType + -- ^ a @channel_type@ is not a type that BOLT #2 defines + deriving (Eq, Show, Generic) + +instance NFData ValidationError + +-- | Validate a feature vector that we're about to send in the given +-- context, per the origin requirements of BOLT #9 and BOLT #1. +-- +-- For 'ChannelTypeContext', the vector must be a channel type that +-- BOLT #2 defines (see 'channel_type'). For every other context, +-- checks that: +-- +-- * no bit outside the known features is set; +-- * no feature has both bits set; +-- * each set feature is presented in the context. A feature with an +-- empty Context column (the ASSUMED features and 'GossipQueries') +-- is accepted in 'InitContext', 'NodeContext' and +-- 'InvoiceContext', where deployed nodes still present it; +-- * each set feature's dependencies are set. A dependency on an +-- ASSUMED feature counts as met even when its bits are clear. +-- +-- Errors are reported in that order. +-- +-- >>> validate_local InitContext (set_feature OptionZeroconf Optional empty) +-- Left (MissingDependency OptionZeroconf OptionScidAlias :| []) +-- >>> validate_local InitContext (set_feature BasicMpp Optional empty) +-- Right () +-- >>> validate_local InitContext (set_bit 101 empty) +-- Left (UnknownBit 101 :| []) +validate_local + :: Context -> FeatureVector -> Either (NonEmpty ValidationError) () +validate_local ctx fv = to_either $ case ctx of + ChannelTypeContext -> channel_type_errors fv + _ -> unknown_bits (const True) fv + ++ both_bits_errors fv + ++ context_errors ctx fv + ++ dependency_errors fv + +-- | Validate a feature vector received in the given context, per the +-- receiver requirements for that context: +-- +-- * 'InitContext' (BOLT #1) and 'InvoiceContext' (BOLT #11): no +-- unknown even bit is set, and each set feature's dependencies are +-- set (ASSUMED dependencies count as met). +-- * 'NodeContext' and 'ChannelContext' (BOLT #7): no unknown even +-- bit is set. A failure here means the node or channel must not be +-- routed through; the announcement itself is still valid. +-- * 'BlindedContext' (BOLT #4): no unknown bit is set, odd or even. +-- * 'ChannelTypeContext' (BOLT #2): the vector is a defined channel +-- type (see 'channel_type'). +-- +-- Unknown odd bits are otherwise ignored. A feature with both bits +-- set is not an error; it counts as 'Required' (see 'test_feature'). +-- Nor is a known feature outside its listed contexts, since that +-- rule binds only the sender. +-- +-- >>> validate_remote InitContext (set_bit 101 empty) +-- Right () +-- >>> validate_remote InitContext (set_bit 100 empty) +-- Left (UnknownBit 100 :| []) +-- >>> validate_remote BlindedContext (set_bit 101 empty) +-- Left (UnknownBit 101 :| []) +validate_remote + :: Context -> FeatureVector -> Either (NonEmpty ValidationError) () +validate_remote ctx fv = to_either $ case ctx of + InitContext -> unknown_even ++ dependency_errors fv + NodeContext -> unknown_even + ChannelContext -> unknown_even + InvoiceContext -> unknown_even ++ dependency_errors fv + BlindedContext -> unknown_bits (const True) fv + ChannelTypeContext -> channel_type_errors fv + where + unknown_even = unknown_bits even fv + +to_either :: [e] -> Either (NonEmpty e) () +to_either [] = Right () +to_either (e : es) = Left (e :| es) + +-- Unknown set bits satisfying the predicate. +unknown_bits :: (Int -> Bool) -> FeatureVector -> [ValidationError] +unknown_bits p fv = + [ UnknownBit i + | i <- set_bits fv, p i, isNothing (feature_by_bit i) ] + +both_bits_errors :: FeatureVector -> [ValidationError] +both_bits_errors fv = + [ BothBitsSet f + | f <- known_features + , test_bit (level_bit f Required) fv + , test_bit (level_bit f Optional) fv ] + +context_errors :: Context -> FeatureVector -> [ValidationError] +context_errors ctx fv = + [ ContextNotAllowed f ctx + | (f, _) <- list_features fv, not (presented f) ] + where + presented f = case feature_contexts f of + [] -> ctx `elem` [InitContext, NodeContext, InvoiceContext] + cs -> ctx `elem` cs + +dependency_errors :: FeatureVector -> [ValidationError] +dependency_errors fv = + [ MissingDependency f d + | (f, _) <- list_features fv + , d <- feature_dependencies f + , not (feature_assumed d) + , isNothing (test_feature d fv) ] + +channel_type_errors :: FeatureVector -> [ValidationError] +channel_type_errors fv = case channel_type fv of + Nothing -> [InvalidChannelType] + Just _ -> [] + +-- channel types ------------------------------------------------------------- + +-- | The basic channel types of BOLT #2. +data BasicChannelType + = BasicStaticRemotekey + -- ^ 'OptionStaticRemotekey' (bit 12) + | BasicAnchors + -- ^ 'OptionAnchors' and 'OptionStaticRemotekey' (bits 22 and 12) + | BasicZeroFeeCommitments + -- ^ 'ZeroFeeCommitments' (bit 40) + deriving (Eq, Ord, Show, Generic) + +instance NFData BasicChannelType + +-- | A channel type, per BOLT #2: a basic type plus any of the +-- 'OptionScidAlias' and 'OptionZeroconf' variations. +-- +-- Channel types are an enumeration that reuses even feature bits; +-- they aren't subject to the dependency or context rules for +-- feature vectors. BOLT #2 also forbids 'OptionScidAlias' in the +-- channel type of a channel to be announced, which needs the +-- @announce_channel@ flag to check. +data ChannelType = ChannelType { + ct_basic :: !BasicChannelType + , ct_scid_alias :: !Bool + , ct_zeroconf :: !Bool + } + deriving (Eq, Show, Generic) + +instance NFData ChannelType + +-- | The channel type that a @channel_type@ vector encodes, if it is a +-- defined one. Leading zero bytes are ignored. +-- +-- >>> let zeroconf = ChannelType BasicStaticRemotekey False True +-- >>> channel_type (parse "\EOT\NUL\NUL\NUL\NUL\DLE\NUL") == Just zeroconf +-- True +-- >>> channel_type (set_bit 13 empty) +-- Nothing +channel_type :: FeatureVector -> Maybe ChannelType +channel_type fv = find ((== fv) . channel_type_features) channel_types + +-- All defined channel types. +channel_types :: [ChannelType] +channel_types = + [ ChannelType b s z + | b <- [BasicStaticRemotekey, BasicAnchors, BasicZeroFeeCommitments] + , s <- [False, True] + , z <- [False, True] ] + +-- | The @channel_type@ vector for a channel type. +-- +-- >>> let anchors = ChannelType BasicAnchors False False +-- >>> render (channel_type_features anchors) +-- "@\DLE\NUL" +channel_type_features :: ChannelType -> FeatureVector +channel_type_features (ChannelType b s z) = + foldr (\f -> set_feature f Required) empty + (basic b ++ [OptionScidAlias | s] ++ [OptionZeroconf | z]) + where + basic BasicStaticRemotekey = [OptionStaticRemotekey] + basic BasicAnchors = [OptionAnchors, OptionStaticRemotekey] + basic BasicZeroFeeCommitments = [ZeroFeeCommitments] diff --git a/lib/Lightning/Protocol/BOLT9/Codec.hs b/lib/Lightning/Protocol/BOLT9/Codec.hs @@ -1,193 +0,0 @@ -{-# OPTIONS_HADDOCK prune #-} -{-# LANGUAGE BangPatterns #-} - --- | --- Module: Lightning.Protocol.BOLT9.Codec --- Copyright: (c) 2025 Jared Tobin --- License: MIT --- Maintainer: Jared Tobin <jared@ppad.tech> --- --- Parsing and rendering for BOLT #9 feature vectors. - -module Lightning.Protocol.BOLT9.Codec ( - -- * Parsing and rendering - parse - , render - - -- * Bit operations - , setBit - , clearBit - , testBit - - -- * Feature operations - , setFeature - , setFeatureWithDeps - , hasFeature - , isFeatureSet - , listFeatures - ) where - -import Data.ByteString (ByteString) -import qualified Data.ByteString as BS -import Data.Word (Word16) -import Lightning.Protocol.BOLT9.Features -import Lightning.Protocol.BOLT9.Types - ( FeatureLevel(..) - , FeatureVector - , bitIndex - , clear - , fromByteString - , member - , set - , unFeatureVector - ) - --- Parsing and rendering ------------------------------------------------------ - --- | Parse a ByteString into a FeatureVector. --- --- Alias for 'fromByteString'. -parse :: ByteString -> FeatureVector -parse = fromByteString -{-# INLINE parse #-} - --- | Render a FeatureVector to a ByteString, trimming leading zero bytes --- for compact encoding. -render :: FeatureVector -> ByteString -render = BS.dropWhile (== 0) . unFeatureVector -{-# INLINE render #-} - --- Bit operations ------------------------------------------------------------- - --- | Set a bit by raw index. --- --- >>> setBit 17 empty --- FeatureVector {unFeatureVector = "\STX"} -setBit :: Word16 -> FeatureVector -> FeatureVector -setBit !idx = set (bitIndex idx) -{-# INLINE setBit #-} - --- | Clear a bit by raw index. --- --- >>> clearBit 17 (setBit 17 empty) --- FeatureVector {unFeatureVector = ""} -clearBit :: Word16 -> FeatureVector -> FeatureVector -clearBit !idx = clear (bitIndex idx) -{-# INLINE clearBit #-} - --- | Test if a bit is set. --- --- >>> testBit 17 (setBit 17 empty) --- True --- >>> testBit 16 (setBit 17 empty) --- False -testBit :: Word16 -> FeatureVector -> Bool -testBit !idx = member (bitIndex idx) -{-# INLINE testBit #-} - --- Feature operations --------------------------------------------------------- - --- | Set a feature's bit at the given level. --- --- 'Required' sets the even bit, 'Optional' sets the odd bit. The --- other bit of the pair is cleared, so both are never set. --- --- >>> import Data.Maybe (fromJust) --- >>> let mpp = fromJust (featureByName "basic_mpp") --- >>> setFeature mpp Optional empty -- set optional bit (17) --- FeatureVector {unFeatureVector = "\STX"} --- >>> setFeature mpp Required empty -- set required bit (16) --- FeatureVector {unFeatureVector = "\SOH"} -setFeature :: Feature -> FeatureLevel -> FeatureVector -> FeatureVector -setFeature !f !level = case level of - Required -> setBit baseBit . clearBit (baseBit + 1) - Optional -> setBit (baseBit + 1) . clearBit baseBit - where - !baseBit = featureBaseBit f -{-# INLINE setFeature #-} - --- | Set a feature and all its transitive dependencies. --- --- Dependencies that are not yet set are set at the same level as --- the feature itself; dependencies already set keep their level. --- Unknown dependencies (not in the known features table) are --- silently skipped. --- --- >>> import Data.Maybe (fromJust) --- >>> let mpp = fromJust (featureByName "basic_mpp") --- >>> let fv = setFeatureWithDeps mpp Optional empty --- >>> isFeatureSet mpp fv --- True --- >>> let Just ps = featureByName "payment_secret" --- >>> isFeatureSet ps fv -- dependency auto-set --- True -setFeatureWithDeps - :: Feature -> FeatureLevel -> FeatureVector -> FeatureVector -setFeatureWithDeps !f !level !fv = - let !fv' = setFeature f level fv - in foldr setDep fv' (featureDependencies f) - where - setDep !depName !acc = case featureByName depName of - Nothing -> acc - Just dep - | isFeatureSet dep acc -> - foldr setDep acc (featureDependencies dep) - | otherwise -> - setFeatureWithDeps dep level acc - --- | Check if a feature is set in the vector. --- --- Returns: --- --- * @Just Required@ if the required (even) bit is set --- * @Just Optional@ if the optional (odd) bit is set (and required is not) --- * @Nothing@ if neither bit is set --- --- >>> import Data.Maybe (fromJust) --- >>> let mpp = fromJust (featureByName "basic_mpp") --- >>> hasFeature mpp (setFeature mpp Optional empty) --- Just Optional --- >>> hasFeature mpp (setFeature mpp Required empty) --- Just Required --- >>> hasFeature mpp empty --- Nothing -hasFeature :: Feature -> FeatureVector -> Maybe FeatureLevel -hasFeature !f !fv - | testBit baseBit fv = Just Required - | testBit (baseBit + 1) fv = Just Optional - | otherwise = Nothing - where - !baseBit = featureBaseBit f -{-# INLINE hasFeature #-} - --- | Check if either bit of a feature is set in the vector. --- --- >>> import Data.Maybe (fromJust) --- >>> let mpp = fromJust (featureByName "basic_mpp") --- >>> isFeatureSet mpp (setFeature mpp Optional empty) --- True --- >>> isFeatureSet mpp empty --- False -isFeatureSet :: Feature -> FeatureVector -> Bool -isFeatureSet !f !fv = - let !baseBit = featureBaseBit f - in testBit baseBit fv || testBit (baseBit + 1) fv -{-# INLINE isFeatureSet #-} - --- | List all known features that are set in the vector. --- --- Returns pairs of (Feature, FeatureLevel) indicating whether each --- feature is set as required or optional. --- --- >>> import Data.Maybe (fromJust) --- >>> let mpp = fromJust (featureByName "basic_mpp") --- >>> let ps = fromJust (featureByName "payment_secret") --- >>> let fv = setFeature mpp Optional (setFeature ps Required empty) --- >>> map (\(f, l) -> (featureName f, l)) (listFeatures fv) --- [("payment_secret",Required),("basic_mpp",Optional)] -listFeatures :: FeatureVector -> [(Feature, FeatureLevel)] -listFeatures !fv = foldr check [] knownFeatures - where - check !f !acc = case hasFeature f fv of - Just level -> (f, level) : acc - Nothing -> acc diff --git a/lib/Lightning/Protocol/BOLT9/Features.hs b/lib/Lightning/Protocol/BOLT9/Features.hs @@ -1,161 +0,0 @@ -{-# OPTIONS_HADDOCK prune #-} -{-# LANGUAGE BangPatterns #-} -{-# LANGUAGE DeriveGeneric #-} - --- | --- Module: Lightning.Protocol.BOLT9.Features --- Copyright: (c) 2025 Jared Tobin --- License: MIT --- Maintainer: Jared Tobin <jared@ppad.tech> --- --- Known feature table for the Lightning Network, per --- [BOLT #9](https://github.com/lightning/bolts/blob/master/09-features.md). - -module Lightning.Protocol.BOLT9.Features ( - -- * Feature - Feature(..) - - -- * Known feature - , KnownFeature(..) - , knownFeatureByBit - , knownFeatureByName - - -- * Lookup - , featureByBit - , featureByName - - -- * Known features table - , knownFeatures - ) where - -import Control.DeepSeq (NFData) -import Data.IntMap.Strict (IntMap) -import qualified Data.IntMap.Strict as IM -import Data.Map.Strict (Map) -import qualified Data.Map.Strict as M -import Data.Word (Word16) -import GHC.Generics (Generic) -import Lightning.Protocol.BOLT9.Types (Context(..)) - --- | A known feature from the BOLT #9 specification. -data Feature = Feature { - featureName :: !String - -- ^ The canonical name of the feature. - , featureBaseBit :: {-# UNPACK #-} !Word16 - -- ^ The even (compulsory) bit number; the odd (optional) bit is - -- @baseBit + 1@. - , featureContexts :: ![Context] - -- ^ Contexts in which this feature may be presented. - , featureDependencies :: ![String] - -- ^ Names of features this feature depends on. - , featureAssumed :: !Bool - -- ^ Whether this feature is assumed to be universally supported. - } - deriving (Eq, Show, Generic) - -instance NFData Feature - --- | A feature that is known to be in the BOLT #9 specification. --- --- Constructed via 'knownFeatureByBit' or 'knownFeatureByName', --- which validate that the feature exists in the known table. -newtype KnownFeature = KnownFeature { - unKnownFeature :: Feature - -- ^ Extract the underlying 'Feature'. - } - deriving (Eq, Show, Generic) - -instance NFData KnownFeature - --- | Look up a known feature by bit number. --- --- Accepts either the even (compulsory) or odd (optional) bit. --- --- >>> fmap (featureName . unKnownFeature) (knownFeatureByBit 16) --- Just "basic_mpp" --- >>> knownFeatureByBit 999 --- Nothing -knownFeatureByBit :: Word16 -> Maybe KnownFeature -knownFeatureByBit !bit = fmap KnownFeature (featureByBit bit) -{-# INLINE knownFeatureByBit #-} - --- | Look up a known feature by its canonical name. --- --- >>> fmap (featureName . unKnownFeature) --- ... (knownFeatureByName "basic_mpp") --- Just "basic_mpp" --- >>> knownFeatureByName "nonexistent" --- Nothing -knownFeatureByName :: String -> Maybe KnownFeature -knownFeatureByName !name = - fmap KnownFeature (featureByName name) -{-# INLINE knownFeatureByName #-} - --- | The complete table of known features from BOLT #9. -knownFeatures :: [Feature] -knownFeatures = [ - Feature "option_data_loss_protect" 0 [] [] True - , Feature "option_upfront_shutdown_script" 4 [Init, NodeAnn] [] False - , Feature "gossip_queries" 6 [] [] False - , Feature "var_onion_optin" 8 [] [] True - , Feature "gossip_queries_ex" 10 [Init, NodeAnn] [] False - , Feature "option_static_remotekey" 12 [] [] True - , Feature "payment_secret" 14 [] [] True - , Feature "basic_mpp" 16 [Init, NodeAnn, Invoice] - ["payment_secret"] False - , Feature "option_support_large_channel" 18 [Init, NodeAnn] [] False - , Feature "option_anchors" 22 [Init, NodeAnn, ChanType] [] False - , Feature "option_route_blinding" 24 [Init, NodeAnn, Invoice] [] False - , Feature "option_shutdown_anysegwit" 26 [Init, NodeAnn] [] False - , Feature "option_dual_fund" 28 [Init, NodeAnn] [] False - , Feature "option_quiesce" 34 [Init, NodeAnn] [] False - , Feature "option_attribution_data" 36 [Init, NodeAnn, Invoice] - [] False - , Feature "option_onion_messages" 38 [Init, NodeAnn] [] False - , Feature "option_provide_storage" 42 [Init, NodeAnn] [] False - , Feature "option_channel_type" 44 [] [] True - , Feature "option_scid_alias" 46 [Init, NodeAnn, ChanType] [] False - , Feature "option_payment_metadata" 48 [Invoice] [] False - , Feature "option_zeroconf" 50 [Init, NodeAnn, ChanType] - ["option_scid_alias"] False - , Feature "option_simple_close" 60 [Init, NodeAnn] - ["option_shutdown_anysegwit"] False - ] - --- | Look up a feature by bit number. --- --- Accepts either the even (compulsory) or odd (optional) bit of the pair. --- --- >>> fmap featureName (featureByBit 16) --- Just "basic_mpp" --- >>> fmap featureName (featureByBit 17) -- odd bit also works --- Just "basic_mpp" --- >>> featureByBit 999 --- Nothing -featureByBit :: Word16 -> Maybe Feature -featureByBit !bit = - let !baseBit = fromIntegral bit - (fromIntegral bit `mod` 2) - in IM.lookup baseBit featuresByBit -{-# INLINE featureByBit #-} - --- | Look up a feature by its canonical name. --- --- >>> fmap featureBaseBit (featureByName "basic_mpp") --- Just 16 --- >>> featureByName "nonexistent" --- Nothing -featureByName :: String -> Maybe Feature -featureByName !name = M.lookup name featuresByName -{-# INLINE featureByName #-} - --- Lookup tables ------------------------------------------------------------- - --- | Features indexed by base bit (even bit number). -featuresByBit :: IntMap Feature -featuresByBit = IM.fromList - [(fromIntegral (featureBaseBit f), f) | f <- knownFeatures] - --- | Features indexed by canonical name. -featuresByName :: Map String Feature -featuresByName = M.fromList - [(featureName f, f) | f <- knownFeatures] diff --git a/lib/Lightning/Protocol/BOLT9/Types.hs b/lib/Lightning/Protocol/BOLT9/Types.hs @@ -1,282 +0,0 @@ -{-# OPTIONS_HADDOCK prune #-} -{-# LANGUAGE BangPatterns #-} -{-# LANGUAGE DeriveGeneric #-} - --- | --- Module: Lightning.Protocol.BOLT9.Types --- Copyright: (c) 2025 Jared Tobin --- License: MIT --- Maintainer: Jared Tobin <jared@ppad.tech> --- --- Baseline types for BOLT #9 feature flags. - -module Lightning.Protocol.BOLT9.Types ( - -- * Context - Context(..) - , isChannelContext - , channelParity - - -- * Bit indices - , BitIndex - , unBitIndex - , bitIndex - - -- * Required/optional level - , FeatureLevel(..) - - -- * Required/optional bits - , RequiredBit - , unRequiredBit - , requiredBit - , requiredFromBitIndex - - , OptionalBit - , unOptionalBit - , optionalBit - , optionalFromBitIndex - - -- * Feature vectors - , FeatureVector - , unFeatureVector - , empty - , fromByteString - , set - , clear - , member - ) where - -import Control.DeepSeq (NFData) -import qualified Data.Bits as B -import Data.ByteString (ByteString) -import qualified Data.ByteString as BS -import Data.Word (Word8, Word16) -import GHC.Generics (Generic) - --- Context ------------------------------------------------------------------ - --- | Presentation context for feature flags. --- --- Per BOLT #9, features are presented in different message contexts: --- --- * 'Init' - the @init@ message --- * 'NodeAnn' - @node_announcement@ messages --- * 'ChanAnn' - @channel_announcement@ messages (normal) --- * 'ChanAnnOdd' - @channel_announcement@, always odd (optional) --- * 'ChanAnnEven' - @channel_announcement@, always even (required) --- * 'Invoice' - BOLT 11 invoices --- * 'Blinded' - @allowed_features@ field of a blinded path --- * 'ChanType' - @channel_type@ field when opening channels -data Context - = Init -- ^ I: presented in the @init@ message - | NodeAnn -- ^ N: presented in @node_announcement@ messages - | ChanAnn -- ^ C: presented in @channel_announcement@ message - | ChanAnnOdd -- ^ C-: @channel_announcement@, always odd (optional) - | ChanAnnEven -- ^ C+: @channel_announcement@, always even (required) - | Invoice -- ^ 9: presented in BOLT 11 invoices - | Blinded -- ^ B: @allowed_features@ field of a blinded path - | ChanType -- ^ T: @channel_type@ field when opening channels - deriving (Eq, Ord, Show, Generic) - -instance NFData Context - --- | Check if a context is a channel announcement context (C, C-, or C+). -isChannelContext :: Context -> Bool -isChannelContext ChanAnn = True -isChannelContext ChanAnnOdd = True -isChannelContext ChanAnnEven = True -isChannelContext _ = False -{-# INLINE isChannelContext #-} - --- | For channel contexts with forced parity, return 'Just' the required --- parity: 'True' for even (C+), 'False' for odd (C-). Returns 'Nothing' --- for contexts without forced parity. -channelParity :: Context -> Maybe Bool -channelParity ChanAnnOdd = Just False -- odd -channelParity ChanAnnEven = Just True -- even -channelParity _ = Nothing -{-# INLINE channelParity #-} - --- FeatureLevel ------------------------------------------------------------- - --- | Whether a feature is set as required or optional. --- --- Per BOLT #9, each feature has a pair of bits: the even bit indicates --- required (compulsory) support, the odd bit indicates optional support. -data FeatureLevel - = Required -- ^ The feature is required (even bit set) - | Optional -- ^ The feature is optional (odd bit set) - deriving (Eq, Ord, Show, Generic) - -instance NFData FeatureLevel - --- BitIndex ----------------------------------------------------------------- - --- | A bit index into a feature vector. Bit 0 is the least significant bit. --- --- Valid range: 0-65535 (sufficient for any practical feature flag). -newtype BitIndex = BitIndex { unBitIndex :: Word16 } - deriving (Eq, Ord, Show, Generic) - -instance NFData BitIndex - --- | Smart constructor for 'BitIndex'. Always succeeds since all Word16 --- values are valid. -bitIndex :: Word16 -> BitIndex -bitIndex = BitIndex -{-# INLINE bitIndex #-} - --- RequiredBit -------------------------------------------------------------- - --- | A required (compulsory) feature bit. Required bits are always even. -newtype RequiredBit = RequiredBit { unRequiredBit :: Word16 } - deriving (Eq, Ord, Show, Generic) - -instance NFData RequiredBit - --- | Smart constructor for 'RequiredBit'. Returns 'Nothing' if the bit --- index is odd. --- --- >>> requiredBit 16 --- Just (RequiredBit {unRequiredBit = 16}) --- >>> requiredBit 17 --- Nothing -requiredBit :: Word16 -> Maybe RequiredBit -requiredBit !w - | w B..&. 1 == 0 = Just (RequiredBit w) - | otherwise = Nothing -{-# INLINE requiredBit #-} - --- | Convert a 'BitIndex' to a 'RequiredBit'. Returns 'Nothing' if odd. -requiredFromBitIndex :: BitIndex -> Maybe RequiredBit -requiredFromBitIndex (BitIndex w) = requiredBit w -{-# INLINE requiredFromBitIndex #-} - --- OptionalBit -------------------------------------------------------------- - --- | An optional feature bit. Optional bits are always odd. -newtype OptionalBit = OptionalBit { unOptionalBit :: Word16 } - deriving (Eq, Ord, Show, Generic) - -instance NFData OptionalBit - --- | Smart constructor for 'OptionalBit'. Returns 'Nothing' if the bit --- index is even. --- --- >>> optionalBit 17 --- Just (OptionalBit {unOptionalBit = 17}) --- >>> optionalBit 16 --- Nothing -optionalBit :: Word16 -> Maybe OptionalBit -optionalBit !w - | w B..&. 1 == 1 = Just (OptionalBit w) - | otherwise = Nothing -{-# INLINE optionalBit #-} - --- | Convert a 'BitIndex' to an 'OptionalBit'. Returns 'Nothing' if even. -optionalFromBitIndex :: BitIndex -> Maybe OptionalBit -optionalFromBitIndex (BitIndex w) = optionalBit w -{-# INLINE optionalFromBitIndex #-} - --- FeatureVector ------------------------------------------------------------ - --- | A feature vector represented as a strict ByteString. --- --- The vector is stored in big-endian byte order (most significant byte --- first), with bits numbered from the least significant bit of the last --- byte. Bit 0 is at position 0 of the last byte. -newtype FeatureVector = FeatureVector { unFeatureVector :: ByteString } - deriving (Eq, Ord, Show, Generic) - -instance NFData FeatureVector - --- | The empty feature vector (no features set). --- --- >>> empty --- FeatureVector {unFeatureVector = ""} -empty :: FeatureVector -empty = FeatureVector BS.empty -{-# INLINE empty #-} - --- | Wrap a ByteString as a FeatureVector. -fromByteString :: ByteString -> FeatureVector -fromByteString = FeatureVector -{-# INLINE fromByteString #-} - --- | Set a bit in the feature vector. --- --- >>> set (bitIndex 0) empty --- FeatureVector {unFeatureVector = "\SOH"} --- >>> set (bitIndex 8) empty --- FeatureVector {unFeatureVector = "\SOH\NUL"} -set :: BitIndex -> FeatureVector -> FeatureVector -set (BitIndex idx) (FeatureVector bs) = - let byteIdx = fromIntegral idx `div` 8 - bitOffset = fromIntegral idx `mod` 8 - len = BS.length bs - -- Number of bytes needed to hold this bit - needed = byteIdx + 1 - -- Pad with zeros if necessary (prepend to maintain big-endian) - bs' = if needed > len - then BS.replicate (needed - len) 0 <> bs - else bs - len' = BS.length bs' - -- Index from the end (big-endian: last byte has lowest bits) - realIdx = len' - 1 - byteIdx - oldByte = BS.index bs' realIdx - newByte = oldByte B..|. B.shiftL 1 bitOffset - in FeatureVector (updateByteAt realIdx newByte bs') -{-# INLINE set #-} - --- | Clear a bit in the feature vector. -clear :: BitIndex -> FeatureVector -> FeatureVector -clear (BitIndex idx) (FeatureVector bs) - | BS.null bs = FeatureVector bs - | otherwise = - let byteIdx = fromIntegral idx `div` 8 - bitOffset = fromIntegral idx `mod` 8 - len = BS.length bs - in if byteIdx >= len - then FeatureVector bs -- bit not in range, already clear - else - let realIdx = len - 1 - byteIdx - oldByte = BS.index bs realIdx - newByte = oldByte B..&. B.complement (B.shiftL 1 bitOffset) - in FeatureVector (stripLeadingZeros (updateByteAt realIdx newByte bs)) -{-# INLINE clear #-} - --- | Test if a bit is set in the feature vector. --- --- >>> member (bitIndex 0) (set (bitIndex 0) empty) --- True --- >>> member (bitIndex 1) (set (bitIndex 0) empty) --- False -member :: BitIndex -> FeatureVector -> Bool -member (BitIndex idx) (FeatureVector bs) - | BS.null bs = False - | otherwise = - let byteIdx = fromIntegral idx `div` 8 - bitOffset = fromIntegral idx `mod` 8 - len = BS.length bs - in if byteIdx >= len - then False - else - let realIdx = len - 1 - byteIdx - byte = BS.index bs realIdx - in byte B..&. B.shiftL 1 bitOffset /= 0 -{-# INLINE member #-} - --- Internal helpers --------------------------------------------------------- - --- | Update a single byte at the given index. -updateByteAt :: Int -> Word8 -> ByteString -> ByteString -updateByteAt !i !w !bs = - let (before, after) = BS.splitAt i bs - in case BS.uncons after of - Nothing -> bs -- shouldn't happen if i is valid - Just (_, rest) -> before <> BS.singleton w <> rest -{-# INLINE updateByteAt #-} - --- | Remove leading zero bytes from a ByteString. -stripLeadingZeros :: ByteString -> ByteString -stripLeadingZeros = BS.dropWhile (== 0) -{-# INLINE stripLeadingZeros #-} diff --git a/lib/Lightning/Protocol/BOLT9/Validate.hs b/lib/Lightning/Protocol/BOLT9/Validate.hs @@ -1,308 +0,0 @@ -{-# OPTIONS_HADDOCK prune #-} -{-# LANGUAGE BangPatterns #-} -{-# LANGUAGE DeriveGeneric #-} - --- | --- Module: Lightning.Protocol.BOLT9.Validate --- Copyright: (c) 2025 Jared Tobin --- License: MIT --- Maintainer: Jared Tobin <jared@ppad.tech> --- --- Validation for BOLT #9 feature vectors. - -module Lightning.Protocol.BOLT9.Validate ( - -- * Error types - ValidationError(..) - - -- * Local validation - , validateLocal - - -- * Remote validation - , validateRemote - - -- * Validated construction - , setFeatureForContext - , validateNoBothBits - - -- * Helpers - , highestSetBit - , setBits - ) where - -import Control.DeepSeq (NFData) -import Data.ByteString (ByteString) -import qualified Data.ByteString as BS -import qualified Data.Bits as B -import Data.Word (Word16) -import GHC.Generics (Generic) -import Lightning.Protocol.BOLT9.Codec - (isFeatureSet, setFeature, testBit) -import Lightning.Protocol.BOLT9.Features -import Lightning.Protocol.BOLT9.Types - --- | Validation errors for feature vectors. -data ValidationError - = BothBitsSet {-# UNPACK #-} !Word16 !String - -- ^ Both optional and required bits are set for a feature. - -- Arguments: base bit index, feature name. - | MissingDependency !String !String - -- ^ A feature's dependency is not set. - -- Arguments: feature name, missing dependency name. - | ContextNotAllowed !String !Context - -- ^ A feature is not allowed in the given context. - -- Arguments: feature name, context. - | UnknownRequiredBit {-# UNPACK #-} !Int - -- ^ An unknown required (even) bit is set (remote validation only). - -- Argument: bit index. - | InvalidParity {-# UNPACK #-} !Int !Context - -- ^ A bit has invalid parity for a channel context. - -- Arguments: bit index, context (ChanAnnOdd or ChanAnnEven). - deriving (Eq, Show, Generic) - -instance NFData ValidationError - --- Local validation ----------------------------------------------------------- - --- | Validate a feature vector for local use (vectors we create/send). --- --- Checks: --- --- * No feature has both optional and required bits set --- * All set features are valid for the given context --- * All dependencies of set features are also set --- * C- context forces odd bits only, C+ forces even bits only --- --- >>> import Data.Maybe (fromJust) --- >>> import Lightning.Protocol.BOLT9.Codec (setFeature) --- >>> let mpp = fromJust (featureByName "basic_mpp") --- >>> let ps = fromJust (featureByName "payment_secret") --- >>> validateLocal Init (setFeature mpp False empty) --- Left [MissingDependency "basic_mpp" "payment_secret"] --- >>> validateLocal Init (setFeature mpp False (setFeature ps False empty)) --- Right () -validateLocal :: Context -> FeatureVector -> Either [ValidationError] () -validateLocal !ctx !fv = - let errs = bothBitsErrors fv - ++ contextErrors ctx fv - ++ dependencyErrors fv - ++ parityErrors ctx fv - in if null errs - then Right () - else Left errs - --- | Check for features with both bits set. -bothBitsErrors :: FeatureVector -> [ValidationError] -bothBitsErrors !fv = foldr check [] knownFeatures - where - check !f !acc = - let !baseBit = featureBaseBit f - in if testBit baseBit fv && testBit (baseBit + 1) fv - then BothBitsSet baseBit (featureName f) : acc - else acc - --- | Check for features not allowed in the given context. -contextErrors :: Context -> FeatureVector -> [ValidationError] -contextErrors !ctx !fv = foldr check [] knownFeatures - where - check !f !acc = - let !contexts = featureContexts f - in if isFeatureSet f fv - && not (null contexts) - && not (contextAllowed ctx contexts) - then ContextNotAllowed (featureName f) ctx : acc - else acc - --- | Check if a context is allowed given a list of allowed contexts. -contextAllowed :: Context -> [Context] -> Bool -contextAllowed !ctx !allowed = ctx `elem` allowed || channelMatch - where - channelMatch = isChannelContext ctx && any isChannelContext allowed - --- | Check for missing dependencies. -dependencyErrors :: FeatureVector -> [ValidationError] -dependencyErrors !fv = foldr check [] knownFeatures - where - check !f !acc = - if isFeatureSet f fv - then checkDeps f (featureDependencies f) ++ acc - else acc - - checkDeps !f = foldr (checkOneDep f) [] - - checkOneDep !f !depName !acc = - case featureByName depName of - Nothing -> acc -- unknown dep, skip - Just !dep -> - if isFeatureSet dep fv - then acc - else MissingDependency (featureName f) depName : acc - --- | Check for parity errors in C- and C+ contexts. -parityErrors :: Context -> FeatureVector -> [ValidationError] -parityErrors !ctx !fv = case channelParity ctx of - Nothing -> [] - Just wantEven -> foldr (checkParity wantEven) [] (setBits fv) - where - checkParity !wantEven !bit !acc = - let isEven = bit `mod` 2 == 0 - in if isEven /= wantEven - then InvalidParity bit ctx : acc - else acc - --- Validated construction ------------------------------------------------------- - --- | Set a feature in a vector, validating that the feature is --- allowed in the given context and has correct parity. --- --- Checks: --- --- * The feature's context list includes the given context --- (or is empty, meaning all contexts are allowed) --- * For 'ChanAnnOdd', only 'Optional' (odd bit) is allowed --- * For 'ChanAnnEven', only 'Required' (even bit) is allowed --- --- >>> import Data.Maybe (fromJust) --- >>> let pm = fromJust (featureByName "option_payment_metadata") --- >>> setFeatureForContext Invoice pm Optional empty --- Right ... --- >>> setFeatureForContext Init pm Optional empty --- Left (ContextNotAllowed "option_payment_metadata" Init) -setFeatureForContext - :: Context - -> Feature - -> FeatureLevel - -> FeatureVector - -> Either ValidationError FeatureVector -setFeatureForContext !ctx !f !level !fv - | not (null contexts) - , not (contextAllowed ctx contexts) - = Left (ContextNotAllowed (featureName f) ctx) - | otherwise - = case channelParity ctx of - Just True | level == Optional -> - Left (InvalidParity (fromIntegral targetBit) ctx) - Just False | level == Required -> - Left (InvalidParity (fromIntegral targetBit) ctx) - _ -> Right (setFeature f level fv) - where - !contexts = featureContexts f - !baseBit = featureBaseBit f - !targetBit = case level of - Required -> baseBit - Optional -> baseBit + 1 - --- | Validate that no feature has both its required and optional --- bits set simultaneously. --- --- Returns the input vector unchanged on success. --- --- >>> validateNoBothBits empty --- Right ... -validateNoBothBits - :: FeatureVector -> Either ValidationError FeatureVector -validateNoBothBits !fv = go knownFeatures - where - go [] = Right fv - go (f:fs) = - let !baseBit = featureBaseBit f - in if testBit baseBit fv - && testBit (baseBit + 1) fv - then Left (BothBitsSet baseBit (featureName f)) - else go fs - --- Remote validation ---------------------------------------------------------- - --- | Validate a feature vector received from a remote peer. --- --- Checks: --- --- * Unknown odd (optional) bits are acceptable (ignored) --- * Unknown even (required) bits are errors --- * If both bits of a pair are set, treat as required (not an error) --- * Context restrictions still apply for known features --- * All dependencies of set features are also set --- --- >>> import Lightning.Protocol.BOLT9.Codec (setBit) --- >>> validateRemote Init (setBit 999 empty) -- unknown odd bit: ok --- Right () --- >>> validateRemote Init (setBit 998 empty) -- unknown even bit: error --- Left [UnknownRequiredBit 998] -validateRemote :: Context -> FeatureVector -> Either [ValidationError] () -validateRemote !ctx !fv = - let errs = unknownRequiredErrors fv - ++ contextErrors ctx fv - ++ dependencyErrors fv - ++ parityErrors ctx fv - in if null errs - then Right () - else Left errs - --- | Check for unknown required bits. -unknownRequiredErrors :: FeatureVector -> [ValidationError] -unknownRequiredErrors !fv = foldr check [] (setBits fv) - where - check !bit !acc - | bit `mod` 2 == 1 = acc -- odd bit, optional, ignore - | bit > fromIntegral (maxBound :: Word16) = - UnknownRequiredBit bit : acc - | otherwise = case featureByBit (fromIntegral bit) of - Just _ -> acc -- known feature - Nothing -> UnknownRequiredBit bit : acc - --- Helpers -------------------------------------------------------------------- - --- | Find the highest set bit in a feature vector. --- --- Returns 'Nothing' if the vector is empty or has no bits set. -highestSetBit :: FeatureVector -> Maybe Int -highestSetBit !fv = - let !bs = unFeatureVector fv - in if BS.null bs - then Nothing - else findHighestBit bs - --- | Find the highest set bit in a non-empty ByteString. -findHighestBit :: ByteString -> Maybe Int -findHighestBit !bs = go 0 - where - !len = BS.length bs - - go !i - | i >= len = Nothing - | otherwise = - let !byte = BS.index bs i - in if byte == 0 - then go (i + 1) - else - let !bytePos = len - 1 - i - !highBit = 7 - B.countLeadingZeros byte - !bitIdx = bytePos * 8 + highBit - in Just bitIdx - --- | Collect all set bits in a feature vector. --- --- Returns a list of bit indices in ascending order. -setBits :: FeatureVector -> [Int] -setBits !fv = - let !bs = unFeatureVector fv - !len = BS.length bs - in collectBits bs len 0 [] - --- | Collect bits from a ByteString into a list. -collectBits :: ByteString -> Int -> Int -> [Int] -> [Int] -collectBits !bs !len !i !acc - | i >= len = acc - | otherwise = - let !byte = BS.index bs (len - 1 - i) - !baseIdx = i * 8 - !acc' = collectByteBits byte baseIdx acc - in collectBits bs len (i + 1) acc' - --- | Collect set bits from a single byte. -collectByteBits :: B.Bits a => a -> Int -> [Int] -> [Int] -collectByteBits !byte !baseIdx = go 7 - where - go !bit !acc - | bit < 0 = acc - | B.testBit byte bit = go (bit - 1) ((baseIdx + bit) : acc) - | otherwise = go (bit - 1) acc diff --git a/ppad-bolt9.cabal b/ppad-bolt9.cabal @@ -1,6 +1,6 @@ cabal-version: 3.0 name: ppad-bolt9 -version: 0.0.1 +version: 0.1.0 synopsis: Feature flags per BOLT #9 license: MIT license-file: LICENSE @@ -8,11 +8,15 @@ author: Jared Tobin maintainer: jared@ppad.tech category: Cryptography build-type: Simple -tested-with: GHC == 9.10.3 +tested-with: GHC == { 9.10.3 } extra-doc-files: CHANGELOG description: - Feature flags, per + Lightning Network feature flags, per [BOLT #9](https://github.com/lightning/bolts/blob/master/09-features.md). + Provides an abstract feature vector type that preserves its wire + bytes, the table of known features with their contexts and + dependencies, sender- and receiver-side validation for each context, + and BOLT #2 channel types. source-repository head type: git @@ -25,14 +29,9 @@ library -Wall exposed-modules: Lightning.Protocol.BOLT9 - Lightning.Protocol.BOLT9.Codec - Lightning.Protocol.BOLT9.Features - Lightning.Protocol.BOLT9.Types - Lightning.Protocol.BOLT9.Validate build-depends: base >= 4.9 && < 5 , bytestring >= 0.9 && < 0.13 - , containers >= 0.6 && < 0.9 , deepseq >= 1.4 && < 1.6 test-suite bolt9-tests @@ -42,7 +41,7 @@ test-suite bolt9-tests main-is: Main.hs ghc-options: - -rtsopts -Wall -O2 + -rtsopts -Wall build-depends: base @@ -60,7 +59,7 @@ benchmark bolt9-bench other-modules: Fixtures ghc-options: - -rtsopts -O2 -Wall -fno-warn-orphans + -rtsopts -O2 -Wall build-depends: base @@ -77,7 +76,7 @@ benchmark bolt9-weigh other-modules: Fixtures ghc-options: - -rtsopts -O2 -Wall -fno-warn-orphans + -rtsopts -O2 -Wall build-depends: base