packages feed

bitcoin-scripting 0.2.0 → 0.3.0

raw patch · 10 files changed

+784/−55 lines, 10 filesdep +bytesdep +unordered-containersdep +vectordep ~basedep ~haskoin-coredep ~textPVP ok

version bump matches the API change (PVP)

Dependencies added: bytes, unordered-containers, vector

Dependency ranges changed: base, haskoin-core, text, transformers

API changes (from Hackage documentation)

+ Language.Bitcoin.Script.Descriptors: Absent :: ChecksumStatus
+ Language.Bitcoin.Script.Descriptors: ChecksumDescriptor :: OutputDescriptor -> ChecksumStatus -> Text -> ChecksumDescriptor
+ Language.Bitcoin.Script.Descriptors: CompileError :: ScriptDescriptor -> PsbtInputError
+ Language.Bitcoin.Script.Descriptors: Invalid :: Text -> ChecksumStatus
+ Language.Bitcoin.Script.Descriptors: InvalidOutput :: OutputDescriptor -> PsbtInputError
+ Language.Bitcoin.Script.Descriptors: KeyNotAvailable :: KeyDescriptor -> PsbtInputError
+ Language.Bitcoin.Script.Descriptors: OutputIndexOOB :: Tx -> Int -> PsbtInputError
+ Language.Bitcoin.Script.Descriptors: TransactionScripts :: Script -> Maybe Script -> Maybe Script -> TransactionScripts
+ Language.Bitcoin.Script.Descriptors: Valid :: ChecksumStatus
+ Language.Bitcoin.Script.Descriptors: [checksumStatus] :: ChecksumDescriptor -> ChecksumStatus
+ Language.Bitcoin.Script.Descriptors: [descriptor] :: ChecksumDescriptor -> OutputDescriptor
+ Language.Bitcoin.Script.Descriptors: [expectedChecksum] :: ChecksumDescriptor -> Text
+ Language.Bitcoin.Script.Descriptors: [txRedeemScript] :: TransactionScripts -> Maybe Script
+ Language.Bitcoin.Script.Descriptors: [txScriptPubKey] :: TransactionScripts -> Script
+ Language.Bitcoin.Script.Descriptors: [txWitnessScript] :: TransactionScripts -> Maybe Script
+ Language.Bitcoin.Script.Descriptors: data ChecksumDescriptor
+ Language.Bitcoin.Script.Descriptors: data ChecksumStatus
+ Language.Bitcoin.Script.Descriptors: data PsbtInputError
+ Language.Bitcoin.Script.Descriptors: data TransactionScripts
+ Language.Bitcoin.Script.Descriptors: descriptorChecksum :: Text -> Maybe Text
+ Language.Bitcoin.Script.Descriptors: descriptorToTextWithChecksum :: Network -> OutputDescriptor -> Text
+ Language.Bitcoin.Script.Descriptors: keyDescriptorAtIndex :: KeyIndex -> KeyDescriptor -> KeyDescriptor
+ Language.Bitcoin.Script.Descriptors: outputDescriptorAtIndex :: KeyIndex -> OutputDescriptor -> OutputDescriptor
+ Language.Bitcoin.Script.Descriptors: outputDescriptorPubKeys :: OutputDescriptor -> [PubKeyI]
+ Language.Bitcoin.Script.Descriptors: outputDescriptorScripts :: OutputDescriptor -> Maybe TransactionScripts
+ Language.Bitcoin.Script.Descriptors: scriptDescriptorAtIndex :: KeyIndex -> ScriptDescriptor -> ScriptDescriptor
+ Language.Bitcoin.Script.Descriptors: scriptDescriptorPubKeys :: ScriptDescriptor -> [PubKeyI]
+ Language.Bitcoin.Script.Descriptors: toPsbtInput :: Tx -> Int -> OutputDescriptor -> Either PsbtInputError Input
+ Language.Bitcoin.Script.Descriptors: validDescriptorChecksum :: Text -> Text -> Bool
- Language.Bitcoin.Script.Descriptors: outputDescriptorParser :: Network -> Parser OutputDescriptor
+ Language.Bitcoin.Script.Descriptors: outputDescriptorParser :: Network -> Parser ChecksumDescriptor
- Language.Bitcoin.Script.Descriptors: parseDescriptor :: Network -> Text -> Either String OutputDescriptor
+ Language.Bitcoin.Script.Descriptors: parseDescriptor :: Network -> Text -> Either String ChecksumDescriptor

Files

CHANGELOG.md view
@@ -1,5 +1,15 @@ # Revision history for bitcoin-scripting  +## 0.3.0++* Adds ability to parse, compute, and verify checksums++## 0.2.1++* Adds functions to specialize key family descriptors in output (and script) descriptors to a particular index+* Adds function for building a PSBT input+* Adds function for calculating various script fragments associated with a descriptor+ ## 0.2.0  * Refactors script descriptors module in order to make invalid descriptors unrepresentable.
bitcoin-scripting.cabal view
@@ -1,6 +1,6 @@ cabal-version:       2.4 name:                bitcoin-scripting-version:             0.2.0+version:             0.3.0 synopsis:            Resources for working with miniscript, and script descriptors homepage:            https://github.com/bitnomial/bitcoin-scripting copyright:           2020 Bitnomial, Inc.@@ -28,11 +28,12 @@     -Wno-unused-do-bind     -funbox-strict-fields   build-depends:-      base >=4.12 && <4.16+      base >=4.12 && <5     , bytestring >=0.10 && <0.12     , cereal ^>=0.5-    , haskoin-core >=0.18 && <0.21-    , text >=1.2 && <1.3+    , haskoin-core ^>=0.21+    , text >=1.2 && <2.1+    , unordered-containers ^>=0.2  library   import:          base@@ -55,13 +56,15 @@     Language.Bitcoin.Script.Descriptors.Syntax     Language.Bitcoin.Script.Descriptors.Text     Language.Bitcoin.Script.Descriptors.Utils+    Language.Bitcoin.Script.Descriptors.Checksum      Language.Bitcoin.Utils    build-depends:       attoparsec >=0.13 && <0.15     , containers ^>=0.6-    , transformers ^>=0.5+    , transformers >=0.5 && <0.7+    , vector >=0.12.1.2 && <0.14  test-suite bitcoin-scripting-tests   import:          base@@ -82,6 +85,7 @@    build-depends:       bitcoin-scripting-    , tasty                    >=1.0   && <1.5-    , tasty-hunit              >=0.9   && <0.11-    , tasty-quickcheck         >=0.8.1 && <0.11+    , bytes ^>=0.17+    , tasty >=1.0 && <1.5+    , tasty-hunit >=0.9 && <0.11+    , tasty-quickcheck >=0.8.1 && <0.11
src/Language/Bitcoin/Script/Descriptors.hs view
@@ -4,7 +4,9 @@ module Language.Bitcoin.Script.Descriptors (     -- * Descriptors     OutputDescriptor (..),+    outputDescriptorAtIndex,     ScriptDescriptor (..),+    scriptDescriptorAtIndex,      -- * Keys     KeyDescriptor (..),@@ -13,16 +15,22 @@     KeyCollection (..),     isDefinite,     keyAtIndex,+    keyDescriptorAtIndex,     keyDescPubKey,     pubKey,     secKey,     keyBytes,+    outputDescriptorPubKeys,+    scriptDescriptorPubKeys,      -- * Text representation     descriptorToText,+    descriptorToTextWithChecksum,     keyDescriptorToText,      -- * Parsing+    ChecksumDescriptor (..),+    ChecksumStatus (..),     parseDescriptor,     outputDescriptorParser,     parseKeyDescriptor,@@ -31,9 +39,20 @@     -- * Conversions     descriptorAddresses,     compile,+    TransactionScripts (..),+    outputDescriptorScripts,++    -- * PSBT+    toPsbtInput,+    PsbtInputError (..),++    -- * Checksums+    descriptorChecksum,+    validDescriptorChecksum, ) where  import Language.Bitcoin.Script.Descriptors.Parser import Language.Bitcoin.Script.Descriptors.Syntax import Language.Bitcoin.Script.Descriptors.Text import Language.Bitcoin.Script.Descriptors.Utils+import Language.Bitcoin.Script.Descriptors.Checksum
+ src/Language/Bitcoin/Script/Descriptors/Checksum.hs view
@@ -0,0 +1,100 @@+{-# LANGUAGE ParallelListComp #-}++module Language.Bitcoin.Script.Descriptors.Checksum (+  validDescriptorChecksum,+  descriptorChecksum,+) where++import Data.Bifunctor (first)+import Data.Bits (Bits (shiftL, shiftR, testBit, xor, (.&.)))+import Data.Char (ord)+import Data.Foldable (foldl')+import Data.IntMap (IntMap)+import qualified Data.IntMap.Strict as IntMap+import Data.Maybe (fromMaybe)+import Data.Text (Text)+import qualified Data.Text as Text+import Data.Vector.Unboxed (Vector)+import qualified Data.Vector.Unboxed as Vector++{- | Test whether the textual representation of an output descriptor has the+ given checksum.+-}+validDescriptorChecksum :: Text -> Text -> Bool+validDescriptorChecksum desc checksum =+  case mapM (charsetFind checksumCharset) (Text.unpack checksum) of+    Nothing -> False+    Just checkSymbols ->+      1 == polymodChecksum (expandChecksum desc <> checkSymbols)++{- | Compute the checksum of the textual representation of an output descriptor+ if possible.+-}+descriptorChecksum :: Text -> Maybe Text+descriptorChecksum desc = Text.pack <$> sequenceA checksumChars+ where+  checksumChars = [checksumCharset `charsetGet` charsetIndex i | i <- [0 .. 7]]+  charsetIndex i = (checksum `shiftR` (5 * (7 - i))) .&. 31+  symbols = expandChecksum desc <> replicate 8 0+  checksum = 1 `xor` polymodChecksum symbols++expandChecksum :: Text -> [Word]+expandChecksum =+  reverse . end+    . foldl'+      ( \(gs, s) v -> case (v `shiftR` 5 : gs, v .&. 31 : s) of+          ([g2, g1, g0], s') -> ([], 9 * g0 + 3 * g1 + g2 : s')+          x -> x+      )+      mempty+    . fromMaybe []+    . mapM (charsetFind inputCharset)+    . Text.unpack+ where+  end ([g0], s) = g0 : s+  end ([g1, g0], s) = 3 * g0 + g1 : s+  end (_, s) = s++polymodChecksum :: [Word] -> Word+polymodChecksum =+  foldl'+    ( \chk value ->+        foldl'+          xor+          ((chk .&. 0x7ffffffff) `shiftL` 5 `xor` value)+          [if chk `testBit` i then g else 0 | i <- [35 ..] | g <- generator]+    )+    1+ where+  generator =+    [ 0xf5dee51989+    , 0xa9fdca3312+    , 0x1bab10e32d+    , 0x3706b1677a+    , 0x644d626ffd+    ]++data Charset = Charset+  { charToIndex :: IntMap Word+  , indexToChar :: Vector Char+  }++charsetFromString :: String -> Charset+charsetFromString s =+  let xs = [(c, i) | c <- s | i <- [0 ..]]+   in Charset+        { charToIndex = IntMap.fromList $ first ord <$> xs+        , indexToChar = Vector.fromList $ fst <$> xs+        }++charsetFind :: Charset -> Char -> Maybe Word+charsetFind set c = IntMap.lookup (ord c) $ charToIndex set++charsetGet :: Charset -> Word -> Maybe Char+charsetGet set i = indexToChar set Vector.!? fromInteger (toInteger i)++inputCharset :: Charset+inputCharset = charsetFromString "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ "++checksumCharset :: Charset+checksumCharset = charsetFromString "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
src/Language/Bitcoin/Script/Descriptors/Parser.hs view
@@ -1,6 +1,8 @@ {-# LANGUAGE OverloadedStrings #-}  module Language.Bitcoin.Script.Descriptors.Parser (+    ChecksumDescriptor (..),+    ChecksumStatus (..),     parseDescriptor,     outputDescriptorParser,     parseKeyDescriptor,@@ -8,23 +10,29 @@ ) where  import Control.Applicative (optional, (<|>))-import Data.Attoparsec.Text (Parser)+import Data.Attoparsec.Text (Parser, char, count, match) import qualified Data.Attoparsec.Text as A import Data.Bool (bool) import qualified Data.ByteString as BS-import Data.Maybe (isJust)+import Data.Maybe (fromMaybe, isJust) import Data.Text (Text, pack)-import Haskoin.Address (textToAddr)-import Haskoin.Constants (Network)-import Haskoin.Keys (+import Haskoin (     DerivPath,     DerivPathI (..),+    Network,     fromWif,     importPubKey,+    textToAddr,+    textToFingerprint,     wrapPubKey,     xPubImport,  ) +import qualified Data.Text as Text+import Language.Bitcoin.Script.Descriptors.Checksum (+    descriptorChecksum,+    validDescriptorChecksum,+ ) import Language.Bitcoin.Script.Descriptors.Syntax import Language.Bitcoin.Utils (     alphanum,@@ -36,19 +44,43 @@     maybeFail,  ) -parseDescriptor :: Network -> Text -> Either String OutputDescriptor-parseDescriptor net = A.parseOnly $ outputDescriptorParser net+-- | An 'OutputDescriptor' with checksum details+data ChecksumDescriptor = ChecksumDescriptor+    { descriptor :: OutputDescriptor+    -- ^ The output descriptor+    , checksumStatus :: ChecksumStatus+    -- ^ The status of the output descriptor's checksum+    , expectedChecksum :: Text+    -- ^ The expected checksum for the output descriptor+    }+    deriving (Eq, Show) -outputDescriptorParser :: Network -> Parser OutputDescriptor+-- | The status of an output descriptor's checksum+data ChecksumStatus+    = -- | Checksum provided is valid+      Valid+    | -- | Checksum provided is invalid+      Invalid+        Text+        -- ^ The invalid checksum+    | -- | Checksum is not provided+      Absent+    deriving (Eq, Show)++parseDescriptor :: Network -> Text -> Either String ChecksumDescriptor+parseDescriptor = A.parseOnly . outputDescriptorParser++outputDescriptorParser :: Network -> Parser ChecksumDescriptor outputDescriptorParser net =-    spkP-        <|> shP-        <|> wpkhP-        <|> wshP-        <|> shwpkhP-        <|> shwshP-        <|> comboP-        <|> addrP+    checksumParser $+        spkP+            <|> shP+            <|> wpkhP+            <|> wshP+            <|> shwpkhP+            <|> shwshP+            <|> comboP+            <|> addrP   where     sdP = scriptDescriptorParser net     keyP = keyDescriptorParser net@@ -85,8 +117,12 @@ keyDescriptorParser :: Network -> Parser KeyDescriptor keyDescriptorParser net = KeyDescriptor <$> originP <*> keyP   where-    originP = optional . brackets $ Origin <$> A.hexadecimal <*> pathP+    originP = optional . brackets $ Origin <$> fingerprintP <*> pathP +    fingerprintP =+        A.take 8+            >>= either fail pure . textToFingerprint+     keyP = pubP <|> wifP <|> XPub <$> xpubP <*> pathP <*> famP      pubP = do@@ -111,3 +147,17 @@         n <- A.decimal         isHard <- isJust <$> optional (A.char '\'' <|> A.char 'h')         return $ bool (d :/) (d :|) isHard n++checksumParser :: Parser OutputDescriptor -> Parser ChecksumDescriptor+checksumParser p = do+    (input, desc) <- match p+    actual <- fmap (fromMaybe "") . optional $ do+        _ <- char '#'+        Text.pack <$> count 8 alphanum+    let status = case actual of+            "" -> Absent+            _+                | input `validDescriptorChecksum` actual -> Valid+                | otherwise -> Invalid actual+        expected = fromMaybe "" $ descriptorChecksum input+    return $ ChecksumDescriptor desc status expected
src/Language/Bitcoin/Script/Descriptors/Syntax.hs view
@@ -1,11 +1,8 @@-{-# LANGUAGE LambdaCase #-}- module Language.Bitcoin.Script.Descriptors.Syntax (     OutputDescriptor (..),     ScriptDescriptor (..),     KeyDescriptor (..),     isDefinite,-    keyAtIndex,     keyDescPubKey,     keyBytes,     Origin (..),@@ -16,11 +13,9 @@ ) where  import Data.ByteString (ByteString)-import Data.Word (Word32) import Haskoin (     Address,     DerivPath,-    DerivPathI ((:/), (:|)),     Fingerprint,     PubKeyI (..),     SecKeyI,@@ -93,13 +88,6 @@ -- | Simple explicit secret key with no origin information secKey :: SecKeyI -> KeyDescriptor secKey = KeyDescriptor Nothing . SecretKey---- | For key families, get the key at the given index.  Otherwise, return the input key.-keyAtIndex :: Word32 -> Key -> Key-keyAtIndex ix = \case-    XPub xpub path HardKeys -> XPub xpub (path :| ix) Single-    XPub xpub path SoftKeys -> XPub xpub (path :/ ix) Single-    key -> key  -- | Represent whether the key corresponds to a collection (and how) or a single key. data KeyCollection
src/Language/Bitcoin/Script/Descriptors/Text.hs view
@@ -4,31 +4,29 @@ -- | Convert descriptors to text module Language.Bitcoin.Script.Descriptors.Text (     descriptorToText,+    descriptorToTextWithChecksum,     keyDescriptorToText, ) where -import Data.ByteString.Builder (-    toLazyByteString,-    word32BE,- )-import Data.ByteString.Lazy (toStrict) import Data.Maybe (fromMaybe) import Data.Text (     Text,     intercalate,     pack,  )-import Haskoin.Address (addrToText)-import Haskoin.Constants (Network)-import Haskoin.Keys (+import Haskoin (+    Network,     PubKeyI (..),+    addrToText,+    encodeHex,     exportPubKey,+    fingerprintToText,     pathToStr,     toWif,     xPubExport,  )-import Haskoin.Util (encodeHex) +import Language.Bitcoin.Script.Descriptors.Checksum (descriptorChecksum) import Language.Bitcoin.Script.Descriptors.Syntax import Language.Bitcoin.Utils (     applicationText,@@ -51,6 +49,12 @@      addrErr = error "Unable to parse address" +descriptorToTextWithChecksum :: Network -> OutputDescriptor -> Text+descriptorToTextWithChecksum net desc =+    descText <> maybe "" ("#" <>) (descriptorChecksum descText)+  where+    descText = descriptorToText net desc+ scriptDescriptorToText :: Network -> ScriptDescriptor -> Text scriptDescriptorToText net = \case     Pk k -> applicationText "pk" $ keyToText k@@ -66,7 +70,7 @@ keyDescriptorToText :: Network -> KeyDescriptor -> Text keyDescriptorToText net (KeyDescriptor o k) = maybe mempty originText o <> definitionText   where-    originText (Origin fp path) = "[" <> fingerprintText fp <> pack (pathToStr path) <> "]"+    originText (Origin fp path) = "[" <> fingerprintToText fp <> pack (pathToStr path) <> "]"      definitionText = case k of         Pubkey (PubKeyI key c) -> encodeHex $ exportPubKey c key@@ -77,5 +81,3 @@         Single -> ""         HardKeys -> "/*'"         SoftKeys -> "/*"--    fingerprintText = encodeHex . toStrict . toLazyByteString . word32BE
src/Language/Bitcoin/Script/Descriptors/Utils.hs view
@@ -1,19 +1,64 @@ {-# LANGUAGE LambdaCase #-}+{-# LANGUAGE NumericUnderscores #-} +{- |+Module: Language.Bitcoin.Script.Descriptors.Utils+Stability: experimental+-} module Language.Bitcoin.Script.Descriptors.Utils (+    -- * Conversions     descriptorAddresses,     compile,++    -- * Transaction pieces+    TransactionScripts (..),+    outputDescriptorScripts,++    -- * Script families+    keyAtIndex,+    keyDescriptorAtIndex,+    scriptDescriptorAtIndex,+    outputDescriptorAtIndex,++    -- * Pub keys+    outputDescriptorPubKeys,+    scriptDescriptorPubKeys,++    -- * PSBT+    toPsbtInput,+    PsbtInputError (..), ) where +import Control.Applicative ((<|>))+import Control.Exception (Exception)+import Data.Functor ((<&>))+import Data.HashMap.Strict (HashMap)+import qualified Data.HashMap.Strict as HM import Data.List (sortOn)-import Data.Maybe (maybeToList)+import Data.Maybe (fromMaybe, mapMaybe, maybeToList) import Data.Serialize (decode, encode)+import qualified Data.Serialize as S+import Data.Word (Word32) import Haskoin (     Address,+    DerivPath,+    DerivPathI ((:/), (:|)),+    Fingerprint,+    Input,+    KeyIndex,+    PubKeyI (..),     Script,     ScriptOutput (..),+    Tx,     addressHash,     eitherToMaybe,+    emptyInput,+    encodeOutput,+    inputHDKeypaths,+    inputRedeemScript,+    inputWitnessScript,+    nonWitnessUtxo,+    pathToList,     payToNestedScriptAddress,     payToScriptAddress,     payToWitnessScriptAddress,@@ -22,12 +67,30 @@     pubKeyCompressed,     pubKeyWitnessAddr,     sortMulSig,+    toP2SH,+    toP2WSH,+    txOut,+    witnessUtxo,+    xPubFP,+    (++/),  ) -import qualified Language.Bitcoin.Miniscript as M+import qualified Language.Bitcoin.Miniscript.Compiler as M (+    compile,+ )+import qualified Language.Bitcoin.Miniscript.Syntax as M (+    key,+    keyH,+    multi,+ ) import Language.Bitcoin.Script.Descriptors.Syntax (+    Key (XPub),+    KeyCollection (..),+    KeyDescriptor (KeyDescriptor, keyDef),     OutputDescriptor (..),     ScriptDescriptor (..),+    derivation,+    fingerprint,     keyBytes,     keyDescPubKey,  )@@ -76,3 +139,249 @@     Raw bs -> eitherToMaybe (decode bs)   where     compileMaybe = eitherToMaybe . M.compile++data TransactionScripts = TransactionScripts+    { txScriptPubKey :: Script+    , txRedeemScript :: Maybe Script+    , txWitnessScript :: Maybe Script+    }+    deriving (Eq, Show)++outputDescriptorScripts :: OutputDescriptor -> Maybe TransactionScripts+outputDescriptorScripts =+    \case+        ScriptPubKey sd ->+            compile sd <&> \theScriptPubKey ->+                TransactionScripts+                    { txScriptPubKey = theScriptPubKey+                    , txRedeemScript = Nothing+                    , txWitnessScript = Nothing+                    }+        P2SH sd ->+            compile sd <&> \theScript ->+                TransactionScripts+                    { txScriptPubKey = encodeOutput $ toP2SH theScript+                    , txRedeemScript = Just theScript+                    , txWitnessScript = Nothing+                    }+        P2WPKH kd -> do+            theScriptPubKey <- encodeOutput . PayWitnessPKHash . addressHash . S.encode <$> keyDescPubKey kd+            pure+                TransactionScripts+                    { txScriptPubKey = theScriptPubKey+                    , txRedeemScript = Nothing+                    , txWitnessScript = Nothing+                    }+        P2WSH sd ->+            compile sd <&> \theScript ->+                TransactionScripts+                    { txScriptPubKey = encodeOutput $ toP2WSH theScript+                    , txRedeemScript = Nothing+                    , txWitnessScript = Just theScript+                    }+        WrappedWPkh kd -> do+            theRedeemScript <- encodeOutput . PayWitnessPKHash . addressHash . S.encode <$> keyDescPubKey kd+            pure+                TransactionScripts+                    { txScriptPubKey = encodeOutput $ toP2SH theRedeemScript+                    , txRedeemScript = Just theRedeemScript+                    , txWitnessScript = Nothing+                    }+        WrappedWSh sd ->+            compile sd <&> \theScript ->+                let theRedeemScript = encodeOutput $ toP2WSH theScript+                 in TransactionScripts+                        { txScriptPubKey = encodeOutput $ toP2SH theRedeemScript+                        , txRedeemScript = Just theRedeemScript+                        , txWitnessScript = Just theScript+                        }+        Combo _kd -> Nothing+        Addr _ad -> Nothing++{- | For key families, get the key at the given index.  Otherwise, return the input key.++  @since 0.2.1+-}+keyAtIndex :: Word32 -> Key -> Key+keyAtIndex ix = \case+    XPub xpub path HardKeys -> XPub xpub (path :| ix) Single+    XPub xpub path SoftKeys -> XPub xpub (path :/ ix) Single+    key -> key++{- | Specialize key families occurring in the descriptor to the given index++ @since 0.2.1+-}+outputDescriptorAtIndex :: KeyIndex -> OutputDescriptor -> OutputDescriptor+outputDescriptorAtIndex ix = \case+    o@ScriptPubKey{} -> o+    P2SH sd -> P2SH $ scriptDescriptorAtIndex ix sd+    P2WPKH kd -> P2WPKH $ keyDescriptorAtIndex ix kd+    P2WSH sd -> P2WSH $ scriptDescriptorAtIndex ix sd+    WrappedWPkh kd -> WrappedWPkh $ keyDescriptorAtIndex ix kd+    WrappedWSh sd -> WrappedWSh $ scriptDescriptorAtIndex ix sd+    Combo kd -> Combo $ keyDescriptorAtIndex ix kd+    a@Addr{} -> a++{- | Specialize key families occurring in the descriptor to the given index++ @since 0.2.1+-}+scriptDescriptorAtIndex :: KeyIndex -> ScriptDescriptor -> ScriptDescriptor+scriptDescriptorAtIndex ix = \case+    Pk kd -> Pk $ specialize kd+    Pkh kd -> Pkh $ specialize kd+    Multi k ks -> Multi k $ specialize <$> ks+    SortedMulti k ks -> SortedMulti k $ specialize <$> ks+    r@Raw{} -> r+  where+    specialize = keyDescriptorAtIndex ix++{- | Specialize key families occurring in the descriptor to the given index++ @since 0.2.1+-}+keyDescriptorAtIndex :: KeyIndex -> KeyDescriptor -> KeyDescriptor+keyDescriptorAtIndex ix keyDescriptor = keyDescriptor{keyDef = keyAtIndex ix $ keyDef keyDescriptor}++{- | Produce the psbt input parameters needed to spend an output from the+descriptor.  Caveat: This construction fails on `Combo` and `Addr` outputs.++ @since 0.2.1+-}+toPsbtInput ::+    -- | Transaction being spent+    Tx ->+    -- | Output being spent+    Int ->+    -- | Descriptor for output being spent+    OutputDescriptor ->+    Either PsbtInputError Input+toPsbtInput tx ix descriptor = case descriptor of+    ScriptPubKey sd ->+        pure+            emptyInput+                { nonWitnessUtxo = Just tx+                , inputHDKeypaths = hdPaths sd+                }+    P2SH sd -> do+        script <- compileForInput sd+        pure+            emptyInput+                { nonWitnessUtxo = Just tx+                , inputRedeemScript = Just script+                , inputHDKeypaths = hdPaths sd+                }+    P2WPKH kd -> do+        output <- txOut tx `safeIndex` ix+        pure+            emptyInput+                { witnessUtxo = Just output+                , inputHDKeypaths = hdPath kd+                }+    P2WSH sd -> do+        output <- txOut tx `safeIndex` ix+        script <- compileForInput sd+        pure+            emptyInput+                { witnessUtxo = Just output+                , inputWitnessScript = Just script+                , inputHDKeypaths = hdPaths sd+                }+    WrappedWPkh kd -> do+        output <- txOut tx `safeIndex` ix+        k <- maybe (Left $ KeyNotAvailable kd) pure $ keyDescPubKey kd+        pure+            emptyInput+                { witnessUtxo = Just output+                , inputRedeemScript =+                    Just+                        . encodeOutput+                        . PayWitnessPKHash+                        . addressHash+                        $ encode k+                , inputHDKeypaths = hdPath kd+                }+    WrappedWSh sd -> do+        output <- txOut tx `safeIndex` ix+        script <- compileForInput sd+        pure+            emptyInput+                { witnessUtxo = Just output+                , inputRedeemScript = Just . encodeOutput $ toP2WSH script+                , inputWitnessScript = Just script+                , inputHDKeypaths = hdPaths sd+                }+    o@Combo{} -> Left $ InvalidOutput o+    o@Addr{} -> Left $ InvalidOutput o+  where+    hdPaths = foldMap hdPath . scriptKeys+    compileForInput sd = maybe (Left $ CompileError sd) pure $ compile sd++    safeIndex (x : xs) n+        | n == 0 = pure x+        | n > 0 = safeIndex xs (n - 1)+    safeIndex _ _ = Left $ OutputIndexOOB tx ix++data PsbtInputError+    = OutputIndexOOB Tx Int+    | CompileError ScriptDescriptor+    | KeyNotAvailable KeyDescriptor+    | InvalidOutput OutputDescriptor+    deriving (Eq, Show)++instance Exception PsbtInputError++hdPath :: KeyDescriptor -> HashMap PubKeyI (Fingerprint, [KeyIndex])+hdPath k@(KeyDescriptor origin theKeyDef) = fromMaybe mempty $ do+    pubKey <- keyDescPubKey k+    fromOrigin pubKey <|> fromKey pubKey+  where+    fromOrigin pubKey = do+        theOrigin <- origin+        theKeyPath <- keyPath theKeyDef+        pure $+            HM.singleton+                pubKey+                ( fingerprint theOrigin+                , pathToList $ derivation theOrigin ++/ theKeyPath+                )+    fromKey pubKey =+        case theKeyDef of+            XPub xpub path Single ->+                pure $+                    HM.singleton+                        pubKey+                        ( xPubFP xpub+                        , pathToList path+                        )+            _ -> Nothing++keyPath :: Key -> Maybe DerivPath+keyPath = \case+    XPub _ path Single -> Just path+    _ -> Nothing++scriptKeys :: ScriptDescriptor -> [KeyDescriptor]+scriptKeys = \case+    Pk k -> [k]+    Pkh k -> [k]+    Multi _ ks -> ks+    SortedMulti _ ks -> ks+    Raw{} -> mempty++-- | Extract pubkeys from an 'OutputDescriptor' where possible+outputDescriptorPubKeys :: OutputDescriptor -> [PubKeyI]+outputDescriptorPubKeys = \case+    ScriptPubKey sd -> scriptDescriptorPubKeys sd+    P2SH sd -> scriptDescriptorPubKeys sd+    P2WPKH kd -> foldMap pure $ keyDescPubKey kd+    P2WSH sd -> scriptDescriptorPubKeys sd+    WrappedWPkh kd -> foldMap pure $ keyDescPubKey kd+    WrappedWSh sd -> scriptDescriptorPubKeys sd+    Combo kd -> foldMap pure $ keyDescPubKey kd+    Addr _ad -> mempty++-- | Extract pubkeys from a 'ScriptDescriptor' where possible+scriptDescriptorPubKeys :: ScriptDescriptor -> [PubKeyI]+scriptDescriptorPubKeys = mapMaybe keyDescPubKey . scriptKeys
test/Test/Descriptors.hs view
@@ -17,14 +17,19 @@ import Haskoin.Util (decodeHex) import Test.Tasty (TestTree, testGroup) +import Data.Maybe (fromMaybe) import Language.Bitcoin.Script.Descriptors (+    ChecksumDescriptor (..),+    ChecksumStatus (..),     Key (..),     KeyCollection (..),     KeyDescriptor (..),     Origin (..),     OutputDescriptor (..),     ScriptDescriptor (..),+    descriptorChecksum,     descriptorToText,+    descriptorToTextWithChecksum,     parseDescriptor,  ) import Test.Descriptors.Utils (testDescriptorUtils)@@ -33,7 +38,19 @@ descriptorTests :: TestTree descriptorTests =     testGroup "descriptor tests" $-        (testTextRep (parseDescriptor btc) (descriptorToText btc) <$> examples)+        [ testGroup "absent checksum" $+            ( testTextRep+                (parseDescriptor btc)+                (descriptorToText btc . descriptor)+                <$> absentChecksumExamples+            )+        , testGroup "valid checksum" $+            ( testTextRep+                (parseDescriptor btc)+                (descriptorToTextWithChecksum btc . descriptor)+                <$> validChecksumExamples+            )+        ]             <> [testDescriptorUtils]   where     examples =@@ -54,6 +71,28 @@         , example15         , example16         ]+    absentChecksumExamples = withAbsentChecksum <$> examples+    validChecksumExamples =+        zipWith+            withValidChecksum+            examples+            [ "gn28ywm7"+            , "8fhd9pwu"+            , "8zl0zxma"+            , "qkrrc7je"+            , "lq9sf04s"+            , "2wtr0ej5"+            , "hzhjw406"+            , "y9zthqta"+            , "qwx6n9lh"+            , "en3tu306"+            , "ks05yr6p"+            , "axav5m0j"+            , "h69t6zk4"+            , "ml40v0wf"+            , "t2zpj2eu"+            , "v66cvalc"+            ]  key :: PubKeyI -> KeyDescriptor key = KeyDescriptor Nothing . Pubkey@@ -63,6 +102,32 @@   where     Just k = importPubKey =<< decodeHex h +withAbsentChecksum ::+    Example OutputDescriptor -> Example ChecksumDescriptor+withAbsentChecksum example =+    example+        { script =+            ChecksumDescriptor+                { descriptor = script example+                , checksumStatus = Absent+                , expectedChecksum =+                    fromMaybe "" $ descriptorChecksum $ text example+                }+        }++withValidChecksum ::+    Example OutputDescriptor -> Text -> Example ChecksumDescriptor+withValidChecksum example checksum =+    example+        { script =+            ChecksumDescriptor+                { descriptor = script example+                , checksumStatus = Valid+                , expectedChecksum = checksum+                }+        , text = text example <> "#" <> checksum+        }+ example1 :: Example OutputDescriptor example1 =     Example@@ -221,7 +286,7 @@         }   where     Just xpub = xPubImport btc "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL"-    fp = 0xd34db33f+    fp = "d34db33f"  example15 :: Example OutputDescriptor example15 =
test/Test/Descriptors/Utils.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE NumericUnderscores #-} {-# LANGUAGE OverloadedStrings #-} {-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} @@ -6,33 +7,73 @@ ) where  import qualified Data.ByteString as BS+import Data.Bytes.Put (runPutS)+import Data.Bytes.Serial (serialize)+import qualified Data.HashMap.Strict as HM import Data.List (sort) import Data.Maybe (mapMaybe) import Data.Serialize (encode) import Haskoin (+    DerivPath,+    DerivPathI (..),+    OutPoint (OutPoint),     PubKeyI (PubKeyI),     Script (Script),     ScriptOp (..),+    ScriptOutput (..),+    XPubKey,+    addressHash,+    btc,     btcRegTest,+    buildTx,     derivePubKey,+    derivePubPath,+    emptyInput,+    encodeOutput,+    inputHDKeypaths,+    inputRedeemScript,+    inputWitnessScript,+    nonWitnessUtxo,     opPushData,+    pathToList,     ripemd160,     secKey,     textToAddr,+    toP2SH,+    toP2WSH,+    toSoft,+    txOut,+    witnessUtxo,+    xPubFP,+    xPubImport,+    xPubKey,  ) import Test.Tasty (TestTree, testGroup)-import Test.Tasty.HUnit (testCase, (@?=))+import Test.Tasty.HUnit (testCase, testCaseSteps, (@?=))  import Language.Bitcoin.Script.Descriptors (+    Key (XPub),+    KeyCollection (..),+    KeyDescriptor (KeyDescriptor),     OutputDescriptor (..),     ScriptDescriptor (..),     compile,     descriptorAddresses,+    keyDescriptorAtIndex,+    outputDescriptorAtIndex,     pubKey,+    toPsbtInput,  )  testDescriptorUtils :: TestTree-testDescriptorUtils = testGroup "descriptor utils" [testCompile, testAddresses]+testDescriptorUtils =+    testGroup+        "descriptor utils"+        [ testCompile+        , testAddresses+        , testKeyAtIndex+        , testToPsbtInput+        ]  -- Address tests generated using @bitcoin-cli deriveaddresses@ testAddresses :: TestTree@@ -48,6 +89,14 @@         , testCombo         ] +testKeyAtIndex :: TestTree+testKeyAtIndex =+    testGroup+        "keyAtIndex"+        [ testKeyDescriptorAtIndex+        , testOutputDescriptorAtIndex+        ]+ testP2PKH :: TestTree testP2PKH = testCase "P2PKH" $ descriptorAddresses example @?= [expected]   where@@ -129,8 +178,141 @@     ks = take 3 testPubKeys     [k0, k1, k2] = sort $ encode <$> ks +testKeyDescriptorAtIndex :: TestTree+testKeyDescriptorAtIndex = testCase "keyDescriptorAtIndex" $ do+    keyDescriptorAtIndex 5 keyFamA @?= keyA+    keyDescriptorAtIndex 5 keyFamB @?= keyB+    keyDescriptorAtIndex 5 keyC @?= keyC+  where+    keyFamA = KeyDescriptor Nothing $ XPub someXPubA basePath HardKeys+    keyA = KeyDescriptor Nothing $ XPub someXPubA (basePath :| 5) Single++    keyFamB = KeyDescriptor Nothing $ XPub someXPubA basePath SoftKeys+    keyB = KeyDescriptor Nothing $ XPub someXPubA (basePath :/ 5) Single++    keyC = KeyDescriptor Nothing $ XPub someXPubA basePath Single++testOutputDescriptorAtIndex :: TestTree+testOutputDescriptorAtIndex = testCase "outputDescriptorAtIndex" $ do+    outputDescriptorAtIndex 5 descFamA @?= descA+    outputDescriptorAtIndex 5 descFamB @?= descB+    outputDescriptorAtIndex 5 descFamC @?= descC+  where+    descFamA = P2SH $ SortedMulti 2 [keyFamA, keyFamB]+    descA = P2SH $ SortedMulti 2 [keyA, keyB]++    descFamB = P2WSH $ Pkh keyFamB+    descB = P2WSH $ Pkh keyB++    descFamC = P2WPKH keyFamA+    descC = P2WPKH keyA++    keyFamA = KeyDescriptor Nothing $ XPub someXPubA basePath HardKeys+    keyA = KeyDescriptor Nothing $ XPub someXPubA (basePath :| 5) Single++    keyFamB = KeyDescriptor Nothing $ XPub someXPubB basePath HardKeys+    keyB = KeyDescriptor Nothing $ XPub someXPubB (basePath :| 5) Single++testToPsbtInput :: TestTree+testToPsbtInput = testCaseSteps "toPsbtInput" $ \step -> do+    step "P2PKH"+    toPsbtInput p2pkhTx 0 p2pkhDescriptor @?= Right expectedP2pkhInput++    step "P2SH-MS"+    toPsbtInput p2shMsTx 0 p2shMsDescriptor @?= Right expectedP2shMsInput++    step "P2SH-WPKH"+    toPsbtInput p2shWpkhTx 0 p2shWpkhDescriptor @?= Right expectedP2shWpkhInput++    step "P2SH-WSH-MS"+    toPsbtInput p2shWshMsTx 0 p2shWshMsDescriptor @?= Right expectedP2shWshMsInput++    step "P2WPKH"+    toPsbtInput wpkhTx 0 wpkhDescriptor @?= Right expectedWpkhInput++    step "P2WSH-MS"+    toPsbtInput wshMsTx 0 wshMsDescriptor @?= Right expectedWshMsInput+  where+    p2pkhTx = buildTx [outPoint] [(PayPKHash hashA, 1_000_000)]+    p2pkhDescriptor = ScriptPubKey $ Pkh keyA+    expectedP2pkhInput =+        emptyInput+            { nonWitnessUtxo = Just p2pkhTx+            , inputHDKeypaths = hdKeypathA+            }++    p2shMsTx = buildTx [outPoint] [(msScriptOutput, 1_000_000)]+    p2shMsDescriptor = P2SH msDescriptor+    expectedP2shMsInput =+        emptyInput+            { nonWitnessUtxo = Just p2shMsTx+            , inputRedeemScript = Just $ encodeOutput msScriptOutput+            , inputHDKeypaths = hdKeypathA <> hdKeypathB+            }+    msScriptOutput = PayMulSig [pubKeyA, pubKeyB] 1+    msDescriptor = Multi 1 [keyA, keyB]++    keyA = KeyDescriptor Nothing $ XPub someXPubA path Single+    pubKeyA = (`PubKeyI` True) . xPubKey $ derivePubPath softPath someXPubA+    hashA = addressHash . runPutS $ serialize pubKeyA+    hdKeypathA = HM.singleton pubKeyA (xPubFP someXPubA, pathToList path)++    keyB = KeyDescriptor Nothing $ XPub someXPubB path Single+    pubKeyB = (`PubKeyI` True) . xPubKey $ derivePubPath softPath someXPubB+    hdKeypathB = HM.singleton pubKeyB (xPubFP someXPubB, pathToList path)++    p2shWpkhDescriptor = WrappedWPkh keyA+    p2shWpkhTx = buildTx [outPoint] [(p2shWpkhScriptOutput, 1_000_000)]+    expectedP2shWpkhInput =+        emptyInput+            { witnessUtxo = Just $ (head . txOut) p2shWpkhTx+            , inputRedeemScript = Just $ encodeOutput wpkhScriptOutputA+            , inputHDKeypaths = hdKeypathA+            }+    p2shWpkhScriptOutput = toP2SH $ encodeOutput wpkhScriptOutputA+    wpkhScriptOutputA = PayWitnessPKHash hashA++    p2shWshMsTx = buildTx [outPoint] [(p2shWshMsOutput, 1_000_000)]+    p2shWshMsDescriptor = WrappedWSh msDescriptor+    expectedP2shWshMsInput =+        emptyInput+            { witnessUtxo = Just $ (head . txOut) p2shWshMsTx+            , inputRedeemScript = Just $ encodeOutput wshMsOutput+            , inputWitnessScript = Just $ encodeOutput msScriptOutput+            , inputHDKeypaths = hdKeypathA <> hdKeypathB+            }+    p2shWshMsOutput = toP2SH $ encodeOutput wshMsOutput+    wshMsOutput = toP2WSH $ encodeOutput msScriptOutput++    wpkhTx = buildTx [outPoint] [(PayWitnessPKHash hashA, 1_000_000)]+    wpkhDescriptor = P2WPKH keyA+    expectedWpkhInput =+        emptyInput+            { witnessUtxo = Just . head $ txOut wpkhTx+            , inputHDKeypaths = hdKeypathA+            }+    wshMsTx = buildTx [outPoint] [(wshMsOutput, 1_000_000)]+    wshMsDescriptor = P2WSH msDescriptor+    expectedWshMsInput =+        emptyInput+            { witnessUtxo = Just $ (head . txOut) wshMsTx+            , inputWitnessScript = Just $ encodeOutput msScriptOutput+            , inputHDKeypaths = hdKeypathA <> hdKeypathB+            }++    outPoint = OutPoint "0000000000000000000000000000000000000000000000000000000000000000" 0+    path = Deriv :/ 1 :: DerivPath+    Just softPath = toSoft path+ key0 :: PubKeyI testPubKeys :: [PubKeyI] testPubKeys@(key0 : _) = (`PubKeyI` True) . derivePubKey <$> mapMaybe (secKey . mkSecKey) [1 .. 255]   where     mkSecKey i = BS.pack $ replicate 31 0 <> [i]++someXPubA, someXPubB :: XPubKey+Just someXPubA = xPubImport btc "xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB"+Just someXPubB = xPubImport btc "xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH"++basePath :: DerivPath+basePath = Deriv :| 1500