{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE ImportQualifiedPost #-} {-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} module Haskoin.TransactionSpec (spec) where import Control.Monad (unless) 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.Maybe import Data.String (fromString) import Data.String.Conversions import Data.Text (Text) import Data.Word (Word32, Word64) 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 import Test.Hspec import Test.Hspec.QuickCheck import Test.QuickCheck identityTests :: Ctx -> IdentityTests identityTests ctx = def { readTests = [ ReadBox arbitraryTxHash, ReadBox $ flip arbitraryTx ctx =<< arbitraryNetwork, ReadBox $ flip arbitraryTxIn ctx =<< arbitraryNetwork, ReadBox $ flip arbitraryTxOut ctx =<< arbitraryNetwork, ReadBox arbitraryOutPoint ], jsonTests = [ JsonBox arbitraryTxHash, JsonBox $ flip arbitraryTx ctx =<< arbitraryNetwork, JsonBox $ flip arbitraryWitnessTx ctx =<< arbitraryNetwork, JsonBox $ flip arbitraryLegacyTx ctx =<< arbitraryNetwork, JsonBox $ flip arbitraryTxIn ctx =<< arbitraryNetwork, JsonBox $ flip arbitraryTxOut ctx =<< arbitraryNetwork, JsonBox arbitraryOutPoint ], serialTests = [ SerialBox $ flip arbitraryTx ctx =<< arbitraryNetwork, SerialBox $ flip arbitraryWitnessTx ctx =<< arbitraryNetwork, SerialBox $ flip arbitraryLegacyTx ctx =<< arbitraryNetwork, SerialBox $ flip arbitraryTxIn ctx =<< arbitraryNetwork, SerialBox $ flip arbitraryTxOut ctx =<< arbitraryNetwork, SerialBox arbitraryOutPoint ] } spec :: Spec spec = prepareContext $ \ctx -> do testIdentity $ identityTests ctx describe "Transaction properties" $ do prop "decode and encode txid" $ forAll arbitraryTxHash $ \h -> hexToTxHash (txHashToHex h) == Just h prop "from string transaction id" $ forAll arbitraryTxHash $ \h -> fromString (cs $ txHashToHex h) == h prop "building address tx" $ forAll arbitraryNetwork $ \net -> forAll arbitraryAddress $ forAll (arbitrarySatoshi net) . testBuildAddrTx net ctx prop "guess transaction size" $ forAll arbitraryNetwork $ \net -> forAll (arbitraryAddrOnlyTxFull net ctx) (testGuessSize net ctx) prop "choose coins" $ forAll arbitraryNetwork $ \net -> forAll (listOf (arbitrarySatoshi net)) testChooseCoins prop "choose multisig coins" $ forAll arbitraryNetwork $ \net -> forAll arbitraryMSParam $ forAll (listOf (arbitrarySatoshi net)) . testChooseMSCoins prop "sign and validate transaction" $ forAll arbitraryNetwork $ \net -> forAll (arbitrarySigningData net ctx) (testDetSignTx net ctx) prop "sign and validate (nested) transaction" $ forAll arbitraryNetwork $ \net -> forAll (arbitrarySigningData net ctx) (testDetSignNestedTx net ctx) prop "merge partially signed transactions" $ forAll arbitraryNetwork $ \net -> property $ forAll (arbitraryPartialTxs net ctx) (testMergeTx net ctx) describe "Transaction vectors" $ do it "compute txid from tx" $ mapM_ testTxidVector txidVectors it "build pkhash transaction (generated from bitcoind)" $ mapM_ (testPKHashVector ctx) pkHashVectors -- Txid Vectors testTxidVector :: (Text, Text) -> Assertion testTxidVector (tid, tx) = assertEqual "txid" (Just tid) (txHashToHex . txHash <$> txM) where txM = eitherToMaybe . runGetS deserialize =<< decodeHex tx txidVectors :: [(Text, Text)] txidVectors = [ ( "23b397edccd3740a74adb603c9756370fafcde9bcc4483eb271ecad09a94dd63", "0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa4\ \3ad90ba26000000000490047304402203f16c6f40162ab686621ef3000b04e75\ \418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb\ \60996631bf6bf5bc0a0682c4db743ce7ca2b01ffffffff0140420f0000000000\ \1976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000" ), ( "c99c49da4c38af669dea436d3e73780dfdb6c1ecf9958baa52960e8baee30e73", "01000000010276b76b07f4935c70acf54fbf1f438a4c397a9fb7e633873c4dd3\ \bc062b6b40000000008c493046022100d23459d03ed7e9511a47d13292d3430a\ \04627de6235b6e51a40f9cd386f2abe3022100e7d25b080f0bb8d8d5f878bba7\ \d54ad2fda650ea8d158a33ee3cbd11768191fd004104b0e2c879e4daf7b9ab68\ \350228c159766676a14f5815084ba166432aab46198d4cca98fa3e9981d0a90b\ \2effc514b76279476550ba3663fdcaff94c38420e9d5000000000100093d0000\ \0000001976a9149a7b0f3b80c6baaeedce0a0842553800f832ba1f88ac000000\ \00" ), ( "f7fdd091fa6d8f5e7a8c2458f5c38faffff2d3f1406b6e4fe2c99dcc0d2d1cbb", "01000000023d6cf972d4dff9c519eff407ea800361dd0a121de1da8b6f4138a2\ \f25de864b4000000008a4730440220ffda47bfc776bcd269da4832626ac332ad\ \fca6dd835e8ecd83cd1ebe7d709b0e022049cffa1cdc102a0b56e0e04913606c\ \70af702a1149dc3b305ab9439288fee090014104266abb36d66eb4218a6dd31f\ \09bb92cf3cfa803c7ea72c1fc80a50f919273e613f895b855fb7465ccbc8919a\ \d1bd4a306c783f22cd3227327694c4fa4c1c439affffffff21ebc9ba20594737\ \864352e95b727f1a565756f9d365083eb1a8596ec98c97b7010000008a473044\ \0220503ff10e9f1e0de731407a4a245531c9ff17676eda461f8ceeb8c06049fa\ \2c810220c008ac34694510298fa60b3f000df01caa244f165b727d4896eb84f8\ \1e46bcc4014104266abb36d66eb4218a6dd31f09bb92cf3cfa803c7ea72c1fc8\ \0a50f919273e613f895b855fb7465ccbc8919ad1bd4a306c783f22cd32273276\ \94c4fa4c1c439affffffff01f0da5200000000001976a914857ccd42dded6df3\ \2949d4646dfa10a92458cfaa88ac00000000" ), ( "afd9c17f8913577ec3509520bd6e5d63e9c0fd2a5f70c787993b097ba6ca9fae", "010000000370ac0a1ae588aaf284c308d67ca92c69a39e2db81337e563bf40c5\ \9da0a5cf63000000006a4730440220360d20baff382059040ba9be98947fd678\ \fb08aab2bb0c172efa996fd8ece9b702201b4fb0de67f015c90e7ac8a193aeab\ \486a1f587e0f54d0fb9552ef7f5ce6caec032103579ca2e6d107522f012cd00b\ \52b9a65fb46f0c57b9b8b6e377c48f526a44741affffffff7d815b6447e35fbe\ \a097e00e028fb7dfbad4f3f0987b4734676c84f3fcd0e804010000006b483045\ \022100c714310be1e3a9ff1c5f7cacc65c2d8e781fc3a88ceb063c6153bf9506\ \50802102200b2d0979c76e12bb480da635f192cc8dc6f905380dd4ac1ff35a4f\ \68f462fffd032103579ca2e6d107522f012cd00b52b9a65fb46f0c57b9b8b6e3\ \77c48f526a44741affffffff3f1f097333e4d46d51f5e77b53264db8f7f5d2e1\ \8217e1099957d0f5af7713ee010000006c493046022100b663499ef73273a378\ \8dea342717c2640ac43c5a1cf862c9e09b206fcb3f6bb8022100b09972e75972\ \d9148f2bdd462e5cb69b57c1214b88fc55ca638676c07cfc10d8032103579ca2\ \e6d107522f012cd00b52b9a65fb46f0c57b9b8b6e377c48f526a44741affffff\ \ff0380841e00000000001976a914bfb282c70c4191f45b5a6665cad1682f2c9c\ \fdfb88ac80841e00000000001976a9149857cc07bed33a5cf12b9c5e0500b675\ \d500c81188ace0fd1c00000000001976a91443c52850606c872403c0601e69fa\ \34b26f62db4a88ac00000000" ) ] -- Build address transactions vectors generated from bitcoin-core raw tx API testPKHashVector :: Ctx -> ([(Text, Word32)], [(Text, Word64)], Text) -> Assertion testPKHashVector ctx (is, os, res) = assertEqual "Build PKHash Tx" (Right res) (encodeHex . runPutS . serialize <$> txE) where txE = buildAddrTx btc ctx (map f is) os f (tid, ix) = OutPoint (fromJust $ hexToTxHash tid) ix pkHashVectors :: [([(Text, Word32)], [(Text, Word64)], Text)] pkHashVectors = [ ( [ ( "eb29eba154166f6541ebcc9cbdf5088756e026af051f123bcfb526df594549db", 14 ) ], [("14LsRquZfURNFrzpcLVGdaHTfAPjjwiSPb", 90000000)], "0100000001db494559df26b5cf3b121f05af26e0568708f5bd9ccceb41656f1654\ \a1eb29eb0e00000000ffffffff01804a5d05000000001976a91424aa604689cc58\ \2292b97668bedd91dd5bf9374c88ac00000000" ), ( [ ( "eb29eba154166f6541ebcc9cbdf5088756e026af051f123bcfb526df594549db", 0 ), ( "0001000000000000000000000000000000000000000000000000000000000000", 2147483647 ) ], [ ("14LsRquZfURNFrzpcLVGdaHTfAPjjwiSPb", 1), ("19VCgS642vzEA1sdByoSn6GsWBwraV8D4n", 2100000000000000) ], "0100000002db494559df26b5cf3b121f05af26e0568708f5bd9ccceb41656f1654\ \a1eb29eb0000000000ffffffff0000000000000000000000000000000000000000\ \000000000000000000000100ffffff7f00ffffffff0201000000000000001976a9\ \1424aa604689cc582292b97668bedd91dd5bf9374c88ac0040075af07507001976\ \a9145d16672f53981ff21c5f42b40d1954993cbca54f88ac00000000" ), ( [ ( "eb29eba154166f6541ebcc9cbdf5088756e026af051f123bcfb526df594549db", 0 ), ( "0001000000000000000000000000000000000000000000000000000000000000", 2147483647 ) ], [], "0100000002db494559df26b5cf3b121f05af26e0568708f5bd9ccceb41656f1654a\ \1eb29eb0000000000ffffffff000000000000000000000000000000000000000000\ \0000000000000000000100ffffff7f00ffffffff0000000000" ), ( [], [ ("14LsRquZfURNFrzpcLVGdaHTfAPjjwiSPb", 1), ("19VCgS642vzEA1sdByoSn6GsWBwraV8D4n", 2100000000000000) ], "01000000000201000000000000001976a91424aa604689cc582292b97668bedd91d\ \d5bf9374c88ac0040075af07507001976a9145d16672f53981ff21c5f42b40d1954\ \993cbca54f88ac00000000" ) ] -- Transaction Properties -- testBuildAddrTx :: Network -> Ctx -> Address -> TestCoin -> Bool testBuildAddrTx net ctx a (TestCoin v) | isPubKeyAddress a = PayPKHash a.hash160 == out | isScriptAddress a = PayScriptHash a.hash160 == out | otherwise = undefined where out = either error id $ do tx <- buildAddrTx net ctx [] [(fromJust (addrToText net a), v)] unmarshal ctx (head tx.outputs).script -- 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) testGuessSize :: Network -> Ctx -> Tx -> Bool testGuessSize net ctx tx = guess >= len && guess <= len + 2 * delta where delta = pki + sum (map fst msi) guess = guessTxSize pki msi pkout msout len = B.length $ runPutS $ serialize tx ins = map f tx.inputs f i = either error id $ unmarshal (net, ctx) i.script pki = length $ filter isSpendPKHash ins msi = concatMap shData ins shData (ScriptHashInput _ (PayMulSig keys r)) = [(r, length keys)] shData _ = [] out = map (either error id . unmarshal ctx . (.script)) tx.outputs pkout = length $ filter isPayPKHash out msout = length $ filter isPayScriptHash out testChooseCoins :: [TestCoin] -> Word64 -> Word64 -> Int -> Property testChooseCoins coins target byteFee nOut = nOut >= 0 ==> case chooseCoins target byteFee nOut True coins of Right (chosen, change) -> let outSum = sum $ map coinValue chosen fee = guessTxFee byteFee nOut (length chosen) in outSum == target + change + fee Left _ -> let fee = guessTxFee byteFee nOut (length coins) in target == 0 || s < target + fee where s = sum $ map coinValue coins testChooseMSCoins :: (Int, Int) -> [TestCoin] -> Word64 -> Word64 -> Int -> Property testChooseMSCoins (m, n) coins target byteFee nOut = nOut >= 0 ==> case chooseMSCoins target byteFee (m, n) nOut True coins of Right (chosen, change) -> let outSum = sum $ map coinValue chosen fee = guessMSTxFee byteFee (m, n) nOut (length chosen) in outSum == target + change + fee Left _ -> let fee = guessMSTxFee byteFee (m, n) nOut (length coins) in target == 0 || s < target + fee where s = sum $ map coinValue coins {- Signing Transactions -} testDetSignTx :: Network -> Ctx -> (Tx, [SigInput], [PrivateKey]) -> Bool testDetSignTx net ctx (tx, sigis, prv) = not verify1 && not verify2 && verify3 where verify1 = verifyStdTx net ctx tx verData verify2 = verifyStdTx net ctx txSigP verData verify3 = verifyStdTx net ctx txSigC verData txSigP = either error id $ signTx net ctx tx sigis (map (.key) (tail prv)) txSigC = either error id $ signTx net ctx txSigP sigis [(head prv).key] sigData SigInput {..} = (script, value, outpoint) verData = map sigData sigis testDetSignNestedTx :: Network -> Ctx -> (Tx, [SigInput], [PrivateKey]) -> Bool testDetSignNestedTx net ctx (tx, sigis, prv) = not verify1 && not verify2 && verify3 where verify1 = verifyStdTx net ctx tx verData verify2 = verifyStdTx net ctx txSigP verData verify3 = verifyStdTx net ctx txSigC verData txSigP = either error id $ signNestedWitnessTx net ctx tx sigis ((.key) <$> tail prv) txSigC = either error id $ signNestedWitnessTx net ctx txSigP sigis [(head prv).key] verData = handleSegwit <$> sigis handleSegwit (SigInput s v o _ _) | isSegwit s = (toP2SH (encodeOutput ctx s), v, o) | otherwise = (s, v, o) testMergeTx :: Network -> Ctx -> ([Tx], [(ScriptOutput, Word64, OutPoint, Int, Int)]) -> Bool testMergeTx net ctx (txs, os) = and [ isRight mergeRes, length mergedTx.inputs == 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, val, op, _, _) -> (so, val, op)) os mergeRes = mergeTxs net ctx txs outs mergedTx = fromRight (error "Could not merge") mergeRes isValid = verifyStdTx net ctx mergedTx outs enoughSigs = all (\(m, c) -> c >= m) sigMap sigFun (_, _, _, m, _) inp = (m, sigCnt inp) sigMap = zipWith sigFun os mergedTx.inputs sigCnt inp = case unmarshal (net, ctx) inp.script of Right (RegularInput (SpendMulSig sigs)) -> length sigs Right (ScriptHashInput (SpendMulSig sigs) _) -> length sigs _ -> error "Invalid input script type"