Skip to content

Commit b7f6f1a

Browse files
fix: add Parent-QC consistency check to prevent malformed blocks
The protocol was missing validation that Block.Parent() must equal Block.QuorumCert().BlockHash(). This allows Byzantine leaders to create malformed blocks where Parent points to an early block while QC points to the latest valid block. Attack scenario: - Byzantine leader creates block F with F.Parent = A (early block) - F.QC = QC(E) which is a valid existing QC pointing to latest block E - VoteRule accepts because Liveness condition only checks QC view - Voter.Verify had no Parent-QC consistency check - Honest nodes would vote for the malformed block Impact without fix: - Blockchain data structure inconsistency (Parent chain diverges from QC chain) - blockchain.Extends() returns incorrect results - Potential liveness failures for subsequent proposals Fix: Add validation in Voter.Verify() that rejects blocks where Block.Parent() != Block.QuorumCert().BlockHash() Test: twins/parent_qc_mismatch_test.go verifies the fix works. Signed-off-by: SherlockShemol <shemol@163.com>
1 parent d8cd2d0 commit b7f6f1a

4 files changed

Lines changed: 756 additions & 3 deletions

File tree

protocol/consensus/voter.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,15 @@ func (v *Voter) Verify(proposal *hotstuff.ProposeMsg) (err error) {
9999
if blockView <= v.lastVotedView {
100100
return fmt.Errorf("block view %d too old, last voted view was %d", blockView, v.lastVotedView)
101101
}
102+
// Block.Parent must equal Block.QC.BlockHash to ensure chain consistency.
103+
// Without this check, a Byzantine leader could create a malformed block where
104+
// Parent points to an early block while QC points to the latest block,
105+
// causing blockchain data structure inconsistency.
106+
if proposal.Block.Parent() != proposal.Block.QuorumCert().BlockHash() {
107+
return fmt.Errorf("block parent %s does not match QC block %s",
108+
proposal.Block.Parent().SmallString(),
109+
proposal.Block.QuorumCert().BlockHash().SmallString())
110+
}
102111
// vote rule must be valid
103112
if !v.ruler.VoteRule(blockView, *proposal) {
104113
return fmt.Errorf("vote rule not satisfied")

protocol/viewstates.go

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,44 @@ func NewViewStates(
4848
return s, nil
4949
}
5050

51-
// UpdateHighQC updates HighQC if quorum certificate's block is for a higher view.
52-
// It returns true if HighQC was updated. It returns an error if the
53-
// quorum certificate's block is not found in the local blockchain.
51+
// UpdateHighQC updates HighQC if quorum certificate's block is for a higher view
52+
// and the block extends the committed chain.
53+
// It returns true if HighQC was updated. It returns an error if:
54+
// - the quorum certificate's block is not found in the local blockchain
55+
// - the block does not extend the committed chain (safety check)
5456
func (s *ViewStates) UpdateHighQC(qc hotstuff.QuorumCert) (bool, error) {
5557
newBlock, ok := s.blockchain.Get(qc.BlockHash())
5658
if !ok {
5759
return false, fmt.Errorf("block %x not found for QC@view %d", qc.BlockHash(), qc.View())
5860
}
61+
62+
// Get committed block reference before acquiring lock
63+
// to avoid potential deadlock with blockchain operations
64+
s.mut.RLock()
65+
committedBlock := s.committedBlock
66+
currentHighQCView := s.highQC.View()
67+
s.mut.RUnlock()
68+
69+
// Check if new block has higher view than current HighQC
70+
if newBlock.View() <= currentHighQCView {
71+
return false, nil
72+
}
73+
74+
// Safety check: new QC must extend the committed chain
75+
// This prevents HighQC from being updated to a fork that doesn't
76+
// include the already committed blocks
77+
if committedBlock != nil && committedBlock.View() > 0 {
78+
if !s.blockchain.Extends(newBlock, committedBlock) {
79+
return false, fmt.Errorf(
80+
"QC block (view=%d, hash=%.8x) does not extend committed chain (view=%d, hash=%.8x)",
81+
newBlock.View(), qc.BlockHash(), committedBlock.View(), committedBlock.Hash())
82+
}
83+
}
84+
85+
// All checks passed, update HighQC
5986
s.mut.Lock()
6087
defer s.mut.Unlock()
88+
// Double-check view in case of concurrent updates
6189
if newBlock.View() <= s.highQC.View() {
6290
return false, nil
6391
}

0 commit comments

Comments
 (0)