packages feed

bip32-0.2: lib/BIP32.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE StrictData #-}

-- | [BIP-0032](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki)
-- Hierarchical Deterministic Wallets, for bitcoin and other cryptocurrencies.
module BIP32
  ( -- * Base58 serialization
    encodeXPrv
  , encodeXPub
  , decodeXPrv
  , decodeXPub
  , decode

    -- * Raw serialization
  , encodeXPrvRaw
  , encodeXPubRaw
  , decodeXPrvRaw
  , decodeXPubRaw
  , decodeRaw

    -- * Private key
  , XPrv(..)
  , xprvToXPub
    -- ** Single private subkey
  , subXPrvXPrv
  , subPrvPrv
    -- ** Multiple private subkeys
  , subsXPrvXPrv
  , subsPrvPrv
    -- ** Single public subkey
  , subXPrvXPub
  , subPrvPub
    -- ** Multiple public subkeys
  , subsXPrvXPub
  , subsPrvPub

    -- * Public key
  , XPub(..)
    -- ** Single public subkey
  , subXPubXPub
  , subPubPub
    -- ** Multiple public subkeys
  , subsXPubXPub
  , subsPubPub

    -- * Chain code
  , Chain
  , chain
  , unChain

    -- * Derivation path
  , Index(..)
  , indexIsHardened
  , indexNext

    -- * Depth
  , Depth(..)

    -- * Fingerprint
  , Fingerprint(..)
  , fingerprint

    -- * Version
  , Version(..)
    -- ** Example versions
  , version_xprv
  , version_xpub
  , version_tprv
  , version_tpub
  , version_Ltpv
  , version_Ltub
  , version_ttpv
  , version_ttub
  ) where

import Bitcoin.Hash (hash160, check32)
import Bitcoin.Hash.Prim (hmacSHA512)
import qualified Bitcoin.Keys as K
import Control.Applicative
import Control.Monad
import Data.Bits
import Data.Maybe
import qualified Data.Binary.Get as Bin
import qualified Data.ByteString as B
import qualified Data.ByteString.Base58 as B58
import qualified Data.ByteString.Builder as BB
import qualified Data.ByteString.Lazy as BL
import qualified Data.ByteString.Lazy.Char8 as BL8
import Data.Word

--------------------------------------------------------------------------------

-- | Extended private key.
data XPrv = XPrv
  { xprv_version     :: Version
  , xprv_depth       :: Depth
  , xprv_fingerprint :: Fingerprint
  , xprv_index       :: Index
  , xprv_chain       :: Chain
  , xprv_prv         :: K.Prv
  } deriving (Eq, Show)

-- | Obtain the 'XPub' corresponding to a particular 'XPrv', at a particular
-- 'Version'.
xprvToXPub :: Version -> XPrv -> XPub
xprvToXPub v (XPrv _ d f i c prv) = XPub v d f i c (K.prvToPub prv)

--------------------------------------------------------------------------------

-- | Extended private key.
data XPub = XPub
  { xpub_version     :: Version
  , xpub_depth       :: Depth
  , xpub_fingerprint :: Fingerprint
  , xpub_index       :: Index
  , xpub_chain       :: Chain
  , xpub_pub         :: K.Pub
  } deriving (Eq, Show)

--------------------------------------------------------------------------------

-- | The 33-byte serialized contents of either 'K.Pub' or 'K.Prv'.
newtype Key = Key B.ByteString

keyPub :: K.Pub -> Key
{-# INLINE keyPub #-}
keyPub = Key . K.pubCompressed

keyPrv :: K.Prv -> Key
{-# INLINE keyPrv #-}
keyPrv = Key . B.cons 0 . K.prvRaw

--------------------------------------------------------------------------------

-- | Version bytes.
newtype Version = Version { unVersion :: Word32 }
  deriving (Eq, Show)

-- | @0x0488ade4@, “xprv”, Bitcoin mainnet private.
version_xprv :: Version
version_xprv = Version 0x0488ade4
{-# INLINE version_xprv #-}

-- | @0x0488b21e@, “xpub”, Bitcoin mainnet public.
version_xpub :: Version
version_xpub = Version 0x0488b21e
{-# INLINE version_xpub #-}

-- | @0x04358394@, “tprv”, Bitcoin testnet private.
version_tprv :: Version
version_tprv = Version 0x04358394
{-# INLINE version_tprv #-}

-- | @0x043587cf@, “tpub”, Bitcoin testnet public.
version_tpub :: Version
version_tpub = Version 0x043587cf
{-# INLINE version_tpub #-}

-- | @0x019d9cfe@, “Ltpv”, Litecoin mainnet private.
version_Ltpv :: Version
version_Ltpv = Version 0x019d9cfe
{-# INLINE version_Ltpv #-}

-- | @0x019da462@, “Ltub”, Litecoin mainnet public.
version_Ltub :: Version
version_Ltub = Version 0x019da462
{-# INLINE version_Ltub #-}

-- | @0x0436ef7d@, “ttpv”, Litecoin testnet private.
version_ttpv :: Version
version_ttpv = Version 0x0436ef7d
{-# INLINE version_ttpv #-}

-- | @0x0436f6e1@, “ttub”, Litecoin testnet public.
version_ttub :: Version
version_ttub = Version 0x0436f6e1
{-# INLINE version_ttub #-}

--------------------------------------------------------------------------------

-- | Derivation path depth.
--
-- * @0@ for master nodes — @m@
--
-- * @1@ for level-1 derived keys — @m\/0'@, or @m\/13@, or …
--
-- * @2@ for level-2 derived keys — @m\/0'\/28@, or @m\/44'/0'@, or …
--
-- * … up to @255@.
newtype Depth = Depth { unDepth :: Word8 }
  deriving (Eq, Show)

depthNext :: Depth -> Maybe Depth
{-# INLINE depthNext #-}
depthNext (Depth w) = do
  guard (w /= 0xFF)
  pure (Depth (w + 1))

--------------------------------------------------------------------------------

-- | 4-byte fingerprint of a 'K.Pub' key.
newtype Fingerprint = Fingerprint { unFingerprint :: Word32 }
  deriving (Eq, Show)

fingerprint :: K.Pub -> Fingerprint
{-# INLINE fingerprint #-}
fingerprint = fingerprint' . keyId

fingerprint' :: KeyId -> Fingerprint
{-# INLINE fingerprint' #-}
fingerprint' (KeyId x) =
  let b = B.take 4 x :: B.ByteString
  in Fingerprint $! unsafeShiftL (fromIntegral (B.index b 0)) 24 .|.
                    unsafeShiftL (fromIntegral (B.index b 1)) 16 .|.
                    unsafeShiftL (fromIntegral (B.index b 2))  8 .|.
                                  fromIntegral (B.index b 3)

--------------------------------------------------------------------------------

-- | 20-byte key identifier.
newtype KeyId = KeyId B.ByteString

keyId :: K.Pub -> KeyId
{-# INLINE keyId #-}
keyId = KeyId . hash160 . K.pubCompressed

--------------------------------------------------------------------------------

-- | Chain code.
--
-- Construct with 'chain'.
newtype Chain = Chain B.ByteString
  deriving (Eq)

instance Show Chain where
  showsPrec n (Chain b) = showParen (n > 10) $
    showString "Chain " .
    mappend (BL8.unpack (BB.toLazyByteString (BB.byteStringHex b)))

-- | Construct a 'Chain' code.
--
-- See Bitcoin's [BIP-0032](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki)
-- for details.
--
-- 'Nothing' if the 'B.ByteString' length is not 32.
chain :: B.ByteString -> Maybe Chain
{-# INLINE chain #-}
chain x = do
  guard (B.length x == 32)
  Just (Chain x)

-- | Obtain the 32 raw bytes inside a 'Chain'.
unChain :: Chain -> B.ByteString
{-# INLINE unChain #-}
unChain (Chain x) = x

--------------------------------------------------------------------------------

-- | A derivation path 'Index'.
newtype Index = Index { unIndex :: Word32 }
  deriving (Eq, Ord, Show)

-- | Whether a derivation path 'Index' is hardened. That is, \(2^{31}\) or
-- larger.
indexIsHardened :: Index -> Bool
{-# INLINE indexIsHardened #-}
indexIsHardened (Index w) = w >= 0x80000000

-- | Increment the given 'Index' by one, but only if doing so would result in a
-- new 'Index' in the same index group (normal vs. hardened). Otherwise, returns
-- 'Nothing'.
indexNext :: Index -> Maybe Index
{-# INLINE indexNext #-}
indexNext (Index w) = case w of
  0xffffffff -> Nothing -- Would be a normal to hardened transition
  0x7fffffff -> Nothing -- Would be a hardened to normal transition
  _          -> Just (Index (w + 1))

--------------------------------------------------------------------------------

-- | Derive a 'XPrv' subkey from a parent 'XPrv' key.
--
-- Returns 'Nothing' if the given inputs result in an invalid key.
subXPrvXPrv :: XPrv -> Index -> Maybe XPrv
subXPrvXPrv (XPrv v d0 _ _ ch0 prv0) i1 = do
  d1 <- depthNext d0
  (ch1, prv1) <- subPrvPrv ch0 prv0 i1
  let f1 = fingerprint (K.prvToPub prv0)
  pure $ XPrv v d1 f1 i1 ch1 prv1

-- | Derive a 'XPub' subkey from a parent 'XPub' key.
--
-- The given 'Index' is expected to /not/ be hardened.
--
-- Returns 'Nothing' if the given inputs result in an invalid key.
subXPubXPub :: XPub -> Index -> Maybe XPub
subXPubXPub (XPub v d0 _ _ ch0 pub0) i1 = do
  d1 <- depthNext d0
  (ch1, pub1) <- subPubPub ch0 pub0 i1
  let f1 = fingerprint pub0
  pure $ XPub v d1 f1 i1 ch1 pub1

-- | Derive 'XPub' subkey from a parent 'XPrv' key.
--
-- Notice that while @'subXPubXPub' ('xprvToXPub' v) xprv i@ will fail with a
-- hardened 'Index', @'subXPrvXPub' v xprv i@ may succeed.
--
-- Returns 'Nothing' if the given inputs result in an invalid key.
subXPrvXPub :: Version -> XPrv -> Index -> Maybe XPub
subXPrvXPub v xprv i1 = xprvToXPub v <$> subXPrvXPrv xprv i1

--------------------------------------------------------------------------------

-- | Behaves exactly like 'subXPrvXPrv', but deals with 'Chain' and
-- 'K.Prv', rather than an 'XPrv'.
subPrvPrv :: Chain -> K.Prv -> Index -> Maybe (Chain, K.Prv)
subPrvPrv (Chain ch0) prv0 i1@(Index i1w) = do
  let pub0 = K.prvToPub prv0
      x = hmacSHA512 ch0 $ mconcat $ if indexIsHardened i1
            then [B.singleton 0, K.prvRaw prv0, word32BE i1w]
            else [K.pubCompressed pub0, word32BE i1w]
      (xl, xr) = B.splitAt 32 x
  prv1 <- flip K.prvAddTweak prv0 =<< K.parseTweak xl
  ch1 <- chain xr
  pure (ch1, prv1)

-- | Behaves exactly like 'subXPubXPub', but deals with 'Chain' and
-- 'K.Pub', rather than an 'XPub'.
subPubPub :: Chain -> K.Pub -> Index -> Maybe (Chain, K.Pub)
subPubPub (Chain ch0) pub0 i1@(Index i1w) = do
  guard (not (indexIsHardened i1))
  let x = hmacSHA512 ch0 (K.pubCompressed pub0 <> word32BE i1w)
      (xl, xr) = B.splitAt 32 x
  pub1 <- flip K.pubAddTweak pub0 =<< K.parseTweak xl
  ch1 <- chain xr
  pure (ch1, pub1)

-- | Behaves exactly like 'subXPrvXPub', but deals with 'Chain', 'K.Prv' and
-- 'K.Pub', rather than an 'XPrv' and 'XPub'.
subPrvPub :: Chain -> K.Prv -> Index -> Maybe (Chain, K.Pub)
subPrvPub ch0 prv0 i1 = fmap K.prvToPub <$> subPrvPrv ch0 prv0 i1

--------------------------------------------------------------------------------

-- | Lazily derive all the child 'XPrv' subkeys from a parent's 'XPrv'.
--
-- The first child returned is the one at the given 'Index', if any, and the
-- subsequent childs are those immediately after it (see 'indexNext').
--
-- The returned 'Index'es are not necessarily consecutive, since not all
-- 'XPrv' and 'Index' combinations are able to derive valid keys.
-- However, the produced 'Index'es do increase monotonically.
--
-- If the given 'Index' is hardened, then all produced children will have
-- hardened 'Index'es too. If the given 'Index' is normal, then all children
-- will have normal 'Index'es too. That is, this function will never produce
-- children with 'Index'es belonging to another group (normal vs. hardened).
subsXPrvXPrv :: XPrv -> Index -> [XPrv]
subsXPrvXPrv (XPrv v d0 _ _ ch0 prv0) i = do
  d1 <- maybeToList $ depthNext d0
  let f1 = fingerprint (K.prvToPub prv0)
  (i1, ch1, prv1) <- subsPrvPrv ch0 prv0 i
  pure $ XPrv v d1 f1 i1 ch1 prv1

-- | Lazily derive all the 'XPub' subkeys from a parent's 'XPub'.
--
-- The first child returned is the one at the given 'Index', if any, and the
-- subsequent childs are those immediately after it (see 'indexNext').
--
-- The returned 'Index'es are not necessarily consecutive, since not all
-- 'XPub' and 'Index' combinations are able to derive valid keys.
-- However, the produced 'Index'es do increase monotonically.
--
-- The produced list will never include a hardened 'Index'. If a hardened
-- 'Index' is provided as input, the produced list will be empty.
subsXPubXPub :: XPub -> Index -> [XPub]
subsXPubXPub (XPub v d0 _ _ ch0 pub0) i = do
  d1 <- maybeToList $ depthNext d0
  let f1 = fingerprint pub0
  (i1, ch1, pub1) <- subsPubPub ch0 pub0 i
  pure $ XPub v d1 f1 i1 ch1 pub1

-- | Lazily derive all the 'XPub' subkeys from a parent 'XPrv' key.
--
-- The first child returned is the one at the given 'Index', if any, and the
-- subsequent childs are those immediately after it (see 'indexNext').
--
-- The returned 'Index'es are not necessarily consecutive, since not all
-- 'XPrv' and 'Index' combinations are able to derive valid keys.
-- However, the produced 'Index'es do increase monotonically.
--
-- If the given 'Index' is hardened, then all produced children will have
-- hardened 'Index'es too. If the given 'Index' is normal, then all children
-- will have normal 'Index'es too. That is, this function will never produce
-- children with 'Index'es belonging to another group (normal vs. hardened).
--
-- /Notice/ that while @'subsXPubXPub' ('xprvToXPub' v) xprv i@ will fail with a
-- hardened 'Index', @'subsXPrvXPub' v xprv i@ may succeed.
subsXPrvXPub :: Version -> XPrv -> Index -> [XPub]
subsXPrvXPub v xprv i1 = xprvToXPub v <$> subsXPrvXPrv xprv i1

----------------------------------------------------------------------------------

-- | Behaves exactly like 'subsXPrvXPrv', but deals with 'Chain' and
-- 'K.Prv', rather than an 'XPrv'.
subsPrvPrv :: Chain -> K.Prv -> Index -> [(Index, Chain, K.Prv)]
subsPrvPrv ch0 prv0 = go
  where
    go :: Index -> [(Index, Chain, K.Prv)]
    go i = let rest = maybe [] go (indexNext i)
           in case subPrvPrv ch0 prv0 i of
                Just (ch1, prv1) -> (i, ch1, prv1) : rest
                Nothing          -> rest

-- | Behaves exactly like 'subsXPubXPub', but deals with 'Chain' and
-- 'K.Pub', rather than an 'XPub'.
subsPubPub :: Chain -> K.Pub -> Index -> [(Index, Chain, K.Pub)]
subsPubPub ch0 pub0 i0
  | indexIsHardened i0 = []
  | otherwise          = go i0
  where
    go :: Index -> [(Index, Chain, K.Pub)]
    go i = let rest = maybe [] go (indexNext i)
           in case subPubPub ch0 pub0 i of
                Just (ch1, pub1) -> (i, ch1, pub1) : rest
                Nothing          -> rest

-- | Behaves exactly like 'subsXPrvXPub', but deals with 'Chain', 'K.Prv' and
-- 'K.Pub', rather than an 'XPrv' and 'XPub'.
subsPrvPub :: Chain -> K.Prv-> Index -> [(Index, Chain, K.Pub)]
subsPrvPub ch0 prv0 i0 = do
  (i1, ch1, prv1) <- subsPrvPrv ch0 prv0 i0
  pure (i1, ch1, K.prvToPub prv1)

--------------------------------------------------------------------------------

-- | Obtain the Base58 representation for an 'XPub'.
--
-- It can be either 111 or 112 bytes in length, and when rendered as ASCII it
-- looks something like this:
--
-- @
-- xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8
-- @
encodeXPub :: XPub -> B.ByteString
{-# INLINE encodeXPub #-}
encodeXPub a = base58EncodeWithChecksum (encodeXPubRaw a)

-- | Obtain the Base58 representation for an 'XPrv'.
--
-- It can be either 111 or 112 bytes in length, and when rendered as ASCII it
-- looks something like this:
--
-- @
-- xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi
-- @
encodeXPrv :: XPrv -> B.ByteString
{-# INLINE encodeXPrv #-}
encodeXPrv a = base58EncodeWithChecksum (encodeXPrvRaw a)

base58EncodeWithChecksum :: B.ByteString -> B.ByteString
{-# INLINE base58EncodeWithChecksum #-}
base58EncodeWithChecksum a =
  B58.encodeBase58 B58.bitcoinAlphabet (a <> check32 a)

--------------------------------------------------------------------------------

-- | Encode the 78 raw 'XPub' bytes.
encodeXPubRaw :: XPub -> B.ByteString
{-# INLINE encodeXPubRaw #-}
encodeXPubRaw (XPub v d f i c p) = encodeRaw v d f i c (keyPub p)

-- | Encode the 78 raw 'XPrv' bytes.
encodeXPrvRaw :: XPrv -> B.ByteString
{-# INLINE encodeXPrvRaw #-}
encodeXPrvRaw (XPrv v d f i c p) = encodeRaw v d f i c (keyPrv p)

-- | Encode the 78 raw extended key bytes.
encodeRaw
  :: Version -> Depth -> Fingerprint -> Index -> Chain -> Key -> B.ByteString
encodeRaw (Version v) (Depth d) (Fingerprint f) (Index i) (Chain c) (Key k) =
  BL.toStrict $ BB.toLazyByteString $
    BB.word32BE v <>
    BB.word8 d <>
    BB.word32BE f <>
    BB.word32BE i <>
    BB.byteString c <>
    BB.byteString k

--------------------------------------------------------------------------------

-- | Decode the Base58-encoded 'XPub' representation. See 'encodeXPub'.
decodeXPub :: B.ByteString -> Maybe XPub
{-# INLINE decodeXPub #-}
decodeXPub = decodeXPubRaw <=< base58DecodeWithChecksum

-- | Decode the Base58-encoded 'XPrv' representation. See 'encodeXPrv'.
decodeXPrv :: B.ByteString -> Maybe XPrv
{-# INLINE decodeXPrv #-}
decodeXPrv = decodeXPrvRaw <=< base58DecodeWithChecksum

-- | Decode the Base58-encoded representation of either and 'XPub' or an 'XPub'.
decode :: B.ByteString -> Maybe (Either XPub XPrv)
{-# INLINE decode #-}
decode = decodeRaw <=< base58DecodeWithChecksum

base58DecodeWithChecksum :: B.ByteString -> Maybe B.ByteString
{-# INLINE base58DecodeWithChecksum #-}
base58DecodeWithChecksum = \a -> do
  b <- B58.decodeBase58 B58.bitcoinAlphabet a
  guard (B.length b == 78 + 4)
  let (raw, ch) = B.splitAt 78 b
  guard (ch == check32 raw)
  pure raw

--------------------------------------------------------------------------------

-- | Decode the 78 raw 'XPub' bytes.
decodeXPubRaw :: B.ByteString -> Maybe XPub
{-# INLINE decodeXPubRaw #-}
decodeXPubRaw = either Just (\_ -> Nothing) <=< decodeRaw

-- | Decode the 78 raw 'XPrv' bytes.
decodeXPrvRaw :: B.ByteString -> Maybe XPrv
{-# INLINE decodeXPrvRaw #-}
decodeXPrvRaw = either (\_ -> Nothing) Just <=< decodeRaw

-- | Encode the 78 raw 'XPub' or 'XPrv' bytes.
decodeRaw :: B.ByteString -> Maybe (Either XPub XPrv)
decodeRaw = \b -> do
  guard (B.length b == 78)
  case Bin.runGetOrFail getX (BL.fromStrict b) of
    Right (lo, 78, ex) | BL.null lo -> Just ex
    _ -> Nothing

getX :: Bin.Get (Either XPub XPrv)
getX = do
  v <- Version <$> Bin.getWord32be
  d <- Depth <$> Bin.getWord8
  f <- Fingerprint <$> Bin.getWord32be
  i <- Index <$> Bin.getWord32be
  c <- Chain <$> Bin.getByteString 32
  ek <- fmap Left getPub <|> fmap Right getPrv
  pure $ case ek of
    Left  k -> Left  (XPub v d f i c k)
    Right k -> Right (XPrv v d f i c k)

getPrv :: Bin.Get K.Prv
getPrv = do
  0 <- Bin.getWord8
  a <- Bin.getByteString 32
  case K.parsePrv a of
    Just b -> pure b
    Nothing -> fail "Bad private key"

getPub :: Bin.Get K.Pub
getPub = do
  a <- Bin.getByteString 33
  case K.parsePub a of
    Just b -> pure b
    Nothing -> fail "Bad public key"

--------------------------------------------------------------------------------

word32BE :: Word32 -> B.ByteString -- ^ 4 bytes.
{-# INLINE word32BE #-}
word32BE = BL.toStrict . BB.toLazyByteString . BB.word32BE