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

Derive token policy forging keys according to CIP-1855 #2774

Merged
2 changes: 2 additions & 0 deletions lib/core/cardano-wallet-core.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ library
Cardano.Wallet.Primitive.AddressDerivation
Cardano.Wallet.Primitive.AddressDerivation.Byron
Cardano.Wallet.Primitive.AddressDerivation.Icarus
Cardano.Wallet.Primitive.AddressDerivation.MintBurn
Cardano.Wallet.Primitive.AddressDerivation.Shared
Cardano.Wallet.Primitive.AddressDerivation.SharedKey
Cardano.Wallet.Primitive.AddressDerivation.Shelley
Expand Down Expand Up @@ -359,6 +360,7 @@ test-suite unit
Cardano.Wallet.NetworkSpec
Cardano.Wallet.Primitive.AddressDerivation.ByronSpec
Cardano.Wallet.Primitive.AddressDerivation.IcarusSpec
Cardano.Wallet.Primitive.AddressDerivation.MintBurnSpec
Cardano.Wallet.Primitive.AddressDerivationSpec
Cardano.Wallet.Primitive.AddressDiscovery.RandomSpec
Cardano.Wallet.Primitive.AddressDiscovery.SequentialSpec
Expand Down
36 changes: 32 additions & 4 deletions lib/core/src/Cardano/Wallet/Primitive/AddressDerivation.hs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ module Cardano.Wallet.Primitive.AddressDerivation
, DerivationPrefix (..)
, DerivationIndex (..)
, liftIndex
, hashVerificationKey

-- * Delegation
, RewardAccount (..)
Expand Down Expand Up @@ -85,7 +86,7 @@ module Cardano.Wallet.Primitive.AddressDerivation
import Prelude

import Cardano.Address.Derivation
( XPrv, XPub )
( XPrv, XPub, xpubPublicKey )
import Cardano.Mnemonic
( SomeMnemonic )
import Cardano.Wallet.Primitive.Types
Expand All @@ -103,7 +104,7 @@ import Control.Monad
import Crypto.Hash
( Digest, HashAlgorithm )
import Crypto.Hash.Utils
( blake2b256 )
( blake2b224, blake2b256 )
import Crypto.KDF.PBKDF2
( Parameters (..), fastPBKDF2_SHA512 )
import Crypto.Random.Types
Expand Down Expand Up @@ -151,6 +152,8 @@ import Quiet
import Safe
( readMay, toEnumMay )

import Cardano.Address.Script
( KeyHash (KeyHash), KeyRole )
import qualified Codec.CBOR.Encoding as CBOR
import qualified Codec.CBOR.Write as CBOR
import qualified Crypto.Scrypt as Scrypt
Expand All @@ -163,11 +166,28 @@ import qualified Data.Text.Encoding as T
HD Hierarchy
-------------------------------------------------------------------------------}

-- | Key Depth in the derivation path, according to BIP-0044 / CIP-1852
-- | Typically used as a phantom type parameter, a witness to the type of the
-- key being used.
--
-- For example, @key 'RootK XPrv@, represents the private key at the root of the
-- HD hierarchy.
--
-- According to BIP-0044 / CIP-1852, we have the following keys in our HD
-- hierarchy:
--
-- @m | purpose' | cointype' | account' | role | address@
--
-- Plus, we also have script keys (which are used in shared wallets) and policy
-- keys (which are used in minting and burning).
data Depth
= RootK | PurposeK | CoinTypeK | AccountK | RoleK | AddressK | ScriptK
= RootK
| PurposeK
| CoinTypeK
| AccountK
| RoleK
| AddressK
| ScriptK
| PolicyK
Copy link
Contributor

Choose a reason for hiding this comment

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

Better update all the comments about Depth, starting with the one just above.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Mmmm... not ideal. Kinda brutalizing the abstraction here, but 'ScriptK did it first and it's the path of least resistance...

Copy link
Contributor Author

@sevanspowell sevanspowell Jul 23, 2021

Choose a reason for hiding this comment

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

Updated doco.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah if it weren't for ScriptK I would have asked you to try and find a better way of representing depth.


-- | Marker for addresses type engaged. We want to handle four cases here.
-- The first two are pertinent to UTxO accounting,
Expand Down Expand Up @@ -492,6 +512,14 @@ deriveRewardAccount pwd rootPrv =
let accPrv = deriveAccountPrivateKey pwd rootPrv minBound
in deriveAddressPrivateKey pwd accPrv MutableAccount minBound

hashVerificationKey
:: WalletKey key
=> KeyRole
-> key depth XPub
-> KeyHash
hashVerificationKey keyRole =
KeyHash keyRole . blake2b224 . xpubPublicKey . getRawKey

{-------------------------------------------------------------------------------
Passphrases
-------------------------------------------------------------------------------}
Expand Down
100 changes: 100 additions & 0 deletions lib/core/src/Cardano/Wallet/Primitive/AddressDerivation/MintBurn.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
{-# LANGUAGE DataKinds #-}

-- |
-- Copyright: © 2018-2021 IOHK
-- License: Apache-2.0
--
-- Derivation of policy keys which are used to create scripts for the purposes
-- of minting and burning. Derived according to CIP-1855
-- (https:/cardano-foundation/CIPs/blob/b2e9d02cb9a71ba9e754a432c78197428abf7e4c/CIP-1855/CIP-1855.md).
--
-- The policy keys are derived from the following path:
--
-- m / purpose' / coin_type' / policy_ix'
-- m / 1855' / 1815' / [2^31 .. 2^32-1]'
--
-- Where purpose' and coin_type' are fixed, and each new policy_ix' represents a
-- different policy key.

module Cardano.Wallet.Primitive.AddressDerivation.MintBurn
( -- * Constants
purposeCIP1855
-- * Helpers
, derivePolicyKeyAndHash
, derivePolicyPrivateKey
) where

import Prelude

import Cardano.Address.Derivation
( XPrv )
import Cardano.Address.Script
( KeyHash )
import Cardano.Crypto.Wallet
( deriveXPrv )
import Cardano.Crypto.Wallet.Types
( DerivationScheme (DerivationScheme2) )
import Cardano.Wallet.Primitive.AddressDerivation
( Depth (..)
, DerivationType (..)
, Index (..)
, Passphrase (..)
, WalletKey
, getIndex
, getRawKey
, hashVerificationKey
, liftRawKey
, publicKey
)
import Cardano.Wallet.Primitive.AddressDiscovery
( coinTypeAda )

import qualified Cardano.Address.Script as CA

-- | Purpose for forged policy keys is a constant set to 1855' (or 0x8000073F)
-- following the original CIP-1855: "Forging policy keys for HD Wallets".
--
-- It indicates that the subtree of this node is used according to this
-- specification.
--
-- Hardened derivation is used at this level.
purposeCIP1855 :: Index 'Hardened 'PurposeK
purposeCIP1855 = toEnum 0x8000073F

-- | Derive the policy private key that should be used to create mint/burn
-- scripts.
derivePolicyPrivateKey
:: Passphrase purpose
-- ^ Passphrase for wallet
-> XPrv
-- ^ Root private key to derive policy private key from
-> Index 'Hardened 'PolicyK
-- ^ Index of policy script
-> XPrv
-- ^ Policy private key
derivePolicyPrivateKey (Passphrase pwd) rootXPrv (Index policyIx) =
let
purposeXPrv = -- lvl1 derivation; hardened derivation of purpose'
deriveXPrv DerivationScheme2 pwd rootXPrv (getIndex purposeCIP1855)
coinTypeXPrv = -- lvl2 derivation; hardened derivation of coin_type'
deriveXPrv DerivationScheme2 pwd purposeXPrv (getIndex coinTypeAda)
-- lvl3 derivation; hardened derivation of policy' index
in deriveXPrv DerivationScheme2 pwd coinTypeXPrv policyIx

-- | Derive the policy private key that should be used to create mint/burn
-- scripts, as well as the key hash of the policy public key.
derivePolicyKeyAndHash
:: WalletKey key
=> Passphrase "encryption"
-- ^ Passphrase for wallet
-> key 'RootK XPrv
-- ^ Root private key to derive policy private key from
-> Index 'Hardened 'PolicyK
-- ^ Index of policy script
-> (key 'PolicyK XPrv, KeyHash)
-- ^ Policy private key
derivePolicyKeyAndHash pwd rootPrv policyIx = (policyK, vkeyHash)
where
policyK = liftRawKey policyPrv
policyPrv = derivePolicyPrivateKey pwd (getRawKey rootPrv) policyIx
vkeyHash = hashVerificationKey CA.Payment (publicKey policyK)
Copy link
Contributor

Choose a reason for hiding this comment

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

Purpose field here doesn't look right ... right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You mean the CA.Payment value? I stripped out the Role -> KeyRole indirection because I found that confusing when initially using the function. As someone who wasn't familiar with the wallet, Payment | Delegation :: KeyRole made a lot more sense than UtxoExternal | UtxoInternal | MutableAccount :: Role, and I figured since I was the only one using it, I might as well use the simpler option. Also it doesn't feel like there's much need for abstraction here, we're literally reaching in and manipulating bytes. But I'm not strongly opposed to putting the Role -> KeyRole indirection back in.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah that's all fine, but KeyHash { role = Payment } ? Actually I suppose Payment is suitable for this.

Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleInstances #-}

{-# OPTIONS_GHC -Wno-orphans #-}

module Cardano.Wallet.Primitive.AddressDerivation.MintBurnSpec
( spec
) where

import Prelude

import Cardano.Address.Derivation
( XPrv, XPub )
import Cardano.Address.Script
( KeyHash )
import Cardano.Wallet.Primitive.AddressDerivation
( Depth (PolicyK, RootK, ScriptK)
, DerivationType (Hardened)
, Index
, Passphrase
, WalletKey (publicKey)
, getRawKey
, hashVerificationKey
, liftRawKey
)
import Cardano.Wallet.Primitive.AddressDerivation.MintBurn
( derivePolicyKeyAndHash, derivePolicyPrivateKey )
import Cardano.Wallet.Primitive.AddressDerivation.Shelley
( ShelleyKey )
import Cardano.Wallet.Primitive.AddressDerivationSpec
()
import Cardano.Wallet.Unsafe
( unsafeXPrv )
import qualified Data.ByteString as BS
import Test.Hspec
( Spec, describe, it )
import Test.Hspec.Extra
( parallel )
import Test.QuickCheck
( Arbitrary (..), Property, property, vector, (=/=), (===) )
import Test.QuickCheck.Arbitrary
( arbitraryBoundedEnum )

import qualified Cardano.Address.Script as CA

spec :: Spec
spec = do
parallel $ describe "Mint/Burn Policy key Address Derivation Properties" $ do
it "Policy key derivation from master key works for various indexes" $
property prop_keyDerivationFromXPrv
it "Policy public key hash matches private key" $
property prop_keyHashMatchesXPrv
it "The same index always returns the same private key" $
property prop_keyDerivationSameIndexSameKey
it "A different index always returns a different private key" $
property prop_keyDerivationDiffIndexDiffKey
it "Using derivePolicyKeyAndHash returns same private key as using derivePolicyPrivateKey" $
property prop_keyDerivationRelation

Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps one or two golden tests that you have generated manually with cardano-cli?

{-------------------------------------------------------------------------------
Properties
-------------------------------------------------------------------------------}

prop_keyDerivationFromXPrv
:: Passphrase "encryption"
-> XPrv
-> Index 'Hardened 'PolicyK
-> Property
prop_keyDerivationFromXPrv pwd masterkey policyIx =
rndKey `seq` property () -- NOTE Making sure this doesn't throw
where
rndKey :: XPrv
rndKey = derivePolicyPrivateKey pwd masterkey policyIx

prop_keyHashMatchesXPrv
:: Passphrase "encryption"
-> ShelleyKey 'RootK XPrv
-> Index 'Hardened 'PolicyK
-> Property
prop_keyHashMatchesXPrv pwd masterkey policyIx =
hashVerificationKey
CA.Payment
(getPublicKey rndKey)
=== keyHash
where
rndKey :: ShelleyKey 'PolicyK XPrv
keyHash :: KeyHash
(rndKey, keyHash) = derivePolicyKeyAndHash pwd masterkey policyIx

getPublicKey
:: ShelleyKey 'PolicyK XPrv
-> ShelleyKey 'ScriptK XPub
getPublicKey =
publicKey . (liftRawKey :: XPrv -> ShelleyKey 'ScriptK XPrv) . getRawKey

prop_keyDerivationSameIndexSameKey
:: Passphrase "encryption"
-> XPrv
-> Index 'Hardened 'PolicyK
-> Property
prop_keyDerivationSameIndexSameKey pwd masterkey policyIx =
key1 === key2
where
key1 :: XPrv
key2 :: XPrv
key1 = derivePolicyPrivateKey pwd masterkey policyIx
key2 = derivePolicyPrivateKey pwd masterkey policyIx

prop_keyDerivationDiffIndexDiffKey
:: Passphrase "encryption"
-> XPrv
-> Index 'Hardened 'PolicyK
-> Index 'Hardened 'PolicyK
-> Property
prop_keyDerivationDiffIndexDiffKey pwd masterkey policyIx1 policyIx2 =
key1 =/= key2
where
key1 :: XPrv
key2 :: XPrv
Comment on lines +147 to +148
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
key1 :: XPrv
key2 :: XPrv
key1, key2 :: XPrv

... or just leave out the type annotations.

key1 = derivePolicyPrivateKey pwd masterkey policyIx1
key2 = derivePolicyPrivateKey pwd masterkey policyIx2

prop_keyDerivationRelation
:: Passphrase "encryption"
-> XPrv
-> Index 'Hardened 'PolicyK
-> Property
prop_keyDerivationRelation pwd masterkey policyIx =
key1 === key2
where
key1 :: XPrv
key1 = derivePolicyPrivateKey pwd masterkey policyIx

keyAndHash :: (ShelleyKey 'PolicyK XPrv, KeyHash)
keyAndHash = derivePolicyKeyAndHash pwd (liftRawKey masterkey) policyIx

key2 :: XPrv
key2 = getRawKey $ fst keyAndHash

instance Arbitrary XPrv where
arbitrary = unsafeXPrv . BS.pack <$> vector 128

instance Arbitrary (Index 'Hardened 'PolicyK) where
shrink _ = []
arbitrary = arbitraryBoundedEnum