packages feed

haskoin-core-0.2.0: tests/Network/Haskoin/Transaction/Tests.hs

module Network.Haskoin.Transaction.Tests (tests) where

import Test.Framework (Test, testGroup)
import Test.Framework.Providers.QuickCheck2 (testProperty)

import Data.String (fromString)
import Data.String.Conversions (cs)
import Data.Word (Word64)
import qualified Data.ByteString as BS (length)

import Network.Haskoin.Test
import Network.Haskoin.Transaction
import Network.Haskoin.Script
import Network.Haskoin.Crypto
import Network.Haskoin.Util

tests :: [Test]
tests =
    [ testGroup "Transaction tests"
        [ testProperty "decode . encode Txid" decEncTxid
        , testProperty "Read/Show transaction id" testReadShowTxHash
        , testProperty "From string transaction id" testFromStringTxHash
        ]
    , testGroup "Building Transactions"
        [ testProperty "building address tx" testBuildAddrTx
        , testProperty "testing guessTxSize function" testGuessSize
        , testProperty "testing chooseCoins function" testChooseCoins
        , testProperty "testing chooseMSCoins function" testChooseMSCoins
        ]
    , testGroup "Signing Transactions"
        [ testProperty "Sign and validate transactions" testDetSignTx
        , testProperty "Merge partially signed transactions" testMergeTx
        ]
    ]

{- Transaction Tests -}

decEncTxid :: ArbitraryTxHash -> Bool
decEncTxid (ArbitraryTxHash h) = hexToTxHash (txHashToHex h) == Just h

testReadShowTxHash :: ArbitraryTxHash -> Bool
testReadShowTxHash (ArbitraryTxHash h) = read (show h) == h

testFromStringTxHash :: ArbitraryTxHash -> Bool
testFromStringTxHash (ArbitraryTxHash h) = fromString (cs $ txHashToHex h) == h

{- Building Transactions -}

testBuildAddrTx :: ArbitraryAddress -> ArbitrarySatoshi -> Bool
testBuildAddrTx (ArbitraryAddress a) (ArbitrarySatoshi v) = case a of
    x@(PubKeyAddress _) -> Right (PayPKHash x) == out
    x@(ScriptAddress _) -> Right (PayScriptHash x) == out
  where
    tx  = buildAddrTx [] [(addrToBase58 a,v)]
    out = decodeOutputBS $ scriptOutput $ txOut (fromRight tx) !! 0

testGuessSize :: ArbitraryAddrOnlyTx -> Bool
testGuessSize (ArbitraryAddrOnlyTx tx) =
    -- We compute an upper bound but it should be close enough to the real size
    -- We give 2 bytes of slack on every signature (1 on r and 1 on s)
    guess >= len && guess <= len + 2*delta
  where
    delta    = pki + (sum $ map fst msi)
    guess    = guessTxSize pki msi pkout msout
    len      = BS.length $ encode' tx
    ins      = map f $ txIn tx
    f i      = fromRight $ decodeInputBS $ scriptInput i
    pki      = length $ filter isSpendPKHash ins
    msi      = concat $ map shData ins
    shData (ScriptHashInput _ (PayMulSig keys r)) = [(r,length keys)]
    shData _ = []
    out      = map (fromRight . decodeOutputBS . scriptOutput) $ txOut tx
    pkout    = length $ filter isPayPKHash out
    msout    = length $ filter isPayScriptHash out

testChooseCoins :: Word64 -> Word64 -> [ArbitrarySatoshi] -> Bool
testChooseCoins target kbfee coins = case chooseCoins target kbfee True coins of
    Right (chosen, change) ->
        let outSum = sum $ map coinValue chosen
            fee    = getFee kbfee (length chosen)
        in outSum == target + change + fee
    Left _ ->
        let fee = getFee kbfee (length coins)
        in target == 0 || s < target || s < target + fee
  where
    s  = sum $ map coinValue coins

testChooseMSCoins :: Word64 -> Word64
                  -> ArbitraryMSParam -> [ArbitrarySatoshi] -> Bool
testChooseMSCoins target kbfee (ArbitraryMSParam m n) coins =
    case chooseMSCoins target kbfee (m,n) True coins of
        Right (chosen,change) ->
            let outSum = sum $ map coinValue chosen
                fee    = getMSFee kbfee (m,n) (length chosen)
            in outSum == target + change + fee
        Left _ ->
            let fee = getMSFee kbfee (m,n) (length coins)
            in target == 0 || s < target + fee
  where
    s  = sum $ map coinValue coins

{- Signing Transactions -}

testDetSignTx :: ArbitrarySigningData -> Bool
testDetSignTx (ArbitrarySigningData tx sigis prv) =
    (not $ verifyStdTx tx verData)
        && (not $ verifyStdTx txSigP verData)
        && verifyStdTx txSigC verData
  where
    txSigP  = fromRight $ signTx tx sigis (tail prv)
    txSigC  = fromRight $ signTx txSigP sigis [head prv]
    verData = map (\(SigInput s o _ _) -> (s,o)) sigis

testMergeTx :: ArbitraryPartialTxs -> Bool
testMergeTx (ArbitraryPartialTxs txs os) = and
    [ isRight mergeRes
    , length (txIn mergedTx) == length os
    , if enoughSigs then isValid else not isValid
    -- Signature count == min (length txs) (sum required signatures)
    , sum (map snd sigMap) == min (length txs) (sum (map fst sigMap))
    ]
  where
    outs = map (\(so, op, _, _) -> (so, op)) os
    mergeRes = mergeTxs txs outs
    mergedTx = fromRight mergeRes
    isValid = verifyStdTx mergedTx outs
    enoughSigs = and $ map (\(m,c) -> c >= m) sigMap
    sigMap = map (\((_,_,m,_), inp) -> (m, sigCnt inp)) $ zip os $ txIn mergedTx
    sigCnt inp = case decodeInputBS $ scriptInput inp of
        Right (RegularInput (SpendMulSig sigs)) -> length sigs
        Right (ScriptHashInput (SpendMulSig sigs) _) -> length sigs
        _ -> error "Invalid input script type"