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 e09ea49868bd9ca761e61d427d68c8db401421d8
parent 28f6b8e310fe4b5afa76c247b0e6cb10e983c562
Author: Jared Tobin <jared@jtobin.io>
Date:   Mon, 20 Apr 2026 14:55:57 +0800

test: cover KnownFeature, setFeatureWithDeps,
setFeatureForContext, validateNoBothBits

20 new test cases covering:
- KnownFeature construction and lookup (5)
- setFeatureWithDeps with transitive deps (4)
- setFeatureForContext context and parity checks (7)
- validateNoBothBits both-bits detection (4)

Diffstat:
Mtest/Main.hs | 189+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 189 insertions(+), 0 deletions(-)

diff --git a/test/Main.hs b/test/Main.hs @@ -4,6 +4,7 @@ module Main where import qualified Data.ByteString as BS +import Data.Either (isLeft, isRight) import Data.Maybe (isJust, isNothing) import Data.Word (Word16) import Lightning.Protocol.BOLT9 @@ -20,6 +21,10 @@ tests = testGroup "ppad-bolt9" [ , bitParityTests , featureVectorTests , validationTests + , knownFeatureTests + , setFeatureWithDepsTests + , setFeatureForContextTests + , validateNoBothBitsTests , propertyTests ] @@ -232,6 +237,190 @@ 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 + ] + +-- 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 () + ] + +-- 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 + ] + +-- 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 + ] + +isInvalidParity :: ValidationError -> Bool +isInvalidParity (InvalidParity _ _) = True +isInvalidParity _ = False + -- Property tests -------------------------------------------------------------- propertyTests :: TestTree