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 710798bbc64d9aad92da076b5a56896a53fbd847
parent 5c2e239fdd5d5ccce99572aad742bef4cdd3e70f
Author: Jared Tobin <jared@jtobin.io>
Date:   Sat, 10 Oct 2026 13:28:21 +0400

test: rewrite against the new API

Asserts the feature table against the spec, adds the BOLT #11 invoice
feature vectors, channel-type cases, negative tests for each
validation rule per context, and properties over arbitrary vectors.

Diffstat:
Mtest/Main.hs | 1117++++++++++++++++++++++++++++++++++++-------------------------------------------
1 file changed, 513 insertions(+), 604 deletions(-)

diff --git a/test/Main.hs b/test/Main.hs @@ -1,638 +1,547 @@ {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE ScopedTypeVariables #-} module Main where +import qualified Data.Bits as B +import Data.ByteString (ByteString) import qualified Data.ByteString as BS -import Data.Either (isLeft, isRight) -import Data.Maybe (isJust, isNothing) -import Data.Word (Word8, Word16) +import Data.List (elemIndex, subsequences) +import Data.List.NonEmpty (NonEmpty(..)) +import Data.Word (Word8) import Lightning.Protocol.BOLT9 import Test.Tasty import Test.Tasty.HUnit import Test.Tasty.QuickCheck main :: IO () -main = defaultMain tests - -tests :: TestTree -tests = testGroup "ppad-bolt9" [ - featureTableTests - , bitParityTests - , featureVectorTests - , validationTests - , knownFeatureTests - , setFeatureWithDepsTests - , setFeatureForContextTests - , validateNoBothBitsTests - , remoteDependencyTests - , longVectorTests - , oppositeBitTests - , propertyTests +main = defaultMain $ testGroup "ppad-bolt9" [ + table_tests + , vector_tests + , feature_tests + , local_tests + , remote_tests + , invoice_tests + , channel_type_tests + , properties ] --- Feature table tests --------------------------------------------------------- - -featureTableTests :: TestTree -featureTableTests = testGroup "Feature table" [ - testCase "all features have even baseBit" $ - all (even . featureBaseBit) knownFeatures @?= True - - , testCase "featureByBit works for even bit" $ - case featureByBit 14 of - Nothing -> assertFailure "expected to find payment_secret" - Just f -> featureName f @?= "payment_secret" - - , testCase "featureByBit works for odd bit" $ - case featureByBit 15 of - Nothing -> assertFailure "expected to find payment_secret" - Just f -> featureName f @?= "payment_secret" - - , testCase "featureByBit returns Nothing for unknown bit" $ - featureByBit 100 @?= Nothing - - , testCase "featureByName finds known features" $ do - isJust (featureByName "var_onion_optin") @?= True - isJust (featureByName "payment_secret") @?= True - isJust (featureByName "basic_mpp") @?= True - - , testCase "featureByName returns Nothing for unknown names" $ - featureByName "nonexistent_feature" @?= Nothing - - , testCase "assumed features are present" $ do - let assumedNames = [ "option_data_loss_protect" - , "var_onion_optin" - , "option_static_remotekey" - , "payment_secret" - , "option_channel_type" - ] - mapM_ (\n -> isJust (featureByName n) @? - ("assumed feature missing: " ++ n)) assumedNames - - , testCase "assumed features are marked as assumed" $ do - let checkAssumed n = case featureByName n of - Nothing -> assertFailure $ "feature not found: " ++ n - Just f -> featureAssumed f @?= True - mapM_ checkAssumed [ "option_data_loss_protect" - , "var_onion_optin" - , "option_static_remotekey" - , "payment_secret" - , "option_channel_type" - ] +-- feature table ------------------------------------------------------------- + +-- | The BOLT #9 table at lightning/bolts@1aadb719: bits, name, whether +-- it is ASSUMED, the Context column, and the dependency names. +spec_table :: [(Int, String, Bool, String, [String])] +spec_table = [ + (0, "option_data_loss_protect", True, "", []) + , (4, "option_upfront_shutdown_script", False, "IN", []) + , (6, "gossip_queries", False, "", []) + , (8, "var_onion_optin", True, "", []) + , (10, "gossip_queries_ex", False, "IN", []) + , (12, "option_static_remotekey", True, "", []) + , (14, "payment_secret", True, "", []) + , (16, "basic_mpp", False, "IN9", ["payment_secret"]) + , (18, "option_support_large_channel", False, "IN", []) + , (22, "option_anchors", False, "INT", []) + , (24, "option_route_blinding", False, "IN9", []) + , (26, "option_shutdown_anysegwit", False, "IN", []) + , (28, "option_dual_fund", False, "IN", []) + , (34, "option_quiesce", False, "IN", []) + , (36, "option_attribution_data", False, "IN9", []) + , (38, "option_onion_messages", False, "IN", []) + , (40, "zero_fee_commitments", False, "IN", ["option_channel_type"]) + , (42, "option_provide_storage", False, "IN", []) + , (44, "option_channel_type", True, "", []) + , (46, "option_scid_alias", False, "INT", []) + , (48, "option_payment_metadata", False, "9", []) + , (50, "option_zeroconf", False, "INT", ["option_scid_alias"]) + , (60, "option_simple_close", False, "IN" + , ["option_shutdown_anysegwit"]) + , (62, "option_splice", False, "IN", []) + , (66, "option_onion_messages_only_channels", False, "IN" + , ["option_onion_messages"]) ] --- Bit parity tests ------------------------------------------------------------ - -bitParityTests :: TestTree -bitParityTests = testGroup "Bit parity" [ - testCase "requiredBit accepts even numbers" $ do - isJust (requiredBit 0) @?= True - isJust (requiredBit 2) @?= True - isJust (requiredBit 100) @?= True - - , testCase "requiredBit rejects odd numbers" $ do - isNothing (requiredBit 1) @?= True - isNothing (requiredBit 3) @?= True - isNothing (requiredBit 101) @?= True - - , testCase "optionalBit accepts odd numbers" $ do - isJust (optionalBit 1) @?= True - isJust (optionalBit 3) @?= True - isJust (optionalBit 101) @?= True - - , testCase "optionalBit rejects even numbers" $ do - isNothing (optionalBit 0) @?= True - isNothing (optionalBit 2) @?= True - isNothing (optionalBit 100) @?= True - - , testCase "requiredBit smart constructor preserves value" $ - case requiredBit 42 of - Nothing -> assertFailure "expected Just" - Just rb -> unRequiredBit rb @?= 42 - - , testCase "optionalBit smart constructor preserves value" $ - case optionalBit 43 of - Nothing -> assertFailure "expected Just" - Just ob -> unOptionalBit ob @?= 43 - ] - --- FeatureVector tests --------------------------------------------------------- - -featureVectorTests :: TestTree -featureVectorTests = testGroup "FeatureVector" [ - testCase "empty has no bits set" $ do - unFeatureVector empty @?= BS.empty - member (bitIndex 0) empty @?= False - member (bitIndex 100) empty @?= False - - , testCase "set adds a bit" $ do - let fv = set (bitIndex 0) empty - member (bitIndex 0) fv @?= True - - , testCase "set multiple bits" $ do - let fv = set (bitIndex 8) (set (bitIndex 0) empty) - member (bitIndex 0) fv @?= True - member (bitIndex 8) fv @?= True - member (bitIndex 1) fv @?= False - - , testCase "clear removes a bit" $ do - let fv = set (bitIndex 5) empty - fv' = clear (bitIndex 5) fv - member (bitIndex 5) fv @?= True - member (bitIndex 5) fv' @?= False - - , testCase "clear on unset bit is no-op" $ do - let fv = clear (bitIndex 10) empty - unFeatureVector fv @?= BS.empty - - , testCase "member returns False for unset bits" $ do - let fv = set (bitIndex 4) empty - member (bitIndex 0) fv @?= False - member (bitIndex 1) fv @?= False - member (bitIndex 5) fv @?= False - - , testCase "render strips leading zeros" $ do - let fv = set (bitIndex 0) empty - render fv @?= BS.pack [0x01] - - , testCase "render handles high bits correctly" $ do - let fv = set (bitIndex 15) empty - render fv @?= BS.pack [0x80, 0x00] - - , testCase "render of empty is empty" $ - render empty @?= BS.empty +table_row :: Feature -> (Int, String, Bool, String, [String]) +table_row f = + ( feature_bit f + , feature_name f + , feature_assumed f + , concatMap letter (feature_contexts f) + , fmap feature_name (feature_dependencies f) ) + where + letter c = case c of + InitContext -> "I" + NodeContext -> "N" + ChannelContext -> "C" + InvoiceContext -> "9" + BlindedContext -> "B" + ChannelTypeContext -> "T" + +table_tests :: TestTree +table_tests = testGroup "feature table" [ + testCase "matches the spec table" $ + fmap table_row known_features @?= spec_table + + , testCase "feature_by_bit maps both bits to the feature" $ + mapM_ (\f -> do + feature_by_bit (feature_bit f) @?= Just f + feature_by_bit (feature_bit f + 1) @?= Just f) + known_features + + , testCase "feature_by_bit rejects unassigned bits" $ do + let assigned = concatMap (\f -> [feature_bit f, feature_bit f + 1]) + known_features + free = filter (`notElem` assigned) [0 .. 600] + mapM_ (\i -> feature_by_bit i @?= Nothing) free + feature_by_bit (-1) @?= Nothing + feature_by_bit (-2) @?= Nothing ] --- Validation tests ------------------------------------------------------------ - -validationTests :: TestTree -validationTests = testGroup "Validation" [ - testCase "BothBitsSet error when both bits of a pair are set" $ do - let baseBit = 14 -- payment_secret - fv = setBit (baseBit + 1) (setBit baseBit empty) - case validateLocal Init fv of - Right () -> assertFailure "expected BothBitsSet error" - Left errs -> any isBothBitsSet errs @?= True - - , testCase "MissingDependency error when dep not set" $ do - -- basic_mpp (16) depends on payment_secret (14) - case featureByName "basic_mpp" of - Nothing -> assertFailure "basic_mpp not found" - Just f -> do - let fv = setBit (featureBaseBit f) empty - case validateLocal Init fv of - Right () -> assertFailure "expected MissingDependency error" - Left errs -> any isMissingDependency errs @?= True - - , testCase "ContextNotAllowed error for wrong context" $ do - -- option_payment_metadata (48) is only allowed in Invoice context - case featureByName "option_payment_metadata" of - Nothing -> assertFailure "option_payment_metadata not found" - Just f -> do - let fv = setBit (featureBaseBit f) empty - case validateLocal Init fv of - Right () -> assertFailure "expected ContextNotAllowed error" - Left errs -> any isContextNotAllowed errs @?= True - - , testCase "Remote validation accepts unknown optional bits" $ do - -- bit 201 is unknown and odd (optional) - let fv = setBit 201 empty - validateRemote Init fv @?= Right () - - , testCase "Remote validation rejects unknown required bits" $ do - -- bit 200 is unknown and even (required) - let fv = setBit 200 empty - case validateRemote Init fv of - Right () -> assertFailure "expected UnknownRequiredBit error" - Left errs -> any isUnknownRequiredBit errs @?= True - - , testCase "Valid local vector passes validation" $ do - -- payment_secret (14) with its required bit set - case featureByName "payment_secret" of - Nothing -> assertFailure "payment_secret not found" - Just f -> do - let fv = setBit (featureBaseBit f) empty - validateLocal Init fv @?= Right () - - , testCase "Valid feature with dependency passes" $ do - -- basic_mpp (16) with payment_secret (14) dependency set - case (featureByName "basic_mpp", featureByName "payment_secret") of - (Just mpp, Just ps) -> do - let fv = setBit (featureBaseBit mpp) - $ setBit (featureBaseBit ps) empty - validateLocal Init fv @?= Right () - _ -> assertFailure "features not found" - ] +-- feature vectors ----------------------------------------------------------- -isBothBitsSet :: ValidationError -> Bool -isBothBitsSet (BothBitsSet _ _) = True -isBothBitsSet _ = False - -isMissingDependency :: ValidationError -> Bool -isMissingDependency (MissingDependency _ _) = True -isMissingDependency _ = False - -isContextNotAllowed :: ValidationError -> Bool -isContextNotAllowed (ContextNotAllowed _ _) = True -isContextNotAllowed _ = False - -isUnknownRequiredBit :: ValidationError -> Bool -isUnknownRequiredBit (UnknownRequiredBit _) = True -isUnknownRequiredBit _ = False - --- KnownFeature tests ----------------------------------------------------------- - -knownFeatureTests :: TestTree -knownFeatureTests = testGroup "KnownFeature" [ - testCase "knownFeatureByBit finds known feature" $ - case knownFeatureByBit 16 of - Nothing -> assertFailure "expected basic_mpp" - Just kf -> - featureName (unKnownFeature kf) @?= "basic_mpp" - - , testCase "knownFeatureByBit works for odd bit" $ - case knownFeatureByBit 17 of - Nothing -> assertFailure "expected basic_mpp" - Just kf -> - featureName (unKnownFeature kf) @?= "basic_mpp" - - , testCase "knownFeatureByBit returns Nothing for unknown" $ - knownFeatureByBit 999 @?= Nothing - - , testCase "knownFeatureByName finds known feature" $ - case knownFeatureByName "payment_secret" of - Nothing -> assertFailure "expected payment_secret" - Just kf -> - featureBaseBit (unKnownFeature kf) @?= 14 - - , testCase "knownFeatureByName returns Nothing for unknown" $ - knownFeatureByName "nonexistent" @?= Nothing +-- | A vector of (n + 1) bytes whose most significant byte is b. +long_vector :: Int -> Word8 -> FeatureVector +long_vector n b = parse (BS.cons b (BS.replicate n 0)) + +vector_tests :: TestTree +vector_tests = testGroup "feature vectors" [ + testCase "empty" $ do + render empty @?= "" + set_bits empty @?= [] + + , testCase "render returns parsed bytes exactly" $ do + render (parse "\NUL\NUL\STX") @?= "\NUL\NUL\STX" + render (parse "\NUL") @?= "\NUL" + + , testCase "equality ignores leading zero bytes" $ do + parse "\NUL\STX" @?= parse "\STX" + parse "\NUL\NUL" @?= empty + assertBool "distinct" (parse "\STX" /= parse "\SOH") + assertBool "distinct" (parse "\SOH\NUL" /= parse "\SOH") + + , testCase "show" $ + show (parse "\NUL\STX") @?= "parse \"\\NUL\\STX\"" + + , testCase "bit numbering" $ do + render (set_bit 0 empty) @?= "\SOH" + render (set_bit 7 empty) @?= "\128" + render (set_bit 8 empty) @?= "\SOH\NUL" + render (set_bit 17 empty) @?= "\STX\NUL\NUL" + set_bits (parse "\128\NUL\SOH") @?= [0, 23] + + , testCase "operations encode minimally" $ do + render (set_bit 1 (parse "\NUL\NUL\SOH")) @?= "\ETX" + render (clear_bit 0 (parse "\NUL\SOH\SOH")) @?= "\SOH\NUL" + render (clear_bit 8 (parse "\SOH\SOH")) @?= "\SOH" + render (union (parse "\NUL\NUL") empty) @?= "" + + , testCase "union" $ do + union (parse "\SOH\NUL") (parse "\STX") @?= parse "\SOH\STX" + union (parse "\STX") empty @?= parse "\STX" + set_bits (union (parse "\SOH\SOH") (parse "\128\SOH")) @?= [0, 8, 15] + + , testCase "negative indices are ignored" $ do + set_bit (-1) (parse "\STX") @?= parse "\STX" + clear_bit (-1) (parse "\STX") @?= parse "\STX" + test_bit (-1) (parse "\255") @?= False + + , testCase "clearing a bit beyond the vector is a no-op" $ + render (clear_bit 1000 (parse "\STX")) @?= "\STX" + + , testCase "bit 65536 does not wrap" $ do + let fv = long_vector 8192 0x01 + set_bits fv @?= [65536] + test_bit 65536 fv @?= True + test_bit 0 fv @?= False + + , testCase "bits of a maximum-length vector" $ do + let fv = long_vector 65534 0x80 + BS.length (render fv) @?= 65535 + set_bits fv @?= [524279] + set_bit 524279 empty @?= fv + clear_bit 524279 fv @?= empty ] --- setFeatureWithDeps tests ----------------------------------------------------- - -setFeatureWithDepsTests :: TestTree -setFeatureWithDepsTests = testGroup "setFeatureWithDeps" [ - testCase "sets feature and its dependency" $ do - case featureByName "basic_mpp" of - Nothing -> assertFailure "basic_mpp not found" - Just mpp -> do - let fv = setFeatureWithDeps mpp Optional empty - isFeatureSet mpp fv @?= True - -- payment_secret should also be set - case featureByName "payment_secret" of - Nothing -> - assertFailure "payment_secret not found" - Just ps -> isFeatureSet ps fv @?= True - - , testCase "sets transitive dependencies" $ do - -- option_zeroconf depends on option_scid_alias - case featureByName "option_zeroconf" of - Nothing -> - assertFailure "option_zeroconf not found" - Just zc -> do - let fv = setFeatureWithDeps zc Required empty - isFeatureSet zc fv @?= True - case featureByName "option_scid_alias" of - Nothing -> - assertFailure "option_scid_alias not found" - Just sa -> isFeatureSet sa fv @?= True - - , testCase "feature without deps sets only itself" $ do - case featureByName "payment_secret" of - Nothing -> - assertFailure "payment_secret not found" - Just ps -> do - let fv = setFeatureWithDeps ps Optional empty - isFeatureSet ps fv @?= True - -- no other features should be set - let others = filter - (\(f, _) -> featureName f /= "payment_secret") - (listFeatures fv) - null others @?= True - - , testCase "passes validateLocal after setFeatureWithDeps" $ do - case featureByName "basic_mpp" of - Nothing -> assertFailure "basic_mpp not found" - Just mpp -> do - let fv = setFeatureWithDeps mpp Optional empty - validateLocal Init fv @?= Right () +-- feature operations -------------------------------------------------------- + +feature_tests :: TestTree +feature_tests = testGroup "feature operations" [ + testCase "set_feature sets the bit for its level" $ do + set_bits (set_feature BasicMpp Required empty) @?= [16] + set_bits (set_feature BasicMpp Optional empty) @?= [17] + + , testCase "set_feature clears the other bit" $ do + set_bits (set_feature PaymentSecret Optional (set_bit 14 empty)) + @?= [15] + set_bits (set_feature PaymentSecret Required (set_bit 15 empty)) + @?= [14] + + , testCase "set_feature leaves other features alone" $ + set_bits (set_feature BasicMpp Optional (parse "\STX\NUL")) + @?= [9, 17] + + , testCase "clear_feature clears both bits" $ do + clear_feature BasicMpp (set_bit 16 (set_bit 17 empty)) @?= empty + clear_feature BasicMpp (set_bit 9 (set_bit 17 empty)) + @?= set_bit 9 empty + + , testCase "test_feature" $ do + test_feature BasicMpp (set_bit 16 empty) @?= Just Required + test_feature BasicMpp (set_bit 17 empty) @?= Just Optional + test_feature BasicMpp (set_bit 16 (set_bit 17 empty)) + @?= Just Required + test_feature BasicMpp (set_bit 18 empty) @?= Nothing + + , testCase "list_features is in bit order and skips unknown bits" $ + list_features (set_bit 100 (set_bit 51 (set_bit 46 (set_bit 15 empty)))) + @?= [ (PaymentSecret, Optional), (OptionScidAlias, Required) + , (OptionZeroconf, Optional) ] + + , testCase "set_feature_with_deps sets dependencies" $ do + list_features (set_feature_with_deps OptionZeroconf Required empty) + @?= [(OptionScidAlias, Required), (OptionZeroconf, Required)] + list_features (set_feature_with_deps ZeroFeeCommitments Optional empty) + @?= [(ZeroFeeCommitments, Optional), (OptionChannelType, Optional)] + + , testCase "set_feature_with_deps keeps a set dependency's level" $ + list_features + (set_feature_with_deps OptionSimpleClose Optional + (set_feature OptionShutdownAnysegwit Required empty)) + @?= [ (OptionShutdownAnysegwit, Required) + , (OptionSimpleClose, Optional) ] ] --- setFeatureForContext tests --------------------------------------------------- - -setFeatureForContextTests :: TestTree -setFeatureForContextTests = testGroup "setFeatureForContext" [ - testCase "allows feature in valid context" $ do - case featureByName "option_payment_metadata" of - Nothing -> - assertFailure "option_payment_metadata not found" - Just pm -> - isRight - (setFeatureForContext Invoice pm Optional empty) - @?= True - - , testCase "rejects feature in wrong context" $ do - case featureByName "option_payment_metadata" of - Nothing -> - assertFailure "option_payment_metadata not found" - Just pm -> - case setFeatureForContext Init pm Optional empty of - Right _ -> assertFailure "expected error" - Left err -> isContextNotAllowed err @?= True - - , testCase "allows feature with empty context list" $ do - -- payment_secret has empty context list (all allowed) - case featureByName "payment_secret" of - Nothing -> - assertFailure "payment_secret not found" - Just ps -> - isRight - (setFeatureForContext Init ps Optional empty) - @?= True - - , testCase "rejects Required in ChanAnnOdd context" $ do - case featureByName "payment_secret" of - Nothing -> - assertFailure "payment_secret not found" - Just ps -> - case setFeatureForContext - ChanAnnOdd ps Required empty of - Right _ -> assertFailure "expected parity error" - Left err -> isInvalidParity err @?= True - - , testCase "allows Optional in ChanAnnOdd context" $ do - case featureByName "payment_secret" of - Nothing -> - assertFailure "payment_secret not found" - Just ps -> - isRight - (setFeatureForContext - ChanAnnOdd ps Optional empty) - @?= True - - , testCase "rejects Optional in ChanAnnEven context" $ do - case featureByName "payment_secret" of - Nothing -> - assertFailure "payment_secret not found" - Just ps -> - case setFeatureForContext - ChanAnnEven ps Optional empty of - Right _ -> assertFailure "expected parity error" - Left err -> isInvalidParity err @?= True - - , testCase "allows Required in ChanAnnEven context" $ do - case featureByName "payment_secret" of - Nothing -> - assertFailure "payment_secret not found" - Just ps -> - isRight - (setFeatureForContext - ChanAnnEven ps Required empty) - @?= True +-- local validation ---------------------------------------------------------- + +-- | A vector with the given bits set. +bits :: [Int] -> FeatureVector +bits = foldr set_bit empty + +contexts :: [Context] +contexts = + [ InitContext, NodeContext, ChannelContext, InvoiceContext + , BlindedContext, ChannelTypeContext ] + +local_tests :: TestTree +local_tests = testGroup "validate_local" [ + testCase "empty is valid in every context but channel_type" $ do + mapM_ (\c -> validate_local c empty @?= Right ()) + (filter (/= ChannelTypeContext) contexts) + validate_local ChannelTypeContext empty + @?= Left (InvalidChannelType :| []) + + , testCase "rejects unknown bits, odd or even" $ do + validate_local InitContext (bits [20, 101]) + @?= Left (UnknownBit 20 :| [UnknownBit 101]) + validate_local NodeContext (bits [200]) @?= Left (UnknownBit 200 :| []) + + , testCase "rejects both bits set" $ + validate_local InitContext (bits [46, 47]) + @?= Left (BothBitsSet OptionScidAlias :| []) + + , testCase "rejects features outside their contexts" $ do + validate_local InitContext (bits [49]) + @?= Left (ContextNotAllowed OptionPaymentMetadata InitContext :| []) + validate_local InvoiceContext (bits [23]) + @?= Left (ContextNotAllowed OptionAnchors InvoiceContext :| []) + validate_local ChannelContext (bits [17, 15]) + @?= Left (ContextNotAllowed PaymentSecret ChannelContext + :| [ContextNotAllowed BasicMpp ChannelContext]) + + , testCase "accepts features in their contexts" $ do + validate_local InvoiceContext (bits [49]) @?= Right () + validate_local NodeContext (bits [23, 47]) @?= Right () + validate_local InitContext (bits [63, 67, 39]) @?= Right () + + , testCase "empty-context features: accepted in I, N and 9 only" $ do + let fv = bits [1, 7, 9, 13, 15, 45] + mapM_ (\c -> validate_local c fv @?= Right ()) + [InitContext, NodeContext, InvoiceContext] + validate_local ChannelContext (bits [9]) + @?= Left (ContextNotAllowed VarOnionOptin ChannelContext :| []) + validate_local BlindedContext (bits [15]) + @?= Left (ContextNotAllowed PaymentSecret BlindedContext :| []) + + , testCase "allowed_features must be empty" $ + validate_local BlindedContext (bits [17]) + @?= Left (ContextNotAllowed BasicMpp BlindedContext :| []) + + , testCase "rejects missing dependencies" $ do + validate_local InitContext (bits [51]) + @?= Left (MissingDependency OptionZeroconf OptionScidAlias :| []) + validate_local InitContext (bits [61]) + @?= Left (MissingDependency OptionSimpleClose + OptionShutdownAnysegwit :| []) + validate_local NodeContext (bits [67]) + @?= Left (MissingDependency OptionOnionMessagesOnlyChannels + OptionOnionMessages :| []) + + , testCase "dependencies on ASSUMED features count as met" $ do + validate_local InitContext (bits [17]) @?= Right () + validate_local InvoiceContext (bits [16]) @?= Right () + validate_local InitContext (bits [41]) @?= Right () + + , testCase "reports errors in rule order" $ + validate_local InitContext (bits [20, 22, 23, 48, 61]) + @?= Left (UnknownBit 20 :| + [ BothBitsSet OptionAnchors + , ContextNotAllowed OptionPaymentMetadata InitContext + , MissingDependency OptionSimpleClose + OptionShutdownAnysegwit ]) + + , testCase "channel_type follows BOLT #2, not the generic rules" $ do + validate_local ChannelTypeContext (bits [12, 50]) @?= Right () + validate_local ChannelTypeContext (bits [12, 23]) + @?= Left (InvalidChannelType :| []) ] --- validateNoBothBits tests ----------------------------------------------------- - -validateNoBothBitsTests :: TestTree -validateNoBothBitsTests = testGroup "validateNoBothBits" [ - testCase "passes for empty vector" $ - isRight (validateNoBothBits empty) @?= True - - , testCase "passes when only one bit of pair is set" $ do - case featureByName "payment_secret" of - Nothing -> - assertFailure "payment_secret not found" - Just ps -> do - let fv = setFeature ps Optional empty - isRight (validateNoBothBits fv) @?= True - - , testCase "fails when both bits of pair are set" $ do - let fv = setBit 15 (setBit 14 empty) - case validateNoBothBits fv of - Right _ -> assertFailure "expected BothBitsSet" - Left err -> isBothBitsSet err @?= True - - , testCase "returns first error found" $ do - -- set both bits for two features - let fv = setBit 15 (setBit 14 - (setBit 17 (setBit 16 empty))) - isLeft (validateNoBothBits fv) @?= True +-- remote validation --------------------------------------------------------- + +remote_tests :: TestTree +remote_tests = testGroup "validate_remote" [ + testCase "init: unknown odd bits are ignored" $ + validate_remote InitContext (bits [101, 201]) @?= Right () + + , testCase "init: unknown even bits are rejected" $ + validate_remote InitContext (bits [20, 100, 101]) + @?= Left (UnknownBit 20 :| [UnknownBit 100]) + + , testCase "init: missing dependencies are rejected" $ do + validate_remote InitContext (bits [51]) + @?= Left (MissingDependency OptionZeroconf OptionScidAlias :| []) + validate_remote InitContext (bits [60]) + @?= Left (MissingDependency OptionSimpleClose + OptionShutdownAnysegwit :| []) + validate_remote InitContext (bits [51, 47]) @?= Right () + + , testCase "init: ASSUMED dependencies count as met" $ do + validate_remote InitContext (bits [17]) @?= Right () + validate_remote InitContext (bits [40]) @?= Right () + + , testCase "init: both bits set is not an error" $ + validate_remote InitContext (bits [46, 47]) @?= Right () + + , testCase "init: known features outside their contexts pass" $ + validate_remote InitContext (bits [49]) @?= Right () + + , testCase "node and channel announcements: unknown even bits" $ do + validate_remote NodeContext (bits [100]) + @?= Left (UnknownBit 100 :| []) + validate_remote ChannelContext (bits [100]) + @?= Left (UnknownBit 100 :| []) + validate_remote NodeContext (bits [101]) @?= Right () + validate_remote ChannelContext (bits [101]) @?= Right () + + , testCase "node and channel announcements: no dependency rule" $ do + validate_remote NodeContext (bits [51]) @?= Right () + validate_remote ChannelContext (bits [51]) @?= Right () + + , testCase "invoice: unknown even bits and dependencies" $ do + validate_remote InvoiceContext (bits [100]) + @?= Left (UnknownBit 100 :| []) + validate_remote InvoiceContext (bits [51]) + @?= Left (MissingDependency OptionZeroconf OptionScidAlias :| []) + validate_remote InvoiceContext (bits [101, 17]) @?= Right () + + , testCase "blinded path: any unknown bit is rejected" $ do + validate_remote BlindedContext (bits [101]) + @?= Left (UnknownBit 101 :| []) + validate_remote BlindedContext (bits [100]) + @?= Left (UnknownBit 100 :| []) + validate_remote BlindedContext (bits [15, 17]) @?= Right () + validate_remote BlindedContext empty @?= Right () + + , testCase "channel_type: must be a defined type" $ do + validate_remote ChannelTypeContext (bits [12, 22]) @?= Right () + validate_remote ChannelTypeContext (bits [22]) + @?= Left (InvalidChannelType :| []) + + , testCase "bits above 65535 do not wrap" $ do + validate_remote InitContext (long_vector 8192 0x01) + @?= Left (UnknownBit 65536 :| []) + validate_remote InitContext (long_vector 8192 0x04) + @?= Left (UnknownBit 65538 :| []) + validate_remote InitContext (long_vector 8194 0x01) + @?= Left (UnknownBit 65552 :| []) + + , testCase "maximum-length vectors" $ do + validate_remote InitContext (long_vector 65534 0x80) @?= Right () + validate_remote InitContext (long_vector 65534 0x40) + @?= Left (UnknownBit 524278 :| []) ] --- Remote dependency tests ----------------------------------------------------- - -remoteDependencyTests :: TestTree -remoteDependencyTests = testGroup "validateRemote dependencies" [ - testCase "rejects option_zeroconf without option_scid_alias" $ - validateRemote Init (setBit 51 empty) @?= - Left [MissingDependency "option_zeroconf" "option_scid_alias"] - - , testCase "rejects basic_mpp without payment_secret (init)" $ - validateRemote Init (setBit 17 empty) @?= - Left [MissingDependency "basic_mpp" "payment_secret"] - - , testCase "rejects basic_mpp without payment_secret (invoice)" $ - validateRemote Invoice (setBit 17 empty) @?= - Left [MissingDependency "basic_mpp" "payment_secret"] - - , testCase "rejects option_simple_close without anysegwit" $ - validateRemote Init (setBit 60 empty) @?= - Left [ MissingDependency - "option_simple_close" "option_shutdown_anysegwit" ] - - , testCase "accepts features with dependencies set" $ do - validateRemote Init (setBit 51 (setBit 47 empty)) @?= Right () - validateRemote Invoice (setBit 17 (setBit 14 empty)) @?= Right () +-- BOLT #11 vectors ---------------------------------------------------------- + +-- | The feature vector carried by a BOLT #11 @9@ field, given the +-- field's data as bech32 characters. +invoice_features :: String -> Maybe FeatureVector +invoice_features cs = do + vs <- traverse (`elemIndex` charset) cs + let n = length vs + pure $ bits + [ (n - 1 - k) * 5 + j + | (k, v) <- zip [0 ..] vs, j <- [0 .. 4], B.testBit v j ] + where + charset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" + +invoice_case + :: String -> String -> [Int] + -> Either (NonEmpty ValidationError) () -> TestTree +invoice_case name dat expected result = testCase name $ + case invoice_features dat of + Nothing -> assertFailure "invalid bech32 data" + Just fv -> do + set_bits fv @?= expected + validate_remote InvoiceContext fv @?= result + +invoice_tests :: TestTree +invoice_tests = testGroup "BOLT #11 invoice features" [ + invoice_case "features 8, 14 and 99" "sqqqqqqqqqqqqqqqqsgq" + [8, 14, 99] (Right ()) + , invoice_case "payment metadata (8, 14, 48)" "gqqqqqqsgq" + [8, 14, 48] (Right ()) + , invoice_case "high-S signature example (8, 14)" "sgq" + [8, 14] (Right ()) + , invoice_case "invalid unknown feature 100" "psqqqqqqqqqqqqqqqqsgq" + [8, 14, 99, 100] (Left (UnknownBit 100 :| [])) + , testCase "a writer may set 8, 14 and 48" $ + validate_local InvoiceContext (bits [8, 14, 48]) @?= Right () ] --- Long vector tests ----------------------------------------------------------- - --- | A vector of (n + 1) bytes whose most significant byte is b. -longVector :: Int -> Word8 -> FeatureVector -longVector n b = parse (BS.cons b (BS.replicate n 0)) - -longVectorTests :: TestTree -longVectorTests = testGroup "Long vectors" [ - testCase "bit 65536 is reported without wrapping" $ do - let fv = longVector 8192 0x01 - setBits fv @?= [65536] - highestSetBit fv @?= Just 65536 - validateRemote Init fv @?= Left [UnknownRequiredBit 65536] - - , testCase "bit 65538 is reported as itself" $ - validateRemote Init (longVector 8192 0x04) @?= - Left [UnknownRequiredBit 65538] - - , testCase "bit 65552 does not alias basic_mpp" $ - validateRemote Init (longVector 8194 0x01) @?= - Left [UnknownRequiredBit 65552] - - , testCase "max-length vector, top odd bit accepted" $ do - let fv = longVector 65534 0x80 - setBits fv @?= [524279] - highestSetBit fv @?= Just 524279 - validateRemote Init fv @?= Right () - - , testCase "max-length vector, top even bit rejected" $ - validateRemote Init (longVector 65534 0x40) @?= - Left [UnknownRequiredBit 524278] +-- channel types ------------------------------------------------------------- + +all_channel_types :: [ChannelType] +all_channel_types = + [ ChannelType b s z + | b <- [BasicStaticRemotekey, BasicAnchors, BasicZeroFeeCommitments] + , s <- [False, True] + , z <- [False, True] ] + +channel_type_tests :: TestTree +channel_type_tests = testGroup "channel types" [ + testCase "every defined type round-trips" $ + mapM_ (\ct -> channel_type (channel_type_features ct) @?= Just ct) + all_channel_types + + , testCase "encodings" $ do + let enc b s z = render (channel_type_features (ChannelType b s z)) + enc BasicStaticRemotekey False False @?= BS.pack [0x10, 0x00] + enc BasicAnchors False False @?= BS.pack [0x40, 0x10, 0x00] + enc BasicZeroFeeCommitments False False + @?= BS.pack [0x01, 0, 0, 0, 0, 0] + enc BasicAnchors True True + @?= BS.pack [0x04, 0x40, 0, 0, 0x40, 0x10, 0x00] + + , testCase "exactly the twelve defined types decode" $ do + let candidates = [12, 13, 14, 22, 23, 40, 46, 47, 50, 51, 52] + decoded = [ ct | s <- subsequences candidates + , Just ct <- [channel_type (bits s)] ] + length decoded @?= 12 + mapM_ (\ct -> assertBool (show ct) (ct `elem` all_channel_types)) + decoded + + , testCase "rejects undefined types" $ + mapM_ (\s -> channel_type (bits s) @?= Nothing) + [ [], [13], [22], [23, 13], [12, 40], [12, 14], [46], [50] + , [12, 47], [12, 51], [12, 52], [40, 44] ] + + , testCase "variations need no dependency closure" $ + channel_type (bits [12, 50]) + @?= Just (ChannelType BasicStaticRemotekey False True) + + , testCase "leading zero bytes are ignored" $ + channel_type (parse "\NUL\NUL\DLE\NUL") + @?= Just (ChannelType BasicStaticRemotekey False False) ] --- Opposite bit tests ---------------------------------------------------------- - --- | Look up a known feature by name, failing the test if absent. -knownFeature :: String -> IO Feature -knownFeature n = case featureByName n of - Nothing -> assertFailure ("feature not found: " ++ n) - Just f -> pure f - -oppositeBitTests :: TestTree -oppositeBitTests = testGroup "Opposite bit" [ - testCase "setFeature Optional clears the required bit" $ do - ps <- knownFeature "payment_secret" - let fv = setFeature ps Optional (setFeature ps Required empty) - setBits fv @?= [15] - hasFeature ps fv @?= Just Optional - - , testCase "setFeature Required clears the optional bit" $ do - ps <- knownFeature "payment_secret" - let fv = setFeature ps Required (setBit 15 empty) - setBits fv @?= [14] - hasFeature ps fv @?= Just Required - - , testCase "setFeatureWithDeps keeps a set dependency's level" $ do - ps <- knownFeature "payment_secret" - mpp <- knownFeature "basic_mpp" - let fv = setFeatureWithDeps mpp Optional - (setFeature ps Required empty) - hasFeature ps fv @?= Just Required - hasFeature mpp fv @?= Just Optional - validateLocal Init fv @?= Right () - - , testCase "setFeatureForContext clears the opposite bit" $ do - anc <- knownFeature "option_anchors" - let start = setFeature anc Required empty - case setFeatureForContext Init anc Optional start of - Left e -> assertFailure (show e) - Right fv -> do - hasFeature anc fv @?= Just Optional - validateNoBothBits fv @?= Right fv - - , testProperty "setFeature never sets both bits (any start)" $ - forAll genFeatureAndLevel $ \(f, lvl) bs -> - let fv = setFeature f lvl (parse (BS.pack bs)) - b = featureBaseBit f - in not (testBit b fv && testBit (b + 1) fv) - && hasFeature f fv == Just lvl - - , testProperty "setFeature leaves other bits alone" $ - forAll genFeatureAndLevel $ \(f, lvl) bs -> - let fv0 = parse (BS.pack bs) - fv = setFeature f lvl fv0 - b = fromIntegral (featureBaseBit f) +-- properties ---------------------------------------------------------------- + +gen_bytes :: Gen ByteString +gen_bytes = BS.pack <$> arbitrary + +gen_vector :: Gen FeatureVector +gen_vector = parse <$> gen_bytes + +gen_feature :: Gen Feature +gen_feature = elements known_features + +gen_level :: Gen FeatureLevel +gen_level = elements [Required, Optional] + +-- | A bit index somewhat beyond the end of a vector. +gen_index :: FeatureVector -> Gen Int +gen_index fv = choose (0, 8 * BS.length (render fv) + 16) + +has_leading_zero :: FeatureVector -> Bool +has_leading_zero fv = BS.take 1 (render fv) == BS.singleton 0 + +properties :: TestTree +properties = testGroup "properties" [ + testProperty "render . parse == id" $ + forAll gen_bytes $ \bs -> render (parse bs) === bs + + , testProperty "equality ignores leading zero bytes" $ + forAll gen_bytes $ \bs (Small k) -> + parse (BS.replicate k 0 <> bs) === parse bs + + , testProperty "equality agrees with set_bits" $ + forAll gen_vector $ \a -> forAll gen_vector $ \b -> + (a == b) === (set_bits a == set_bits b) + + , testProperty "set_bits lists exactly the set bits, ascending" $ + forAll gen_vector $ \fv -> + set_bits fv + === filter (`test_bit` fv) [0 .. 8 * BS.length (render fv) - 1] + + , testProperty "set_bit sets only its bit" $ + forAll gen_vector $ \fv -> forAll (gen_index fv) $ \i -> + let fv' = set_bit i fv + in test_bit i fv' + .&&. filter (/= i) (set_bits fv') + === filter (/= i) (set_bits fv) + + , testProperty "clear_bit clears only its bit" $ + forAll gen_vector $ \fv -> forAll (gen_index fv) $ \i -> + let fv' = clear_bit i fv + in not (test_bit i fv') + .&&. set_bits fv' === filter (/= i) (set_bits fv) + + , testProperty "union is bitwise OR" $ + forAll gen_vector $ \a -> forAll gen_vector $ \b -> + let u = union a b + n = 8 * max (BS.length (render a)) (BS.length (render b)) + in fmap (`test_bit` u) [0 .. n - 1] + === fmap (\i -> test_bit i a || test_bit i b) [0 .. n - 1] + + , testProperty "operations encode minimally" $ + forAll gen_vector $ \a -> forAll gen_vector $ \b -> + forAll (gen_index a) $ \i -> + not (any has_leading_zero + [union a b, set_bit i a, clear_bit i a]) + + , testProperty "set_feature sets exactly one bit of the pair" $ + forAll gen_vector $ \fv -> forAll gen_feature $ \f -> + forAll gen_level $ \l -> + let fv' = set_feature f l fv + b = feature_bit f other = filter (\i -> i /= b && i /= b + 1) - in other (setBits fv) === other (setBits fv0) - ] - -isInvalidParity :: ValidationError -> Bool -isInvalidParity (InvalidParity _ _) = True -isInvalidParity _ = False - --- Property tests -------------------------------------------------------------- - -propertyTests :: TestTree -propertyTests = testGroup "Properties" [ - testProperty "render . parse == id for stripped ByteStrings" $ - \bs -> let stripped = BS.dropWhile (== 0) (BS.pack bs) - in render (parse stripped) === stripped - - , testProperty "set then member returns True" $ - \(Small n) -> let idx = bitIndex (n `mod` 256) - fv = set idx empty - in member idx fv === True - - , testProperty "clear then member returns False" $ - \(Small n) -> let idx = bitIndex (n `mod` 256) - fv = set idx empty - fv' = clear idx fv - in member idx fv' === False - - , testProperty "setBits returns all set bits without duplicates" $ - \bs -> let fv = parse (BS.pack bs) - bits = setBits fv - -- Verify no duplicates and length matches unique count - in length bits === length (removeDups bits) - - , testProperty "double set is idempotent" $ - \(Small n) -> let idx = bitIndex (n `mod` 256) - fv = set idx empty - fv' = set idx fv - in unFeatureVector fv === unFeatureVector fv' - - , testProperty "set then clear restores original (when was unset)" $ - \(Small n) -> let idx = bitIndex (n `mod` 256) - fv = set idx empty - fv' = clear idx fv - in unFeatureVector fv' === BS.empty - - , testProperty "member consistent with setBits" $ - \bs -> let fv = parse (BS.pack bs) - bits = setBits fv - in all (\b -> member (bitIndex (fromIntegral b)) fv) bits - === True - - , testProperty "requiredBit only succeeds for even" $ - \(n :: Word16) -> isJust (requiredBit n) === (n `mod` 2 == 0) - - , testProperty "optionalBit only succeeds for odd" $ - \(n :: Word16) -> isJust (optionalBit n) === (n `mod` 2 == 1) - - , testProperty "setFeature then hasFeature" $ - forAll genFeatureAndLevel $ \(f, lvl) -> - let fv = setFeature f lvl empty - in hasFeature f fv == Just lvl - - , testProperty - "setFeatureWithDeps passes validateLocal" $ - forAll genInitFeatureAndLevel $ \(f, lvl) -> - let fv = setFeatureWithDeps f lvl empty - in isRight (validateLocal Init fv) - - , testProperty "parse . render is identity" $ - \bs -> - let stripped = BS.dropWhile (== 0) (BS.pack bs) - fv = parse stripped - in parse (render fv) == fv - - , testProperty - "setFeature never sets both bits" $ - forAll genFeatureAndLevel $ \(f, lvl) -> - let fv = setFeature f lvl empty - in isRight (validateNoBothBits fv) + in test_feature f fv' === Just l + .&&. test_bit b fv' =/= test_bit (b + 1) fv' + .&&. other (set_bits fv') === other (set_bits fv) + + , testProperty "vectors built with set_feature_with_deps validate" $ + forAll (elements (filter (/= ChannelTypeContext) contexts)) $ \c -> + forAll (gen_presented c) $ \fs -> + let fv = foldr (\(f, l) -> set_feature_with_deps f l) empty fs + in validate_local c fv === Right () + .&&. validate_remote c fv === Right () ] --- | Generate a random known feature and level. -genFeatureAndLevel :: Gen (Feature, FeatureLevel) -genFeatureAndLevel = do - f <- elements knownFeatures - lvl <- elements [Required, Optional] - pure (f, lvl) - --- | Generate a feature valid in Init context with a level. -genInitFeatureAndLevel :: Gen (Feature, FeatureLevel) -genInitFeatureAndLevel = do - f <- elements initFeatures - lvl <- elements [Required, Optional] - pure (f, lvl) +-- | Features presented in a context, each with a level. +gen_presented :: Context -> Gen [(Feature, FeatureLevel)] +gen_presented c = do + fs <- sublistOf (filter presented known_features) + traverse (\f -> (,) f <$> gen_level) fs where - initFeatures = - filter validForInit knownFeatures - validForInit f = - null (featureContexts f) - || Init `elem` featureContexts f - --- | Remove duplicates from a list. -removeDups :: Eq a => [a] -> [a] -removeDups [] = [] -removeDups (x:xs) = x : removeDups (filter (/= x) xs) + presented f = case feature_contexts f of + [] -> c `elem` [InitContext, NodeContext, InvoiceContext] + cs -> c `elem` cs