diff --git a/core/src/Pos/Core/Util/LogSafe.hs b/core/src/Pos/Core/Util/LogSafe.hs index 88c350994c4..cde6f1a2ce3 100644 --- a/core/src/Pos/Core/Util/LogSafe.hs +++ b/core/src/Pos/Core/Util/LogSafe.hs @@ -334,8 +334,11 @@ logErrorSP = logMessageSP Error instance BuildableSafe a => Buildable (SecureLog [a]) where build = bprint (buildSafeList secure) . getSecureLog +instance Buildable (SecureLog Bool) where + build _ = "" + instance Buildable (SecureLog Text) where - build _ = "" + build _ = "" instance Buildable (SecureLog PassPhrase) where build _ = "" diff --git a/core/test/Test/Pos/Core/ExampleHelpers.hs b/core/test/Test/Pos/Core/ExampleHelpers.hs index 945595a3848..76aa33a6900 100644 --- a/core/test/Test/Pos/Core/ExampleHelpers.hs +++ b/core/test/Test/Pos/Core/ExampleHelpers.hs @@ -81,7 +81,6 @@ module Test.Pos.Core.ExampleHelpers import Universum import qualified Crypto.SCRAPE as Scrape -import qualified Crypto.Sign.Ed25519 as Ed25519 import Data.Coerce (coerce) import Data.Fixed (Fixed (..)) import qualified Data.HashMap.Strict as HM @@ -146,7 +145,7 @@ import Pos.Crypto (AbstractHash (..), EncShare (..), redeemDeterministicKeyGen, redeemSign, safeCreatePsk, sign, toVssPublicKey) import Pos.Crypto.Signing (ProxyCert (..), ProxySecretKey (..), - PublicKey (..), RedeemPublicKey (..)) + PublicKey (..)) import Test.Pos.Core.Gen (genProtocolConstants) import Test.Pos.Crypto.Bi (getBytes) @@ -630,17 +629,19 @@ exampleGenesisConfiguration_GCSpec = exampleGenesisAvvmBalances :: GenesisAvvmBalances exampleGenesisAvvmBalances = - GenesisAvvmBalances {getGenesisAvvmBalances = - (HM.fromList [(RedeemPublicKey (Ed25519.PublicKey fstRedKey) - , Coin {getCoin = 36524597913081152}) - ,(RedeemPublicKey (Ed25519.PublicKey sndRedKey) - ,Coin {getCoin = 37343863242999412}) - ]) } - where - fstRedKey = hexToBS "e2a1773a2a82d10c30890cbf84eccbdc1aaaee9204\ - \96424d36e868039d9cb519" - sndRedKey = hexToBS "9cdabcec332abbc6fdf883ca5bf3a8afddca69bfea\ - \c14c013304da88ac032fe6" + GenesisAvvmBalances + { getGenesisAvvmBalances = HM.fromList + [ ( exampleRedeemPublicKey' (0, 32) + , Coin {getCoin = 36524597913081152} + ) + , ( exampleRedeemPublicKey' (32, 32) + , Coin {getCoin = 37343863242999412} + ) + ] + } + where + exampleRedeemPublicKey' :: (Int, Int) -> RedeemPublicKey + exampleRedeemPublicKey' (m, n) = fromJust (fst <$> redeemDeterministicKeyGen (getBytes m n)) exampleSharedSeed :: SharedSeed exampleSharedSeed = SharedSeed (getBytes 8 32) diff --git a/core/test/golden/GenesisConfiguration_GCSpec b/core/test/golden/GenesisConfiguration_GCSpec index fe0df1afbfc..7cbe4b9fed8 100644 --- a/core/test/golden/GenesisConfiguration_GCSpec +++ b/core/test/golden/GenesisConfiguration_GCSpec @@ -1 +1 @@ -{"spec":{"avvmDistr":{"nNq87DMqu8b9-IPKW_Oor93Kab_qwUwBMwTaiKwDL-Y=":37343863242999412,"4qF3OiqC0QwwiQy_hOzL3Bqq7pIElkJNNuhoA52ctRk=":36524597913081152},"ftsSeed":"RTVTNGZTSDZldE5vdWlYZXpDeUVqS2MzdEc0amEwa0Y=","heavyDelegation":{"ed41bf35f4cd2109bdee27b0439942f6de3f55f0ac8d170de7136d3d":{"pskDelegatePk":"3cppv+rBTAEzBNqIrAMu5jKBqwNsGxuRiOSxdLMD9D5VFjsXjpmbn9UGN7Ltq4yFioeaw8S9PmEAlUGaGWllcw==","pskOmega":68300481033,"pskCert":"bae5422af5405e3803154a4ad986da5d14cf624d6701c5c78a79ec73777f74e13973af83752114d9f18166085997fc81e432cab7fee99a275d8bf138ad04e103","pskIssuerPk":"4qF3OiqC0QwwiQy/hOzL3Bqq7pIElkJNNuhoA52ctRkhsl7+Az2bANTwLM2c2rzsMyq7xv34g8pb86iv9KrCfg=="}},"blockVersionData":{"scriptVersion":999,"slotDuration":999,"maxBlockSize":999,"maxHeaderSize":999,"maxTxSize":999,"maxProposalSize":999,"mpcThd":9.9e-14,"heavyDelThd":9.9e-14,"updateVoteThd":9.9e-14,"updateProposalThd":9.9e-14,"updateImplicit":99,"softforkRule":{"initThd":9.9e-14,"minThd":9.9e-14,"thdDecrement":9.9e-14},"txFeePolicy":{"txSizeLinear":{"a":9.99e-7,"b":7.7e-8}},"unlockStakeEpoch":99},"protocolConstants":{"k":37,"protocolMagic":1783847074,"vssMaxTTL":1477558317,"vssMinTTL":744040476},"initializer":{"testBalance":{"poors":2448641325904532856,"richmen":14071205313513960321,"totalBalance":10953275486128625216,"richmenShare":4.2098713311249885,"useHDAddresses":true},"fakeAvvmBalance":{"count":17853231730478779264,"oneBalance":15087947214890024355},"avvmBalanceFactor":0.366832547637728,"useHeavyDlg":false,"seed":0}}} \ No newline at end of file +{"spec":{"avvmDistr":{"dtXbK7ASFLEa611AHt7SDk_48fVbNja-Tj7PgbE7tPE=":37343863242999412,"U4lgqRZyawnwXJ1NSpIrhbThGs_MFDRnPZUBm3qaUtI=":36524597913081152},"ftsSeed":"RTVTNGZTSDZldE5vdWlYZXpDeUVqS2MzdEc0amEwa0Y=","heavyDelegation":{"ed41bf35f4cd2109bdee27b0439942f6de3f55f0ac8d170de7136d3d":{"pskDelegatePk":"3cppv+rBTAEzBNqIrAMu5jKBqwNsGxuRiOSxdLMD9D5VFjsXjpmbn9UGN7Ltq4yFioeaw8S9PmEAlUGaGWllcw==","pskOmega":68300481033,"pskCert":"bae5422af5405e3803154a4ad986da5d14cf624d6701c5c78a79ec73777f74e13973af83752114d9f18166085997fc81e432cab7fee99a275d8bf138ad04e103","pskIssuerPk":"4qF3OiqC0QwwiQy/hOzL3Bqq7pIElkJNNuhoA52ctRkhsl7+Az2bANTwLM2c2rzsMyq7xv34g8pb86iv9KrCfg=="}},"blockVersionData":{"scriptVersion":999,"slotDuration":999,"maxBlockSize":999,"maxHeaderSize":999,"maxTxSize":999,"maxProposalSize":999,"mpcThd":9.9e-14,"heavyDelThd":9.9e-14,"updateVoteThd":9.9e-14,"updateProposalThd":9.9e-14,"updateImplicit":99,"softforkRule":{"initThd":9.9e-14,"minThd":9.9e-14,"thdDecrement":9.9e-14},"txFeePolicy":{"txSizeLinear":{"a":9.99e-7,"b":7.7e-8}},"unlockStakeEpoch":99},"protocolConstants":{"k":37,"protocolMagic":1783847074,"vssMaxTTL":1477558317,"vssMinTTL":744040476},"initializer":{"testBalance":{"poors":2448641325904532856,"richmen":14071205313513960321,"totalBalance":10953275486128625216,"richmenShare":4.2098713311249885,"useHDAddresses":true},"fakeAvvmBalance":{"count":17853231730478779264,"oneBalance":15087947214890024355},"avvmBalanceFactor":0.366832547637728,"useHeavyDlg":false,"seed":0}}} \ No newline at end of file diff --git a/crypto/Pos/Crypto/Orphans.hs b/crypto/Pos/Crypto/Orphans.hs index e1595ef9615..4ab5927a2a1 100644 --- a/crypto/Pos/Crypto/Orphans.hs +++ b/crypto/Pos/Crypto/Orphans.hs @@ -8,55 +8,112 @@ import Universum import qualified Cardano.Crypto.Wallet as CC import qualified Cardano.Crypto.Wallet.Encrypted as CC +import qualified Codec.CBOR.Encoding as E +import Crypto.Error (CryptoFailable (..)) +import qualified Crypto.PubKey.Ed25519 as Ed25519 import qualified Crypto.SCRAPE as Scrape import Crypto.Scrypt (EncryptedPass (..)) -import qualified Crypto.Sign.Ed25519 as Ed25519 import Data.Aeson (FromJSON (..), ToJSON (..)) +import qualified Data.ByteArray as BA +import qualified Data.ByteString as BS import Data.Hashable (Hashable) -import Data.SafeCopy (base, deriveSafeCopySimple) +import Data.SafeCopy (SafeCopy (..), base, contain, + deriveSafeCopySimple, safeGet, safePut) +import qualified Data.Text as T import Serokell.Util.Base64 (JsonByteString (..)) import Pos.Binary.Class (Bi (..), Size, decodeBinary, encodeBinary, withWordSize) -instance Hashable Ed25519.PublicKey -instance Hashable Ed25519.SecretKey -instance Hashable Ed25519.Signature -instance NFData Ed25519.PublicKey -instance NFData Ed25519.SecretKey -instance NFData Ed25519.Signature +fromByteStringToBytes :: BS.ByteString -> BA.Bytes +fromByteStringToBytes = BA.convert + +fromByteStringToScrubbedBytes :: BS.ByteString -> BA.ScrubbedBytes +fromByteStringToScrubbedBytes = BA.convert + +toByteString :: (BA.ByteArrayAccess bin) => bin -> BS.ByteString +toByteString = BA.convert + + +instance Hashable Ed25519.PublicKey where + hashWithSalt salt pk = hashWithSalt salt $ toByteString pk + +instance Hashable Ed25519.SecretKey where + hashWithSalt salt pk = hashWithSalt salt $ toByteString pk + +instance Hashable Ed25519.Signature where + hashWithSalt salt pk = hashWithSalt salt $ toByteString pk + + +instance Ord Ed25519.PublicKey where + compare x1 x2 = compare (toByteString x1) (toByteString x2) + +instance Ord Ed25519.SecretKey where + compare x1 x2 = compare (toByteString x1) (toByteString x2) + +instance Ord Ed25519.Signature where + compare x1 x2 = compare (toByteString x1) (toByteString x2) + + + +instance SafeCopy BA.Bytes where + putCopy s = contain $ safePut (toByteString s) + getCopy = contain $ fromByteStringToBytes <$> safeGet + +instance SafeCopy BA.ScrubbedBytes where + putCopy s = contain $ safePut (toByteString s) + getCopy = contain $ fromByteStringToScrubbedBytes <$> safeGet + deriveSafeCopySimple 0 'base ''Ed25519.PublicKey deriveSafeCopySimple 0 'base ''Ed25519.SecretKey deriveSafeCopySimple 0 'base ''Ed25519.Signature + +fromCryptoFailable :: MonadFail m => T.Text -> CryptoFailable a -> m a +fromCryptoFailable item (CryptoFailed e) = fail $ T.unpack $ "Pos.Crypto.Orphan." <> item <> " failed because " <> show e +fromCryptoFailable _ (CryptoPassed r) = return r + + instance FromJSON Ed25519.PublicKey where - parseJSON v = Ed25519.PublicKey . getJsonByteString <$> parseJSON v + parseJSON v = do + res <- Ed25519.publicKey . fromByteStringToBytes . getJsonByteString <$> parseJSON v + fromCryptoFailable "parseJSON Ed25519.PublicKey" res instance ToJSON Ed25519.PublicKey where - toJSON = toJSON . JsonByteString . Ed25519.openPublicKey + toJSON = toJSON . JsonByteString . toByteString instance FromJSON Ed25519.Signature where - parseJSON v = Ed25519.Signature . getJsonByteString <$> parseJSON v + parseJSON v = do + res <- Ed25519.signature . fromByteStringToBytes . getJsonByteString <$> parseJSON v + fromCryptoFailable "parseJSON Ed25519.Signature" res instance ToJSON Ed25519.Signature where - toJSON = toJSON . JsonByteString . Ed25519.unSignature + toJSON = toJSON . JsonByteString . toByteString + + instance Bi Ed25519.PublicKey where - encode (Ed25519.PublicKey k) = encode k - decode = Ed25519.PublicKey <$> decode encodedSizeExpr _ _ = bsSize 32 + encode = E.encodeBytes . toByteString + decode = do + res <- Ed25519.publicKey . fromByteStringToBytes <$> decode + fromCryptoFailable "decode Ed25519.PublicKey" res instance Bi Ed25519.SecretKey where - encode (Ed25519.SecretKey k) = encode k - decode = Ed25519.SecretKey <$> decode encodedSizeExpr _ _ = bsSize 64 + encode sk = E.encodeBytes $ BS.append (toByteString sk) (toByteString $ Ed25519.toPublic sk) + decode = do + res <- Ed25519.secretKey . fromByteStringToScrubbedBytes . BS.take Ed25519.secretKeySize <$> decode + fromCryptoFailable "decode Ed25519.SecretKey" res instance Bi Ed25519.Signature where - encode (Ed25519.Signature s) = encode s - decode = Ed25519.Signature <$> decode encodedSizeExpr _ _ = bsSize 64 + encode = E.encodeBytes . toByteString + decode = do + res <- Ed25519.signature . fromByteStringToBytes <$> decode + fromCryptoFailable "decode Ed25519.Signature" res -- Helper for encodedSizeExpr in Bi instances bsSize :: Int -> Size @@ -113,5 +170,3 @@ deriveSafeCopySimple 0 'base ''CC.EncryptedKey deriveSafeCopySimple 0 'base ''CC.XSignature deriveSafeCopySimple 0 'base ''CC.XPub deriveSafeCopySimple 0 'base ''CC.XPrv - - diff --git a/crypto/Pos/Crypto/SafeCopy.hs b/crypto/Pos/Crypto/SafeCopy.hs index ef27b9d33c8..9d6e9ad2053 100644 --- a/crypto/Pos/Crypto/SafeCopy.hs +++ b/crypto/Pos/Crypto/SafeCopy.hs @@ -13,11 +13,8 @@ import Universum import qualified Cardano.Crypto.Wallet as CC import qualified Cardano.Crypto.Wallet.Encrypted as CC import Crypto.Hash (HashAlgorithm) -import qualified Crypto.Sign.Ed25519 as EDS25519 - import Data.SafeCopy (SafeCopy (..), base, contain, deriveSafeCopySimple, safeGet, safePut) - import Pos.Binary.Class (AsBinary (..), Bi) import qualified Pos.Binary.Class as Bi import Pos.Binary.SafeCopy (getCopyBi, putCopyBi) @@ -38,10 +35,6 @@ deriveSafeCopySimple 0 'base ''CC.XSignature deriveSafeCopySimple 0 'base ''CC.XPub deriveSafeCopySimple 0 'base ''CC.XPrv -deriveSafeCopySimple 0 'base ''EDS25519.PublicKey -deriveSafeCopySimple 0 'base ''EDS25519.SecretKey -deriveSafeCopySimple 0 'base ''EDS25519.Signature - deriveSafeCopySimple 0 'base ''PublicKey deriveSafeCopySimple 0 'base ''SecretKey diff --git a/crypto/Pos/Crypto/Signing/Redeem.hs b/crypto/Pos/Crypto/Signing/Redeem.hs index 3e95dbd9eb4..dd408b4a923 100644 --- a/crypto/Pos/Crypto/Signing/Redeem.hs +++ b/crypto/Pos/Crypto/Signing/Redeem.hs @@ -8,11 +8,12 @@ module Pos.Crypto.Signing.Redeem import Universum -import Crypto.Random (MonadRandom, getRandomBytes) +import Crypto.Error (maybeCryptoError) +import qualified Crypto.PubKey.Ed25519 as Ed25519 +import Crypto.Random (MonadRandom) +import qualified Data.ByteArray as BA import qualified Data.ByteString as BS import Data.Coerce (coerce) - -import qualified Crypto.Sign.Ed25519 as Ed25519 import Pos.Binary.Class (Bi, Raw) import qualified Pos.Binary.Class as Bi import Pos.Crypto.Configuration (ProtocolMagic) @@ -27,18 +28,21 @@ import Pos.Crypto.Signing.Types.Redeem -- from "Pos.Crypto.Random" because the OpenSSL generator is probably safer -- than the default IO generator. redeemKeyGen :: MonadRandom m => m (RedeemPublicKey, RedeemSecretKey) -redeemKeyGen = - getRandomBytes 32 >>= - maybe err pure . redeemDeterministicKeyGen - where - err = error "Pos.Crypto.RedeemSigning.redeemKeyGen: createKeypairFromSeed_ failed" +redeemKeyGen = do + sk <- Ed25519.generateSecretKey + return (RedeemPublicKey $ Ed25519.toPublic sk, RedeemSecretKey sk) + +fromByteStringToBytes :: BS.ByteString -> BA.Bytes +fromByteStringToBytes = BA.convert -- | Create key pair deterministically from 32 bytes. redeemDeterministicKeyGen :: BS.ByteString -> Maybe (RedeemPublicKey, RedeemSecretKey) redeemDeterministicKeyGen seed = - bimap RedeemPublicKey RedeemSecretKey <$> Ed25519.createKeypairFromSeed_ seed + case maybeCryptoError $ Ed25519.secretKey $ fromByteStringToBytes seed of + Just r -> Just (RedeemPublicKey $ Ed25519.toPublic r, RedeemSecretKey r) + Nothing -> fail "Pos.Crypto.Signing.Redeem.hs redeemDeterministicKeyGen failed" ---------------------------------------------------------------------------- -- Redeem signatures @@ -62,7 +66,7 @@ redeemSignRaw -> ByteString -> RedeemSignature Raw redeemSignRaw pm mbTag (RedeemSecretKey k) x = - RedeemSignature (Ed25519.dsign k (tag <> x)) + RedeemSignature (Ed25519.sign k (Ed25519.toPublic k) (fromByteStringToBytes $ tag <> x) ) where tag = maybe mempty (signTag pm) mbTag @@ -84,6 +88,6 @@ redeemVerifyRaw -> RedeemSignature Raw -> Bool redeemVerifyRaw pm mbTag (RedeemPublicKey k) x (RedeemSignature s) = - Ed25519.dverify k (tag <> x) s + Ed25519.verify k (fromByteStringToBytes $ tag <> x) s where tag = maybe mempty (signTag pm) mbTag diff --git a/crypto/Pos/Crypto/Signing/Types/Redeem.hs b/crypto/Pos/Crypto/Signing/Types/Redeem.hs index 84cb54eeecb..4e9b939965e 100644 --- a/crypto/Pos/Crypto/Signing/Types/Redeem.hs +++ b/crypto/Pos/Crypto/Signing/Types/Redeem.hs @@ -14,11 +14,13 @@ import Universum import Control.Exception.Safe (Exception (..)) import Control.Lens (_Left) -import qualified Crypto.Sign.Ed25519 as Ed25519 +import Crypto.Error (CryptoFailable (..)) +import qualified Crypto.PubKey.Ed25519 as Ed25519 import Data.Aeson (FromJSONKey (..), FromJSONKeyFunction (..), ToJSONKey (..), ToJSONKeyFunction (..)) import Data.Aeson.Encoding (text) import Data.Aeson.TH (defaultOptions, deriveJSON) +import qualified Data.ByteArray as BA import qualified Data.ByteString as BS import Data.Hashable (Hashable) import Data.SafeCopy (SafeCopy (..), base, contain, @@ -70,22 +72,25 @@ newtype RedeemSecretKey = RedeemSecretKey Ed25519.SecretKey deriving instance Bi RedeemSecretKey +fromPublicKeyToByteString :: Ed25519.PublicKey -> BS.ByteString +fromPublicKeyToByteString = BA.convert + redeemPkB64F :: Format r (RedeemPublicKey -> r) redeemPkB64F = - later $ \(RedeemPublicKey pk) -> formatBase64 $ Ed25519.openPublicKey pk + later $ \(RedeemPublicKey pk) -> formatBase64 $ fromPublicKeyToByteString pk -- | Base64url Format for 'RedeemPublicKey'. redeemPkB64UrlF :: Format r (RedeemPublicKey -> r) redeemPkB64UrlF = later $ \(RedeemPublicKey pk) -> - B.build $ B64.encodeUrl $ Ed25519.openPublicKey pk + B.build $ B64.encodeUrl $ fromPublicKeyToByteString pk redeemPkB64ShortF :: Format r (RedeemPublicKey -> r) redeemPkB64ShortF = fitLeft 8 %. redeemPkB64F -- | Public key derivation function. redeemToPublic :: RedeemSecretKey -> RedeemPublicKey -redeemToPublic (RedeemSecretKey k) = RedeemPublicKey (Ed25519.secretToPublicKey k) +redeemToPublic (RedeemSecretKey k) = RedeemPublicKey (Ed25519.toPublic k) instance B.Buildable RedeemPublicKey where build = bprint ("redeem_pk:"%redeemPkB64F) @@ -148,10 +153,12 @@ fromAvvmPk addrText = do redeemPkBuild :: ByteString -> RedeemPublicKey redeemPkBuild bs | BS.length bs /= 32 = - error $ - "consRedeemPk: failed to form pk, wrong bs length: " <> show (BS.length bs) <> - ", when should be 32" - | otherwise = RedeemPublicKey $ Ed25519.PublicKey $ bs + error $ + "consRedeemPk: failed to form pk, wrong bs length: " <> show (BS.length bs) <> + ", when should be 32" + | otherwise = case Ed25519.publicKey $ (BA.convert bs :: BA.Bytes) of + CryptoPassed r -> RedeemPublicKey r + CryptoFailed e -> error $ mappend "Pos.Crypto.Signing.Types.Redeem.hs consRedeemPk failed because " (T.pack $ show e) ---------------------------------------------------------------------------- -- Helpers diff --git a/crypto/cardano-sl-crypto.cabal b/crypto/cardano-sl-crypto.cabal index d7f1d2e6359..d71becedea5 100644 --- a/crypto/cardano-sl-crypto.cabal +++ b/crypto/cardano-sl-crypto.cabal @@ -53,7 +53,6 @@ library , cryptonite , cryptonite-openssl , data-default - , ed25519 , formatting , hashable , lens diff --git a/docs/tls-authentication.md b/docs/tls-authentication.md index eae3d6f94c9..8d798c3c6d2 100644 --- a/docs/tls-authentication.md +++ b/docs/tls-authentication.md @@ -108,11 +108,19 @@ $ cardano-node \ All those options are actually optional. When missing, the node looks for default development certificates and key in `/scripts/tls-files/`. -#### Disable TLS (Not Recommended) +### Disabling TLS (Not Recommended) + +#### Fully Turn Off TLS If needed, you can disable TLS by providing the `--no-tls` flag to the wallet or by running a wallet in debug mode with `--wallet-debug` turned on. +#### Turn Off Client Certificates Verification + +It is possible to lower the TLS requirements down a bit by disabling only client certificates +verification. The communication will still be done in a TLS tunnel though, the server won't +require nor verify any client certificate presented to it. To do so, simply provide the +`--no-client-auth` flag upon starting a wallet node. ### Contacting Cardano-SL Backend diff --git a/explorer/scripts/fetch_dependencies.sh b/explorer/scripts/fetch_dependencies.sh index cf2d62a3e64..4c72a93aeb5 100755 --- a/explorer/scripts/fetch_dependencies.sh +++ b/explorer/scripts/fetch_dependencies.sh @@ -8,7 +8,6 @@ https://github.com/serokell/log-warper.git https://github.com/serokell/kademlia.git https://github.com/serokell/rocksdb-haskell.git https://github.com/serokell/time-warp-nt.git -https://github.com/thoughtpolice/hs-ed25519.git https://github.com/serokell/network-transport.git https://github.com/serokell/network-transport-tcp.git https://github.com/input-output-hk/cardano-crypto.git diff --git a/explorer/scripts/generate_cabal2nix.sh b/explorer/scripts/generate_cabal2nix.sh index a92d16d195c..ac3a63ed20f 100755 --- a/explorer/scripts/generate_cabal2nix.sh +++ b/explorer/scripts/generate_cabal2nix.sh @@ -8,7 +8,6 @@ https://github.com/serokell/log-warper.git https://github.com/serokell/kademlia.git https://github.com/serokell/rocksdb-haskell.git https://github.com/serokell/time-warp-nt.git -https://github.com/thoughtpolice/hs-ed25519.git https://github.com/serokell/network-transport.git https://github.com/serokell/network-transport-tcp.git https://github.com/input-output-hk/cardano-crypto.git diff --git a/lib/cardano-sl.cabal b/lib/cardano-sl.cabal index 819d45abb99..6a1b4eaebd9 100644 --- a/lib/cardano-sl.cabal +++ b/lib/cardano-sl.cabal @@ -174,6 +174,7 @@ library , http-client , http-client-tls , http-conduit + , http-types , lens , log-warper >= 1.1.1 , memory diff --git a/lib/src/Pos/Util/Servant.hs b/lib/src/Pos/Util/Servant.hs index 6ed44dd9d28..1d8e11d1d2c 100644 --- a/lib/src/Pos/Util/Servant.hs +++ b/lib/src/Pos/Util/Servant.hs @@ -2,6 +2,8 @@ {-# LANGUAGE CPP #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE ExistentialQuantification #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE InstanceSigs #-} {-# LANGUAGE KindSignatures #-} {-# LANGUAGE Rank2Types #-} {-# LANGUAGE RecordWildCards #-} @@ -54,6 +56,9 @@ module Pos.Util.Servant , DCQueryParam , DQueryParam + , Flaggable (..) + , CustomQueryFlag + , serverHandlerL , serverHandlerL' , inRouteServer @@ -69,6 +74,7 @@ import Control.Monad.Except (ExceptT (..), MonadError (..)) import Data.Constraint ((\\)) import Data.Constraint.Forall (Forall, inst) import Data.Default (Default (..)) +import Data.List (lookup) import Data.Reflection (Reifies (..), reflect) import qualified Data.Text as T import Data.Time.Clock.POSIX (getPOSIXTime) @@ -76,11 +82,14 @@ import Formatting (bprint, build, builder, fconst, formatToString, sformat, shown, stext, string, (%)) import qualified Formatting.Buildable import GHC.IO.Unsafe (unsafePerformIO) -import GHC.TypeLits (KnownSymbol, symbolVal) +import GHC.TypeLits (KnownSymbol, Symbol, symbolVal) +import Network.HTTP.Types (parseQueryText) +import Network.Wai (rawQueryString) import Serokell.Util (listJsonIndent) import Serokell.Util.ANSI (Color (..), colorizeDull) import Servant.API ((:<|>) (..), (:>), Capture, Description, - QueryParam, ReflectMethod (..), ReqBody, Summary, Verb) + QueryFlag, QueryParam, ReflectMethod (..), ReqBody, + Summary, Verb) import Servant.Client (Client, HasClient (..)) import Servant.Client.Core (RunClient) import Servant.Server (Handler (..), HasServer (..), ServantErr (..), @@ -89,8 +98,8 @@ import qualified Servant.Server.Internal as SI import Servant.Swagger (HasSwagger (toSwagger)) import System.Wlog (LoggerName, LoggerNameBox, usingLoggerName) -import Pos.Infra.Util.LogSafe (BuildableSafe, SecureLog, SecuredText, - buildSafe, logInfoSP, plainOrSecureF, secretOnlyF) +import Pos.Infra.Util.LogSafe (BuildableSafe, SecuredText, buildSafe, + logInfoSP, plainOrSecureF, secretOnlyF) ------------------------------------------------------------------------- -- Utility functions @@ -148,6 +157,13 @@ type ApiHasArg subApi res = ) instance KnownSymbol s => ApiHasArgClass (Capture s a) + +instance KnownSymbol s => ApiHasArgClass (QueryFlag s) where + type ApiArg (QueryFlag s) = Bool + + apiArgName :: Proxy (QueryFlag s) -> String + apiArgName _ = formatToString ("'"%string%"' field") $ symbolVal (Proxy @s) + instance KnownSymbol s => ApiHasArgClass (QueryParam s a) where type ApiArg (QueryParam s a) = Maybe a instance ApiHasArgClass (ReqBody ct a) where @@ -472,14 +488,14 @@ class ApiHasArgClass subApi => :: BuildableSafe (ApiArgToLog subApi) => Proxy subApi -> ApiArg subApi -> SecuredText default toLogParamInfo - :: ( Buildable (ApiArg subApi) - , Buildable (SecureLog (ApiArg subApi)) - ) + :: BuildableSafe (ApiArg subApi) => Proxy subApi -> ApiArg subApi -> SecuredText toLogParamInfo _ param = \sl -> sformat (buildSafe sl) param instance KnownSymbol s => ApiCanLogArg (Capture s a) +instance KnownSymbol s => ApiCanLogArg (QueryFlag s) + instance ApiCanLogArg (ReqBody ct a) instance KnownSymbol cs => ApiCanLogArg (QueryParam cs a) where @@ -529,9 +545,8 @@ instance {-# OVERLAPPABLE #-} , ApiHasArg subApi (LoggingApiRec config res) , ApiCanLogArg subApi , BuildableSafe (ApiArgToLog subApi) - , subApi ~ apiType a ) => - HasLoggingServer config (apiType a :> res) ctx where + HasLoggingServer config (subApi :> res) ctx where routeWithLog = paramRouteWithLog instance {-# OVERLAPPING #-} @@ -700,6 +715,54 @@ instance ReportDecodeError api => reportDecodeError _ msg = (ApiNoParamsLogInfo msg, reportDecodeError (Proxy @api) msg) + +------------------------------------------------------------------------- +-- Custom query flag +------------------------------------------------------------------------- + +-- This type is used as a helper to implement custom query flags. +-- Instead of using `QueryFlag "some_flag"` which should serialize +-- into boolean flag now we can say `CustomQueryFlag "some_flag" SomeFlag` +-- where SomeFlag has instance of Flaggable. This way we won't be using +-- Boolean type for all flags but we can implement custom type. +data CustomQueryFlag (sym :: Symbol) flag + +class Flaggable flag where + toBool :: flag -> Bool + fromBool :: Bool -> flag + +instance Flaggable Bool where + toBool = identity + fromBool = identity + +-- TODO (akegalj): this instance is almost the same as upstream HasServer instance of QueryFlag. The only difference is addition of `fromBool` function in `route` implementation. Can we somehow reuse `route` implementation of CustomQuery instead of copy-pasting it here with this small `fromBool` addition? +instance (KnownSymbol sym, HasServer api context, Flaggable flag) + => HasServer (CustomQueryFlag sym flag :> api) context where + + type ServerT (CustomQueryFlag sym flag :> api) m = + flag -> ServerT api m + + hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy @api) pc nt . s + + route Proxy context subserver = + let querytext r = parseQueryText $ rawQueryString r + param r = case lookup paramname (querytext r) of + Just Nothing -> True -- param is there, with no value + Just (Just v) -> examine v -- param with a value + Nothing -> False -- param not in the query string + in route (Proxy @api) context (SI.passToServer subserver $ fromBool . param) + where paramname = toText $ symbolVal (Proxy @sym) + examine v | v == "true" || v == "1" || v == "" = True + | otherwise = False + +instance (KnownSymbol sym, Flaggable flag, HasClient m api) => HasClient m (CustomQueryFlag sym flag :> api) where + type Client m (CustomQueryFlag sym flag :> api) = flag -> Client m api + + clientWithRoute p _ req = clientWithRoute p (Proxy @(QueryFlag sym :> api)) req . toBool + +instance KnownSymbol s => ApiCanLogArg (CustomQueryFlag s a) +instance KnownSymbol s => ApiHasArgClass (CustomQueryFlag s a) + ------------------------------------------------------------------------- -- API construction Helpers ------------------------------------------------------------------------- diff --git a/networking/src/Ntp/Client.hs b/networking/src/Ntp/Client.hs index 14906390b8e..4245cd7d434 100644 --- a/networking/src/Ntp/Client.hs +++ b/networking/src/Ntp/Client.hs @@ -7,7 +7,6 @@ {-# LANGUAGE RankNTypes #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TypeApplications #-} -- | This module implements functionality of NTP client. @@ -147,7 +146,6 @@ updateStatus cli = updateStatus' cli fn , (Wlog.Info, sformat ("Evaluated clock offset "%shown%"mcs") offset) ) --- | -- Every `ntpPollDelay` we send a request to the list of `ntpServers`. Before -- sending a request, we put `NtpSyncPending` to `ncState`. After sending -- all requests we wait until either all servers responded or diff --git a/pkgs/default.nix b/pkgs/default.nix index b3836caf309..a57b5e8525f 100644 --- a/pkgs/default.nix +++ b/pkgs/default.nix @@ -14568,8 +14568,8 @@ version = "1.1.0"; src = fetchgit { url = "https://github.com/input-output-hk/cardano-crypto"; -sha256 = "1v165n4cmp4g272406vaaan7bwvrw2m55jwcbz8qqjxslxv1l63p"; -rev = "33c7ecc6e4bd71c3ea0195e9d796eeace7be22cf"; +sha256 = "0ax9gxbr3qnji1jwz2rz97573lkyi4128m2ck90bi1x15p4bm65y"; +rev = "e87fcd3fea18f664d6e4f39c11abb8cafbd25785"; }; isLibrary = true; @@ -14746,6 +14746,7 @@ license = stdenv.lib.licenses.bsd3; , http-client , http-client-tls , http-conduit +, http-types , lens , log-warper , memory @@ -14841,6 +14842,7 @@ hspec http-client http-client-tls http-conduit +http-types lens log-warper memory @@ -15944,7 +15946,6 @@ license = stdenv.lib.licenses.mit; , cryptonite , cryptonite-openssl , data-default -, ed25519 , formatting , generic-arbitrary , hashable @@ -15990,7 +15991,6 @@ cereal cryptonite cryptonite-openssl data-default -ed25519 formatting hashable lens @@ -17601,6 +17601,7 @@ license = stdenv.lib.licenses.mit; , monad-control , MonadRandom , mtl +, pvss , QuickCheck , random , reflection @@ -17737,10 +17738,12 @@ lens log-warper MonadRandom mtl +pvss QuickCheck safe-exceptions safecopy serokell-util +servant servant-server stm universum @@ -17786,7 +17789,6 @@ license = stdenv.lib.licenses.mit; , cardano-sl-util , cardano-sl-util-test , cardano-sl-wallet -, cardano-sl-wallet-test , cassava , cereal , conduit @@ -17800,6 +17802,7 @@ license = stdenv.lib.licenses.mit; , ed25519 , exceptions , filepath +, foldl , formatting , gauge , generics-sop @@ -17900,7 +17903,6 @@ cardano-sl-networking cardano-sl-node-ipc cardano-sl-util cardano-sl-wallet -cardano-sl-wallet-test cereal conduit connection @@ -17911,6 +17913,7 @@ data-default-class directory ed25519 exceptions +foldl formatting generics-sop http-api-data @@ -18028,7 +18031,6 @@ filepath formatting hedgehog hspec -ixset-typed lens log-warper mtl @@ -18060,7 +18062,6 @@ base bytestring cardano-sl-client cardano-sl-core -cardano-sl-db cardano-sl-wallet cassava connection @@ -27206,7 +27207,6 @@ license = stdenv.lib.licenses.bsd3; mkDerivation , base , bytestring -, fetchgit , ghc-prim , stdenv }: @@ -27214,13 +27214,9 @@ mkDerivation { pname = "ed25519"; version = "0.0.5.0"; -src = fetchgit { - -url = "https://github.com/thoughtpolice/hs-ed25519"; -sha256 = "0fah4vkmqdkjsdh3s3x27yfaif2fbdg6049xvp54b5mh50yvxkfq"; -rev = "da4247b5b3420120e20451e6a252e2a2ca15b43c"; - -}; +sha256 = "d8a5958ebfa9309790efade64275dc5c441b568645c45ceed1b0c6ff36d6156d"; +revision = "2"; +editedCabalFile = "1cq6h3jqkb1kvd9fjfhsllg5gq78sdiyf2gy9862xhlbv6wil19f"; libraryHaskellDepends = [ base bytestring @@ -27228,7 +27224,7 @@ ghc-prim ]; doHaddock = false; doCheck = false; -homepage = "https://thoughtpolice.github.com/hs-ed25519"; +homepage = "http://thoughtpolice.github.com/hs-ed25519"; description = "Ed25519 cryptographic signatures"; license = stdenv.lib.licenses.mit; diff --git a/scripts/build/cardano-sl.sh b/scripts/build/cardano-sl.sh index 73c35abc22e..ec3b81000ff 100755 --- a/scripts/build/cardano-sl.sh +++ b/scripts/build/cardano-sl.sh @@ -140,7 +140,7 @@ do fi done -commonargs='--test --no-haddock-deps --bench --jobs=4' +commonargs='--test --no-haddock-deps --bench --jobs=1' norun='--no-run-tests --no-run-benchmarks' if [[ "$no_nix" == true ]]; then diff --git a/stack.yaml b/stack.yaml index 0c2e2c04448..a3975923a0d 100644 --- a/stack.yaml +++ b/stack.yaml @@ -56,7 +56,7 @@ packages: - location: git: https://github.com/input-output-hk/cardano-crypto - commit: 33c7ecc6e4bd71c3ea0195e9d796eeace7be22cf + commit: e87fcd3fea18f664d6e4f39c11abb8cafbd25785 extra-dep: true # to be removed when haskell-ip is in the current stackage version - location: @@ -95,12 +95,6 @@ packages: commit: 7120bb4d28e708acd52dfd61d3dca7914fac7d7f # master extra-dep: true -# We're waiting on next release: https://github.com/thoughtpolice/hs-ed25519/issues/20 -- location: - git: https://github.com/thoughtpolice/hs-ed25519 - commit: da4247b5b3420120e20451e6a252e2a2ca15b43c - extra-dep: true - # These three are needed for cardano-sl-networking # # This defines a bundle type and attribute: diff --git a/wallet-new/README.md b/wallet-new/README.md index 2efe99e3edc..eda2d739105 100755 --- a/wallet-new/README.md +++ b/wallet-new/README.md @@ -73,25 +73,53 @@ $ stack exec cardano-node -- --topology=wallet-new/topology-examples/testnet.yam From there, you can browse the API documentation for V0 and V1 through the following URLs: -- http://localhost:8090/docs/v0/index/ -- http://localhost:8090/docs/v1/index/ +- https://localhost:8091/docs/v0/index/ +- https://localhost:8091/docs/v1/index/ -### HTTPS -By default, wallet backend only accepts HTTPS connections: +You may also run a simple cURL command to check whether the node is up-and-running: ``` -$ curl localhost:8090/docs/v1/index/ -This server only accepts secure HTTPS connections. +$ curl https://localhost:8090/api/v1/node-info \ + --cacert scripts/tls-files/ca.crt \ + --cert scripts/tls-files/client.pem ``` -We should provide our `ca.crt`: +> *NOTE* +> +> Every node running a wallet API needs x509 certificates for enabling TLS support. By default, +> those certificates are located in `./scripts/tls-files`. Use them if you need a CA or a +> client certificate. + + +## Local Cluster + +Running a node against `mainnet_staging` may not be ideal for testing. The node will also need +time to synchronize and won't run the full API capabilities until having done so. To cope with +this, one may run a local cluster of nodes, acting upon a fresh database, speeding up most of +the operations. To run a local cluster, _nix_ is your friend: + +``` +$ nix-build -A demoCluster -o run-demo --arg useStackBinaries true && ./run-demo +``` + +This will run a local cluster after having set up a fresh environment for it in `./state-demo`. +There are some files of interest in this folder you may need like the tls certificates or the +logging configurations. + + +### HTTPS + +By default, wallet backend only accepts HTTPS connections: ``` -$ curl --cacert scripts/tls-files/ca.crt https://localhost:8090/docs/v1/index/ +$ curl localhost:8090/api/v1/node-info +This server only accepts secure HTTPS connections. ``` -But if we launch a node with `--wallet-debug` option, we can send simple `http`-requests. +Read the documentation about TLS authentication in [docs/tls-authentication.md](../docs/tls-authentication.md) +for details about how to contact a wallet node with TLS. + ### Swagger Specification @@ -126,9 +154,33 @@ $ stack test cardano-sl-wallet-new Wallet integration tests can be run using this command (from the project *root* directory): ``` -$ nix-build release.nix -A walletIntegrationTests +$ nix-build -A walletIntegrationTests --arg useStackBinaries true ``` +> **NOTE**: +> `nix-build -A walletIntegrationTests` (with or without `useStackBinaries`) runs a +> local demo cluster, either via stack or nix by default on your local machine +> that is fully usable by daedalus/curl etc... and requires port 8090 and +> ports 3001-3004 and 3101 to be available. This cluster has four core nodes, 1 +> relay, and a single wallet and has full x509 CA cert enabled. It then +> pre-loads some genesis poor keys for testing and runs the wal-integr-test +> haskell program, which connects to the running cluster. When it completes, it +> terminates the demo cluster and wallet. This will fail if ports aren't +> available to bind (although cardano-node will happily run without crashing, +> it just will be broken), you try running two of these at once, etc... +> +> This is differentiated from `nix-build -A tests.walletIntegration` which **DOES +> NOT** support `useStackBinaries` and builds/runs the entire cluster in a sandbox +> isolated from the rest of the system (assuming nix sandboxing is enabled). +> This is how hydra runs the tests and why hydra is capable or running more +> than one cluster at the same time. This will use any binaries cached by hydra +> if you have the IOHK binary cache enabled, or will build everything cleanly +> in nix if the binaries aren't available in the local nix store. One other +> thing to note is that tests.walletIntegration will only run once and will +> cache the results (unless of a failure). If you have a need to rerun the +> test, you can pass the `--check` flag to force the test to run again. `--check` +> is used to confirm that results from one test match the results again. + ## Developing We have a [`Makefile`](./Makefile) with some helpful commands for development. @@ -152,7 +204,8 @@ Now use following command (from the `cardano-sl` *root* directory): $ curl -X POST \ -H "Content-Type: application/json" \ -d '"PATH_TO_SECRET_KEY"' \ - --cacert scripts/tls-files/ca.crt + --cacert scripts/tls-files/ca.crt \ + --cert scripts/tls-files/client.pem \ https://localhost:8090/api/wallets/keys ``` @@ -217,6 +270,7 @@ using environment variables as follows: LANG=en_GB.UTF-8 LC_ALL=en_GB.UTF-8 stack exec -- ... ``` + ##### API returns `415 Unsupported Media Type` The wallet's API can be quite picky about media-types and expect both a given type and an @@ -232,3 +286,10 @@ value: ``` application/json;charset=utf-8 ``` + + +##### API returns `error:14094416:SSL routines:ssl3_read_bytes:sslv3 alert certificate unknown` + +You're missing a valid client certificate to contact the node. For development, you may run the +node with `--no-client-auth` or provide a valid corresponding client x509 certificates. More +information in [docs/tls-authentication.md](../docs/tls-authentication.md). diff --git a/wallet-new/cardano-sl-wallet-new.cabal b/wallet-new/cardano-sl-wallet-new.cabal index b52ebc5ff61..9b89b6ca4de 100755 --- a/wallet-new/cardano-sl-wallet-new.cabal +++ b/wallet-new/cardano-sl-wallet-new.cabal @@ -129,6 +129,7 @@ library Cardano.Wallet.Server.CLI Cardano.Wallet.Server.Plugins Cardano.Wallet.TypeLits + Cardano.Wallet.Types.UtxoStatistics Cardano.Wallet.Client Cardano.Wallet.Client.Http @@ -175,7 +176,6 @@ library , cardano-sl-node-ipc , cardano-sl-util , cardano-sl-wallet - , cardano-sl-wallet-test , cereal , conduit , connection @@ -194,6 +194,7 @@ library , http-client-tls , http-types , ixset-typed + , foldl , lens , log-warper , memory @@ -207,6 +208,7 @@ library , reflection , resourcet , retry + , stm , safecopy , safe-exceptions , serokell-util @@ -354,6 +356,7 @@ executable wal-integr-test , aeson-diff , aeson-pretty , bytestring + , cardano-sl , cardano-sl-core , cardano-sl-wallet , cardano-sl-wallet-new @@ -502,7 +505,6 @@ test-suite wallet-unit-tests , data-default , formatting , hspec - , ixset-typed , lens , log-warper , mtl @@ -640,7 +642,6 @@ benchmark cardano-sl-wallet-new-bench , bytestring , cardano-sl-client , cardano-sl-core - , cardano-sl-db , cardano-sl-wallet , cassava , connection diff --git a/wallet-new/integration/QuickCheckSpecs.hs b/wallet-new/integration/QuickCheckSpecs.hs index 18f957afc9e..f726676f850 100644 --- a/wallet-new/integration/QuickCheckSpecs.hs +++ b/wallet-new/integration/QuickCheckSpecs.hs @@ -6,6 +6,7 @@ module QuickCheckSpecs (mkSpec) where import Universum +import GHC.TypeLits (KnownSymbol) import Network.HTTP.Client hiding (Proxy) import Network.HTTP.Types import Servant @@ -21,6 +22,7 @@ import qualified Cardano.Wallet.API.V1 as V0 import qualified Cardano.Wallet.API.V1 as V1 import Cardano.Wallet.API.V1.Parameters (WalletRequestParams, WithWalletRequestParams) +import Pos.Util.Servant (CustomQueryFlag) -- Our API apparently is returning JSON Arrays which is considered bad practice as very old -- browsers can be hacked: https://haacked.com/archive/2009/06/25/json-hijacking.aspx/ @@ -72,6 +74,12 @@ instance HasGenRequest sub => HasGenRequest (Tags tags :> sub) where instance HasGenRequest (sub :: *) => HasGenRequest (WalletRequestParams :> sub) where genRequest _ = genRequest (Proxy @(WithWalletRequestParams sub)) +instance ( KnownSymbol sym + , HasGenRequest sub + ) => + HasGenRequest (CustomQueryFlag sym flag :> sub) where + genRequest _ = genRequest (Proxy @(QueryFlag sym :> sub)) + -- -- RESTful-abiding predicates -- diff --git a/wallet-new/integration/TransactionSpecs.hs b/wallet-new/integration/TransactionSpecs.hs index 89dc730e515..0cce1dd91f3 100644 --- a/wallet-new/integration/TransactionSpecs.hs +++ b/wallet-new/integration/TransactionSpecs.hs @@ -11,9 +11,12 @@ import Control.Lens import Test.Hspec import Text.Show.Pretty (ppShow) +import Util + +import qualified Data.Map.Strict as Map import qualified Pos.Core as Core +import qualified Pos.Core.Txp as Txp -import Util {-# ANN module ("HLint: ignore Reduce duplication" :: Text) #-} @@ -24,7 +27,7 @@ ppShowT :: Show a => a -> Text ppShowT = fromString . ppShow transactionSpecs :: WalletRef -> WalletClient IO -> Spec -transactionSpecs wRef wc = do +transactionSpecs wRef wc = describe "Transactions" $ do it "posted transactions appear in the index" $ do genesis <- genesisWallet wc @@ -187,3 +190,43 @@ transactionSpecs wRef wc = do etxn <- postTransaction wc payment void $ etxn `shouldPrism` _Left + + xit "posted transactions gives rise to nonempty Utxo histogram" $ do + genesis <- genesisWallet wc + (fromAcct, _) <- firstAccountAndId wc genesis + + wallet <- sampleWallet wRef wc + (_, toAddr) <- firstAccountAndId wc wallet + + let payment val = Payment + { pmtSource = PaymentSource + { psWalletId = walId genesis + , psAccountIndex = accIndex fromAcct + } + , pmtDestinations = pure PaymentDistribution + { pdAddress = addrId toAddr + , pdAmount = V1 (Core.mkCoin val) + } + , pmtGroupingPolicy = Nothing + , pmtSpendingPassword = Nothing + } + + eresp0 <- getUtxoStatistics wc (walId wallet) + utxoStatistics0 <- fmap wrData eresp0 `shouldPrism` _Right + let utxoStatistics0Expected = computeUtxoStatistics log10 [] + utxoStatistics0 `shouldBe` utxoStatistics0Expected + + void $ postTransaction wc (payment 1) + threadDelay 120000000 + + let txIn = Txp.TxInUnknown 0 "test" + let txOut = Txp.TxOutAux Txp.TxOut + { Txp.txOutAddress = unV1 (addrId toAddr) + , Txp.txOutValue = Core.mkCoin 1 + } + let utxos = [Map.fromList [(txIn, txOut)]] + + eresp <- getUtxoStatistics wc (walId wallet) + utxoStatistics <- fmap wrData eresp `shouldPrism` _Right + let utxoStatisticsExpected = computeUtxoStatistics log10 utxos + utxoStatistics `shouldBe` utxoStatisticsExpected diff --git a/wallet-new/integration/WalletSpecs.hs b/wallet-new/integration/WalletSpecs.hs index e16749f3177..d056476be5d 100644 --- a/wallet-new/integration/WalletSpecs.hs +++ b/wallet-new/integration/WalletSpecs.hs @@ -13,7 +13,7 @@ import Util walletSpecs :: WalletRef -> WalletClient IO -> Spec -walletSpecs _ wc = do +walletSpecs _ wc = describe "Wallets" $ do it "Creating a wallet makes it available." $ do newWallet <- randomWallet CreateWallet @@ -51,6 +51,15 @@ walletSpecs _ wc = do } eresp `shouldPrism_` _Right + + it "creating wallet gives rise to an empty Utxo histogram" $ do + newWallet <- randomWallet CreateWallet + wallet <- createWalletCheck wc newWallet + + eresp <- getUtxoStatistics wc (walId wallet) + utxoStatistics <- fmap wrData eresp `shouldPrism` _Right + let utxoStatisticsExpected = computeUtxoStatistics log10 [] + utxoStatistics `shouldBe` utxoStatisticsExpected where testWalletAlreadyExists action = do newWallet1 <- randomWallet action diff --git a/wallet-new/src/Cardano/Wallet/API/V1/Handlers/Info.hs b/wallet-new/src/Cardano/Wallet/API/V1/Handlers/Info.hs index 8c450589d61..34ee9273de9 100644 --- a/wallet-new/src/Cardano/Wallet/API/V1/Handlers/Info.hs +++ b/wallet-new/src/Cardano/Wallet/API/V1/Handlers/Info.hs @@ -6,12 +6,16 @@ import Servant import Cardano.Wallet.API.Response (WalletResponse, single) import qualified Cardano.Wallet.API.V1.Info as Info -import Cardano.Wallet.API.V1.Types (NodeInfo) +import Cardano.Wallet.API.V1.Types (ForceNtpCheck, NodeInfo) import Cardano.Wallet.WalletLayer (ActiveWalletLayer) import qualified Cardano.Wallet.WalletLayer as WalletLayer handlers :: ActiveWalletLayer IO -> ServerT Info.API Handler handlers = getNodeInfo -getNodeInfo :: ActiveWalletLayer IO -> Handler (WalletResponse NodeInfo) -getNodeInfo w = liftIO $ single <$> WalletLayer.getNodeInfo w +getNodeInfo + :: ActiveWalletLayer IO + -> ForceNtpCheck + -> Handler (WalletResponse NodeInfo) +getNodeInfo w forceNtp = + liftIO $ single <$> WalletLayer.getNodeInfo w forceNtp diff --git a/wallet-new/src/Cardano/Wallet/API/V1/Handlers/Wallets.hs b/wallet-new/src/Cardano/Wallet/API/V1/Handlers/Wallets.hs index 4f8c69bcbd2..191a81ad19c 100644 --- a/wallet-new/src/Cardano/Wallet/API/V1/Handlers/Wallets.hs +++ b/wallet-new/src/Cardano/Wallet/API/V1/Handlers/Wallets.hs @@ -2,17 +2,17 @@ module Cardano.Wallet.API.V1.Handlers.Wallets where import Universum -import Pos.Core (Coin) +import Servant import Cardano.Wallet.API.Request import Cardano.Wallet.API.Response import Cardano.Wallet.API.V1.Types as V1 import qualified Cardano.Wallet.API.V1.Wallets as Wallets - import Cardano.Wallet.WalletLayer (PassiveWalletLayer) import qualified Cardano.Wallet.WalletLayer as WalletLayer -import Servant +import Pos.Core.Common (Coin (..)) + -- | All the @Servant@ handlers for wallet-specific operations. handlers :: PassiveWalletLayer IO -> ServerT Wallets.API Handler @@ -22,6 +22,7 @@ handlers pwl = newWallet pwl :<|> deleteWallet pwl :<|> getWallet pwl :<|> updateWallet pwl + :<|> getUtxoStatistics pwl :<|> checkExternalWallet pwl :<|> newExternalWallet pwl :<|> deleteExternalWallet pwl @@ -96,6 +97,17 @@ updateWallet pwl wid walletUpdateRequest = do Left e -> throwM e Right w -> return $ single w +getUtxoStatistics + :: PassiveWalletLayer IO + -> WalletId + -> Handler (WalletResponse UtxoStatistics) +getUtxoStatistics pwl wid = do + res <- liftIO $ WalletLayer.getUtxos pwl wid + case res of + Left e -> throwM e + Right w -> + return $ single $ V1.computeUtxoStatistics V1.log10 (map snd w) + checkExternalWallet :: PassiveWalletLayer IO -> PublicKeyAsBase58 -> Handler (WalletResponse WalletAndTxHistory) diff --git a/wallet-new/src/Cardano/Wallet/API/V1/Info.hs b/wallet-new/src/Cardano/Wallet/API/V1/Info.hs index 864e865e705..03cb87dec97 100644 --- a/wallet-new/src/Cardano/Wallet/API/V1/Info.hs +++ b/wallet-new/src/Cardano/Wallet/API/V1/Info.hs @@ -4,9 +4,11 @@ import Cardano.Wallet.API.Response (ValidJSON, WalletResponse) import Cardano.Wallet.API.Types import Cardano.Wallet.API.V1.Types +import Pos.Util.Servant (CustomQueryFlag) import Servant type API = Tags '["Info"] :> - ( "node-info" :> Summary "Retrieves the dynamic information for this node." - :> Get '[ValidJSON] (WalletResponse NodeInfo) + ( "node-info" :> Summary "Retrieves the dynamic information for this node." + :> CustomQueryFlag "force_ntp_check" ForceNtpCheck + :> Get '[ValidJSON] (WalletResponse NodeInfo) ) diff --git a/wallet-new/src/Cardano/Wallet/API/V1/LegacyHandlers/Info.hs b/wallet-new/src/Cardano/Wallet/API/V1/LegacyHandlers/Info.hs index 3ebc03dbd4f..2fe1d0e5b86 100644 --- a/wallet-new/src/Cardano/Wallet/API/V1/LegacyHandlers/Info.hs +++ b/wallet-new/src/Cardano/Wallet/API/V1/LegacyHandlers/Info.hs @@ -2,6 +2,7 @@ module Cardano.Wallet.API.V1.LegacyHandlers.Info where import Universum +import Control.Monad.STM (retry) import System.Wlog (WithLogger) import Cardano.Wallet.API.Response (WalletResponse, single) @@ -9,7 +10,7 @@ import qualified Cardano.Wallet.API.V1.Info as Info import Cardano.Wallet.API.V1.Migration import Cardano.Wallet.API.V1.Types as V1 -import Ntp.Client (NtpStatus) +import Ntp.Client (NtpStatus (NtpSyncPending)) import Pos.Infra.Diffusion.Subscription.Status (ssMap) import Pos.Infra.Diffusion.Types (Diffusion (..)) import Pos.Wallet.WalletMode (MonadBlockchainInfo) @@ -37,12 +38,22 @@ getInfo :: ( MonadIO m ) => Diffusion MonadV1 -> TVar NtpStatus + -> ForceNtpCheck -> m (WalletResponse NodeInfo) -getInfo Diffusion{..} ntpStatus = do +getInfo Diffusion{..} ntpStatus ntpCheck = do + timeDifference <- V0.localTimeDifferencePure <$> + if ntpCheck == ForceNtpCheck + then do + atomically $ writeTVar ntpStatus NtpSyncPending + atomically $ do + s <- readTVar ntpStatus + case s of + NtpSyncPending -> retry + _ -> pure s + else readTVarIO ntpStatus subscribers <- readTVarIO (ssMap subscriptionStates) spV0 <- V0.syncProgress syncProgress <- migrate spV0 - timeDifference <- V0.localTimeDifference ntpStatus return $ single NodeInfo { nfoSyncProgress = syncProgress , nfoSubscriptionStatus = subscribers diff --git a/wallet-new/src/Cardano/Wallet/API/V1/LegacyHandlers/Wallets.hs b/wallet-new/src/Cardano/Wallet/API/V1/LegacyHandlers/Wallets.hs index f9a234cb044..07e17339681 100644 --- a/wallet-new/src/Cardano/Wallet/API/V1/LegacyHandlers/Wallets.hs +++ b/wallet-new/src/Cardano/Wallet/API/V1/LegacyHandlers/Wallets.hs @@ -33,6 +33,7 @@ import Pos.Wallet.Web.Methods.Logic (MonadWalletLogic, import Pos.Wallet.Web.Tracking.Types (SyncQueue) import Servant + -- | All the @Servant@ handlers for wallet-specific operations. handlers :: HasConfigurations => ServerT Wallets.API MonadV1 @@ -42,11 +43,11 @@ handlers = newWallet :<|> deleteWallet :<|> getWallet :<|> updateWallet + :<|> getUtxoStatistics :<|> checkExternalWallet :<|> newExternalWallet :<|> deleteExternalWallet - -- | Pure function which returns whether or not the underlying node is -- \"synced enough\" to allow wallet creation/restoration. The notion of -- \"synced enough\" is quite vague and if made too stringent could prevent @@ -186,6 +187,15 @@ updateWallet wid WalletUpdate{..} = do ws' <- V0.askWalletSnapshot addWalletInfo ws' updated +-- | Gets Utxo statistics for a wallet. +-- | Stub, not calling data layer. +getUtxoStatistics + :: (MonadWalletLogic ctx m) + => WalletId + -> m (WalletResponse UtxoStatistics) +getUtxoStatistics _ = do + return $ single (V1.computeUtxoStatistics V1.log10 []) + -- | Check if external wallet is presented in node's wallet db. checkExternalWallet :: -- ( V0.MonadWalletLogic ctx m diff --git a/wallet-new/src/Cardano/Wallet/API/V1/Swagger.hs b/wallet-new/src/Cardano/Wallet/API/V1/Swagger.hs index 0027e330f70..f86249d6761 100644 --- a/wallet-new/src/Cardano/Wallet/API/V1/Swagger.hs +++ b/wallet-new/src/Cardano/Wallet/API/V1/Swagger.hs @@ -24,6 +24,11 @@ import Cardano.Wallet.API.V1.Swagger.Example import Cardano.Wallet.API.V1.Types import Cardano.Wallet.TypeLits (KnownSymbols (..)) +import Pos.Core.Update (SoftwareVersion) +import Pos.Util.CompileInfo (CompileTimeInfo, ctiGitRevision) +import Pos.Util.Servant (CustomQueryFlag, LoggingApi) +import Pos.Wallet.Web.Swagger.Instances.Schema () + import Control.Lens ((?~)) import Data.Aeson (encode) import Data.Aeson.Encode.Pretty @@ -31,12 +36,9 @@ import Data.Map (Map) import Data.Swagger hiding (Example, Header) import Data.Typeable import Formatting (build, sformat) +import GHC.TypeLits (KnownSymbol) import NeatInterpolation -import Pos.Core.Update (SoftwareVersion) -import Pos.Util.CompileInfo (CompileTimeInfo, ctiGitRevision) -import Pos.Util.Servant (LoggingApi) -import Pos.Wallet.Web.Swagger.Instances.Schema () -import Servant (Handler, ServantErr (..), Server) +import Servant (Handler, QueryFlag, ServantErr (..), Server) import Servant.API.Sub import Servant.Swagger import Servant.Swagger.UI (SwaggerSchemaUI') @@ -188,16 +190,44 @@ instance ToParamSchema Core.Address where instance ToParamSchema (V1 Core.Address) where toParamSchema _ = toParamSchema (Proxy @Core.Address) +instance ( KnownSymbol sym + , HasSwagger sub + ) => + HasSwagger (CustomQueryFlag sym flag :> sub) where + toSwagger _ = + let swgr = toSwagger (Proxy @(QueryFlag sym :> sub)) + in swgr & over (operationsOf swgr . parameters) (map toDescription) + where + toDescription :: Referenced Param -> Referenced Param + toDescription (Inline p@(_paramName -> pName)) = + case M.lookup pName customQueryFlagToDescription of + Nothing -> Inline p + Just d -> Inline (p & description .~ Just d) + toDescription x = x + + -- -- Descriptions -- +customQueryFlagToDescription :: Map T.Text T.Text +customQueryFlagToDescription = M.fromList [ + ("force_ntp_check", forceNtpCheckDescription) + ] + requestParameterToDescription :: Map T.Text T.Text requestParameterToDescription = M.fromList [ ("page", pageDescription) , ("per_page", perPageDescription (fromString $ show maxPerPageEntries) (fromString $ show defaultPerPageEntries)) ] +-- TODO: it would be nice to read ntp configuration directly here to fetch +-- 30 seconds wait time instead of hardcoding it here. +forceNtpCheckDescription :: T.Text +forceNtpCheckDescription = [text| +In some cases, API Clients need to force a new NTP check as a previous result gets cached. A typical use-case is after asking a user to fix its system clock. If this flag is set, request will block until NTP server responds or it will timout if NTP server is not available within **30** seconds. +|] + pageDescription :: T.Text pageDescription = [text| The page number to fetch for this request. The minimum is **1**. If nothing is specified, **this value defaults to 1** and always shows the first entries in the requested collection. @@ -286,10 +316,6 @@ Software Version | Git Revision -------------------|------------------- $deSoftwareVersion | $deGitRevision -> **Warning**: This version is currently a **BETA-release** which is still under testing before -> its final stable release. Should you encounter any issues or have any remarks, please let us -> know; your feedback is highly appreciated. - Getting Started =============== @@ -831,6 +857,24 @@ curl -X GET 'https://127.0.0.1:8090/api/v1/transactions?wallet_id=Ae2tdPwU...3AV --cert ./scripts/tls-files/client.pem ``` + +Getting Utxo statistics +--------------------------------- + +You can get Utxo statistics of a given wallet using + [`GET /api/v1/wallets/{{walletId}}/statistics/utxos`](#tag/Accounts%2Fpaths%2F~1api~1v1~1wallets~1{walletId}~1statistics~1utxos%2Fget) +``` +curl -X GET \ + https://127.0.0.1:8090/api/v1/wallets/Ae2tdPwUPE...8V3AVTnqGZ/statistics/utxos \ + -H 'Accept: application/json;charset=utf-8' \ + --cacert ./scripts/tls-files/ca.crt \ + --cert ./scripts/tls-files/client.pem +``` + +```json +$readUtxoStatistics +``` + Make sure to carefully read the section about [Pagination](#section/Pagination) to fully leverage the API capabilities. |] @@ -845,7 +889,7 @@ leverage the API capabilities. readFees = decodeUtf8 $ encodePretty $ genExample @(WalletResponse EstimatedFees) readNodeInfo = decodeUtf8 $ encodePretty $ genExample @(WalletResponse NodeInfo) readTransactions = decodeUtf8 $ encodePretty $ genExample @(WalletResponse [Transaction]) - + readUtxoStatistics = decodeUtf8 $ encodePretty $ genExample @(WalletResponse UtxoStatistics) -- | Provide an alternative UI (ReDoc) for rendering Swagger documentation. swaggerSchemaUIServer diff --git a/wallet-new/src/Cardano/Wallet/API/V1/Types.hs b/wallet-new/src/Cardano/Wallet/API/V1/Types.hs index 16ff4517398..6ca16740e85 100644 --- a/wallet-new/src/Cardano/Wallet/API/V1/Types.hs +++ b/wallet-new/src/Cardano/Wallet/API/V1/Types.hs @@ -27,6 +27,7 @@ module Cardano.Wallet.API.V1.Types ( , NewAccount (..) , Update , New + , ForceNtpCheck (..) -- * Domain-specific types -- * Wallets , Wallet (..) @@ -122,6 +123,8 @@ module Cardano.Wallet.API.V1.Types ( , WalletError(..) , toServantError , toHttpErrorStatus + + , module Cardano.Wallet.Types.UtxoStatistics ) where import qualified Prelude @@ -177,7 +180,9 @@ import Cardano.Wallet.API.V1.Generic (jsendErrorGenericParseJSON, import Cardano.Wallet.API.V1.Swagger.Example (Example, example, genExample) import Cardano.Wallet.Orphans.Aeson () +import Cardano.Wallet.Types.UtxoStatistics import Cardano.Wallet.Util (showApiUtcTime) + import qualified Pos.Binary.Class as Bi import qualified Pos.Client.Txp.Util as Core import Pos.Core (addressF) @@ -193,6 +198,7 @@ import Pos.Infra.Util.LogSafe (BuildableSafeGen (..), SecureLog (..), buildSafe, buildSafeList, buildSafeMaybe, deriveSafeBuildable, plainOrSecureF) import Pos.Util.Mnemonic (Mnemonic) +import Pos.Util.Servant (Flaggable (..)) import Pos.Wallet.Web.ClientTypes.Instances () import qualified Pos.Wallet.Web.State.Storage as OldStorage import Test.Pos.Core.Arbitrary () @@ -2476,7 +2482,7 @@ instance ToSchema TimeInfo where declareNamedSchema = genericSchemaDroppingPrefix "time" $ \(--^) p -> p & "differenceFromNtpServer" --^ ("The difference in microseconds between the node time and the NTP " - <> "server. This value will be null if the NTP server is pending or " + <> "server. This value will be null if the NTP server is " <> "unavailable.") instance Arbitrary TimeInfo where @@ -2669,6 +2675,22 @@ instance Arbitrary Redemption where <*> arbitrary <*> arbitrary +data ForceNtpCheck + = ForceNtpCheck + | NoNtpCheck + deriving (Eq) + +instance Flaggable ForceNtpCheck where + toBool ForceNtpCheck = True + toBool NoNtpCheck = False + fromBool True = ForceNtpCheck + fromBool False = NoNtpCheck + +deriveSafeBuildable ''ForceNtpCheck +instance BuildableSafeGen ForceNtpCheck where + buildSafeGen _ ForceNtpCheck = "force ntp check" + buildSafeGen _ NoNtpCheck = "no ntp check" + -- -- POST/PUT requests isomorphisms -- diff --git a/wallet-new/src/Cardano/Wallet/API/V1/Wallets.hs b/wallet-new/src/Cardano/Wallet/API/V1/Wallets.hs index 6d74dfc8cd0..cc771ee74ad 100644 --- a/wallet-new/src/Cardano/Wallet/API/V1/Wallets.hs +++ b/wallet-new/src/Cardano/Wallet/API/V1/Wallets.hs @@ -37,6 +37,9 @@ type API = Tags '["Wallets"] :> :> Summary "Update the Wallet identified by the given walletId." :> ReqBody '[ValidJSON] (Update Wallet) :> Put '[ValidJSON] (WalletResponse Wallet) + :<|> "wallets" :> CaptureWalletId :> "statistics" :> "utxos" + :> Summary "Returns Utxo statistics for the Wallet identified by the given walletId." + :> Get '[ValidJSON] (WalletResponse UtxoStatistics) :<|> "external-wallets" :> Capture "rootPK" PublicKeyAsBase58 :> Summary "Check if this external wallet is presented in the node." diff --git a/wallet-new/src/Cardano/Wallet/Client.hs b/wallet-new/src/Cardano/Wallet/Client.hs index e83a2513d2a..4ec18ad2dbc 100644 --- a/wallet-new/src/Cardano/Wallet/Client.hs +++ b/wallet-new/src/Cardano/Wallet/Client.hs @@ -89,6 +89,8 @@ data WalletClient m :: WalletId -> Resp m Wallet , updateWallet :: WalletId -> Update Wallet -> Resp m Wallet + , getUtxoStatistics + :: WalletId -> Resp m UtxoStatistics , postCheckExternalWallet :: PublicKeyAsBase58 -> Resp m WalletAndTxHistory , postExternalWallet @@ -136,7 +138,7 @@ data WalletClient m :: Resp m NodeSettings -- info , getNodeInfo - :: Resp m NodeInfo + :: ForceNtpCheck -> Resp m NodeInfo } deriving Generic -- | Paginates through all request pages and concatenates the result. @@ -218,6 +220,8 @@ hoistClient phi wc = WalletClient phi . getWallet wc , updateWallet = \x -> phi . updateWallet wc x + , getUtxoStatistics = + phi . getUtxoStatistics wc , postCheckExternalWallet = phi . postCheckExternalWallet wc , postExternalWallet = @@ -250,7 +254,7 @@ hoistClient phi wc = WalletClient , getNodeSettings = phi (getNodeSettings wc) , getNodeInfo = - phi (getNodeInfo wc) + phi . getNodeInfo wc } -- | Generalize a @'WalletClient' 'IO'@ into a @('MonadIO' m) => diff --git a/wallet-new/src/Cardano/Wallet/Client/Http.hs b/wallet-new/src/Cardano/Wallet/Client/Http.hs index 8af96c759d4..7b112967a1f 100644 --- a/wallet-new/src/Cardano/Wallet/Client/Http.hs +++ b/wallet-new/src/Cardano/Wallet/Client/Http.hs @@ -105,6 +105,8 @@ mkHttpClient baseUrl manager = WalletClient = run . getWalletR , updateWallet = \x -> run . updateWalletR x + , getUtxoStatistics + = run . getUtxoStatisticsR , postCheckExternalWallet = run . postCheckExternalWalletR , postExternalWallet @@ -141,7 +143,7 @@ mkHttpClient baseUrl manager = WalletClient = run getNodeSettingsR -- info , getNodeInfo - = run getNodeInfoR + = run . getNodeInfoR } where @@ -171,6 +173,7 @@ mkHttpClient baseUrl manager = WalletClient :<|> deleteWalletR :<|> getWalletR :<|> updateWalletR + :<|> getUtxoStatisticsR :<|> postCheckExternalWalletR :<|> postExternalWalletR :<|> deleteExternalWalletR diff --git a/wallet-new/src/Cardano/Wallet/Kernel/DB/InDb.hs b/wallet-new/src/Cardano/Wallet/Kernel/DB/InDb.hs index 14d7630b934..0e3155cad19 100644 --- a/wallet-new/src/Cardano/Wallet/Kernel/DB/InDb.hs +++ b/wallet-new/src/Cardano/Wallet/Kernel/DB/InDb.hs @@ -10,8 +10,9 @@ import Universum import Control.Arrow ((***)) import Control.Lens.TH (makeLenses) +import Crypto.Error (CryptoFailable (..)) import Crypto.Hash (Digest, digestFromByteString) -import qualified Crypto.Sign.Ed25519 as Ed25519 +import qualified Crypto.PubKey.Ed25519 as Ed25519 import qualified Data.ByteArray as BA import qualified Data.ByteString as B import qualified Data.ByteString.Lazy as BL @@ -302,16 +303,20 @@ instance SC.SafeCopy (InDb Txp.TxSigData) where instance SC.SafeCopy (InDb Core.RedeemPublicKey) where getCopy = SC.contain $ do bs :: B.ByteString <- SC.safeGet - pure (InDb (Core.RedeemPublicKey (Ed25519.PublicKey bs))) + case Ed25519.publicKey bs of + CryptoPassed key -> pure (InDb (Core.RedeemPublicKey key)) + CryptoFailed err -> fail (show err) putCopy (InDb (Core.RedeemPublicKey pk)) = SC.contain $ do - SC.safePut (Ed25519.openPublicKey pk :: B.ByteString) + SC.safePut (BA.convert pk :: ByteString) instance SC.SafeCopy (InDb (Core.RedeemSignature a)) where getCopy = SC.contain $ do bs :: B.ByteString <- SC.safeGet - pure (InDb (Core.RedeemSignature (Ed25519.Signature bs))) + case Ed25519.signature bs of + CryptoPassed sig -> pure (InDb (Core.RedeemSignature sig)) + CryptoFailed err -> fail (show err) putCopy (InDb (Core.RedeemSignature pk)) = SC.contain $ do - SC.safePut (Ed25519.unSignature pk :: B.ByteString) + SC.safePut (BA.convert pk :: ByteString) instance SC.SafeCopy (InDb h) => SC.SafeCopy (InDb (Core.Attributes h)) where getCopy = SC.contain $ do diff --git a/wallet-new/src/Cardano/Wallet/Kernel/NodeStateAdaptor.hs b/wallet-new/src/Cardano/Wallet/Kernel/NodeStateAdaptor.hs index 7b29bcaf49a..3f18db7a7b4 100644 --- a/wallet-new/src/Cardano/Wallet/Kernel/NodeStateAdaptor.hs +++ b/wallet-new/src/Cardano/Wallet/Kernel/NodeStateAdaptor.hs @@ -25,7 +25,7 @@ module Cardano.Wallet.Kernel.NodeStateAdaptor ( , getNextEpochSlotDuration , curSoftwareVersion , compileInfo - , getNtpStatus + , getNtpDrift -- * Non-mockable , filterUtxo , mostRecentMainBlock @@ -44,16 +44,19 @@ import Universum import Control.Lens (lens) import Control.Monad.IO.Unlift (MonadUnliftIO, UnliftIO (UnliftIO), askUnliftIO, unliftIO, withUnliftIO) +import Control.Monad.STM (retry) import Data.Conduit (mapOutputMaybe, runConduitRes, (.|)) import qualified Data.Conduit.List as Conduit import Data.SafeCopy (base, deriveSafeCopy) -import Data.Time.Units (Millisecond) +import Data.Time.Units (Millisecond, toMicroseconds) import Formatting (bprint, build, sformat, shown, (%)) import qualified Formatting.Buildable import Ntp.Client (NtpStatus (..)) +import Ntp.Packet (NtpOffset) import Serokell.Data.Memory.Units (Byte) import System.Wlog (CanLog (..), HasLoggerName (..)) +import qualified Cardano.Wallet.API.V1.Types as V1 import Pos.Chain.Block (Block, HeaderHash, MainBlock, blockHeader, headerHash, mainBlockSlot, prevBlockL) import Pos.Chain.Update (ConfirmedProposalState, @@ -261,7 +264,7 @@ data NodeStateAdaptor m = Adaptor { , compileInfo :: m CompileTimeInfo -- | Ask the NTP client for the status - , getNtpStatus :: m NtpStatus + , getNtpDrift :: V1.ForceNtpCheck -> m V1.TimeInfo } {------------------------------------------------------------------------------- @@ -352,7 +355,7 @@ newNodeStateAdaptor nr ntpStatus = Adaptor { , getSlotCount = return $ pcEpochSlots protocolConstants , curSoftwareVersion = return $ Upd.curSoftwareVersion , compileInfo = return $ Util.compileInfo - , getNtpStatus = liftIO $ readTVarIO ntpStatus + , getNtpDrift = defaultGetNtpDrift ntpStatus } where run :: forall a. @@ -403,7 +406,7 @@ defaultGetNextEpochSlotDuration :: MonadIO m => WithNodeState m Millisecond defaultGetNextEpochSlotDuration = Slotting.getNextEpochSlotDuration {------------------------------------------------------------------------------- - Non-mockable functinos + Non-mockable functions -------------------------------------------------------------------------------} filterUtxo :: (NodeConstraints, MonadCatch m, MonadUnliftIO m) @@ -425,6 +428,30 @@ waitForUpdate = liftIO . takeMVar =<< asks l l :: Res -> MVar ConfirmedProposalState l = ucDownloadedUpdate . view lensOf' +-- | Get the difference between NTP time and local system time, nothing if the +-- NTP server couldn't be reached in the last 30min. +-- +-- Note that one can force a new query to the NTP server in which case, it may +-- take up to 30s to resolve. +defaultGetNtpDrift :: MonadIO m => TVar NtpStatus -> V1.ForceNtpCheck -> m V1.TimeInfo +defaultGetNtpDrift tvar ntpCheckBehavior = liftIO $ do + when (ntpCheckBehavior == V1.ForceNtpCheck) $ + atomically $ writeTVar tvar NtpSyncPending + mkTimeInfo <$> waitForNtpStatus + where + mkTimeInfo :: Maybe NtpOffset -> V1.TimeInfo + mkTimeInfo = V1.TimeInfo . fmap (V1.mkLocalTimeDifference . toMicroseconds) + + -- NOTE This usually takes ~100-300ms and at most 30s + waitForNtpStatus :: MonadIO m => m (Maybe NtpOffset) + waitForNtpStatus = atomically $ do + status <- readTVar tvar + case status of + NtpSyncPending -> retry + NtpDrift offset -> pure (Just offset) + NtpSyncUnavailable -> pure Nothing + + -- | Get the most recent main block starting at the specified header -- -- Returns nothing if there are no (regular) blocks on the blockchain yet. @@ -484,12 +511,12 @@ mockNodeState MockNodeStateParams{..} = , getTipSlotId = return mockNodeStateTipSlotId , getSecurityParameter = return mockNodeStateSecurityParameter , getNextEpochSlotDuration = return mockNodeStateNextEpochSlotDuration - , getNtpStatus = return mockNodeStateNtpStatus , getSlotStart = return . mockNodeStateSlotStart , getMaxTxSize = return $ bvdMaxTxSize genesisBlockVersionData , getSlotCount = return $ pcEpochSlots protocolConstants , curSoftwareVersion = return $ Upd.curSoftwareVersion , compileInfo = return $ Util.compileInfo + , getNtpDrift = return . mockNodeStateNtpDrift } -- | Variation on 'mockNodeState' that uses the default params @@ -513,8 +540,8 @@ data MockNodeStateParams = NodeConstraints => MockNodeStateParams { -- | Value for 'getNextEpochSlotDuration' , mockNodeStateNextEpochSlotDuration :: Millisecond - -- | Value for 'getNtpStatus' - , mockNodeStateNtpStatus :: NtpStatus + -- | Value for 'getNtpDrift' + , mockNodeStateNtpDrift :: V1.ForceNtpCheck -> V1.TimeInfo } -- | Default 'MockNodeStateParams' @@ -537,7 +564,7 @@ defMockNodeStateParams = , mockNodeStateSlotStart = notDefined "mockNodeStateSlotStart" , mockNodeStateNextEpochSlotDuration = notDefined "mockNodeStateNextEpochSlotDuration" , mockNodeStateSecurityParameter = SecurityParameter 2160 - , mockNodeStateNtpStatus = NtpSyncUnavailable + , mockNodeStateNtpDrift = const (V1.TimeInfo Nothing) } where notDefined :: Text -> a diff --git a/wallet-new/src/Cardano/Wallet/Types/UtxoStatistics.hs b/wallet-new/src/Cardano/Wallet/Types/UtxoStatistics.hs new file mode 100644 index 00000000000..7016febdabb --- /dev/null +++ b/wallet-new/src/Cardano/Wallet/Types/UtxoStatistics.hs @@ -0,0 +1,316 @@ +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE ViewPatterns #-} + +module Cardano.Wallet.Types.UtxoStatistics + ( -- * Types + UtxoStatistics + , BoundType + , UtxoStatisticsError(..) + + -- * Constructing 'UtxoStatistics' + , computeUtxoStatistics + + -- * Constructing 'BoundType' + , log10 + ) where + + +import Universum + +import Control.Lens (at, (?~)) +import Data.Aeson (FromJSON (..), Object, ToJSON (..), Value (..), + genericParseJSON, genericToJSON, object, withObject, (.:), + (.=)) +import Data.Aeson.Types (Parser) +import Data.Swagger (NamedSchema (..), Referenced (..), + SwaggerType (..), ToSchema (..), declareSchemaRef, + genericDeclareNamedSchema, minimum_, properties, required, + type_) +import Data.Word (Word64) +import Formatting (bprint, build, formatToString, (%)) +import Serokell.Util (listJson) +import Test.QuickCheck (Arbitrary (..), arbitrary, choose, elements, + infiniteListOf, shuffle) + +import Cardano.Wallet.API.V1.Swagger.Example (Example) +import Pos.Chain.Txp (Utxo) +import Pos.Core.Common (Coin (..)) +import Pos.Core.Txp (TxOut (..), TxOutAux (..)) +import Pos.Infra.Util.LogSafe (BuildableSafeGen (..), + deriveSafeBuildable) + +import qualified Control.Foldl as L +import qualified Data.Aeson as Aeson +import qualified Data.HashMap.Strict as HMS +import qualified Data.List.NonEmpty as NL +import qualified Data.Map.Strict as Map +import qualified Data.Swagger as Swagger +import qualified Formatting.Buildable + + +-- +-- TYPES +-- + +data UtxoStatistics = UtxoStatistics + { theHistogram :: ![HistogramBar] + , theAllStakes :: !Word64 + } deriving (Show, Generic, Ord) + +data UtxoStatisticsError + = ErrEmptyHistogram + | ErrInvalidBounds !Text + | ErrInvalidTotalStakes !Text + deriving (Eq, Show, Read, Generic) + +-- Buckets boundaries can be constructed in different ways +data BoundType = Log10 deriving (Eq, Show, Read, Generic) + +instance ToJSON BoundType where + toJSON = genericToJSON aesonEnumOpts + +instance FromJSON BoundType where + parseJSON = genericParseJSON aesonEnumOpts + +instance ToSchema BoundType where + declareNamedSchema = genericDeclareNamedSchema Swagger.defaultSchemaOptions + +instance Buildable UtxoStatisticsError where + build = \case + ErrEmptyHistogram -> + bprint "Utxo statistics histogram cannot be empty." + ErrInvalidBounds err -> + bprint ("Utxo statistics have invalid bounds: "%build%".") err + ErrInvalidTotalStakes err -> + bprint ("Utxo statistics have invalid total stakes: "%build%".") err + +instance Eq UtxoStatistics where + (UtxoStatistics h s) == (UtxoStatistics h' s') = + s == s' && sorted h == sorted h' + where + sorted :: [HistogramBar] -> [HistogramBar] + sorted = sortOn (\(HistogramBarCount key _) -> key) + +instance ToJSON UtxoStatistics where + toJSON (UtxoStatistics bars allStakes) = + let + histogramObject = + Object . HMS.fromList . map extractBarKey + + extractBarKey (HistogramBarCount bound stake) = + show bound .= stake + in + object + [ "histogram" .= histogramObject bars + , "allStakes" .= allStakes + , "boundType" .= log10 + ] + +instance FromJSON UtxoStatistics where + parseJSON = withObject "UtxoStatistics" parseUtxoStatistics + where + parseUtxoStatistics :: Object -> Parser UtxoStatistics + parseUtxoStatistics o = + eitherToParser =<< mkUtxoStatistics + <$> (o .: "boundType") + <*> (o .: "histogram") + <*> (o .: "allStakes") + + eitherToParser :: Buildable a => Either a b -> Parser b + eitherToParser = + either (fail . formatToString build) pure + +instance Arbitrary UtxoStatistics where + arbitrary = do + upperBounds <- shuffle (NL.toList $ generateBounds Log10) + counts <- infiniteListOf arbitrary + let histogram = zip upperBounds counts + let histoBars = map (uncurry HistogramBarCount) histogram + allStakes <- choose (getPossibleBounds $ Map.fromList histogram) + return $ UtxoStatistics histoBars allStakes + +instance BuildableSafeGen UtxoStatistics where + buildSafeGen _ UtxoStatistics{..} = bprint ("{" + %" histogram="%build + %" allStakes="%build + %" }") + theHistogram + theAllStakes + +instance Example UtxoStatistics + +instance ToSchema UtxoStatistics where + declareNamedSchema _ = do + wordRef <- declareSchemaRef (Proxy :: Proxy Word64) + btypeRef <- declareSchemaRef (Proxy :: Proxy BoundType) + pure $ NamedSchema (Just "UtxoStatistics") $ mempty + & type_ .~ SwaggerObject + & required .~ ["histogram", "allStakes"] + & properties .~ (mempty + & at "boundType" ?~ btypeRef + & at "allStakes" ?~ (Inline $ mempty + & type_ .~ SwaggerNumber + & minimum_ .~ Just 0 + ) + & at "histogram" ?~ Inline (mempty + & type_ .~ SwaggerObject + & properties .~ (mempty + & at "10" ?~ wordRef + & at "100" ?~ wordRef + & at "1000" ?~ wordRef + & at "10000" ?~ wordRef + & at "100000" ?~ wordRef + & at "1000000" ?~ wordRef + & at "10000000" ?~ wordRef + & at "100000000" ?~ wordRef + & at "1000000000" ?~ wordRef + & at "10000000000" ?~ wordRef + & at "100000000000" ?~ wordRef + & at "1000000000000" ?~ wordRef + & at "10000000000000" ?~ wordRef + & at "100000000000000" ?~ wordRef + & at "1000000000000000" ?~ wordRef + & at "10000000000000000" ?~ wordRef + & at "45000000000000000" ?~ wordRef + ) + ) + ) + +-- +-- CONSTRUCTING +-- + +-- | Smart-constructor to create bounds using a log-10 scale +log10 :: BoundType +log10 = Log10 +{-# INLINE log10 #-} + +-- | Compute UtxoStatistics from a bunch of UTXOs +computeUtxoStatistics :: BoundType -> [Utxo] -> UtxoStatistics +computeUtxoStatistics btype = + L.fold foldStatistics . concatMap getCoins + where + getCoins :: Utxo -> [Word64] + getCoins = + map (getCoin . txOutValue . toaOut) . Map.elems + + foldStatistics :: L.Fold Word64 UtxoStatistics + foldStatistics = UtxoStatistics + <$> foldBuckets (generateBounds btype) + <*> L.sum + + foldBuckets :: NonEmpty Word64 -> L.Fold Word64 [HistogramBar] + foldBuckets bounds = + let + step :: Map Word64 Word64 -> Word64 -> Map Word64 Word64 + step x a = + case Map.lookupGE a x of + Just (k, v) -> Map.insert k (v+1) x + Nothing -> Map.adjust (+1) (head bounds) x + initial :: Map Word64 Word64 + initial = + Map.fromList $ zip (NL.toList bounds) (repeat 0) + extract :: Map Word64 Word64 -> [HistogramBar] + extract = + map (uncurry HistogramBarCount) . Map.toList + in + L.Fold step initial extract + +-- +-- INTERNALS +-- + +-- Utxo statistics for the wallet. +-- Histogram is composed of bars that represent the bucket. The bucket is tagged by upper bound of a given bucket. +-- The bar value corresponds to the number of stakes +-- In the future the bar value could be different things: +-- (a) sum of stakes in a bucket +-- (b) avg or std of stake in a bucket +-- (c) topN buckets +-- to name a few +data HistogramBar = HistogramBarCount + { bucketUpperBound :: !Word64 + , bucketCount :: !Word64 + } deriving (Show, Eq, Ord, Generic) + +instance Example HistogramBar + +instance Arbitrary HistogramBar where + arbitrary = do + upperBound <- elements (NL.toList $ generateBounds log10) + count <- arbitrary + pure (HistogramBarCount upperBound count) + +instance Buildable [HistogramBar] where + build = + bprint listJson + +instance BuildableSafeGen HistogramBar where + buildSafeGen _ HistogramBarCount{..} = + bprint ("{" + %" upperBound="%build + %" count="%build + %" }") + bucketUpperBound + bucketCount + +mkUtxoStatistics + :: BoundType + -> Map Word64 Word64 + -> Word64 + -> Either UtxoStatisticsError UtxoStatistics +mkUtxoStatistics btype histogram allStakes = do + let (histoKeys, histoElems) = (Map.keys histogram, Map.elems histogram) + let acceptedKeys = NL.toList $ generateBounds btype + let (minPossibleValue, maxPossibleValue) = getPossibleBounds histogram + let constructHistogram = uncurry HistogramBarCount + let histoBars = map constructHistogram $ Map.toList histogram + + when (length histoKeys <= 0) $ + Left ErrEmptyHistogram + when (any (`notElem` acceptedKeys) histoKeys) $ + Left $ ErrInvalidBounds $ "given bounds are incompatible with bound type (" <> show btype <> ")" + when (any (< 0) histoElems) $ + Left $ ErrInvalidBounds "encountered negative bound" + when (allStakes < 0) $ + Left $ ErrInvalidTotalStakes "total stakes is negative" + when (allStakes < minPossibleValue && allStakes > maxPossibleValue) $ + Left $ ErrInvalidTotalStakes "inconsistent total stakes & histogram" + + pure UtxoStatistics + { theHistogram = histoBars + , theAllStakes = allStakes + } + +generateBounds :: BoundType -> NonEmpty Word64 +generateBounds bType = + let (^!) :: Word64 -> Word64 -> Word64 + (^!) = (^) + in case bType of + Log10 -> NL.fromList $ map (\toPower -> 10 ^! toPower) [1..16] ++ [45 * (10 ^! 15)] + +getPossibleBounds :: Map Word64 Word64 -> (Word64, Word64) +getPossibleBounds histogram = + (calculatePossibleBound fst, calculatePossibleBound snd) + where + createBracketPairs :: Num a => [a] -> [(a,a)] + createBracketPairs (reverse -> (x:xs)) = zip (map (+1) $ reverse (xs ++ [0])) (reverse (x:xs)) + createBracketPairs _ = [] + matching fromPair (key,value) = + map ( (*value) . fromPair ) . filter (\(_,upper) -> key == upper) + acceptedKeys = NL.toList $ generateBounds log10 + calculatePossibleBound fromPair = + sum . + concatMap (\pair -> matching fromPair pair $ createBracketPairs acceptedKeys) $ + Map.toList histogram + +aesonEnumOpts :: Aeson.Options +aesonEnumOpts = Aeson.defaultOptions + { Aeson.tagSingleConstructors = True + } + + +-- | TH at the end because it needs mostly everything to be declared first +deriveSafeBuildable ''UtxoStatistics +deriveSafeBuildable ''HistogramBar diff --git a/wallet-new/src/Cardano/Wallet/WalletLayer.hs b/wallet-new/src/Cardano/Wallet/WalletLayer.hs index fd3ab7af5c1..cc14544eb2d 100644 --- a/wallet-new/src/Cardano/Wallet/WalletLayer.hs +++ b/wallet-new/src/Cardano/Wallet/WalletLayer.hs @@ -7,6 +7,7 @@ module Cardano.Wallet.WalletLayer , UpdateWalletError(..) , UpdateWalletPasswordError(..) , DeleteWalletError(..) + , GetUtxosError(..) , NewPaymentError(..) , EstimateFeesError(..) , RedeemAdaError(..) @@ -28,6 +29,7 @@ import qualified Prelude import Test.QuickCheck (Arbitrary (..), oneof) import Pos.Chain.Block (Blund) +import Pos.Chain.Txp (Utxo) import Pos.Core (Coin, Timestamp) import Pos.Core.Chrono (NE, NewestFirst (..), OldestFirst (..)) import Pos.Core.Txp (Tx, TxId) @@ -39,8 +41,8 @@ import Cardano.Wallet.API.Request.Filter (FilterOperations (..)) import Cardano.Wallet.API.Request.Sort (SortOperations (..)) import Cardano.Wallet.API.Response (SliceOf (..), WalletResponse) import Cardano.Wallet.API.V1.Types (Account, AccountBalance, - AccountIndex, AccountUpdate, Address, NewAccount, - NewAddress, NewWallet, NodeInfo, NodeSettings, + AccountIndex, AccountUpdate, Address, ForceNtpCheck, + NewAccount, NewAddress, NewWallet, NodeInfo, NodeSettings, PasswordUpdate, Payment, Redemption, Transaction, V1 (..), Wallet, WalletAddress, WalletId, WalletUpdate) import qualified Cardano.Wallet.Kernel.Accounts as Kernel @@ -157,6 +159,25 @@ instance Buildable DeleteWalletError where build (DeleteWalletError kernelError) = bprint ("DeleteWalletError " % build) kernelError +data GetUtxosError = + GetUtxosWalletIdDecodingFailed Text + | GetUtxosGetAccountsError Kernel.UnknownHdRoot + | GetUtxosCurrentAvailableUtxoError Kernel.UnknownHdAccount + deriving Eq + +instance Show GetUtxosError where + show = formatToString build + +instance Exception GetUtxosError + +instance Buildable GetUtxosError where + build (GetUtxosWalletIdDecodingFailed txt) = + bprint ("GetUtxosWalletIdDecodingFailed " % build) txt + build (GetUtxosGetAccountsError kernelError) = + bprint ("GetUtxosGetAccountsError " % build) kernelError + build (GetUtxosCurrentAvailableUtxoError kernelError) = + bprint ("GetUtxosCurrentAvailableUtxoError " % build) kernelError + ------------------------------------------------------------ -- Errors when dealing with addresses ------------------------------------------------------------ @@ -348,6 +369,8 @@ data PassiveWalletLayer m = PassiveWalletLayer -> PasswordUpdate -> m (Either UpdateWalletPasswordError Wallet) , deleteWallet :: WalletId -> m (Either DeleteWalletError ()) + , getUtxos :: WalletId + -> m (Either GetUtxosError [(Account, Utxo)]) -- accounts , createAccount :: WalletId -> NewAccount @@ -441,7 +464,7 @@ data ActiveWalletLayer m = ActiveWalletLayer { -- -- This lives in the active wallet layer as the node info endpoint returns -- status information about the diffusion layer - , getNodeInfo :: m NodeInfo + , getNodeInfo :: ForceNtpCheck -> m NodeInfo } ------------------------------------------------------------ diff --git a/wallet-new/src/Cardano/Wallet/WalletLayer/Kernel.hs b/wallet-new/src/Cardano/Wallet/WalletLayer/Kernel.hs index 9540a5044be..d0743b68389 100644 --- a/wallet-new/src/Cardano/Wallet/WalletLayer/Kernel.hs +++ b/wallet-new/src/Cardano/Wallet/WalletLayer/Kernel.hs @@ -85,9 +85,11 @@ bracketPassiveWallet logFunction keystore rocksDB f = do , resetWalletState = Internal.resetWalletState w , applyBlocks = invokeIO . Actions.ApplyBlocks , rollbackBlocks = invokeIO . Actions.RollbackBlocks . length + -- Read-only operations , getWallets = ro $ Wallets.getWallets , getWallet = \wId -> ro $ Wallets.getWallet wId + , getUtxos = \wId -> ro $ Wallets.getWalletUtxos wId , getAccounts = \wId -> ro $ Accounts.getAccounts wId , getAccount = \wId acc -> ro $ Accounts.getAccount wId acc , getAccountBalance = \wId acc -> ro $ Accounts.getAccountBalance wId acc @@ -106,6 +108,7 @@ bracketPassiveWallet logFunction keystore rocksDB f = do invokeIO :: forall m'. MonadIO m' => Actions.WalletAction Blund -> m' () invokeIO = liftIO . STM.atomically . invoke + -- The use of the unsafe constructor 'UnsafeRawResolvedBlock' is justified -- by the invariants established in the 'Blund'. blundToResolvedBlock :: (Core.SlotId -> IO Core.Timestamp) -> Blund -> IO (Maybe ResolvedBlock) diff --git a/wallet-new/src/Cardano/Wallet/WalletLayer/Kernel/Info.hs b/wallet-new/src/Cardano/Wallet/WalletLayer/Kernel/Info.hs index 31f7d6f2651..67a2151952a 100644 --- a/wallet-new/src/Cardano/Wallet/WalletLayer/Kernel/Info.hs +++ b/wallet-new/src/Cardano/Wallet/WalletLayer/Kernel/Info.hs @@ -4,32 +4,21 @@ module Cardano.Wallet.WalletLayer.Kernel.Info ( import Universum -import Data.Time.Units (toMicroseconds) -import Ntp.Client (NtpStatus (..)) - import qualified Cardano.Wallet.API.V1.Types as V1 import Cardano.Wallet.Kernel.Diffusion (walletGetSubscriptionStatus) import qualified Cardano.Wallet.Kernel.Internal as Kernel import Cardano.Wallet.Kernel.NodeStateAdaptor (NodeStateAdaptor) import qualified Cardano.Wallet.Kernel.NodeStateAdaptor as Node -getNodeInfo :: MonadIO m => Kernel.ActiveWallet -> m V1.NodeInfo -getNodeInfo aw = liftIO $ do +getNodeInfo :: MonadIO m => Kernel.ActiveWallet -> V1.ForceNtpCheck -> m V1.NodeInfo +getNodeInfo aw ntpCheckBehavior = liftIO $ V1.NodeInfo <$> (pure $ V1.mkSyncPercentage 100) -- TODO (Restoration [CBR-243]) <*> (pure $ Nothing) -- TODO (Restoration [CBR-243]) <*> (pure $ V1.mkBlockchainHeight 0) -- TODO (Restoration [CBR-243]) - <*> (mkTimeInfo <$> Node.getNtpStatus node) + <*> (Node.getNtpDrift node ntpCheckBehavior) <*> (walletGetSubscriptionStatus (Kernel.walletDiffusion aw)) where - mkTimeInfo :: NtpStatus -> V1.TimeInfo - mkTimeInfo = V1.TimeInfo . fmap V1.mkLocalTimeDifference . diff - - diff :: NtpStatus -> Maybe Integer - diff (NtpDrift time) = Just (toMicroseconds time) - diff NtpSyncPending = Nothing - diff NtpSyncUnavailable = Nothing - node :: NodeStateAdaptor IO node = pw ^. Kernel.walletNode diff --git a/wallet-new/src/Cardano/Wallet/WalletLayer/Kernel/Wallets.hs b/wallet-new/src/Cardano/Wallet/WalletLayer/Kernel/Wallets.hs index 2a5322b0478..a5f16f7f77e 100644 --- a/wallet-new/src/Cardano/Wallet/WalletLayer/Kernel/Wallets.hs +++ b/wallet-new/src/Cardano/Wallet/WalletLayer/Kernel/Wallets.hs @@ -5,12 +5,14 @@ module Cardano.Wallet.WalletLayer.Kernel.Wallets ( , deleteWallet , getWallet , getWallets + , getWalletUtxos ) where import Universum import Data.Coerce (coerce) +import Pos.Chain.Txp (Utxo) import Pos.Core (mkCoin) import Pos.Core.Slotting (Timestamp) import Pos.Crypto.Signing @@ -29,8 +31,9 @@ import Cardano.Wallet.Kernel.Types (WalletId (..)) import Cardano.Wallet.Kernel.Util.Core (getCurrentTimestamp) import qualified Cardano.Wallet.Kernel.Wallets as Kernel import Cardano.Wallet.WalletLayer (CreateWalletError (..), - DeleteWalletError (..), GetWalletError (..), - UpdateWalletError (..), UpdateWalletPasswordError (..)) + DeleteWalletError (..), GetUtxosError (..), + GetWalletError (..), UpdateWalletError (..), + UpdateWalletPasswordError (..)) import Cardano.Wallet.WalletLayer.Kernel.Conv createWallet :: MonadIO m @@ -148,3 +151,23 @@ getWallets :: Kernel.DB -> IxSet V1.Wallet getWallets db = IxSet.fromList . map (toWallet db) . IxSet.toList $ allRoots where allRoots = db ^. dbHdWallets . HD.hdWalletsRoots + +-- | Gets Utxos per account of a wallet. +getWalletUtxos + :: V1.WalletId + -> Kernel.DB + -> Either GetUtxosError [(V1.Account, Utxo)] +getWalletUtxos wId db = runExcept $ do + rootId <- withExceptT GetUtxosWalletIdDecodingFailed $ + fromRootId wId + + withExceptT GetUtxosGetAccountsError $ exceptT $ do + _rootExists <- Kernel.lookupHdRootId db rootId + return () + + let accounts = Kernel.accountsByRootId db rootId + + forM (IxSet.toList accounts) $ \account -> + withExceptT GetUtxosCurrentAvailableUtxoError $ exceptT $ do + utxo <- Kernel.currentAvailableUtxo db (account ^. HD.hdAccountId) + return (toAccount db account, utxo) diff --git a/wallet-new/test/Cardano/Wallet/WalletLayer/QuickCheck.hs b/wallet-new/test/Cardano/Wallet/WalletLayer/QuickCheck.hs index 02fd3e9ff2a..1bbb288f1e1 100644 --- a/wallet-new/test/Cardano/Wallet/WalletLayer/QuickCheck.hs +++ b/wallet-new/test/Cardano/Wallet/WalletLayer/QuickCheck.hs @@ -13,15 +13,17 @@ import Cardano.Wallet.Orphans.Arbitrary () import Cardano.Wallet.WalletLayer (ActiveWalletLayer (..), CreateAccountError (..), DeleteAccountError (..), DeleteWalletError (..), GetAccountError (..), - GetAccountsError (..), GetWalletError (..), - PassiveWalletLayer (..), UpdateAccountError (..), - UpdateWalletError (..), UpdateWalletPasswordError (..), - ValidateAddressError (..)) + GetAccountsError (..), GetUtxosError (..), + GetWalletError (..), PassiveWalletLayer (..), + UpdateAccountError (..), UpdateWalletError (..), + UpdateWalletPasswordError (..), ValidateAddressError (..)) import Cardano.Wallet.API.V1.Types (V1 (..)) import Pos.Core () -import Test.QuickCheck (Arbitrary, arbitrary, generate, oneof) +import Test.Pos.Core.Arbitrary.Txp () +import Test.QuickCheck (Arbitrary (..), arbitrary, generate, oneof) + -- | Initialize the passive wallet. -- The passive wallet cannot send new transactions. @@ -41,6 +43,7 @@ bracketPassiveWallet = , updateWallet = \_ _ -> liftedGen , updateWalletPassword = \_ _ -> liftedGen , deleteWallet = \_ -> liftedGen + , getUtxos = \_ -> liftedGen , createAccount = \_ _ -> liftedGen , getAccounts = \_ -> liftedGen @@ -132,6 +135,12 @@ instance Arbitrary GetWalletError where , GetWalletError . V1 <$> arbitrary ] +instance Arbitrary GetUtxosError where + arbitrary = oneof [ pure (GetUtxosWalletIdDecodingFailed "foobar") + , GetUtxosGetAccountsError <$> arbitrary + , GetUtxosCurrentAvailableUtxoError <$> arbitrary + ] + instance Arbitrary UpdateWalletPasswordError where arbitrary = oneof [ UpdateWalletPasswordError <$> arbitrary , UpdateWalletPasswordWalletIdDecodingFailed <$> arbitrary diff --git a/wallet/cardano-sl-wallet.cabal b/wallet/cardano-sl-wallet.cabal index 8e4c4af08e1..e8aaeebb7a2 100644 --- a/wallet/cardano-sl-wallet.cabal +++ b/wallet/cardano-sl-wallet.cabal @@ -158,7 +158,6 @@ library , stm , swagger2 , text - , formatting , time , time-units , transformers @@ -236,8 +235,6 @@ test-suite cardano-wallet-test type: exitcode-stdio-1.0 build-depends: base - , MonadRandom - , QuickCheck , aeson , bytestring , cardano-crypto @@ -247,15 +244,18 @@ test-suite cardano-wallet-test , cardano-sl-core , cardano-sl-core-test , cardano-sl-crypto - , cardano-sl-crypto-test , cardano-sl-db , cardano-sl-generator , cardano-sl-infra , cardano-sl-util , cardano-sl-util-test , cardano-sl-wallet + , cardano-sl-crypto-test , containers + , safe-exceptions , data-default + , servant + , servant-server , deepseq , ekg-core , ether @@ -263,13 +263,13 @@ test-suite cardano-wallet-test , hspec , lens , log-warper + , MonadRandom , mtl - , safe-exceptions + , pvss + , QuickCheck , safecopy - , serokell-util >= 0.1.3.4 - , servant-server + , serokell-util , stm - , formatting , universum >= 0.1.11 , unordered-containers diff --git a/wallet/src/Pos/Util/Mnemonic.hs b/wallet/src/Pos/Util/Mnemonic.hs index 047236d82da..ba7e83d74bd 100644 --- a/wallet/src/Pos/Util/Mnemonic.hs +++ b/wallet/src/Pos/Util/Mnemonic.hs @@ -12,12 +12,15 @@ module Pos.Util.Mnemonic , MnemonicWords -- * Errors - , MnemonicErr(..) + , MnemonicError(..) , MnemonicException(..) + -- ** Re-exports from 'cardano-crypto' + , EntropyError(..) + , DictionaryError(..) + , MnemonicWordsError(..) -- * Creating @Mnemonic@ (resp. @Entropy@) , mkEntropy - , eitherToParser , mkMnemonic , genEntropy @@ -27,14 +30,17 @@ module Pos.Util.Mnemonic , mnemonicToAesKey , entropyToMnemonic , entropyToByteString + + -- * Helper (FIXME: Move to a separated module) + , eitherToParser ) where import Universum import Basement.Sized.List (unListN) +import Control.Arrow (left) import Control.Lens ((?~)) import Crypto.Encoding.BIP39 -import Crypto.Encoding.BIP39.Dictionary (mnemonicSentenceToListN) import Crypto.Hash (Blake2b_256, Digest, hash) import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Aeson.Types (Parser) @@ -75,14 +81,15 @@ data Mnemonic (mw :: Nat) = Mnemonic -- ERRORS -- -data MnemonicException = UnexpectedMnemonicErr MnemonicErr +data MnemonicException csz = UnexpectedEntropyError (EntropyError csz) deriving (Show, Typeable) -data MnemonicErr - = MnemonicErrInvalidEntropyLength Int - | MnemonicErrFailedToCreate - | MnemonicErrForbiddenMnemonic +data MnemonicError csz + = ErrMnemonicWords MnemonicWordsError + | ErrEntropy (EntropyError csz) + | ErrDictionary DictionaryError + | ErrForbidden deriving (Show) @@ -94,12 +101,8 @@ data MnemonicErr mkEntropy :: forall n csz. (ValidEntropySize n, ValidChecksumSize n csz) => ByteString - -> Either MnemonicErr (Entropy n) -mkEntropy = - let - n = fromIntegral $ natVal (Proxy @n) - in - maybe (Left $ MnemonicErrInvalidEntropyLength n) Right . toEntropy @n + -> Either (EntropyError csz) (Entropy n) +mkEntropy = toEntropy -- | Generate Entropy of a given size using a random seed. @@ -115,7 +118,7 @@ genEntropy = size = fromIntegral $ natVal (Proxy @n) eitherToIO = - either (throwM . UnexpectedMnemonicErr) return + either (throwM . UnexpectedEntropyError) return in (eitherToIO . mkEntropy) =<< Crypto.getEntropy (size `div` 8) @@ -127,22 +130,20 @@ mkMnemonic , EntropySize mw ~ n ) => [Text] - -> Either MnemonicErr (Mnemonic mw) + -> Either (MnemonicError csz) (Mnemonic mw) mkMnemonic wordsm = do - sentence <- maybe - (Left MnemonicErrFailedToCreate) - (Right . mnemonicPhraseToMnemonicSentence Dictionary.english) - (mnemonicPhrase @mw (toUtf8String <$> wordsm)) + phrase <- left ErrMnemonicWords + $ mnemonicPhrase @mw (toUtf8String <$> wordsm) + + sentence <- left ErrDictionary + $ mnemonicPhraseToMnemonicSentence Dictionary.english phrase - entropy <- maybe - (Left MnemonicErrFailedToCreate) - Right - (wordsToEntropy sentence :: Maybe (Entropy n)) + entropy <- left ErrEntropy + $ wordsToEntropy sentence - when (isForbiddenMnemonic sentence) $ - Left MnemonicErrForbiddenMnemonic + when (isForbiddenMnemonic sentence) $ Left ErrForbidden - pure $ Mnemonic + pure Mnemonic { mnemonicToEntropy = entropy , mnemonicToSentence = sentence } @@ -260,7 +261,7 @@ instance size = fromIntegral $ natVal (Proxy @n) entropy = mkEntropy @n . B8.pack <$> vectorOf (size `quot` 8) arbitrary in - either (error . show . UnexpectedMnemonicErr) identity <$> entropy + either (error . show . UnexpectedEntropyError) identity <$> entropy -- Same remark from 'Arbitrary Entropy' applies here. @@ -276,7 +277,7 @@ instance entropyToMnemonic <$> arbitrary @(Entropy n) -instance Exception MnemonicException +instance (KnownNat csz) => Exception (MnemonicException csz) -- FIXME: Suggestion, we could -- when certain flags are turned on -- display @@ -294,15 +295,22 @@ instance Buildable (SecureLog (Mnemonic mw)) where build _ = "" -instance Buildable MnemonicErr where +instance Buildable (MnemonicError csz) where build = \case - MnemonicErrInvalidEntropyLength l -> - bprint ("Entropy must be a sequence of " % build % " bytes") l - MnemonicErrFailedToCreate -> - bprint "Invalid Mnemonic words" - MnemonicErrForbiddenMnemonic -> + ErrMnemonicWords (ErrWrongNumberOfWords a e) -> + bprint ("MnemonicError: Invalid number of mnemonic words: got "%build%" words, expected "%build%" words") a e + ErrDictionary (ErrInvalidDictionaryWord w) -> + bprint ("MnemonicError: Invalid dictionary word: "%build%"") (fromUtf8String w) + ErrEntropy (ErrInvalidEntropyLength a e) -> + bprint ("MnemonicError: Invalid entropy length: got "%build%" bits, expected "%build%" bits") a e + ErrEntropy (ErrInvalidEntropyChecksum a e) -> + bprint ("MnemonicError: Invalid entropy checksum: got "%build%", expected "%build) (show' a) (show' e) + ErrForbidden -> bprint "Forbidden Mnemonic: an example Mnemonic has been submitted. \ \Please generate a fresh and private Mnemonic from a trusted source" + where + show' :: Checksum csz -> String + show' = show -- | To use everytime we need to show an example of a Mnemonic. This particular @@ -325,13 +333,13 @@ instance Default (Mnemonic 12) where , "flee" ] - sentence = maybe - (error $ show $ UnexpectedMnemonicErr MnemonicErrFailedToCreate) - (mnemonicPhraseToMnemonicSentence Dictionary.english) + phrase = either (error . show) id (mnemonicPhrase @12 (toUtf8String <$> wordsm)) - entropy = fromMaybe - (error $ show $ UnexpectedMnemonicErr MnemonicErrFailedToCreate) + sentence = either (error . show) id + (mnemonicPhraseToMnemonicSentence Dictionary.english phrase) + + entropy = either (error . show) id (wordsToEntropy @(EntropySize 12) sentence) in Mnemonic { mnemonicToSentence = sentence diff --git a/wallet/src/Pos/Wallet/Web/Methods/Misc.hs b/wallet/src/Pos/Wallet/Web/Methods/Misc.hs index 66e3b83f0fb..a0eaf132c95 100644 --- a/wallet/src/Pos/Wallet/Web/Methods/Misc.hs +++ b/wallet/src/Pos/Wallet/Web/Methods/Misc.hs @@ -15,6 +15,7 @@ module Pos.Wallet.Web.Methods.Misc , syncProgress , localTimeDifference + , localTimeDifferencePure , requestShutdown @@ -170,14 +171,13 @@ syncProgress = do -- NTP (Network Time Protocol) based time difference ---------------------------------------------------------------------------- +localTimeDifferencePure :: NtpStatus -> Maybe Integer +localTimeDifferencePure (NtpDrift time) = Just (toMicroseconds time) +localTimeDifferencePure NtpSyncPending = Nothing +localTimeDifferencePure NtpSyncUnavailable = Nothing + localTimeDifference :: MonadIO m => TVar NtpStatus -> m (Maybe Integer) -localTimeDifference ntpStatus = diff <$> readTVarIO ntpStatus - where - diff :: NtpStatus -> Maybe Integer - diff = \case - NtpDrift time -> Just (toMicroseconds time) - NtpSyncPending -> Nothing - NtpSyncUnavailable -> Nothing +localTimeDifference ntpStatus = localTimeDifferencePure <$> (atomically $ readTVar ntpStatus) ---------------------------------------------------------------------------- -- Reset diff --git a/wallet/test/Test/Pos/Util/MnemonicSpec.hs b/wallet/test/Test/Pos/Util/MnemonicSpec.hs index 6b43a211f64..179b87e68c5 100644 --- a/wallet/test/Test/Pos/Util/MnemonicSpec.hs +++ b/wallet/test/Test/Pos/Util/MnemonicSpec.hs @@ -138,10 +138,10 @@ spec = do ) ] where + orFail :: Show e => Either e a -> a orFail = either (error . (<>) "Failed to create golden Mnemonic: " . show) identity - mkEntropy' = - maybe (Left MnemonicErrFailedToCreate) Right . toEntropy @128 @4 @ByteString + mkEntropy' = toEntropy @128 @4 @ByteString -- | V0 Mnemonics are wrapped in a singleton object with a `bpToList` prop jsonV0Compat :: BL.ByteString -> BL.ByteString