Skip to content

Commit

Permalink
port address derivation module from cardano-wallet-legacy
Browse files Browse the repository at this point in the history
  • Loading branch information
KtorZ committed Mar 12, 2019
1 parent 1a920d7 commit d3dee38
Show file tree
Hide file tree
Showing 2 changed files with 288 additions and 9 deletions.
21 changes: 12 additions & 9 deletions cardano-wallet.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ library
base
, binary
, bytestring
, cardano-crypto
, cborg
, containers
, cryptonite
Expand All @@ -51,12 +52,13 @@ library
src
exposed-modules:
Cardano.ChainProducer.RustHttpBridge.Api
, Cardano.ChainProducer.RustHttpBridge.Client
, Cardano.Wallet.Binary
, Cardano.Wallet.Binary.Packfile
, Cardano.Wallet.BlockSyncer
, Cardano.Wallet.Primitive
, Servant.Extra.ContentTypes
Cardano.ChainProducer.RustHttpBridge.Client
Cardano.Wallet.AddressDerivation
Cardano.Wallet.Binary
Cardano.Wallet.Binary.Packfile
Cardano.Wallet.BlockSyncer
Cardano.Wallet.Primitive
Servant.Extra.ContentTypes
other-modules:
Paths_cardano_wallet

Expand Down Expand Up @@ -110,7 +112,8 @@ test-suite unit
main-is:
Main.hs
other-modules:
Cardano.Wallet.AddressDerivationSpec
Cardano.Wallet.Binary.PackfileSpec
Cardano.Wallet.BinarySpec
, Cardano.Wallet.Binary.PackfileSpec
, Cardano.Wallet.PrimitiveSpec
, Cardano.Wallet.BlockSyncerSpec
Cardano.Wallet.BlockSyncerSpec
Cardano.Wallet.PrimitiveSpec
276 changes: 276 additions & 0 deletions src/Cardano/Wallet/AddressDerivation.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeApplications #-}

-- |
-- Copyright: © 2018-2019 IOHK
-- License: MIT
--
-- Primitives for performing address derivation for some given schemes. This is
-- where most of the crypto happens in the wallet and, it is quite important to
-- ensure that the following implementation matches with other wallet softwares
-- (like Yoroi/Icarus or the cardano-cli)

module Cardano.Wallet.AddressDerivation
(
-- * Polymorphic / General Purpose Types
Key
, Depth (..)
, Index
, getIndex
, DerivationType (..)
, Passphrase(..)
, publicKey

-- * Sequential Derivation
, ChangeChain(..)
, generateKeyFromSeed
, unsafeGenerateKeyFromSeed
, deriveAccountPrivateKey
, deriveAddressPrivateKey
, deriveAddressPublicKey
) where

import Prelude

import Cardano.Crypto.Wallet
( DerivationScheme (..)
, XPrv
, XPub
, deriveXPrv
, deriveXPub
, generateNew
, toXPub
)
import Data.ByteArray
( ScrubbedBytes )
import Data.ByteString
( ByteString )
import Data.Maybe
( fromMaybe )
import Data.Word
( Word32 )
import GHC.Generics
( Generic )


{-------------------------------------------------------------------------------
Polymorphic / General Purpose Types
-------------------------------------------------------------------------------}

-- | A cryptographic key, with phantom-types to disambiguate key types.
--
-- @
-- let rootPrivateKey = Key 'RootK XPrv
-- let accountPubKey = Key 'AccountK XPub
-- let addressPubKey = Key 'AddressK XPub
-- @
newtype Key (level :: Depth) key = Key key
deriving stock (Generic, Show, Eq)

-- | Key Depth in the derivation path, according to BIP-0039 / BIP-0044
--
-- root' / purpose' / cointype' / account' / change / address
--
-- We do not manipulate purpose, cointype and change paths directly, so they are
-- left out of the sum type.
data Depth = RootK | AccountK | AddressK

-- | A derivation index, with phantom-types to disambiguate derivation type.
--
-- @
-- let accountIx = Index 'Hardened 'AccountK
-- let addressIx = Index 'Soft 'AddressK
-- @
newtype Index (derivationType :: DerivationType) (level :: Depth) = Index
{ getIndex :: Word32 }
deriving stock (Show, Eq, Ord)

instance Bounded (Index 'Hardened level) where
minBound = Index 0x80000000
maxBound = Index maxBound

instance Bounded (Index 'Soft level) where
minBound = Index minBound
maxBound = let (Index ix) = minBound @(Index 'Hardened _) in Index (ix - 1)

instance Enum (Index 'Hardened level) where
fromEnum (Index ix) = fromIntegral ix
toEnum ix
| Index (fromIntegral ix) < minBound @(Index 'Hardened _) =
error "Index@Hardened.toEnum: bad argument"
| otherwise =
Index (fromIntegral ix)

instance Enum (Index 'Soft level) where
fromEnum (Index ix) = fromIntegral ix
toEnum ix
| Index (fromIntegral ix) > maxBound @(Index 'Soft _) =
error "Index@Soft.toEnum: bad argument"
| otherwise =
Index (fromIntegral ix)


-- | Type of derivation that should be used with the given indexes.
data DerivationType = Hardened | Soft

-- | An encapsulated passphrase. The inner format is free, but the wrapper helps
-- readability in function signatures.
--
-- Note that the internal type is a 'ScrubbedBytes' and not a plain
-- 'ByteString', which if fairly important from a crypto / security POV.
newtype Passphrase = Passphrase ScrubbedBytes
deriving stock (Show)

-- | Extract the public key part of a private key.
publicKey
:: Key level XPrv
-> Key level XPub
publicKey (Key xprv) =
Key (toXPub xprv)


{-------------------------------------------------------------------------------
Sequential Derivation
-------------------------------------------------------------------------------}

-- | Marker for the change chain. In practice, change of a transaction goes onto
-- the addresses generated on the internal chain, whereas the external chain is
-- used for addresses that are part of the 'advertised' targets of a transaction
data ChangeChain
= InternalChain
| ExternalChain
deriving (Generic, Show, Eq)

-- Not deriving 'Enum' because this could have a dramatic impact if we were
-- to assign the wrong index to the corresponding constructor (by swapping
-- around the constructor above for instance).
instance Enum ChangeChain where
toEnum = \case
0 -> ExternalChain
1 -> InternalChain
_ -> error "ChangeChain.toEnum: bad argument"
fromEnum = \case
ExternalChain -> 0
InternalChain -> 1

-- | Purpose is a constant set to 44' (or 0x8000002C) following the BIP-44
-- recommendation. It indicates that the subtree of this node is used
-- according to this specification.
--
-- Hardened derivation is used at this level.
purposeIndex :: Word32
purposeIndex = 0x8000002C

-- | One master node (seed) can be used for unlimited number of independent
-- cryptocoins such as Bitcoin, Litecoin or Namecoin. However, sharing the
-- same space for various cryptocoins has some disadvantages.
--
-- This level creates a separate subtree for every cryptocoin, avoiding reusing
-- addresses across cryptocoins and improving privacy issues.
--
-- Coin type is a constant, set for each cryptocoin. For Cardano this constant
-- is set to 1815' (or 0x80000717). 1815 is the birthyear of our beloved Ada
-- Lovelace.
--
-- Hardened derivation is used at this level.
coinTypeIndex :: Word32
coinTypeIndex = 0x80000717

-- | Generate a new key from seed. Note that the @depth@ is left open so that
-- the caller gets to decide what type of key this is. This is mostly for
-- testing, in practice, seeds are used to represent root keys, and one should
-- use 'generateKeyFromSeed'.
unsafeGenerateKeyFromSeed
:: ByteString -- ^ The actual seed
-> Passphrase
-> Key depth XPrv
unsafeGenerateKeyFromSeed seed (Passphrase storagePwd) =
Key $ generateNew seed recoveryPwd storagePwd
where
-- Mnemonic recovery passphrase, see #
recoveryPwd :: ByteString
recoveryPwd = mempty

-- | Generate a root key from a corresponding seed
generateKeyFromSeed
:: ByteString -- ^ The actual seed
-> Passphrase
-> Key 'RootK XPrv
generateKeyFromSeed = unsafeGenerateKeyFromSeed

-- | Derives account private key from the given root private key, using
-- derivation scheme 2 (see <https://github.com/input-output-hk/cardano-crypto/ cardano-crypto>
-- package for more details).
--
-- NOTE: The caller is expected to provide the corresponding passphrase.
deriveAccountPrivateKey
:: Passphrase
-> Key 'RootK XPrv
-> Index 'Hardened 'AccountK
-> Key 'AccountK XPrv
deriveAccountPrivateKey (Passphrase pwd) (Key rootXPrv) (Index accIx) =
let
purposeXPrv = -- lvl1 derivation; hardened derivation of purpose'
deriveXPrv DerivationScheme2 pwd rootXPrv purposeIndex
coinTypeXPrv = -- lvl2 derivation; hardened derivation of coin_type'
deriveXPrv DerivationScheme2 pwd purposeXPrv coinTypeIndex
acctXPrv = -- lvl3 derivation; hardened derivation of account' index
deriveXPrv DerivationScheme2 pwd coinTypeXPrv accIx
in
Key acctXPrv

-- | Derives address private key from the given account private key, using
-- derivation scheme 2 (see <https://github.com/input-output-hk/cardano-crypto/ cardano-crypto>
-- package for more details).
--
-- NOTE: The caller is expected to provide the corresponding passphrase. It is
-- preferred to use 'deriveAddressPublicKey' whenever possible to avoid having
-- to manipulate passphrases and private keys.
deriveAddressPrivateKey
:: Passphrase
-> Key 'AccountK XPrv
-> ChangeChain
-> Index 'Soft 'AddressK
-> Key 'AddressK XPrv
deriveAddressPrivateKey (Passphrase pwd) (Key accXPrv) changeChain (Index addrIx) =
let
changeCode =
fromIntegral $ fromEnum changeChain
changeXPrv = -- lvl4 derivation; soft derivation of change chain
deriveXPrv DerivationScheme2 pwd accXPrv changeCode
addrXPrv = -- lvl5 derivation; soft derivation of address index
deriveXPrv DerivationScheme2 pwd changeXPrv addrIx
in
Key addrXPrv

-- | Derives address public key from the given account public key, using
-- derivation scheme 2 (see <https://github.com/input-output-hk/cardano-crypto/ cardano-crypto>
-- package for more details).
--
-- This is the preferred way of deriving new sequential address public keys.
deriveAddressPublicKey
:: Key 'AccountK XPub
-> ChangeChain
-> Index 'Soft 'AddressK
-> Key 'AddressK XPub
deriveAddressPublicKey (Key accXPub) changeChain (Index addrIx) =
fromMaybe errWrongIndex $ do
let changeCode = fromIntegral $ fromEnum changeChain
changeXPub <- -- lvl4 derivation in bip44 is derivation of change chain
deriveXPub DerivationScheme2 accXPub changeCode
addrXPub <- -- lvl5 derivation in bip44 is derivation of address chain
deriveXPub DerivationScheme2 changeXPub addrIx
return $ Key addrXPub
where
errWrongIndex = error $
"Cardano.Wallet.AddressDerivation.deriveAddressPublicKey failed: \
\was given an hardened (or too big) index for soft path derivation \
\( " ++ show addrIx ++ "). This is either a programmer error, or, \
\we may have reached the maximum number of addresses for a given \
\wallet."

0 comments on commit d3dee38

Please sign in to comment.