{-# LANGUAGE DataKinds #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# OPTIONS_GHC -Wno-type-defaults #-}
module Main (main) where
import qualified Data.ByteString as B
import qualified Data.ByteString.Base16 as B16
import Data.Maybe
import Data.Word
import qualified Test.Tasty as Tasty
import qualified Test.Tasty.Runners as Tasty
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (testCase, (@?=), (@=?))
import Test.Tasty.Hedgehog (testProperty, HedgehogTestLimit(..))
import Hedgehog (MonadGen, property, forAll, (===))
import qualified Hedgehog.Gen as Gen
import qualified Hedgehog.Range as Range
import qualified System.IO as IO
import qualified Bitcoin.Keys as S
--------------------------------------------------------------------------------
main :: IO ()
main = do
IO.hSetBuffering IO.stdout IO.LineBuffering
IO.hSetBuffering IO.stderr IO.LineBuffering
Tasty.defaultMainWithIngredients
[ Tasty.consoleTestReporter
, Tasty.listingTests
] $ Tasty.localOption (HedgehogTestLimit (Just 2000))
$ tt
tt :: TestTree
tt = testGroup "bitcoin-keys"
[ testCase "Show Pub" $ do
show pub1c @?= "Pub 024ed59d3f6548c41172ec26b9144f547185cb2a9f28fccb6e3ae53dc4f1827154"
, testCase "Show Prv" $ do
show prv1 @?= "Prv 76deed24fcdfb479656af6c5c4ab906090c5ee39da1c1ad1f1ad56dfce378a03"
, testCase "Show Tweak" $ do
show tweak1 @?= "Tweak 0ef561fb5f4e9e2319a5b75ed3be3c895e6ec272c0b5185dc414fb7e47c3a770"
, testCase "parsePub: uncompressed vs compressed (even y)" $ do
pub1c @=? pub1u
S.pubCompressed pub1c @?= pub1c_raw
S.pubCompressed pub1u @?= pub1c_raw
S.pubUncompressed pub1c @?= pub1u_raw
S.pubUncompressed pub1u @?= pub1u_raw
, testCase "parsePub: uncompressed vs compressed (odd y)" $ do
pub2c @=? pub2u
S.pubCompressed pub2c @?= pub2c_raw
S.pubCompressed pub2u @?= pub2c_raw
S.pubUncompressed pub2c @?= pub2u_raw
S.pubUncompressed pub2u @?= pub2u_raw
, testCase "Ord Pub" $ do
compare pub1c pub2c @?= LT
compare pub2c pub1c @?= GT
compare pub1c pub1c @?= EQ
compare pub2c pub2c @?= EQ
, testCase "Ord Prv" $ do
compare prv1 prv2 @?= LT
compare prv2 prv1 @?= GT
compare prv1 prv1 @?= EQ
compare prv2 prv2 @?= EQ
, testCase "prvToPub prv1" $ do
S.prvToPub prv1 @?= pub1c
, testCase "prvToPub prv2" $ do
S.prvToPub prv2 @?= pub2c
, testProperty "parsePrv . prvRaw" $ property $ do
prv <- forAll genPrv
Just prv === S.parsePrv (S.prvRaw prv)
, testProperty "parsePub . pubCompressed" $ property $ do
pub <- forAll genPub
Just pub === S.parsePub (S.pubCompressed pub)
, testProperty "parsePub . pubUncompressed" $ property $ do
pub <- forAll genPub
Just pub === S.parsePub (S.pubUncompressed pub)
, testProperty "parsePrv: invalid" $ property $ do
raw <- forAll $ Gen.bytes (Range.constant 0 200)
case B.length raw of
32 -> pure ()
_ -> Nothing === S.parsePrv raw
, testProperty "parsePub: invalid" $ property $ do
raw <- forAll $ Gen.bytes (Range.constant 0 200)
case B.length raw of
33 | B.head raw `elem` [0x02, 0x03] -> pure ()
65 | B.head raw == 0x04 -> pure ()
_ -> Nothing === S.parsePub raw
, testProperty "parseTweak" $ property $ do
raw <- forAll $ Gen.bytes (Range.constant 0 200)
let yt = S.parsePrv raw
case B.length raw of
32 -> True === isJust yt
_ -> Nothing === yt
, testProperty "addPrvTweak vs addPubTweak (prop)" $ property $ do
tweak <- forAll genTweak
prv <- forAll genPrv
let pub = S.prvToPub prv
yprv' = S.prvAddTweak tweak prv
ypub' = S.pubAddTweak tweak pub
ypub' === fmap S.prvToPub yprv'
, testCase "addPrvTweak vs addPubTweak (prv1)" $ do
let Just pub' = S.prvToPub <$> S.prvAddTweak tweak1 prv1
Just pub' @?= S.pubAddTweak tweak1 pub1c
, testCase "addPrvTweak vs addPubTweak (prv2)" $ do
let Just pub' = S.prvToPub <$> S.prvAddTweak tweak1 prv2
Just pub' @?= S.pubAddTweak tweak1 pub2c
, testCase "addPrvTweak prv1" $ do
let Just x = S.parsePrv $ fromBase16
"85d44f205c2e529c7f10ae249869cce9ef34b0ac9ad1332fb5c2525e15fb3173"
S.prvAddTweak tweak1 prv1 @?= Just x
, testCase "addPrvTweak prv2" $ do
let Just x = S.parsePrv $ fromBase16
"cb186f2bc6a2ace5580fb1fda1861ea2757435aa1e66a812d7decb7d368eddee"
S.prvAddTweak tweak1 prv2 @?= Just x
, testCase "addPubTweak pub1" $ do
let Just x = S.parsePub $ fromBase16
"038089e37ec58d5ace95bc84ee11d6c366d1b464ba703c2f8ec189bc2bce76f1f3"
S.pubAddTweak tweak1 pub1c @?= Just x
, testCase "addPubTweak pub2" $ do
let Just x = S.parsePub $ fromBase16
"025c8fd62030ba8ee8ede5bf053613a31a4850c9489eb62bec7cfb3efaa1e07b43"
S.pubAddTweak tweak1 pub2c @?= Just x
]
--------------------------------------------------------------------------------
genTweak :: MonadGen m => m S.Tweak
genTweak = do
b <- Gen.bytes (Range.singleton 32)
let Just k = S.parseTweak b
pure k
genPrv :: MonadGen m => m S.Prv
genPrv = do
b <- Gen.bytes (Range.singleton 32)
let Just k = S.parsePrv b
pure k
genPub :: MonadGen m => m S.Pub
genPub = go 10000 where
go 0 = error "genPub: too many attempts"
go n = do
h <- Gen.element [2, 3 :: Word8]
b <- Gen.bytes (Range.singleton 32)
case S.parsePub (B.cons h b) of
Just k -> pure k
Nothing -> go (n - 1)
--------------------------------------------------------------------------------
-- BIP39 mnemonic: abandon abandon arm
--
-- BIP39 seed: 2f7b8b6eeeb7201d88ed45dcd4706ccd76e5feb61f1961990869581781f8944ae2bebff36156c6965054add29f2cf9ee8b7b488179bfba24773ce2b06ec0ce04
--
-- BIP32 Derivation path: m/44'/0'/0'/0/0
--
-- Private key WIF: L1CnCq2yTvfzngQTF4Fnh5daYUwAuysrqU5dNwZFcv33Cwu4Gf2F
prv1 :: S.Prv
prv1 = maybe (error "prv1") id (S.parsePrv prv1_raw)
prv1_raw :: B.ByteString
prv1_raw = fromBase16
"76deed24fcdfb479656af6c5c4ab906090c5ee39da1c1ad1f1ad56dfce378a03"
-- | Compressed version of `pub1u`
pub1c :: S.Pub
pub1c = maybe (error "pub1c") id (S.parsePub pub1c_raw)
pub1c_raw :: B.ByteString
pub1c_raw = fromBase16
"024ed59d3f6548c41172ec26b9144f547185cb2a9f28fccb6e3ae53dc4f1827154"
-- | Uncompressed version of `pub1c`
pub1u :: S.Pub
pub1u = maybe (error "pub1u") id (S.parsePub pub1u_raw)
pub1u_raw :: B.ByteString
pub1u_raw = fromBase16
"044ed59d3f6548c41172ec26b9144f547185cb2a9f28fccb6e3ae53dc4f1827154\
\ba3a12d8b383bfff1dacd6930285f4a51a01ee64e102652d5bc137c49b97815e"
--------------------------------------------------------------------------------
-- BIP39 mnemonic: abandon abandon arm
--
-- BIP39 seed: 2f7b8b6eeeb7201d88ed45dcd4706ccd76e5feb61f1961990869581781f8944ae2bebff36156c6965054add29f2cf9ee8b7b488179bfba24773ce2b06ec0ce04
--
-- BIP32 Derivation path: m/84'/0'/0'/0/0
--
-- Private key WIF: L3XRaAfqnZLCcUpcCi4g1GqMLTKvr9nbvCYBmrs1TAjx4acpZ2eY
prv2 :: S.Prv
prv2 = maybe (error "prv2") id (S.parsePrv prv2_raw)
prv2_raw :: B.ByteString
prv2_raw = fromBase16
"bc230d3067540ec23e69fa9ecdc7e219170573375db18fb513c9cffeeecb367e"
-- | Compressed version of `pub2u`
pub2c :: S.Pub
pub2c = maybe (error "pub2c") id (S.parsePub pub2c_raw)
pub2c_raw :: B.ByteString
pub2c_raw = fromBase16
"0315bd146d8b45383ad431a8204717dc9f46a7a46c1f44eeaab02d49f4511ccff0"
-- | Uncompressed version of `pub2c`
pub2u :: S.Pub
pub2u = maybe (error "pub2u") id (S.parsePub pub2u_raw)
pub2u_raw :: B.ByteString
pub2u_raw = fromBase16
"0415bd146d8b45383ad431a8204717dc9f46a7a46c1f44eeaab02d49f4511ccff0\
\7d7817de34c0a281b45691eec966e95d936554b2320462214cd2a17aebcf460f"
--------------------------------------------------------------------------------
-- Randomly generated
tweak1 :: S.Tweak
tweak1 = maybe (error "tweak1") id (S.parseTweak tweak1_raw)
tweak1_raw :: B.ByteString
tweak1_raw = fromBase16
"0ef561fb5f4e9e2319a5b75ed3be3c895e6ec272c0b5185dc414fb7e47c3a770"
--------------------------------------------------------------------------------
fromBase16 :: B.ByteString -> B.ByteString
fromBase16 a = case B16.decode a of
(b, "") -> b
_ -> error ("Invalid base16 string: " <> show a)