From d3dee38a107e9e28a4b81e29398b5cbac54a6ff0 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Mon, 11 Mar 2019 13:55:36 +0100 Subject: [PATCH] port address derivation module from cardano-wallet-legacy --- cardano-wallet.cabal | 21 +- src/Cardano/Wallet/AddressDerivation.hs | 276 ++++++++++++++++++++++++ 2 files changed, 288 insertions(+), 9 deletions(-) create mode 100644 src/Cardano/Wallet/AddressDerivation.hs diff --git a/cardano-wallet.cabal b/cardano-wallet.cabal index 20b7ee5fa7f..696e804b617 100644 --- a/cardano-wallet.cabal +++ b/cardano-wallet.cabal @@ -34,6 +34,7 @@ library base , binary , bytestring + , cardano-crypto , cborg , containers , cryptonite @@ -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 @@ -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 diff --git a/src/Cardano/Wallet/AddressDerivation.hs b/src/Cardano/Wallet/AddressDerivation.hs new file mode 100644 index 00000000000..f46b7560f4b --- /dev/null +++ b/src/Cardano/Wallet/AddressDerivation.hs @@ -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 +-- 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 +-- 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 +-- 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."