Skip to content

Commit

Permalink
Introduce header validation as concept
Browse files Browse the repository at this point in the history
This then paves the way for introducing additional checks
(which this doesn't do yet).
  • Loading branch information
edsko committed Feb 5, 2020
1 parent 972cc8c commit f7703d0
Show file tree
Hide file tree
Showing 18 changed files with 291 additions and 114 deletions.
1 change: 1 addition & 0 deletions ouroboros-consensus/ouroboros-consensus.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ library
Ouroboros.Consensus.ChainSyncClient
Ouroboros.Consensus.ChainSyncServer
Ouroboros.Consensus.Crypto.DSIGN.Cardano
Ouroboros.Consensus.Header
Ouroboros.Consensus.Ledger.Abstract
Ouroboros.Consensus.Ledger.Byron
Ouroboros.Consensus.Ledger.Byron.Auxiliary
Expand Down
103 changes: 50 additions & 53 deletions ouroboros-consensus/src/Ouroboros/Consensus/ChainSyncClient.hs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import Ouroboros.Network.Protocol.ChainSync.PipelineDecision

import Ouroboros.Consensus.Block
import Ouroboros.Consensus.BlockchainTime
import Ouroboros.Consensus.Header
import Ouroboros.Consensus.Ledger.Abstract
import Ouroboros.Consensus.Ledger.Extended
import Ouroboros.Consensus.Protocol.Abstract
Expand Down Expand Up @@ -180,7 +181,7 @@ bracketChainSyncClient tracer ChainDbView { getIsInvalidBlock } varCandidates
-- | State used when the intersection between the candidate and the current
-- chain is unknown.
data UnknownIntersectionState blk = UnknownIntersectionState
{ ourFrag :: !(AnchoredFragment (Header blk))
{ ourFrag :: !(AnchoredFragment (Header blk))
-- ^ A view of the current chain fragment. Note that this might be
-- temporarily out of date w.r.t. the actual current chain until we update
-- it again.
Expand All @@ -189,10 +190,10 @@ data UnknownIntersectionState blk = UnknownIntersectionState
-- with the candidate.
--
-- INVARIANT: 'ourFrag' contains @k@ headers, unless close to genesis.
, ourChainState :: !(ChainState (BlockProtocol blk))
-- ^ 'ChainState' corresponding to the tip (most recent block) of
, ourHeaderState :: !(HeaderState blk)
-- ^ 'HeaderState' corresponding to the tip (most recent block) of
-- 'ourFrag'.
, ourTip :: !(Our (Tip blk))
, ourTip :: !(Our (Tip blk))
-- ^ INVARIANT: must correspond to the tip of 'ourFrag'.
}
deriving (Generic)
Expand All @@ -204,12 +205,12 @@ instance ( ProtocolLedgerView blk
-- | State used when the intersection between the candidate and the current
-- chain is known.
data KnownIntersectionState blk = KnownIntersectionState
{ theirFrag :: !(AnchoredFragment (Header blk))
{ theirFrag :: !(AnchoredFragment (Header blk))
-- ^ The candidate, the synched fragment of their chain.
, theirChainState :: !(ChainState (BlockProtocol blk))
-- ^ 'ChainState' corresponding to the tip (most recent block) of
, theirHeaderState :: !(HeaderState blk)
-- ^ 'HeaderState' corresponding to the tip (most recent block) of
-- 'theirFrag'.
, ourFrag :: !(AnchoredFragment (Header blk))
, ourFrag :: !(AnchoredFragment (Header blk))
-- ^ A view of the current chain fragment used to maintain the invariants
-- with. Note that this might be temporarily out of date w.r.t. the actual
-- current chain until we update it again.
Expand All @@ -220,7 +221,7 @@ data KnownIntersectionState blk = KnownIntersectionState
-- this follows that both fragments intersect. This also means that
-- 'theirFrag' forks off within the last @k@ headers/blocks of the
-- 'ourFrag'.
, ourTip :: !(Our (Tip blk))
, ourTip :: !(Our (Tip blk))
-- ^ INVARIANT: must correspond to the tip of 'ourFrag'.
}
deriving (Generic)
Expand Down Expand Up @@ -274,9 +275,9 @@ chainSyncClient mkPipelineDecision0 tracer cfg btime
-- ^ Exception to throw when no intersection is found.
-> Stateful m blk () (ClientPipelinedStIdle Z)
findIntersection mkEx = Stateful $ \() -> do
(ourFrag, ourChainState, ourTip) <- atomically $ (,,)
(ourFrag, ourHeaderState, ourTip) <- atomically $ (,,)
<$> getCurrentChain
<*> (ouroborosChainState <$> getCurrentLedger)
<*> (headerState <$> getCurrentLedger)
<*> (Our <$> getOurTip)
-- We select points from the last @k@ headers of our current chain. This
-- means that if an intersection is found for one of these points, it
Expand All @@ -287,9 +288,9 @@ chainSyncClient mkPipelineDecision0 tracer cfg btime
(map fromIntegral (offsets maxOffset))
ourFrag
uis = UnknownIntersectionState
{ ourFrag = ourFrag
, ourChainState = ourChainState
, ourTip = ourTip
{ ourFrag = ourFrag
, ourHeaderState = ourHeaderState
, ourTip = ourTip
}
return $ SendMsgFindIntersect points $ ClientPipelinedStIntersect
{ recvMsgIntersectFound = \i theirTip' ->
Expand All @@ -309,7 +310,7 @@ chainSyncClient mkPipelineDecision0 tracer cfg btime
intersectFound intersection theirTip
= Stateful $ \UnknownIntersectionState
{ ourFrag
, ourChainState
, ourHeaderState
, ourTip = ourTip
} -> do
traceWith tracer $ TraceFoundIntersection intersection ourTip theirTip
Expand All @@ -331,10 +332,10 @@ chainSyncClient mkPipelineDecision0 tracer cfg btime
-- it is followed by rolling forward again), but we need some
-- guarantees that the ChainSync protocol /does/ in fact give us a
-- switch-to-fork instead of a true rollback.
(theirFrag, theirChainState) <- do
let i = castPoint intersection
case (,) <$> AF.rollback i ourFrag
<*> rewindChainState cfg ourChainState i of
(theirFrag, theirHeaderState) <- do
let intersection' = castPoint intersection
case (,) <$> AF.rollback intersection' ourFrag
<*> rewindHeaderState cfg intersection ourHeaderState of
Just (c, d) -> return (c, d)
-- The @intersection@ is not on the candidate chain, even though
-- we sent only points from the candidate chain to find an
Expand All @@ -347,10 +348,10 @@ chainSyncClient mkPipelineDecision0 tracer cfg btime
}
atomically $ writeTVar varCandidate theirFrag
let kis = KnownIntersectionState
{ theirFrag = theirFrag
, theirChainState = theirChainState
, ourFrag = ourFrag
, ourTip = ourTip
{ theirFrag = theirFrag
, theirHeaderState = theirHeaderState
, ourFrag = ourFrag
, ourTip = ourTip
}
continueWithState kis $ nextStep mkPipelineDecision0 Zero theirTip

Expand Down Expand Up @@ -531,7 +532,7 @@ chainSyncClient mkPipelineDecision0 tracer cfg btime
(ClientPipelinedStIdle n)
rollForward mkPipelineDecision n hdr theirTip
= Stateful $ \kis@KnownIntersectionState
{ theirChainState
{ theirHeaderState
, theirFrag
, ourTip
} -> traceException $ do
Expand Down Expand Up @@ -567,24 +568,20 @@ chainSyncClient mkPipelineDecision0 tracer cfg btime
, _theirTip = theirTip
}

theirChainState' <-
case runExcept $ applyChainState
cfg
ledgerView
(validateView cfg hdr)
theirChainState of
Right theirChainState' -> return theirChainState'
Left vErr -> disconnect ChainError
{ _newPoint = hdrPoint
, _chainValidationErr = vErr
, _ourTip = ourTip
, _theirTip = theirTip
theirHeaderState' <-
case runExcept $ validateHeader cfg ledgerView hdr theirHeaderState of
Right theirHeaderState' -> return theirHeaderState'
Left vErr -> disconnect HeaderError
{ _newPoint = hdrPoint
, _headerErr = vErr
, _ourTip = ourTip
, _theirTip = theirTip
}

let theirFrag' = theirFrag :> hdr
kis' = kis
{ theirFrag = theirFrag'
, theirChainState = theirChainState'
{ theirFrag = theirFrag'
, theirHeaderState = theirHeaderState'
}
atomically $ writeTVar varCandidate theirFrag'

Expand Down Expand Up @@ -637,13 +634,13 @@ chainSyncClient mkPipelineDecision0 tracer cfg btime
theirTip
= Stateful $ \kis@KnownIntersectionState
{ theirFrag
, theirChainState
, theirHeaderState
, ourTip
} -> traceException $ do
(theirFrag', theirChainState') <- do
let i = castPoint intersection
case (,) <$> AF.rollback i theirFrag
<*> rewindChainState cfg theirChainState i of
(theirFrag', theirHeaderState') <- do
let intersection' = castPoint intersection
case (,) <$> AF.rollback intersection' theirFrag
<*> rewindHeaderState cfg intersection theirHeaderState of
Just (c, d) -> return (c,d)
-- Remember that we use our current chain fragment as the starting
-- point for the candidate's chain. Our fragment contained @k@
Expand Down Expand Up @@ -674,8 +671,8 @@ chainSyncClient mkPipelineDecision0 tracer cfg btime
}

let kis' = kis
{ theirFrag = theirFrag'
, theirChainState = theirChainState'
{ theirFrag = theirFrag'
, theirHeaderState = theirHeaderState'
}
atomically $ writeTVar varCandidate theirFrag'

Expand Down Expand Up @@ -815,13 +812,13 @@ data ChainSyncClientException =
, _theirTip :: Their (Tip blk)
}

-- | The chain validation threw an error.
-- | Header validation threw an error.
| forall blk. SupportedBlock blk =>
ChainError
{ _newPoint :: Point blk
, _chainValidationErr :: ValidationErr (BlockProtocol blk)
, _ourTip :: Our (Tip blk)
, _theirTip :: Their (Tip blk)
HeaderError
{ _newPoint :: Point blk
, _headerErr :: HeaderError blk
, _ourTip :: Our (Tip blk)
, _theirTip :: Their (Tip blk)
}

-- | The upstream node rolled forward to a point too far in our past.
Expand Down Expand Up @@ -898,11 +895,11 @@ instance Eq ChainSyncClientException where
Just Refl -> (a, b, c) == (a', b', c')
ForkTooDeep{} == _ = False

ChainError (a :: Point blk) b c d == ChainError (a' :: Point blk') b' c' d' =
HeaderError (a :: Point blk) b c d == HeaderError (a' :: Point blk') b' c' d' =
case eqT @blk @blk' of
Nothing -> False
Just Refl -> (a, b, c, d) == (a', b', c', d')
ChainError{} == _ = False
HeaderError{} == _ = False

InvalidRollForward (a :: Point blk) b c == InvalidRollForward (a' :: Point blk') b' c' =
case eqT @blk @blk' of
Expand Down
160 changes: 160 additions & 0 deletions ouroboros-consensus/src/Ouroboros/Consensus/Header.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}

-- | Header validation
module Ouroboros.Consensus.Header (
validateHeader
-- * State
, HeaderState(..)
, initHeaderState
, rewindHeaderState
, castHeaderState
-- * Errors
, HeaderError(..)
, castHeaderError
-- * Serialisation
, encodeHeaderState
, decodeHeaderState
) where

import Codec.CBOR.Decoding (Decoder)
import Codec.CBOR.Encoding (Encoding)
import Codec.Serialise (Serialise)
import Control.Monad.Except
import GHC.Generics (Generic)

import Cardano.Prelude (NoUnexpectedThunks)

import Ouroboros.Network.Block (HeaderHash, Point)

import Ouroboros.Consensus.Block
import Ouroboros.Consensus.Protocol.Abstract

{-------------------------------------------------------------------------------
State
-------------------------------------------------------------------------------}

-- | State required to validate the header
--
-- See 'validateHeader' for details
data HeaderState blk = HeaderState {
headerChainState :: ChainState (BlockProtocol blk)
}
deriving (Generic)

deriving instance SupportedBlock blk => Show (HeaderState blk)
deriving instance SupportedBlock blk => NoUnexpectedThunks (HeaderState blk)
deriving instance Eq (ChainState (BlockProtocol blk)) => Eq (HeaderState blk)

initHeaderState :: ChainState (BlockProtocol blk)
-> HeaderState blk
initHeaderState = HeaderState

castHeaderState :: ( ChainState (BlockProtocol blk )
~ ChainState (BlockProtocol blk')
)
=> HeaderState blk -> HeaderState blk'
castHeaderState HeaderState{..} = HeaderState{..}

rewindHeaderState :: ( SupportedBlock blk
, Serialise (HeaderHash blk)
)
=> NodeConfig (BlockProtocol blk)
-> Point blk
-> HeaderState blk -> Maybe (HeaderState blk)
rewindHeaderState cfg p HeaderState{..} = do
chainState' <- rewindChainState cfg headerChainState p
return $ HeaderState {
headerChainState = chainState'
}

{-------------------------------------------------------------------------------
Errors
-------------------------------------------------------------------------------}

-- | Invalid header
data HeaderError blk =
-- | Invalid consensus protocol fields
HeaderProtocolError (ValidationErr (BlockProtocol blk))
deriving (Generic)

deriving instance SupportedBlock blk => Eq (HeaderError blk)
deriving instance SupportedBlock blk => Show (HeaderError blk)
deriving instance SupportedBlock blk => NoUnexpectedThunks (HeaderError blk)

castHeaderError :: ( ValidationErr (BlockProtocol blk )
~ ValidationErr (BlockProtocol blk')
)
=> HeaderError blk -> HeaderError blk'
castHeaderError (HeaderProtocolError e) = HeaderProtocolError e

{-------------------------------------------------------------------------------
Validation proper
-------------------------------------------------------------------------------}

-- | Header validation
--
-- Header validation (as opposed to block validation) is done by the chain sync
-- client: as we download headers from other network nodes, we validate those
-- headers before deciding whether or not to download the corresponding blocks.
--
-- Before we /adopt/ any blocks we download, however, we will do a full block
-- validation. As such, the header validation check can omit some checks
-- (provided that we do those checks when we do the full validation); at worst,
-- this would mean we might download some blocks that we will reject as being
-- invalid where we could have detected that sooner.
--
-- For this reason, the header validation currently only checks two things:
--
-- o It verifies the consensus part of the header.
--
-- For example, for Praos this means checking the VRF proofs.
--
-- o It verifies the 'HasHeader' part of the header.
--
-- Specifically, we verify that
--
-- x Block numbers are consecutive
-- x The block number of the first block is 'genesisBlockNo'
-- x Hashes line up
--
-- Note that this check is independent from both the consensus protocol and
-- from the choice of ledger.
--
-- /If/ a particular ledger wants to verify additional fields in the header,
-- it will get the chance to do so in 'applyLedgerBlock', which is passed the
-- entire block (not just the block body).
validateHeader :: SupportedBlock blk
=> NodeConfig (BlockProtocol blk)
-> LedgerView (BlockProtocol blk)
-> Header blk
-> HeaderState blk
-> Except (HeaderError blk) (HeaderState blk)
validateHeader cfg ledgerView hdr HeaderState{..} = do
chainState' <- withExcept HeaderProtocolError $
applyChainState
cfg
ledgerView
(validateView cfg hdr)
headerChainState
return HeaderState {
headerChainState = chainState'
}

{-------------------------------------------------------------------------------
Serialisation
-------------------------------------------------------------------------------}

encodeHeaderState :: (ChainState (BlockProtocol blk) -> Encoding)
-> (HeaderState blk -> Encoding)
encodeHeaderState encodeChainState (HeaderState x) = encodeChainState x

decodeHeaderState :: (forall s. Decoder s (ChainState (BlockProtocol blk)))
-> (forall s. Decoder s (HeaderState blk))
decodeHeaderState decodeChainState = HeaderState <$> decodeChainState
Loading

0 comments on commit f7703d0

Please sign in to comment.