haskoin-core-0.2.0: tests/Network/Haskoin/Script/Tests.hs
module Network.Haskoin.Script.Tests
( tests
, execScriptIO
, testValid
, testInvalid
, runTests
) where
import Test.QuickCheck.Property (Property, (==>))
import Test.Framework (Test, testGroup, buildTest)
import Test.Framework.Providers.HUnit (testCase)
import Test.Framework.Providers.QuickCheck2 (testProperty)
import Test.Framework.Runners.Console (defaultMainWithArgs)
import qualified Test.HUnit as HUnit (assertFailure, assertBool)
import Control.Monad (when)
import Data.Bits (testBit)
import Data.List (isPrefixOf)
import Data.List.Split ( splitOn )
import Data.Char (ord)
import Data.Maybe (catMaybes, isNothing)
import Data.Int (Int64)
import Data.Word (Word8, Word32)
import Data.Binary (encode, decode, decodeOrFail)
import qualified Data.Aeson as A (decode)
import qualified Data.ByteString.Lazy as LBS (pack, unpack)
import qualified Data.ByteString.Lazy.Char8 as C (readFile)
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
( singleton
, length
, tail
, head
, pack
, empty
)
import qualified Data.ByteString.Char8 as C (putStrLn)
import Numeric (readHex)
import Text.Read (readMaybe)
import Network.Haskoin.Test
import Network.Haskoin.Transaction
import Network.Haskoin.Script
import Network.Haskoin.Crypto
import Network.Haskoin.Util
import Network.Haskoin.Internals
( Flag
, runStack
, dumpStack
, decodeInt
, encodeInt
, decodeBool
, encodeBool
, execScript
)
tests :: [Test]
tests =
[ testGroup "Script Parser"
[ testProperty "decode . encode OP_1 .. OP_16" testScriptOpInt
, testProperty "decode . encode ScriptOutput" testScriptOutput
, testProperty "decode . encode ScriptInput" testScriptInput
, testProperty "sorting MultiSig scripts" testSortMulSig
]
, testGroup "Script SigHash"
[ testProperty "canonical signatures" $
\(ArbitraryTxSignature _ _ sig) -> testCanonicalSig sig
, testProperty "decode SigHash from Word8" binSigHashByte
, testProperty "encodeSigHash32 is 4 bytes long" testEncodeSH32
, testProperty "decode . encode TxSignature" $
\(ArbitraryTxSignature _ _ sig) -> binTxSig sig
, testProperty "decodeCanonical . encode TxSignature" $
\(ArbitraryTxSignature _ _ sig) -> binTxSigCanonical sig
, testProperty "Testing txSigHash with SigSingle" testSigHashOne
]
, testGroup "Integer Types"
[ testProperty "decodeInt . encodeInt Int" testEncodeInt
, testProperty "decodeBool . encodeBool Bool" testEncodeBool
]
, testFile "Canonical Valid Script Test Cases"
"tests/data/script_valid.json"
True
, testFile "Canonical Invalid Script Test Cases"
"tests/data/script_invalid.json"
False
]
{- Script Parser -}
testScriptOpInt :: ArbitraryIntScriptOp -> Bool
testScriptOpInt (ArbitraryIntScriptOp i) =
(intToScriptOp <$> scriptOpToInt i) == Right i
testScriptOutput :: ArbitraryScriptOutput -> Bool
testScriptOutput (ArbitraryScriptOutput so) =
decodeOutput (encodeOutput so) == Right so
testScriptInput :: ArbitraryScriptInput -> Bool
testScriptInput (ArbitraryScriptInput si) =
decodeInput (encodeInput si) == Right si
testSortMulSig :: ArbitraryMSOutput -> Bool
testSortMulSig (ArbitraryMSOutput out) =
snd $ foldl f (head pubs,True) $ tail pubs
where
pubs = getOutputMulSigKeys $ sortMulSig out
f (a,t) b | t && encode' a <= encode' b = (b,True)
| otherwise = (b,False)
{- Script SigHash -}
testCanonicalSig :: TxSignature -> Bool
testCanonicalSig ts@(TxSignature _ sh)
| isSigUnknown sh = isLeft $ decodeCanonicalSig bs
| otherwise =
isRight (decodeCanonicalSig bs) && isCanonicalHalfOrder (txSignature ts)
where
bs = encodeSig ts
binSigHashByte :: Word8 -> Bool
binSigHashByte w
| w == 0x01 = res == SigAll False
| w == 0x02 = res == SigNone False
| w == 0x03 = res == SigSingle False
| w == 0x81 = res == SigAll True
| w == 0x82 = res == SigNone True
| w == 0x83 = res == SigSingle True
| testBit w 7 = res == SigUnknown True w
| otherwise = res == SigUnknown False w
where
res = decode' $ BS.singleton w
testEncodeSH32 :: ArbitrarySigHash -> Bool
testEncodeSH32 (ArbitrarySigHash sh) =
BS.length bs == 4 &&
BS.head bs == (BS.head $ encode' sh) &&
BS.tail bs == BS.pack [0,0,0]
where
bs = encodeSigHash32 sh
binTxSig :: TxSignature -> Bool
binTxSig ts = decodeSig (encodeSig ts) == Right ts
binTxSigCanonical :: TxSignature -> Bool
binTxSigCanonical ts@(TxSignature _ sh)
| isSigUnknown sh = isLeft $ decodeCanonicalSig $ encodeSig ts
| otherwise = (fromRight $ decodeCanonicalSig $ encodeSig ts) == ts
testSigHashOne :: ArbitraryTx -> ArbitraryScript -> Bool -> Property
testSigHashOne (ArbitraryTx tx) (ArbitraryScript s) acp = not (null $ txIn tx) ==>
if length (txIn tx) > length (txOut tx)
then res == one
else res /= one
where
res = txSigHash tx s (length (txIn tx) - 1) (SigSingle acp)
one = "0100000000000000000000000000000000000000000000000000000000000000"
{- Script Evaluation Primitives -}
testEncodeInt :: Int64 -> Bool
testEncodeInt i
| i > 0x7fffffff = isNothing i'
| i < -0x7fffffff = isNothing i'
| otherwise = i' == Just i
where
i' = decodeInt $ encodeInt i
testEncodeBool :: Bool -> Bool
testEncodeBool b = decodeBool (encodeBool b) == b
{- Script Evaluation -}
rejectSignature :: SigCheck
rejectSignature _ _ _ = False
{- Parse tests from bitcoin-qt repository -}
type ParseError = String
parseHex' :: String -> Maybe [Word8]
parseHex' (a:b:xs) = case readHex $ [a, b] :: [(Integer, String)] of
[(i, "")] -> case parseHex' xs of
Just ops -> Just $ fromIntegral i:ops
Nothing -> Nothing
_ -> Nothing
parseHex' [_] = Nothing
parseHex' [] = Just []
parseFlags :: String -> [ Flag ]
parseFlags "" = []
parseFlags s = map read . splitOn "," $ s
parseScript :: String -> Either ParseError Script
parseScript scriptString =
do bytes <- LBS.pack <$> parseBytes scriptString
script <- decodeScript bytes
when (encode script /= bytes) $
Left "encode script /= bytes"
when (decode (encode script) /= script) $
Left "decode (encode script) /= script"
return script
where
decodeScript bytes = case decodeOrFail bytes of
Left (_, _, e) -> Left $ "decode error: " ++ e
Right (_, _, Script s) -> Right $ Script s
parseBytes :: String -> Either ParseError [Word8]
parseBytes string = concat <$> mapM parseToken (words string)
parseToken :: String -> Either ParseError [Word8]
parseToken tok =
case alternatives of
(ops:_) -> Right ops
_ -> Left $ "unknown token " ++ tok
where alternatives :: [[Word8]]
alternatives = catMaybes [ parseHex
, parseInt
, parseQuote
, parseOp
]
parseHex | "0x" `isPrefixOf` tok = parseHex' (drop 2 tok)
| otherwise = Nothing
parseInt = fromInt . fromIntegral <$>
(readMaybe tok :: Maybe Integer)
parseQuote | tok == "''" = Just [0]
| (head tok) == '\'' && (last tok) == '\'' =
Just $ encodeBytes $ opPushData $ BS.pack
$ map (fromIntegral . ord)
$ init . tail $ tok
| otherwise = Nothing
fromInt :: Int64 -> [Word8]
fromInt n | n == 0 = [0x00]
| n == -1 = [0x4f]
| 1 <= n && n <= 16 = [0x50 + fromIntegral n]
| otherwise = encodeBytes
$ opPushData $ BS.pack
$ encodeInt n
parseOp = encodeBytes <$> (readMaybe $ "OP_" ++ tok)
encodeBytes = LBS.unpack . encode
testFile :: String -> String -> Bool -> Test
testFile groupLabel path expected = buildTest $ do
dat <- C.readFile path
case (A.decode dat) :: Maybe [[String]] of
Nothing -> return $
testCase groupLabel $
HUnit.assertFailure $ "can't read test file " ++ path
Just testDefs -> return $ testGroup groupLabel
$ map parseTest
$ filterPureComments testDefs
where parseTest :: [String] -> Test
parseTest s = case testParts s of
Nothing -> testCase "can't parse test case" $
HUnit.assertFailure $ "json element " ++ show s
Just ( sig, pubKey, flags, label ) -> makeTest label sig pubKey flags
makeTest :: String -> String -> String -> String -> Test
makeTest label sig pubKey flags =
testCase label' $ case (parseScript sig, parseScript pubKey) of
(Left e, _) -> parseError $ "can't parse sig: " ++
show sig ++ " error: " ++ e
(_, Left e) -> parseError $ "can't parse key: " ++
show pubKey ++ " error: " ++ e
(Right scriptSig, Right scriptPubKey) ->
runTest scriptSig scriptPubKey ( parseFlags flags )
where label' = if null label
then "sig: [" ++ sig ++ "] " ++
" pubKey: [" ++ pubKey ++ "] "
else " label: " ++ label
parseError message = HUnit.assertBool
("parse error in valid script: " ++ message)
(expected == False)
filterPureComments = filter ( not . null . tail )
runTest scriptSig scriptPubKey scriptFlags =
HUnit.assertBool
(" eval error: " ++ errorMessage)
(expected == scriptPairTestExec scriptSig scriptPubKey scriptFlags)
where run f = f scriptSig scriptPubKey rejectSignature scriptFlags
errorMessage = case run execScript of
Left e -> show e
Right _ -> " none"
-- | Splits the JSON test into the different parts. No processing,
-- just handling the fact that comments may not be there or might have
-- junk before it. Output is the tuple ( sig, pubKey, flags, comment
-- ) as strings
testParts :: [String] -> Maybe (String, String, String, String)
testParts l = let ( x, r ) = splitAt 3 l
comment = if null r then "" else last r
in if length x < 3
then Nothing
else let ( sig:pubKey:flags:[] ) = x in
Just ( sig, pubKey, flags, comment )
-- repl utils
execScriptIO :: String -> String -> String -> IO ()
execScriptIO sig key flgs = case (parseScript sig, parseScript key) of
(Left e, _) -> print $ "sig parse error: " ++ e
(_, Left e) -> print $ "key parse error: " ++ e
(Right scriptSig, Right scriptPubKey) ->
case execScript scriptSig scriptPubKey rejectSignature ( parseFlags flgs ) of
Left e -> putStrLn $ "error " ++ show e
Right p -> do putStrLn $ "successful execution"
C.putStrLn $ dumpStack $ runStack p
testValid :: Test
testValid = testFile "Canonical Valid Script Test Cases"
"tests/data/script_valid.json" True
testInvalid :: Test
testInvalid = testFile "Canonical Valid Script Test Cases"
"tests/data/script_invalid.json" False
-- | Maximum value of sequence number
maxSeqNum :: Word32
maxSeqNum = 0xffffffff -- Perhaps this should be moved to constants.
-- | Null output used to create CoinbaseTx
nullOutPoint :: OutPoint
nullOutPoint =
OutPoint
{ outPointHash =
"0000000000000000000000000000000000000000000000000000000000000000"
, outPointIndex = -1
}
-- | Some of the scripts tests require transactions be built in a
-- standard way. This function builds the crediting transaction.
-- Quoting the top comment of script_valid.json: "It is evaluated as
-- if there was a crediting coinbase transaction with two 0 pushes as
-- scriptSig, and one output of 0 satoshi and given scriptPubKey,
-- followed by a spending transaction which spends this output as only
-- input (and correct prevout hash), using the given scriptSig. All
-- nLockTimes are 0, all nSequences are max."
buildCreditTx :: ByteString -> Tx
buildCreditTx scriptPubKey = Tx {
txVersion = 1
, txIn = [ txI ]
, txOut = [ txO ]
, txLockTime = 0
}
where txO = TxOut {
outValue = 0
, scriptOutput = scriptPubKey
}
txI = TxIn {
prevOutput = nullOutPoint
, scriptInput = encode' $ Script [ OP_0, OP_0 ]
, txInSequence = maxSeqNum
}
-- | Build a spending transaction for the tests. Takes as input the
-- crediting transaction
buildSpendTx :: ByteString -- ScriptSig
-> Tx -- Creditting Tx
-> Tx
buildSpendTx scriptSig creditTx = Tx {
txVersion = 1
, txIn = [ txI ]
, txOut = [ txO ]
, txLockTime = 0
}
where txI = TxIn {
prevOutput = OutPoint { outPointHash = txHash creditTx , outPointIndex = 0 }
, scriptInput = scriptSig
, txInSequence = maxSeqNum
}
txO = TxOut { outValue = 0, scriptOutput = BS.empty }
-- | Executes the test of a scriptSig, pubKeyScript pair, including
-- building the required transactions and verifying the spending
-- transaction.
scriptPairTestExec :: Script -- scriptSig
-> Script -- pubKey
-> [ Flag ] -- Evaluation flags
-> Bool
scriptPairTestExec scriptSig pubKey flags =
let bsScriptSig = encode' scriptSig
bsPubKey = encode' pubKey
spendTx = buildSpendTx bsScriptSig ( buildCreditTx bsPubKey )
in verifySpend spendTx 0 pubKey flags
runTests :: [Test] -> IO ()
runTests ts = defaultMainWithArgs ts ["--hide-success"]