packages feed

haskoin-core-0.21.0: src/Haskoin/Transaction/Builder/Sign.hs

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}

{- |
Module      : Haskoin.Transaction.Builder.Sign
Copyright   : No rights reserved
License     : MIT
Maintainer  : jprupp@protonmail.ch
Stability   : experimental
Portability : POSIX

Types and logic for signing transactions.
-}
module Haskoin.Transaction.Builder.Sign (
    SigInput (..),
    makeSignature,
    makeSigHash,
    signTx,
    findInputIndex,
    signInput,
    buildInput,
    sigKeys,
) where

import Control.DeepSeq (NFData)
import Control.Monad (foldM, when)
import Data.Aeson (
    FromJSON,
    ToJSON (..),
    object,
    pairs,
    parseJSON,
    withObject,
    (.:),
    (.:?),
    (.=),
 )
import Data.Bytes.Get
import Data.Bytes.Put
import Data.Bytes.Serial
import Data.Either (rights)
import Data.Hashable (Hashable)
import Data.List (find, nub)
import Data.Maybe (
    catMaybes,
    fromMaybe,
    mapMaybe,
    maybeToList,
 )
import Data.Word (Word64)
import GHC.Generics (Generic)
import Haskoin.Address (getAddrHash160, pubKeyAddr)
import Haskoin.Crypto (Hash256, SecKey)
import Haskoin.Crypto.Signature (signHash, verifyHashSig)
import Haskoin.Data (Network)
import Haskoin.Keys.Common (
    PubKeyI (..),
    SecKeyI (..),
    derivePubKeyI,
    wrapSecKey,
 )
import Haskoin.Script
import Haskoin.Transaction.Common
import Haskoin.Transaction.Segwit
import Haskoin.Util (matchTemplate, updateIndex)

{- | Data type used to specify the signing parameters of a transaction input.
 To sign an input, the previous output script, outpoint and sighash are
 required. When signing a pay to script hash output, an additional redeem
 script is required.
-}
data SigInput = SigInput
    { -- | output script to spend
      -- ^ output script value
      sigInputScript :: !ScriptOutput
    , -- | output script value
      -- ^ outpoint to spend
      sigInputValue :: !Word64
    , -- | outpoint to spend
      -- ^ signature type
      sigInputOP :: !OutPoint
    , -- | signature type
      -- ^ redeem script
      sigInputSH :: !SigHash
    , -- | redeem script
      sigInputRedeem :: !(Maybe RedeemScript)
    }
    deriving (Eq, Show, Read, Generic, Hashable, NFData)

instance ToJSON SigInput where
    toJSON (SigInput so val op sh rdm) =
        object $
            [ "pkscript" .= so
            , "value" .= val
            , "outpoint" .= op
            , "sighash" .= sh
            ]
                ++ ["redeem" .= r | r <- maybeToList rdm]
    toEncoding (SigInput so val op sh rdm) =
        pairs $
            "pkscript" .= so
                <> "value" .= val
                <> "outpoint" .= op
                <> "sighash" .= sh
                <> maybe mempty ("redeem" .=) rdm

instance FromJSON SigInput where
    parseJSON =
        withObject "SigInput" $ \o ->
            SigInput <$> o .: "pkscript"
                <*> o .: "value"
                <*> o .: "outpoint"
                <*> o .: "sighash"
                <*> o .:? "redeem"

{- | Sign a transaction by providing the 'SigInput' signing parameters and a
 list of private keys. The signature is computed deterministically as defined
 in RFC-6979.
-}
signTx ::
    Network ->
    -- | transaction to sign
    Tx ->
    -- | signing parameters, with nesting flag
    [(SigInput, Bool)] ->
    -- | private keys to sign with
    [SecKey] ->
    -- | signed transaction
    Either String Tx
signTx net otx sigis allKeys
    | null ti = Left "signTx: Transaction has no inputs"
    | otherwise = foldM go otx $ findInputIndex (sigInputOP . fst) sigis ti
  where
    ti = txIn otx
    go tx (sigi@(SigInput so _ _ _ rdmM, _), i) = do
        keys <- sigKeys so rdmM allKeys
        foldM (\t k -> signInput net t i sigi k) tx keys

{- | Sign a single input in a transaction deterministically (RFC-6979).  The
 nesting flag only affects the behavior of segwit inputs.
-}
signInput ::
    Network ->
    Tx ->
    Int ->
    -- | boolean flag: nest input
    (SigInput, Bool) ->
    SecKeyI ->
    Either String Tx
signInput net tx i (sigIn@(SigInput so val _ _ rdmM), nest) key = do
    let sig = makeSignature net tx i sigIn key
    si <- buildInput net tx i so val rdmM sig $ derivePubKeyI key
    w <- updatedWitnessData tx i so si
    return
        tx
            { txIn = nextTxIn so si
            , txWitness = w
            }
  where
    f si x = x{scriptInput = encodeInputBS si}
    g so' x = x{scriptInput = runPutS . serialize . opPushData $ encodeOutputBS so'}
    txis = txIn tx
    nextTxIn so' si
        | isSegwit so' && nest = updateIndex i txis (g so')
        | isSegwit so' = txIn tx
        | otherwise = updateIndex i txis (f si)

{- | Add the witness data of the transaction given segwit parameters for an input.

 @since 0.11.0.0
-}
updatedWitnessData :: Tx -> Int -> ScriptOutput -> ScriptInput -> Either String WitnessData
updatedWitnessData tx i so si
    | isSegwit so = updateWitness . toWitnessStack =<< calcWitnessProgram so si
    | otherwise = return $ txWitness tx
  where
    updateWitness w
        | null $ txWitness tx = return $ updateIndex i defaultStack (const w)
        | length (txWitness tx) /= n = Left "Invalid number of witness stacks"
        | otherwise = return $ updateIndex i (txWitness tx) (const w)
    defaultStack = replicate n $ toWitnessStack EmptyWitnessProgram
    n = length $ txIn tx

-- | Associate an input index to each value in a list
findInputIndex ::
    -- | extract an outpoint
    (a -> OutPoint) ->
    -- | input list
    [a] ->
    -- | reference list of inputs
    [TxIn] ->
    [(a, Int)]
findInputIndex getOutPoint as ti =
    mapMaybe g $ zip (matchTemplate as ti f) [0 ..]
  where
    f s txin = getOutPoint s == prevOutput txin
    g (Just s, i) = Just (s, i)
    g (Nothing, _) = Nothing

{- | Find from the list of provided private keys which one is required to sign
 the 'ScriptOutput'.
-}
sigKeys ::
    ScriptOutput ->
    Maybe RedeemScript ->
    [SecKey] ->
    Either String [SecKeyI]
sigKeys so rdmM keys =
    case (so, rdmM) of
        (PayPK p, Nothing) ->
            return . map fst . maybeToList $ find ((== p) . snd) zipKeys
        (PayPKHash h, Nothing) -> return $ keyByHash h
        (PayMulSig ps r, Nothing) ->
            return $ map fst $ take r $ filter ((`elem` ps) . snd) zipKeys
        (PayScriptHash _, Just rdm) -> sigKeys rdm Nothing keys
        (PayWitnessPKHash h, _) -> return $ keyByHash h
        (PayWitnessScriptHash _, Just rdm) -> sigKeys rdm Nothing keys
        _ -> Left "sigKeys: Could not decode output script"
  where
    zipKeys =
        [ (prv, pub)
        | k <- keys
        , t <- [True, False]
        , let prv = wrapSecKey t k
        , let pub = derivePubKeyI prv
        ]
    keyByHash h = fmap fst . maybeToList . findKey h $ zipKeys
    findKey h = find $ (== h) . getAddrHash160 . pubKeyAddr . snd

{- | Construct an input for a transaction given a signature, public key and data
 about the previous output.
-}
buildInput ::
    Network ->
    -- | transaction where input will be added
    Tx ->
    -- | input index where signature will go
    Int ->
    -- | output script being spent
    ScriptOutput ->
    -- | amount of previous output
    Word64 ->
    -- | redeem script if pay-to-script-hash
    Maybe RedeemScript ->
    TxSignature ->
    PubKeyI ->
    Either String ScriptInput
buildInput net tx i so val rdmM sig pub = do
    when (i >= length (txIn tx)) $ Left "buildInput: Invalid input index"
    case (so, rdmM) of
        (PayScriptHash _, Just rdm) -> buildScriptHashInput rdm
        (PayWitnessScriptHash _, Just rdm) -> buildScriptHashInput rdm
        (PayWitnessPKHash _, Nothing) -> return . RegularInput $ SpendPKHash sig pub
        (_, Nothing) -> buildRegularInput so
        _ -> Left "buildInput: Invalid output/redeem script combination"
  where
    buildRegularInput = \case
        PayPK _ -> return $ RegularInput $ SpendPK sig
        PayPKHash _ -> return $ RegularInput $ SpendPKHash sig pub
        PayMulSig msPubs r -> do
            let mSigs = take r $ catMaybes $ matchTemplate allSigs msPubs f
                allSigs = nub $ sig : parseExistingSigs net tx so i
            return $ RegularInput $ SpendMulSig mSigs
        _ -> Left "buildInput: Invalid output/redeem script combination"
    buildScriptHashInput rdm = do
        inp <- buildRegularInput rdm
        return $ ScriptHashInput (getRegularInput inp) rdm
    f (TxSignature x sh) p =
        verifyHashSig (makeSigHash net tx i so val sh rdmM) x (pubKeyPoint p)
    f TxSignatureEmpty _ = False

{- | Apply heuristics to extract the signatures for a particular input that are
 embedded in the transaction.

 @since 0.11.0.0
-}
parseExistingSigs :: Network -> Tx -> ScriptOutput -> Int -> [TxSignature]
parseExistingSigs net tx so i = insSigs <> witSigs
  where
    insSigs = case decodeInputBS net scp of
        Right (ScriptHashInput (SpendMulSig xs) _) -> xs
        Right (RegularInput (SpendMulSig xs)) -> xs
        _ -> []
    scp = scriptInput $ txIn tx !! i
    witSigs
        | not $ isSegwit so = []
        | null $ txWitness tx = []
        | otherwise = rights $ decodeTxSig net <$> (txWitness tx !! i)

-- | Produce a structured representation of a deterministic (RFC-6979) signature over an input.
makeSignature :: Network -> Tx -> Int -> SigInput -> SecKeyI -> TxSignature
makeSignature net tx i (SigInput so val _ sh rdmM) key =
    TxSignature (signHash (secKeyData key) m) sh
  where
    m = makeSigHash net tx i so val sh rdmM

{- | A function which selects the digest algorithm and parameters as appropriate

 @since 0.11.0.0
-}
makeSigHash ::
    Network ->
    Tx ->
    Int ->
    ScriptOutput ->
    Word64 ->
    SigHash ->
    Maybe RedeemScript ->
    Hash256
makeSigHash net tx i so val sh rdmM = h net tx (encodeOutput so') val i sh
  where
    so' = case so of
        PayWitnessPKHash h' -> PayPKHash h'
        _ -> fromMaybe so rdmM
    h
        | isSegwit so = txSigHashForkId
        | otherwise = txSigHash