packages feed

cardano-addresses-4.0.0: test/Cardano/Address/Style/IcarusSpec.hs

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}

{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-}

module Cardano.Address.Style.IcarusSpec
    ( spec
    ) where

import Prelude

import Cardano.Address
    ( PaymentAddress (..), base58 )
import Cardano.Address.Derivation
    ( Depth (..)
    , DerivationType (..)
    , GenMasterKey (..)
    , HardDerivation (..)
    , Index (..)
    , SoftDerivation (..)
    , XPrv
    , indexFromWord32
    , toXPub
    , unsafeMkIndex
    )
import Cardano.Address.Style.Icarus
    ( Icarus (..)
    , Role (..)
    , icarusMainnet
    , roleToIndex
    , unsafeGenerateKeyFromHardwareLedger
    )
import Cardano.Mnemonic
    ( ConsistentEntropy
    , EntropySize
    , SomeMnemonic (..)
    , mkMnemonic
    , mkSomeMnemonic
    )
import Control.Monad
    ( forM_ )
import Data.Proxy
    ( Proxy (..) )
import Data.Text
    ( Text )
import Data.Word
    ( Word32 )
import Test.Arbitrary
    ( unsafeMkSomeMnemonicFromEntropy )
import Test.Hspec
    ( Spec, describe, it, shouldBe )
import Test.QuickCheck
    ( Property, property, (===) )

import qualified Data.Text as T

spec :: Spec
spec = do
    describe "BIP-0044 Derivation Properties" $ do
        it "deriveAccountPrivateKey works for various indexes" $
            property prop_accountKeyDerivation
        it "N(CKDpriv((kpar, cpar), i)) === CKDpub(N(kpar, cpar), i)" $
            property prop_publicChildKeyDerivation

    describe "Golden Tests - Icarus' style addresses" $ do
        let seed0 = unsafeMkSomeMnemonicFromEntropy (Proxy @15)
                "4\175\242L\184\243\191 \169]\171 \207\r\v\233\NUL~&\ETB"
            mkSoftIx = unsafeMkIndex :: Word32 -> Index 'Soft depth
            mkHardIx = unsafeMkIndex :: Word32 -> Index 'Hardened depth

        goldenAddressGeneration $ GoldenAddressGeneration
            seed0 (mkHardIx 0x80000000) UTxOExternal (mkSoftIx 0x00000000)
            "Ae2tdPwUPEZGQVrA6qKreDzdtYxcWMMrpTFYCpFcuJfhJBEfoeiuW4MtaXZ"

        goldenAddressGeneration $ GoldenAddressGeneration
            seed0 (mkHardIx 0x80000000) UTxOExternal (mkSoftIx 0x0000000E)
            "Ae2tdPwUPEZDLWQQEBR1UW7HeXJVaqUnuw8DUFu52TDWCJbxbkCyQYyxckP"

        goldenAddressGeneration $ GoldenAddressGeneration
            seed0 (mkHardIx 0x8000000E) UTxOInternal (mkSoftIx 0x0000002A)
            "Ae2tdPwUPEZFRbyhz3cpfC2CumGzNkFBN2L42rcUc2yjQpEkxDbkPodpMAi"

        let (Right seed1) = mkSomeMnemonic @'[12]
              [ "ghost", "buddy", "neutral", "broccoli", "face", "rack"
              , "relief", "odor", "swallow", "real", "once", "ecology"
              ]

        goldenAddressGeneration $ GoldenAddressGeneration
            seed1 (mkHardIx 0x80000000) UTxOExternal (mkSoftIx 0x00000000)
            "Ae2tdPwUPEYz6ExfbWubiXPB6daUuhJxikMEb4eXRp5oKZBKZwrbJ2k7EZe"

        goldenAddressGeneration $ GoldenAddressGeneration
            seed1 (mkHardIx 0x80000000) UTxOExternal (mkSoftIx 0x00000001)
            "Ae2tdPwUPEZCJUCuVgnysar8ZJeyKuhjXU35VNgKMMTcXWmS9zzYycmwKa4"

        goldenAddressGeneration $ GoldenAddressGeneration
            seed1 (mkHardIx 0x80000000) UTxOExternal (mkSoftIx 0x00000002)
            "Ae2tdPwUPEZFJtMH1m5HvsaQZrmgLcVcyuk5TxYtdRHZFo8yV7yEnnJyqTs"

    describe "Hardware Ledger" $ do
        goldenHardwareLedger @12
            [ "struggle", "section", "scissors", "siren", "garbage", "yellow"
            , "maximum", "finger", "duty", "require", "mule", "earn"
            ]
            [ "Ae2tdPwUPEZ4Gs4s2recjNjQHBKfuBTkeuqbHJJrC6CuyjGyUD44cCTq4sJ"
            , "Ae2tdPwUPEZ8ozZuJWsLVb7aEb5p9ntcja47B9i68GV3y9by1eY5C2y6WUT"
            , "Ae2tdPwUPEZJoUCoyoCxUAKAbn2vFo6nu6B7aTWL1Pv9MRKm8unG9ixLurg"
            , "Ae2tdPwUPEYwFNKLxqF8s31nbaNt5MZisVqsQ5qsiY763HY5wsBN3mSzPRa"
            , "Ae2tdPwUPEZ4ZXzzehKoWWC9QYVqJfEL9x63zjH6wyEJbNRsZ9eccR6nSpv"
            , "Ae2tdPwUPEYyX7ug8zm6K7nLWhgEEBo7Ewf1qALxkvqyHHSC5jMFzH418Q1"
            , "Ae2tdPwUPEZ95eCwDjNQjReRkeLZFv6kBs3vwaKPHJsw2cxXc3HaCD2jzqw"
            , "Ae2tdPwUPEZDHGbQ9sbLZuw3cfhcSzqqdK8Xj3dhAzmWZGeVgJhncu5LR9N"
            , "Ae2tdPwUPEYyDca1eVbeEea6CjihoMAgt6mPiNuC1hEpy5U2qQ1Tzt6E8q8"
            , "Ae2tdPwUPEZHRMjjXMT2icJXp5h2k2j3Ph6dB5iGRashA2QxHLgFZbHzdms"
            ]

        goldenHardwareLedger @18
            [ "vague" , "wrist" , "poet" , "crazy" , "danger" , "dinner"
            , "grace" , "home" , "naive" , "unfold" , "april" , "exile"
            , "relief" , "rifle" , "ranch" , "tone" , "betray" , "wrong"
            ]
            [ "Ae2tdPwUPEZMCGyPAK85FrcserPvzVZZUcbFk5TvDmL9LrUyq2KPYubPcru"
            , "Ae2tdPwUPEZ6drrnNd1KW3UoiU3U1ZK3mxSpQpFAdXzJHuwvDcYB7Wzxkp1"
            , "Ae2tdPwUPEZ7Jaw9qt1q2CjCcds6zpHMyzmPGDh9tBeyQG28AdRGHcaWYx7"
            , "Ae2tdPwUPEZ9SW4qxWkFoozTux5i7F9jVpHQFQUycQuNanSUScyMTYrnQXK"
            , "Ae2tdPwUPEZ6YegpN8XurGfWyKqkNHLgdbHpdohumKt5QpkNVJhw4FCSRdo"
            , "Ae2tdPwUPEZLgrXt3zJeHgFWM2stxRjdm6wWATSoUzJ1CmUxKqgbYQXR8cC"
            , "Ae2tdPwUPEZ6axGCfo5nCLn5hEoRo4yNmQKBzn12B2quPncgQRFP6JBZ2ex"
            , "Ae2tdPwUPEYzdHGmJDL9tEWXfzyshohvzyS3K9wmLc5qMrwRNFPQA611uzB"
            , "Ae2tdPwUPEYxLNQJXcT3XUh54BXn5w53pPe5EHMXo6qo47gpNM9QyJsaXz4"
            , "Ae2tdPwUPEYvq2fnzqs9EWxFF2j87nZzBAZZ7y3qoj5oTce1ZGvsc4potp3"
            ]

        goldenHardwareLedger @24
            [ "recall" , "grace" , "sport" , "punch" , "exhibit" , "mad"
            , "harbor" , "stand" , "obey" , "short" , "width" , "stem"
            , "awkward" , "used" , "stairs" , "wool" , "ugly" , "trap"
            , "season" , "stove" , "worth" , "toward" , "congress" , "jaguar"
            ]
            [ "Ae2tdPwUPEZFvG914wGXtCsb9hCr9aKjJC2ZciLKSNRqAKtjnduH7XtPn78"
            , "Ae2tdPwUPEZ8rVsdBE6EMZpac32MLzciY75MrwrPs8ikjf6MWYFJUHkGaw5"
            , "Ae2tdPwUPEZADQdQy2cbHDwwFRYUcrfreiu82Ngm9Bxdw1pJqJFUnFoQmNL"
            , "Ae2tdPwUPEZ3NULtb3fK6qtJYwJbVnmhDeWzoMbjzPbCsEC9MyB4foBABhz"
            , "Ae2tdPwUPEZ3rGvPCdzCPrVRvzEfpUp8XnZ861nss3XfLun5wA3c3YMA41v"
            , "Ae2tdPwUPEZ575pMY9TBJyPdrwGkq2kr49V9fuqRWpF6wM9JbuZLmxHDo2N"
            , "Ae2tdPwUPEZFaVKwy9bcN81ZPVL8uHRfsrCj7ZZhbm2uqiwLrzsy9Bs1rBN"
            , "Ae2tdPwUPEZ4K16qFm6qVRWTEGpq5TJiyt8ZojmRANTSpPDAWZuH2Ge85uB"
            , "Ae2tdPwUPEZMMYd8JP9F16HJgCsDsPjUoERWoFzZugN4mNjhR9ZnFwPonCs"
            , "Ae2tdPwUPEZ3anXo172NFuumSGjrvbk1pHK9LiF82nGmPKC52NMYR77V2dM"
            ]

{-------------------------------------------------------------------------------
                                 Properties
-------------------------------------------------------------------------------}

-- | Deriving address public key should be equal to deriving address
-- private key and extracting public key from it (works only for non-hardened
-- child keys).
--
-- To compute the public child key of a parent private key:
--  * N(CKDpriv((kpar, cpar), i)) (works always).
--  * CKDpub(N(kpar, cpar), i) (works only for non-hardened child keys).
--
-- Thus:
--
-- N(CKDpriv((kpar, cpar), i)) === CKDpub(N(kpar, cpar), i)
--
-- if (kpar, cpar) is a non-hardened key.
--
-- For details see <https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#private-parent-key--public-child-key bip-0039>
prop_publicChildKeyDerivation
    :: SomeMnemonic
    -> Role
    -> Index 'Soft 'PaymentK
    -> Property
prop_publicChildKeyDerivation mw role ix =
    addrXPub1 === addrXPub2
  where
    rootXPrv = genMasterKeyFromMnemonic mw mempty :: Icarus 'RootK XPrv
    accXPrv  = deriveAccountPrivateKey rootXPrv minBound
    -- N(CKDpriv((kpar, cpar), i))
    addrXPub1 = toXPub <$> deriveAddressPrivateKey accXPrv role ix
    -- CKDpub(N(kpar, cpar), i)
    addrXPub2 = deriveAddressPublicKey (toXPub <$> accXPrv) role ix

prop_accountKeyDerivation
    :: SomeMnemonic
    -> Index 'Hardened 'AccountK
    -> Property
prop_accountKeyDerivation mw ix =
    accXPrv `seq` property () -- NOTE Making sure this doesn't throw
  where
    rootXPrv = genMasterKeyFromMnemonic mw mempty :: Icarus 'RootK XPrv
    accXPrv = deriveAccountPrivateKey rootXPrv ix

{-------------------------------------------------------------------------------
                               Golden Tests
-------------------------------------------------------------------------------}

data GoldenAddressGeneration = GoldenAddressGeneration
    { goldSeed :: SomeMnemonic
    , goldAcctIx :: Index 'Hardened 'AccountK
    , goldAcctStyle :: Role
    , goldAddrIx :: Index 'Soft 'PaymentK
    , goldAddr :: Text
    }

-- | Compare addresses obtained from a given derivation path and a root seed to
-- their known equivalent in base58.
goldenAddressGeneration
    :: GoldenAddressGeneration
    -> Spec
goldenAddressGeneration test = it title $ do
    let rootXPrv = genMasterKeyFromMnemonic goldSeed mempty :: Icarus 'RootK XPrv
    let acctXPrv = deriveAccountPrivateKey rootXPrv goldAcctIx
    let addrXPrv = deriveAddressPrivateKey acctXPrv goldAcctStyle goldAddrIx
    base58 (paymentAddress icarusMainnet $ toXPub <$> addrXPrv)
        `shouldBe` goldAddr
  where
    GoldenAddressGeneration
        { goldSeed
        , goldAddr
        , goldAcctIx
        , goldAddrIx
        , goldAcctStyle
        } = test

    title = unwords
        [ fmtPath goldAcctIx goldAcctStyle goldAddrIx
        , "-->"
        , T.unpack goldAddr
        ]

    -- e.g. m/.../0'/0/0
    fmtPath p3 p4 p5 = mconcat
        [ "m/.../"
        , show (indexToWord32 p3 - indexToWord32 (minBound @(Index 'Hardened _)))
        , "'/"
        , show (indexToWord32 (roleToIndex p4))
        , "/"
        , show (indexToWord32 p5)
        ]

goldenHardwareLedger
    :: forall mw ent csz.
        ( ConsistentEntropy ent mw csz
        , EntropySize mw ~ ent
        )
    => [Text]
        -- ^ 24-word mnemonic
    -> [Text]
        -- ^ Some addresses, starting at index 0
    -> Spec
goldenHardwareLedger sentence addrs =
    it title $ do
        let Right mnemonic = SomeMnemonic <$> mkMnemonic @mw sentence
        let rootXPrv = unsafeGenerateKeyFromHardwareLedger mnemonic
        let acctXPrv = deriveAccountPrivateKey rootXPrv minBound
        let deriveAddr = deriveAddressPrivateKey acctXPrv UTxOExternal

        forM_ (zip [0..] addrs) $ \(ix, addr) -> do
            let Just softIx = indexFromWord32 @(Index 'Soft _) ix
                addrXPrv = deriveAddr softIx
            base58 (paymentAddress icarusMainnet $ toXPub <$> addrXPrv)
                `shouldBe` addr
  where
    title = T.unpack
        $ T.unwords
        $ take 3 sentence ++ [ "..." ] ++ drop (length sentence - 3) sentence