Skip to content

fix(controller): avoid panic when producing a proposal before the mempool has cached one#455

Open
amathxbt wants to merge 2 commits into
canopy-network:mainfrom
amathxbt:fix/nil-proposal-block-panic
Open

fix(controller): avoid panic when producing a proposal before the mempool has cached one#455
amathxbt wants to merge 2 commits into
canopy-network:mainfrom
amathxbt:fix/nil-proposal-block-panic

Conversation

@amathxbt

@amathxbt amathxbt commented Jul 4, 2026

Copy link
Copy Markdown

Bug

Controller.GetProposalBlockFromMempool() (in controller/tx.go) does an unchecked type assertion on an atomic.Value:

func (c *Controller) GetProposalBlockFromMempool() *CachedProposal {
	return c.Mempool.cachedProposal.Load().(*CachedProposal)
}

cachedProposal is an atomic.Value{} zero value until CheckMempool() successfully stores the first *CachedProposal. Calling Load() on an atomic.Value that has never been written returns nil (the untyped zero value), and asserting nil.(*CachedProposal) without the , ok form panics with interface conversion: interface {} is nil, not *controller.CachedProposal.

This is reachable from Controller.ProduceProposal() (in controller/block.go), which is invoked by the BFT module whenever this node is selected as the block proposer:

p := c.GetProposalBlockFromMempool()
...
p.Block.BlockHeader.LastQuorumCertificate, p.Block.BlockHeader.Vdf = lastCertificate, vdf

If a node is elected proposer before its background CheckMempool() loop has completed even one pass (e.g. right after startup, or when LazyMempoolCheckFrequencyS is configured such that the first tick hasn't fired yet), this panics the node instead of returning a handled error.

Fix

  • GetProposalBlockFromMempool() now uses the safe two-value type assertion and returns nil instead of panicking when no proposal has been cached yet.
  • ProduceProposal() now checks for a nil result immediately after calling GetProposalBlockFromMempool() and returns lib.ErrNilBlock() (an existing, semantically-appropriate error already used elsewhere in the codebase) instead of dereferencing a nil pointer a few lines later at p.Block.BlockHeader....

Testing

Traced the only call site of GetProposalBlockFromMempool() in the repository (controller/block.go:ProduceProposal) to confirm the nil case is now handled before any dereference of p, and that err is a pre-declared named return so no signature changes were required.

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.

1 participant