diff --git a/README.md b/README.md index f6e1212ef..6f0286399 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,30 @@ $ cardano-address key public --with-chain-code < acct.xsk | cardano-address key ``` +
+ How to generate a private policy key (policy.xsk), a public policy key (policy.vk) and its hash (policy.vkh) + +```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. +
+
How to generate a payment verification key (addr.xvk) diff --git a/command-line/lib/Command/Key/Child.hs b/command-line/lib/Command/Key/Child.hs index 7ab6a37c1..f7b49034f 100644 --- a/command-line/lib/Command/Key/Child.hs +++ b/command-line/lib/Command/Key/Child.hs @@ -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 diff --git a/command-line/lib/Command/Key/Hash.hs b/command-line/lib/Command/Key/Hash.hs index 792dd8cb4..a4528325f 100644 --- a/command-line/lib/Command/Key/Hash.hs +++ b/command-line/lib/Command/Key/Hash.hs @@ -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" diff --git a/command-line/lib/Command/Key/Inspect.hs b/command-line/lib/Command/Key/Inspect.hs index 087b5eb85..1c23c7a22 100644 --- a/command-line/lib/Command/Key/Inspect.hs +++ b/command-line/lib/Command/Key/Inspect.hs @@ -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 diff --git a/command-line/lib/Command/Key/Public.hs b/command-line/lib/Command/Key/Public.hs index 20bbcf57e..d960bcd82 100644 --- a/command-line/lib/Command/Key/Public.hs +++ b/command-line/lib/Command/Key/Public.hs @@ -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 diff --git a/command-line/test/Command/Key/ChildSpec.hs b/command-line/test/Command/Key/ChildSpec.hs index f97a01878..c7eaee0a0 100644 --- a/command-line/test/Command/Key/ChildSpec.hs +++ b/command-line/test/Command/Key/ChildSpec.hs @@ -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"] diff --git a/command-line/test/Command/Key/HashSpec.hs b/command-line/test/Command/Key/HashSpec.hs index 4d7021037..d7b20bc34 100644 --- a/command-line/test/Command/Key/HashSpec.hs +++ b/command-line/test/Command/Key/HashSpec.hs @@ -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" ] "" diff --git a/command-line/test/Command/Key/InspectSpec.hs b/command-line/test/Command/Key/InspectSpec.hs index c0ef26fad..abf724cd7 100644 --- a/command-line/test/Command/Key/InspectSpec.hs +++ b/command-line/test/Command/Key/InspectSpec.hs @@ -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 diff --git a/core/lib/Cardano/Address/Derivation.hs b/core/lib/Cardano/Address/Derivation.hs index e7f3552b8..faaa94950 100644 --- a/core/lib/Cardano/Address/Derivation.hs +++ b/core/lib/Cardano/Address/Derivation.hs @@ -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. -- diff --git a/core/lib/Cardano/Address/Script.hs b/core/lib/Cardano/Address/Script.hs index eca54f534..050871ff2 100644 --- a/core/lib/Cardano/Address/Script.hs +++ b/core/lib/Cardano/Address/Script.hs @@ -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 @@ -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. -- @@ -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 @@ -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." @@ -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 \ @@ -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 -> @@ -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{} -> @@ -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 @@ -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" _ -> diff --git a/core/lib/Cardano/Address/Style/Shelley.hs b/core/lib/Cardano/Address/Style/Shelley.hs index 520d260a2..02457fdfb 100644 --- a/core/lib/Cardano/Address/Style/Shelley.hs +++ b/core/lib/Cardano/Address/Style/Shelley.hs @@ -41,6 +41,7 @@ module Cardano.Address.Style.Shelley , deriveAddressPrivateKey , deriveDelegationPrivateKey , deriveAddressPublicKey + , derivePolicyPrivateKey -- * Addresses -- $addresses @@ -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. @@ -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. diff --git a/core/lib/Cardano/Codec/Bech32/Prefixes.hs b/core/lib/Cardano/Codec/Bech32/Prefixes.hs index f189901f7..a11924f93 100644 --- a/core/lib/Cardano/Codec/Bech32/Prefixes.hs +++ b/core/lib/Cardano/Codec/Bech32/Prefixes.hs @@ -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 @@ -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|] diff --git a/core/test/Cardano/Address/ScriptSpec.hs b/core/test/Cardano/Address/ScriptSpec.hs index f432579a1..d239aa84b 100644 --- a/core/test/Cardano/Address/ScriptSpec.hs +++ b/core/test/Cardano/Address/ScriptSpec.hs @@ -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