Skip to content

Commit 330452e

Browse files
committed
consensus: do not even propagate future headers
This an aggressively-simple interpretation of the easy part of Ouroboros Chronos in the presence of The Header-Body Split. (The hard part is the _synchronization beacons_---which need to be propagated promptly, and will be implemented much later.) Other less-aggressive interpretations would propagate future headers/blocks but set them aside. But this seems much simpler and within an RTT or two, assuming eg the NTP clients are well-configured.
1 parent ce13b8b commit 330452e

File tree

5 files changed

+392
-103
lines changed

5 files changed

+392
-103
lines changed

ouroboros-consensus/ouroboros-consensus.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ library
141141
Ouroboros.Consensus.MiniProtocol.BlockFetch.ClientInterface
142142
Ouroboros.Consensus.MiniProtocol.BlockFetch.Server
143143
Ouroboros.Consensus.MiniProtocol.ChainSync.Client
144+
Ouroboros.Consensus.MiniProtocol.ChainSync.Client.InFutureCheck
144145
Ouroboros.Consensus.MiniProtocol.ChainSync.Server
145146
Ouroboros.Consensus.MiniProtocol.LocalStateQuery.Server
146147
Ouroboros.Consensus.MiniProtocol.LocalTxMonitor.Server

ouroboros-consensus/src/ouroboros-consensus/Ouroboros/Consensus/Fragment/InFuture.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ module Ouroboros.Consensus.Fragment.InFuture (
1818
, defaultClockSkew
1919
-- ** opaque
2020
, ClockSkew
21+
, unClockSkew
2122
-- * Testing
2223
, dontCheck
2324
, miracle

ouroboros-consensus/src/ouroboros-consensus/Ouroboros/Consensus/MiniProtocol/ChainSync/Client.hs

Lines changed: 156 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -51,19 +51,23 @@ import NoThunks.Class (unsafeNoThunks)
5151
import Ouroboros.Consensus.Block
5252
import Ouroboros.Consensus.Config
5353
import Ouroboros.Consensus.Forecast
54+
import Ouroboros.Consensus.HardFork.History (PastHorizonException (PastHorizon))
5455
import Ouroboros.Consensus.HeaderStateHistory
5556
(HeaderStateHistory (..), validateHeader)
5657
import qualified Ouroboros.Consensus.HeaderStateHistory as HeaderStateHistory
5758
import Ouroboros.Consensus.HeaderValidation hiding (validateHeader)
59+
import Ouroboros.Consensus.Ledger.Basics (LedgerState)
5860
import Ouroboros.Consensus.Ledger.Extended
5961
import Ouroboros.Consensus.Ledger.SupportsProtocol
62+
import qualified Ouroboros.Consensus.MiniProtocol.ChainSync.Client.InFutureCheck as InFutureCheck
6063
import Ouroboros.Consensus.Node.NetworkProtocolVersion
6164
import Ouroboros.Consensus.Protocol.Abstract
6265
import Ouroboros.Consensus.Storage.ChainDB (ChainDB,
6366
InvalidBlockReason)
6467
import qualified Ouroboros.Consensus.Storage.ChainDB as ChainDB
6568
import Ouroboros.Consensus.Util
6669
import Ouroboros.Consensus.Util.Assert (assertWithMsg)
70+
import qualified Ouroboros.Consensus.Util.EarlyExit as EarlyExit
6771
import Ouroboros.Consensus.Util.IOLike
6872
import Ouroboros.Consensus.Util.STM (Fingerprint, Watcher (..),
6973
WithFingerprint (..), withWatcher)
@@ -426,13 +430,20 @@ chainSyncClient
426430
=> MkPipelineDecision
427431
-> Tracer m (TraceChainSyncClientEvent blk)
428432
-> TopLevelConfig blk
433+
-> InFutureCheck.HeaderInFutureCheck m blk
429434
-> ChainDbView m blk
430435
-> NodeToNodeVersion
431436
-> ControlMessageSTM m
432437
-> HeaderMetricsTracer m
433438
-> StrictTVar m (AnchoredFragment (Header blk))
434439
-> Consensus ChainSyncClientPipelined blk m
435440
chainSyncClient mkPipelineDecision0 tracer cfg
441+
InFutureCheck.HeaderInFutureCheck
442+
{ handleHeaderArrival
443+
, judgeHeaderArrival
444+
, proxyArrival = Proxy :: Proxy arrival
445+
, recordHeaderArrival
446+
}
436447
ChainDbView
437448
{ getCurrentChain
438449
, getHeaderStateHistory
@@ -706,104 +717,151 @@ chainSyncClient mkPipelineDecision0 tracer cfg
706717
(ClientPipelinedStIdle n)
707718
rollForward mkPipelineDecision n hdr theirTip
708719
= Stateful $ \kis -> traceException $ do
709-
now <- getMonotonicTime
710-
let hdrPoint = headerPoint hdr
711-
712-
isInvalidBlock <- atomically $ forgetFingerprint <$> getIsInvalidBlock
713-
let disconnectWhenInvalid = \case
714-
GenesisHash -> pure ()
715-
BlockHash hash ->
720+
arrival <- recordHeaderArrival hdr
721+
now <- getMonotonicTime
722+
let hdrPoint = headerPoint hdr
723+
slotNo = blockSlot hdr
724+
725+
do
726+
let scrutinee =
727+
case isPipeliningEnabled version of
728+
NotReceivingTentativeBlocks -> BlockHash (headerHash hdr)
729+
-- Disconnect if the parent block of `hdr` is known to be invalid.
730+
ReceivingTentativeBlocks -> headerPrevHash hdr
731+
case scrutinee of
732+
GenesisHash -> return ()
733+
BlockHash hash -> do
734+
-- If the peer is sending headers quickly, the
735+
-- @invalidBlockWatcher@ might miss one. So this call is a
736+
-- lightweight supplement. Note that neither check /must/ be 100%
737+
-- reliable.
738+
isInvalidBlock <- atomically $ forgetFingerprint <$> getIsInvalidBlock
716739
whenJust (isInvalidBlock hash) $ \reason ->
717740
disconnect $ InvalidBlock hdrPoint hash reason
718-
disconnectWhenInvalid $
719-
case isPipeliningEnabled version of
720-
-- Disconnect if the parent block of `hdr` is known to be invalid.
721-
ReceivingTentativeBlocks -> headerPrevHash hdr
722-
NotReceivingTentativeBlocks -> BlockHash (headerHash hdr)
723-
724-
-- Get the ledger view required to validate the header
725-
-- NOTE: This will block if we are too far behind.
726-
intersectCheck <- atomically $ do
727-
-- Before obtaining a 'LedgerView', we must find the most recent
728-
-- intersection with the current chain. Note that this is cheap when
729-
-- the chain and candidate haven't changed.
741+
742+
mLedgerView <- EarlyExit.withEarlyExit $ do
743+
Intersects kis2 lst <- checkArrivalTime kis arrival
744+
Intersects kis3 ledgerView <- case projectLedgerView slotNo lst of
745+
Just ledgerView -> pure $ Intersects kis2 ledgerView
746+
Nothing -> readLedgerState kis2 (projectLedgerView slotNo)
747+
pure $ Intersects kis3 ledgerView
748+
749+
case mLedgerView of
750+
751+
Nothing -> do
752+
-- The above computation exited early, which means our chain (tip)
753+
-- has changed and it no longer intersects with the candidate
754+
-- fragment, so we have to find a new intersection. But first drain
755+
-- the pipe.
756+
continueWithState ()
757+
$ drainThePipe n
758+
$ findIntersection NoMoreIntersection
759+
760+
Just (Intersects kis' ledgerView) -> do
761+
-- Our chain still intersects with the candidate fragment and we
762+
-- have obtained a 'LedgerView' that we can use to validate @hdr@.
763+
let KnownIntersectionState {
764+
ourFrag
765+
, theirFrag
766+
, theirHeaderStateHistory
767+
, mostRecentIntersection
768+
} = kis'
769+
770+
-- Validate header
771+
theirHeaderStateHistory' <-
772+
case runExcept $ validateHeader cfg ledgerView hdr theirHeaderStateHistory of
773+
Right theirHeaderStateHistory' -> return theirHeaderStateHistory'
774+
Left vErr ->
775+
disconnect $
776+
HeaderError hdrPoint vErr (ourTipFromChain ourFrag) theirTip
777+
778+
let theirFrag' = theirFrag :> hdr
779+
-- Advance the most recent intersection if we have the same
780+
-- header on our fragment too. This is cheaper than recomputing
781+
-- the intersection from scratch.
782+
mostRecentIntersection'
783+
| Just ourSuccessor <-
784+
AF.successorBlock (castPoint mostRecentIntersection) ourFrag
785+
, headerHash ourSuccessor == headerHash hdr
786+
= headerPoint hdr
787+
| otherwise
788+
= mostRecentIntersection
789+
kis'' = assertKnownIntersectionInvariants (configConsensus cfg) $
790+
KnownIntersectionState {
791+
theirFrag = theirFrag'
792+
, theirHeaderStateHistory = theirHeaderStateHistory'
793+
, ourFrag = ourFrag
794+
, mostRecentIntersection = mostRecentIntersection'
795+
}
796+
atomically $ writeTVar varCandidate theirFrag'
797+
atomically $ traceWith headerMetricsTracer (slotNo, now)
798+
799+
continueWithState kis'' $ nextStep mkPipelineDecision n theirTip
800+
801+
-- Used in 'rollForward': determines whether the header is from the future,
802+
-- and handle that fact if so. Also return the ledger state used for the
803+
-- determination.
804+
--
805+
-- Relies on 'readLedgerState'.
806+
checkArrivalTime :: KnownIntersectionState blk
807+
-> arrival
808+
-> EarlyExit.WithEarlyExit m (Intersects blk (LedgerState blk))
809+
checkArrivalTime kis arrival = do
810+
Intersects kis' (lst, judgment) <- readLedgerState kis $ \lst ->
811+
case runExcept $ judgeHeaderArrival (configLedger cfg) lst arrival of
812+
Left PastHorizon{} -> Nothing
813+
Right judgment -> Just (lst, judgment)
814+
815+
-- For example, throw an exception if the header is from the far
816+
-- future.
817+
EarlyExit.lift $ handleHeaderArrival judgment >>= \case
818+
Just exn -> disconnect (InFutureHeaderExceedsClockSkew exn)
819+
Nothing -> return $ Intersects kis' lst
820+
821+
-- Used in 'rollForward': block until the the ledger state at the
822+
-- intersection with the local selection returns 'Just'.
823+
--
824+
-- Exits early if the intersection no longer exists.
825+
readLedgerState :: KnownIntersectionState blk
826+
-> (LedgerState blk -> Maybe a)
827+
-> EarlyExit.WithEarlyExit m (Intersects blk a)
828+
readLedgerState kis prj = join $ EarlyExit.lift $ atomically $ do
829+
-- We must first find the most recent intersection with the current
830+
-- chain. Note that this is cheap when the chain and candidate haven't
831+
-- changed.
730832
mKis' <- intersectsWithCurrentChain kis
731833
case mKis' of
732-
Nothing -> return NoLongerIntersects
834+
Nothing -> return EarlyExit.exitEarly
733835
Just kis'@KnownIntersectionState { mostRecentIntersection } -> do
734-
-- We're calling 'ledgerViewForecastAt' in the same STM transaction
735-
-- as 'intersectsWithCurrentChain'. This guarantees the former's
736-
-- precondition: the intersection is within the last @k@ blocks of
737-
-- the current chain.
738-
forecast <-
836+
lst <-
739837
maybe
740838
(error $
741839
"intersection not within last k blocks: " <> show mostRecentIntersection)
742-
(ledgerViewForecastAt (configLedger cfg) . ledgerState)
840+
ledgerState
743841
<$> getPastLedger mostRecentIntersection
744842

745-
case runExcept $ forecastFor forecast (blockSlot hdr) of
746-
-- The header is too far ahead of the intersection point with our
747-
-- current chain. We have to wait until our chain and the
748-
-- intersection have advanced far enough. This will wait on
749-
-- changes to the current chain via the call to
750-
-- 'intersectsWithCurrentChain' before it.
751-
Left OutsideForecastRange{} ->
752-
retry
753-
Right ledgerView ->
754-
return $ Intersects kis' ledgerView
755-
756-
case intersectCheck of
757-
NoLongerIntersects ->
758-
-- Our chain (tip) has changed and it no longer intersects with the
759-
-- candidate fragment, so we have to find a new intersection, but
760-
-- first drain the pipe.
761-
continueWithState ()
762-
$ drainThePipe n
763-
$ findIntersection NoMoreIntersection
764-
765-
Intersects kis' ledgerView -> do
766-
-- Our chain still intersects with the candidate fragment and we
767-
-- have obtained a 'LedgerView' that we can use to validate @hdr@.
768-
769-
let KnownIntersectionState {
770-
ourFrag
771-
, theirFrag
772-
, theirHeaderStateHistory
773-
, mostRecentIntersection
774-
} = kis'
775-
776-
-- Validate header
777-
theirHeaderStateHistory' <-
778-
case runExcept $ validateHeader cfg ledgerView hdr theirHeaderStateHistory of
779-
Right theirHeaderStateHistory' -> return theirHeaderStateHistory'
780-
Left vErr ->
781-
disconnect $
782-
HeaderError hdrPoint vErr (ourTipFromChain ourFrag) theirTip
783-
784-
let theirFrag' = theirFrag :> hdr
785-
-- Advance the most recent intersection if we have the same header
786-
-- on our fragment too. This is cheaper than recomputing the
787-
-- intersection from scratch.
788-
mostRecentIntersection'
789-
| Just ourSuccessor <-
790-
AF.successorBlock (castPoint mostRecentIntersection) ourFrag
791-
, headerHash ourSuccessor == headerHash hdr
792-
= headerPoint hdr
793-
| otherwise
794-
= mostRecentIntersection
795-
kis'' = assertKnownIntersectionInvariants (configConsensus cfg) $
796-
KnownIntersectionState {
797-
theirFrag = theirFrag'
798-
, theirHeaderStateHistory = theirHeaderStateHistory'
799-
, ourFrag = ourFrag
800-
, mostRecentIntersection = mostRecentIntersection'
801-
}
802-
atomically $ writeTVar varCandidate theirFrag'
803-
let slotNo = blockSlot hdr
804-
atomically $ traceWith headerMetricsTracer (slotNo, now)
805-
806-
continueWithState kis'' $ nextStep mkPipelineDecision n theirTip
843+
case prj lst of
844+
Nothing -> retry
845+
Just ledgerView -> return $ return $ Intersects kis' ledgerView
846+
847+
-- Used in 'rollForward': returns 'Nothing' if the ledger state cannot
848+
-- forecast the ledger view that far into the future.
849+
projectLedgerView :: SlotNo
850+
-> LedgerState blk
851+
-> Maybe (LedgerView (BlockProtocol blk))
852+
projectLedgerView slot lst =
853+
let forecast = ledgerViewForecastAt (configLedger cfg) lst
854+
-- TODO cache this in the KnownIntersectionState? Or even in the
855+
-- LedgerDB?
856+
in
857+
case runExcept $ forecastFor forecast slot of
858+
-- The header is too far ahead of the intersection point with our
859+
-- current chain. We have to wait until our chain and the
860+
-- intersection have advanced far enough. This will wait on
861+
-- changes to the current chain via the call to
862+
-- 'intersectsWithCurrentChain' before it.
863+
Left OutsideForecastRange{} -> Nothing
864+
Right ledgerView -> Just ledgerView
807865

808866
rollBackward :: MkPipelineDecision
809867
-> Nat n
@@ -1024,16 +1082,10 @@ invalidBlockRejector tracer version getIsInvalidBlock getCandidate =
10241082
throwIO ex
10251083

10261084
-- | Auxiliary data type used as an intermediary result in 'rollForward'.
1027-
data IntersectCheck blk =
1028-
-- | The upstream chain no longer intersects with our current chain because
1029-
-- our current chain changed in the background.
1030-
NoLongerIntersects
1031-
-- | The upstream chain still intersects with our chain, return the
1032-
-- resulting 'KnownIntersectionState' and the 'LedgerView' corresponding to
1033-
-- the header 'rollForward' received.
1034-
| Intersects
1035-
(KnownIntersectionState blk)
1036-
(LedgerView (BlockProtocol blk))
1085+
data Intersects blk a =
1086+
Intersects
1087+
(KnownIntersectionState blk)
1088+
a
10371089

10381090
{-------------------------------------------------------------------------------
10391091
Explicit state
@@ -1159,6 +1211,8 @@ data ChainSyncClientException =
11591211
-- different from the previous argument.
11601212
(InvalidBlockReason blk)
11611213

1214+
| InFutureHeaderExceedsClockSkew !InFutureCheck.HeaderArrivalException
1215+
11621216
deriving instance Show ChainSyncClientException
11631217

11641218
instance Eq ChainSyncClientException where
@@ -1180,6 +1234,10 @@ instance Eq ChainSyncClientException where
11801234
Just Refl -> (a, b, c) == (a', b', c')
11811235
InvalidBlock{} == _ = False
11821236

1237+
InFutureHeaderExceedsClockSkew a == InFutureHeaderExceedsClockSkew a' =
1238+
a == a'
1239+
InFutureHeaderExceedsClockSkew{} == _ = False
1240+
11831241
instance Exception ChainSyncClientException
11841242

11851243
{-------------------------------------------------------------------------------

0 commit comments

Comments
 (0)