cardano-addresses-4.0.1: lib/Cardano/Address/Style/Byron.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_HADDOCK prune #-}
-- |
-- Copyright: 2020 Input Output (Hong Kong) Ltd., 2021-2022 Input Output Global Inc. (IOG), 2023-2025 Intersect
-- License: Apache-2.0
module Cardano.Address.Style.Byron
( -- $overview
-- * Byron
Byron
, DerivationPath
, payloadPassphrase
, derivationPath
, getKey
-- * Key Derivation
-- $keyDerivation
, genMasterKeyFromXPrv
, genMasterKeyFromMnemonic
, deriveAccountPrivateKey
, deriveAddressPrivateKey
-- * Addresses
-- $addresses
, AddressInfo (..)
, eitherInspectAddress
, inspectAddress
, inspectByronAddress
, paymentAddress
, ErrInspectAddress (..)
, prettyErrInspectAddress
-- * Network Discrimination
, byronMainnet
, byronStaging
, byronTestnet
, byronPreprod
, byronPreview
-- * Unsafe
, liftXPrv
, liftXPub
-- Internals
, minSeedLengthBytes
) where
import Prelude
import Cardano.Address
( Address
, AddressDiscrimination (..)
, HasNetworkDiscriminant (..)
, NetworkTag (..)
, unAddress
, unsafeMkAddress
)
import Cardano.Address.Derivation
( Depth (..)
, DerivationScheme (DerivationScheme1)
, DerivationType (..)
, Index (..)
, XPrv
, XPub
, deriveXPrv
, generate
, toXPub
, xpubToBytes
)
import Cardano.Address.Internal
( DeserialiseFailure, WithErrorMessage (..) )
import Cardano.Mnemonic
( SomeMnemonic (..), entropyToBytes, mnemonicToEntropy )
import Codec.Binary.Encoding
( AbstractEncoding (..), encode )
import Control.DeepSeq
( NFData )
import Control.Exception
( Exception, displayException )
import Control.Exception.Base
( assert )
import Control.Monad.Catch
( MonadThrow, throwM )
import Crypto.Hash
( hash )
import Crypto.Hash.Algorithms
( Blake2b_256, SHA512 (..) )
import Data.Aeson
( ToJSON (..), (.=) )
import Data.Bifunctor
( bimap, first )
import Data.ByteArray
( ScrubbedBytes )
import Data.ByteString
( ByteString )
import Data.Kind
( Type )
import Data.List
( find )
import Data.Word
( Word32, Word8 )
import GHC.Generics
( Generic )
import qualified Cardano.Address as Internal
import qualified Cardano.Address.Derivation as Internal
import qualified Cardano.Codec.Cbor as CBOR
import qualified Codec.CBOR.Decoding as CBOR
import qualified Crypto.KDF.PBKDF2 as PBKDF2
import qualified Data.Aeson as Json
import qualified Data.ByteArray as BA
import qualified Data.Text.Encoding as T
-- $overview
--
-- This module provides an implementation of:
--
-- - 'Cardano.Address.Derivation.GenMasterKey': for generating Byron master keys from mnemonic sentences
-- - 'Cardano.Address.Derivation.HardDerivation': for hierarchical derivation of parent to child keys
-- - 'Cardano.Address.PaymentAddress': for constructing addresses from a public key
--
-- We call 'Byron' addresses the old address type used by Daedalus in the early
-- days of Cardano. Using this type of addresses and underlying key scheme is
-- now considered __deprecated__ because of some security implications.
--
-- The internals of the 'Byron' does not matter for the reader, but basically
-- contains what is necessary to perform key derivation and generate addresses
-- from a 'Byron' type.
--
-- Byron uses WholeDomain (meaning Soft+Hardened) for account key and payment key derivation.
-- It should use Hardened for account and Soft for payment as design,
-- but due to the error made prior 2019 in cardano-sl
-- implementation WholeDomain was adopted to handle all the keys. Nevertheless,
-- it was recommended and enforced to use Hardened for account derivation and Soft for payment
-- key derivation from 2019 onwards. To sum up both account index and payment index can assume
-- values from 0 to 4294967295 (ie. 0xFFFFFFFF)
--
-- == Deprecation Notice
--
-- Unless you have good reason to do so (like writing backward-compatible code
-- with an existing piece), any new implementation __should use__ the
-- 'Cardano.Address.Style.Icarus.Icarus' style for key and addresses.
-- | Material for deriving HD random scheme keys, which can be used for making
-- addresses.
--
-- @since 1.0.0
data Byron (depth :: Depth) key = Byron
{ getKey :: key
-- ^ The raw private or public key.
--
-- @since 1.0.0
, derivationPath :: DerivationPath depth
-- ^ The address derivation indices for the level of this key.
--
-- @since 1.0.0
, payloadPassphrase :: ScrubbedBytes
-- ^ Used for encryption of the derivation path payload within an address.
--
-- @since 1.0.0
} deriving stock (Generic)
{-# DEPRECATED Byron "see 'Cardano.Address.Style.Icarus.Icarus'" #-}
{-# DEPRECATED getKey "see 'Cardano.Address.Style.Icarus.Icarus'" #-}
{-# DEPRECATED derivationPath "see 'Cardano.Address.Style.Icarus.Icarus'" #-}
{-# DEPRECATED payloadPassphrase "see 'Cardano.Address.Style.Icarus.Icarus'" #-}
instance (NFData key, NFData (DerivationPath depth)) => NFData (Byron depth key)
deriving instance (Show key, Show (DerivationPath depth)) => Show (Byron depth key)
deriving instance (Eq key, Eq (DerivationPath depth)) => Eq (Byron depth key)
deriving instance (Functor (Byron depth))
-- | The hierarchical derivation indices for a given level/depth.
--
-- @since 1.0.0
type family DerivationPath (depth :: Depth) :: Type where
-- The root key is generated from the seed.
DerivationPath 'RootK =
()
-- The account key is generated from the root key and account index.
DerivationPath 'AccountK =
Index 'WholeDomain 'AccountK
-- The address key is generated from the account key and address index.
DerivationPath 'PaymentK =
(Index 'WholeDomain 'AccountK, Index 'WholeDomain 'PaymentK)
{-# DEPRECATED DerivationPath "see 'Cardano.Address.Style.Icarus.Icarus'" #-}
-- | Key Derivation
--
-- __Example__:
--
-- ==== Generating a root key from 'SomeMnemonic'
--
-- @
-- >>> :set -XOverloadedStrings
-- >>> :set -XTypeApplications
-- >>> :set -XDataKinds
-- >>> :set -XFlexibleContexts
-- >>> import Cardano.Mnemonic ( mkSomeMnemonic )
-- >>> import Cardano.Address ( base58 )
-- >>> import Cardano.Address.Derivation ( toXPub )
-- >>> import qualified Cardano.Address.Style.Byron as Byron
--
-- >>> let (Right mw) = mkSomeMnemonic @'[12] ["moon","fox","ostrich","quick","cactus","raven","wasp","intact","first","ring","crumble","error"]
-- >>> let rootK = Byron.genMasterKeyFromMnemonic mw :: Byron 'RootK XPrv
-- @
--
-- ==== Deriving child keys
--
-- Both `accIx` and `addIx` assume values from 0 to 4294967295 (ie. 0xFFFFFFFF)
-- In case of account one can get this bound via
--
-- @
-- let accIxMin = minBound @(Index 'WholeDomain 'AccountK)
-- let accIxMax = maxBound @(Index 'WholeDomain 'AccountK)
--
-- >>> let Just accIx = wholeDomainIndex 0x80000000
-- >>> let acctK = Byron.deriveAccountPrivateKey rootK accIx
--
-- >>> let Just addIx = wholeDomainIndex 0x80000014
-- >>> let addrK = Byron.deriveAddressPrivateKey acctK addIx
--
-- >>> base58 $ Byron.paymentAddress Byron.byronMainnet (toXPub <$> addrK)
-- "DdzFFzCqrhsq3KjLtT51mESbZ4RepiHPzLqEhamexVFTJpGbCXmh7qSxnHvaL88QmtVTD1E1sjx8Z1ZNDhYmcBV38ZjDST9kYVxSkhcw"
-- @
--
instance Internal.GenMasterKey Byron where
type SecondFactor Byron = ()
genMasterKeyFromXPrv xprv =
liftXPrv (toXPub xprv) () xprv
genMasterKeyFromMnemonic (SomeMnemonic mw) () =
liftXPrv (toXPub xprv) () xprv
where
xprv = generate (hashSeed seedValidated)
seed = entropyToBytes $ mnemonicToEntropy mw
seedValidated = assert
(BA.length seed >= minSeedLengthBytes && BA.length seed <= 255)
seed
instance Internal.HardDerivation Byron where
type AddressIndexDerivationType Byron = 'WholeDomain
type AccountIndexDerivationType Byron = 'WholeDomain
type WithRole Byron = ()
deriveAccountPrivateKey rootXPrv accIx = Byron
{ getKey = deriveXPrv DerivationScheme1 (getKey rootXPrv) accIx
, derivationPath = accIx
, payloadPassphrase = payloadPassphrase rootXPrv
}
deriveAddressPrivateKey accXPrv () addrIx = Byron
{ getKey = deriveXPrv DerivationScheme1 (getKey accXPrv) addrIx
, derivationPath = (derivationPath accXPrv, addrIx)
, payloadPassphrase = payloadPassphrase accXPrv
}
-- | Generate a root key from a corresponding mnemonic.
--
-- @since 1.0.0
genMasterKeyFromMnemonic
:: SomeMnemonic
-- ^ Some valid mnemonic sentence.
-> Byron 'RootK XPrv
genMasterKeyFromMnemonic =
flip Internal.genMasterKeyFromMnemonic ()
{-# DEPRECATED genMasterKeyFromMnemonic "see 'Cardano.Address.Style.Icarus.Icarus'" #-}
-- | Generate a root key from a corresponding root 'XPrv'
--
-- @since 1.0.0
genMasterKeyFromXPrv
:: XPrv
-> Byron 'RootK XPrv
genMasterKeyFromXPrv =
Internal.genMasterKeyFromXPrv
{-# DEPRECATED genMasterKeyFromXPrv "see 'Cardano.Address.Style.Icarus.Icarus'" #-}
-- Re-export from 'Cardano.Address.Derivation' to have it documented specialized in Haddock.
--
-- | Derives an account private key from the given root private key.
--
-- @since 1.0.0
deriveAccountPrivateKey
:: Byron 'RootK XPrv
-> Index 'WholeDomain 'AccountK
-> Byron 'AccountK XPrv
deriveAccountPrivateKey =
Internal.deriveAccountPrivateKey
{-# DEPRECATED deriveAccountPrivateKey "see 'Cardano.Address.Style.Icarus.Icarus'" #-}
-- Re-export from 'Cardano.Address.Derivation' to have it documented specialized in Haddock.
--
-- | Derives an address private key from the given account private key.
--
-- @since 1.0.0
deriveAddressPrivateKey
:: Byron 'AccountK XPrv
-> Index 'WholeDomain 'PaymentK
-> Byron 'PaymentK XPrv
deriveAddressPrivateKey acctK =
Internal.deriveAddressPrivateKey acctK ()
{-# DEPRECATED deriveAddressPrivateKey "see 'Cardano.Address.Style.Icarus.Icarus'" #-}
--
-- Addresses
--
-- $addresses
--
-- | Possible errors from inspecting a Byron address
--
-- @since 3.0.0
data ErrInspectAddress
= MissingExpectedDerivationPath
| DeserialiseError DeserialiseFailure
| FailedToDecryptPath
deriving (Generic, Show, Eq)
deriving ToJSON via WithErrorMessage ErrInspectAddress
instance Exception ErrInspectAddress where
displayException = prettyErrInspectAddress
-- | Pretty-print an 'ErrInspectAddress'
--
-- @since 3.0.0
prettyErrInspectAddress :: ErrInspectAddress -> String
prettyErrInspectAddress = \case
MissingExpectedDerivationPath ->
"Missing expected derivation path"
DeserialiseError e ->
displayException e
FailedToDecryptPath ->
"Failed to decrypt derivation path"
-- | Determines whether an 'Address' is a Byron address.
--
-- Returns a JSON object with information about the address, or throws
-- 'ErrInspectAddress' if the address isn't a byron address.
--
-- @since 2.0.0
inspectByronAddress :: forall m. MonadThrow m => Maybe XPub -> Address -> m Json.Value
inspectByronAddress = inspectAddress
{-# DEPRECATED inspectByronAddress "use qualified 'inspectAddress' instead." #-}
-- | Determines whether an 'Address' is a Byron address.
--
-- Returns a JSON object with information about the address, or throws
-- 'ErrInspectAddress' if the address isn't a byron address.
--
-- @since 3.0.0
inspectAddress :: forall m. MonadThrow m => Maybe XPub -> Address -> m Json.Value
inspectAddress mRootPub addr = either throwM (pure . toJSON) $
eitherInspectAddress mRootPub addr
-- | Determines whether an 'Address' is a Byron address.
--
-- Returns either details about the 'Address', or 'ErrInspectAddress' if it's
-- not a valid address.
--
-- @since 3.4.0
eitherInspectAddress :: Maybe XPub -> Address -> Either ErrInspectAddress AddressInfo
eitherInspectAddress mRootPub addr = do
payload <- first DeserialiseError $
CBOR.deserialiseCbor CBOR.decodeAddressPayload bytes
(root, attrs) <- first DeserialiseError $
CBOR.deserialiseCbor decodePayload payload
path <- do
attr <- maybe (Left MissingExpectedDerivationPath) Right $
find ((== 1) . fst) attrs
case mRootPub of
Nothing -> Right $ EncryptedDerivationPath $ snd attr
Just rootPub -> decryptPath attr rootPub
ntwrk <- bimap DeserialiseError (fmap NetworkTag) $
CBOR.deserialiseCbor CBOR.decodeProtocolMagicAttr payload
pure AddressInfo
{ infoAddressRoot = root
, infoPayload = path
, infoNetworkTag = ntwrk
}
where
bytes :: ByteString
bytes = unAddress addr
decodePayload :: forall s. CBOR.Decoder s (ByteString, [(Word8, ByteString)])
decodePayload = do
_ <- CBOR.decodeListLenCanonicalOf 3
root <- CBOR.decodeBytes
(root,) <$> CBOR.decodeAllAttributes
decryptPath :: (Word8, ByteString) -> XPub -> Either ErrInspectAddress PayloadInfo
decryptPath attr rootPub = do
let pwd = hdPassphrase rootPub
path <- first (const FailedToDecryptPath) $
CBOR.deserialiseCbor (CBOR.decodeDerivationPathAttr pwd [attr]) mempty
case path of
Nothing -> Left FailedToDecryptPath
Just (accountIndex, addressIndex) -> Right PayloadDerivationPath{..}
-- | The result of 'eitherInspectAddress' for Byron addresses.
--
-- @since 3.4.0
data AddressInfo = AddressInfo
{ infoAddressRoot :: !ByteString
, infoPayload :: !PayloadInfo
, infoNetworkTag :: !(Maybe NetworkTag)
} deriving (Generic, Show, Eq)
-- | The derivation path in a Byron address payload.
--
-- @since 3.4.0
data PayloadInfo
= PayloadDerivationPath
{ accountIndex :: !Word32
, addressIndex :: !Word32
}
| EncryptedDerivationPath
{ encryptedDerivationPath :: !ByteString
}
deriving (Generic, Show, Eq)
instance ToJSON AddressInfo where
toJSON AddressInfo{..} = Json.object
[ "address_root" .= T.decodeUtf8 (encode EBase16 infoAddressRoot)
, "derivation_path" .= infoPayload
, "network_tag" .= maybe Json.Null toJSON infoNetworkTag
, "address_type" .= toJSON @Word8 8
]
instance ToJSON PayloadInfo where
toJSON PayloadDerivationPath{..} = Json.object
[ "account_index" .= prettyIndex accountIndex
, "address_index" .= prettyIndex addressIndex
]
where
prettyIndex :: Word32 -> String
prettyIndex ix
| ix >= firstHardened = show (ix - firstHardened) <> "H"
| otherwise = show ix
where
firstHardened = 0x80000000
toJSON EncryptedDerivationPath{..} = Json.String $
T.decodeUtf8 $ encode EBase16 encryptedDerivationPath
instance Internal.PaymentAddress Byron where
paymentAddress discrimination k = unsafeMkAddress
$ CBOR.toStrictByteString
$ CBOR.encodeAddress (getKey k) attrs
where
(acctIx, addrIx) = bimap indexToWord32 indexToWord32 $ derivationPath k
pwd = payloadPassphrase k
NetworkTag magic = networkTag @Byron discrimination
attrs = case addressDiscrimination @Byron discrimination of
RequiresNetworkTag ->
[ CBOR.encodeDerivationPathAttr pwd acctIx addrIx
, CBOR.encodeProtocolMagicAttr magic
]
RequiresNoTag ->
[ CBOR.encodeDerivationPathAttr pwd acctIx addrIx
]
-- Re-export from 'Cardano.Address' to have it documented specialized in Haddock.
--
-- | Convert a public key to a payment 'Address' valid for the given
-- network discrimination.
--
-- @since 1.0.0
paymentAddress
:: NetworkDiscriminant Byron
-> Byron 'PaymentK XPub
-> Address
paymentAddress =
Internal.paymentAddress
--
-- Network Discrimination
--
instance HasNetworkDiscriminant Byron where
type NetworkDiscriminant Byron = (AddressDiscrimination, NetworkTag)
addressDiscrimination = fst
networkTag = snd
-- | 'NetworkDiscriminant' for Cardano MainNet & Byron
--
-- @since 2.0.0
byronMainnet :: NetworkDiscriminant Byron
byronMainnet = (RequiresNoTag, NetworkTag 764824073)
-- | 'NetworkDiscriminant' for Cardano Staging & Byron
--
-- @since 2.0.0
byronStaging :: NetworkDiscriminant Byron
byronStaging = (RequiresNetworkTag, NetworkTag 633343913)
-- | 'NetworkDiscriminant' for Cardano Testnet & Byron
--
-- @since 2.0.0
byronTestnet :: NetworkDiscriminant Byron
byronTestnet = (RequiresNetworkTag, NetworkTag 1097911063)
-- | 'NetworkDiscriminant' for Cardano Preview & Byron
--
-- @since 3.13.0
byronPreview :: NetworkDiscriminant Byron
byronPreview = (RequiresNetworkTag, NetworkTag 2)
-- | 'NetworkDiscriminant' for Cardano Preprod & Byron
--
-- @since 3.13.0
byronPreprod :: NetworkDiscriminant Byron
byronPreprod = (RequiresNetworkTag, NetworkTag 1)
--
-- Unsafe
--
-- | Backdoor for generating a new key from a raw 'XPrv'.
--
-- 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 'genMasterKeyFromXPrv'
--
-- The first argument is a type-family 'DerivationPath' and its type depends on
-- the 'depth' of the key.
--
-- __examples:__
--
-- >>> liftXPrv rootPrv () prv
-- _ :: Byron RootK XPrv
--
-- >>> liftXPrv rootPrv minBound prv
-- _ :: Byron AccountK XPrv
--
-- >>> liftXPrv rootPrv (minBound, minBound) prv
-- _ :: Byron PaymentK XPrv
--
-- @since 2.0.0
liftXPrv
:: XPub -- ^ A root public key
-> DerivationPath depth
-> XPrv
-> Byron depth XPrv
liftXPrv rootPub derivationPath getKey = Byron
{ getKey
, derivationPath
, payloadPassphrase = hdPassphrase rootPub
}
{-# DEPRECATED liftXPrv "see 'Cardano.Address.Style.Icarus.Icarus'" #-}
-- | Backdoor for generating a new key from a raw 'XPub'.
--
-- 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 'genMasterKeyFromXPrv'
--
-- see also 'liftXPrv'
--
-- @since 2.0.0
liftXPub
:: XPub -- ^ A root public key
-> DerivationPath depth
-> XPub
-> Byron depth XPub
liftXPub rootPub derivationPath getKey = Byron
{ getKey
, derivationPath
, payloadPassphrase = hdPassphrase rootPub
}
{-# DEPRECATED liftXPub "see 'Cardano.Address.Style.Icarus.Icarus'" #-}
--
-- Internal
--
-- The amount of entropy carried by a BIP-39 12-word mnemonic is 16 bytes.
minSeedLengthBytes :: Int
minSeedLengthBytes = 16
-- Hash the seed entropy (generated from mnemonic) used to initiate a HD
-- wallet. This increases the key length to 34 bytes, selectKey is greater than the
-- minimum for 'generate' (32 bytes).
--
-- Note that our current implementation deviates from BIP-39 because we use a
-- hash function (Blake2b) rather than key stretching with PBKDF2.
--
-- There are two methods of hashing the seed entropy, for different use cases.
--
-- 1. Normal random derivation wallet seeds. The seed entropy is hashed using
-- Blake2b_256, inside a double CBOR serialization sandwich.
--
-- 2. Seeds for redeeming paper wallets. The seed entropy is hashed using
-- Blake2b_256, without any serialization.
hashSeed :: ScrubbedBytes -> ScrubbedBytes
hashSeed = serialize . blake2b256 . serialize
where
serialize = BA.convert . cbor . BA.convert
cbor = CBOR.toStrictByteString . CBOR.encodeBytes
-- Hash a byte string through blake2b 256
blake2b256 :: ScrubbedBytes -> ScrubbedBytes
blake2b256 = BA.convert . hash @ScrubbedBytes @Blake2b_256
-- Derive a symmetric key for encrypting and authenticating the address
-- derivation path. PBKDF2 encryption using HMAC with the hash algorithm SHA512
-- is employed.
hdPassphrase :: XPub -> ScrubbedBytes
hdPassphrase masterKey =
PBKDF2.generate
(PBKDF2.prfHMAC SHA512)
(PBKDF2.Parameters 500 32)
(xpubToBytes masterKey)
("address-hashing" :: ByteString)