Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V2 migration #655

Open
wants to merge 16 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions common/src/Common/Network.hs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import Data.CaseInsensitive (CI)
import Data.Coerce (coerce)
import Data.Map (Map)
import Data.Maybe (fromMaybe)
import Data.String
import Data.Text (Text)
import Data.Void (Void)
import GHC.Generics (Generic)
Expand Down Expand Up @@ -69,6 +70,8 @@ instance ToJSON NetworkName where
toJSON = toJSON . CI.original . unNetworkName
instance ToJSONKey NetworkName where
toJSONKey = A.toJSONKeyText (CI.original . unNetworkName)
instance IsString NetworkName where
fromString = mkNetworkName . fromString

mkNetworkName :: Text -> NetworkName
mkNetworkName = NetworkName . CI.mk . T.strip
Expand Down
1 change: 1 addition & 0 deletions frontend/frontend.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ library
, Frontend.VersionedStore.V0
, Frontend.VersionedStore.V0.Wallet
, Frontend.VersionedStore.V1
, Frontend.VersionedStore.V2
, Frontend.Wallet
, Reflex.Class.Extended
, Reflex.Dom.ACE.Extended
Expand Down
32 changes: 24 additions & 8 deletions frontend/src/Frontend/VersionedStore.hs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
module Frontend.VersionedStore
( module V1
( module V2
, VersionedStorage(..)
, StorageVersion
, VersioningDecodeJsonError(..)
Expand All @@ -10,7 +10,7 @@ module Frontend.VersionedStore
, versionedFrontend
) where

import Control.Monad.Except (ExceptT, runExceptT, throwError)
import Control.Monad.Except (ExceptT(..), runExceptT, throwError)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.Trans (lift)
import Control.Error ((!?), hoistEither)
Expand All @@ -34,7 +34,8 @@ import Frontend.Storage.Class
import qualified Frontend.Storage.Class as Storage
import qualified Frontend.VersionedStore.V0 as V0
import qualified Frontend.VersionedStore.V1 as V1
import Frontend.VersionedStore.V1 as Latest
import qualified Frontend.VersionedStore.V2 as V2
import Frontend.VersionedStore.V2 as Latest
import Frontend.Crypto.Class

import Pact.Server.ApiClient (HasTransactionLogger)
Expand All @@ -55,15 +56,18 @@ data VersioningDecodeJsonError
data StoreFrontendVersion key k where
StoreFrontendVersion_0 :: StoreFrontendVersion key (V0.StoreFrontend key)
StoreFrontendVersion_1 :: StoreFrontendVersion key (V1.StoreFrontend key)
StoreFrontendVersion_2 :: StoreFrontendVersion key (V2.StoreFrontend key)

parseVersion :: forall key. StorageVersion -> Maybe (DSum (StoreFrontendVersion key) Proxy)
parseVersion 0 = Just $ StoreFrontendVersion_0 :=> (Proxy @(V0.StoreFrontend key))
parseVersion 1 = Just $ StoreFrontendVersion_1 :=> (Proxy @(V1.StoreFrontend key))
parseVersion 2 = Just $ StoreFrontendVersion_2 :=> (Proxy @(V2.StoreFrontend key))
parseVersion _ = Nothing

_nextVersion :: Some (StoreFrontendVersion key) -> Maybe (Some (StoreFrontendVersion key))
_nextVersion (Some StoreFrontendVersion_0) = Just (Some StoreFrontendVersion_1)
_nextVersion (Some StoreFrontendVersion_1) = Nothing
_nextVersion (Some StoreFrontendVersion_1) = Just (Some StoreFrontendVersion_2)
_nextVersion (Some StoreFrontendVersion_2) = Nothing

versionedFrontend
:: forall t m key
Expand Down Expand Up @@ -122,7 +126,7 @@ versionedStorage = VersionedStorage
prefix = StoreKeyMetaPrefix "StoreFrontend_Meta"

restoreBackup :: DMap (Latest.StoreFrontend key) Identity -> m ()
restoreBackup dm = restoreLocalStorageDump prefix dm 1
restoreBackup dm = restoreLocalStorageDump prefix dm 2

-- Takes a json blob and upgrades it to the latest DMap structure
decodeVersionedJson
Expand All @@ -133,8 +137,11 @@ versionedStorage = VersionedStorage
Nothing -> throwError $ VersioningDecodeJsonError_UnknownVersion ver
Just (StoreFrontendVersion_0 :=> p) -> do
v0map <- decodeDMap p jval
lift $ V1.upgradeFromV0 v0map
lift $ V2.upgradeFromV0 v0map
Just (StoreFrontendVersion_1 :=> p) -> do
v1map <- decodeDMap p jval
lift $ V2.upgradeFromV1 v1map
Just (StoreFrontendVersion_2 :=> p) -> do
decodeDMap p jval

decodeDMap
Expand All @@ -157,7 +164,16 @@ versionedStorage = VersionedStorage
removeKeyUniverse p localStorage
removeKeyUniverse p sessionStorage
restoreLocalStorageDump prefix v1Dump 1
-- Complete the whole upgrade process, then move the logs (TODO: Is this necessary now?)
liftIO $ Api._transactionLogger_rotateLogFile txLogger
ExceptT $ upgradeStorage txLogger
Just (StoreFrontendVersion_1 :=> p) -> do
dump <- (backupLocalStorage prefix p ver) !? VersioningUpgradeError_CouldNotBackup ver
lift $ do
v2Dump <- V2.upgradeFromV1 dump
removeKeyUniverse p localStorage
removeKeyUniverse p sessionStorage
restoreLocalStorageDump prefix v2Dump 2
-- Complete the whole upgrade process, then move the logs
liftIO $ Api._transactionLogger_rotateLogFile txLogger
pure ()
Just (StoreFrontendVersion_1 :=> _) -> pure ()
Just (StoreFrontendVersion_2 :=> _) -> pure ()
Copy link
Contributor

Choose a reason for hiding this comment

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

Reminder to fix this

241 changes: 241 additions & 0 deletions frontend/src/Frontend/VersionedStore/V2.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
{-# LANGUAGE GADTs #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE ViewPatterns #-}
module Frontend.VersionedStore.V2 where

import Data.Aeson
import Data.Aeson.GADT.TH
import Data.Constraint (Dict(Dict))
import Data.Constraint.Extras
import Data.Dependent.Map (DMap)
import Data.Dependent.Sum (DSum(..))
import qualified Data.Dependent.Map as DMap
import Data.Function ((&), on)
import Data.Functor ((<&>))
import Data.Functor.Identity (Identity(Identity), runIdentity)
import qualified Data.IntMap as IntMap
import Data.Map (Map)
import qualified Data.Map as Map
import qualified Data.Map.Merge.Lazy as Map
import Data.Maybe (catMaybes)
import Data.Text (Text)
import Text.Printf (printf)

import Common.Foundation
import Common.Wallet
import Common.Network (NetworkName, NodeRef, parseNodeRef)
import Common.OAuth (OAuthProvider(..))
import Common.GistStore (GistMeta)

import Frontend.VersionedStore.TH
import qualified Frontend.VersionedStore.V0 as V0
import qualified Frontend.VersionedStore.V1 as V1
import qualified Frontend.VersionedStore.V0.Wallet as V0
import Frontend.VersionedStore.MigrationUtils
import Frontend.Crypto.Class

-- WARNING: Upstream deps. Check this when we bump pact and obelisk!
-- May be worth storing this in upstream independent datatypes.
import Pact.Types.ChainId (ChainId(ChainId))
import Pact.Types.ChainMeta (PublicMeta (..))
import Obelisk.OAuth.Common (AccessToken, OAuthState)

data StoreFrontend key a where
StoreFrontend_Wallet_Keys :: StoreFrontend key (KeyStorage key)
StoreFrontend_Wallet_Accounts :: StoreFrontend key AccountStorage

StoreFrontend_Network_PublicMeta :: StoreFrontend key PublicMeta
StoreFrontend_Network_Networks :: StoreFrontend key (Map NetworkName [NodeRef])
StoreFrontend_Network_SelectedNetwork :: StoreFrontend key NetworkName

StoreFrontend_OAuth_Tokens :: StoreFrontend key (Map OAuthProvider AccessToken)
StoreFrontend_OAuth_State :: OAuthProvider -> StoreFrontend key OAuthState

StoreFrontend_Gist_GistRequested :: StoreFrontend key (GistMeta, Text)

StoreFrontend_ModuleExplorer_SessionFile :: StoreFrontend key Text

deriving instance Show (StoreFrontend key a)

upgradeFromV0 :: (Monad m, HasCrypto key m) => DMap (V0.StoreFrontend key) Identity -> m (DMap (StoreFrontend key) Identity)
Copy link
Contributor

Choose a reason for hiding this comment

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

Might it make more sense to just compose V1.upgradeFromV0 and V2.upgradeFromV1 so that we aren't stuck copying code over for each migration and having to make sure that we have all of the "previous stuff" from the intermediary migration as well as all of the new migration stuff?

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmmmm, interesting point. I think we probably should compose the migrations otherwise the logical conclusion just won't be scalable. That's typically the way I've done it with DB migrations.

upgradeFromV0 v0 = do
(newKeysList, newAccountStorage) <- foldMapM splitOldKey oldKeysList
Copy link
Contributor

Choose a reason for hiding this comment

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

splitOldKey really concerns me, for the web version. Its relying on the keys' index in a list, and then using that index to "regenerate" them instead of saving /copying them somewhere. This code is so easy to accidentally copy over, for, say the next migration. We should either rewrite it or at the very least use CPP to make it a no-op on ghcjs. Not quite sure its in the scope of this task, but it could so easily go really bad really quickly if it isn't taken care of imo

let newKeys = IntMap.fromList newKeysList
pure $ DMap.fromList . catMaybes $
[ copyKeyDSum V0.StoreNetwork_PublicMeta StoreFrontend_Network_PublicMeta v0
, copyKeyDSum V0.StoreNetwork_SelectedNetwork StoreFrontend_Network_SelectedNetwork v0
-- Technically these are session only and shouldn't be here given the backup restore only works on
-- local storage, but desktop ignores the session vs local distinction so migrating them probably
-- does some good and certainly doesn't hurt.
-- Also, this is currently being very lazy not leaning on the Universe instance of OAuthProvider
, copyKeyDSum V0.StoreOAuth_Tokens StoreFrontend_OAuth_Tokens v0
, copyKeyDSum (V0.StoreOAuth_State OAuthProvider_GitHub) (StoreFrontend_OAuth_State OAuthProvider_GitHub) v0

, copyKeyDSum V0.StoreModuleExplorer_SessionFile StoreFrontend_ModuleExplorer_SessionFile v0

, Just (StoreFrontend_Wallet_Keys :=> Identity newKeys)
, Just (StoreFrontend_Wallet_Accounts :=> Identity newAccountStorage)
, newNetworks
]
where
oldKeysList = maybe [] (IntMap.toList . runIdentity) (DMap.lookup V0.StoreWallet_Keys v0)

-- We have to walk through the slightly different encoding of the Network information.
-- Also if the storage contains _no_ network configuration then we shouldn't break the new version
-- by storing an empty object.
newNetworks = (\nets -> StoreFrontend_Network_Networks :=> Identity (convertNodeRefs $ V0.unNetworkMap $ runIdentity nets))
<$> DMap.lookup V0.StoreNetwork_Networks v0

-- This will regenerate the missing key. Desktop will recover the key with
Copy link
Contributor

@jmininger jmininger Jun 4, 2021

Choose a reason for hiding this comment

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

This is something we really need to look out for with the upcoming chainweaver changes. Might be ok in the immediate future, but we can't copy the same code over for a V2 --> V3 if this ever happens in the future, because then we would risk wiping out users' keys

-- BIP, but the web version will generate a new key!
splitOldKey (keyIdx, V0.SomeAccount_Deleted) = do
(private, public) <- cryptoGenKey keyIdx
let regenerated = KeyPair
{ _keyPair_publicKey = public
, _keyPair_privateKey = Just private
}
pure ([(keyIdx, Key regenerated)], mempty)

splitOldKey (keyIdx, V0.SomeAccount_Account a) = pure
([(keyIdx, Key (extractKey a))]
, oldAccountToNewStorage a
)

oldAccountToNewStorage :: V0.Account key -> AccountStorage
oldAccountToNewStorage a =
let
accountNameText = V0.unAccountName . V0._account_name $ a
chainIdText = V0.unChainId . V0._account_chainId $ a
newChainId = ChainId chainIdText
accountNotesText = V0.unAccountNotes . V0._account_notes $ a
newAccountNotes = mkAccountNotes accountNotesText
newUnfinishedXChain = V0._account_unfinishedCrossChainTransfer a

accounts = Map.singleton (AccountName accountNameText) $ AccountInfo Nothing
$ Map.singleton newChainId $ VanityAccount newAccountNotes newUnfinishedXChain

in AccountStorage $ Map.singleton (V0._account_network a) accounts

upgradePublicKey = PublicKey . V0.unPublicKey

extractKey (V0.Account { V0._account_key = kp } ) = KeyPair
-- This relies on the V0.Wallet.PublicKey FromJSON checking that it is Base16!
{ _keyPair_publicKey = upgradePublicKey $ V0._keyPair_publicKey kp
, _keyPair_privateKey = V0._keyPair_privateKey kp
}

upgradeFromV1 :: (Monad m, HasCrypto key m) => DMap (V1.StoreFrontend key) Identity -> m (DMap (StoreFrontend key) Identity)
upgradeFromV1 v1 =
pure $ DMap.fromList . catMaybes $
[
copyKeyDSum V1.StoreFrontend_Network_PublicMeta StoreFrontend_Network_PublicMeta v1
, copyKeyDSum V1.StoreFrontend_Network_SelectedNetwork StoreFrontend_Network_SelectedNetwork v1
, copyKeyDSum V1.StoreFrontend_OAuth_Tokens StoreFrontend_OAuth_Tokens v1
, copyKeyDSum (V1.StoreFrontend_OAuth_State OAuthProvider_GitHub) (StoreFrontend_OAuth_State OAuthProvider_GitHub) v1
, copyKeyDSum V1.StoreFrontend_Wallet_Keys StoreFrontend_Wallet_Keys v1
, copyKeyDSum V1.StoreFrontend_Wallet_Accounts StoreFrontend_Wallet_Accounts v1
, copyKeyDSum V1.StoreFrontend_ModuleExplorer_SessionFile StoreFrontend_ModuleExplorer_SessionFile v1
, newNetworks
]
where
newNetworks = DMap.lookup V1.StoreFrontend_Network_Networks v1
<&> \nets -> StoreFrontend_Network_Networks :=> convertNodeRefs <$> nets

toMultiSet :: Ord a => [a] -> Map a Int
toMultiSet = Map.fromListWith (+) . flip zip (repeat 1)

fromMultiSet :: Ord a => Map a Int -> [a]
fromMultiSet = ($ []) . Map.foldrWithKey (\k i -> (.) (dlrep k i)) id
where
dlrep v n
| n < 0 = error "fromMultiSet: IMPOSSIBLE"
| n == 0 = id
| otherwise = (v:) . dlrep v (n - 1)

-- It is equivalent to "These () a" except that we take out the
-- "This" constructor. With it gone, we still get the same applicative
-- operations from "These" (sans "This") but we don't have to account for
-- the "This" constructor in situations where we know it is impossible to
-- produce it. This is also morally equivalent to Either a a with a
-- different applicative instance
newtype Deez a = Deez {getDeez :: Either a a}

instance Functor Deez where
fmap f (Deez (Right a)) = Deez (Right $ f a)
fmap f (Deez (Left a)) = Deez (Left $ f a)

instance Applicative Deez where
pure = Deez . Right
Deez (Right f) <*> Deez (Right x) = Deez (Right $ f x)
Deez (Right f) <*> Deez (Left x) = Deez (Left $ f x)
Deez (Left f) <*> Deez (Right x) = Deez (Left $ f x)
Deez (Left f) <*> Deez (Left x) = Deez (Left $ f x)

convertNodeRefs :: Map NetworkName [NodeRef] -> Map NetworkName [NodeRef]
convertNodeRefs = fmap migrate
where
Copy link
Contributor

Choose a reason for hiding this comment

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

Is the nested where necessary ?

migrate = replaceRefsWith "api.chainweb.com" mainnetNodeRefs . replaceRefsWith "api.testnet.chainweb.com" testnetNodeRefs
replaceRefsWith ref baseRefs refs =
refs
& on (Map.mergeA Map.dropMissing Map.preserveMissing (Map.zipWithMaybeAMatched (\_ _ _ -> Deez $ Left Nothing))) toMultiSet baseRefs
& getDeez
& \case
Right m -> m
Left m -> addRef ref m -- if we hit this case, there were matching keys
& fromMultiSet
addRef (unsafeParseNodeRef -> ref) = Map.insert ref 1
testnetNodeRefs = unsafeParseNodeRef <$>
[ "us1.testnet.chainweb.com"
, "us2.testnet.chainweb.com"
, "eu1.testnet.chainweb.com"
, "eu2.testnet.chainweb.com"
, "ap1.testnet.chainweb.com"
, "ap2.testnet.chainweb.com"]
mainnetNodeRefs = unsafeParseNodeRef <$>
[ "us-e1.chainweb.com"
, "us-e2.chainweb.com"
, "us-w1.chainweb.com"
, "us-w2.chainweb.com"
, "jp1.chainweb.com"
, "jp2.chainweb.com"
, "fr1.chainweb.com"
, "fr2.chainweb.com"]

unsafeParseNodeRef :: Text -> NodeRef
unsafeParseNodeRef = either (error . printf "unsafeParseNodeRef: %s") id . parseNodeRef

-- The TH doesn't deal with the key type param well because the key in each constructor is actually a
-- different type variable to the one in the data decl.
--
-- src/Frontend.VersionedStore/V0.hs:69:1-29: error:
-- The exact Name ‘key_a2Kfr’ is not in scope
-- Probable cause: you used a unique Template Haskell name (NameU),
-- perhaps via newName, but did not bind it
-- If that's it, then -ddump-splices might be useful

instance ArgDict c (StoreFrontend key) where
type ConstraintsFor (StoreFrontend key) c
= ( c (KeyStorage key)
, c AccountStorage
, c PublicMeta
, c (Map NetworkName [NodeRef])
, c NetworkName
, c (Map OAuthProvider AccessToken)
, c OAuthState
, c (GistMeta, Text)
, c Text
)
argDict = \case
StoreFrontend_Wallet_Keys {} -> Dict
StoreFrontend_Wallet_Accounts {} -> Dict
StoreFrontend_Network_PublicMeta {} -> Dict
StoreFrontend_Network_Networks {} -> Dict
StoreFrontend_Network_SelectedNetwork {} -> Dict
StoreFrontend_OAuth_Tokens {} -> Dict
StoreFrontend_OAuth_State {} -> Dict
StoreFrontend_Gist_GistRequested {} -> Dict
StoreFrontend_ModuleExplorer_SessionFile {} -> Dict

deriveStoreInstances ''StoreFrontend
deriveJSONGADT ''StoreFrontend
2 changes: 1 addition & 1 deletion frontend/src/Frontend/Wallet.hs
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ makeWallet mChangePassword model conf = do
--addStarterAccount :: IntMap (Key key) -> AccountData -> AccountData
addStarterAccount net ks ad =
case IntMap.toList ks of
[(i,k)] -> if Map.size (ad ^. _AccountData . ix net) == 0
[(_i,k)] -> if Map.size (ad ^. _AccountData . ix net) == 0
then ad <> (AccountData $ net =: (AccountName $ keyToText $ _keyPair_publicKey $ _key_pair k) =: mempty)
else ad
_ -> ad
Expand Down
Loading