Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prototype lightweight checkpointing (non-combinator approach) #453

Closed
wants to merge 6 commits into from

Conversation

amesgen
Copy link
Member

@amesgen amesgen commented Oct 24, 2023

Prototype of lightweight checkpointing, see #448

To be merged, this would need more polish (Haddocks, addressing the inline TODOs) and tests.

Notably, this PR adds a new field to ProtocolParams (CardanoBlock c):

data instance ProtocolParams (CardanoBlock c) = ProtocolParamsCardano {
  ...
  , cardanoCheckpoints            :: Map BlockNo (HeaderHash (CardanoBlock c))
  }

with the semantics described in #447 (comment).

Note that this is not the approach described in #447 (comment); the approach of this PR is likely less invasive/has a smaller diff, but less elegant than the combinator-based appraoch.

@amesgen amesgen added the Genesis PRs related to Genesis testing and implementation label Oct 24, 2023
Copy link
Contributor

@nfrisby nfrisby left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First impressions

@@ -297,6 +297,7 @@ protocolInfoTPraosShelleyBased ProtocolParamsShelleyBased {
protVer
genesis
(shelleyBlockIssuerVKey <$> credentialss)
mempty -- TODO propagate?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the hypothesis for why we wouldn't need to propagate?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function isn't ever called by protocolInfoCardano (which is the only place actually interesting to us), so I just did the lazy thing and didn't propagate it further for this prototype PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would consider addressing this if necessary for testing.

unless (m <= maxpv) $ throwError (ObsoleteNode m maxpv)
unless (fromIntegral (bhviewHSize bhv) <= maxHeaderSize) $
throwError $
HeaderSizeTooLarge (fromIntegral $ bhviewHSize bhv) maxHeaderSize
unless (bhviewBSize bhv <= maxBodySize) $
throwError $
BlockSizeTooLarge (bhviewBSize bhv) maxBodySize
whenJust (Map.lookup (pHeaderBlock hdr) checkpoints) $ \checkpoint ->
unless (checkpoint == pHeaderHash hdr) $
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same comment as other guard

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I am always paralyzed by the choice between when and unless... I used unless here as it is also used in all preceeding checks (even though it would be easy to rewrite these places from eg unless (m <= maxpv) to when (m > maxpv)), and then also used it in all other additionalEnvelopeChecks for consistency.

Copy link
Contributor

@facundominguez facundominguez Jan 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switched unless to when in 7ca4c63

@facundominguez facundominguez force-pushed the amesgen/lightweight-checkpointing-non-comb branch 2 times, most recently from cbc3877 to e88f6c2 Compare January 11, 2024 19:13
@facundominguez facundominguez force-pushed the amesgen/lightweight-checkpointing-non-comb branch 2 times, most recently from d2507b5 to eb1740f Compare January 12, 2024 14:15
@facundominguez
Copy link
Contributor

facundominguez commented Jan 19, 2024

The plan now is to rearrange the PR so Ouroboros.Consensus.HeaderValidation.validateEnvelope invokes the validation of checkpoints. Let's call this function

validateIfCheckpoint :: TopLevelConfig blk -> Header blk -> Except (HeaderEnvelopeError blk) ()

This has the advantage of only needing to call the validation code at one place for ALL types of blocks.

This in turn requires getting the map of checkpoints at this point in the code. This can be done by adding the map as a method to the class ValidateEnvelope:

class ... => ValidateEnvelope blk where 
  ...
  -- | Checkpoints to validate. See 'validateIfCheckpoint'.
  checkpoints :: TopLevelConfig blk -> Map BlockNo (HeaderHash blk)

I wouldn't use a default method for this. It is better to signal to authors of instances that they need to define this method if they care about checkpointing, and they can easily return the empty map when they don't care.

Then the instances of ValidateEnvelope for ShelleyBlock and ByronBlock would need to return the checkpoints in their respective top-level configs.

As testing plan, we would only test validateIfCheckpoint for TestBlock. In particular, we wouldn't write any tests for the instances of ValidateEnvelope for ShelleyBlock and ByronBlock.

@nfrisby
Copy link
Contributor

nfrisby commented Jan 19, 2024

I suggest we take the "universality"-based approach even farther, and simply add a field topLevelConfigCheckpoints :: Map BlockNo (HeaderHash blk) to TopLevelConfig.

-- | The top-level node configuration
data TopLevelConfig blk = TopLevelConfig {
topLevelConfigProtocol :: !(ConsensusConfig (BlockProtocol blk))
, topLevelConfigLedger :: !(LedgerConfig blk)
, topLevelConfigBlock :: !(BlockConfig blk)
, topLevelConfigCodec :: !(CodecConfig blk)
, topLevelConfigStorage :: !(StorageConfig blk)
}

@amesgen
Copy link
Member Author

amesgen commented Jan 22, 2024

Just ensured that there are no roadblocks for this approach: #898 (diff could get much smaller with a compat layer/pattern synonym, but also not really necessary).

For completeness: A (we think relatively minor) disadvantage of #898 is that it adds checkpointing as a built-in concept to the abstract Consensus layer, despite this not being strictly necessary. (It is of course always possible to not do checkpointing by simply not providing any checkpoints.)

A small subtlety I noticed: In the HFC, we often create TopLevelConfigs for each era. In #898, these per-era TopLevelConfigs will contain no checkpoints, and this won't be caught by tests as validateEnvelope is only called at the HF layer.

@nfrisby
Copy link
Contributor

nfrisby commented Jan 22, 2024

I noticed: In the HFC, we often create TopLevelConfigs for each era

I'm not seeing the "in the HFC" aspect of this in the PR 898 diff. I see topLevelConfigCheckpoints = mempty in plenty of protocolInfo* functions, for example, and some testing functions. But those are not specific to the HFC, right?

... validateEnvelope is only called at the HF layer.

Does that imply that it's harmless for the per-era configs to have an empty checkpoints field? Since our intended semantics is for the checkpoints field to only affect validateEnvelope? Or am I oversimplifying something?

@amesgen
Copy link
Member Author

amesgen commented Jan 22, 2024

I noticed: In the HFC, we often create TopLevelConfigs for each era

I'm not seeing the "in the HFC" aspect of this in the PR 898 diff. I see topLevelConfigCheckpoints = mempty in plenty of protocolInfo* functions, for example, and some testing functions. But those are not specific to the HFC, right?

It happens here:

distribTopLevelConfig :: All SingleEraBlock xs
=> EpochInfo (Except PastHorizonException)
-> TopLevelConfig (HardForkBlock xs)
-> NP TopLevelConfig xs
distribTopLevelConfig ei tlc =
hcpure proxySingle
(fn_5 (\cfgConsensus cfgLedger cfgBlock cfgCodec cfgStorage ->
mkTopLevelConfig
(completeConsensusConfig' ei cfgConsensus)
(completeLedgerConfig' ei cfgLedger)
cfgBlock
cfgCodec
cfgStorage))

mkTopLevelConfig is touched in #898 and currently always uses mempty (just was the most direct thing to get things to compile).

... validateEnvelope is only called at the HF layer.

Does that imply that it's harmless for the per-era configs to have an empty checkpoints field? Since our intended semantics is for the checkpoints field to only affect validateEnvelope? Or am I oversimplifying something?

Yes, that is exactly right 👍 (Theoretically, some concrete block could of course depend on them in some non-trivial way in eg additionalEnvelopeChecks, but I don't have a realistic use case in mind.)

@nbacquey nbacquey force-pushed the amesgen/lightweight-checkpointing-non-comb branch from eb1740f to d83a3a9 Compare February 29, 2024 10:29
@amesgen
Copy link
Member Author

amesgen commented Mar 13, 2024

Superseded by #898

@amesgen amesgen closed this Mar 13, 2024
@amesgen amesgen deleted the amesgen/lightweight-checkpointing-non-comb branch March 13, 2024 11:48
github-merge-queue bot pushed a commit that referenced this pull request Mar 22, 2024
Lightweight checkpointing is a mechanism to ensure that new nodes end up
in the correct chain even when the chain is too sparse for normal
operation of Praos and Genesis.

The idea is to supply the new node with a list of block hashes that
should be present on the chain at specific block numbers. This list is
provided in the TopLevelConfig record, and it is used during validation
of headers in validateIfCheckpoint called by validateEnvelope.

If the hashes of checkpoints don't match the hashes of a supplied header
for a given block number, then validation of the header fails.

The substance of the change is modifying TopLevelConfig and
validateEnvelope. Most other changes derive from these modifications.
There are three new unit tests of validateIfCheckpoint added in
ouroboros-consensus:test:consensus-test.

Implements
#453 (comment)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Genesis PRs related to Genesis testing and implementation
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants