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

Restrict Checkpoints to be constructible from genesis only #3048

Merged
merged 1 commit into from
Dec 16, 2021
Merged
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
29 changes: 20 additions & 9 deletions lib/core/src/Cardano/Wallet/DB/Checkpoints.hs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ module Cardano.Wallet.DB.Checkpoints
( getPoint

-- * Checkpoints
, Checkpoints (..)
, singleton
, Checkpoints
, checkpoints
, loadCheckpoints
, fromGenesis
, getLatest
, findNearestPoint

Expand Down Expand Up @@ -85,21 +87,30 @@ getPoint =
-- | Collection of checkpoints indexed by 'Slot'.
data Checkpoints a = Checkpoints
{ checkpoints :: Map W.Slot a
-- ^ Map of checkpoints. Always contains the genesis checkpoint.
} deriving (Eq,Show,Generic)
-- FIXME LATER during ADP-1043:
-- Use a more sophisticated 'Checkpoints' type that stores deltas.

-- | Make a single checkpoint.
singleton :: W.Slot -> a -> Checkpoints a
singleton key a = Checkpoints $ Map.singleton key a
-- | Turn the list of checkpoints into a map of checkpoints.
--
-- FIXME LATER during ADP-1043:
-- The database actually does not store the checkpoint at genesis,
-- but the checkpoint after that.
-- Hence, this function does not check whether the genesis checkpoint
-- is in the list of checkpoints.
loadCheckpoints :: [(W.Slot, a)] -> Checkpoints a
loadCheckpoints = Checkpoints . Map.fromList

-- | Begin with the genesis checkpoint.
fromGenesis :: a -> Checkpoints a
fromGenesis a = Checkpoints $ Map.singleton W.Origin a
HeinrichApfelmus marked this conversation as resolved.
Show resolved Hide resolved

-- | Get the checkpoint with the largest 'SlotNo'.
getLatest :: Checkpoints a -> (W.Slot, a)
getLatest = from . Map.lookupMax . view #checkpoints
where
from = fromMaybe (error "getLatest: Genesis checkpoint is missing!")
-- FIXME LATER during ADP-1043:
-- Make sure that 'Checkpoints' always has a genesis checkpoint
from = fromMaybe (error "getLatest: there should always be at least a genesis checkpoint")

-- | Find the nearest 'Checkpoint' that is either at the given point or before.
findNearestPoint :: Checkpoints a -> W.Slot -> Maybe W.Slot
Expand All @@ -123,7 +134,7 @@ instance Delta (DeltaCheckpoints a) where
apply (RollbackTo pt) = over #checkpoints $
Map.filterWithKey (\k _ -> k <= pt)
apply (RestrictTo pts) = over #checkpoints $ \m ->
Map.restrictKeys m $ Set.fromList pts
Map.restrictKeys m $ Set.fromList (W.Origin:pts)

{-------------------------------------------------------------------------------
A Delta type for Maps
Expand Down
28 changes: 24 additions & 4 deletions lib/core/src/Cardano/Wallet/DB/Sqlite.hs
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,9 @@ import Cardano.Wallet.DB.Checkpoints
( DeltaCheckpoints (..)
, DeltaMap (..)
, findNearestPoint
, fromGenesis
, getLatest
, getPoint
, singleton
)
import Cardano.Wallet.DB.Sqlite.CheckpointsOld
( PersistState (..), blockHeaderFromEntity, mkStoreWalletsCheckpoints )
Expand Down Expand Up @@ -535,15 +535,26 @@ newDBLayerWith _cacheBehavior tr ti SqliteContext{runQuery} = do
-- mutex (queryLock), which means no concurrent queries.
queryLock <- newMVar () -- fixme: ADP-586

-- Insert a checkpoint into the DBVar
-- Insert a checkpoint for a given wallet ID into the DBVar,
-- assuming that the wallet already exists.
let insertCheckpointCached wid cp = do
liftIO $ traceWith tr $ MsgCheckpointCache wid MsgPutCheckpoint
modifyDBMaybe checkpointsVar $ \ws ->
let point = getPoint cp
in case Map.lookup wid ws of
Nothing -> (Just $ Insert wid $ singleton point cp, ())
Nothing -> (Nothing, ())
Just _ -> (Just $ Adjust wid $ PutCheckpoint point cp, ())

-- Insert genesis checkpoint into the DBVar.
-- Throws an internal error if the checkpoint is not actually at genesis.
let insertCheckpointGenesis wid cp
| W.isGenesisBlockHeader header =
updateDBVar checkpointsVar $ Insert wid $ fromGenesis cp
| otherwise =
throwIO $ ErrInitializeNotGenesis wid header
HeinrichApfelmus marked this conversation as resolved.
Show resolved Hide resolved
where
header = cp ^. #currentTip

-- Retrieve the latest checkpoint from the DBVar
let selectLatestCheckpointCached
:: W.WalletId
Expand Down Expand Up @@ -588,7 +599,7 @@ newDBLayerWith _cacheBehavior tr ti SqliteContext{runQuery} = do
res <- handleConstraint (ErrWalletAlreadyExists wid) $
insert_ (mkWalletEntity wid meta gp)
when (isRight res) $ do
insertCheckpointCached wid cp
insertCheckpointGenesis wid cp
let (metas, txins, txcins, txouts, txoutTokens, ws) =
mkTxHistory wid txs
putTxs metas txins txcins txouts txoutTokens ws
Expand Down Expand Up @@ -1538,6 +1549,9 @@ selectGenesisParameters wid = do
gp <- selectFirst [WalId ==. wid] []
pure $ (genesisParametersFromEntity . entityVal) <$> gp

{-------------------------------------------------------------------------------
Internal errors
-------------------------------------------------------------------------------}
-- | A fatal exception thrown when trying to rollback but, there's no checkpoint
-- to rollback to. The database maintain the invariant that there's always at
-- least one checkpoint (the first one made for genesis) present in the
Expand All @@ -1547,3 +1561,9 @@ selectGenesisParameters wid = do
-- violated.
data ErrRollbackTo = ErrNoOlderCheckpoint W.WalletId W.Slot deriving (Show)
instance Exception ErrRollbackTo

-- | Can't initialize a wallet because the given 'BlockHeader' is not genesis.
data ErrInitializeNotGenesis
HeinrichApfelmus marked this conversation as resolved.
Show resolved Hide resolved
= ErrInitializeNotGenesis W.WalletId W.BlockHeader deriving (Eq, Show)

instance Exception ErrInitializeNotGenesis
12 changes: 8 additions & 4 deletions lib/core/src/Cardano/Wallet/DB/Sqlite/CheckpointsOld.hs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,12 @@ import Cardano.Address.Script
import Cardano.DB.Sqlite
( dbChunked )
import Cardano.Wallet.DB.Checkpoints
( Checkpoints (..), DeltaCheckpoints (..), DeltaMap (..), getPoint )
( Checkpoints (..)
, DeltaCheckpoints (..)
, DeltaMap (..)
, getPoint
, loadCheckpoints
)
import Cardano.Wallet.DB.Sqlite.TH
( Checkpoint (..)
, CosignerKey (..)
Expand Down Expand Up @@ -187,10 +192,9 @@ mkStoreCheckpoints wid =
where
load = do
cps <- selectAllCheckpoints wid
pure $ Right $ Checkpoints{ checkpoints = Map.fromList cps }
pure $ Right $ loadCheckpoints cps

write Checkpoints{checkpoints} =
forM_ (Map.toList checkpoints) $ \(pt,cp) ->
write cps = forM_ (Map.toList $ checkpoints cps) $ \(pt,cp) ->
update (PutCheckpoint pt cp)

update (PutCheckpoint _ state) =
Expand Down
83 changes: 42 additions & 41 deletions lib/core/test/unit/Cardano/Wallet/DB/Properties.hs
Original file line number Diff line number Diff line change
Expand Up @@ -445,51 +445,51 @@ assertWith lbl condition = do
-- | Can list created wallets
prop_createListWallet
:: DBLayer IO s ShelleyKey
-> KeyValPairs WalletId (Wallet s , WalletMetadata)
-> KeyValPairs WalletId (InitialCheckpoint s, WalletMetadata)
-> Property
prop_createListWallet db@DBLayer{..} (KeyValPairs pairs) =
monadicIO (setup >> prop)
where
setup = liftIO (cleanDB db)
prop = liftIO $ do
res <- once pairs $ \(k, (cp, meta)) ->
res <- once pairs $ \(k, (InitialCheckpoint cp0, meta)) ->
atomically $ unsafeRunExceptT $
initializeWallet k cp meta mempty gp
initializeWallet k cp0 meta mempty gp
(length <$> atomically listWallets) `shouldReturn` length res

-- | Trying to create a same wallet twice should yield an error
prop_createWalletTwice
:: DBLayer IO s ShelleyKey
-> ( WalletId
, Wallet s
, InitialCheckpoint s
, WalletMetadata
)
-> Property
prop_createWalletTwice db@DBLayer{..} (wid, cp, meta) =
prop_createWalletTwice db@DBLayer{..} (wid, InitialCheckpoint cp0, meta) =
monadicIO (setup >> prop)
where
setup = liftIO (cleanDB db)
prop = liftIO $ do
let err = ErrWalletAlreadyExists wid
atomically (runExceptT $ initializeWallet wid cp meta mempty gp)
atomically (runExceptT $ initializeWallet wid cp0 meta mempty gp)
`shouldReturn` Right ()
atomically (runExceptT $ initializeWallet wid cp meta mempty gp)
atomically (runExceptT $ initializeWallet wid cp0 meta mempty gp)
`shouldReturn` Left err

-- | Trying to remove a same wallet twice should yield an error
prop_removeWalletTwice
:: DBLayer IO s ShelleyKey
-> ( WalletId
, Wallet s
, InitialCheckpoint s
, WalletMetadata
)
-> Property
prop_removeWalletTwice db@DBLayer{..} (wid, cp, meta) =
prop_removeWalletTwice db@DBLayer{..} (wid, InitialCheckpoint cp0, meta) =
monadicIO (setup >> prop)
where
setup = liftIO $ do
cleanDB db
atomically $ unsafeRunExceptT $ initializeWallet wid cp meta mempty gp
atomically $ unsafeRunExceptT $ initializeWallet wid cp0 meta mempty gp
prop = liftIO $ do
let err = ErrNoSuchWallet wid
atomically (runExceptT $ removeWallet wid) `shouldReturn` Right ()
Expand All @@ -516,9 +516,9 @@ prop_readAfterPut putOp readOp db@DBLayer{..} (wid, a) =
where
setup = do
run $ cleanDB db
(InitialCheckpoint cp, meta) <- namedPick "Initial Checkpoint" arbitrary
(InitialCheckpoint cp0, meta) <- pick arbitrary
run $ atomically $ unsafeRunExceptT $
initializeWallet wid cp meta mempty gp
initializeWallet wid cp0 meta mempty gp
prop = do
run $ unsafeRunExceptT $ putOp db wid a
res <- run $ readOp db wid
Expand All @@ -538,9 +538,9 @@ prop_getTxAfterPutValidTxId db@DBLayer{..} wid txGen =
where
setup = do
run $ cleanDB db
(InitialCheckpoint cp, meta) <- namedPick "Initial Checkpoint" arbitrary
(InitialCheckpoint cp0, meta) <- pick arbitrary
run $ atomically $ unsafeRunExceptT $
initializeWallet wid cp meta mempty gp
initializeWallet wid cp0 meta mempty gp
prop = do
let txs = unGenTxHistory txGen
run $ unsafeRunExceptT $ mapExceptT atomically $ putTxHistory wid txs
Expand All @@ -566,9 +566,9 @@ prop_getTxAfterPutInvalidTxId db@DBLayer{..} wid txGen txId' =
where
setup = do
run $ cleanDB db
(InitialCheckpoint cp, meta) <- namedPick "Initial Checkpoint" arbitrary
(InitialCheckpoint cp0, meta) <- pick arbitrary
run $ atomically $ unsafeRunExceptT $
initializeWallet wid cp meta mempty gp
initializeWallet wid cp0 meta mempty gp
prop = do
let txs = unGenTxHistory txGen
run $ unsafeRunExceptT $ mapExceptT atomically $ putTxHistory wid txs
Expand All @@ -579,18 +579,19 @@ prop_getTxAfterPutInvalidTxId db@DBLayer{..} wid txGen txId' =
prop_getTxAfterPutInvalidWalletId
:: DBLayer IO s ShelleyKey
-> ( WalletId
, Wallet s
, InitialCheckpoint s
, WalletMetadata
)
-> GenTxHistory
-> WalletId
-> Property
prop_getTxAfterPutInvalidWalletId db@DBLayer{..} (wid, cp, meta) txGen wid' =
wid /= wid' ==> monadicIO (setup >> prop)
prop_getTxAfterPutInvalidWalletId db@DBLayer{..}
(wid, InitialCheckpoint cp0, meta) txGen wid'
= wid /= wid' ==> monadicIO (setup >> prop)
where
setup = liftIO $ do
cleanDB db
atomically $ unsafeRunExceptT $ initializeWallet wid cp meta mempty gp
atomically $ unsafeRunExceptT $ initializeWallet wid cp0 meta mempty gp
prop = liftIO $ do
let txs = unGenTxHistory txGen
atomically (runExceptT $ putTxHistory wid txs) `shouldReturn` Right ()
Expand All @@ -600,7 +601,7 @@ prop_getTxAfterPutInvalidWalletId db@DBLayer{..} (wid, cp, meta) txGen wid' =

-- | Can't put resource before a wallet has been initialized
prop_putBeforeInit
:: (Buildable (f a), Eq (f a))
:: (Buildable (f a), Eq (f a), GenState s)
=> ( DBLayer IO s ShelleyKey
-> WalletId
-> a
Expand Down Expand Up @@ -633,7 +634,7 @@ prop_isolation
:: ( Buildable (f b), Eq (f b)
, Buildable (g c), Eq (g c)
, Buildable (h d), Eq (h d)
, Arbitrary (Wallet s)
, GenState s
, Show s
)
=> ( DBLayer IO s ShelleyKey
Expand Down Expand Up @@ -661,16 +662,16 @@ prop_isolation putA readB readC readD db@DBLayer{..} (ShowFmt wid, ShowFmt a) =
monadicIO (setup >>= prop)
where
setup = do
liftIO (cleanDB db)
(cp, meta, GenTxHistory txs) <- pick arbitrary
liftIO $ atomically $ do
unsafeRunExceptT $ initializeWallet wid cp meta mempty gp
run $ cleanDB db
(InitialCheckpoint cp0, meta) <- pick arbitrary
(GenTxHistory txs) <- pick arbitrary
run $ atomically $ do
unsafeRunExceptT $ initializeWallet wid cp0 meta mempty gp
unsafeRunExceptT $ putTxHistory wid txs
(b, c, d) <- liftIO $ (,,)
run $ (,,)
<$> readB db wid
<*> readC db wid
<*> readD db wid
return (b, c, d)

prop (b, c, d) = liftIO $ do
unsafeRunExceptT $ putA db wid a
Expand All @@ -694,10 +695,10 @@ prop_readAfterDelete readOp empty db@DBLayer{..} (ShowFmt wid) =
monadicIO (setup >> prop)
where
setup = do
liftIO (cleanDB db)
(cp, meta) <- pick arbitrary
liftIO $ atomically $ unsafeRunExceptT $
initializeWallet wid cp meta mempty gp
run $ cleanDB db
(InitialCheckpoint cp0, meta) <- pick arbitrary
run $ atomically $ unsafeRunExceptT $
initializeWallet wid cp0 meta mempty gp
prop = liftIO $ do
atomically $ unsafeRunExceptT $ removeWallet wid
(ShowFmt <$> readOp db wid) `shouldReturn` ShowFmt empty
Expand Down Expand Up @@ -731,9 +732,9 @@ prop_sequentialPut putOp readOp resolve db@DBLayer{..} kv =
ids = map fst pairs
setup = do
run $ cleanDB db
(InitialCheckpoint cp, meta) <- pick arbitrary
(InitialCheckpoint cp0, meta) <- pick arbitrary
run $ atomically $ unsafeRunExceptT $ once_ pairs $ \(k, _) ->
initializeWallet k cp meta mempty gp
initializeWallet k cp0 meta mempty gp
prop = do
run $ unsafeRunExceptT $ forM_ pairs $ uncurry (putOp db)
res <- run $ once pairs (readOp db . fst)
Expand All @@ -744,7 +745,7 @@ prop_sequentialPut putOp readOp resolve db@DBLayer{..} kv =

-- | Check that the DB supports multiple sequential puts for a given resource
prop_parallelPut
:: (Arbitrary (Wallet s), Show s)
:: (GenState s)
=> ( DBLayer IO s ShelleyKey
-> WalletId
-> a
Expand All @@ -769,10 +770,10 @@ prop_parallelPut putOp readOp resolve db@DBLayer{..} (KeyValPairs pairs) =
where
ids = map fst pairs
setup = do
liftIO (cleanDB db)
(cp, meta) <- pick arbitrary
liftIO $ atomically $ unsafeRunExceptT $ once_ pairs $ \(k, _) ->
initializeWallet k cp meta mempty gp
run $ cleanDB db
(InitialCheckpoint cp0, meta) <- pick arbitrary
run $ atomically $ unsafeRunExceptT $ once_ pairs $ \(k, _) ->
initializeWallet k cp0 meta mempty gp
prop = liftIO $ do
forConcurrently_ pairs $ unsafeRunExceptT . uncurry (putOp db)
res <- once pairs (readOp db . fst)
Expand All @@ -783,10 +784,10 @@ prop_parallelPut putOp readOp resolve db@DBLayer{..} (KeyValPairs pairs) =
prop_rollbackCheckpoint
:: forall s k. (GenState s, Eq s)
=> DBLayer IO s k
-> Wallet s
-> InitialCheckpoint s
-> MockChain
-> Property
prop_rollbackCheckpoint db@DBLayer{..} cp0 (MockChain chain) = do
prop_rollbackCheckpoint db@DBLayer{..} (InitialCheckpoint cp0) (MockChain chain) = do
monadicIO $ do
ShowFmt wid <- namedPick "Wallet ID" arbitrary
ShowFmt meta <- namedPick "Wallet Metadata" arbitrary
Expand Down
Loading