Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Address Derivation (Sequential) #46

Merged
merged 8 commits into from
Mar 13, 2019
12 changes: 8 additions & 4 deletions cardano-wallet.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ library
Cardano.ChainProducer.RustHttpBridge.Client
Cardano.ChainProducer.RustHttpBridge.NetworkLayer
Cardano.Wallet
Cardano.Wallet.AddressDerivation
Cardano.Wallet.AddressDiscovery
Cardano.Wallet.Binary
Cardano.Wallet.Binary.Packfile
Cardano.Wallet.BlockSyncer
Expand Down Expand Up @@ -135,27 +137,29 @@ test-suite unit
, containers
, deepseq
, exceptions
, fmt
, hspec
, hspec-expectations
, memory
, mtl
, QuickCheck
, text
, time-units
, transformers
, QuickCheck
type:
exitcode-stdio-1.0
hs-source-dirs:
test/unit
main-is:
Main.hs
other-modules:
Cardano.WalletSpec
Cardano.Wallet.BinarySpec
Cardano.ChainProducer.RustHttpBridge.MockNetworkLayer
Cardano.ChainProducer.RustHttpBridgeSpec
Cardano.Wallet.AddressDerivationSpec
Cardano.Wallet.AddressDiscoverySpec
Cardano.Wallet.Binary.PackfileSpec
Cardano.Wallet.BinarySpec
Cardano.Wallet.BlockSyncerSpec
Cardano.Wallet.MnemonicSpec
Cardano.Wallet.PrimitiveSpec
Cardano.Wallet.SlottingSpec
Cardano.WalletSpec
17 changes: 1 addition & 16 deletions src/Cardano/Wallet.hs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ module Cardano.Wallet
, availableUTxO

-- * Helpers
, invariant
, txOutsOurs
, utxoFromTx
) where
Expand All @@ -53,6 +52,7 @@ import Cardano.Wallet.Primitive
, UTxO (..)
, balance
, excluding
, invariant
, restrictedBy
, restrictedTo
, txIns
Expand Down Expand Up @@ -163,21 +163,6 @@ totalUTxO wallet@(Wallet _ pending s) =

-- * Helpers

-- | Check whether an invariants holds or not.
--
-- >>> invariant "not empty" [1,2,3] (not . null)
-- [1, 2, 3]
--
-- >>> invariant "not empty" [] (not . null)
-- *** Exception: not empty
invariant
:: String -- ^ A title / message to throw in case of violation
-> a
-> (a -> Bool)
-> a
invariant msg a predicate =
if predicate a then a else error msg

-- | Return all transaction outputs that are ours. This plays well within a
-- 'State' monad.
--
Expand Down
311 changes: 311 additions & 0 deletions src/Cardano/Wallet/AddressDerivation.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# 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
, XPub
, XPrv

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

import Prelude

import Cardano.Crypto.Wallet
( DerivationScheme (..)
, XPrv
, XPub
, deriveXPrv
, deriveXPub
, generateNew
, toXPub
)
import Cardano.Wallet.Binary
( encodeAddress )
import Cardano.Wallet.Primitive
( Address (..) )
import Control.DeepSeq
( NFData )
import Data.ByteArray
( ScrubbedBytes )
import Data.ByteString
( ByteString )
import Data.Maybe
( fromMaybe )
import Data.Word
( Word32 )
import GHC.Generics
( Generic )
import GHC.TypeLits
( Symbol )

import qualified Codec.CBOR.Encoding as CBOR
import qualified Codec.CBOR.Write as CBOR


{-------------------------------------------------------------------------------
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

finaly :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😉

deriving stock (Generic, Show, Eq)

instance (NFData key) => NFData (Key level key)


-- | 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 (Generic, Show, Eq, Ord)

instance NFData (Index derivationType level)

instance Bounded (Index 'Hardened level) where
minBound = Index 0x80000000
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice idea for sequential types to deal with "Word31" type

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.
newtype Passphrase (goal :: Symbol) = Passphrase ScrubbedBytes
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just wonder why for Index and Key there is Depth and DerivationType phantom-type used, respectively. And here, you are relying on Symbol (in a sense making phantom-type like specialization open) rather than on data. Using :

data PassphrasePurpose = Encryption | Generation

would have some drawbacks? Maybe we want to extend it somewhere outside ant making not visible in this module?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly for the reason you mention, using a symbol leaves the type quite open which makes sense for types that are rather general like "Passphrase". On the contrary, the Index and Key have a very precise semantic for which we already know the spectrum, and therefore, we can be explicit and have a closed typed.

So, it's just slightly more convenient to have symbols than sum types and we get almost the same guarantee. Typos are still possible but in such case, GHC will probably yell at you that types don't match.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

deriving stock (Show)
deriving newtype (Semigroup, Monoid)

-- | 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)

instance NFData ChangeChain

-- 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, Passphrase "generation")
-- ^ The actual seed and its recovery / generation passphrase
-> Passphrase "encryption"
-> Key depth XPrv
unsafeGenerateKeyFromSeed (seed, Passphrase recPwd) (Passphrase encPwd) =
Key $ generateNew seed recPwd encPwd

-- | Generate a root key from a corresponding seed
generateKeyFromSeed
:: (ByteString, Passphrase "generation")
-- ^ The actual seed and its recovery / generation passphrase
-> Passphrase "encryption"
-> 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 (and to
-- have checked that the passphrase is valid). Providing a wrong passphrase will
-- not make the function fail but will instead, yield an incorrect new key that
-- doesn't belong to the wallet.
deriveAccountPrivateKey
:: Passphrase "encryption"
-> Key 'RootK XPrv
-> Index 'Hardened 'AccountK
-> Key 'AccountK XPrv
deriveAccountPrivateKey (Passphrase pwd) (Key rootXPrv) (Index accIx) =
let
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I notice there was a passphrase check in the previous version.
This function is no longer failable (partly because the isHardened check is obviated by the Index type).
What happens when the key encryption passphrase is wrong?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left a comment about that but I'll extend it. This has been left to the caller as controlling the password would require to either keep a hash of the password in memory (which was a wrong design decision) like before, or have an access to the keystore / password vault. This is a responsibility of the wallet layer calling those derivation primitives.

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).
--
-- It is preferred to use 'deriveAddressPublicKey' whenever possible to avoid
-- having to manipulate passphrases and private keys.
--
-- NOTE: The caller is expected to provide the corresponding passphrase (and to
-- have checked that the passphrase is valid). Providing a wrong passphrase will
-- not make the function fail but will instead, yield an incorrect new key that
-- doesn't belong to the wallet.
deriveAddressPrivateKey
:: Passphrase "encryption"
-> 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 $
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So it's ok to crash with error when the index is greater than a billion or so?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In practice it shouldn't happen because we can't create such index. Our Index type is completely opaque and the only way to create indexes is through the Bounded and Enum instance (Enum which also throws at runtime if succ is called on maxBound, as specified in the base built-in). Again, the caller would have to check that prior to calling those primitives whether it makes sense.

We know it can't happen, but it's better to have a rather clear invariant here than doing a partial pattern match like Just res <- ... , which would not be very helpful if we ever violate this invariant.

"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."

-- | Encode a public key to a (Byron / Legacy) Cardano 'Address'. This is mostly
-- dubious CBOR serializations with no data attributes.
keyToAddress
:: Key 'AddressK XPub
-> Address
keyToAddress (Key xpub) =
Address $ CBOR.toStrictByteString $ encodeAddress xpub encodeAttributes
where
encodeAttributes = CBOR.encodeMapLen 0
Loading