packages feed

haskoin-core-0.21.0: test/Haskoin/ScriptSpec.hs

{-# LANGUAGE OverloadedStrings #-}

module Haskoin.ScriptSpec (spec) where

import Control.Monad
import Data.Aeson as A
import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import Data.Bytes.Get
import Data.Bytes.Put
import Data.Bytes.Serial
import Data.Either
import Data.List
import Data.Maybe
import Data.String
import Data.String.Conversions (cs)
import Data.Text (Text)
import Data.Word
import Haskoin.Address
import Haskoin.Constants
import Haskoin.Data
import Haskoin.Keys
import Haskoin.Script
import Haskoin.Transaction
import Haskoin.Util
import Haskoin.Util.Arbitrary
import Haskoin.UtilSpec (readTestFile)
import Test.HUnit as HUnit
import Test.Hspec
import Test.Hspec.QuickCheck
import Test.QuickCheck
import Text.Read

serialVals :: [SerialBox]
serialVals =
    [ SerialBox arbitraryScriptOp
    , SerialBox arbitraryScript
    ]

readVals :: [ReadBox]
readVals =
    [ ReadBox arbitrarySigHash
    , ReadBox arbitrarySigHashFlag
    , ReadBox arbitraryScript
    , ReadBox arbitraryPushDataType
    , ReadBox arbitraryScriptOp
    , ReadBox (arbitraryScriptOutput =<< arbitraryNetwork)
    ]

jsonVals :: [JsonBox]
jsonVals =
    [ JsonBox $ arbitraryScriptOutput =<< arbitraryNetwork
    , JsonBox arbitraryOutPoint
    , JsonBox arbitrarySigHash
    , JsonBox $ fst <$> (arbitrarySigInput =<< arbitraryNetwork)
    ]

spec :: Spec
spec = do
    testIdentity serialVals readVals jsonVals []
    describe "btc scripts" $ props btc
    describe "bch scripts" $ props bch
    describe "multi signatures" $
        zipWithM_ (curry mapMulSigVector) mulSigVectors [0 ..]
    describe "signature decoding" $
        zipWithM_ (curry (sigDecodeMap btc)) scriptSigSignatures [0 ..]
    describe "SigHashFlag fromEnum/toEnum" $
        prop "fromEnum/toEnum" $
            forAll arbitrarySigHashFlag $ \f -> toEnum (fromEnum f) `shouldBe` f
    describe "Script vectors" $
        it "Can encode script vectors" encodeScriptVector

props :: Network -> Spec
props net = do
    standardSpec net
    strictSigSpec net
    scriptSpec net
    txSigHashForkIdSpec net
    forkIdScriptSpec net
    sigHashSpec net
    txSigHashSpec net

standardSpec :: Network -> Spec
standardSpec net = do
    prop "has intToScriptOp . scriptOpToInt identity" $
        forAll arbitraryIntScriptOp $ \i ->
            intToScriptOp <$> scriptOpToInt i `shouldBe` Right i
    prop "has decodeOutput . encodeOutput identity" $
        forAll (arbitraryScriptOutput net) $ \so ->
            decodeOutput (encodeOutput so) `shouldBe` Right so
    prop "has decodeInput . encodeOutput identity" $
        forAll (arbitraryScriptInput net) $ \si ->
            decodeInput net (encodeInput si) `shouldBe` Right si
    prop "can sort multisig scripts" $
        forAll arbitraryMSOutput $ \out ->
            map
                (runPutS . serialize)
                (getOutputMulSigKeys (sortMulSig out))
                `shouldSatisfy` \xs -> xs == sort xs
    it "can decode inputs with empty signatures" $ do
        decodeInput net (Script [OP_0])
            `shouldBe` Right (RegularInput (SpendPK TxSignatureEmpty))
        decodeInput net (Script [opPushData ""])
            `shouldBe` Right (RegularInput (SpendPK TxSignatureEmpty))
        let pk =
                derivePubKeyI $
                    wrapSecKey True $ fromJust $ secKey $ B.replicate 32 1
        decodeInput net (Script [OP_0, opPushData $ runPutS $ serialize pk])
            `shouldBe` Right (RegularInput (SpendPKHash TxSignatureEmpty pk))
        decodeInput net (Script [OP_0, OP_0])
            `shouldBe` Right (RegularInput (SpendMulSig [TxSignatureEmpty]))
        decodeInput net (Script [OP_0, OP_0, OP_0, OP_0])
            `shouldBe` Right (RegularInput (SpendMulSig $ replicate 3 TxSignatureEmpty))

scriptSpec :: Network -> Spec
scriptSpec net =
    when (getNetworkName net == "btc") $
        it "can verify standard scripts from script_tests.json file" $ do
            xs <- readTestFile "script_tests.json" :: IO [A.Value]
            let vectorsA =
                    mapMaybe (A.decode . A.encode) xs ::
                        [ ( String
                          , String
                          , String
                          , String
                          , String
                          )
                        ]
                vectorsB =
                    mapMaybe (A.decode . A.encode) xs ::
                        [ ( [Word64]
                          , String
                          , String
                          , String
                          , String
                          , String
                          )
                        ]
                vectors =
                    map (\(a, b, c, d, e) -> ([0], a, b, c, d, e)) vectorsA
                        <> vectorsB
            length vectors `shouldBe` 86
            forM_ vectors $ \([val], siStr, soStr, flags, res, desc) ->
                -- We can disable specific tests by adding a DISABLED flag in the data

                unless ("DISABLED" `isInfixOf` flags) $ do
                    let _strict =
                            "DERSIG" `isInfixOf` flags
                                || "STRICTENC" `isInfixOf` flags
                                || "NULLDUMMY" `isInfixOf` flags
                        scriptSig = parseScript siStr
                        scriptPubKey = parseScript soStr
                        decodedOutput = decodeOutputBS scriptPubKey
                        ver = either (const False) $ \so ->
                            verifyStdInput
                                net
                                (spendTx scriptPubKey 0 scriptSig)
                                0
                                so
                                (val * 100000000)
                    case res of
                        "OK" -> assertBool desc $ ver decodedOutput
                        _ -> assertBool desc (not $ ver decodedOutput)

forkIdScriptSpec :: Network -> Spec
forkIdScriptSpec net =
    when (isJust (getSigHashForkId net)) $
        it "can verify scripts from forkid_script_tests.json file" $ do
            xs <- readTestFile "forkid_script_tests.json" :: IO [A.Value]
            let vectors =
                    mapMaybe (A.decode . A.encode) xs ::
                        [ ( [Word64]
                          , String
                          , String
                          , String
                          , String
                          , String
                          )
                        ]
            length vectors `shouldBe` 3
            forM_ vectors $ \([valBTC], siStr, soStr, _, res, _) -> do
                let val = valBTC * 100000000
                    scriptSig = parseScript siStr
                    scriptPubKey = parseScript soStr
                    decodedOutput =
                        fromRight (error $ "Could not decode output: " <> soStr) $
                            decodeOutputBS scriptPubKey
                    ver =
                        verifyStdInput
                            net
                            (spendTx scriptPubKey val scriptSig)
                            0
                            decodedOutput
                            val
                case res of
                    "OK" -> ver `shouldBe` True
                    _ -> ver `shouldBe` False

creditTx :: ByteString -> Word64 -> Tx
creditTx scriptPubKey val =
    Tx 1 [txI] [txO] [] 0
  where
    txO = TxOut{outValue = val, scriptOutput = scriptPubKey}
    txI =
        TxIn
            { prevOutput = nullOutPoint
            , scriptInput = runPutS $ serialize $ Script [OP_0, OP_0]
            , txInSequence = maxBound
            }

spendTx :: ByteString -> Word64 -> ByteString -> Tx
spendTx scriptPubKey val scriptSig =
    Tx 1 [txI] [txO] [] 0
  where
    txO = TxOut{outValue = val, scriptOutput = B.empty}
    txI =
        TxIn
            { prevOutput = OutPoint (txHash $ creditTx scriptPubKey val) 0
            , scriptInput = scriptSig
            , txInSequence = maxBound
            }

parseScript :: String -> ByteString
parseScript str =
    B.concat $ fromMaybe err $ mapM f $ words str
  where
    f = decodeHex . cs . dropHex . replaceToken
    dropHex ('0' : 'x' : xs) = xs
    dropHex xs = xs
    err = error $ "Could not decode script: " <> str

replaceToken :: String -> String
replaceToken str = case readMaybe $ "OP_" <> str of
    Just opcode -> "0x" <> cs (encodeHex $ runPutS $ serialize (opcode :: ScriptOp))
    _ -> str

strictSigSpec :: Network -> Spec
strictSigSpec net =
    when (getNetworkName net == "btc") $ do
        it "can decode strict signatures" $ do
            xs <- readTestFile "sig_strict.json"
            let vectors = mapMaybe decodeHex xs
            length vectors `shouldBe` 3
            forM_ vectors $ \sig ->
                decodeTxSig net sig `shouldSatisfy` isRight
        it "can detect non-strict signatures" $ do
            xs <- readTestFile "sig_nonstrict.json"
            let vectors = mapMaybe decodeHex xs
            length vectors `shouldBe` 17
            forM_ vectors $ \sig ->
                decodeTxSig net sig `shouldSatisfy` isLeft

txSigHashSpec :: Network -> Spec
txSigHashSpec net =
    when (getNetworkName net == "btc") $
        it "can produce valid sighashes from sighash.json test vectors" $ do
            xs <- readTestFile "sighash.json" :: IO [A.Value]
            let vectors =
                    mapMaybe (A.decode . A.encode) xs ::
                        [ ( String
                          , String
                          , Int
                          , Integer
                          , String
                          )
                        ]
            length vectors `shouldBe` 500
            forM_ vectors $ \(txStr, scpStr, i, shI, resStr) -> do
                let tx = fromString txStr
                    s =
                        fromMaybe (error $ "Could not decode script: " <> cs scpStr) $
                            eitherToMaybe . runGetS deserialize =<< decodeHex (cs scpStr)
                    sh = fromIntegral shI
                    res =
                        eitherToMaybe . runGetS deserialize . B.reverse
                            =<< decodeHex (cs resStr)
                Just (txSigHash net tx s 0 i sh) `shouldBe` res

txSigHashForkIdSpec :: Network -> Spec
txSigHashForkIdSpec net =
    when (getNetworkName net == "btc") $
        it "can produce valid sighashes from forkid_sighash.json test vectors" $ do
            xs <- readTestFile "forkid_sighash.json" :: IO [A.Value]
            let vectors =
                    mapMaybe (A.decode . A.encode) xs ::
                        [ ( String
                          , String
                          , Int
                          , Word64
                          , Integer
                          , String
                          )
                        ]
            length vectors `shouldBe` 13
            forM_ vectors $ \(txStr, scpStr, i, val, shI, resStr) -> do
                let tx = fromString txStr
                    s =
                        fromMaybe (error $ "Could not decode script: " <> cs scpStr) $
                            eitherToMaybe . runGetS deserialize =<< decodeHex (cs scpStr)
                    sh = fromIntegral shI
                    res = eitherToMaybe . runGetS deserialize =<< decodeHex (cs resStr)
                Just (txSigHashForkId net tx s val i sh) `shouldBe` res

sigHashSpec :: Network -> Spec
sigHashSpec net = do
    it "can correctly show" $ do
        show (0x00 :: SigHash) `shouldBe` "SigHash " <> show (0x00 :: Word32)
        show (0x01 :: SigHash) `shouldBe` "SigHash " <> show (0x01 :: Word32)
        show (0xff :: SigHash) `shouldBe` "SigHash " <> show (0xff :: Word32)
        show (0xabac3344 :: SigHash) `shouldBe` "SigHash "
            <> show (0xabac3344 :: Word32)
    it "can add a forkid" $ do
        0x00 `sigHashAddForkId` 0x00 `shouldBe` 0x00
        0xff `sigHashAddForkId` 0x00ffffff `shouldBe` 0xffffffff
        0xffff `sigHashAddForkId` 0x00aaaaaa `shouldBe` 0xaaaaaaff
        0xffff `sigHashAddForkId` 0xaaaaaaaa `shouldBe` 0xaaaaaaff
        0xffff `sigHashAddForkId` 0x00004444 `shouldBe` 0x004444ff
        0xff01 `sigHashAddForkId` 0x44440000 `shouldBe` 0x44000001
        0xff03 `sigHashAddForkId` 0x00550000 `shouldBe` 0x55000003
    it "can extract a forkid" $ do
        sigHashGetForkId 0x00000000 `shouldBe` 0x00000000
        sigHashGetForkId 0x80000000 `shouldBe` 0x00800000
        sigHashGetForkId 0xffffffff `shouldBe` 0x00ffffff
        sigHashGetForkId 0xabac3403 `shouldBe` 0x00abac34
    it "can build some vectors" $ do
        sigHashAll `shouldBe` 0x01
        sigHashNone `shouldBe` 0x02
        sigHashSingle `shouldBe` 0x03
        setForkIdFlag sigHashAll `shouldBe` 0x41
        setAnyoneCanPayFlag sigHashAll `shouldBe` 0x81
        setAnyoneCanPayFlag (setForkIdFlag sigHashAll) `shouldBe` 0xc1
    it "can test flags" $ do
        hasForkIdFlag sigHashAll `shouldBe` False
        hasForkIdFlag (setForkIdFlag sigHashAll) `shouldBe` True
        hasAnyoneCanPayFlag sigHashAll `shouldBe` False
        hasAnyoneCanPayFlag (setAnyoneCanPayFlag sigHashAll) `shouldBe` True
        isSigHashAll sigHashNone `shouldBe` False
        isSigHashAll sigHashAll `shouldBe` True
        isSigHashNone sigHashSingle `shouldBe` False
        isSigHashNone sigHashNone `shouldBe` True
        isSigHashSingle sigHashAll `shouldBe` False
        isSigHashSingle sigHashSingle `shouldBe` True
        isSigHashUnknown sigHashAll `shouldBe` False
        isSigHashUnknown sigHashNone `shouldBe` False
        isSigHashUnknown sigHashSingle `shouldBe` False
        isSigHashUnknown 0x00 `shouldBe` True
        isSigHashUnknown 0x04 `shouldBe` True
    it "can decodeTxSig . encode a TxSignature" $
        property $
            forAll (arbitraryTxSignature net) $ \(_, _, ts) ->
                decodeTxSig net (encodeTxSig ts) `shouldBe` Right ts
    it "can produce the sighash one" $
        property $
            forAll (arbitraryTx net) $ forAll arbitraryScript . testSigHashOne net

testSigHashOne :: Network -> Tx -> Script -> Word64 -> Bool -> Property
testSigHashOne net tx s val acp =
    not (null $ txIn tx)
        ==> if length (txIn tx) > length (txOut tx)
            then res `shouldBe` one
            else res `shouldNotBe` one
  where
    res = txSigHash net tx s val (length (txIn tx) - 1) (f sigHashSingle)
    one = "0100000000000000000000000000000000000000000000000000000000000000"
    f =
        if acp
            then setAnyoneCanPayFlag
            else id

{- Parse tests from bitcoin-qt repository -}

mapMulSigVector :: ((Text, Text), Int) -> Spec
mapMulSigVector (v, i) =
    it name $ runMulSigVector v
  where
    name = "check multisig vector " <> show i

runMulSigVector :: (Text, Text) -> Assertion
runMulSigVector (a, ops) = assertBool "multisig vector" $ Just a == b
  where
    s = do
        s' <- decodeHex ops
        eitherToMaybe $ runGetS deserialize s'
    b = do
        o <- s
        d <- eitherToMaybe $ decodeOutput o
        addrToText btc $ payToScriptAddress d

sigDecodeMap :: Network -> (Text, Int) -> Spec
sigDecodeMap net (_, i) =
    it ("check signature " ++ show i) func
  where
    func = testSigDecode net $ scriptSigSignatures !! i

testSigDecode :: Network -> Text -> Assertion
testSigDecode net str =
    let bs = fromJust $ decodeHex str
        eitherSig = decodeTxSig net bs
     in assertBool
            ( unwords
                [ "Decode failed:"
                , fromLeft (error "Decode did not fail") eitherSig
                ]
            )
            $ isRight eitherSig

mulSigVectors :: [(Text, Text)]
mulSigVectors =
    [
        ( "3QJmV3qfvL9SuYo34YihAf3sRCW3qSinyC"
        , "52410491bba2510912a5bd37da1fb5b1673010e43d2c6d812c514e91bfa9f2eb\
          \129e1c183329db55bd868e209aac2fbc02cb33d98fe74bf23f0c235d6126b1d8\
          \334f864104865c40293a680cb9c020e7b1e106d8c1916d3cef99aa431a56d253\
          \e69256dac09ef122b1a986818a7cb624532f062c1d1f8722084861c5c3291ccf\
          \fef4ec687441048d2455d2403e08708fc1f556002f1b6cd83f992d085097f997\
          \4ab08a28838f07896fbab08f39495e15fa6fad6edbfb1e754e35fa1c7844c41f\
          \322a1863d4621353ae"
        )
    ]

scriptSigSignatures :: [Text]
scriptSigSignatures =
    -- Signature in input of txid
    -- 1983a69265920c24f89aac81942b1a59f7eb30821a8b3fb258f88882b6336053
    [ "304402205ca6249f43538908151fe67b26d020306c0e59fa206cf9f3ccf641f333\
      \57119d02206c82f244d04ac0a48024fb9cc246b66e58598acf206139bdb7b75a29\
      \41a2b1e401"
      -- Signature in input of txid
      -- fb0a1d8d34fa5537e461ac384bac761125e1bfa7fec286fa72511240fa66864d.
      -- Strange DER sizes, but in Blockchain. Now invalid as Haskoin can only
      -- decode strict signatures.
      -- "3048022200002b83d59c1d23c08efd82ee0662fec23309c3adbcbd1f0b8695378d\
      -- \b4b14e736602220000334a96676e58b1bb01784cb7c556dd8ce1c220171904da22\
      -- \e18fe1e7d1510db501"
    ]

encodeScriptVector :: Assertion
encodeScriptVector =
    assertEqual "Encode script" res (encodeHex $ runPutS $ serialize s)
  where
    res =
        "514104cc71eb30d653c0c3163990c47b976f3fb3f37cccdcbedb169a1dfef58b\
        \bfbfaff7d8a473e7e2e6d317b87bafe8bde97e3cf8f065dec022b51d11fcdd0d\
        \348ac4410461cbdcc5409fb4b4d42b51d33381354d80e550078cb532a34bfa2f\
        \cfdeb7d76519aecc62770f5b0e4ef8551946d8a540911abe3e7854a26f39f58b\
        \25c15342af52ae"
    s =
        Script
            [ OP_1
            , opPushData $
                d
                    "04cc71eb30d653c0c3163990c47b976f3fb3f37cccdcbedb169a1dfef5\
                    \8bbfbfaff7d8a473e7e2e6d317b87bafe8bde97e3cf8f065dec022b51d\
                    \11fcdd0d348ac4"
            , opPushData $
                d
                    "0461cbdcc5409fb4b4d42b51d33381354d80e550078cb532a34bfa2fcf\
                    \deb7d76519aecc62770f5b0e4ef8551946d8a540911abe3e7854a26f39\
                    \f58b25c15342af"
            , OP_2
            , OP_CHECKMULTISIG
            ]
    d = fromJust . decodeHex