packages feed

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

{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings #-}

module Haskoin.ScriptSpec (spec) where

import Control.Monad
import Data.Aeson as A
import Data.ByteString (ByteString)
import Data.ByteString qualified as B
import Data.Bytes.Get
import Data.Bytes.Put
import Data.Bytes.Serial
import Data.Default (def)
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.Crypto
import Haskoin.Network.Constants
import Haskoin.Network.Data
import Haskoin.Script
import Haskoin.Transaction
import Haskoin.Util
import Haskoin.Util.Arbitrary
import Test.HUnit as HUnit
import Test.Hspec
import Test.Hspec.QuickCheck
import Test.QuickCheck
import Text.Read

identityTests :: Ctx -> IdentityTests
identityTests ctx =
  def
    { readTests =
        [ ReadBox arbitrarySigHash,
          ReadBox arbitrarySigHashFlag,
          ReadBox arbitraryScript,
          ReadBox arbitraryPushDataType,
          ReadBox arbitraryScriptOp,
          ReadBox ((`arbitraryScriptOutput` ctx) =<< arbitraryNetwork),
          ReadBox ((`arbitraryScriptInput` ctx) =<< arbitraryNetwork)
        ],
      jsonTests =
        [ JsonBox arbitraryScript,
          JsonBox arbitraryOutPoint,
          JsonBox arbitrarySigHash
        ],
      marshalJsonTests =
        [ MarshalJsonBox $ do
            n <- arbitraryNetwork
            (_, _, ts) <- arbitraryTxSignature n ctx
            return ((n, ctx), ts),
          MarshalJsonBox $ do
            n <- arbitraryNetwork
            o <- arbitraryScriptOutput n ctx
            return (ctx, o),
          MarshalJsonBox $ do
            n <- arbitraryNetwork
            (i, _) <- arbitrarySigInput n ctx
            return (ctx, i)
        ],
      serialTests =
        [ SerialBox arbitraryScriptOp,
          SerialBox arbitraryScript
        ],
      marshalTests =
        [ MarshalBox $ do
            n <- arbitraryNetwork
            (_, _, ts) <- arbitraryTxSignature n ctx
            return ((n, ctx), ts),
          MarshalBox $ do
            n <- arbitraryNetwork
            o <- arbitraryScriptOutput n ctx
            return (ctx, o),
          MarshalBox $ do
            n <- arbitraryNetwork
            i <- arbitraryScriptInput n ctx
            return ((n, ctx), i)
        ]
    }

spec :: Spec
spec = prepareContext $ \ctx -> do
  testIdentity $ identityTests ctx
  describe "btc scripts" $ props btc ctx
  describe "bch scripts" $ props bch ctx
  describe "multi signatures" $
    zipWithM_ (curry (mapMulSigVector ctx)) mulSigVectors [0 ..]
  describe "signature decoding" $
    zipWithM_ (curry (sigDecodeMap btc ctx)) 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 -> Ctx -> Spec
props net ctx = do
  standardSpec net ctx
  strictSigSpec net ctx
  scriptSpec net ctx
  txSigHashForkIdSpec net
  forkIdScriptSpec net ctx
  sigHashSpec net ctx
  txSigHashSpec net

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

scriptSpec :: Network -> Ctx -> Spec
scriptSpec net ctx =
  when (net.name == "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 =
                any
                  (`isInfixOf` flags)
                  ["DERSIG", "STRICTENC", "NULLDUMMY"]
              scriptSig = parseScript siStr
              scriptPubKey = parseScript soStr
              out = unmarshal ctx scriptPubKey
              tx = spendTx scriptPubKey 0 scriptSig
              sat = val * 100000000
              ver o = verifyStdInput net ctx tx 0 o sat
              valid = either (const False) ver out
          assertBool desc $ if res == "OK" then valid else not valid

forkIdScriptSpec :: Network -> Ctx -> Spec
forkIdScriptSpec net ctx =
  when (isJust net.sigHashForkId) $
    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
            out = unmarshal ctx scriptPubKey
            tx = spendTx scriptPubKey val scriptSig
            ver o = verifyStdInput net ctx tx 0 o val
            valid = either (const False) ver out
        case res of
          "OK" -> valid `shouldBe` True
          _ -> valid `shouldBe` False

creditTx :: ByteString -> Word64 -> Tx
creditTx scriptPubKey val =
  Tx 1 [txI] [txO] [] 0
  where
    txO = TxOut {value = val, script = scriptPubKey}
    txI =
      TxIn
        { outpoint = nullOutPoint,
          script = runPutS $ serialize $ Script [OP_0, OP_0],
          sequence = maxBound
        }

spendTx :: ByteString -> Word64 -> ByteString -> Tx
spendTx scriptPubKey val scriptSig =
  Tx 1 [txI] [txO] [] 0
  where
    txO = TxOut {value = val, script = B.empty}
    txI =
      TxIn
        { outpoint = OutPoint (txHash $ creditTx scriptPubKey val) 0,
          script = scriptSig,
          sequence = 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 -> Ctx -> Spec
strictSigSpec net ctx =
  when (net.name == "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 ->
        let eitherSig :: Either String TxSignature
            eitherSig = decodeTxSig net ctx sig
         in eitherSig `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 ->
        let eitherSig = decodeTxSig net ctx sig
         in eitherSig `shouldSatisfy` isLeft

txSigHashSpec :: Network -> Spec
txSigHashSpec net =
  when (net.name == "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 (net.name == "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 -> Ctx -> Spec
sigHashSpec net ctx = 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
    setAnyoneCanPay sigHashAll `shouldBe` 0x81
    setAnyoneCanPay (setForkIdFlag sigHashAll) `shouldBe` 0xc1
  it "can test flags" $ do
    hasForkIdFlag sigHashAll `shouldBe` False
    hasForkIdFlag (setForkIdFlag sigHashAll) `shouldBe` True
    anyoneCanPay sigHashAll `shouldBe` False
    anyoneCanPay (setAnyoneCanPay 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 ctx) $ \(_, _, ts) ->
        let f = decodeTxSig net ctx . encodeTxSig net ctx
         in f ts `shouldBe` Right ts
  it "can produce the sighash one" $
    property $
      forAll (arbitraryTx net ctx) $
        forAll arbitraryScript . testSigHashOne net

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

{- Parse tests from bitcoin-qt repository -}

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

runMulSigVector :: Ctx -> (Text, Text) -> Assertion
runMulSigVector ctx (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 ctx o
      addrToText btc $ payToScriptAddress ctx d

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

testSigDecode :: Network -> Ctx -> Text -> Assertion
testSigDecode net ctx str =
  let bs = fromJust $ decodeHex str
      eitherSig = decodeTxSig net ctx 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