Skip to content
This repository was archived by the owner on Aug 18, 2020. It is now read-only.

Commit d86e334

Browse files
authored
Merge pull request #3245 from input-output-hk/feature/cbr-150-implement-rollback
[CBR-150] Implement rollback
2 parents 86d3fcf + 3b6317a commit d86e334

File tree

25 files changed

+1120
-533
lines changed

25 files changed

+1120
-533
lines changed

wallet-new/cardano-sl-wallet-new.cabal

+1
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,7 @@ test-suite wallet-unit-tests
422422
Wallet.Inductive.Generator
423423
Wallet.Inductive.Interpreter
424424
Wallet.Inductive.Invariants
425+
Wallet.Inductive.History
425426
Wallet.Inductive.Validation
426427
Wallet.Prefiltered
427428
Wallet.Rollback.Basic

wallet-new/src/Cardano/Wallet/Kernel.hs

+30-6
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,19 @@ module Cardano.Wallet.Kernel (
99
PassiveWallet -- opaque
1010
, DB -- opaque
1111
, WalletId
12-
, applyBlock
13-
, applyBlocks
1412
, bracketPassiveWallet
1513
, init
1614
, walletLogMessage
1715
, walletPassive
18-
-- * The only effectful getter you will ever need
16+
-- ** Respond to block chain events
17+
, applyBlock
18+
, applyBlocks
19+
, switchToFork
20+
-- *** Testing
21+
, observableRollbackUseInTestsOnly
22+
-- ** The only effectful getter you will ever need
1923
, getWalletSnapshot
20-
-- * Pure getters acting on a DB snapshot
24+
-- ** Pure getters acting on a DB snapshot
2125
, module Getters
2226
-- * Active wallet
2327
, ActiveWallet -- opaque
@@ -49,7 +53,8 @@ import Cardano.Wallet.Kernel.Types (WalletId (..))
4953

5054
import Cardano.Wallet.Kernel.DB.AcidState (ApplyBlock (..),
5155
CancelPending (..), DB, NewPending (..), NewPendingError,
52-
Snapshot (..), defDB)
56+
ObservableRollbackUseInTestsOnly (..), Snapshot (..),
57+
SwitchToFork (..), defDB)
5358
import Cardano.Wallet.Kernel.DB.HdWallet
5459
import Cardano.Wallet.Kernel.DB.InDb
5560
import Cardano.Wallet.Kernel.DB.Resolved (ResolvedBlock)
@@ -140,7 +145,7 @@ applyBlock pw@PassiveWallet{..} b
140145
= do
141146
blocksByAccount <- prefilterBlock' pw b
142147
-- apply block to all Accounts in all Wallets
143-
void $ update' _wallets $ ApplyBlock blocksByAccount
148+
update' _wallets $ ApplyBlock blocksByAccount
144149

145150
-- | Apply multiple blocks, one at a time, to all wallets in the PassiveWallet
146151
--
@@ -150,6 +155,25 @@ applyBlocks :: PassiveWallet
150155
-> IO ()
151156
applyBlocks = mapM_ . applyBlock
152157

158+
-- | Switch to a new fork
159+
--
160+
-- NOTE: The Ouroboros protocol says that this is only valid if the number of
161+
-- resolved blocks exceeds the length of blocks to roll back.
162+
switchToFork :: PassiveWallet
163+
-> Int -- ^ Number of blocks to roll back
164+
-> [ResolvedBlock] -- ^ Blocks in the new fork
165+
-> IO ()
166+
switchToFork pw@PassiveWallet{..} n bs = do
167+
blockssByAccount <- mapM (prefilterBlock' pw) bs
168+
update' _wallets $ SwitchToFork n blockssByAccount
169+
170+
-- | Observable rollback
171+
--
172+
-- Only used for tests. See 'switchToFork'.
173+
observableRollbackUseInTestsOnly :: PassiveWallet -> IO ()
174+
observableRollbackUseInTestsOnly PassiveWallet{..} =
175+
update' _wallets $ ObservableRollbackUseInTestsOnly
176+
153177
{-------------------------------------------------------------------------------
154178
Active wallet
155179
-------------------------------------------------------------------------------}

wallet-new/src/Cardano/Wallet/Kernel/DB/AcidState.hs

+84-16
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@ module Cardano.Wallet.Kernel.DB.AcidState (
2626
-- *** DELETE
2727
, DeleteHdRoot(..)
2828
, DeleteHdAccount(..)
29-
-- * errors
29+
-- * Errors
3030
, NewPendingError
31+
-- * Testing
32+
, ObservableRollbackUseInTestsOnly(..)
3133
) where
3234

3335
import Universum
@@ -38,6 +40,7 @@ import Control.Monad.Except (MonadError, catchError)
3840
import Test.QuickCheck (Arbitrary (..), oneof)
3941

4042
import Data.Acid (Query, Update, makeAcidic)
43+
import qualified Data.Map.Merge.Strict as Map.Merge
4144
import qualified Data.Map.Strict as Map
4245
import Data.SafeCopy (base, deriveSafeCopy)
4346
import Formatting (bprint, build, (%))
@@ -47,8 +50,9 @@ import Pos.Core.Chrono (OldestFirst (..))
4750
import qualified Pos.Core.Txp as Txp
4851
import Pos.Txp (Utxo)
4952

53+
import qualified Cardano.Wallet.Kernel.DB.Util.IxSet as IxSet
5054
import Cardano.Wallet.Kernel.PrefilterTx (AddrWithId,
51-
PrefilteredBlock (..))
55+
PrefilteredBlock (..), emptyPrefilteredBlock)
5256

5357
import Cardano.Wallet.Kernel.DB.HdWallet
5458
import qualified Cardano.Wallet.Kernel.DB.HdWallet.Create as HD
@@ -160,21 +164,26 @@ cancelPending cancelled = void . runUpdate' . zoom dbHdWallets $
160164
-- * For every address encountered in the block outputs, create an HdAddress if it
161165
-- does not already exist.
162166
applyBlock :: Map HdAccountId PrefilteredBlock -> Update DB ()
163-
applyBlock blocksByAccount = runUpdateNoErrors $ zoom dbHdWallets $
167+
applyBlock blocksByAccount = runUpdateNoErrors $ zoom dbHdWallets $ do
168+
blocksByAccount' <- fillInEmptyBlock blocksByAccount
164169
createPrefiltered
165170
initUtxoAndAddrs
166171
(\prefBlock -> zoom hdAccountCheckpoints $
167172
modify $ Spec.applyBlock prefBlock)
168-
blocksByAccount
173+
blocksByAccount'
169174
where
170-
-- NOTE: When we create the new wallet, we look at the genesis UTxO and create
171-
-- an initial balance for all accounts that we recognize as ours. This means that
172-
-- when we later discover a new account that is also ours, it cannot appear
173-
-- in the genesis UTxO, because if it did, we would already have seen it
174-
-- (the genesis UTxO is static, after all). Hence we use empty initial utxo
175-
-- for accounts discovered during applyBlock.
176-
-- The Addrs need to be created during account initialisation and so we pass them here.
177-
initUtxoAndAddrs :: PrefilteredBlock -> (Utxo,[AddrWithId])
175+
-- Initial UTxO and addresses for a new account
176+
--
177+
-- NOTE: When we initialize the kernel, we look at the genesis UTxO and create
178+
-- an initial balance for all accounts that we recognize as ours. This means
179+
-- that when we later discover a new account that is also ours, it cannot appear
180+
-- in the genesis UTxO, because if it did, we would already have seen it (the
181+
-- genesis UTxO is static, after all). Hence we use empty initial utxo for
182+
-- accounts discovered during 'applyBlock' (and 'switchToFork')
183+
--
184+
-- The Addrs need to be created during account initialisation and so we pass
185+
-- them here.
186+
initUtxoAndAddrs :: PrefilteredBlock -> (Utxo, [AddrWithId])
178187
initUtxoAndAddrs pb = (Map.empty, pfbAddrs pb)
179188

180189
-- | Switch to a fork
@@ -184,11 +193,40 @@ applyBlock blocksByAccount = runUpdateNoErrors $ zoom dbHdWallets $
184193
-- TODO: We use a plain list here rather than 'OldestFirst' since the latter
185194
-- does not have a 'SafeCopy' instance.
186195
switchToFork :: Int
187-
-> [PrefilteredBlock]
196+
-> [Map HdAccountId PrefilteredBlock]
188197
-> Update DB ()
189-
switchToFork n blocks = runUpdateNoErrors $
198+
switchToFork n blocks = runUpdateNoErrors $ zoom dbHdWallets $ do
199+
blocks' <- mapM fillInEmptyBlock blocks
200+
createPrefiltered
201+
initUtxoAndAddrs
202+
(\prefBlocks -> zoom hdAccountCheckpoints $
203+
modify $ Spec.switchToFork n (OldestFirst prefBlocks))
204+
(distribute blocks')
205+
where
206+
-- The natural result of prefiltering each block is a list of maps, but
207+
-- in order to apply them to each account, we want a map of lists
208+
--
209+
-- NOTE: We have to be careful to /first/ use 'fillInEmptyBlock' to make
210+
-- sure that if, say, the first and third slot both contain a block for
211+
-- account A, but the second does not, we end up with an empty block
212+
-- inserted for slot 2.
213+
distribute :: [Map HdAccountId PrefilteredBlock]
214+
-> Map HdAccountId [PrefilteredBlock]
215+
distribute = Map.unionsWith (++) . map (Map.map (:[]))
216+
217+
-- See comments in 'applyBlock'
218+
initUtxoAndAddrs :: [PrefilteredBlock] -> (Utxo, [AddrWithId])
219+
initUtxoAndAddrs pbs = (Map.empty, concatMap pfbAddrs pbs)
220+
221+
-- | Observable rollback, used for tests only
222+
--
223+
-- See 'switchToFork' for use in real code.
224+
observableRollbackUseInTestsOnly :: Update DB ()
225+
observableRollbackUseInTestsOnly = runUpdateNoErrors $
190226
zoomAll (dbHdWallets . hdWalletsAccounts) $
191-
hdAccountCheckpoints %~ Spec.switchToFork n (OldestFirst blocks)
227+
hdAccountCheckpoints %~ Spec.observableRollbackUseInTestsOnly
228+
229+
192230

193231
{-------------------------------------------------------------------------------
194232
Wallet creation
@@ -215,8 +253,37 @@ createHdWallet newRoot utxoByAccount = runUpdate' . zoom dbHdWallets $ do
215253
Internal auxiliary: apply a function to a prefiltered block/utxo
216254
-------------------------------------------------------------------------------}
217255

256+
-- | Given a map from account IDs, add default values for all accounts in
257+
-- the wallet that aren't given a value in the map
258+
fillInDefaults :: forall p e.
259+
(HdAccount -> p) -- ^ Default value
260+
-> Map HdAccountId p -- ^ Map with values per account
261+
-> Update' HdWallets e (Map HdAccountId p)
262+
fillInDefaults def accs =
263+
aux . IxSet.toMap <$> use hdWalletsAccounts
264+
where
265+
aux :: Map HdAccountId HdAccount -> Map HdAccountId p
266+
aux = Map.Merge.merge newAccount needsDefault valueForExistingAcc accs
267+
268+
newAccount :: Map.Merge.SimpleWhenMissing HdAccountId p p
269+
newAccount = Map.Merge.mapMaybeMissing $ \_accId p -> Just p
270+
271+
needsDefault :: Map.Merge.SimpleWhenMissing HdAccountId HdAccount p
272+
needsDefault = Map.Merge.mapMaybeMissing $ \_accId acc -> Just (def acc)
273+
274+
valueForExistingAcc :: Map.Merge.SimpleWhenMatched HdAccountId p HdAccount p
275+
valueForExistingAcc = Map.Merge.zipWithMatched $ \_accId p _acc -> p
276+
277+
-- | Specialization of 'fillInDefaults' for prefiltered blocks
278+
fillInEmptyBlock :: Map HdAccountId PrefilteredBlock
279+
-> Update' HdWallets e (Map HdAccountId PrefilteredBlock)
280+
fillInEmptyBlock = fillInDefaults (const emptyPrefilteredBlock)
281+
218282
-- | For each of the specified accounts, create them if they do not exist,
219283
-- and apply the specified function.
284+
--
285+
-- NOTE: Any accounts that aren't in the map are simply skilled. See
286+
-- 'fillInDefaults'.
220287
createPrefiltered :: forall p e.
221288
(p -> (Utxo,[AddrWithId]))
222289
-- ^ Initial UTxO (when we are creating the account),
@@ -257,7 +324,6 @@ createPrefiltered initUtxoAndAddrs applyP accs = do
257324
firstCheckpoint utxo' = Checkpoint {
258325
_checkpointUtxo = InDb utxo'
259326
, _checkpointUtxoBalance = InDb $ Spec.balance utxo'
260-
, _checkpointExpected = InDb Map.empty
261327
, _checkpointPending = Pending . InDb $ Map.empty
262328
-- Since this is the first checkpoint before we have applied
263329
-- any blocks, the block metadata is empty
@@ -329,4 +395,6 @@ makeAcidic ''DB [
329395
, 'updateHdAccountName
330396
, 'deleteHdRoot
331397
, 'deleteHdAccount
398+
-- Testing
399+
, 'observableRollbackUseInTestsOnly
332400
]

wallet-new/src/Cardano/Wallet/Kernel/DB/BlockMeta.hs

+27
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ import Control.Lens.TH (makeLenses)
1616
import qualified Data.Map.Strict as Map
1717
import Data.SafeCopy (SafeCopy (..), base, contain, deriveSafeCopy,
1818
safeGet, safePut)
19+
import Formatting (bprint, build, (%))
20+
import qualified Formatting.Buildable
21+
import Serokell.Util (mapJson)
1922

2023
import qualified Pos.Core as Core
2124
import qualified Pos.Core.Txp as Txp
@@ -81,3 +84,27 @@ instance Monoid BlockMeta where
8184
_blockMetaAddressMeta = InDb Map.empty
8285
}
8386
mappend = (<>)
87+
88+
{-------------------------------------------------------------------------------
89+
Pretty-printing
90+
-------------------------------------------------------------------------------}
91+
92+
instance Buildable AddressMeta where
93+
build AddressMeta{..} = bprint
94+
( "AddressMeta"
95+
% "{ isUsed: " % build
96+
% ", isChange: " % build
97+
% "}"
98+
)
99+
_addressMetaIsUsed
100+
_addressMetaIsChange
101+
102+
instance Buildable BlockMeta where
103+
build BlockMeta{..} = bprint
104+
( "BlockMeta"
105+
% "{ slotId: " % mapJson
106+
% ", addressMeta: " % mapJson
107+
% "}"
108+
)
109+
(_fromDb _blockMetaSlotId)
110+
(_fromDb _blockMetaAddressMeta)

wallet-new/src/Cardano/Wallet/Kernel/DB/Spec.hs

+31-9
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,12 @@ module Cardano.Wallet.Kernel.DB.Spec (
1414
, pendingTransactions
1515
, checkpointUtxo
1616
, checkpointUtxoBalance
17-
, checkpointExpected
1817
, checkpointPending
1918
, checkpointBlockMeta
2019
-- ** Lenses into the current checkpoint
2120
, currentCheckpoint
2221
, currentUtxo
2322
, currentUtxoBalance
24-
, currentExpected
2523
, currentPending
2624
, currentPendingTxs
2725
, currentBlockMeta
@@ -33,9 +31,9 @@ import Control.Lens (to)
3331
import Control.Lens.TH (makeLenses)
3432
import qualified Data.Map.Strict as M
3533
import Data.SafeCopy (base, deriveSafeCopy)
36-
import Formatting (bprint, (%))
37-
import Formatting.Buildable (build)
38-
import Serokell.Util.Text (listJsonIndent)
34+
import Formatting (bprint, build, (%))
35+
import qualified Formatting.Buildable
36+
import Serokell.Util.Text (listJsonIndent, mapJson)
3937

4038
import qualified Pos.Core as Core
4139
import qualified Pos.Core.Txp as Txp
@@ -80,11 +78,23 @@ removePending ids (Pending (InDb old)) = Pending (InDb $ old `withoutKeys` ids)
8078

8179
-- | Per-wallet state
8280
--
83-
-- This is the same across all wallet types.
81+
-- NOTE: At the moment this does not included the expected UTxO. The expected
82+
-- UTxO is used for two things:
83+
--
84+
-- * Block resolution (translating tx inputs to their corresponding outputs, so
85+
-- that we know the corresponding addresses, needed for prefilering)
86+
-- * Minimum balance computation
87+
--
88+
-- Fortunately however we can rely on a full node as backing, so we don't need
89+
-- to use the expected UTxO for block resolution (this is explained in the
90+
-- formal spec in section "Prefiltering -- Consequences", under "possible
91+
-- alternatives"), and minimum balance computation is a new feature that we
92+
-- haven't implemented yet.
93+
--
94+
-- NOTE: This is the same across all wallet types.
8495
data Checkpoint = Checkpoint {
8596
_checkpointUtxo :: InDb Core.Utxo
8697
, _checkpointUtxoBalance :: InDb Core.Coin
87-
, _checkpointExpected :: InDb Core.Utxo
8898
, _checkpointPending :: Pending
8999
, _checkpointBlockMeta :: BlockMeta
90100
}
@@ -107,14 +117,12 @@ currentCheckpoint = neHead
107117

108118
currentUtxo :: Lens' Checkpoints Core.Utxo
109119
currentUtxoBalance :: Lens' Checkpoints Core.Coin
110-
currentExpected :: Lens' Checkpoints Core.Utxo
111120
currentBlockMeta :: Lens' Checkpoints BlockMeta
112121
currentPending :: Lens' Checkpoints Pending
113122
currentPendingTxs :: Lens' Checkpoints PendingTxs
114123

115124
currentUtxo = currentCheckpoint . checkpointUtxo . fromDb
116125
currentUtxoBalance = currentCheckpoint . checkpointUtxoBalance . fromDb
117-
currentExpected = currentCheckpoint . checkpointExpected . fromDb
118126
currentBlockMeta = currentCheckpoint . checkpointBlockMeta
119127
currentPending = currentCheckpoint . checkpointPending
120128
currentPendingTxs = currentPending . pendingTransactions . fromDb
@@ -134,3 +142,17 @@ instance Buildable Pending where
134142
build (Pending p) =
135143
let elems = p ^. fromDb . to M.toList
136144
in bprint ("Pending " % listJsonIndent 4) (map fst elems)
145+
146+
instance Buildable Checkpoint where
147+
build Checkpoint{..} = bprint
148+
( "Checkpoint"
149+
% "{ utxo: " % mapJson
150+
% ", utxoBalance: " % build
151+
% ", pending: " % build
152+
% ", blockMeta: " % build
153+
% "}"
154+
)
155+
(_fromDb _checkpointUtxo)
156+
(_fromDb _checkpointUtxoBalance)
157+
_checkpointPending
158+
_checkpointBlockMeta

0 commit comments

Comments
 (0)