haskoin-wallet-0.9.4: test/Haskoin/Wallet/EntropySpec.hs
{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Haskoin.Wallet.EntropySpec where
import Control.Exception (evaluate)
import Control.Monad
import Control.Monad.Trans (liftIO)
import qualified Data.ByteString as BS
import Data.Default (def)
import Data.Maybe
import Data.Text (Text)
import Haskoin
import Haskoin.Util.Arbitrary
import Haskoin.Wallet.Config
import Haskoin.Wallet.Database
import Haskoin.Wallet.Entropy
import Haskoin.Wallet.Signing
import Haskoin.Wallet.TestUtils
import Test.HUnit
import Test.Hspec
import Test.Hspec.QuickCheck
import Test.QuickCheck
spec :: Spec
spec = do
let cfg = def :: Config
prepareContext $ \ctx -> do
diceSpec
entropySpec
mnemonicSpec ctx cfg
diceSpec :: Spec
diceSpec =
describe "Base6 Encoding" $ do
it "can encode a Word8 to Base6" $ do
let vs = [6, 1, 2, 3, 4, 5]
xs = [[a, b, c] | a <- vs, b <- vs, c <- vs]
ys = [[a, b] | a <- vs, b <- vs]
as = [0x00 .. 0xd7]
bs = [0xd8 .. 0xfb]
cs = [0xfc .. 0xff]
forM_ (zip as xs) $ \(a, b) -> word8ToBase6 a `shouldBe` b
forM_ (zip bs ys) $ \(a, b) -> word8ToBase6 a `shouldBe` b
forM_ cs $ \c -> word8ToBase6 c `shouldBe` []
-- Checking a few fixed values
word8ToBase6 0x00 `shouldBe` [6, 6, 6]
word8ToBase6 0x01 `shouldBe` [6, 6, 1]
word8ToBase6 0xd7 `shouldBe` [5, 5, 5]
word8ToBase6 0xd8 `shouldBe` [6, 6]
word8ToBase6 0xd9 `shouldBe` [6, 1]
word8ToBase6 0xfb `shouldBe` [5, 5]
it "can decode base6 to Word8" $ do
base6ToWord8 [6] [] `shouldBe` ([], [False, False])
base6ToWord8 [1] [] `shouldBe` ([], [False, True])
base6ToWord8 [2] [] `shouldBe` ([], [True, False])
base6ToWord8 [3] [] `shouldBe` ([], [True, True])
base6ToWord8 [4] [] `shouldBe` ([], [False])
base6ToWord8 [5] [] `shouldBe` ([], [True])
base6ToWord8 [5, 6] [] `shouldBe` ([], [True, False, False])
base6ToWord8 [6, 6, 6, 5] []
`shouldBe` ([], [False, False, False, False, False, False, True])
base6ToWord8 [6, 6, 6, 6] [] `shouldBe` ([0x00], [])
base6ToWord8 [6, 6, 6, 1] [] `shouldBe` ([0x01], [])
base6ToWord8 [3, 3, 3, 3] [] `shouldBe` ([0xff], [])
base6ToWord8 [4, 3, 3, 3, 3] [] `shouldBe` ([0xff], [False])
base6ToWord8 [1, 1, 1, 1, 3, 3, 3, 3] [] `shouldBe` ([0x55, 0xff], [])
entropySpec :: Spec
entropySpec = do
it "can mix entropy" $ do
evaluate (xorBytes BS.empty $ BS.pack [0x00]) `shouldThrow` anyException
evaluate (xorBytes (BS.pack [0x00]) BS.empty) `shouldThrow` anyException
evaluate (xorBytes (BS.pack [0x00, 0x00]) (BS.pack [0x00]))
`shouldThrow` anyException
evaluate (xorBytes (BS.pack [0x00]) (BS.pack [0x00, 0x00]))
`shouldThrow` anyException
xorBytes (BS.pack [0x00]) (BS.pack [0x00]) `shouldBe` BS.pack [0x00]
xorBytes (BS.pack [0x00]) (BS.pack [0xff]) `shouldBe` BS.pack [0xff]
xorBytes (BS.pack [0xff]) (BS.pack [0x00]) `shouldBe` BS.pack [0xff]
xorBytes (BS.pack [0xff]) (BS.pack [0xff]) `shouldBe` BS.pack [0x00]
xorBytes (BS.pack [0xaa]) (BS.pack [0x55]) `shouldBe` BS.pack [0xff]
xorBytes (BS.pack [0x55, 0xaa]) (BS.pack [0xaa, 0x55])
`shouldBe` BS.pack [0xff, 0xff]
xorBytes (BS.pack [0x7a, 0x54]) (BS.pack [0xd3, 0x8e])
`shouldBe` BS.pack [0xa9, 0xda]
it "can split entropy" $ do
splitEntropyWith (BS.pack [0x00]) [] `shouldBe` [BS.pack [0x00]]
splitEntropyWith (BS.pack [0x00]) [BS.pack [0x00]]
`shouldBe` [BS.pack [0x00], BS.pack [0x00]]
splitEntropyWith (BS.pack [0x55]) [BS.pack [0xaa]]
`shouldBe` [BS.pack [0xff], BS.pack [0xaa]]
splitEntropyWith (BS.pack [0x55, 0xaa]) [BS.pack [0xaa, 0x55]]
`shouldBe` [BS.pack [0xff, 0xff], BS.pack [0xaa, 0x55]]
prop "prop: can split entropy x2" $
forAll (arbitraryBSn 32) $ \s ->
forAll (arbitraryBSn 32) $ \k ->
splitEntropyWith s [k] `shouldBe` [s `xorBytes` k, k]
prop "prop: can split entropy x3" $
forAll (arbitraryBSn 32) $ \s ->
forAll (arbitraryBSn 32) $ \k1 ->
forAll (arbitraryBSn 32) $ \k2 ->
splitEntropyWith s [k1, k2]
`shouldBe` [s `xorBytes` k1 `xorBytes` k2, k1, k2]
prop "prop: can reconstruct entropy" $
forAll (arbitraryBSn 32) $ \s ->
forAll (arbitraryBSn 32) $ \k1 ->
forAll (arbitraryBSn 32) $ \k2 -> do
case splitEntropyWith s [k1, k2] of
[a, b, c] -> (a `xorBytes` b `xorBytes` c) `shouldBe` s
_ -> expectationFailure "Invalid splitEntropyWith"
prop "prop: can reconstruct original mnemonic" $
forAll (arbitraryBSn 32) $ \s ->
forAll (arbitraryBSn 32) $ \k1 ->
case splitEntropyWith s [k1] of
[a, b] ->
let mnem = toMnemonic s
splitMnems = mapM toMnemonic [a, b]
unsplitMnem = mergeMnemonicParts $ forceRight splitMnems
in unsplitMnem `shouldBe` mnem
_ -> expectationFailure "Invalid splitEntropyWith"
mnemonicSpec :: Ctx -> Config -> Spec
mnemonicSpec ctx cfg =
describe "Mnemonic API" $ do
-- https://github.com/iancoleman/bip39/issues/58
it "Can derive iancoleman issue 58" $ do
let m = "fruit wave dwarf banana earth journey tattoo true farm silk olive fence"
p = "banana"
mnemPass = MnemonicPass m p
xpub = forceRight $ deriveXPubKey ctx <$> signingKey btc ctx mnemPass 0
(addr0, _) = derivePathAddr ctx xpub extDeriv 0
addrToText btc addr0 `shouldBe` Just "17rxURoF96VhmkcEGCj5LNQkmN9HVhWb7F"
it "Passes the test vectors 3 (zero padding)" $
mapM_ (testVector ctx) testVectors3
it "Passes the test vectors 4 (zero padding)" $
mapM_ (testVector ctx) testVectors4
it "Passes the BIP-44 test vectors" $
mapM_ (testBip44Vector ctx cfg) bip44Vectors
testVector :: Ctx -> (Text, HardPath, Text, Text) -> Assertion
testVector ctx (seedT, deriv, pubT, prvT) = do
xPrvExport btc xPrv `shouldBe` prvT
xPubExport btc ctx xPub `shouldBe` pubT
where
seed = fromJust $ decodeHex seedT
xPrv = derivePath ctx deriv (makeXPrvKey seed)
xPub = deriveXPubKey ctx xPrv
-- bitpay/bitcore-lib#47 and iancoleman/bip39#58
-- https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#test-vector-3
testVectors3 :: [(Text, HardPath, Text, Text)]
testVectors3 =
[ ( "4b381541583be4423346c643850da4b320e46a87ae3d2a4e6da11eba819cd4acba45d239319ac14f863b8d5ab5a0d0c64d2e8a1e7d1457df2e5a3c51c73235be",
Deriv, -- m/
"xpub661MyMwAqRbcEZVB4dScxMAdx6d4nFc9nvyvH3v4gJL378CSRZiYmhRoP7mBy6gSPSCYk6SzXPTf3ND1cZAceL7SfJ1Z3GC8vBgp2epUt13",
"xprv9s21ZrQH143K25QhxbucbDDuQ4naNntJRi4KUfWT7xo4EKsHt2QJDu7KXp1A3u7Bi1j8ph3EGsZ9Xvz9dGuVrtHHs7pXeTzjuxBrCmmhgC6"
),
( "4b381541583be4423346c643850da4b320e46a87ae3d2a4e6da11eba819cd4acba45d239319ac14f863b8d5ab5a0d0c64d2e8a1e7d1457df2e5a3c51c73235be",
Deriv :| 0, -- m/0'
"xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y",
"xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L"
)
]
-- btcsuite/btcutil#172
-- https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#test-vector-4
testVectors4 :: [(Text, HardPath, Text, Text)]
testVectors4 =
[ ( "3ddd5602285899a946114506157c7997e5444528f3003f6134712147db19b678",
Deriv, -- m/
"xpub661MyMwAqRbcGczjuMoRm6dXaLDEhW1u34gKenbeYqAix21mdUKJyuyu5F1rzYGVxyL6tmgBUAEPrEz92mBXjByMRiJdba9wpnN37RLLAXa",
"xprv9s21ZrQH143K48vGoLGRPxgo2JNkJ3J3fqkirQC2zVdk5Dgd5w14S7fRDyHH4dWNHUgkvsvNDCkvAwcSHNAQwhwgNMgZhLtQC63zxwhQmRv"
),
( "3ddd5602285899a946114506157c7997e5444528f3003f6134712147db19b678",
Deriv :| 0, -- m/0'
"xpub69AUMk3qDBi3uW1sXgjCmVjJ2G6WQoYSnNHyzkmdCHEhSZ4tBok37xfFEqHd2AddP56Tqp4o56AePAgCjYdvpW2PU2jbUPFKsav5ut6Ch1m",
"xprv9vB7xEWwNp9kh1wQRfCCQMnZUEG21LpbR9NPCNN1dwhiZkjjeGRnaALmPXCX7SgjFTiCTT6bXes17boXtjq3xLpcDjzEuGLQBM5ohqkao9G"
),
( "3ddd5602285899a946114506157c7997e5444528f3003f6134712147db19b678",
Deriv :| 0 :| 1, -- m/0'/1'
"xpub6BJA1jSqiukeaesWfxe6sNK9CCGaujFFSJLomWHprUL9DePQ4JDkM5d88n49sMGJxrhpjazuXYWdMf17C9T5XnxkopaeS7jGk1GyyVziaMt",
"xprv9xJocDuwtYCMNAo3Zw76WENQeAS6WGXQ55RCy7tDJ8oALr4FWkuVoHJeHVAcAqiZLE7Je3vZJHxspZdFHfnBEjHqU5hG1Jaj32dVoS6XLT1"
)
]
testBip44Vector :: Ctx -> Config -> (Text, Text, Text, Text) -> Assertion
testBip44Vector ctx cfg (mnem, pass, addr0, addr1) = do
runDBMemoryE $ do
(accId, _) <- insertAccount btc ctx walletFP "test" pub
extAddr <- genExtAddress ctx cfg accId ""
intAddr <- nextFreeIntAddr ctx cfg accId
liftIO $ addr0 `shouldBe` dBAddressAddress extAddr
liftIO $ addr1 `shouldBe` dBAddressAddress intAddr
where
mnemPass = MnemonicPass mnem pass
walletFP = forceRight $ walletFingerprint btc ctx mnemPass
prv = forceRight $ signingKey btc ctx mnemPass 0
pub = deriveXPubKey ctx prv
-- (Mnemonic, BIP38 password, external 0/0, internal 1/0)
bip44Vectors :: [(Text, Text, Text, Text)]
bip44Vectors =
[ ( "modify truck lens identify brief coffee \
\gather volcano fatal together muscle elephant",
"",
"1KiWbwzHhwH2KdLyreGuJq36SP2pPeGeim",
"1BY9FShyC6rnEUdPAVQTX37TFZqEkR1enQ"
),
( "modify truck lens identify brief coffee \
\gather volcano fatal together muscle elephant",
"password",
"1Ha18XQ74YfCnuUAjGTF9rnbS7pm1aCzW4",
"17SounFZ11k8urp9Pee2FNYjzGU5qwLUfT"
),
( "modify truck lens identify brief coffee \
\gather volcano fatal together muscle elephant",
"Hello world",
"1KpHPSa9eMohQznkddzmHT9AeMAscYe29Y",
"16WJrKzdik1HgnoiNiLUW2uGDqry2fSGVx"
),
( "fiscal gadget drastic coconut awful crime \
\during common salmon manage random cost \
\evil owner city",
"",
"131dALC6WxwTZzTsHUsifAJ8bkQdDEeeqJ",
"1JHCmBLzDARCdDgn2WwJQr88tGW8V3EyKj"
),
( "host mind elephant tone sound apple \
\service tomato subject attend motion stick \
\fuel fan rail bamboo tree build",
"",
"15nbJ6oHvYiapKu1vBYmyQzVSTjiJNcfuV",
"12rMvadbhRxFaUP4FoRUmABa1JEPLrrqHL"
),
( "flight skill wisdom mixture patch dirt \
\trouble behind chair glad detect swarm \
\swap truly cruise medal walnut glide \
\wrestle route defy",
"",
"12uDDJU7BJUxJomzbFt5BW3GnukGZz6Y5t",
"1Gv62T4UkLxJ6dnsmNXENMgjBhLzEzJ1Sc"
),
( "use turtle trap pause spin venue \
\hazard hope slot april cattle fork \
\stand finish arrest nasty acoustic clog \
\entire course universe evil desert produce",
"",
"19fJdt5HjJUrmnvKM8fTS43o9aK3NmFZMT",
"17Kmofo8651DdydBS7zVVy9JLkixkxj6SK"
),
( "use turtle trap pause spin venue \
\hazard hope slot april cattle fork \
\stand finish arrest nasty acoustic clog \
\entire course universe evil desert produce",
"nMCm#Qe7u-*LG^99KS%hSNZJh6w&e&w4S&7HA-22^7vDE#F\
\@=hNZZZ$M=ZSYG42J8P+U+3sf$xS6YpJf7DemaU#fLEVBW8\
\!FmugPj=K=E*KDmUuQh_2%7K2W8@RJ4?Pua2HFCb8$@ZB^p\
\s=T=PNFy5APBBdaz_eTq&r&v3hp6P-tTfvNP9J+xfQ-FKZS\
\7_?g+P*9MTkpjy*K5=8y+y9db#Sv+b*5bfmV8?@YKrf#AEH\
\7!ARqBXMhS7d%GYAxB%RG",
"1L1oKPYJ9j3EFX5CcFXz1d3kkAW9Hoh5ic",
"1J7jpmP7NVoryofiVSCKMMvMitkCP3U6Qj"
)
]