packages feed

haskoin-core-0.20.0: src/Haskoin/Transaction/Partial.hs

{-# LANGUAGE DeriveGeneric              #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE NamedFieldPuns             #-}
{-# LANGUAGE OverloadedStrings          #-}
{-# LANGUAGE ScopedTypeVariables        #-}
{-|
Module      : Haskoin.Transaction.Partial
Copyright   : No rights reserved
License     : MIT
Maintainer  : matt@bitnomial.com
Stability   : experimental
Portability : POSIX

Code related to PSBT parsing and serialization.
-}
module Haskoin.Transaction.Partial
    ( -- * Partially-Signed Transactions
      PartiallySignedTransaction (..)
    , Input (..)
    , Output (..)
    , UnknownMap (..)
    , Key (..)
    , merge
    , mergeInput
    , mergeOutput
    , complete
    , finalTransaction
    , emptyPSBT
    , emptyInput
    , emptyOutput
    ) where

import           Control.Applicative        ((<|>))
import           Control.DeepSeq
import           Control.Monad              (guard, replicateM, void)
import           Data.ByteString            (ByteString)
import qualified Data.ByteString            as B
import           Data.Bytes.Get             (runGetS)
import           Data.Bytes.Put             (runPutS)
import           Data.Bytes.Serial          (Serial (..))
import           Data.HashMap.Strict        (HashMap)
import qualified Data.HashMap.Strict        as HashMap
import           Data.Hashable              (Hashable)
import           Data.List                  (foldl')
import           Data.Maybe                 (fromMaybe, isJust)
import           Data.Serialize             (Get, Put, Serialize)
import qualified Data.Serialize             as S
import           GHC.Generics               (Generic)
import           GHC.Word                   (Word32, Word8)
import           Haskoin.Address            (Address (..), pubKeyAddr)
import           Haskoin.Keys               (Fingerprint, KeyIndex, PubKeyI)
import           Haskoin.Network            (VarInt (..), VarString (..),
                                             putVarInt)
import           Haskoin.Script             (Script (..), ScriptOp (..),
                                             ScriptOutput (..), SigHash,
                                             decodeOutput, decodeOutputBS,
                                             encodeOutputBS, isPayScriptHash,
                                             opPushData, toP2SH, toP2WSH)
import           Haskoin.Transaction.Common (Tx (..), TxOut, WitnessStack,
                                             outPointIndex, prevOutput,
                                             scriptInput, scriptOutput)
import           Haskoin.Util               (eitherToMaybe)

-- | PSBT data type as specified in
-- [BIP-174](https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki).
-- This contains an unsigned transaction, inputs and outputs, and unspecified
-- extra data. There is one input per input in the unsigned transaction, and one
-- output per output in the unsigned transaction. The inputs and outputs in the
-- 'PartiallySignedTransaction' line up by index with the inputs and outputs in
-- the unsigned transaction.
data PartiallySignedTransaction =
    PartiallySignedTransaction
    { unsignedTransaction :: Tx
    , globalUnknown       :: UnknownMap
    , inputs              :: [Input]
    , outputs             :: [Output]
    } deriving (Show, Eq, Generic)

instance NFData PartiallySignedTransaction

-- | Inputs contain all of the data needed to sign a transaction and all of the
-- resulting signature data after signing.
data Input = Input
    { nonWitnessUtxo     :: Maybe Tx
    , witnessUtxo        :: Maybe TxOut
    , partialSigs        :: HashMap PubKeyI ByteString
    , sigHashType        :: Maybe SigHash
    , inputRedeemScript  :: Maybe Script
    , inputWitnessScript :: Maybe Script
    , inputHDKeypaths    :: HashMap PubKeyI (Fingerprint, [KeyIndex])
    , finalScriptSig     :: Maybe Script
    , finalScriptWitness :: Maybe WitnessStack
    , inputUnknown       :: UnknownMap
    } deriving (Show, Eq, Generic)

instance NFData Input

-- | Outputs can contain information needed to spend the output at a later date.
data Output = Output
    { outputRedeemScript  :: Maybe Script
    , outputWitnessScript :: Maybe Script
    , outputHDKeypaths    :: HashMap PubKeyI (Fingerprint, [KeyIndex])
    , outputUnknown       :: UnknownMap
    } deriving (Show, Eq, Generic)

instance NFData Output

-- | A map of raw PSBT keys to byte strings for extra data. The 'keyType' field
-- cannot overlap with any of the reserved 'keyType' fields specified in the
-- PSBT specification.
newtype UnknownMap = UnknownMap { unknownMap :: HashMap Key ByteString }
    deriving (Show, Eq, Semigroup, Monoid, Generic)

instance NFData UnknownMap

-- | Raw keys for the map type used in PSBTs.
data Key = Key
    { keyType :: Word8
    , key     :: ByteString
    } deriving (Show, Eq, Generic)

instance NFData Key

instance Hashable Key

-- | Take two 'PartiallySignedTransaction's and merge them. The
-- 'unsignedTransaction' field in both must be the same.
merge :: PartiallySignedTransaction
      -> PartiallySignedTransaction
      -> Maybe PartiallySignedTransaction
merge psbt1 psbt2
    | unsignedTransaction psbt1 == unsignedTransaction psbt2
    = Just $ psbt1
        { globalUnknown = globalUnknown psbt1 <> globalUnknown psbt2
        , inputs = zipWith mergeInput (inputs psbt1) (inputs psbt2)
        , outputs = zipWith mergeOutput (outputs psbt1) (outputs psbt2)
        }
merge _ _ = Nothing

mergeInput :: Input -> Input -> Input
mergeInput a b = Input
    { nonWitnessUtxo =
            if isJust witUtx
            then Nothing
            else nonWitnessUtxo a <|> nonWitnessUtxo b
    , witnessUtxo =
            witUtx
    , sigHashType =
            sigHashType a <|> sigHashType b
    , partialSigs =
            partialSigs a <> partialSigs b
    , inputHDKeypaths =
            inputHDKeypaths a <> inputHDKeypaths b
    , inputUnknown =
            inputUnknown a <> inputUnknown b
    , inputRedeemScript =
            inputRedeemScript a <|> inputRedeemScript b
    , inputWitnessScript =
            inputWitnessScript a <|> inputWitnessScript b
    , finalScriptSig =
            finalScriptSig a <|> finalScriptSig b
    , finalScriptWitness =
            finalScriptWitness a <|> finalScriptWitness b
    }
  where
    witUtx = witnessUtxo a <|> witnessUtxo b

mergeOutput :: Output -> Output -> Output
mergeOutput a b = Output
    { outputRedeemScript =
            outputRedeemScript a <|> outputRedeemScript b
    , outputWitnessScript =
            outputWitnessScript a <|> outputWitnessScript b
    , outputHDKeypaths =
            outputHDKeypaths a <> outputHDKeypaths b
    , outputUnknown =
            outputUnknown a <> outputUnknown b
    }

-- | Take partial signatures from all of the 'Input's and finalize the signature.
complete :: PartiallySignedTransaction
         -> PartiallySignedTransaction
complete psbt =
    psbt
    {
        inputs = map
                 (completeInput . analyzeInputs)
                 (indexed $ inputs psbt)
    }
  where
    analyzeInputs (i, input) =
        (,)
        (outputScript =<< witnessUtxo input <|> nonWitScript)
        input
      where
        nonWitScript = getPrevOut i =<< nonWitnessUtxo input

    getPrevOut i tx =
       (txOut tx !!?) .
       fromIntegral .
       outPointIndex .
       prevOutput =<<
       txIn (unsignedTransaction psbt) !!? i
    xs !!? i = lookup i $ indexed xs

    outputScript = eitherToMaybe . decodeOutputBS . scriptOutput

    completeInput (Nothing, input)     = input
    completeInput (Just script, input) = completeSig input script

    indexed :: [a] -> [(Word32, a)]
    indexed = zip [0..]

completeSig :: Input -> ScriptOutput -> Input

completeSig input (PayPK k) =
    input
    { finalScriptSig =
            eitherToMaybe . runGetS deserialize =<<
            HashMap.lookup k (partialSigs input)
    }

completeSig input (PayPKHash h)
    | [(k, sig)] <- HashMap.toList (partialSigs input)
    , PubKeyAddress h == pubKeyAddr k =
            input
            {
                finalScriptSig =
                    Just $
                    Script
                    [
                        opPushData sig,
                        opPushData (runPutS (serialize k))
                    ]
            }

completeSig input (PayMulSig pubKeys m)
    | length sigs >= m =
          input { finalScriptSig = finalSig }
  where
    sigs = collectSigs m pubKeys input
    finalSig =
        Script .
        (OP_0 :) .
        (map opPushData sigs <>) .
        pure . opPushData . runPutS . serialize <$>
        inputRedeemScript input

completeSig input (PayScriptHash h)
    | Just rdmScript <- inputRedeemScript input
    , PayScriptHash h == toP2SH rdmScript
    , Right decodedScript <- decodeOutput rdmScript
    , not (isPayScriptHash decodedScript) =
            completeSig input decodedScript

completeSig input (PayWitnessPKHash h)
    | [(k, sig)] <- HashMap.toList (partialSigs input)
    , PubKeyAddress h == pubKeyAddr k =
            input
            {
                finalScriptWitness =
                    Just [sig, runPutS $ serialize k],
                finalScriptSig =
                    Script . pure . opPushData . runPutS . serialize <$>
                    inputRedeemScript input
            }
completeSig input (PayWitnessScriptHash h)
    | Just witScript <- inputWitnessScript input
    , PayWitnessScriptHash h == toP2WSH witScript
    , Right decodedScript <- decodeOutput witScript =
            completeWitnessSig input decodedScript

completeSig input _ = input

completeWitnessSig :: Input -> ScriptOutput -> Input
completeWitnessSig input script@(PayMulSig pubKeys m)
    | length sigs >= m =
          input
          {
              finalScriptWitness = Just finalWit,
              finalScriptSig = finalSig
          }
  where
    sigs = collectSigs m pubKeys input
    finalSig = Script . pure . opPushData . runPutS . serialize <$>
               inputRedeemScript input
    finalWit = mempty : sigs <> [encodeOutputBS script]

completeWitnessSig input _ = input

collectSigs :: Int -> [PubKeyI] -> Input -> [ByteString]
collectSigs m pubKeys input =
    take m . reverse $ foldl' lookupKey [] pubKeys
  where
    lookupKey sigs key =
        maybe sigs (: sigs) $
        HashMap.lookup key (partialSigs input)

-- | Take a finalized 'PartiallySignedTransaction' and produce the signed final
-- transaction. You may need to call 'complete' on the
-- 'PartiallySignedTransaction' before producing the final transaction.
finalTransaction :: PartiallySignedTransaction -> Tx
finalTransaction psbt =
    setInputs .
    foldl' finalizeInput ([], []) $
    zip (txIn tx) (inputs psbt)
  where
    tx = unsignedTransaction psbt
    hasWitness =
        any
        (isJust . finalScriptWitness)
        (inputs psbt)
    setInputs (ins, witData) =
        tx
        {
            txIn = reverse ins,
            txWitness = if hasWitness then reverse witData else []
        }
    finalizeInput (ins, witData) (txInput, psbtInput) =
        maybe finalWitness finalScript $
        finalScriptSig psbtInput
      where
        finalScript script =
            (
                txInput { scriptInput = runPutS $ serialize script } : ins,
                [] : witData
            )
        finalWitness =
            (
                ins,
                fromMaybe [] (finalScriptWitness psbtInput) : witData
            )

-- | Take an unsigned transaction and produce an empty
-- 'PartiallySignedTransaction'
emptyPSBT :: Tx -> PartiallySignedTransaction
emptyPSBT tx = PartiallySignedTransaction
    { unsignedTransaction = tx
    , globalUnknown = mempty
    , inputs = replicate (length (txIn tx)) emptyInput
    , outputs = replicate (length (txOut tx)) emptyOutput
    }

emptyInput :: Input
emptyInput = Input
    Nothing Nothing HashMap.empty Nothing
    Nothing Nothing HashMap.empty
    Nothing Nothing (UnknownMap HashMap.empty)

emptyOutput :: Output
emptyOutput = Output Nothing Nothing HashMap.empty (UnknownMap HashMap.empty)

instance Serialize PartiallySignedTransaction where
    get = do
        magic <- S.getBytes 4
        guard $ magic == "psbt"
        headerSep <- S.getWord8
        guard $ headerSep == 0xff

        keySize <- S.getWord8
        guard $ keySize == 1
        globalUnsignedTxType <- S.getWord8
        guard $ globalUnsignedTxType == 0x00
        unsignedTransaction <- getSizedBytes deserialize
        guard $ all (B.null . scriptInput) (txIn unsignedTransaction)
        guard $ null (txWitness unsignedTransaction)

        globalUnknown <- S.get
        globalEnd <- S.getWord8
        guard $ globalEnd == 0x00

        inputs <-
            replicateM
            (length (txIn unsignedTransaction))
            S.get
        outputs <-
            replicateM
            (length (txOut unsignedTransaction))
            S.get

        return
            PartiallySignedTransaction
            {
                unsignedTransaction,
                globalUnknown,
                inputs,
                outputs
            }

    put PartiallySignedTransaction
        {
            unsignedTransaction,
            globalUnknown,
            inputs,
            outputs
        } = do
        S.putByteString "psbt"
        S.putWord8 0xff -- Header separator

        S.putWord8 0x01 -- Key size
        S.putWord8 0x00 -- Unsigned Transaction type
        putSizedBytes $ serialize unsignedTransaction
        S.put globalUnknown
        S.putWord8 0x00 -- Global end

        mapM_ S.put inputs
        mapM_ S.put outputs

instance Serialize Key where
    get = do
        VarInt keySize <- deserialize
        guard $ keySize > 0
        t <- S.getWord8
        k <- S.getBytes (fromIntegral keySize - 1)
        return (Key t k)

    put (Key t k) = do
        putVarInt $ 1 + B.length k
        S.putWord8 t
        S.putByteString k

instance Serialize UnknownMap where
    get = go HashMap.empty
      where
        getItem m = do
            k <- S.get
            VarString v <- deserialize
            go $ HashMap.insert k v m
        go m = do
            isEnd <- S.lookAhead S.getWord8
            if isEnd == 0x00
                then return (UnknownMap m)
                else getItem m

    put (UnknownMap m) =
        void $
        HashMap.traverseWithKey
        (\k v -> S.put k >> serialize (VarString v))
        m

instance Serialize Input where
    get =
        getMap getInputItem setInputUnknown emptyInput
      where
        setInputUnknown f input =
            input
            {
                inputUnknown =
                    UnknownMap (f (unknownMap (inputUnknown input)))
            }

    put Input
        {
            nonWitnessUtxo,
            witnessUtxo,
            partialSigs,
            sigHashType,
            inputRedeemScript,
            inputWitnessScript,
            inputHDKeypaths,
            finalScriptSig,
            finalScriptWitness,
            inputUnknown
        } = do
        whenJust (putKeyValue InNonWitnessUtxo . serialize)
            nonWitnessUtxo
        whenJust (putKeyValue InWitnessUtxo . serialize)
            witnessUtxo
        putPartialSig partialSigs
        whenJust putSigHash
            sigHashType
        whenJust (putKeyValue InRedeemScript . serialize)
            inputRedeemScript
        whenJust (putKeyValue InWitnessScript . serialize)
            inputWitnessScript
        putHDPath InBIP32Derivation inputHDKeypaths
        whenJust (putKeyValue InFinalScriptSig . serialize)
            finalScriptSig
        whenJust (putKeyValue InFinalScriptWitness . serialize)
            finalScriptWitness
        S.put inputUnknown
        S.putWord8 0x00
      where
        putPartialSig =
            putPubKeyMap serialize InPartialSig . fmap VarString
        putSigHash sigHash = do
            putKey InSigHashType
            S.putWord8 0x04
            S.putWord32le (fromIntegral sigHash)

instance Serialize Output where
    get = getMap getOutputItem setOutputUnknown emptyOutput
      where
        setOutputUnknown f output =
            output
            {
                outputUnknown =
                    UnknownMap (f (unknownMap (outputUnknown output)))
            }

    put Output
        {
            outputRedeemScript,
            outputWitnessScript,
            outputHDKeypaths,
            outputUnknown
        } = do
        whenJust (putKeyValue OutRedeemScript . serialize)
            outputRedeemScript
        whenJust (putKeyValue OutWitnessScript . serialize)
            outputWitnessScript
        putHDPath OutBIP32Derivation
            outputHDKeypaths
        S.put outputUnknown
        S.putWord8 0x00

putSizedBytes :: Put -> Put
putSizedBytes f = do
    putVarInt (B.length bs)
    S.putByteString bs
  where
    bs = S.runPut f

getSizedBytes :: Get a -> Get a
getSizedBytes f =
    S.getNested
    (fromIntegral . getVarInt <$> deserialize)
    f

putKeyValue :: Enum t => t -> Put -> Put
putKeyValue t v = do
    putKey t
    putSizedBytes v

putKey :: Enum t => t -> Put
putKey t = do
    putVarInt (1 :: Word8)
    S.putWord8 (enumWord8 t)

getMap :: (Bounded t, Enum t)
       => (Int -> a -> t -> Get a)
       -> ((HashMap Key ByteString -> HashMap Key ByteString) -> a -> a)
       -> a -> Get a
getMap getMapItem setUnknown = go
  where
    getItem keySize m (Right t) =
        getMapItem (fromIntegral keySize - 1) m t >>= go
    getItem keySize m (Left t)  = do
        k <- S.getBytes (fromIntegral keySize - 1)
        VarString v <- deserialize
        go $ setUnknown (HashMap.insert (Key t k) v) m
    go m = do
        keySize <- getVarInt <$> deserialize
        if keySize == 0
            then return m
            else getItem keySize m =<< (word8Enum <$> S.getWord8)

data InputType
    = InNonWitnessUtxo
    | InWitnessUtxo
    | InPartialSig
    | InSigHashType
    | InRedeemScript
    | InWitnessScript
    | InBIP32Derivation
    | InFinalScriptSig
    | InFinalScriptWitness
    deriving (Show, Eq, Enum, Bounded, Generic)

instance NFData InputType

data OutputType
    = OutRedeemScript
    | OutWitnessScript
    | OutBIP32Derivation
    deriving (Show, Eq, Enum, Bounded, Generic)

instance NFData OutputType

getInputItem :: Int -> Input -> InputType -> Get Input

getInputItem 0 input@Input{nonWitnessUtxo = Nothing} InNonWitnessUtxo = do
    utxo <- getSizedBytes deserialize
    return input { nonWitnessUtxo = Just utxo }

getInputItem 0 input@Input{witnessUtxo = Nothing} InWitnessUtxo = do
    utxo <- getSizedBytes deserialize
    return input { witnessUtxo = Just utxo }

getInputItem keySize input InPartialSig = do
    (k, v) <- getPartialSig
    return
        input
        {
            partialSigs = HashMap.insert k v (partialSigs input)
        }
  where
    getPartialSig =
        (,)
        <$> S.isolate keySize deserialize
        <*> (getVarString <$> deserialize)

getInputItem 0 input@Input{sigHashType = Nothing} InSigHashType = do
    VarInt size <- deserialize
    guard $ size == 0x04
    sigHash <- fromIntegral <$> S.getWord32le
    return $ input { sigHashType = Just sigHash }

getInputItem 0 input@Input{inputRedeemScript = Nothing} InRedeemScript = do
    script <- getSizedBytes deserialize
    return $ input { inputRedeemScript = Just script }

getInputItem 0 input@Input{inputWitnessScript = Nothing} InWitnessScript = do
    script <- getSizedBytes deserialize
    return $ input { inputWitnessScript = Just script }

getInputItem keySize input InBIP32Derivation = do
    (k, v) <- getHDPath keySize
    return
        input
        {
            inputHDKeypaths = HashMap.insert k v (inputHDKeypaths input)
        }

getInputItem 0 input@Input{finalScriptSig = Nothing} InFinalScriptSig = do
    script <- getSizedBytes deserialize
    return $ input { finalScriptSig = Just script }

getInputItem 0 input@Input{finalScriptWitness = Nothing} InFinalScriptWitness = do
    scripts <- map getVarString <$> getVarIntList
    return $ input { finalScriptWitness = Just scripts }
  where
    getVarIntList = do
        VarInt n <- deserialize
        replicateM (fromIntegral n) deserialize

getInputItem keySize input inputType = fail $
    "Incorrect key size for input item or item already existed: " <>
    show (keySize, input, inputType)

getOutputItem :: Int -> Output -> OutputType -> Get Output

getOutputItem 0 output@Output{outputRedeemScript = Nothing} OutRedeemScript = do
    script <- getSizedBytes deserialize
    return $ output { outputRedeemScript = Just script }

getOutputItem 0 output@Output{outputWitnessScript = Nothing} OutWitnessScript = do
    script <- getSizedBytes deserialize
    return $ output { outputWitnessScript = Just script }

getOutputItem keySize output OutBIP32Derivation = do
    (k, v) <- getHDPath keySize
    return $ output { outputHDKeypaths = HashMap.insert k v (outputHDKeypaths output) }

getOutputItem keySize output outputType = fail $
    "Incorrect key size for output item or item already existed: " <>
    show (keySize, output, outputType)

getHDPath :: Int -> Get (PubKeyI, (Fingerprint, [KeyIndex]))
getHDPath keySize =
    (,)
    <$> S.isolate keySize deserialize
    <*> (unPSBTHDPath <$> S.get)

putHDPath :: Enum t => t -> HashMap PubKeyI (Fingerprint, [KeyIndex]) -> Put
putHDPath t = putPubKeyMap S.put t . fmap PSBTHDPath

newtype PSBTHDPath =
    PSBTHDPath { unPSBTHDPath :: (Fingerprint, [KeyIndex]) }
    deriving (Show, Eq, Generic)

instance NFData PSBTHDPath

instance Serialize PSBTHDPath where
    get = do
        VarInt valueSize <- deserialize
        guard $ valueSize `mod` 4 == 0
        let numIndices = (fromIntegral valueSize - 4) `div` 4
        PSBTHDPath <$>
            S.isolate
                (fromIntegral valueSize)
                ((,) <$> S.getWord32le <*> getKeyIndexList numIndices)
      where
        getKeyIndexList n = replicateM n S.getWord32le

    put (PSBTHDPath (fp, kis)) = do
        putVarInt (B.length bs)
        S.putByteString bs
      where
        bs = S.runPut $ S.putWord32le fp >> mapM_ S.putWord32le kis

putPubKeyMap :: Enum t => (a -> Put) -> t -> HashMap PubKeyI a -> Put
putPubKeyMap f t =
    void . HashMap.traverseWithKey putItem
  where
    putItem k v = do
        S.put $ Key (enumWord8 t) (runPutS (serialize k))
        f v

enumWord8 :: Enum a => a -> Word8
enumWord8 = fromIntegral . fromEnum

word8Enum :: forall a. (Bounded a, Enum a) => Word8 -> Either Word8 a
word8Enum n | n <= enumWord8 (maxBound :: a) = Right . toEnum $ fromIntegral n
word8Enum n = Left n

whenJust :: Monad m => (a -> m ()) -> Maybe a -> m ()
whenJust = maybe (return ())