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

Allow to generate stake addresses #58

Merged
merged 2 commits into from
Aug 20, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions command-line/cardano-addresses-cli.cabal
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
cabal-version: 1.12

-- This file has been generated from package.yaml by hpack version 0.31.2.
-- This file has been generated from package.yaml by hpack version 0.33.0.
--
-- see: https://github.com/sol/hpack
--
-- hash: 3737cc69d18f00a50e3c375be8e4be7728f2315689dc3bc92200f54e3c79c5b6
-- hash: 23602f11d7fb1ac8c8333e3d2d3c95385ea53972cbde55027a9b540b975d71c2

name: cardano-addresses-cli
version: 1.0.0
Expand Down Expand Up @@ -38,6 +38,7 @@ library
Command.Address.Inspect
Command.Address.Payment
Command.Address.Pointer
Command.Address.Reward
Command.Key
Command.Key.Child
Command.Key.FromRecoveryPhrase
Expand Down Expand Up @@ -68,7 +69,9 @@ library
, cardano-addresses
, cardano-crypto
, code-page
, exceptions
, extra
, fmt
, optparse-applicative
, safe
, text
Expand Down Expand Up @@ -102,6 +105,7 @@ test-suite unit
Command.Address.InspectSpec
Command.Address.PaymentSpec
Command.Address.PointerSpec
Command.Address.RewardSpec
Command.Key.ChildSpec
Command.Key.FromRecoveryPhraseSpec
Command.Key.InspectSpec
Expand Down
4 changes: 4 additions & 0 deletions command-line/lib/Command/Address.hs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import qualified Command.Address.Delegation as Delegation
import qualified Command.Address.Inspect as Inspect
import qualified Command.Address.Payment as Payment
import qualified Command.Address.Pointer as Pointer
import qualified Command.Address.Reward as Reward


data Cmd
Expand All @@ -37,6 +38,7 @@ data Cmd
| Delegation Delegation.Cmd
| Pointer Pointer.Cmd
| Inspect Inspect.Cmd
| Reward Reward.Cmd
deriving (Show)

mod :: (Cmd -> parent) -> Mod CommandFields parent
Expand All @@ -57,6 +59,7 @@ mod liftCmd = command "address" $
, Delegation.mod Delegation
, Pointer.mod Pointer
, Inspect.mod Inspect
, Reward.mod Reward
]

run :: Cmd -> IO ()
Expand All @@ -66,3 +69,4 @@ run = \case
Delegation sub -> Delegation.run sub
Pointer sub -> Pointer.run sub
Inspect sub -> Inspect.run sub
Reward sub -> Reward.run sub
90 changes: 90 additions & 0 deletions command-line/lib/Command/Address/Reward.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}

{-# OPTIONS_HADDOCK hide #-}

module Command.Address.Reward
( Cmd
, mod
, run
) where

import Prelude hiding
( mod )

import Cardano.Address
( NetworkTag (..), bech32With )
import Cardano.Address.Style.Shelley
( mkNetworkDiscriminant, shelleyMainnet, shelleyTestnet )
import Codec.Binary.Bech32
( humanReadablePartFromText )
import Control.Monad.Catch
( throwM )
import Fmt
( build, fmt )
import Options.Applicative
( CommandFields, Mod, command, footerDoc, header, helper, info, progDesc )
import Options.Applicative.Discrimination
( networkTagOpt )
import Options.Applicative.Help.Pretty
( bold, indent, string, vsep )
import Options.Applicative.Style
( Style (..) )
import System.Exit
( die )
import System.IO
( stdin, stdout )
import System.IO.Extra
( hGetXPub, progName )

import qualified Cardano.Address.Style.Shelley as Shelley
import qualified Data.ByteString.Char8 as B8
import qualified Data.Text.Encoding as T


newtype Cmd = Cmd
{ networkTag :: NetworkTag
} deriving (Show)

mod :: (Cmd -> parent) -> Mod CommandFields parent
mod liftCmd = command "stake" $
info (helper <*> fmap liftCmd parser) $ mempty
<> progDesc "Create a stake address"
<> header "Create a stake address \
\that references a staking key (1-1)."
<> footerDoc (Just $ vsep
[ string "The public key is read from stdin."
, string ""
, string "Example:"
, indent 2 $ bold $ string $ "$ "<>progName<>" recovery-phrase generate --size 15 \\"
, indent 4 $ bold $ string $ "| "<>progName<>" key from-recovery-phrase Shelley > root.prv"
, indent 2 $ string ""
, indent 2 $ bold $ string "$ cat root.prv \\"
, indent 4 $ bold $ string $ "| "<>progName<>" key child 1852H/1815H/0H/2/0 > stake.prv"
, indent 2 $ string ""
, indent 2 $ bold $ string "$ cat stake.prv \\"
, indent 4 $ bold $ string $ "| "<>progName<>" key public \\"
, indent 4 $ bold $ string $ "| "<>progName<>" address stake --network-tag 0"
, indent 2 $ string "stake1uqly0fjvrgguywze067gwhsexggtj8rrdnxczgp5vexe8zgxqns3g"
])
where
parser = Cmd
<$> networkTagOpt Shelley

run :: Cmd -> IO ()
run Cmd{networkTag} = do
xpub <- hGetXPub stdin
hpr <- hprFromNetTag
case (mkNetworkDiscriminant . fromIntegral . unNetworkTag) networkTag of
Left e -> die (fmt $ build e)
Right discriminant -> do
let addr = Shelley.stakeAddress discriminant (Shelley.liftXPub xpub)
B8.hPutStr stdout $ T.encodeUtf8 $ bech32With hpr addr
where
hprFromText = either throwM pure . humanReadablePartFromText
hasufell marked this conversation as resolved.
Show resolved Hide resolved
hprFromNetTag
| shelleyMainnet == networkTag = hprFromText "stake"
| shelleyTestnet == networkTag = hprFromText "stake_test"
| otherwise = hprFromText "addr"
hasufell marked this conversation as resolved.
Show resolved Hide resolved


2 changes: 2 additions & 0 deletions command-line/package.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ library:
- cardano-addresses
- cardano-crypto
- code-page
- exceptions
- extra
- fmt
- optparse-applicative
- safe
- text
Expand Down
57 changes: 57 additions & 0 deletions command-line/test/Command/Address/RewardSpec.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{-# LANGUAGE FlexibleContexts #-}

module Command.Address.RewardSpec
( spec
) where

import Prelude

import Test.Hspec
( Spec, SpecWith, it, shouldBe, shouldContain )
import Test.Utils
( cli, describeCmd )

spec :: Spec
spec = describeCmd [ "address", "stake" ] $ do
specShelley defaultPhrase "1852H/1815H/0H/2/0" 0
"stake_test1ura3dk68y6echdmfmnvm8mej8u5truwv8ufmv830w5a45tcsfhtt2"

specShelley defaultPhrase "1852H/1815H/0H/2/0" 3
"addr1u0a3dk68y6echdmfmnvm8mej8u5truwv8ufmv830w5a45tc3kzy0t"

specMalformedNetwork "💩"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I love this one 😆

Copy link
Contributor

Choose a reason for hiding this comment

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

:) It is very important. In particular, this one caught some nice regressions on Windows Powershell.


specInvalidNetwork "42"

specShelley :: [String] -> String -> Int -> String -> SpecWith ()
specShelley phrase path networkTag want = it ("golden shelley (payment) " <> path) $ do
out <- cli [ "key", "from-recovery-phrase", "shelley" ] (unwords phrase)
>>= cli [ "key", "child", path ]
>>= cli [ "key", "public" ]
>>= cli [ "address", "stake", "--network-tag", show networkTag ]
out `shouldBe` want

specMalformedNetwork :: String -> SpecWith ()
specMalformedNetwork networkTag = it ("malformed network " <> networkTag) $ do
(out, err) <- cli [ "key", "from-recovery-phrase", "shelley" ] (unwords defaultPhrase)
>>= cli [ "key", "public" ]
>>= cli [ "address", "stake", "--network-tag", networkTag ]
out `shouldBe` ""
err `shouldContain` "Invalid network tag"
err `shouldContain` "Usage"

specInvalidNetwork :: String -> SpecWith ()
specInvalidNetwork networkTag = it ("invalid network " <> networkTag) $ do
(out, err) <- cli [ "key", "from-recovery-phrase", "shelley" ] (unwords defaultPhrase)
>>= cli [ "key", "public" ]
>>= cli [ "address", "stake", "--network-tag", networkTag ]
out `shouldBe` ""
err `shouldContain` "Invalid network tag"

defaultPhrase :: [String]
defaultPhrase =
[ "pole", "pulse", "wolf", "blame", "chronic"
, "ship", "vivid", "tree", "small", "onion"
, "host", "accident", "burden", "lazy", "swarm"
]

11 changes: 11 additions & 0 deletions core/lib/Cardano/Address.hs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ module Cardano.Address
( -- * Address
Address
, PaymentAddress (..)
, StakeAddress (..)
, DelegationAddress (..)
, PointerAddress (..)
, ChainPointer (..)
Expand Down Expand Up @@ -121,6 +122,16 @@ fromBech32 :: Text -> Maybe Address
fromBech32 =
eitherToMaybe . fmap unsafeMkAddress. E.fromBech32 (const id) . T.encodeUtf8

-- | Encoding of addresses for certain key types and backend targets.
--
-- @since 2.0.0
class HasNetworkDiscriminant key => StakeAddress key where
-- | Convert a staking key to a stake 'Address' (aka: reward account address)
-- valid for the given network discrimination.
--
-- @since 2.0.0
stakeAddress :: NetworkDiscriminant key -> key 'StakingK XPub -> Address

-- | Encoding of addresses for certain key types and backend targets.
--
-- @since 1.0.0
Expand Down
45 changes: 43 additions & 2 deletions core/lib/Cardano/Address/Style/Shelley.hs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ module Cardano.Address.Style.Shelley
, paymentAddress
, delegationAddress
, pointerAddress
, stakeAddress
, extendAddress
, ErrExtendAddress (..)

Expand Down Expand Up @@ -127,6 +128,8 @@ import Data.Word
( Word32 )
import Data.Word7
( getVariableLengthNat, putVariableLengthNat )
import Fmt
( Buildable, build, (+|), (|+) )
import GHC.Generics
( Generic )

Expand Down Expand Up @@ -374,6 +377,23 @@ deriveStakingPrivateKey =
-- > bech32 $ pointerAddress tag (toXPub <$> addrK) ptr
-- > "addr1gxpfffuj3zkp5g7ct6h4va89caxx9ayq2gvkyfvww48sdnmmqypqfcp5um"

instance Internal.StakeAddress Shelley where
stakeAddress discrimination k = unsafeMkAddress $
invariantSize expectedLength $ BL.toStrict $ runPut $ do
putWord8 firstByte
putByteString (blake2b224 k)
where
-- First 4 bits are `1110` for keyhash28 reward account address.
-- Next 4 bits are network discriminator.
-- `1110 0000` is 224 in decimal.
firstByte =
let addrType = 224
netTagLimit = 16
in addrType + invariantNetworkTag netTagLimit (networkTag @Shelley discrimination)
expectedLength =
let headerSizeBytes = 1
in headerSizeBytes + pubkeyHashSize

instance Internal.PaymentAddress Shelley where
paymentAddress discrimination k = unsafeMkAddress $
invariantSize expectedLength $ BL.toStrict $ runPut $ do
Expand All @@ -389,8 +409,12 @@ instance Internal.PaymentAddress Shelley where
-- will be `0110`. The next for 4 bits are reserved for network discriminator.
-- `0110 0000` is 96 in decimal.
firstByte =
96 + invariantNetworkTag 16 (networkTag @Shelley discrimination)
expectedLength = 1 + pubkeyHashSize
let addrType = 96
netTagLimit = 16
in addrType + invariantNetworkTag netTagLimit (networkTag @Shelley discrimination)
expectedLength =
let headerSizeBytes = 1
in headerSizeBytes + pubkeyHashSize

instance Internal.DelegationAddress Shelley where
delegationAddress discrimination paymentKey =
Expand Down Expand Up @@ -617,6 +641,20 @@ pointerAddress
pointerAddress =
Internal.pointerAddress

-- Re-export from 'Cardano.Address' to have it documented specialized in Haddock.
--
-- | Convert a staking key to a stake Address (aka reward account address)
-- for the given network discrimination.
--
-- @since 2.0.0
stakeAddress
:: NetworkDiscriminant Shelley
-> Shelley 'StakingK XPub
-> Address
stakeAddress =
Internal.stakeAddress


--
-- Network Discriminant
--
Expand All @@ -634,6 +672,9 @@ newtype MkNetworkDiscriminantError
-- ^ Wrong network tag.
deriving (Eq, Show)

instance Buildable MkNetworkDiscriminantError where
build (ErrWrongNetworkTag i) = "Invalid network tag "+|i|+". Must be between [0, 15]"

-- | Construct 'NetworkDiscriminant' for Cardano 'Shelley' from a number.
-- If the number is invalid, ie., not between 0 and 15, then
-- 'MkNetworkDiscriminantError' is thrown.
Expand Down