Skip to content

Conversation

@tim-barry
Copy link
Contributor

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

@github-actions
Copy link
Contributor

github-actions bot commented Dec 4, 2025

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@tim-barry tim-barry marked this pull request as ready for review December 8, 2025 18:03
@tim-barry tim-barry requested a review from a team as a code owner December 8, 2025 18:03
}
// 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)) {
Copy link
Member

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:

  1. Update the API to describe newly added sentinel error.
  2. 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.

Copy link
Member

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
Copy link
Member

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 {
Copy link
Member

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) {
Copy link
Member

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) {
Copy link
Member

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 {
Copy link
Member

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.

Comment on lines +20 to +23
// 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) (
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// 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
Copy link
Member

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$, then minRefHeight is actually $FirstBlockInEpoch.Height$ (we don't need to search further back).
  • 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)) {
Copy link
Member

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 {
Copy link
Member

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 NewClusterHeaders
    • NewClusterHeaders returns an error if if thinks the chain ID input is not a cluster chain
    • NewHeaders returns 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 newHeaders function 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[LN Decentralization E] Differentiate between consensus Headers and cluster Headers

5 participants