-
Notifications
You must be signed in to change notification settings - Fork 202
Differentiate between Consensus and Cluster Headers storage #8222
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
base: master
Are you sure you want to change the base?
Conversation
Weakens the chainID requirement for cluster chains when reading from storage.
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
| } | ||
| // raise an error when the retrieved header is for a different chain than expected, | ||
| // except in the case of cluster chains where the previous epoch(=chain) can be checked for transaction deduplication | ||
| if header.ChainID != chainID && !(isClusterChain(chainID) && isClusterChain(header.ChainID)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not particularly fond of this. Seems like a workaround for a particular case, additionally we rely on the naming of the chain ID to be specific for recognizing a cluster. Additionally, I don't see that sentinel error is explained in the general interface.
I would propose next:
- Update the API to describe newly added sentinel error.
- Avoid any specific workarounds for specific chain IDs and just return a sentinel if there is a wrong chain. The user of the interface needs to deal with this on his own but not rely on the implementation detail of the storage layer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 - See my comment here https://github.com/onflow/flow-go/pull/8222/files#r2608027971, I think we can remove the problematic part of this.
| if err != nil { | ||
| return err | ||
| } | ||
| storages := common.InitStorages(db, chainID) // TODO(4204) - header storage not used |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you want to address this TODO in this PR?
| } | ||
|
|
||
| func (fnb *FlowNodeBuilder) determineChainID() error { | ||
| if ok, _ := badgerState.IsBootstrapped(fnb.ProtocolDB); ok { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should handle the error from IsBootstrapped
|
|
||
| // GetLatestFinalizedHeader attempts to retrieve the latest finalized header | ||
| // without going through the storage.Headers interface. | ||
| func GetLatestFinalizedHeader(db storage.DB) (*flow.Header, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
document expected errors here
| return true, nil | ||
| } | ||
|
|
||
| func GetChainIDFromLatestFinalizedHeader(db storage.DB) (flow.ChainID, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
document expected errors here
| func (fnb *FlowNodeBuilder) determineChainID() error { | ||
| if ok, _ := badgerState.IsBootstrapped(fnb.ProtocolDB); ok { | ||
| chainID, err := badgerState.GetChainIDFromLatestFinalizedHeader(fnb.ProtocolDB) | ||
| if err == nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would invert this to handle the err != nil case in the conditional (return unexpected error) and set the chain ID outside the conditional. Otherwise we are ignoring unexpected errors, which we should generally never do.
| // ChainID refers to the consensus chain, from which reference blocks are used. | ||
| // | ||
| // Must return ErrNotAuthorizedForEpoch if this node is not authorized in the epoch. | ||
| Create(epoch protocol.CommittedEpoch) ( | ||
| Create(epoch protocol.CommittedEpoch, chainID flow.ChainID) ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| // ChainID refers to the consensus chain, from which reference blocks are used. | |
| // | |
| // Must return ErrNotAuthorizedForEpoch if this node is not authorized in the epoch. | |
| Create(epoch protocol.CommittedEpoch) ( | |
| Create(epoch protocol.CommittedEpoch, chainID flow.ChainID) ( | |
| // | |
| // Must return ErrNotAuthorizedForEpoch if this node is not authorized in the epoch. | |
| Create(epoch protocol.CommittedEpoch, consensusChainID flow.ChainID) ( |
|
|
||
| for _, blockID := range clusterBlockIDs { | ||
| header, err := b.clusterHeaders.ByBlockID(blockID) | ||
| header, err := b.clusterHeaders.ByBlockID(blockID) // TODO(4204) transaction deduplication crosses clusterHeaders epoch boundary |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Transaction de-duplication actually does not occur across cluster and epoch boundaries.
- Each transaction is uniquely assigned to one cluster in one epoch, based on the transaction's reference block (see ingestion logic)
- Therefore, each cluster has a range of reference block heights they can accept. These ranges are equivalent to the height range of blocks within an epoch (
$[FirstBlockInEpoch.Height, LastBlockInEpoch.Height]$ . These ranges are consecutive and do not overlap.- In short, if we are considering a cluster block with reference block height
$FirstBlockInEpoch.Height$ , thenminRefHeightis actually$FirstBlockInEpoch.Height$ (we don't need to search further back).
- In short, if we are considering a cluster block with reference block height
- We already take this into account when determining the lowest possible reference block
So I think we can remove this TODO, and remove the special-case logic in storage.Headers meant to work around this. I would also suggest adding some documentation here explaining why there is no overlap between clusters and epochs.
| } | ||
| // raise an error when the retrieved header is for a different chain than expected, | ||
| // except in the case of cluster chains where the previous epoch(=chain) can be checked for transaction deduplication | ||
| if header.ChainID != chainID && !(isClusterChain(chainID) && isClusterChain(header.ChainID)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 - See my comment here https://github.com/onflow/flow-go/pull/8222/files#r2608027971, I think we can remove the problematic part of this.
| return operation.InsertHeader(lctx, rw, blockID, header) | ||
| } | ||
|
|
||
| isClusterChain := func(chainID flow.ChainID) bool { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would suggest defining this as a method on flow.ChainID to consolidate the logic and documentation. I also think we can improve safety slightly:
- Create a new constructor
NewClusterHeadersNewClusterHeadersreturns an error if if thinks the chain ID input is not a cluster chainNewHeadersreturns an error if if thinks the chain ID input is a cluster chain- Each constructor binds the appropriate height lookup function for the kind of header it is for. (Most of the constructor logic can go into a shared, private
newHeadersfunction that accepts the height lookup as an argument).
I prefer this because it makes the clients expectations more explicit. If the IsClusterChain logic fails to match the constructor used, then we will error, rather than continuing in an inconsistent state.
A ChainID must now be provided to Headers storage instance on creation.
That storage instance will then only be able to successfully store or retrieve headers corresponding to the correct ChainID. In addition, the height-based index will also be specific to that ChainID.
Currently, an exception is made for cluster chains: Since the ChainID changes when a new epoch begins, but collection nodes still access collections from the previous epoch/chain (to deduplicate transactions), storage instances for cluster chains are allowed to retrieve (but not store) headers from other cluster chains.
This should likely be further refactored.
Closes: #4204