-
Notifications
You must be signed in to change notification settings - Fork 213
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
Changes from all commits
9cd9ebc
c4d6039
0a7207c
63e9070
5ac3a07
ce122e7
d0b1c37
b554949
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice idea for sequential types to deal with " |
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. just wonder why for
would have some drawbacks? Maybe we want to extend it somewhere outside ant making not visible in this module? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I notice there was a passphrase check in the previous version. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 $ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So it's ok to crash with There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 We know it can't happen, but it's better to have a rather clear invariant here than doing a partial pattern match like |
||
"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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
finaly :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😉