Skip to content

Commit

Permalink
ChainDB: use CheckInFuture
Browse files Browse the repository at this point in the history
Previously, we knew the current slot and were able to tell that a block was
from the future by comparing the block's slot against the current slot. For
such blocks we would schedule a chain selection at the block's slot, which
would be performed by a background thread.

Now, we no longer know the current slot. Instead, we validate candidate chains
and use the resulting ledgers to call `CheckInFuture`, which returns the
headers in the candidate fragment that are from the future. We truncate these
headers from the fragment, record that they're from the future
(`cdbFutureBlocks`), and repeat chain selection without them. Headers that are
too far into the future, i.e., exceeding the max clock skew, are not recorded
in `cdbFutureBlocks`, but are recorded as invalid blocks (with
`InFutureExceedsClockSkew` as the `InvalidBlockReason`).

For each new block we receive, we perform chain selection for all future
blocks before performing chain selection for the new block.

* Rename `CandidateSuffix` to `ChainDiff`, split it off into a separate
  module, and use it throughout chain selection instead of only partially. A
  `ChainDiff` is the number of headers to roll back the current chain + a
  fragment containing the new headers to add, i.e., a diff w.r.t. the current
  chain. Previously, we converted such a `ChainDiff` to a `ChainAndLedger`,
  i.e., a fragment starting from the immutable tip (typically containing >= k
  headers) + a ledger matching the tip. Now, we stick to the `ChainDiff` until
  the end, when we actually install the candidate as the new chain by applying
  the diff. Also introduce `ValidatedChainDiff` and use that instead of
  `ChainAndLedger` for the validated candidate. We still use `ChainAndLedger`
  for the current chain.

* Simplify `trySwitchTo` because there is no concurrency thanks to the queue
  introduced in #1709. Remove the obsolete trace message `ChainChangedInBg`.

* New trace messages:
  - `ChainSelectionForFutureBlock`
  - `CandidateContainsFutureBlocks`
  - `CandidateContainsFutureBlocksExceedingClockSkew`

* Remove `chainSelectionPerformed` from `AddBlockPromise` as it was not really
  used and complicated our new handling of blocks from the future.

* Don't mark successors of an invalid block as invalid, as this is redundant,
  see why in `ChainDB.md`. This means we remove the `InChainAfterInvalidBlock`
  constructor of `InvalidBlockReason`.

* Introduce `ChainSelEnv` to reduce the number of parameters to pass around.
  • Loading branch information
mrBliss authored and edsko committed May 1, 2020
1 parent c045537 commit 25dada6
Show file tree
Hide file tree
Showing 24 changed files with 1,000 additions and 1,118 deletions.
14 changes: 2 additions & 12 deletions ouroboros-consensus-byron/tools/db-converter/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import Data.Bifunctor (first)
import qualified Data.ByteString as BS
import Data.Foldable (for_)
import Data.List (sort)
import Data.Proxy (Proxy (..))
import qualified Data.Text as Text
import Data.Time (UTCTime)
import Data.Typeable (Typeable)
Expand All @@ -48,10 +47,9 @@ import Cardano.Crypto (Hash, RequiresNetworkMagic (..),
decodeAbstractHash)
import Cardano.Slotting.Slot

import Ouroboros.Consensus.BlockchainTime
import qualified Ouroboros.Consensus.Fragment.InFuture as InFuture
import qualified Ouroboros.Consensus.Node as Node
import Ouroboros.Consensus.Node.ProtocolInfo (ProtocolInfo (..))
import Ouroboros.Consensus.Node.Run
import Ouroboros.Consensus.Util.Condense (condense)
import Ouroboros.Consensus.Util.IOLike (atomically)
import Ouroboros.Consensus.Util.Orphans ()
Expand All @@ -62,7 +60,6 @@ import Ouroboros.Consensus.Storage.ChainDB.Impl.Args (fromChainDbArgs)
import qualified Ouroboros.Consensus.Storage.ChainDB.Impl.ImmDB as ImmDB
import Ouroboros.Consensus.Storage.ImmutableDB (simpleChunkInfo)

import Ouroboros.Consensus.Byron.Ledger (ByronBlock)
import qualified Ouroboros.Consensus.Byron.Ledger as Byron
import Ouroboros.Consensus.Byron.Node

Expand Down Expand Up @@ -167,12 +164,7 @@ validateChainDb
-> IO ()
validateChainDb dbDir genesisConfig onlyImmDB verbose =
withRegistry $ \registry -> do
btime <- simpleBlockchainTime
registry
nullTracer
(nodeStartTime (Proxy @ByronBlock) cfg)
slotLength
let chainDbArgs = mkChainDbArgs registry btime
let chainDbArgs = mkChainDbArgs registry InFuture.dontCheck
(immDbArgs, _, _, _) = fromChainDbArgs chainDbArgs
if onlyImmDB then
ImmDB.withImmDB immDbArgs $ \immDB -> do
Expand All @@ -183,8 +175,6 @@ validateChainDb dbDir genesisConfig onlyImmDB verbose =
chainDbTipPoint <- atomically $ ChainDB.getTipPoint chainDB
putStrLn $ "DB tip: " ++ condense chainDbTipPoint
where
-- This converts the old chain, which has a 20s slot length.
slotLength = slotLengthFromSec 20
ProtocolInfo { pInfoInitLedger = initLedger, pInfoConfig = cfg } =
protocolInfoByron
genesisConfig
Expand Down
20 changes: 15 additions & 5 deletions ouroboros-consensus/docs/ChainDB.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,8 +207,8 @@ The initialization of the chain DB proceeds as follows.
validate each candidate chain fragment, starting with `L` each
time[^ledgerState]. As soon as we find a candidate that is valid, we adopt
it as our current chain. If we find a candidate that is _invalid_, we mark
the invalid block and all its successors as invalid[^invalidSuccessors],
and go back[^whyGoBack] to step (2).
the invalid block[^invalidSuccessors], and go back[^whyGoBack] to step
(2).
[^ledgerState]: We make no attempt to share ledger states between candidates,
even if they share a common prefix, trading runtime performance for lower memory
Expand All @@ -218,9 +218,19 @@ pressure.
invalid because (1) those blocks may also exist in other candidates and (2) we
do not know how the valid prefixes of those candidates should now be ordered.
[^invalidSuccessors]: The chain sync client also depends on this information to
terminate connections to nodes that produce invalid blocks, so it is important
to mark _all_ invalid blocks.
[^invalidSuccessors]: We do not need to also mark the successors of the
invalid block as invalid. The chain sync client will use this information to
terminate connections to nodes with a chain that contains an invalid block.
Say the node has the following chain:
```
A -> I -> C
```
where `I` is an invalid block. It is impossible for there to be a candidate
chain containing `C`, but not `I`, which means that it is not necessary to
also mark `C` (and any other successors) as invalid. Proof: every chain sync
candidate fragment is anchored at a point on _our_ chain, and since `I` is
invalid, we will never adopt `I`. So if a candidate fragment contains `C` and
is anchored on our chain, it must also contain `I`.
[^selectThenValidate]: Technically speaking we should _first_ validate all
chains, and then apply selection only to the valid chains. We run chain selection
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import qualified Ouroboros.Network.TxSubmission.Outbound as TxOutbound
import Ouroboros.Consensus.Block
import Ouroboros.Consensus.BlockchainTime
import Ouroboros.Consensus.Config
import qualified Ouroboros.Consensus.Fragment.InFuture as InFuture
import Ouroboros.Consensus.Ledger.Abstract
import Ouroboros.Consensus.Ledger.Extended
import Ouroboros.Consensus.Ledger.SupportsProtocol
Expand Down Expand Up @@ -601,7 +602,9 @@ runThreadNetwork ThreadNetworkArgs
, cdbIsEBB = nodeIsEBB
, cdbCheckIntegrity = nodeCheckIntegrity cfg
, cdbGenesis = return initLedger
, cdbBlockchainTime = testBlockchainTime btime
, cdbCheckInFuture = InFuture.miracle
(testBlockchainTimeSlot btime)
1 -- One slot clock skew
, cdbAddHdrEnv = nodeAddHeaderEnvelope (Proxy @blk)
, cdbImmDbCacheConfig = Index.CacheConfig 2 60
-- Misc
Expand Down
4 changes: 2 additions & 2 deletions ouroboros-consensus/ouroboros-consensus.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,10 @@ library
Ouroboros.Consensus.BlockchainTime.WallClock
Ouroboros.Consensus.Config
Ouroboros.Consensus.Forecast
Ouroboros.Consensus.Fragment.Diff
Ouroboros.Consensus.Fragment.InFuture
Ouroboros.Consensus.Fragment.Validated
Ouroboros.Consensus.Fragment.ValidatedDiff
Ouroboros.Consensus.HardFork.Abstract
Ouroboros.Consensus.HardFork.History
Ouroboros.Consensus.HeaderValidation
Expand Down Expand Up @@ -376,8 +378,6 @@ test-suite test-storage
Test.Ouroboros.Storage.ChainDB.AddBlock
Test.Ouroboros.Storage.ChainDB.Iterator
Test.Ouroboros.Storage.ChainDB.GcSchedule
Test.Ouroboros.Storage.ChainDB.Mock
Test.Ouroboros.Storage.ChainDB.Mock.Test
Test.Ouroboros.Storage.ChainDB.Model
Test.Ouroboros.Storage.ChainDB.Model.Test
Test.Ouroboros.Storage.ChainDB.StateMachine
Expand Down
17 changes: 0 additions & 17 deletions ouroboros-consensus/src/Ouroboros/Consensus/BlockchainTime/API.hs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
module Ouroboros.Consensus.BlockchainTime.API (
BlockchainTime(..)
, onKnownSlotChange
-- * Testing
, settableBlockchainTime
) where

import GHC.Stack
Expand Down Expand Up @@ -54,18 +52,3 @@ onKnownSlotChange :: (IOLike m, HasCallStack)
onKnownSlotChange registry BlockchainTime{getCurrentSlot} label =
fmap cancelThread
. onEachChange registry label id Nothing getCurrentSlot

{-------------------------------------------------------------------------------
Test infrastructure
TODO: these will go after
<https://github.com/input-output-hk/ouroboros-network/pull/1989>
-------------------------------------------------------------------------------}

-- | The current slot can be changed by modifying the given 'StrictTVar'.
--
-- 'onSlotChange_' is not implemented and will return an 'error'.
settableBlockchainTime :: MonadSTM m => StrictTVar m SlotNo -> BlockchainTime m
settableBlockchainTime varCurSlot = BlockchainTime {
getCurrentSlot = readTVar varCurSlot
}
173 changes: 173 additions & 0 deletions ouroboros-consensus/src/Ouroboros/Consensus/Fragment/Diff.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE UndecidableInstances #-}
-- | Intended for qualified import
--
-- > import Ouroboros.Consensus.Fragment.Diff (ChainDiff (..))
-- > import qualified Ouroboros.Consensus.Fragment.Diff as Diff
module Ouroboros.Consensus.Fragment.Diff
( ChainDiff(ChainDiff)
-- * Queries
, getRollback
, getSuffix
, getTip
, getAnchorPoint
-- * Constructors
, extend
, diff
-- * Application
, apply
-- * Manipulation
, truncate
, takeWhileOldest
) where

import Prelude hiding (truncate)

import Data.Word (Word64)
import GHC.Stack (HasCallStack)

import Ouroboros.Network.AnchoredFragment (AnchoredFragment (..))
import qualified Ouroboros.Network.AnchoredFragment as AF
import Ouroboros.Network.Block (HasHeader, Point, castPoint)

import Ouroboros.Consensus.Block (Header)


-- | A diff of a chain (fragment).
--
-- INVARIANT: the length of the suffix must always be >= the rollback
--
-- Note: we allow the suffix with new headers to be empty, even though it is
-- rather pointless. Allowing empty ones makes working with them easier: fewer
-- cases to deal with. Without any headers, the rollback must be 0, so such a
-- diff would be an empty diff.
data ChainDiff blk = UnsafeChainDiff
{ getRollback :: !Word64
-- ^ The number of headers to roll back the current chain
, getSuffix :: !(AnchoredFragment (Header blk))
-- ^ The new headers to add after rolling back the current chain.
}

deriving instance (HasHeader blk, Eq (Header blk))
=> Eq (ChainDiff blk)
deriving instance (HasHeader blk, Show (Header blk))
=> Show (ChainDiff blk)

-- | Allow for pattern matching on a 'ChainDiff' without exposing the (unsafe)
-- constructor. Use 'extend' and 'diff' to construct a 'ChainDiff'.
pattern ChainDiff
:: Word64 -> AnchoredFragment (Header blk) -> ChainDiff blk
pattern ChainDiff r s <- UnsafeChainDiff r s
{-# COMPLETE ChainDiff #-}

-- | Internal. Return 'Nothing' if the length of the suffix < the rollback.
mkRollback
:: HasHeader (Header blk)
=> Word64
-> AnchoredFragment (Header blk)
-> Maybe (ChainDiff blk)
mkRollback nbRollback suffix
| fromIntegral (AF.length suffix) >= nbRollback
= Just $ UnsafeChainDiff nbRollback suffix
| otherwise
= Nothing

{-------------------------------------------------------------------------------
Queries
-------------------------------------------------------------------------------}

-- | Return the tip of the new suffix
getTip :: HasHeader (Header blk) => ChainDiff blk -> Point blk
getTip = castPoint . AF.headPoint . getSuffix

-- | Return the anchor point of the new suffix
getAnchorPoint :: ChainDiff blk -> Point blk
getAnchorPoint = castPoint . AF.anchorPoint . getSuffix

{-------------------------------------------------------------------------------
Constructors
-------------------------------------------------------------------------------}

-- | Make an extension-only (no rollback) 'ChainDiff'.
extend :: AnchoredFragment (Header blk) -> ChainDiff blk
extend = UnsafeChainDiff 0

-- | Diff a candidate chain with the current chain.
--
-- If the candidate fragment is shorter than the current chain, 'Nothing' is
-- returned (this would violate the invariant of 'ChainDiff').
--
-- PRECONDITION: the candidate fragment must intersect with the current chain
-- fragment.
diff
:: (HasHeader (Header blk), HasCallStack)
=> AnchoredFragment (Header blk) -- ^ Current chain
-> AnchoredFragment (Header blk) -- ^ Candidate chain
-> Maybe (ChainDiff blk)
diff curChain candChain =
case AF.intersect curChain candChain of
Just (_curChainPrefix, _candPrefix, curChainSuffix, candSuffix)
-> mkRollback
(fromIntegral (AF.length curChainSuffix))
candSuffix
-- Precondition violated.
_ -> error "candidate fragment doesn't intersect with current chain"

{-------------------------------------------------------------------------------
Application
-------------------------------------------------------------------------------}

-- | Apply the 'ChainDiff' on the given chain fragment.
--
-- The fragment is first rolled back a number of blocks before appending the
-- new suffix.
--
-- If the 'ChainDiff' doesn't fit (anchor point mismatch), 'Nothing' is
-- returned.
--
-- The returned fragment will have the same anchor point as the given
-- fragment.
apply
:: HasHeader (Header blk)
=> AnchoredFragment (Header blk)
-> ChainDiff blk
-> Maybe (AnchoredFragment (Header blk))
apply curChain (ChainDiff nbRollback suffix) =
AF.join (AF.dropNewest (fromIntegral nbRollback) curChain) suffix

{-------------------------------------------------------------------------------
Manipulation
-------------------------------------------------------------------------------}

-- | Truncate the diff by rolling back the new suffix to the given point.
--
-- PRECONDITION: the given point must correspond to one of the new headers of
-- the new suffix or its anchor (i.e, @'AF.withinFragmentBounds' pt (getSuffix
-- diff)@).
--
-- If the length of the truncated suffix is shorter than the rollback,
-- 'Nothing' is returned.
truncate
:: (HasHeader (Header blk), HasCallStack, HasHeader blk)
=> Point blk
-> ChainDiff blk
-> Maybe (ChainDiff blk)
truncate pt (ChainDiff nbRollback suffix)
| Just suffix' <- AF.rollback (castPoint pt) suffix
= mkRollback nbRollback suffix'
| otherwise
= error $ "rollback point not on the candidate suffix: " <> show pt

-- | Return the longest prefix of the suffix matching the given predicate,
-- starting from the left, i.e., the \"oldest\" blocks.
--
-- If the new suffix is shorter than the diff's rollback, return 'Nothing'.
takeWhileOldest
:: HasHeader (Header blk)
=> (Header blk -> Bool)
-> ChainDiff blk
-> Maybe (ChainDiff blk)
takeWhileOldest accept (ChainDiff nbRollback suffix) =
mkRollback nbRollback (AF.takeWhileOldest accept suffix)
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{-# LANGUAGE PatternSynonyms #-}
-- | Intended for qualified import
--
-- > import Ouroboros.Consensus.Fragment.ValidatedDiff (ValidatedChainDiff (..))
-- > import qualified Ouroboros.Consensus.Fragment.ValidatedDiff as ValidatedDiff
module Ouroboros.Consensus.Fragment.ValidatedDiff
( ValidatedChainDiff(ValidatedChainDiff)
, getChainDiff
, getLedger
, new
, toValidatedFragment
) where

import Control.Monad.Except (throwError)
import GHC.Stack (HasCallStack)

import Ouroboros.Consensus.Fragment.Diff
import Ouroboros.Consensus.Fragment.Validated (ValidatedFragment)
import qualified Ouroboros.Consensus.Fragment.Validated as VF
import Ouroboros.Consensus.Ledger.Abstract
import Ouroboros.Consensus.Util.Assert

-- | A 'ChainDiff' along with the ledger state after validation.
--
-- INVARIANT:
--
-- > getTip chainDiff == ledgerTipPoint ledger
data ValidatedChainDiff blk l = UnsafeValidatedChainDiff
{ getChainDiff :: ChainDiff blk
, getLedger :: l
}

-- | Allow for pattern matching on a 'ValidatedChainDiff' without exposing the
-- (unsafe) constructor. Use 'new' to construct a 'ValidatedChainDiff'.
pattern ValidatedChainDiff
:: ChainDiff blk -> l -> ValidatedChainDiff blk l
pattern ValidatedChainDiff d l <- UnsafeValidatedChainDiff d l
{-# COMPLETE ValidatedChainDiff #-}

-- | Create a 'ValidatedChainDiff'.
--
-- PRECONDITION:
--
-- > getTip chainDiff == ledgerTipPoint ledger
new
:: (ApplyBlock l blk, HasCallStack)
=> ChainDiff blk
-> l
-> ValidatedChainDiff blk l
new chainDiff ledger =
assertWithMsg precondition $
UnsafeValidatedChainDiff chainDiff ledger
where
chainDiffTip = getTip chainDiff
ledgerTip = ledgerTipPoint ledger
precondition
| chainDiffTip == ledgerTip
= return ()
| otherwise
= throwError $
"tip of ChainDiff doesn't match ledger: " <>
show chainDiffTip <> " /= " <> show ledgerTip

toValidatedFragment
:: (ApplyBlock l blk, HasCallStack)
=> ValidatedChainDiff blk l
-> ValidatedFragment blk l
toValidatedFragment (UnsafeValidatedChainDiff cs l) =
VF.new (getSuffix cs) l
Loading

0 comments on commit 25dada6

Please sign in to comment.