quantumvm: count distinct validators toward both finality thresholds

AddCoronaSignature and AddBLSSignature appended caller-supplied signatures with no
signer check, and TryFinalize compares len(signatures) against q.threshold. One
peer resending the same signature therefore drove the count to the threshold on
its own. Proven: three calls carrying one validator's signature left three entries
in the list.

The Corona leg is the sharp end: TryFinalize finalizes it on count alone and
verifies none of the signatures, unlike the BLS leg which aggregates and verifies.
So a single peer satisfied the post-quantum half of quantum finality by itself.

Insertion for both legs now goes through addBLS/addCorona, which record one
signature per ValidatorID, and SignBlock's own contribution goes through the same
door instead of appending directly.

Tests: TestOneValidatorCannotReachCoronaThreshold,
TestDistinctValidatorsReachCoronaThreshold, TestOneValidatorCannotReachBLSThreshold.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-07-29 16:48:10 -07:00
co-authored by Hanzo Dev
parent bcf93b97db
commit af5096e6fe
2 changed files with 143 additions and 5 deletions
+45 -5
View File
@@ -13,6 +13,7 @@ package quantumvm
import (
"context"
"errors"
"fmt"
"sync"
@@ -195,8 +196,8 @@ func (q *Quasar) SignBlock(ctx context.Context, blockID ids.ID, blockHash []byte
}
q.mu.Lock()
pending.BLSSignatures = append(pending.BLSSignatures, blsSig)
pending.CoronaSignatures = append(pending.CoronaSignatures, pqSig)
pending.addBLS(blsSig)
pending.addCorona(pqSig)
q.mu.Unlock()
q.log.Debug("Block signed with Quasar (BLS + Corona parallel)",
@@ -209,6 +210,36 @@ func (q *Quasar) SignBlock(ctx context.Context, blockID ids.ID, blockHash []byte
return &BlockSigs{BLS: blsSig, Corona: pqSig}, nil
}
// errDuplicateSigner — a validator that already contributed a signature for this
// block sent another. Both finality legs are counted by len(signatures) against
// the threshold, so admitting a second signature from one validator would let a
// single peer reach the threshold alone by resending.
var errDuplicateSigner = errors.New("quantumvm: validator already signed this block")
// addBLS records one BLS signature per validator and reports whether it was new.
// Caller holds q.mu.
func (p *PendingBlock) addBLS(sig *quasar.BLSSignature) bool {
for _, have := range p.BLSSignatures {
if have.ValidatorID == sig.ValidatorID {
return false
}
}
p.BLSSignatures = append(p.BLSSignatures, sig)
return true
}
// addCorona records one Corona signature per validator and reports whether it was
// new. Caller holds q.mu.
func (p *PendingBlock) addCorona(sig *quasar.CoronaSignature) bool {
for _, have := range p.CoronaSignatures {
if have.ValidatorID == sig.ValidatorID {
return false
}
}
p.CoronaSignatures = append(p.CoronaSignatures, sig)
return true
}
// AddBLSSignature adds a BLS signature from another validator
func (q *Quasar) AddBLSSignature(blockID ids.ID, sig *quasar.BLSSignature) error {
q.mu.Lock()
@@ -219,7 +250,9 @@ func (q *Quasar) AddBLSSignature(blockID ids.ID, sig *quasar.BLSSignature) error
return fmt.Errorf("pending block not found: %s", blockID)
}
pending.BLSSignatures = append(pending.BLSSignatures, sig)
if !pending.addBLS(sig) {
return fmt.Errorf("%w: %s (BLS)", errDuplicateSigner, sig.ValidatorID)
}
q.log.Debug("Added BLS signature",
"blockID", blockID,
@@ -230,7 +263,12 @@ func (q *Quasar) AddBLSSignature(blockID ids.ID, sig *quasar.BLSSignature) error
return nil
}
// AddCoronaSignature adds a Corona signature from another validator
// AddCoronaSignature adds a Corona signature from another validator.
//
// The Corona leg is finalized on count alone (TryFinalize does not verify Corona
// signatures the way the BLS leg verifies its aggregate), so the count must be a
// count of DISTINCT validators or the post-quantum half of quantum finality is
// satisfied by one peer resending one signature.
func (q *Quasar) AddCoronaSignature(blockID ids.ID, sig *quasar.CoronaSignature) error {
q.mu.Lock()
defer q.mu.Unlock()
@@ -240,7 +278,9 @@ func (q *Quasar) AddCoronaSignature(blockID ids.ID, sig *quasar.CoronaSignature)
return fmt.Errorf("pending block not found: %s", blockID)
}
pending.CoronaSignatures = append(pending.CoronaSignatures, sig)
if !pending.addCorona(sig) {
return fmt.Errorf("%w: %s (Corona)", errDuplicateSigner, sig.ValidatorID)
}
q.log.Debug("Added Corona signature",
"blockID", blockID,
+98
View File
@@ -0,0 +1,98 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package quantumvm
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"github.com/luxfi/consensus/protocol/quasar"
"github.com/luxfi/ids"
"github.com/luxfi/log"
)
// pendingQuasar returns a Quasar with one pending block awaiting signatures.
func pendingQuasar(threshold int) (*Quasar, ids.ID) {
blockID := ids.GenerateTestID()
q := &Quasar{
threshold: threshold,
log: log.NewNoOpLogger(),
pendingBlocks: map[ids.ID]*PendingBlock{
blockID: {
BlockID: blockID,
BlockHash: blockID[:],
Height: 1,
BLSSignatures: make([]*quasar.BLSSignature, 0),
CoronaSignatures: make([]*quasar.CoronaSignature, 0),
},
},
finalizedBlocks: make(map[ids.ID]bool),
}
return q, blockID
}
// TestOneValidatorCannotReachCoronaThreshold proves the post-quantum finality leg
// counts distinct validators.
//
// TryFinalize finalizes the Corona leg on len(CoronaSignatures) >= threshold and
// verifies none of those signatures (unlike the BLS leg, which aggregates and
// verifies). A validator resending its signature therefore drove the count to the
// threshold on its own and satisfied the post-quantum half of quantum finality
// alone.
func TestOneValidatorCannotReachCoronaThreshold(t *testing.T) {
require := require.New(t)
q, blockID := pendingQuasar(3)
sig := &quasar.CoronaSignature{Signature: []byte{1}, ValidatorID: "validator-1", SignerIndex: 0, Round: 1}
require.NoError(q.AddCoronaSignature(blockID, sig))
require.ErrorIs(q.AddCoronaSignature(blockID, sig), errDuplicateSigner)
require.ErrorIs(q.AddCoronaSignature(blockID, sig), errDuplicateSigner)
pending := q.pendingBlocks[blockID]
require.Len(pending.CoronaSignatures, 1, "one validator contributes one signature")
require.Less(len(pending.CoronaSignatures), q.threshold,
"a single validator must stay below the threshold")
// Even a signature with the same validator but different bytes is one vote.
require.ErrorIs(q.AddCoronaSignature(blockID, &quasar.CoronaSignature{
Signature: []byte{9}, ValidatorID: "validator-1", SignerIndex: 0, Round: 2,
}), errDuplicateSigner)
require.Len(pending.CoronaSignatures, 1)
_, finalized, err := q.TryFinalize(context.Background(), blockID)
require.Error(err, "quasar core is not initialized in this fixture")
require.False(finalized)
require.False(pending.CoronaFinalized, "the Corona leg must not be finalized by one validator")
}
// TestDistinctValidatorsReachCoronaThreshold is the other half: three different
// validators do reach a threshold of three.
func TestDistinctValidatorsReachCoronaThreshold(t *testing.T) {
require := require.New(t)
q, blockID := pendingQuasar(3)
for _, id := range []string{"validator-1", "validator-2", "validator-3"} {
require.NoError(q.AddCoronaSignature(blockID, &quasar.CoronaSignature{
Signature: []byte{1}, ValidatorID: id, Round: 1,
}))
}
require.Len(q.pendingBlocks[blockID].CoronaSignatures, 3)
}
// TestOneValidatorCannotReachBLSThreshold is the same rule on the classical leg,
// where a resent signature also inflated the count compared against the
// threshold.
func TestOneValidatorCannotReachBLSThreshold(t *testing.T) {
require := require.New(t)
q, blockID := pendingQuasar(3)
sig := &quasar.BLSSignature{Signature: []byte{1}, ValidatorID: "validator-1"}
require.NoError(q.AddBLSSignature(blockID, sig))
require.ErrorIs(q.AddBLSSignature(blockID, sig), errDuplicateSigner)
require.ErrorIs(q.AddBLSSignature(blockID, sig), errDuplicateSigner)
require.Len(q.pendingBlocks[blockID].BLSSignatures, 1)
}