diff --git a/ouroboros-consensus/ouroboros-consensus.cabal b/ouroboros-consensus/ouroboros-consensus.cabal index 0f2fd5a2c71..63ff36eb664 100644 --- a/ouroboros-consensus/ouroboros-consensus.cabal +++ b/ouroboros-consensus/ouroboros-consensus.cabal @@ -99,6 +99,7 @@ library Ouroboros.Consensus.Protocol.ModChainSel Ouroboros.Consensus.Protocol.PBFT Ouroboros.Consensus.Protocol.PBFT.ChainState + Ouroboros.Consensus.Protocol.PBFT.ChainState.HeaderHashBytes Ouroboros.Consensus.Protocol.PBFT.Crypto Ouroboros.Consensus.Protocol.Praos Ouroboros.Consensus.Protocol.Signed diff --git a/ouroboros-consensus/src/Ouroboros/Consensus/ChainSyncClient.hs b/ouroboros-consensus/src/Ouroboros/Consensus/ChainSyncClient.hs index 6c4cd5bae68..358f97f17e0 100644 --- a/ouroboros-consensus/src/Ouroboros/Consensus/ChainSyncClient.hs +++ b/ouroboros-consensus/src/Ouroboros/Consensus/ChainSyncClient.hs @@ -329,10 +329,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) <- - case (,) <$> AF.rollback (castPoint intersection) ourFrag - <*> rewindChainState cfg ourChainState (pointSlot intersection) - of + (theirFrag, theirChainState) <- do + let i = castPoint intersection + case (,) <$> AF.rollback i ourFrag + <*> rewindChainState cfg ourChainState i 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 @@ -634,10 +634,10 @@ chainSyncClient mkPipelineDecision0 tracer cfg btime , theirChainState , ourTip } -> traceException $ do - (theirFrag', theirChainState') <- - case (,) <$> AF.rollback (castPoint intersection) theirFrag - <*> rewindChainState cfg theirChainState (pointSlot intersection) - of + (theirFrag', theirChainState') <- do + let i = castPoint intersection + case (,) <$> AF.rollback i theirFrag + <*> rewindChainState cfg theirChainState i 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@ diff --git a/ouroboros-consensus/src/Ouroboros/Consensus/Protocol/Abstract.hs b/ouroboros-consensus/src/Ouroboros/Consensus/Protocol/Abstract.hs index 229396357b8..ec9c15737c2 100644 --- a/ouroboros-consensus/src/Ouroboros/Consensus/Protocol/Abstract.hs +++ b/ouroboros-consensus/src/Ouroboros/Consensus/Protocol/Abstract.hs @@ -42,8 +42,7 @@ import GHC.Stack import Cardano.Prelude (NoUnexpectedThunks) -import Ouroboros.Network.Block (HasHeader (..), SlotNo (..)) -import Ouroboros.Network.Point (WithOrigin) +import Ouroboros.Network.Block (HasHeader (..), Point, SlotNo (..)) import Ouroboros.Consensus.BlockchainTime.SlotLengths import Ouroboros.Consensus.Util.Random @@ -188,10 +187,10 @@ class ( Show (ChainState p) -- blocks. -- -- This function should attempt to rewind the chain state to the state at some - -- given slot, or Origin to rewind to the state with no blocks. + -- given point. -- - -- PRECONDITION: the slot to rewind to must correspond to the slot of a - -- header (or 'Origin') that was previously applied to the chain state using + -- PRECONDITION: the point to rewind to must correspond to a header (or + -- 'GenesisPoint') that was previously applied to the chain state using -- 'applyChainState'. -- -- Rewinding the chain state is intended to be used when switching to a @@ -206,14 +205,10 @@ class ( Show (ChainState p) -- state). For example, rewinding a chain state by @i@ blocks and then -- rewinding that chain state again by @j@ where @i + j > k@ is not possible -- and will yield 'Nothing'. - rewindChainState :: NodeConfig p + rewindChainState :: CanValidate p hdr + => NodeConfig p -> ChainState p - -> WithOrigin SlotNo - -- ^ Slot to rewind to - -- - -- This should be the state at the /end/ of the specified - -- slot (i.e., after the block in that slot, if any, has - -- been applied). + -> Point hdr -- ^ Point to rewind to -> Maybe (ChainState p) -- diff --git a/ouroboros-consensus/src/Ouroboros/Consensus/Protocol/PBFT.hs b/ouroboros-consensus/src/Ouroboros/Consensus/Protocol/PBFT.hs index e369767f79a..8906498c54c 100644 --- a/ouroboros-consensus/src/Ouroboros/Consensus/Protocol/PBFT.hs +++ b/ouroboros-consensus/src/Ouroboros/Consensus/Protocol/PBFT.hs @@ -5,6 +5,7 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} @@ -39,6 +40,7 @@ import Control.Monad.Except import Crypto.Random (MonadRandom) import Data.Bimap (Bimap) import qualified Data.Bimap as Bimap +import Data.Proxy (Proxy (..)) import qualified Data.Set as Set import Data.Typeable (Typeable) import Data.Word (Word64) @@ -49,8 +51,10 @@ import qualified Cardano.Chain.Genesis as CC.Genesis import Cardano.Crypto.DSIGN.Class import Cardano.Prelude (NoUnexpectedThunks) -import Ouroboros.Network.Block (HasHeader (..), SlotNo (..)) -import Ouroboros.Network.Point (WithOrigin (At)) +import Ouroboros.Network.Block (pattern BlockPoint, + pattern GenesisPoint, HasHeader (..), HeaderHash, Point, + SlotNo (..)) +import Ouroboros.Network.Point (WithOrigin (..)) import Ouroboros.Consensus.Block import Ouroboros.Consensus.BlockchainTime @@ -60,6 +64,8 @@ import Ouroboros.Consensus.NodeId (CoreNodeId (..)) import Ouroboros.Consensus.Protocol.Abstract import Ouroboros.Consensus.Protocol.PBFT.ChainState (PBftChainState) import qualified Ouroboros.Consensus.Protocol.PBFT.ChainState as CS +import Ouroboros.Consensus.Protocol.PBFT.ChainState.HeaderHashBytes + (headerHashBytes) import Ouroboros.Consensus.Protocol.PBFT.Crypto import Ouroboros.Consensus.Util.Condense import Ouroboros.Consensus.Util.Orphans () @@ -91,6 +97,7 @@ instance (PBftCrypto c, Typeable toSign) => NoUnexpectedThunks (PBftFields c toS -- epoch boundary blocks (EBBs), which are unsigned. Of course the intention -- here is that 'headerPBftFields' will return 'Just' for regular blocks. class ( HasHeader hdr + , Serialise (HeaderHash hdr) , Signable (PBftDSIGN c) (OptSigned hdr) , BlockProtocol hdr ~ PBft cfg c ) => HeaderSupportsPBft cfg c hdr where @@ -244,7 +251,7 @@ instance ( PBftCrypto c applyChainState cfg@PBftNodeConfig{..} lv@(PBftLedgerView dms) (b :: hdr) chainState = case headerPBftFields pbftExtConfig b of Nothing -> do - return $! appendEBB cfg params (blockSlot b) chainState + return $! appendEBB cfg params b chainState Just (PBftFields{..}, signed) -> do -- Check that the issuer signature verifies, and that it's a delegate of a -- genesis key, and that genesis key hasn't voted too many times. @@ -354,26 +361,32 @@ append PBftNodeConfig{..} PBftWindowParams{..} = where PBftParams{..} = pbftParams -appendEBB :: PBftCrypto c +appendEBB :: forall cfg c hdr. + (PBftCrypto c, HeaderSupportsPBft cfg c hdr) => NodeConfig (PBft cfg c) -> PBftWindowParams - -> SlotNo + -> hdr -> PBftChainState c -> PBftChainState c -appendEBB PBftNodeConfig{..} PBftWindowParams{..} = +appendEBB PBftNodeConfig{..} PBftWindowParams{..} b = CS.appendEBB pbftSecurityParam windowSize + (blockSlot b) (headerHashBytes (Proxy :: Proxy hdr) (blockHash b)) where PBftParams{..} = pbftParams -rewind :: PBftCrypto c +rewind :: forall cfg c hdr. + (PBftCrypto c, HeaderSupportsPBft cfg c hdr) => NodeConfig (PBft cfg c) -> PBftWindowParams - -> WithOrigin SlotNo + -> Point hdr -> PBftChainState c -> Maybe (PBftChainState c) -rewind PBftNodeConfig{..} PBftWindowParams{..} = - CS.rewind pbftSecurityParam windowSize +rewind PBftNodeConfig{..} PBftWindowParams{..} p = + CS.rewind pbftSecurityParam windowSize p' where PBftParams{..} = pbftParams + p' = case p of + GenesisPoint -> Origin + BlockPoint s hh -> At (s, headerHashBytes (Proxy :: Proxy hdr) hh) {------------------------------------------------------------------------------- Extract necessary context diff --git a/ouroboros-consensus/src/Ouroboros/Consensus/Protocol/PBFT/ChainState.hs b/ouroboros-consensus/src/Ouroboros/Consensus/Protocol/PBFT/ChainState.hs index 49ed5693926..1dc5f405a3d 100644 --- a/ouroboros-consensus/src/Ouroboros/Consensus/Protocol/PBFT/ChainState.hs +++ b/ouroboros-consensus/src/Ouroboros/Consensus/Protocol/PBFT/ChainState.hs @@ -4,6 +4,7 @@ {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE LambdaCase #-} +{-# LANGUAGE MultiWayIf #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} @@ -26,7 +27,8 @@ module Ouroboros.Consensus.Protocol.PBFT.ChainState ( , countSignedBy , lastSignedSlot -- * Support for tests - , EbbMap (..) + , MaybeEbbInfo (..) + , EbbInfo (..) , PBftSigner(..) , invariant , fromList @@ -59,6 +61,7 @@ import Ouroboros.Network.Point (WithOrigin (..), withOriginFromMaybe, withOriginToMaybe) import Ouroboros.Consensus.Protocol.Abstract +import Ouroboros.Consensus.Protocol.PBFT.ChainState.HeaderHashBytes import Ouroboros.Consensus.Protocol.PBFT.Crypto import Ouroboros.Consensus.Util (repeatedly) @@ -137,34 +140,48 @@ data PBftChainState c = PBftChainState { -- | Cached counts of the signatures in the window , counts :: !(Map (PBftVerKeyHash c) Word64) - -- | Map from slots of relevant epoch boundary blocks (EBBs) to signed - -- slots + -- | Info about a relevant EBB, if any -- -- EBBs are not signed, so the 'preAnchor', 'postAnchor', 'preWindow', -- and 'postWindow' fields are unaffected by EBBs. However, EBBs must - -- also be valid targets for 'rewind', so this field maps each slot that + -- also be valid targets for 'rewind', so this field maps a slot that -- contains an EBB to the preceding signed slot, /if/ that signed slot is -- still a valid target for 'rewind'. -- - -- See INVARIANTs on 'EbbMap'. + -- By assumption, there can be at most one EBB relevant to the @n + k@ + -- window. Current choices that justify this assumption: -- - -- INVARIANT For all @(ebbSlot, mSlot)@ in @'ebbs' (cs :: 'ChainState')@, + -- * The real nodes currently pervasively require that every chain + -- includes at least @k@ signed blocks in every span of @2k@ slots. -- - -- * @mSlot >= anchorSlot cs@; see 'pruneEBBsLT' + -- * The real nodes are currently configured such that epochs to have a + -- duration of @10k@ slots (so EBBs are @10k@ slots apart). -- - -- * @'At' ebbSlot <= tgt@ if @cs@ is the result of a 'rewind' to @tgt@; + -- * The PBFT window size @n@ is currently @k@. + -- + -- * Thus we crucially have that @n + k < 10k@. + -- + -- See INVARIANTs on 'MaybeEbbInfo'. + -- + -- INVARIANT For all @EbbInfo{eiSlot, eiPrevSlot)@ in @'ebbs' (cs :: + -- 'ChainState')@, + -- + -- * @eiPrevSlot >= anchorSlot cs@ or @At eiSlot == anchorSlot cs@; see + -- 'pruneEBBsLT' + -- + -- * @'At' eiSlot <= tgt@ if @cs@ is the result of a 'rewind' to @tgt@; -- see 'pruneEBBsGT' -- - -- * @and [ At s <= mSlot | s <- precedingSignedSlots ]@ + -- * @and [ At s <= eiPrevSlot | s <- precedingSignedSlots ]@ -- - -- * @'rewind' k n ('At' ebbSlot) cs = 'rewind' k n mSlot cs@ + -- * @'rewind' k n ('At' eiSlot) cs = 'rewind' k n eiPrevSlot cs@ -- -- where -- - -- * @precedingSignedSlots = filter (< ebbSlot) signedSlots@ + -- * @precedingSignedSlots = filter (< eiSlot) signedSlots@ -- -- * @signedSlots = 'pbftSignerSlotNo' <$> ('preAnchor' <> 'postAnchor')@ - , ebbs :: !EbbMap + , ebbs :: !MaybeEbbInfo } deriving (Generic) @@ -210,7 +227,8 @@ invariant (SecurityParam k) unless (computeCounts inWindow == counts) $ failure "Cached counts incorrect" - unless (allEbbs $ \_ mSlot -> mSlot >= anchorSlot st) $ + unless (allEbbs $ \slot mSlot -> At slot == anchorSlot st || + mSlot >= anchorSlot st) $ failure "EBB mapped to slot before anchor" unless (allEbbs $ \slot mSlot -> @@ -222,21 +240,16 @@ invariant (SecurityParam k) ) $ failure "EBB does not map to the preceding signature" - -- 'EbbMap''s "Key greater" + -- 'MaybeEbbInfo''s "Key greater" unless (allEbbs $ \slot mSlot -> At slot > mSlot) $ failure "EBB mapped to a simultaneous or future slot" - - -- 'EbbMap''s "Non-descending" - unless (let nonDescending es = and $ zipWith (<=) es (tail es) - in nonDescending $ map snd $ Map.toAscList $ unEbbMap ebbs) $ - failure $ "EBB mappings are not non-descending" where failure :: String -> Except String () failure err = throwError $ err ++ ": " ++ show st - allEbbs p = - Map.null $ - Map.filterWithKey (\slot mSlot -> not (p slot mSlot)) (unEbbMap ebbs) + allEbbs p = case ebbs of + NothingEbbInfo -> True + JustEbbInfo EbbInfo{..} -> p eiSlot eiPrevSlot -- | The 'PBftChainState' tests don't rely on this flag but check the -- invariant manually. This flag is here so that the invariant checks could be @@ -353,15 +366,15 @@ append :: forall c. PBftCrypto c -> PBftChainState c -> PBftChainState c append k n signer@(PBftSigner _ gk) PBftChainState{..} = assertInvariant k n $ + pruneEBBsLT $ PBftChainState { preAnchor = preAnchor' , postAnchor = postAnchor' , preWindow = preWindow' , inWindow = inWindow' , counts = updateCounts counts - -- NOTE: 'pruneEBBsLT' is inlined here to avoid a strange space leak - -- that also goes away with @-O0@, see #1356. - , ebbs = EbbMap $ Map.filter (>= anchorSlot') (unEbbMap ebbs) + -- Will be pruned by the enclosing call to 'pruneEBBsLT' + , ebbs = ebbs } where (preAnchor', postAnchor') = @@ -382,19 +395,19 @@ append k n signer@(PBftSigner _ gk) PBftChainState{..} = , incrementKey gk ) - anchorSlot' = case preAnchor' of - _ :|> anchorSigner -> At (pbftSignerSlotNo anchorSigner) - _otherwise -> Origin - -- | Rewind the state to the specified slot -- -- This matches the semantics of 'rewindChainState' in 'OuroborosTag', in that --- this should be the state at the /end/ of the specified slot (i.e., after the --- block in that slot, if any, has been applied). +-- this should be the state after the given point. -- --- NOTE: It only makes sense to rewind to a slot containing a block that we have --- previously applied (the "genesis block" can be understood as having been --- implicitly applied). +-- NOTE: It only makes sense to rewind to a slot containing a block that we +-- have previously applied (the "genesis block" can be understood as having +-- been implicitly applied). HOWEVER, this function does not check this +-- precondition: it only uses the provided header hash to check if the +-- requested point is an EBB that was previously applied. If the header hash is +-- just random bytes, then the function will assume the target is a signed +-- block in the slot, without trying to confirm the signed block's header hash +-- matches that of the request. -- -- In addition to preserving the invariant, we also have the guarantee that -- rolling back to a point (within @k@) and then reapplying the blocks that were @@ -402,22 +415,25 @@ append k n signer@(PBftSigner _ gk) PBftChainState{..} = rewind :: forall c. PBftCrypto c => SecurityParam -> WindowSize - -> WithOrigin SlotNo + -> WithOrigin (SlotNo, HeaderHashBytes) + -- ^ the target \"point\"; see 'EbbInfo' -> PBftChainState c -> Maybe (PBftChainState c) -rewind k n mSlot cs@PBftChainState{..} = - case rewind_ k n mSlot cs of - Right mbCs' -> pruneEBBsGT mSlot <$> mbCs' - Left mSlot' -> - error $ "rewind: rollback to block not previously applied, " - ++ show (mSlot, mSlot', ebbs) +rewind k n p cs@PBftChainState{..} = case p of + Origin -> go Origin + At (slot, hashBytes) -> case ebbsLookup slot ebbs of + Just EbbInfo{..} + | hashBytes == eiHashBytes -> go eiPrevSlot + _ -> go (At slot) + where + go mSlot = pruneEBBsGT (fst <$> p) <$> rewind_ k n mSlot cs -- | Internal worker for 'rewind' rewind_ :: forall c. PBftCrypto c => SecurityParam -> WindowSize -> WithOrigin SlotNo - -> PBftChainState c -> Either SlotNo (Maybe (PBftChainState c)) -rewind_ k n mSlot cs@PBftChainState{..} = + -> PBftChainState c -> Maybe (PBftChainState c) +rewind_ k n mSlot PBftChainState{..} = case mSlot of At slot -> -- We scan from the right, since block to roll back to likely at end @@ -427,7 +443,7 @@ rewind_ k n mSlot cs@PBftChainState{..} = -- after that slot. (toDiscard, toKeep@(_ :|> x)) -> if slot == pbftSignerSlotNo x - then Right $ Just $ go toDiscard toKeep + then Just $ go toDiscard toKeep else notFound slot -- The slot was not found post-anchor. If the slot matches the last @@ -436,7 +452,7 @@ rewind_ k n mSlot cs@PBftChainState{..} = (toDiscard, Empty) -> case preAnchor of _ :|> x - | slot == pbftSignerSlotNo x -> Right $ Just $ go toDiscard Empty + | slot == pbftSignerSlotNo x -> Just $ go toDiscard Empty | slot < pbftSignerSlotNo x -> rollbackTooFar | otherwise -> notFound slot Empty -> notFound slot @@ -447,18 +463,18 @@ rewind_ k n mSlot cs@PBftChainState{..} = -- have more than @k@ blocks, the pre-anchor will not be empty. Origin -> case preAnchor of - Empty -> Right $ Just $ go postAnchor Empty + Empty -> Just $ go postAnchor Empty _otherwise -> rollbackTooFar where -- If we didn't find a non-EBB, check if the slot was known to have an EBB. -- If so, recur (just once, as long as 'ebbs' is well-formed). - notFound :: SlotNo -> Either SlotNo (Maybe (PBftChainState c)) - notFound slot = case ebbsLookup slot ebbs of - Just mSlot' -> rewind_ k n mSlot' cs - Nothing -> Left slot + notFound :: forall a. SlotNo -> a + notFound slot = + error $ "rewind: rollback to block not previously applied, " + ++ show slot - rollbackTooFar :: Either x (Maybe y) - rollbackTooFar = Right Nothing + rollbackTooFar :: Maybe y + rollbackTooFar = Nothing -- Construct new state, given the remaining post-anchor signatures -- @@ -519,7 +535,7 @@ prune (SecurityParam n) (WindowSize k) (xs, ys) = Conversion -------------------------------------------------------------------------------} -toList :: PBftChainState c -> (WithOrigin SlotNo, StrictSeq (PBftSigner c), EbbMap) +toList :: PBftChainState c -> (WithOrigin SlotNo, StrictSeq (PBftSigner c), MaybeEbbInfo) toList PBftChainState{..} = ( case preAnchor of Empty -> Origin @@ -531,7 +547,7 @@ toList PBftChainState{..} = ( fromList :: PBftCrypto c => SecurityParam -> WindowSize - -> (WithOrigin SlotNo, StrictSeq (PBftSigner c), EbbMap) + -> (WithOrigin SlotNo, StrictSeq (PBftSigner c), MaybeEbbInfo) -> PBftChainState c fromList k n (anchor, signers, ebbs) = assertInvariant k n $ @@ -551,18 +567,24 @@ fromList k n (anchor, signers, ebbs) = serializationFormatVersion1 :: Word8 serializationFormatVersion1 = 1 + +serializationFormatVersion2 :: Word8 +serializationFormatVersion2 = 2 -- CHANGELOG -- -- Version 0 is 2 fields, the anchor and the window. Note that it does not -- have the version marker. -- - -- Version 1 has 4 fields, the version marker, anchor, window, and EbbMap. + -- Version 1 has 4 fields, the version marker, anchor, window, and @~(Map + -- SlotNo (WithOrigin SlotNo))@. + -- + -- Version 2 has 4 fields, the version marker, anchor, window, and @~(Maybe EbbInfo)@. encodePBftChainState :: (PBftCrypto c, Serialise (PBftVerKeyHash c)) => PBftChainState c -> Encoding encodePBftChainState st@PBftChainState{..} = mconcat [ Serialise.encodeListLen 4 - , encode serializationFormatVersion1 + , encode serializationFormatVersion2 , encode (withOriginToMaybe anchor) , encode signers , encode ebbs' @@ -579,14 +601,21 @@ decodePBftChainState k n = Serialise.decodeListLen >>= \case anchor <- withOriginFromMaybe <$> decode signers <- decode return $ fromList k n (anchor, signers, ebbsEmpty) - 4 -> do -- Version is >0 - v <- decode - unless (v == serializationFormatVersion1) $ error $ - "decode list length is 4, but version is not 1: " ++ show v - anchor <- withOriginFromMaybe <$> decode - signers <- decode - ebbs' <- decode - return $ fromList k n (anchor, signers, ebbs') + 4 -> decode >>= \v -> if + | v == serializationFormatVersion1 -> do + anchor <- withOriginFromMaybe <$> decode + signers <- decode + ebbs' <- decode + let _ = ebbs' :: Map SlotNo (WithOrigin SlotNo) + -- NB we discard ebbs' + return $ fromList k n (anchor, signers, ebbsEmpty) + | v == serializationFormatVersion2 -> do + anchor <- withOriginFromMaybe <$> decode + signers <- decode + ebbs' <- decode + return $ fromList k n (anchor, signers, ebbs') + | otherwise -> + error $ "unexpected serialisation format version: " <> show v o -> error $ "unexpected list length: " <> show o instance Serialise (PBftVerKeyHash c) => Serialise (PBftSigner c) where @@ -609,53 +638,90 @@ appendEBB :: forall c. (PBftCrypto c, HasCallStack) => SecurityParam -> WindowSize -> SlotNo + -> HeaderHashBytes -> PBftChainState c -> PBftChainState c -appendEBB k n newEbbSlot cs@PBftChainState{..} = +appendEBB k n newEbbSlot hashBytes cs@PBftChainState{..} = assertInvariant k n $ Exn.assert valid $ - cs{ebbs = ebbsInsert newEbbSlot latestNonEbbSlot ebbs} + cs{ebbs = JustEbbInfo EbbInfo + { eiSlot = newEbbSlot + , eiHashBytes = hashBytes + , eiPrevSlot = latestNonEbbSlot + }} where latestEbbSlot = ebbsMax ebbs latestNonEbbSlot = lastSignedSlot cs valid = At newEbbSlot > max latestEbbSlot latestNonEbbSlot --- | Discard 'ebbs' mappings whose /value/ is before the anchor +-- | Discard 'ebbs' mappings whose 'eiPrevSlot' is before the anchor, except +-- if its 'eiSlot' is equal to the anchor's slot -- -- Called by 'append', since 'ebbs' do not increase how far back the chain --- state can rewind. +-- state can rewind. However, we must retain the EBB that shares a slot with +-- the anchor so that we can fail if we attempt to rewind to it -- if we forget +-- about that EBB, then we won't be able to recognize its hash in the requested +-- rewind point. pruneEBBsLT :: PBftChainState c -> PBftChainState c pruneEBBsLT cs@PBftChainState{..} = - cs{ebbs = EbbMap $ Map.filter (>= anchorSlot cs) (unEbbMap ebbs)} - --- | Discard 'ebbs' mappings whose /key/ is after the given slot + cs{ ebbs = ebbsFilter ebbs $ \EbbInfo{..} -> + eiPrevSlot >= anchorSlot cs || + At eiSlot == anchorSlot cs } +-- NOTE: this INLINE seems redundant but we add it here to avoid a strange +-- space leak that also goes away with @-O0@, see #1356. +{-# INLINE pruneEBBsLT #-} + +-- | Discard 'ebbs' mappings whose 'eiSlot' is after the given slot -- -- Called by 'rewind', since 'rewind'ing to a slot should forget the EBBs it -- precedes. pruneEBBsGT :: WithOrigin SlotNo -> PBftChainState c -> PBftChainState c pruneEBBsGT mSlot cs@PBftChainState{..} = - cs{ ebbs = - EbbMap $ Map.filterWithKey (\s _ -> At s <= mSlot) (unEbbMap ebbs) - } + cs{ ebbs = ebbsFilter ebbs $ \EbbInfo{..} -> At eiSlot <= mSlot } --- | A map from the slots containing an EBB to the preceding signed slot +-- | Info about the latest EBB, if there is one recent enough to be relevant to +-- the chain state -- --- INVARIANT Key greater: For all @(k, v)@, @At k > v@. --- --- INVARIANT Non-descending: For all @(k1, v1)@ and @(k2, v2)@, @k1 < k2@ --- implies @v1 <= v2@. -newtype EbbMap = EbbMap {unEbbMap :: Map SlotNo (WithOrigin SlotNo)} - deriving stock (Generic, Show) - deriving newtype (Eq, Ord, NoUnexpectedThunks, Serialise) - -ebbsEmpty :: EbbMap -ebbsEmpty = EbbMap Map.empty - -ebbsInsert :: SlotNo -> WithOrigin SlotNo -> EbbMap -> EbbMap -ebbsInsert k v = EbbMap . Map.insert k v . unEbbMap +data MaybeEbbInfo + = NothingEbbInfo + | JustEbbInfo !EbbInfo + deriving stock (Eq, Generic, Ord, Show) + deriving anyclass (NoUnexpectedThunks, Serialise) -ebbsMax :: EbbMap -> WithOrigin SlotNo -ebbsMax = maybe Origin (At . fst) . Map.lookupMax . unEbbMap - -ebbsLookup :: SlotNo -> EbbMap -> Maybe (WithOrigin SlotNo) -ebbsLookup k = Map.lookup k . unEbbMap +-- | Info about an EBB +-- +-- The serialised bytes of the EBB's header hash and its latest previous signed +-- slot. We use 'HeaderHashBytes' instead of the EBB's actual @HeaderHash@ +-- because the 'ChainState' type family (which we instantiate as +-- 'PBftChainState') does not take a type argument that to which we can apply +-- @HeaderHash@. This is a compromise. +-- +-- INVARIANT @At 'eiSlot' > 'eiPrevSlot'@ +data EbbInfo = EbbInfo + { eiSlot :: !SlotNo + -- ^ the slot of the EBB + , eiHashBytes :: !HeaderHashBytes + -- ^ the bytes of the serialised header hash of the EBB + , eiPrevSlot :: !(WithOrigin SlotNo) + -- ^ the slot of the latest non-EBB that precedes the EBB + } + deriving stock (Eq, Generic, Ord, Show) + deriving anyclass (NoUnexpectedThunks, Serialise) + +ebbsEmpty :: MaybeEbbInfo +ebbsEmpty = NothingEbbInfo + +ebbsMax :: MaybeEbbInfo -> WithOrigin SlotNo +ebbsMax = \case + NothingEbbInfo -> Origin + JustEbbInfo EbbInfo{..} -> At eiSlot + +ebbsLookup :: SlotNo -> MaybeEbbInfo -> Maybe EbbInfo +ebbsLookup k = \case + NothingEbbInfo -> Nothing + JustEbbInfo ei@EbbInfo{..} -> if eiSlot == k then Just ei else Nothing + +ebbsFilter :: MaybeEbbInfo -> (EbbInfo -> Bool) -> MaybeEbbInfo +ebbsFilter x f = case x of + NothingEbbInfo -> NothingEbbInfo + JustEbbInfo ei -> if f ei then x else NothingEbbInfo diff --git a/ouroboros-consensus/src/Ouroboros/Consensus/Protocol/PBFT/ChainState/HeaderHashBytes.hs b/ouroboros-consensus/src/Ouroboros/Consensus/Protocol/PBFT/ChainState/HeaderHashBytes.hs new file mode 100644 index 00000000000..fcbbb3f9ec9 --- /dev/null +++ b/ouroboros-consensus/src/Ouroboros/Consensus/Protocol/PBFT/ChainState/HeaderHashBytes.hs @@ -0,0 +1,37 @@ +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} + +-- | A type in which each value is the byte serialisation of a header hash. +-- +-- This is an implementation detail of +-- "Ouroboros.Consensus.Protocol.PBFT.ChainState". +module Ouroboros.Consensus.Protocol.PBFT.ChainState.HeaderHashBytes ( + HeaderHashBytes, + headerHashBytes, + -- * For testing + mkHeaderHashBytesForTestingOnly, + ) where + +import Codec.Serialise (Serialise (..), serialise) +import qualified Data.ByteString as BS +import qualified Data.ByteString.Lazy as BSL +import GHC.Generics (Generic) + +import Cardano.Prelude (NoUnexpectedThunks) + +import Ouroboros.Network.Block (HeaderHash) + +newtype HeaderHashBytes = HeaderHashBytes BS.ByteString + deriving stock (Generic, Show) + deriving newtype (Eq, Ord, NoUnexpectedThunks, Serialise) + +-- | The safe way to construct 'HeaderHashBytes' +headerHashBytes + :: Serialise (HeaderHash hdr) + => proxy hdr -> HeaderHash hdr -> HeaderHashBytes +headerHashBytes _ = HeaderHashBytes . BSL.toStrict . serialise + +mkHeaderHashBytesForTestingOnly :: BSL.ByteString -> HeaderHashBytes +mkHeaderHashBytesForTestingOnly = HeaderHashBytes . BSL.toStrict diff --git a/ouroboros-consensus/src/Ouroboros/Consensus/Protocol/Praos.hs b/ouroboros-consensus/src/Ouroboros/Consensus/Protocol/Praos.hs index 405052d13ab..a3b90912882 100644 --- a/ouroboros-consensus/src/Ouroboros/Consensus/Protocol/Praos.hs +++ b/ouroboros-consensus/src/Ouroboros/Consensus/Protocol/Praos.hs @@ -58,7 +58,8 @@ import Cardano.Crypto.VRF.Mock (MockVRF) import Cardano.Crypto.VRF.Simple (SimpleVRF) import Cardano.Prelude (NoUnexpectedThunks (..)) -import Ouroboros.Network.Block (HasHeader (..), SlotNo (..)) +import Ouroboros.Network.Block (HasHeader (..), SlotNo (..), + pointSlot) import Ouroboros.Network.Point (WithOrigin (At)) import Ouroboros.Consensus.Block @@ -331,7 +332,7 @@ instance ( PraosCrypto c -- filled; instead we roll back the the block just before it. rewindChainState PraosNodeConfig{..} cs rewindTo = -- This may drop us back to the empty list if we go back to genesis - Just $ dropWhile (\bi -> At (biSlot bi) > rewindTo) cs + Just $ dropWhile (\bi -> At (biSlot bi) > pointSlot rewindTo) cs -- (Standard) Praos uses the standard chain selection rule, so no need to -- override (though see note regarding clock skew). diff --git a/ouroboros-consensus/test-consensus/Test/Consensus/Ledger/Byron.hs b/ouroboros-consensus/test-consensus/Test/Consensus/Ledger/Byron.hs index 02f3233246f..291102e96b2 100644 --- a/ouroboros-consensus/test-consensus/Test/Consensus/Ledger/Byron.hs +++ b/ouroboros-consensus/test-consensus/Test/Consensus/Ledger/Byron.hs @@ -17,6 +17,7 @@ import Control.Monad.Except (runExcept) import qualified Data.Binary.Get as Get import qualified Data.Binary.Put as Put import qualified Data.ByteString.Lazy as Lazy +import qualified Data.ByteString.Lazy.Char8 as Lazy8 import qualified Data.Sequence.Strict as Seq import Cardano.Binary (fromCBOR, toCBOR) @@ -27,18 +28,20 @@ import Cardano.Chain.Slotting (EpochSlots (..)) import qualified Cardano.Chain.Update as CC.Update import Cardano.Crypto (ProtocolMagicId (..)) -import Ouroboros.Network.Block (HeaderHash) +import Ouroboros.Network.Block (HeaderHash, SlotNo) import Ouroboros.Network.Point (WithOrigin (At)) -import Ouroboros.Consensus.Block (Header) +import Ouroboros.Consensus.Block (BlockProtocol, Header) import Ouroboros.Consensus.Ledger.Byron import Ouroboros.Consensus.Ledger.Byron.Auxiliary import qualified Ouroboros.Consensus.Ledger.Byron.DelegationHistory as DH import Ouroboros.Consensus.Mempool.API (ApplyTxErr) import Ouroboros.Consensus.Node.ProtocolInfo import Ouroboros.Consensus.Protocol -import Ouroboros.Consensus.Protocol.Abstract (SecurityParam (..)) +import Ouroboros.Consensus.Protocol.Abstract (ChainState, + SecurityParam (..)) import qualified Ouroboros.Consensus.Protocol.PBFT.ChainState as CS +import Ouroboros.Consensus.Protocol.PBFT.ChainState.HeaderHashBytes import Ouroboros.Storage.ImmutableDB (BinaryInfo (..), HashInfo (..)) @@ -46,6 +49,7 @@ import Test.QuickCheck import Test.QuickCheck.Hedgehog (hedgehog) import Test.Tasty import Test.Tasty.Golden +import Test.Tasty.HUnit import Test.Tasty.QuickCheck import qualified Test.Cardano.Chain.Block.Gen as CC @@ -85,6 +89,9 @@ tests = testGroup "Byron" -- Note that for most Byron types, we simply wrap the en/decoders from -- cardano-ledger, which already has golden tests for them. [ test_golden_ChainState + , test_golden_ChainState_backwardsCompat_version0 + , test_golden_ChainState_backwardsCompat_version1 + , test_golden_ChainState_backwardsCompat_version2 , test_golden_LedgerState , test_golden_GenTxId ] @@ -219,22 +226,64 @@ prop_byronHashInfo_hashSize h = Golden tests -------------------------------------------------------------------------------} +-- | Note that we must use the same value for the 'SecurityParam' as for the +-- 'CS.WindowSize', because 'decodeByronChainState' only takes the +-- 'SecurityParam' and uses it as the basis for the 'CS.WindowSize'. +secParam :: SecurityParam +secParam = SecurityParam 2 + +windowSize :: CS.WindowSize +windowSize = CS.WindowSize 2 + +exampleChainStateWithoutEBB, exampleChainStateWithEBB :: ChainState (BlockProtocol ByronBlock) +(exampleChainStateWithoutEBB, exampleChainStateWithEBB) = + (withoutEBB, withEBB) + where + signers = map (`CS.PBftSigner` CC.exampleKeyHash) [1..4] + + withoutEBB = CS.fromList + secParam + windowSize + (At 2, Seq.fromList signers, CS.NothingEbbInfo) + + -- info about an arbitrary hypothetical EBB + exampleEbbSlot :: SlotNo + exampleEbbHeaderHashBytes :: HeaderHashBytes + exampleEbbSlot = 6 + exampleEbbHeaderHashBytes = mkHeaderHashBytesForTestingOnly + (Lazy8.pack "test_golden_ChainState6") + + withEBB = CS.appendEBB secParam windowSize + exampleEbbSlot exampleEbbHeaderHashBytes + withoutEBB + test_golden_ChainState :: TestTree test_golden_ChainState = goldenTestCBOR "ChainState" encodeByronChainState - exampleChainState - "test-consensus/golden/cbor/byron/ChainState" - where - exampleChainState = CS.appendEBB secParam windowSize 6 $ - CS.fromList - secParam - windowSize - (At 3, Seq.fromList signers, CS.EbbMap mempty) - - secParam = SecurityParam 2 - windowSize = CS.WindowSize 3 - signers = map (`CS.PBftSigner` CC.exampleKeyHash) [1..5] + exampleChainStateWithEBB + "test-consensus/golden/cbor/byron/ChainState2" + +test_golden_ChainState_backwardsCompat_version0 :: TestTree +test_golden_ChainState_backwardsCompat_version0 = + testCase "ChainState version 0" $ goldenTestCBORBackwardsCompat + (decodeByronChainState secParam) + exampleChainStateWithoutEBB + "test-consensus/golden/cbor/byron/ChainState0" + +test_golden_ChainState_backwardsCompat_version1 :: TestTree +test_golden_ChainState_backwardsCompat_version1 = + testCase "ChainState version 1" $ goldenTestCBORBackwardsCompat + (decodeByronChainState secParam) + exampleChainStateWithoutEBB + "test-consensus/golden/cbor/byron/ChainState1" + +test_golden_ChainState_backwardsCompat_version2 :: TestTree +test_golden_ChainState_backwardsCompat_version2 = + testCase "ChainState version 2" $ goldenTestCBORBackwardsCompat + (decodeByronChainState secParam) + exampleChainStateWithEBB + "test-consensus/golden/cbor/byron/ChainState2" test_golden_LedgerState :: TestTree test_golden_LedgerState = goldenTestCBOR @@ -267,6 +316,25 @@ goldenTestCBOR name enc a path = where bs = toLazyByteString (enc a) +-- | Check whether we can successfully decode the contents of the given file. +-- This file will typically contain an older serialisation format. +goldenTestCBORBackwardsCompat + :: (Eq a, Show a) + => (forall s. Decoder s a) + -> a + -> FilePath + -> Assertion +goldenTestCBORBackwardsCompat dec a path = do + bytes <- Lazy.readFile path + case deserialiseFromBytes dec bytes of + Left failure + -> assertFailure (show failure) + Right (leftover, a') + | Lazy.null leftover + -> a' @?= a + | otherwise + -> assertFailure $ "Left-over bytes: " <> show leftover + {------------------------------------------------------------------------------- Integrity -------------------------------------------------------------------------------} diff --git a/ouroboros-consensus/test-consensus/Test/Consensus/Protocol/PBFT.hs b/ouroboros-consensus/test-consensus/Test/Consensus/Protocol/PBFT.hs index e731cb8f99e..14029be91e3 100644 --- a/ouroboros-consensus/test-consensus/Test/Consensus/Protocol/PBFT.hs +++ b/ouroboros-consensus/test-consensus/Test/Consensus/Protocol/PBFT.hs @@ -2,6 +2,8 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeFamilies #-} + module Test.Consensus.Protocol.PBFT ( tests -- * Used in the roundtrip tests @@ -12,7 +14,7 @@ import qualified Control.Exception as Exn import Data.Coerce (coerce) import Data.Functor ((<&>)) import Data.List (inits, tails) -import qualified Data.Map.Strict as Map +import Data.Proxy (Proxy (..)) import qualified Data.Sequence.Strict as Seq import Data.Word @@ -22,13 +24,14 @@ import Test.Tasty.QuickCheck import Cardano.Crypto.DSIGN import qualified Cardano.Prelude -import Ouroboros.Network.Block (SlotNo (..)) +import Ouroboros.Network.Block (HeaderHash, SlotNo (..)) import Ouroboros.Network.Point (WithOrigin (..)) import Ouroboros.Consensus.Protocol.Abstract -import Ouroboros.Consensus.Protocol.PBFT.ChainState (EbbMap (..), - PBftChainState) +import Ouroboros.Consensus.Protocol.PBFT.ChainState (EbbInfo (..), + MaybeEbbInfo (..), PBftChainState) import qualified Ouroboros.Consensus.Protocol.PBFT.ChainState as CS +import Ouroboros.Consensus.Protocol.PBFT.ChainState.HeaderHashBytes import Ouroboros.Consensus.Protocol.PBFT.Crypto import Ouroboros.Consensus.Util (lastMaybe, repeatedly) @@ -111,7 +114,7 @@ tests = testGroup "PBftChainState" $ -- The test properties themselves focus on the state @csABb@ defined by: -- -- > csAB = appendInputs (inA <> inB) empty --- > Just csABb = rewind (slotLatestInput (lastInput inA)) csAB +-- > Just csABb = rewind (pointLatestInput (lastInput inA)) csAB -- -- Segment A could be empty or have several times @k@ signed blocks interleaved -- with any number of EBBs. Segment B has @<= k@ signed blocks interleaved with @@ -128,7 +131,7 @@ tests = testGroup "PBftChainState" $ -- In particular, appending a sufficient prefix of segment C will always -- restore the invariants that were not ensured before PR #1307. -- --- * 'testChainRewind': +-- * 'testChainRewindPoint': -- -- Since @csABb@ is fully representative of a realistic state, we will test an -- arbitrary rewind from it. @@ -158,9 +161,9 @@ data TestChainState = TestChainState { , testChainOldState :: PBftChainState PBftMockCrypto -- | The slot of some input within segment A - , testChainRewind :: WithOrigin SlotNo + , testChainRewindPoint :: WithOrigin (SlotNo, HeaderHashBytes) - -- | The inputs in segment A occupying slots @> 'testChainRewind'@ + -- | The inputs in segment A after 'testChainRewindPoint' , testChainRewoundInputs :: Inputs PBftMockCrypto } deriving (Show) @@ -200,7 +203,7 @@ genTestChainState = do -- Generate all the inputs (inA, inB, inC) <- do - inA <- generateInputs genKey numSignersA (LatestInput Nothing) + inA <- generateInputs paramK paramN genKey numSignersA (LatestInput Nothing) -- Segment B must not begin with the same slot that A ended with, -- because rewinds can't split a slot and our tests focus on the result @@ -212,8 +215,8 @@ genTestChainState = do where tick (PBftEBB prev slot) = PBftEBB prev (succ slot) - inB <- generateInputs genKey numSignersB lastInputOfA' - inC <- generateInputs genKey (n + k) (toLastInput inA) + inB <- generateInputs paramK paramN genKey numSignersB lastInputOfA' + inC <- generateInputs paramK paramN genKey (n + k) (toLastInput inA) pure (inA, inB, inC) @@ -230,7 +233,8 @@ genTestChainState = do (signatureInputs inps) (ebbInputs inps) where - inps = snd $ splitAtSigner (numSignersA .- ((n + k) .- numSignersB)) inA + inps = + snd $ splitAtSigner (numSignersA .- ((n + k) .- numSignersB)) inA -- the state before PR #1307 didn't retain as many signatures and -- didn't track EBBs at all @@ -247,29 +251,26 @@ genTestChainState = do , choose (0, numSignersA) -- rollback that might fail (too far) ] - let (mbAPrefix, inSuffixA) = splitAtSigner (numSignersA .- numSignersSuffixA) inA - -- if the rollback succeeds, its new window ends with this signed slot - signedTarget = case mbAPrefix of - Nothing -> Origin - Just (_, x) -> At $ CS.pbftSignerSlotNo x + let (mbAPrefix, inSuffixA) = + splitAtSigner (numSignersA .- numSignersSuffixA) inA + -- if the rollback succeeds, its new window ends with this signed point + signedPoint = case mbAPrefix of + Nothing -> Origin + Just (_, x) -> + At (CS.pbftSignerSlotNo x, headerHashBytesInput (InputSigner x)) - -- Pick a slot to rewind to + -- Pick a point to rewind to -- -- appending the @rewoundInputs@ will undo the rewind to @rewindSlot@ - (rewindSlot, rewoundInputs) <- elements $ + (rewindPoint, rewoundInputs) <- elements $ [ Exn.assert (unInputs inSuffixA == int <> tal) $ - ( maybe signedTarget (At . slotInput) $ lastMaybe int + ( case lastMaybe int of + Nothing -> signedPoint + Just x -> At (slotInput x, headerHashBytesInput x) , Inputs tal ) | (int, tal) <- inits (unInputs inSuffixA) `zip` tails (unInputs inSuffixA) - -- this is ultimately picking a /slot/, so don't split between two - -- inputs that have the same slot - , case ( slotInput <$> lastMaybe int - , slotInput <$> Cardano.Prelude.head tal - ) of - (Just l, Just r) -> l /= r - _ -> True ] pure TestChainState { @@ -281,7 +282,7 @@ genTestChainState = do , testChainInputsA = inA , testChainInputsB = inB , testChainInputsC = inC - , testChainRewind = rewindSlot + , testChainRewindPoint = rewindPoint , testChainRewoundInputs = rewoundInputs } @@ -296,13 +297,19 @@ genMockKey numKeys = VerKeyMockDSIGN <$> choose (1, numKeys) -- -- POSTCONDITION The output contains exactly the specified number of -- 'InputSigner's. +-- +-- POSTCONDITION The 'InputEBB's in the output are separated by at least @n + +-- k@ signed blocks. generateInputs :: forall c. - Gen (PBftVerKeyHash c) + SecurityParam + -> CS.WindowSize + -> Gen (PBftVerKeyHash c) -> Word64 -> LatestInput c -> Gen (Inputs c) -generateInputs genKey = go [] +generateInputs paramK paramN genKey = + \n lastInput -> post <$> go [] n lastInput where go :: [Input c] -> Word64 -> LatestInput c -> Gen (Inputs c) go acc n lastInput = do @@ -333,6 +340,23 @@ generateInputs genKey = go [] key <- genKey pure $ InputSigner $ CS.PBftSigner slot key + -- remove EBBs that come too soon + post :: Inputs c -> Inputs c + post = Inputs . go2 0 . unInputs + where + lim = n + k + where + CS.WindowSize n = paramN + SecurityParam k = paramK + + go2 numSigned = \case + [] -> [] + inp:inps -> case inp of + InputEBB{} + | numSigned < lim -> go2 numSigned inps + | otherwise -> inp : go2 0 inps + InputSigner{} -> inp : go2 (succ numSigned) inps + {------------------------------------------------------------------------------- Labelling -------------------------------------------------------------------------------} @@ -413,7 +437,7 @@ prop_directABb TestChainState{..} = (testChainInputsA <> testChainInputsB) state0 mbState2 = CS.rewind k n - (slotLatestInput $ toLastInput testChainInputsA) + (pointLatestInput $ toLastInput testChainInputsA) state1 in Just testChainState === mbState2 @@ -450,7 +474,7 @@ prop_rewindPreservesInvariant TestChainState{..} = let rewound = CS.rewind testChainStateK testChainStateN - testChainRewind + testChainRewindPoint testChainState in case rewound of Nothing -> label "rollback too far in the past" True @@ -467,7 +491,7 @@ prop_rewindReappendId TestChainState{..} = let rewound = CS.rewind testChainStateK testChainStateN - testChainRewind + testChainRewindPoint testChainState in case rewound of Nothing -> label "rollback too far in the past" True @@ -561,8 +585,9 @@ appendInput -> CS.WindowSize -> Input c -> CS.PBftChainState c -> CS.PBftChainState c -appendInput k n = \case - InputEBB ebb -> CS.appendEBB k n (pbftEbbSlotNo ebb) +appendInput k n inp = case inp of + InputEBB{} -> + CS.appendEBB k n (slotInput inp) (headerHashBytesInput inp) InputSigner signer -> CS.append k n signer appendInputs @@ -573,7 +598,8 @@ appendInputs -> CS.PBftChainState c -> CS.PBftChainState c appendInputs k n = repeatedly (appendInput k n) . unInputs -splitAtSigner :: Word64 -> Inputs c -> (Maybe (Inputs c, CS.PBftSigner c), Inputs c) +splitAtSigner + :: Word64 -> Inputs c -> (Maybe (Inputs c, CS.PBftSigner c), Inputs c) splitAtSigner n (Inputs inps) = coerce $ splitAtJust prjSigner n inps where @@ -607,10 +633,33 @@ fromInputs -> [PBftEBB] -- ^ determines 'CS.ebbs' -> CS.PBftChainState c -fromInputs k n anchor signers ebbs = - CS.fromList k n (anchor, Seq.fromList signers, EbbMap m) +fromInputs k n anchor signers ebbs0 = + CS.fromList k n (anchor, Seq.fromList signers, ebbs2) + where + ebbs1 = + [ mkEbbInfo slot mSlot + | PBftEBB mSlot slot <- ebbs0 + , At slot == anchor || mSlot >= anchor + ] + ebbs2 = case lastMaybe ebbs1 of + Nothing -> NothingEbbInfo + Just ei -> JustEbbInfo ei + + mkEbbInfo slot mSlot = EbbInfo + { eiSlot = slot + , eiHashBytes = headerHashBytesInput $ InputEBB $ PBftEBB mSlot slot + , eiPrevSlot = mSlot + } + +type instance HeaderHash (Input c) = (Bool, SlotNo) + +headerHashBytesInput :: forall c. Input c -> HeaderHashBytes +headerHashBytesInput inp = + headerHashBytes (Proxy :: Proxy (Input c)) (flag, slotInput inp) where - m = Map.fromList [ (slot, mSlot) | PBftEBB mSlot slot <- ebbs ] + flag = case inp of + InputEBB{} -> True + InputSigner{} -> False {------------------------------------------------------------------------------- "The previous input" @@ -623,10 +672,10 @@ toLastInput :: Inputs c -> LatestInput c toLastInput = LatestInput . lastMaybe . unInputs -- | The slot of the latest block -slotLatestInput :: LatestInput c -> WithOrigin SlotNo -slotLatestInput (LatestInput mbInp) = case mbInp of +pointLatestInput :: LatestInput c -> WithOrigin (SlotNo, HeaderHashBytes) +pointLatestInput (LatestInput mbInp) = case mbInp of Nothing -> Origin - Just inp -> At $ slotInput inp + Just inp -> At (slotInput inp, headerHashBytesInput inp) -- | The slot of the latest /signed/ block signedSlotLatestInput :: LatestInput c -> WithOrigin SlotNo diff --git a/ouroboros-consensus/test-consensus/golden/cbor/byron/ChainState b/ouroboros-consensus/test-consensus/golden/cbor/byron/ChainState0 similarity index 56% rename from ouroboros-consensus/test-consensus/golden/cbor/byron/ChainState rename to ouroboros-consensus/test-consensus/golden/cbor/byron/ChainState0 index bf42ed8735f..1800a03da8a 100644 --- a/ouroboros-consensus/test-consensus/golden/cbor/byron/ChainState +++ b/ouroboros-consensus/test-consensus/golden/cbor/byron/ChainState0 @@ -1,6 +1,5 @@ -„…‚X΄ΐΰ +‚„‚X΄ΐΰ ŒΤυΉ$œA³^Ρ† \pŠ@΅«¬‚X΄ΐΰ ŒΤυΉ$œA³^Ρ† \pŠ@΅«¬‚X΄ΐΰ ŒΤυΉ$œA³^Ρ† \pŠ@΅«¬‚X΄ΐΰ -ŒΤυΉ$œA³^Ρ† \pŠ@΅«¬‚X΄ΐΰ -ŒΤυΉ$œA³^Ρ† \pŠ@΅«¬‘‚ \ No newline at end of file +ŒΤυΉ$œA³^Ρ† \pŠ@΅«¬ \ No newline at end of file diff --git a/ouroboros-consensus/test-consensus/golden/cbor/byron/ChainState1 b/ouroboros-consensus/test-consensus/golden/cbor/byron/ChainState1 new file mode 100644 index 00000000000..279f191b0bf --- /dev/null +++ b/ouroboros-consensus/test-consensus/golden/cbor/byron/ChainState1 @@ -0,0 +1,5 @@ +„„‚X΄ΐΰ +ŒΤυΉ$œA³^Ρ† \pŠ@΅«¬‚X΄ΐΰ +ŒΤυΉ$œA³^Ρ† \pŠ@΅«¬‚X΄ΐΰ +ŒΤυΉ$œA³^Ρ† \pŠ@΅«¬‚X΄ΐΰ +ŒΤυΉ$œA³^Ρ† \pŠ@΅«¬‘‚ \ No newline at end of file diff --git a/ouroboros-consensus/test-consensus/golden/cbor/byron/ChainState2 b/ouroboros-consensus/test-consensus/golden/cbor/byron/ChainState2 new file mode 100644 index 00000000000..857b9961501 Binary files /dev/null and b/ouroboros-consensus/test-consensus/golden/cbor/byron/ChainState2 differ