haskoin-core-1.0.0: src/Haskoin/Script/Standard.hs
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE NoFieldSelectors #-}
-- |
-- Module : Haskoin.Script.Standard
-- Copyright : No rights reserved
-- License : MIT
-- Maintainer : jprupp@protonmail.ch
-- Stability : experimental
-- Portability : POSIX
--
-- Standard scripts like pay-to-public-key, pay-to-public-key-hash,
-- pay-to-script-hash, pay-to-multisig and corresponding SegWit variants.
module Haskoin.Script.Standard
( -- * Standard Script Outputs
ScriptOutput (..),
RedeemScript,
isPayPK,
isPayPKHash,
isPayMulSig,
isPayScriptHash,
isPayWitness,
isPayWitnessPKHash,
isPayWitnessScriptHash,
isDataCarrier,
encodeOutput,
decodeOutput,
toP2SH,
toP2WSH,
sortMulSig,
-- * Standard Script Inputs
ScriptInput (..),
SimpleInput (..),
encodeInput,
decodeInput,
isSpendPK,
isSpendPKHash,
isSpendMulSig,
isScriptHashInput,
)
where
import Control.Applicative ((<|>))
import Control.DeepSeq
import Control.Monad (guard, liftM2, (<=<))
import Crypto.Secp256k1
import Data.Aeson (ToJSON (..), Value (..), withText)
import Data.Aeson.Encoding (Encoding, text)
import Data.Aeson.Types (Parser)
import Data.ByteString (ByteString)
import Data.ByteString qualified as B
import Data.Bytes.Get
import Data.Bytes.Put
import Data.Bytes.Serial
import Data.Function (on)
import Data.Hashable
import Data.List (sortBy)
import Data.Maybe (fromJust, isJust)
import Data.Word (Word8)
import GHC.Generics (Generic)
import Haskoin.Crypto.Hash
import Haskoin.Crypto.Keys.Common
import Haskoin.Network.Data
import Haskoin.Script.Common
import Haskoin.Script.SigHash
import Haskoin.Util
-- | Data type describing standard transaction output scripts. Output scripts
-- provide the conditions that must be fulfilled for someone to spend the funds
-- in a transaction output.
data ScriptOutput
= -- | pay to public key
PayPK {key :: !PublicKey}
| -- | pay to public key hash
PayPKHash {hash160 :: !Hash160}
| -- | multisig
PayMulSig
{ keys :: ![PublicKey],
required :: !Int
}
| -- | pay to a script hash
PayScriptHash {hash160 :: !Hash160}
| -- | pay to witness public key hash
PayWitnessPKHash {hash160 :: !Hash160}
| -- | pay to witness script hash
PayWitnessScriptHash {hash256 :: !Hash256}
| -- | another pay to witness address
PayWitness
{ version :: !Word8,
bytes :: !ByteString
}
| -- | provably unspendable data carrier
DataCarrier {bytes :: !ByteString}
deriving (Eq, Show, Read, Generic, NFData)
instance MarshalJSON Ctx ScriptOutput where
unmarshalValue ctx =
withText "ScriptOutput" $ \t ->
case decodeHex t of
Nothing -> fail "Could not decode hex script"
Just bs -> either fail return $ unmarshal ctx bs
marshalValue ctx = String . encodeHex . marshal ctx
marshalEncoding ctx = hexEncoding . runPutL . marshalPut ctx
-- | Is script a pay-to-public-key output?
isPayPK :: ScriptOutput -> Bool
isPayPK (PayPK _) = True
isPayPK _ = False
-- | Is script a pay-to-pub-key-hash output?
isPayPKHash :: ScriptOutput -> Bool
isPayPKHash (PayPKHash _) = True
isPayPKHash _ = False
-- | Is script a pay-to-multi-sig output?
isPayMulSig :: ScriptOutput -> Bool
isPayMulSig (PayMulSig _ _) = True
isPayMulSig _ = False
-- | Is script a pay-to-script-hash output?
isPayScriptHash :: ScriptOutput -> Bool
isPayScriptHash (PayScriptHash _) = True
isPayScriptHash _ = False
-- | Is script a pay-to-witness-pub-key-hash output?
isPayWitnessPKHash :: ScriptOutput -> Bool
isPayWitnessPKHash (PayWitnessPKHash _) = True
isPayWitnessPKHash _ = False
-- | Is script a pay-to-witness-script-hash output?
isPayWitnessScriptHash :: ScriptOutput -> Bool
isPayWitnessScriptHash (PayWitnessScriptHash _) = True
isPayWitnessScriptHash _ = False
-- | Is script paying to a different type of witness address?
isPayWitness :: ScriptOutput -> Bool
isPayWitness (PayWitness _ _) = True
isPayWitness _ = False
-- | Is script a data carrier output?
isDataCarrier :: ScriptOutput -> Bool
isDataCarrier (DataCarrier _) = True
isDataCarrier _ = False
-- | Tries to decode a 'ScriptOutput' from a 'Script'. This can fail if the
-- script is not recognized as any of the standard output types.
decodeOutput :: Ctx -> Script -> Either String ScriptOutput
decodeOutput ctx s = case s.ops of
-- Pay to PubKey
[OP_PUSHDATA bs _, OP_CHECKSIG] ->
PayPK <$> unmarshal ctx bs
-- Pay to PubKey Hash
[OP_DUP, OP_HASH160, OP_PUSHDATA bs _, OP_EQUALVERIFY, OP_CHECKSIG] ->
PayPKHash <$> runGetS deserialize bs
-- Pay to Script Hash
[OP_HASH160, OP_PUSHDATA bs _, OP_EQUAL] ->
PayScriptHash <$> runGetS deserialize bs
-- Pay to Witness
[OP_0, OP_PUSHDATA bs OPCODE]
| B.length bs == 20 ->
PayWitnessPKHash <$> runGetS deserialize bs
| B.length bs == 32 ->
PayWitnessScriptHash <$> runGetS deserialize bs
| B.length bs /= 20 && B.length bs /= 32 ->
Left
"decodeOutput: invalid version 0 segwit \
\(must be 20 or 32 bytes)"
-- Other Witness
[ver, OP_PUSHDATA bs _]
| Just wv <- opWitnessVersion ver,
B.length bs >= 2,
B.length bs <= 40 ->
Right $ PayWitness wv bs
-- Provably unspendable data carrier output
[OP_RETURN, OP_PUSHDATA bs _] -> Right $ DataCarrier bs
-- Pay to MultiSig Keys
_ -> matchPayMulSig ctx s <|> Left "decodeOutput: Non-standard output"
witnessVersionOp :: Word8 -> Maybe ScriptOp
witnessVersionOp 0 = Just OP_0
witnessVersionOp 1 = Just OP_1
witnessVersionOp 2 = Just OP_2
witnessVersionOp 3 = Just OP_3
witnessVersionOp 4 = Just OP_4
witnessVersionOp 5 = Just OP_5
witnessVersionOp 6 = Just OP_6
witnessVersionOp 7 = Just OP_7
witnessVersionOp 8 = Just OP_8
witnessVersionOp 9 = Just OP_9
witnessVersionOp 10 = Just OP_10
witnessVersionOp 11 = Just OP_11
witnessVersionOp 12 = Just OP_12
witnessVersionOp 13 = Just OP_13
witnessVersionOp 14 = Just OP_14
witnessVersionOp 15 = Just OP_15
witnessVersionOp 16 = Just OP_16
witnessVersionOp _ = Nothing
opWitnessVersion :: ScriptOp -> Maybe Word8
opWitnessVersion OP_0 = Just 0
opWitnessVersion OP_1 = Just 1
opWitnessVersion OP_2 = Just 2
opWitnessVersion OP_3 = Just 3
opWitnessVersion OP_4 = Just 4
opWitnessVersion OP_5 = Just 5
opWitnessVersion OP_6 = Just 6
opWitnessVersion OP_7 = Just 7
opWitnessVersion OP_8 = Just 8
opWitnessVersion OP_9 = Just 9
opWitnessVersion OP_10 = Just 10
opWitnessVersion OP_11 = Just 11
opWitnessVersion OP_12 = Just 12
opWitnessVersion OP_13 = Just 13
opWitnessVersion OP_14 = Just 14
opWitnessVersion OP_15 = Just 15
opWitnessVersion OP_16 = Just 16
opWitnessVersion _ = Nothing
-- | Computes a 'Script' from a standard 'ScriptOutput'.
encodeOutput :: Ctx -> ScriptOutput -> Script
encodeOutput ctx s = Script $ case s of
-- Pay to PubKey
(PayPK k) -> [opPushData $ marshal ctx k, OP_CHECKSIG]
-- Pay to PubKey Hash Address
(PayPKHash h) ->
[ OP_DUP,
OP_HASH160,
opPushData $ runPutS $ serialize h,
OP_EQUALVERIFY,
OP_CHECKSIG
]
-- Pay to MultiSig Keys
(PayMulSig ps r)
| r <= length ps ->
let opM = intToScriptOp r
opN = intToScriptOp $ length ps
keys = map (opPushData . marshal ctx) ps
in opM : keys ++ [opN, OP_CHECKMULTISIG]
| otherwise -> error "encodeOutput: PayMulSig r must be <= than pkeys"
-- Pay to Script Hash Address
(PayScriptHash h) ->
[OP_HASH160, opPushData $ runPutS $ serialize h, OP_EQUAL]
-- Pay to Witness PubKey Hash Address
(PayWitnessPKHash h) ->
[OP_0, opPushData $ runPutS $ serialize h]
(PayWitnessScriptHash h) ->
[OP_0, opPushData $ runPutS $ serialize h]
(PayWitness v h) ->
[ case witnessVersionOp v of
Nothing -> error "encodeOutput: invalid witness version"
Just c -> c,
opPushData h
]
-- Provably unspendable output
(DataCarrier d) -> [OP_RETURN, opPushData d]
instance Marshal Ctx ScriptOutput where
marshalGet ctx = do
script <- deserialize
case decodeOutput ctx script of
Left e -> fail e
Right o -> return o
marshalPut ctx = serialize . encodeOutput ctx
-- | Encode script as pay-to-script-hash script
toP2SH :: Script -> ScriptOutput
toP2SH = PayScriptHash . addressHash . runPutS . serialize
-- | Encode script as a pay-to-witness-script-hash script
toP2WSH :: Script -> ScriptOutput
toP2WSH = PayWitnessScriptHash . sha256 . runPutS . serialize
-- | Match @[OP_N, PubKey1, ..., PubKeyM, OP_M, OP_CHECKMULTISIG]@
matchPayMulSig :: Ctx -> Script -> Either String ScriptOutput
matchPayMulSig ctx (Script ops) = case splitAt (length ops - 2) ops of
(m : xs, [n, OP_CHECKMULTISIG]) -> do
(intM, intN) <- liftM2 (,) (scriptOpToInt m) (scriptOpToInt n)
if intM <= intN && length xs == intN
then liftM2 PayMulSig (go xs) (return intM)
else Left "matchPayMulSig: Invalid M or N parameters"
_ -> Left "matchPayMulSig: script did not match output template"
where
go (OP_PUSHDATA bs _ : xs) =
liftM2 (:) (unmarshal ctx bs) (go xs)
go [] =
Right []
go _ =
Left "matchPayMulSig: invalid multisig opcode"
-- | Sort the public keys of a multisig output in ascending order by comparing
-- their compressed serialized representations. Refer to BIP-67.
sortMulSig :: Ctx -> ScriptOutput -> ScriptOutput
sortMulSig ctx out = case out of
PayMulSig keys r ->
PayMulSig
(sortBy (compare `on` marshal ctx) keys)
r
_ -> error "Can only call orderMulSig on PayMulSig scripts"
-- | Data type describing standard transaction input scripts. Input scripts
-- provide the signing data required to unlock the coins of the output they are
-- trying to spend, except in pay-to-witness-public-key-hash and
-- pay-to-script-hash transactions.
data SimpleInput
= SpendPK
{ -- | transaction signature
signature :: !TxSignature
}
| SpendPKHash
{ -- | embedded signature
signature :: !TxSignature,
-- | public key
key :: !PublicKey
}
| SpendMulSig
{ -- | list of signatures
signatures :: ![TxSignature]
}
deriving (Eq, Show, Read, Generic, NFData)
-- | Returns true if the input script is spending from a pay-to-public-key
-- output.
isSpendPK :: ScriptInput -> Bool
isSpendPK (RegularInput (SpendPK _)) = True
isSpendPK _ = False
-- | Returns true if the input script is spending from a pay-to-public-key-hash
-- output.
isSpendPKHash :: ScriptInput -> Bool
isSpendPKHash (RegularInput (SpendPKHash _ _)) = True
isSpendPKHash _ = False
-- | Returns true if the input script is spending a multisig output.
isSpendMulSig :: ScriptInput -> Bool
isSpendMulSig (RegularInput (SpendMulSig _)) = True
isSpendMulSig _ = False
-- | Returns true if the input script is spending a pay-to-script-hash output.
isScriptHashInput :: ScriptInput -> Bool
isScriptHashInput (ScriptHashInput _ _) = True
isScriptHashInput _ = False
-- | A redeem script is the output script serialized into the spending input
-- script. It must be included in inputs that spend pay-to-script-hash outputs.
type RedeemScript = ScriptOutput
-- | Standard input script high-level representation.
data ScriptInput
= RegularInput
{ -- | get wrapped simple input
get :: !SimpleInput
}
| ScriptHashInput
{ -- | get simple input associated with redeem script
get :: !SimpleInput,
-- | redeem script
redeem :: !RedeemScript
}
deriving (Show, Read, Eq, Generic, NFData)
-- | Heuristic to decode an input script into one of the standard types.
decodeSimpleInput :: Network -> Ctx -> Script -> Either String SimpleInput
decodeSimpleInput net ctx (Script ops) =
maybeToEither errMsg $ matchPK ops <|> matchPKHash ops <|> matchMulSig ops
where
matchPK [op] = SpendPK <$> f op
matchPK _ = Nothing
matchPKHash [op, OP_PUSHDATA pub _] =
SpendPKHash <$> f op <*> eitherToMaybe (unmarshal ctx pub)
matchPKHash _ = Nothing
matchMulSig (x : xs) = do
guard $ x == OP_0
SpendMulSig <$> mapM f xs
matchMulSig _ = Nothing
f OP_0 = return TxSignatureEmpty
f (OP_PUSHDATA "" OPCODE) = f OP_0
f (OP_PUSHDATA bs _) = eitherToMaybe $ decodeTxSig net ctx bs
f _ = Nothing
errMsg = "decodeInput: Could not decode script input"
-- | Heuristic to decode a 'ScriptInput' from a 'Script'. This function fails if
-- the script can not be parsed as a standard script input.
decodeInput :: Network -> Ctx -> Script -> Either String ScriptInput
decodeInput net ctx s@(Script ops) =
maybeToEither errMsg $ matchSimpleInput <|> matchPayScriptHash
where
matchSimpleInput =
RegularInput <$> eitherToMaybe (decodeSimpleInput net ctx s)
matchPayScriptHash =
case splitAt (length s.ops - 1) ops of
(is, [OP_PUSHDATA bs _]) -> do
rdm <- eitherToMaybe $ unmarshal ctx bs
inp <- eitherToMaybe $ decodeSimpleInput net ctx $ Script is
return $ ScriptHashInput inp rdm
_ -> Nothing
errMsg = "decodeInput: Could not decode script input"
instance Marshal (Network, Ctx) ScriptInput where
marshalGet (net, ctx) =
deserialize >>= either fail return . decodeInput net ctx
marshalPut (net, ctx) =
serialize . encodeInput net ctx
-- | Encode a standard input into a script.
encodeInput :: Network -> Ctx -> ScriptInput -> Script
encodeInput net ctx s = case s of
RegularInput ri -> encodeSimpleInput net ctx ri
ScriptHashInput i o ->
Script $ (encodeSimpleInput net ctx i).ops ++ [opPushData $ marshal ctx o]
-- | Encode a standard 'SimpleInput' into opcodes as an input 'Script'.
encodeSimpleInput :: Network -> Ctx -> SimpleInput -> Script
encodeSimpleInput net ctx s =
Script $
case s of
SpendPK ts -> [f ts]
SpendPKHash ts p -> [f ts, opPushData $ marshal ctx p]
SpendMulSig xs -> OP_0 : map f xs
where
f TxSignatureEmpty = OP_0
f ts = opPushData $ encodeTxSig net ctx ts