bitcoin-scripting-0.3.0: src/Language/Bitcoin/Script/Descriptors/Utils.hs
{-# 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 (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,
pubKeyAddr,
pubKeyCompatWitnessAddr,
pubKeyCompressed,
pubKeyWitnessAddr,
sortMulSig,
toP2SH,
toP2WSH,
txOut,
witnessUtxo,
xPubFP,
(++/),
)
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,
)
{- | Get the set of addresses associated with an output descriptor. The list will be empty if:
* any keys are indefinite
* the output is p2pk
* the output has a non-standard script
The list can contain more than one address in the case of the "combo" construct.
-}
descriptorAddresses :: OutputDescriptor -> [Address]
descriptorAddresses = \case
ScriptPubKey Pk{} -> mempty
ScriptPubKey (Pkh key) -> foldMap (pure . pubKeyAddr) $ keyDescPubKey key
P2SH descriptor -> maybeToList $ payToScriptAddress <$> scriptDescriptorOutput descriptor
P2WPKH key -> foldMap (pure . pubKeyWitnessAddr) $ keyDescPubKey key
P2WSH descriptor -> maybeToList $ payToWitnessScriptAddress <$> scriptDescriptorOutput descriptor
WrappedWPkh key -> foldMap (pure . pubKeyCompatWitnessAddr) $ keyDescPubKey key
WrappedWSh descriptor -> maybeToList $ payToNestedScriptAddress <$> scriptDescriptorOutput descriptor
Combo key
| Just pk <- keyDescPubKey key ->
[pubKeyAddr pk]
<> if pubKeyCompressed pk
then [pubKeyWitnessAddr pk, pubKeyCompatWitnessAddr pk]
else mempty
Addr addr -> [addr]
_ -> mempty
scriptDescriptorOutput :: ScriptDescriptor -> Maybe ScriptOutput
scriptDescriptorOutput = \case
Pk key -> PayPK <$> keyDescPubKey key
Pkh key -> PayPKHash . addressHash . encode <$> keyDescPubKey key
Multi k ks -> PayMulSig <$> traverse keyDescPubKey ks <*> pure k
SortedMulti k ks -> sortMulSig <$> (PayMulSig <$> traverse keyDescPubKey ks <*> pure k)
_ -> Nothing
-- | Produce the script described by the descriptor. Fails when any keys in the descriptor are indeterminate.
compile :: ScriptDescriptor -> Maybe Script
compile = \case
Pk key -> compileMaybe $ M.key key
Pkh key -> compileMaybe $ M.keyH key
Multi k ks -> compileMaybe $ M.multi k ks
SortedMulti k ks -> compileMaybe $ M.multi k (sortOn keyBytes ks)
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