Skip to content

Commit

Permalink
Merge pull request #172 from input-output-hk/paweljakubas/adp-1191/ad…
Browse files Browse the repository at this point in the history
…d-policy-support

Add policy key support
  • Loading branch information
paweljakubas authored Mar 7, 2022
2 parents f267c79 + 3820392 commit c6472d2
Show file tree
Hide file tree
Showing 13 changed files with 99 additions and 10 deletions.
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,30 @@ $ cardano-address key public --with-chain-code < acct.xsk | cardano-address key
```
</details>

<details>
<summary>How to generate a private policy key (<strong>policy.xsk</strong>), a public policy key (<strong>policy.vk</strong>) and its hash (<strong>policy.vkh</strong>)</summary>

```console
$ cardano-address key child 1855H/1815H/0H < root.xsk > policy.xsk
policy_xsk1hr47zvxgzeeutgq50r965ygwxys86cwp8wdjqftlhan8mw6849pus6vc50dznjs5vkyjcz9usl6964u6nha88slrh8hyex74xnlfehcrkp80cp8wgzkqh22dzy7c48ekhhvvf2zz8hqakjwgfzgrjq5lx538et75

$ cardano-address key child 1855H/1815H/0H < root.xsk | cardano-address key public --with-chain-code > policy.xvk
policy_xvk1e9ngmlhcwhszwyuxwc7anwk6tvzwndldz7j262rvfpd049tq74mq8vzwlszwus9vpw556yfa320nd0wccj5yy0wpmdyusjys8ypf7dgaauf0m

$ cardano-address key child 1855H/1815H/0H < root.xsk | cardano-address key public --without-chain-code > policy.vk
policy_vk1e9ngmlhcwhszwyuxwc7anwk6tvzwndldz7j262rvfpd049tq74mq0ylkrs

$ cardano-address key hash < policy.xvk
policy_vkh1qpc9xly4lc7yt98gcf59kdcqcss6dda4u9g72e775yxpxeypamc
$ cardano-address key hash < policy.vk
policy_vkh1qpc9xly4lc7yt98gcf59kdcqcss6dda4u9g72e775yxpxeypamc
$ cardano-address key hash --hex < policy.vk
0070537c95fe3c4594e8c2685b3700c421a6b7b5e151e567dea10c13
```

> :information_source: The last segment in the path is the key index and can be incremented up to `2^31-1` to derive more keys.
</details>

<details>
<summary>How to generate a payment verification key (<strong>addr.xvk</strong>)</summary>
Expand Down
3 changes: 3 additions & 0 deletions command-line/lib/Command/Key/Child.hs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,9 @@ run Child{path} = do
-- 2147485502 stands for 1854H
if p == 2147485502 then
pure CIP5.acct_shared_xsk
-- 2147485503 stands for 1855H
else if p == 2147485503 then
pure CIP5.policy_xsk
else
pure CIP5.acct_xsk
| hrp == CIP5.root_shared_xsk = pure CIP5.acct_shared_xsk
Expand Down
4 changes: 3 additions & 1 deletion command-line/lib/Command/Key/Hash.hs
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,14 @@ run Hash{outputFormat} = do
, ( CIP5.addr_shared_xvk , CIP5.addr_shared_vkh )
, ( CIP5.stake_shared_vk , CIP5.stake_shared_vkh )
, ( CIP5.stake_shared_xvk, CIP5.stake_shared_vkh )
, ( CIP5.policy_vk , CIP5.policy_vkh )
, ( CIP5.policy_xvk , CIP5.policy_vkh )
]
allowedPrefixes = map fst prefixes
prefixFor = fromJust . flip lookup prefixes

guardBytes hrp bytes
| hrp `elem` [CIP5.addr_xvk, CIP5.stake_xvk, CIP5.addr_shared_xvk, CIP5.stake_shared_xvk] = do
| hrp `elem` [CIP5.addr_xvk, CIP5.stake_xvk, CIP5.addr_shared_xvk, CIP5.stake_shared_xvk, CIP5.policy_xvk] = do
when (BS.length bytes /= 64) $
fail "data should be a 32-byte public key with a 32-byte chain-code appended"

Expand Down
2 changes: 2 additions & 0 deletions command-line/lib/Command/Key/Inspect.hs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ run Inspect = do
, CIP5.addr_xsk
, CIP5.stake_xvk
, CIP5.stake_xsk
, CIP5.policy_xvk
, CIP5.policy_xsk
, CIP5.root_shared_xvk
, CIP5.root_shared_xsk
, CIP5.acct_shared_xvk
Expand Down
1 change: 1 addition & 0 deletions command-line/lib/Command/Key/Public.hs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ run Public{chainCode} = do
, (CIP5.acct_shared_xsk, (CIP5.acct_shared_xvk, CIP5.acct_shared_vk) )
, (CIP5.addr_shared_xsk, (CIP5.addr_shared_xvk, CIP5.addr_shared_vk) )
, (CIP5.stake_shared_xsk, (CIP5.stake_shared_xvk, CIP5.stake_shared_vk) )
, (CIP5.policy_xsk, (CIP5.policy_xvk, CIP5.policy_vk) )
]
allowedPrefixes = map fst prefixes
getCC WithChainCode = fst
Expand Down
1 change: 1 addition & 0 deletions command-line/test/Command/Key/ChildSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ spec = describeCmd [ "key", "child" ] $ do
specChildValidPath "addr_shared_xsk" ["1854H/1815H/0H/0/0"]
specChildValidPath "addr_shared_xsk" ["1854H/1815H/0H", "0/0"]
specChildValidPath "addr_xsk" ["14H/42H"]
specChildValidPath "policy_xsk" ["1855H/1815H/0H"]

specChildInvalidPath "from a parent root key" ["0H"]
specChildInvalidPath "from a parent account key" ["1852H/1815H/0H", "0H"]
Expand Down
3 changes: 3 additions & 0 deletions command-line/test/Command/Key/HashSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ spec = describeCmd [ "key", "hash" ] $ do
specKeyPublic "shared" "addr_shared_vkh" "1854H/1815H/0H/0/0" "--with-chain-code"
specKeyPublic "shared" "stake_shared_vkh" "1854H/1815H/0H/2/0" "--with-chain-code"

specKeyPublic "shelley" "policy_vkh" "1855H/1815H/0H" "--with-chain-code"
specKeyPublic "shelley" "policy_vkh" "1855H/1815H/0H" "--without-chain-code"

specKeyNotPublic :: SpecWith ()
specKeyNotPublic = it "fail if key isn't public" $ do
(out, err) <- cli [ "recovery-phrase", "generate" ] ""
Expand Down
2 changes: 2 additions & 0 deletions command-line/test/Command/Key/InspectSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,13 @@ spec = describeCmd [ "key", "inspect" ] $ do
specInspectPrivate CIP5.acct_shared_xsk
specInspectPrivate CIP5.addr_shared_xsk
specInspectPrivate CIP5.stake_shared_xsk
specInspectPrivate CIP5.policy_xsk

specInspectPublic CIP5.root_xvk
specInspectPublic CIP5.acct_xvk
specInspectPublic CIP5.addr_xvk
specInspectPublic CIP5.stake_xvk
specInspectPublic CIP5.policy_xvk
specInspectPublic CIP5.root_shared_xvk
specInspectPublic CIP5.acct_shared_xvk
specInspectPublic CIP5.addr_shared_xvk
Expand Down
2 changes: 1 addition & 1 deletion core/lib/Cardano/Address/Derivation.hs
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ credentialHashSize = hashDigestSize Blake2b_224
-- are no constructors for these.
--
-- @since 1.0.0
data Depth = RootK | AccountK | PaymentK | DelegationK | ScriptK
data Depth = RootK | AccountK | PaymentK | DelegationK | ScriptK | PolicyK

-- | A derivation index, with phantom-types to disambiguate derivation type.
--
Expand Down
23 changes: 16 additions & 7 deletions core/lib/Cardano/Address/Script.hs
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ scriptHashFromBytes bytes
| BS.length bytes /= credentialHashSize = Nothing
| otherwise = Just $ ScriptHash bytes

data KeyRole = Payment | Delegation
data KeyRole = Payment | Delegation | Policy
deriving (Generic, Show, Ord, Eq)
instance NFData KeyRole

Expand Down Expand Up @@ -247,15 +247,20 @@ keyHashToText (KeyHash cred keyHash) = case cred of
T.decodeUtf8 $ encode (EBech32 CIP5.addr_shared_vkh) keyHash
Delegation ->
T.decodeUtf8 $ encode (EBech32 CIP5.stake_shared_vkh) keyHash
Policy ->
T.decodeUtf8 $ encode (EBech32 CIP5.policy_vkh) keyHash

-- | Construct a 'KeyHash' from 'Text'. It should be
-- Bech32 encoded text with one of following hrp:
-- - `addr_shared_vkh`
-- - `stake_shared_vkh`
-- - `policy_vkh`
-- - `addr_shared_vk`
-- - `stake_shared_vk`
-- - `addr_shared_xvk`
-- - `stake_shared_xvk`
-- - `policy_vk`
-- - `policy_xvk`
-- Raw keys will be hashed on the fly, whereas hash that are directly
-- provided will remain as such.
--
Expand All @@ -272,10 +277,13 @@ keyHashFromText txt = do
convertBytes hrp bytes
| hrp == CIP5.addr_shared_vkh = Just (Payment, bytes)
| hrp == CIP5.stake_shared_vkh = Just (Delegation, bytes)
| hrp == CIP5.policy_vkh = Just (Policy, bytes)
| hrp == CIP5.addr_shared_vk = Just (Payment, hashCredential bytes)
| hrp == CIP5.addr_shared_xvk = Just (Payment, hashCredential $ BS.take 32 bytes)
| hrp == CIP5.stake_shared_vk = Just (Delegation, hashCredential bytes)
| hrp == CIP5.stake_shared_xvk = Just (Delegation, hashCredential $ BS.take 32 bytes)
| hrp == CIP5.policy_vk = Just (Policy, hashCredential bytes)
| hrp == CIP5.policy_xvk = Just (Policy, hashCredential $ BS.take 32 bytes)
| otherwise = Nothing

-- Validation level. Required level does basic check that will make sure the script
Expand Down Expand Up @@ -307,7 +315,7 @@ prettyErrKeyHashFromText = \case
ErrKeyHashFromTextWrongPayload ->
"Verification key hash must contain exactly 28 bytes."
ErrKeyHashFromTextWrongHrp ->
"Invalid human-readable prefix: must be 'script_vkh'."
"Invalid human-readable prefix: must be 'X_vkh', 'X_vk', 'X_xvk' where X is 'addr_shared', 'stake_shared' or 'policy'."
ErrKeyHashFromTextWrongDataPart ->
"Verification key hash is Bech32-encoded but has an invalid data part."

Expand Down Expand Up @@ -534,7 +542,8 @@ prettyErrValidateScript = \case
"All keys of a script must have the same role: either payment or delegation."
Malformed ->
"Parsing of the script failed. The script should be composed of nested \
\lists, the verification keys should be bech32-encoded with prefix 'script_vhk', \
\lists, the verification keys should be bech32-encoded with prefix \
\'X_vkh', 'X_vk', 'X_xvk' where X is 'addr_shared', 'stake_shared' or 'policy' and\
\timelocks must use non-negative numbers as slots."
NotRecommended EmptyList ->
"The list inside a script is empty or only contains timelocks \
Expand Down Expand Up @@ -562,7 +571,7 @@ prettyErrValidateScriptTemplate = \case
DuplicateXPubs ->
"The cosigners in a script template must stand behind an unique extended public key."
MissingCosignerXPub ->
"Each cosigner in a script template must have extended public key."
"Each cosigner in a script template must have an extended public key."
NoCosignerInScript ->
"The script of a template must have at least one cosigner defined."
NoCosignerXPub ->
Expand Down Expand Up @@ -650,7 +659,7 @@ instance FromJSON (Script KeyHash) where
(Nothing, Just{}, Nothing) -> parseAllOf v
(Nothing, Nothing, Just{}) -> parseAtLeast v
(Nothing, Nothing, Nothing) -> fail
"Found object with no known key 'any', 'all' or 'some'"
"Found object with unknown key. Expecting 'any', 'all' or 'some'"
( _, _, _) -> fail
"Found multiple keys 'any', 'all' and/or 'some' at the same level"
String{} ->
Expand Down Expand Up @@ -722,7 +731,7 @@ instance FromJSON Cosigner where
fail "Cosigner number should be between '0' and '255'"
pure $ Cosigner num
_ -> fail "Cosigner should be enumerated with number"
_ -> fail "Cosigner should be of form: cosigner#num"
_ -> fail "Cosigner should be of the form: cosigner#num"

encodeXPub :: XPub -> Value
encodeXPub = String . T.decodeUtf8 . encode EBase16 . xpubToBytes
Expand Down Expand Up @@ -762,7 +771,7 @@ instance FromJSON (Script Cosigner) where
(Nothing, Nothing, Just{}, Nothing) -> parseAtLeast v
(Nothing, Nothing, Nothing, Just{}) -> parserCosigner v
(Nothing, Nothing, Nothing, Nothing) -> fail
"Found object with no known key 'any', 'all', 'some' or 'cosigner'"
"Found object with unknown key. Expecting 'any', 'all', 'some' or 'cosigner'"
( _, _, _, _) -> fail
"Found multiple keys 'any', 'all', 'cosigner' and/or 'some' at the same level"
_ ->
Expand Down
24 changes: 24 additions & 0 deletions core/lib/Cardano/Address/Style/Shelley.hs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ module Cardano.Address.Style.Shelley
, deriveAddressPrivateKey
, deriveDelegationPrivateKey
, deriveAddressPublicKey
, derivePolicyPrivateKey

-- * Addresses
-- $addresses
Expand Down Expand Up @@ -310,6 +311,18 @@ deriveAccountPrivateKey
deriveAccountPrivateKey =
Internal.deriveAccountPrivateKey

-- Re-export from 'Cardano.Address.Derivation' to have it documented specialized in Haddock.
--
-- | Derives a policy private key from the given root private key.
--
-- @since 3.9.0
derivePolicyPrivateKey
:: Shelley 'RootK XPrv
-> Index 'Hardened 'PolicyK
-> Shelley 'PolicyK XPrv
derivePolicyPrivateKey (Shelley rootXPrv) policyIx =
Shelley $ deriveAccountPrivateKeyShelley rootXPrv policyIx policyPurposeIndex

-- Re-export from 'Cardano.Address.Derivation' to have it documented specialized in Haddock.
--
-- | Derives an address private key from the given account private key.
Expand Down Expand Up @@ -979,6 +992,17 @@ unsafeFromRight =
purposeIndex :: Word32
purposeIndex = 0x8000073c

-- Policy purpose is a constant set to 1855' (or 0x8000073c) following the CIP-1855
-- https://github.com/cardano-foundation/CIPs/tree/master/CIP-1855
--
-- It indicates that the subtree of this node is used according to this
-- specification.
--
-- Hardened derivation is used at this level.
policyPurposeIndex :: Word32
policyPurposeIndex = 0x8000073f


-- 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.
Expand Down
18 changes: 18 additions & 0 deletions core/lib/Cardano/Codec/Bech32/Prefixes.hs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ module Cardano.Codec.Bech32.Prefixes
, stake_shared_xvk
, stake_shared_xsk

-- * Keys for 1855H
, policy_vk
, policy_xvk
, policy_vkh
, policy_xsk
) where

import Codec.Binary.Bech32
Expand Down Expand Up @@ -194,3 +199,16 @@ addr_shared_vkh = [humanReadablePart|addr_shared_vkh|]

stake_shared_vkh :: HumanReadablePart
stake_shared_vkh = [humanReadablePart|stake_shared_vkh|]

-- Policy
policy_vk :: HumanReadablePart
policy_vk = [humanReadablePart|policy_vk|]

policy_xvk :: HumanReadablePart
policy_xvk = [humanReadablePart|policy_xvk|]

policy_vkh :: HumanReadablePart
policy_vkh = [humanReadablePart|policy_vkh|]

policy_xsk :: HumanReadablePart
policy_xsk = [humanReadablePart|policy_xsk|]
2 changes: 1 addition & 1 deletion core/test/Cardano/Address/ScriptSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -674,7 +674,7 @@ spec = do
`shouldBe` Left err

it "Unknown keys" $ do
let err = "Error in $: Found object with no known key 'any', 'all' or 'some'"
let err = "Error in $: Found object with unknown key. Expecting 'any', 'all' or 'some'"
Json.eitherDecode @(Script KeyHash) "{ \"patate\": {} }"
`shouldBe` Left err

Expand Down

0 comments on commit c6472d2

Please sign in to comment.