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

Commit 1807318

Browse files
committed
[CBR-150] Implement rollback
Also wrote a hand-written test to test specifically for dependent pending transactions, because the generator doesn't always hit that case (and fixed the bug that Ryan intentionally left in because he rightly wanted a test first that exposes it first). Also fixed a bug in the test infrastructure: the interpreter requires state that depends on the blockchain (hash of the previous block, the stake distrubtion, etc). It is therefore important that we correctly track this state and rollback the interpreter state when there is a rollback in the blockchain. Added shrinking to test comparing the kernel to the pure model.
1 parent 5cb5a86 commit 1807318

File tree

18 files changed

+478
-132
lines changed

18 files changed

+478
-132
lines changed

core/src/Pos/Core/Common/Coin.hs

-1
Original file line numberDiff line numberDiff line change
@@ -153,4 +153,3 @@ unsafeIntegerToCoin n = leftToPanic "unsafeIntegerToCoin: " (integerToCoin n)
153153

154154
-- Place this here to avoid TH staging issues.
155155
deriveSafeCopySimple 0 'base ''Coin
156-

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

+31-6
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,22 @@ module Cardano.Wallet.Kernel (
1111
PassiveWallet -- opaque
1212
, DB -- opaque
1313
, WalletId
14-
, applyBlock
15-
, applyBlocks
1614
, bracketPassiveWallet
1715
, createWalletHdRnd
1816
, init
1917
, walletLogMessage
2018
, walletPassive
2119
, walletKeystore
2220
, wallets
23-
-- * The only effectful getter you will ever need
21+
-- ** Respond to block chain events
22+
, applyBlock
23+
, applyBlocks
24+
, switchToFork
25+
-- *** Testing
26+
, observableRollbackUseInTestsOnly
27+
-- ** The only effectful getter you will ever need
2428
, getWalletSnapshot
25-
-- * Pure getters acting on a DB snapshot
29+
-- ** Pure getters acting on a DB snapshot
2630
, module Getters
2731
-- * Active wallet
2832
, ActiveWallet -- opaque
@@ -53,7 +57,9 @@ import Cardano.Wallet.Kernel.Types (WalletId (..))
5357

5458
import Cardano.Wallet.Kernel.DB.AcidState (ApplyBlock (..),
5559
CancelPending (..), CreateHdWallet (..), DB,
56-
NewPending (..), NewPendingError, Snapshot (..), defDB)
60+
NewPending (..), NewPendingError,
61+
ObservableRollbackUseInTestsOnly (..), Snapshot (..),
62+
SwitchToFork (..), defDB)
5763
import Cardano.Wallet.Kernel.DB.HdWallet
5864
import qualified Cardano.Wallet.Kernel.DB.HdWallet as HD
5965
import qualified Cardano.Wallet.Kernel.DB.HdWallet.Create as HD
@@ -211,7 +217,7 @@ applyBlock pw@PassiveWallet{..} b
211217
= do
212218
blocksByAccount <- prefilterBlock' pw b
213219
-- apply block to all Accounts in all Wallets
214-
void $ update' _wallets $ ApplyBlock blocksByAccount
220+
update' _wallets $ ApplyBlock blocksByAccount
215221

216222
-- | Apply multiple blocks, one at a time, to all wallets in the PassiveWallet
217223
--
@@ -221,6 +227,25 @@ applyBlocks :: PassiveWallet
221227
-> IO ()
222228
applyBlocks = mapM_ . applyBlock
223229

230+
-- | Switch to a new fork
231+
--
232+
-- NOTE: The Ouroboros protocol says that this is only valid if the number of
233+
-- resolved blocks exceeds the length of blocks to roll back.
234+
switchToFork :: PassiveWallet
235+
-> Int -- ^ Number of blocks to roll back
236+
-> [ResolvedBlock] -- ^ Blocks in the new fork
237+
-> IO ()
238+
switchToFork pw@PassiveWallet{..} n bs = do
239+
blockssByAccount <- mapM (prefilterBlock' pw) bs
240+
update' _wallets $ SwitchToFork n blockssByAccount
241+
242+
-- | Observable rollback
243+
--
244+
-- Only used for tests. See 'switchToFork'.
245+
observableRollbackUseInTestsOnly :: PassiveWallet -> IO ()
246+
observableRollbackUseInTestsOnly PassiveWallet{..} =
247+
update' _wallets $ ObservableRollbackUseInTestsOnly
248+
224249
{-------------------------------------------------------------------------------
225250
Active wallet
226251
-------------------------------------------------------------------------------}

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
@@ -36,15 +38,17 @@ import Control.Lens.TH (makeLenses)
3638
import Control.Monad.Except (MonadError, catchError)
3739

3840
import Data.Acid (Query, Update, makeAcidic)
41+
import qualified Data.Map.Merge.Strict as Map.Merge
3942
import qualified Data.Map.Strict as Map
4043
import Data.SafeCopy (base, deriveSafeCopy)
4144

4245
import qualified Pos.Core as Core
4346
import Pos.Core.Chrono (OldestFirst (..))
4447
import Pos.Txp (Utxo)
4548

49+
import qualified Cardano.Wallet.Kernel.DB.Util.IxSet as IxSet
4650
import Cardano.Wallet.Kernel.PrefilterTx (AddrWithId,
47-
PrefilteredBlock (..))
51+
PrefilteredBlock (..), emptyPrefilteredBlock)
4852

4953
import Cardano.Wallet.Kernel.DB.HdWallet
5054
import qualified Cardano.Wallet.Kernel.DB.HdWallet.Create as HD
@@ -145,21 +149,26 @@ cancelPending cancelled = void . runUpdate' . zoom dbHdWallets $
145149
-- * For every address encountered in the block outputs, create an HdAddress if it
146150
-- does not already exist.
147151
applyBlock :: Map HdAccountId PrefilteredBlock -> Update DB ()
148-
applyBlock blocksByAccount = runUpdateNoErrors $ zoom dbHdWallets $
152+
applyBlock blocksByAccount = runUpdateNoErrors $ zoom dbHdWallets $ do
153+
blocksByAccount' <- fillInEmptyBlock blocksByAccount
149154
createPrefiltered
150155
initUtxoAndAddrs
151156
(\prefBlock -> zoom hdAccountCheckpoints $
152157
modify $ Spec.applyBlock prefBlock)
153-
blocksByAccount
158+
blocksByAccount'
154159
where
155-
-- NOTE: When we create the new wallet, we look at the genesis UTxO and create
156-
-- an initial balance for all accounts that we recognize as ours. This means that
157-
-- when we later discover a new account that is also ours, it cannot appear
158-
-- in the genesis UTxO, because if it did, we would already have seen it
159-
-- (the genesis UTxO is static, after all). Hence we use empty initial utxo
160-
-- for accounts discovered during applyBlock.
161-
-- The Addrs need to be created during account initialisation and so we pass them here.
162-
initUtxoAndAddrs :: PrefilteredBlock -> (Utxo,[AddrWithId])
160+
-- Initial UTxO and addresses for a new account
161+
--
162+
-- NOTE: When we initialize the kernel, we look at the genesis UTxO and create
163+
-- an initial balance for all accounts that we recognize as ours. This means
164+
-- that when we later discover a new account that is also ours, it cannot appear
165+
-- in the genesis UTxO, because if it did, we would already have seen it (the
166+
-- genesis UTxO is static, after all). Hence we use empty initial utxo for
167+
-- accounts discovered during 'applyBlock' (and 'switchToFork')
168+
--
169+
-- The Addrs need to be created during account initialisation and so we pass
170+
-- them here.
171+
initUtxoAndAddrs :: PrefilteredBlock -> (Utxo, [AddrWithId])
163172
initUtxoAndAddrs pb = (Map.empty, pfbAddrs pb)
164173

165174
-- | Switch to a fork
@@ -169,11 +178,40 @@ applyBlock blocksByAccount = runUpdateNoErrors $ zoom dbHdWallets $
169178
-- TODO: We use a plain list here rather than 'OldestFirst' since the latter
170179
-- does not have a 'SafeCopy' instance.
171180
switchToFork :: Int
172-
-> [PrefilteredBlock]
181+
-> [Map HdAccountId PrefilteredBlock]
173182
-> Update DB ()
174-
switchToFork n blocks = runUpdateNoErrors $
183+
switchToFork n blocks = runUpdateNoErrors $ zoom dbHdWallets $ do
184+
blocks' <- mapM fillInEmptyBlock blocks
185+
createPrefiltered
186+
initUtxoAndAddrs
187+
(\prefBlocks -> zoom hdAccountCheckpoints $
188+
modify $ Spec.switchToFork n (OldestFirst prefBlocks))
189+
(distribute blocks')
190+
where
191+
-- The natural result of prefiltering each block is a list of maps, but
192+
-- in order to apply them to each account, we want a map of lists
193+
--
194+
-- NOTE: We have to be careful to /first/ use 'fillInEmptyBlock' to make
195+
-- sure that if, say, the first and third slot both contain a block for
196+
-- account A, but the second does not, we end up with an empty block
197+
-- inserted for slot 2.
198+
distribute :: [Map HdAccountId PrefilteredBlock]
199+
-> Map HdAccountId [PrefilteredBlock]
200+
distribute = Map.unionsWith (++) . map (Map.map (:[]))
201+
202+
-- See comments in 'applyBlock'
203+
initUtxoAndAddrs :: [PrefilteredBlock] -> (Utxo, [AddrWithId])
204+
initUtxoAndAddrs pbs = (Map.empty, concatMap pfbAddrs pbs)
205+
206+
-- | Observable rollback, used for tests only
207+
--
208+
-- See 'switchToFork' for use in real code.
209+
observableRollbackUseInTestsOnly :: Update DB ()
210+
observableRollbackUseInTestsOnly = runUpdateNoErrors $
175211
zoomAll (dbHdWallets . hdWalletsAccounts) $
176-
hdAccountCheckpoints %~ Spec.switchToFork n (OldestFirst blocks)
212+
hdAccountCheckpoints %~ Spec.observableRollbackUseInTestsOnly
213+
214+
177215

178216
{-------------------------------------------------------------------------------
179217
Wallet creation
@@ -200,8 +238,37 @@ createHdWallet newRoot utxoByAccount = runUpdate' . zoom dbHdWallets $ do
200238
Internal auxiliary: apply a function to a prefiltered block/utxo
201239
-------------------------------------------------------------------------------}
202240

241+
-- | Given a map from account IDs, add default values for all accounts in
242+
-- the wallet that aren't given a value in the map
243+
fillInDefaults :: forall p e.
244+
(HdAccount -> p) -- ^ Default value
245+
-> Map HdAccountId p -- ^ Map with values per account
246+
-> Update' HdWallets e (Map HdAccountId p)
247+
fillInDefaults def accs =
248+
aux <$> IxSet.toMap <$> use hdWalletsAccounts
249+
where
250+
aux :: Map HdAccountId HdAccount -> Map HdAccountId p
251+
aux = Map.Merge.merge newAccount needsDefault valueForExistingAcc accs
252+
253+
newAccount :: Map.Merge.SimpleWhenMissing HdAccountId p p
254+
newAccount = Map.Merge.mapMaybeMissing $ \_accId p -> Just p
255+
256+
needsDefault :: Map.Merge.SimpleWhenMissing HdAccountId HdAccount p
257+
needsDefault = Map.Merge.mapMaybeMissing $ \_accId acc -> Just (def acc)
258+
259+
valueForExistingAcc :: Map.Merge.SimpleWhenMatched HdAccountId p HdAccount p
260+
valueForExistingAcc = Map.Merge.zipWithMatched $ \_accId p _acc -> p
261+
262+
-- | Specialization of 'fillInDefaults' for prefiltered blocks
263+
fillInEmptyBlock :: Map HdAccountId PrefilteredBlock
264+
-> Update' HdWallets e (Map HdAccountId PrefilteredBlock)
265+
fillInEmptyBlock = fillInDefaults (const emptyPrefilteredBlock)
266+
203267
-- | For each of the specified accounts, create them if they do not exist,
204268
-- and apply the specified function.
269+
--
270+
-- NOTE: Any accounts that aren't in the map are simply skilled. See
271+
-- 'fillInDefaults'.
205272
createPrefiltered :: forall p e.
206273
(p -> (Utxo,[AddrWithId]))
207274
-- ^ Initial UTxO (when we are creating the account),
@@ -242,7 +309,6 @@ createPrefiltered initUtxoAndAddrs applyP accs = do
242309
firstCheckpoint utxo' = Checkpoint {
243310
_checkpointUtxo = InDb utxo'
244311
, _checkpointUtxoBalance = InDb $ Spec.balance utxo'
245-
, _checkpointExpected = InDb Map.empty
246312
, _checkpointPending = Pending . InDb $ Map.empty
247313
-- Since this is the first checkpoint before we have applied
248314
-- any blocks, the block metadata is empty
@@ -314,4 +380,6 @@ makeAcidic ''DB [
314380
, 'updateHdAccountName
315381
, 'deleteHdRoot
316382
, 'deleteHdAccount
383+
-- Testing
384+
, 'observableRollbackUseInTestsOnly
317385
]

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

@@ -80,3 +83,27 @@ instance Monoid BlockMeta where
8083
_blockMetaAddressMeta = InDb Map.empty
8184
}
8285
mappend = (<>)
86+
87+
{-------------------------------------------------------------------------------
88+
Pretty-printing
89+
-------------------------------------------------------------------------------}
90+
91+
instance Buildable AddressMeta where
92+
build AddressMeta{..} = bprint
93+
( "AddressMeta"
94+
% "{ isUsed: " % build
95+
% ", isChange: " % build
96+
% "}"
97+
)
98+
_addressMetaIsUsed
99+
_addressMetaIsChange
100+
101+
instance Buildable BlockMeta where
102+
build BlockMeta{..} = bprint
103+
( "BlockMeta"
104+
% "{ slotId: " % mapJson
105+
% ", addressMeta: " % mapJson
106+
% "}"
107+
)
108+
(_fromDb _blockMetaSlotId)
109+
(_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.Txp as Core
@@ -79,11 +77,23 @@ removePending ids (Pending (InDb old)) = Pending (InDb $ old `withoutKeys` ids)
7977

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

107117
currentUtxo :: Lens' Checkpoints Core.Utxo
108118
currentUtxoBalance :: Lens' Checkpoints Core.Coin
109-
currentExpected :: Lens' Checkpoints Core.Utxo
110119
currentBlockMeta :: Lens' Checkpoints BlockMeta
111120
currentPending :: Lens' Checkpoints Pending
112121
currentPendingTxs :: Lens' Checkpoints PendingTxs
113122

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

0 commit comments

Comments
 (0)