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

Commit 75a94e1

Browse files
committed
better index/db intersection computation
Now the index will not always rollback to origin when its tip is not in the database. It will try to find an intersection point within the security parameter, and roll back only to that point.
1 parent 1a04368 commit 75a94e1

File tree

5 files changed

+154
-75
lines changed

5 files changed

+154
-75
lines changed

src/exec/Byron.hs

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import qualified Data.List.NonEmpty as NE
2222
import Data.Maybe (mapMaybe)
2323
import qualified Data.Text.Lazy.Builder as Text
2424
import Data.Typeable (Typeable)
25-
import Data.Word (Word64)
2625
import System.Random (StdGen, getStdGen, randomR)
2726

2827
import qualified Cardano.Binary as Binary
@@ -35,11 +34,11 @@ import qualified Pos.Chain.Block as CSL (Block, BlockHeader (..), GenesisBlock,
3534
import qualified Pos.Infra.Diffusion.Types as CSL
3635

3736
import Ouroboros.Byron.Proxy.Block (Block, ByronBlockOrEBB (..),
38-
coerceHashToLegacy, unByronHeaderOrEBB, headerHash)
37+
checkpointOffsets, coerceHashToLegacy, unByronHeaderOrEBB, headerHash)
3938
import Ouroboros.Byron.Proxy.Main
4039
import Ouroboros.Consensus.Block (Header)
4140
import Ouroboros.Consensus.Ledger.Byron (ByronGiven)
42-
import Ouroboros.Consensus.Protocol.Abstract (SecurityParam (maxRollbacks))
41+
import Ouroboros.Consensus.Protocol.Abstract (SecurityParam)
4342
import Ouroboros.Network.Block (ChainHash (..), Point, pointHash)
4443
import qualified Ouroboros.Network.AnchoredFragment as AF
4544
import qualified Ouroboros.Network.ChainFragment as CF
@@ -134,25 +133,14 @@ download tracer genesisBlock epochSlots securityParam db bp = do
134133
checkpoints
135134
:: AF.AnchoredFragment (Header (Block cfg))
136135
-> [CSL.HeaderHash]
137-
checkpoints = mapMaybe pointToHash . AF.selectPoints (fmap fromIntegral offsets)
136+
checkpoints = mapMaybe pointToHash .
137+
AF.selectPoints (fmap fromIntegral (checkpointOffsets securityParam))
138138

139139
pointToHash :: Point (Header (Block cfg)) -> Maybe CSL.HeaderHash
140140
pointToHash pnt = case pointHash pnt of
141141
GenesisHash -> Nothing
142142
BlockHash hash -> Just $ coerceHashToLegacy hash
143143

144-
-- Offsets for selectPoints. Defined in the same way as for the Shelley
145-
-- chain sync client: fibonacci numbers including 0 and k.
146-
offsets :: [Word64]
147-
offsets = 0 : foldr includeK ([] {- this is never forced -}) (tail fibs)
148-
149-
includeK :: Word64 -> [Word64] -> [Word64]
150-
includeK w ws | w >= k = [k]
151-
| otherwise = w : ws
152-
153-
fibs :: [Word64]
154-
fibs = 1 : 1 : zipWith (+) fibs (tail fibs)
155-
156144
streamer :: CSL.HeaderHash -> CSL.StreamBlocks CSL.Block IO CSL.HeaderHash
157145
streamer tipHash = CSL.StreamBlocks
158146
{ CSL.streamBlocksMore = \blocks -> do
@@ -181,9 +169,6 @@ download tracer genesisBlock epochSlots securityParam db bp = do
181169
let (idx, rndGen') = randomR (0, NE.length ne - 1) rndGen
182170
in (ne NE.!! idx, rndGen')
183171

184-
k :: Word64
185-
k = maxRollbacks securityParam
186-
187172
recodeBlockOrFail
188173
:: Cardano.EpochSlots
189174
-> (forall x . Binary.DecoderError -> IO x)

src/lib/Ouroboros/Byron/Proxy/Block.hs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,12 @@ module Ouroboros.Byron.Proxy.Block
2020
, coerceHashToLegacy
2121
, headerHash
2222
, isEBB
23+
, checkpointOffsets
2324
) where
2425

2526
import qualified Codec.CBOR.Write as CBOR (toStrictByteString)
27+
import qualified Data.ByteString.Lazy as Lazy
28+
import Data.Word (Word64)
2629

2730
import qualified Pos.Chain.Block as CSL (HeaderHash)
2831
import qualified Pos.Crypto.Hashing as Legacy (AbstractHash (..))
@@ -34,6 +37,7 @@ import Cardano.Crypto.Hashing (AbstractHash (..))
3437
import qualified Ouroboros.Consensus.Block as Consensus (GetHeader (..))
3538
import Ouroboros.Consensus.Ledger.Byron (ByronBlockOrEBB (..),
3639
pattern ByronHeaderOrEBB, encodeByronBlock, unByronHeaderOrEBB)
40+
import Ouroboros.Consensus.Protocol.Abstract (SecurityParam (..))
3741

3842
-- For type instance HeaderHash (Header blk) = HeaderHash blk
3943
-- Anyone who imports this module will almost certainly want that instance.
@@ -70,3 +74,16 @@ isEBB :: Block cfg -> Maybe Cardano.HeaderHash
7074
isEBB blk = case unByronBlockOrEBB blk of
7175
Cardano.ABOBBlock _ -> Nothing
7276
Cardano.ABOBBoundary _ -> Just $ headerHash (Consensus.getHeader blk)
77+
78+
-- | Compute the offsets for use by ChainFragment.selectPoints or
79+
-- AnchoredFragment.selectPoints for some security parameter k. It uses
80+
-- fibonacci numbers with 0 and k as endpoints, and the duplicate 1 at the
81+
-- start removed.
82+
checkpointOffsets :: SecurityParam -> [Word64]
83+
checkpointOffsets (SecurityParam k) = 0 : foldr includeK ([] {- this is never forced -}) (tail fibs)
84+
where
85+
includeK :: Word64 -> [Word64] -> [Word64]
86+
includeK w ws | w >= k = [k]
87+
| otherwise = w : ws
88+
fibs :: [Word64]
89+
fibs = 1 : 1 : zipWith (+) fibs (tail fibs)

src/lib/Ouroboros/Byron/Proxy/Index/ChainDB.hs

Lines changed: 61 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@ module Ouroboros.Byron.Proxy.Index.ChainDB
66
) where
77

88
import Control.Exception (bracket)
9+
import Data.Word (Word64)
910

11+
import Ouroboros.Byron.Proxy.Block (checkpointOffsets)
1012
import Ouroboros.Byron.Proxy.Index.Types (Index)
1113
import qualified Ouroboros.Byron.Proxy.Index.Types as Index
1214
import Ouroboros.Consensus.Block (GetHeader (Header))
1315
import Ouroboros.Consensus.Util.ResourceRegistry (ResourceRegistry)
16+
import Ouroboros.Consensus.Protocol.Abstract (SecurityParam)
1417
import Ouroboros.Network.Block (ChainUpdate (..), Point (..))
15-
import Ouroboros.Network.Point (WithOrigin (Origin))
18+
import Ouroboros.Network.Point (WithOrigin (Origin), block)
1619
import Ouroboros.Storage.ChainDB.API (ChainDB, Reader)
1720
import qualified Ouroboros.Storage.ChainDB.API as ChainDB
1821

@@ -21,7 +24,7 @@ trackReaderBlocking
2124
:: ( Monad m )
2225
=> Index m (Header blk)
2326
-> Reader m blk (Header blk)
24-
-> m x
27+
-> m void
2528
trackReaderBlocking idx reader = do
2629
instruction <- ChainDB.readerInstructionBlocking reader
2730
case instruction of
@@ -50,42 +53,69 @@ trackReader idx reader = do
5053
trackReader idx reader
5154
Nothing -> pure ()
5255

53-
-- | Have an Index track a ChainDB using its Reader API. You probably want to
54-
-- race this with some other thread that runs your application.
55-
--
56-
-- If the ChainDB does not contain the tip of the Index, then the whole index
57-
-- will be rebuilt.
56+
-- | Have an Index track a ChainDB using its Reader API for the duration of
57+
-- some monadic action.
5858
--
5959
-- It will spawn a thread to do the index updates. This must be the only
6060
-- index writer. It is run by `race` with the action, so exceptions in either
6161
-- the action or the writer thread will be re-thrown here.
6262
--
63-
-- If the tip of the index is not in the ChainDB, then the entire index will be
64-
-- rebuilt. This is not ideal: there may be an intersection. TODO would be
65-
-- better to check the newest slot older than `k` back from tip of index, and
66-
-- go from there.
63+
-- If the tip of the index is in the ChainDB, then no work must be done in the
64+
-- beginning. But if it's not in the ChainDB, there will have to be a rollback
65+
-- on the index. The SecurityParam k is used to decide how far back to try. If
66+
-- Only index entries at most k slots old will be checked against the
67+
-- ChainDB. If none are in it, then the entire index will be rebuild (rollback
68+
-- to Origin).
6769
trackChainDB
68-
:: forall blk void .
70+
:: forall blk t .
6971
ResourceRegistry IO
7072
-> Index IO (Header blk)
7173
-> ChainDB IO blk
72-
-> IO void
73-
trackChainDB rr idx cdb = bracket acquireReader releaseReader $ \rdr -> do
74-
tipPoint <- Index.tip idx
75-
mPoint <- ChainDB.readerForward rdr [Point tipPoint]
76-
-- `readerForward` docs say that if we get `Nothing`, the next reader
77-
-- instruction may not be a rollback, so we'll manually roll the index
78-
-- back. It's assumed the read pointer will be at origin (nothing else
79-
-- would make sense).
80-
case mPoint of
81-
Nothing -> Index.rollbackward idx Origin
82-
Just _ -> pure ()
83-
-- First, block until the index is caught up to the tip ...
84-
trackReader idx rdr
85-
-- ... then attempt to stay in sync.
86-
trackReaderBlocking idx rdr
74+
-> SecurityParam
75+
-> IO t
76+
-> IO t
77+
trackChainDB rr idx cdb k act = bracket acquireReader releaseReader $ \rdr -> do
78+
checkpoints <- Index.streamFromTip idx checkpointsFold
79+
mPoint <- ChainDB.readerForward rdr checkpoints
80+
case mPoint of
81+
-- `readerForward` docs say that the next instruction will be a rollback,
82+
-- so we don't have to do anything here; the call to `trackReader` will
83+
-- do what needs to be done.
84+
Just _ -> pure ()
85+
-- `readerForward` docs say that if we get `Nothing`, the next reader
86+
-- instruction may not be a rollback, so we'll manually roll the index
87+
-- back. It's assumed the read pointer will be at origin (nothing else
88+
-- would make sense).
89+
Nothing -> Index.rollbackward idx Origin
90+
-- First, block until the index is caught up to the tip ...
91+
trackReader idx rdr
92+
-- ... then attempt to stay in sync.
93+
outcome <- race (trackReaderBlocking idx rdr) act
94+
case outcome of
95+
Left impossible -> impossible
96+
Right t -> pure t
8797
where
88-
acquireReader :: IO (Reader IO blk (Header blk))
89-
acquireReader = ChainDB.newHeaderReader cdb rr
90-
releaseReader :: Reader IO blk (Header blk) -> IO ()
91-
releaseReader = ChainDB.readerClose
98+
acquireReader :: IO (Reader IO blk (Header blk))
99+
acquireReader = ChainDB.newHeaderReader cdb rr
100+
releaseReader :: Reader IO blk (Header blk) -> IO ()
101+
releaseReader = ChainDB.readerClose
102+
103+
checkpointsFold :: Index.Fold (Header blk) [Point blk]
104+
checkpointsFold = checkpointsFoldN 0 (checkpointOffsets k)
105+
106+
-- Count up from 0 on the first parameter. Whenever it coincides with the
107+
-- head of the second parameter (an increasing list) include that point.
108+
-- Stop when the second list is empty.
109+
-- Since checkpointsFold always includes the paramater k, the k'th entry
110+
-- in the index will always be in here, unless the index is shorter
111+
-- than k. This block is _at least_ k slots behind the DB, so if it's not
112+
-- in the DB then the index is way out of date.
113+
checkpointsFoldN
114+
:: Word64
115+
-> [Word64]
116+
-> Index.Fold (Header blk) [Point blk]
117+
checkpointsFoldN _ [] = Index.Stop []
118+
checkpointsFoldN w (o : os) = Index.More [] $ \slotNo hash ->
119+
if w == o
120+
then fmap ((:) (Point (block slotNo hash))) (checkpointsFoldN (w+1) os)
121+
else checkpointsFoldN (w+1) (o : os)

src/lib/Ouroboros/Byron/Proxy/Index/Sqlite.hs

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,11 @@ data TraceEvent where
4646
-- (BEGIN TRANSACTION).
4747
index :: EpochSlots -> Tracer IO TraceEvent -> Connection -> Index IO (Header (Block cfg))
4848
index epochSlots tracer conn = Index
49-
{ Index.lookup = sqliteLookup epochSlots conn
50-
, tip = sqliteTip epochSlots conn
51-
, rollforward = sqliteRollforward epochSlots tracer conn
52-
, rollbackward = sqliteRollbackward epochSlots tracer conn
49+
{ Index.lookup = sqliteLookup epochSlots conn
50+
, tip = sqliteTip epochSlots conn
51+
, streamFromTip = sqliteStreamFromTip epochSlots conn
52+
, rollforward = sqliteRollforward epochSlots tracer conn
53+
, rollbackward = sqliteRollbackward epochSlots tracer conn
5354
}
5455

5556
-- | Open a new or existing SQLite database. If new, it will set up the schema.
@@ -145,6 +146,24 @@ createTable conn = Sql.execute_ conn sql_create_table
145146
createIndex :: Sql.Connection -> IO ()
146147
createIndex conn = Sql.execute_ conn sql_create_index
147148

149+
convertHashBlob :: ByteString -> IO HeaderHash
150+
convertHashBlob blob = case digestFromByteString blob of
151+
Just hh -> pure (AbstractHash hh)
152+
Nothing -> throwIO $ InvalidHash blob
153+
154+
-- | Convert the database encoding of relative slot to the offset in an
155+
-- epoch. The header hash is taken for error-reporting purposes.
156+
offsetInEpoch :: HeaderHash -> Int -> IO Word64
157+
offsetInEpoch hh i
158+
| i == -1 = pure 0
159+
| i >= 0 = pure $ fromIntegral i
160+
| otherwise = throwIO $ InvalidRelativeSlot hh i
161+
162+
toAbsoluteSlot :: EpochSlots -> HeaderHash -> Word64 -> Int -> IO SlotNo
163+
toAbsoluteSlot epochSlots hh epochNo slotInt = do
164+
offset <- offsetInEpoch hh slotInt
165+
pure $ SlotNo $ unEpochSlots epochSlots * epochNo + offset
166+
148167
-- | The tip is the entry with the highest epoch and slot pair.
149168
sql_get_tip :: Query
150169
sql_get_tip =
@@ -157,18 +176,36 @@ sqliteTip epochSlots conn = do
157176
case rows of
158177
[] -> pure Origin
159178
((hhBlob, epoch, slotInt) : _) -> do
160-
hh <- case digestFromByteString hhBlob of
161-
Just hh -> pure (AbstractHash hh)
162-
Nothing -> throwIO $ InvalidHash hhBlob
163-
offsetInEpoch :: Word64 <-
164-
if slotInt == -1
165-
then pure 0
166-
else if slotInt >= 0
167-
then pure $ fromIntegral slotInt
168-
else throwIO $ InvalidRelativeSlot hh slotInt
169-
let slotNo = SlotNo $ unEpochSlots epochSlots * epoch + offsetInEpoch
179+
hh <- convertHashBlob hhBlob
180+
slotNo <- toAbsoluteSlot epochSlots hh epoch slotInt
170181
pure $ At $ Point.Block slotNo hh
171182

183+
sql_get_all :: Query
184+
sql_get_all =
185+
"SELECT header_hash, epoch, slot FROM block_index\
186+
\ ORDER BY epoch DESC, slot DESC;"
187+
188+
-- | Stream rows from the tip by using the prepared statement and nextRow
189+
-- API.
190+
sqliteStreamFromTip
191+
:: EpochSlots
192+
-> Sql.Connection
193+
-> Index.Fold (Header (Block cfg)) t
194+
-> IO t
195+
sqliteStreamFromTip epochSlots conn fold = Sql.withStatement conn sql_get_all $ \stmt ->
196+
go stmt fold
197+
where
198+
go stmt step = case step of
199+
Index.Stop t -> pure t
200+
Index.More t k -> do
201+
next <- Sql.nextRow stmt
202+
case next of
203+
Nothing -> pure t
204+
Just (hhBlob, epoch, slotInt) -> do
205+
hh <- convertHashBlob hhBlob
206+
slotNo <- toAbsoluteSlot epochSlots hh epoch slotInt
207+
go stmt (k slotNo hh)
208+
172209
sql_get_hash :: Query
173210
sql_get_hash =
174211
"SELECT epoch, slot FROM block_index\
@@ -182,13 +219,8 @@ sqliteLookup epochSlots conn hh@(AbstractHash digest) = do
182219
case rows of
183220
[] -> pure Nothing
184221
((epoch, slotInt) : _) -> do
185-
offsetInEpoch :: Word64 <-
186-
if slotInt == -1
187-
then pure 0
188-
else if slotInt >= 0
189-
then pure $ fromIntegral slotInt
190-
else throwIO $ InvalidRelativeSlot hh slotInt
191-
pure $ Just $ SlotNo $ unEpochSlots epochSlots * epoch + offsetInEpoch
222+
slotNo <- toAbsoluteSlot epochSlots hh epoch slotInt
223+
pure $ Just slotNo
192224

193225
-- | Note that there is a UNIQUE constraint. This will fail if a duplicate
194226
-- entry is inserted.

src/lib/Ouroboros/Byron/Proxy/Index/Types.hs

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
{-# LANGUAGE GADTSyntax #-}
2+
{-# LANGUAGE RankNTypes #-}
23

34
module Ouroboros.Byron.Proxy.Index.Types
45
( Index (..)
6+
, Fold (..)
57
) where
68

79
import Ouroboros.Network.Block (HeaderHash, SlotNo (..))
@@ -19,19 +21,32 @@ import qualified Ouroboros.Network.Point as Point (Block)
1921
data Index m header = Index
2022
{ -- | Lookup the epoch number and relative slot for a given header hash.
2123
-- `Nothing` means it's not in the index.
22-
lookup :: HeaderHash header -> m (Maybe SlotNo)
24+
lookup :: HeaderHash header -> m (Maybe SlotNo)
2325
-- | Check the current tip. `Nothing` means the index is empty. Otherwise,
2426
-- you get the point and also its header hash.
25-
, tip :: m (WithOrigin (Point.Block SlotNo (HeaderHash header)))
27+
, tip :: m (WithOrigin (Point.Block SlotNo (HeaderHash header)))
28+
-- | Lazily fold over all entries in the index beginning at the tip.
29+
, streamFromTip :: forall t . Fold header t -> m t
2630
-- | Extend the index with a new entry. The point must be newer than
2731
-- the latest point in the index (current tip). Whether this is checked
2832
-- or enforced depends upon the implementation.
29-
, rollforward :: header -> m ()
33+
, rollforward :: header -> m ()
3034
-- | Roll back to a given point, making it the tip of the index.
3135
-- TPoint is used because you can rollback to the origin, clearing the
3236
-- index.
3337
-- An index implementation need not actually use the header hash here.
3438
-- It could or could not check that the point actually corresponds to the
3539
-- entry at that hash.
36-
, rollbackward :: WithOrigin (Point.Block SlotNo (HeaderHash header)) -> m ()
40+
, rollbackward :: WithOrigin (Point.Block SlotNo (HeaderHash header)) -> m ()
3741
}
42+
43+
-- | Defined for use in streamFromTip. A fold over the slot number and header
44+
-- hash pairs in an index.
45+
data Fold header t where
46+
Stop :: t -> Fold header t
47+
-- | First argument is for when there are no more entries in the index.
48+
More :: t -> (SlotNo -> HeaderHash header -> Fold header t) -> Fold header t
49+
50+
instance Functor (Fold header) where
51+
fmap f (Stop t) = Stop (f t)
52+
fmap f (More eof k) = More (f eof) (\slot hash -> fmap f (k slot hash))

0 commit comments

Comments
 (0)