Skip to content

Commit

Permalink
generalize public key derivation in the API for all roles.
Browse files Browse the repository at this point in the history
  And also, to keys and not key hashes. The goal of this endpoint is to
  expose public keys so clients can perform signature verification all
  by themselves.
  • Loading branch information
KtorZ committed Oct 22, 2020
1 parent c62a654 commit cf172b5
Show file tree
Hide file tree
Showing 15 changed files with 283 additions and 193 deletions.
14 changes: 0 additions & 14 deletions lib/core-integration/src/Test/Integration/Framework/DSL.hs
Expand Up @@ -117,7 +117,6 @@ module Test.Integration.Framework.DSL
, unsafeGetTransactionTime
, getTxId
, oneSecond
, getWalletKey

-- * Delegation helpers
, mkEpochInfo
Expand Down Expand Up @@ -182,7 +181,6 @@ import Cardano.Wallet.Api.Types
, ApiTransaction
, ApiTxId (ApiTxId)
, ApiUtxoStatistics (..)
, ApiVerificationKeyHash
, ApiWallet
, ApiWalletDelegation (..)
, ApiWalletDelegationNext (..)
Expand Down Expand Up @@ -221,7 +219,6 @@ import Cardano.Wallet.Primitive.Types
( ActiveSlotCoefficient (..)
, Address (..)
, Coin (..)
, DerivationIndex
, EpochLength (..)
, EpochNo
, Hash (..)
Expand Down Expand Up @@ -1404,17 +1401,6 @@ listAddresses ctx w = do
(_, addrs) <- unsafeRequest @[ApiAddress n] ctx link Empty
return addrs

getWalletKey
:: forall t w. ( HasType (ApiT WalletId) w )
=> Context t
-> w
-> ApiT DerivationIndex
-> IO ApiVerificationKeyHash
getWalletKey ctx w index = do
let link = Link.getWalletKey w index
(_, keyHash) <- unsafeRequest @ApiVerificationKeyHash ctx link Empty
return keyHash

listAllTransactions
:: forall n t w.
( DecodeAddress n
Expand Down
Expand Up @@ -28,21 +28,21 @@ import Cardano.Wallet.Api.Types
, ApiT (..)
, ApiTransaction
, ApiUtxoStatistics
, ApiVerificationKeyHash (..)
, ApiVerificationKey (..)
, ApiWallet
, DecodeAddress
, DecodeStakeAddress
, EncodeAddress (..)
, WalletStyle (..)
)
import Cardano.Wallet.Primitive.AddressDerivation
( DerivationIndex (..)
( AccountingStyle (..)
, DerivationIndex (..)
, DerivationType (..)
, Index (..)
, PassphraseMaxLength (..)
, PassphraseMinLength (..)
, PaymentAddress
, hex
)
import Cardano.Wallet.Primitive.AddressDerivation.Byron
( ByronKey )
Expand All @@ -55,13 +55,11 @@ import Cardano.Wallet.Primitive.AddressDiscovery.Sequential
import Cardano.Wallet.Primitive.SyncProgress
( SyncProgress (..) )
import Cardano.Wallet.Primitive.Types
( DerivationIndex (..)
, Hash (..)
, walletNameMaxLength
, walletNameMinLength
)
( walletNameMaxLength, walletNameMinLength )
import Control.Monad
( forM_ )
import Data.Aeson
( ToJSON (..) )
import Data.Generics.Internal.VL.Lens
( view, (^.) )
import Data.List
Expand Down Expand Up @@ -102,7 +100,6 @@ import Test.Integration.Framework.DSL
, fixtureWallet
, genMnemonics
, getFromResponse
, getWalletKey
, json
, listAddresses
, minUTxOValue
Expand Down Expand Up @@ -135,7 +132,6 @@ import qualified Cardano.Wallet.Api.Link as Link
import qualified Data.List as L
import qualified Data.List.NonEmpty as NE
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Network.HTTP.Types.Status as HTTP

spec :: forall n t.
Expand Down Expand Up @@ -1131,48 +1127,80 @@ spec = describe "SHELLEY_WALLETS" $ do
r <- request @ApiUtxoStatistics ctx (Link.getUTxOsStatistics @'Shelley w) headers Empty
verify r expectations

describe "WALLETS_GET_KEY_01 - can get verification key hash for valid indices" $ do
let indices = [ 0, 100, 2147483647 ]
forM_ indices $ \index -> it (show index) $ \ctx -> do

m <- genMnemonics M15
let payload = Json [json| {
"name": "Wallet",
"mnemonic_sentence": #{m},
"passphrase": #{fixturePassphrase}
} |]
r <- request @ApiWallet ctx (Link.postWallet @'Shelley) Default payload
verify r [ expectResponseCode @IO HTTP.status201 ]
let apiWal = getFromResponse id r

let link = Link.getWalletKey (apiWal ^. id) (ApiT $ DerivationIndex index)
rGet <- request @ApiVerificationKeyHash ctx link Default Empty
verify rGet [ expectResponseCode @IO HTTP.status200 ]

describe "WALLETS_GET_KEY_02 - golden tests for verification key hashes" $ do
describe "WALLETS_GET_KEY_01 - golden tests for verification key" $ do
--- $ cat recovery-phrase.txt
-- pulp ten light rhythm replace vessel slow drift kingdom amazing negative join auction ugly symptom
--- $ cat recovery-phrase.txt | cardano-address key from-recovery-phrase Shelley > root.prv
--- $ cat root.prv \
--- > | cardano-address key child 1852H/1815H/0H/3/INDEX \
--- > | cardano-address key child 1852H/1815H/0H/ROLE/INDEX \
--- > | cardano-address key public \
--- > | cardano-address key hash --base16
let matrix = [ (0, "6b36aac60b70e47a7ed365e20d1a2ad9466c244b5d90227f0f4ecdcf")
, (100, "99d083e217fe70e113ea57f40214c6a3ad44e2ebbe2e1fc3ac07a988")
, (2147483647, "f9e4aab174b83f711a0e243cfee407cd0f24ac9fbf48f899ac660143") ]
forM_ matrix $ \(index, keyHash) -> it (show index) $ \ctx -> do
let payload = Json [json| {
"name": "Wallet",
"mnemonic_sentence": #{explicitMnemonics},
"passphrase": #{fixturePassphrase}
} |]
r <- request @ApiWallet ctx (Link.postWallet @'Shelley) Default payload
verify r [ expectResponseCode @IO HTTP.status201 ]
let apiWal = getFromResponse id r
let matrix :: [(AccountingStyle, DerivationIndex, String)]
matrix =
[ ( UtxoExternal
, DerivationIndex 0
, "addr_xvk1tmdggpmj7r2mqfhneqsc5fzfj2j4sxf4j7mqwwsa8w58vz94zr\
\463ndsajkjvn82va30fgf22qruc53zxyjp6pu7yd87ppdefd6u98cue5ul9"
)
, ( UtxoInternal
, DerivationIndex 100
, "addr_xvk1wchen6vz4zz7kpfjld3g89zdcpdv2hzvtsufphgvpjxjkl49pq\
\rfd2lgqg228zl7ylax8c5tr0rkys3phfkxd5j6s56c0tfy7r49jhct38kq6"
)
, ( MutableAccount
, DerivationIndex 2147483647
, "stake_xvk1qy9tp370ze3cfre8f6daz7l85pgk3wpg6s5zqae2yjljwqkx4\
\ht28cxx7j5ju0wl8795s3yj8z3te4h2j9eaztll2z4mxhwwkppy53sf49ev5"
)
, ( MultisigScript
, DerivationIndex 42
, "script_xvk1mjr5lrrlxuvelx94hu2cttmg5pp6cwy5h0sa37qvpcd07pv9g\
\23nlvugj5ez9qfxxvkmjwnpn69s48cv572phfy6qpmnwat0hwcdrasapqewe"
)
]

(ApiVerificationKeyHash (ApiT (Hash h))) <-
getWalletKey ctx (apiWal ^. id) (ApiT $ DerivationIndex index)
T.decodeUtf8 (hex h) `shouldBe` keyHash
forM_ matrix
$ \(role_, index, expected) -> it (show role_ <> "/" <> show index)
$ \ctx -> do
let payload = Json [json|{
"name": "Wallet",
"mnemonic_sentence": #{explicitMnemonics},
"passphrase": #{fixturePassphrase}
}|]

r <- request @ApiWallet ctx (Link.postWallet @'Shelley) Default payload
verify r [ expectResponseCode @IO HTTP.status201 ]
let apiWal = getFromResponse id r

let link = Link.getWalletKey (apiWal ^. id) role_ index
rGet <- request @ApiVerificationKey ctx link Default Empty
verify rGet
[ expectResponseCode @IO HTTP.status200
, expectField id (\k -> toJSON k `shouldBe` toJSON expected)
]

it "WALLETS_GET_KEY_02 - invalid index for verification key" $ \ctx -> do
w <- emptyWallet ctx

let link = Link.getWalletKey w UtxoExternal (DerivationIndex 2147483648)
r <- request @ApiVerificationKey ctx link Default Empty

verify r
[ expectResponseCode @IO HTTP.status400
, expectErrorMessage
"It looks like you've provided a derivation index that is out of bound."
]

it "WALLETS_GET_KEY_03 - unknown wallet" $ \ctx -> do
w <- emptyWallet ctx
_ <- request @ApiWallet ctx (Link.deleteWallet @'Shelley w) Default Empty

let link = Link.getWalletKey w UtxoExternal (DerivationIndex 0)
r <- request @ApiVerificationKey ctx link Default Empty

verify r
[ expectResponseCode @IO HTTP.status404
, expectErrorMessage (errMsg404NoWallet $ w ^. walletId)
]

it "BYRON_WALLETS_UTXO -\
\ Cannot show Byron wal utxo with shelley ep (404)" $ \ctx -> do
Expand Down
61 changes: 56 additions & 5 deletions lib/core/src/Cardano/Wallet.hs
Expand Up @@ -167,10 +167,13 @@ module Cardano.Wallet

-- ** Root Key
, withRootKey
, derivePublicKey
, signMetadataWith
, ErrWithRootKey (..)
, ErrWrongPassphrase (..)
, ErrSignMetadataWith (..)
, ErrDerivePublicKey(..)
, ErrInvalidDerivationIndex(..)

-- * Logging
, WalletLog (..)
Expand All @@ -183,7 +186,7 @@ import Prelude hiding
( log )

import Cardano.Address.Derivation
( XPrv )
( XPrv, XPub )
import Cardano.Api.Typed
( serialiseToCBOR )
import Cardano.BM.Data.Severity
Expand Down Expand Up @@ -227,6 +230,7 @@ import Cardano.Wallet.Primitive.AddressDerivation
, NetworkDiscriminant (..)
, Passphrase
, PaymentAddress (..)
, SoftDerivation (..)
, ToChimericAccount (..)
, WalletKey (..)
, checkPassphrase
Expand Down Expand Up @@ -2251,9 +2255,7 @@ signMetadataWith
-> TxMetadata
-> ExceptT ErrSignMetadataWith IO (Signature TxMetadata)
signMetadataWith ctx wid pwd (role_, ix) metadata = db & \DBLayer{..} -> do
addrIx <- if ix > DerivationIndex (getIndex @'Soft maxBound)
then throwE $ ErrSignMetadataWithIndexTooHigh maxBound ix
else pure (Index $ getDerivationIndex ix)
addrIx <- withExceptT ErrSignMetadataWithInvalidIndex $ guardSoftIndex ix

cp <- mapExceptT atomically
$ withExceptT ErrSignMetadataWithNoSuchWallet
Expand All @@ -2271,6 +2273,44 @@ signMetadataWith ctx wid pwd (role_, ix) metadata = db & \DBLayer{..} -> do
where
db = ctx ^. dbLayer @s @k

-- | Derive public key of a wallet's account.
derivePublicKey
:: forall ctx s k n.
( HasDBLayer s k ctx
, SoftDerivation k
, s ~ SeqState n k
)
=> ctx
-> WalletId
-> AccountingStyle
-> DerivationIndex
-> ExceptT ErrDerivePublicKey IO (k 'AddressK XPub)
derivePublicKey ctx wid role_ ix = db & \DBLayer{..} -> do
addrIx <- withExceptT ErrDerivePublicKeyInvalidIndex $ guardSoftIndex ix

cp <- mapExceptT atomically
$ withExceptT ErrDerivePublicKeyNoSuchWallet
$ withNoSuchWallet wid
$ readCheckpoint (PrimaryKey wid)

-- NOTE: Alternatively, we could use 'internalPool', they share the same
-- account public key.
let acctK = Seq.accountPubKey $ Seq.externalPool $ getState cp
let addrK = deriveAddressPublicKey acctK role_ addrIx

return addrK
where
db = ctx ^. dbLayer @s @k

guardSoftIndex
:: Monad m
=> DerivationIndex
-> ExceptT ErrInvalidDerivationIndex m (Index 'Soft whatever)
guardSoftIndex ix =
if ix > DerivationIndex (getIndex @'Soft maxBound)
then throwE $ ErrIndexTooHigh maxBound ix
else pure (Index $ getDerivationIndex ix)

{-------------------------------------------------------------------------------
Errors
-------------------------------------------------------------------------------}
Expand All @@ -2280,10 +2320,21 @@ data ErrSignMetadataWith
-- ^ The wallet exists, but there's no root key attached to it
| ErrSignMetadataWithNoSuchWallet ErrNoSuchWallet
-- ^ The wallet doesn't exist?
| ErrSignMetadataWithIndexTooHigh (Index 'Soft 'AddressK) DerivationIndex
| ErrSignMetadataWithInvalidIndex ErrInvalidDerivationIndex
-- ^ User provided a derivation index outside of the 'Soft' domain
deriving (Eq, Show)

data ErrDerivePublicKey
= ErrDerivePublicKeyNoSuchWallet ErrNoSuchWallet
-- ^ The wallet doesn't exist?
| ErrDerivePublicKeyInvalidIndex ErrInvalidDerivationIndex
-- ^ User provided a derivation index outside of the 'Soft' domain
deriving (Eq, Show)

data ErrInvalidDerivationIndex
= ErrIndexTooHigh (Index 'Soft 'AddressK) DerivationIndex
deriving (Eq, Show)

data ErrUTxOTooSmall
= ErrUTxOTooSmall Word64 [Word64]
-- ^ UTxO(s) participating in transaction are too small to make transaction
Expand Down
7 changes: 3 additions & 4 deletions lib/core/src/Cardano/Wallet/Api.hs
Expand Up @@ -132,7 +132,7 @@ import Cardano.Wallet.Api.Types
, ApiTransactionT
, ApiTxId
, ApiUtxoStatistics
, ApiVerificationKeyHash
, ApiVerificationKey
, ApiWallet
, ApiWalletMigrationInfo
, ApiWalletMigrationPostDataT
Expand Down Expand Up @@ -162,7 +162,6 @@ import Cardano.Wallet.Primitive.Types
( AddressState
, Block
, Coin (..)
, DerivationIndex
, NetworkParameters
, SortOrder (..)
, WalletId (..)
Expand Down Expand Up @@ -305,9 +304,9 @@ type WalletKeys =
type GetWalletKey = "wallets"
:> Capture "walletId" (ApiT WalletId)
:> "keys"
:> "script"
:> Capture "role" (ApiT AccountingStyle)
:> Capture "index" (ApiT DerivationIndex)
:> Get '[JSON] ApiVerificationKeyHash
:> Get '[JSON] ApiVerificationKey

{-------------------------------------------------------------------------------
Addresses
Expand Down
18 changes: 6 additions & 12 deletions lib/core/src/Cardano/Wallet/Api/Link.hs
Expand Up @@ -105,16 +105,9 @@ import Cardano.Wallet.Api.Types
, WalletStyle (..)
)
import Cardano.Wallet.Primitive.AddressDerivation
( NetworkDiscriminant (..) )
( AccountingStyle, DerivationIndex, NetworkDiscriminant (..) )
import Cardano.Wallet.Primitive.Types
( AddressState
, Coin (..)
, DerivationIndex
, Hash
, PoolId
, SortOrder
, WalletId (..)
)
( AddressState, Coin (..), Hash, PoolId, SortOrder, WalletId (..) )
import Data.Function
( (&) )
import Data.Generics.Internal.VL.Lens
Expand Down Expand Up @@ -274,10 +267,11 @@ getWalletKey
( HasType (ApiT WalletId) w
)
=> w
-> ApiT DerivationIndex
-> AccountingStyle
-> DerivationIndex
-> (Method, Text)
getWalletKey w index =
endpoint @Api.GetWalletKey (\mk -> mk wid index)
getWalletKey w role_ index =
endpoint @Api.GetWalletKey (\mk -> mk wid (ApiT role_) (ApiT index))
where
wid = w ^. typed @(ApiT WalletId)

Expand Down

0 comments on commit cf172b5

Please sign in to comment.