Files
consensus/protocol/quasar/security_regression_test.go
zeekay cf73ba3c48 consensus(chain): round-2/3 hardening — epoch-bind certs, SyncState monotonic guard, BFT msg, drop dead witness
MEDIUM (epoch binding): stake-weighted finality must be measured at the
cert-position epoch, not at verification time. Bind the active weighted
validator-set commitment into VotePosition.ValidatorSetRoot and fold it into
CanonicalVoteMessage + the wire codec, so a cert is cryptographically pinned to
the exact set it was certified under: re-presenting an epoch-E cert as
certifying under a different epoch's set fails clause-6 signature verification,
and a later stake change cannot retroactively flip an already-correct cert.
Mirrors quasar.ConsensusCert.ValidatorSetRoot. New optional ValidatorSetRootSource
(WithValidatorSetRoot / NetworkConfig.ValidatorSetRoot), stamped in
blockPositionLocked; nil source => ids.Empty => behavior identical to before
(backward-safe for fixed-set chains). QuorumCertVersion bumped 1->2 so a
mixed-version cert fails loudly (version + signature) rather than mis-parsing —
a deliberate forward-only break (the set upgrades together in one cascade).
TDD: TestEpochBinding_{CrossEpochCertRejected,StakeReadAtCertHeight,RoundTripPreservesRoot}
(proven to fail without the canonical-message binding).

INFO-2 (SyncState monotonic guard): ChainConsensus.SyncState bypasses
markFinalizedLocked by design (import is an out-of-band reconcile), so the
monotonic invariant is re-asserted explicitly — a backward import (height below
the already-finalized height) is refused (ErrSyncStateRegression) and leaves
finalized state untouched; an equal-height re-import must agree on the block;
forward + empty-reset still succeed. SyncState now returns error; the sole caller
(Transitive.SyncState) propagates it and does NOT partially mutate on refusal.
TDD: TestSyncState_{RefusesBackwardRegression,EqualHeightDifferentBlockRefused,
ForwardImportAdvances,EmptyResetAllowed} (proven to fail without the guard).

INFO-3 (BFT message): ErrKTooLowForMainnet/Testnet now state the real bound
(2*AlphaPreference-K >= floor((K-1)/3)+1 with f>=3 / f>=1) instead of a bare
"K>=11" magic number. Sentinel-based assertions unaffected.

INFO-1 (dead code): delete the VerkleWitness apparatus (witness.go + its tests +
the VerkleWitness funcs in comprehensive_test.go + the verifyIPAOpening test) —
zero non-test callers; verifyBLSAggregate was a deserialize-only (no-pairing)
forgery sink if ever revived. The live deserialization path that matters is
crypto/bls.SignatureFromBytes (fixed + tested in the crypto module);
TestVerifyBLSAggregate_C1_ActualDeserialization was proven green in CGO_ENABLED=0
against the crypto fix BEFORE this deletion removed it.
2026-06-22 18:49:19 -07:00

65 lines
2.6 KiB
Go

package quasar
import (
"testing"
"github.com/stretchr/testify/require"
)
// NOTE: TestF002_ConstantTimeProofComparison was removed together with the dead
// VerkleWitness apparatus (witness.go) it exercised — verifyIPAOpening was a
// helper of that forgery-sink-adjacent, zero-non-test-caller code. The
// constant-time-compare property it asserted lives at the (still-tested) callers
// of crypto/subtle elsewhere; nothing in production depended on verifyIPAOpening.
// TestDualSignRound1_ErrorPropagation verifies that DualSignRound1
// propagates errors from BOTH the BLS and Corona signing paths.
// Previously, the Corona error was silently discarded (line 329).
//
// Regression test for the silent error drop found during review analysis.
func TestDualSignRound1_ErrorPropagation(t *testing.T) {
// Attempting dual sign with unconfigured validator should error
s, err := newSigner(2) // threshold of 2
require.NoError(t, err)
_, _, err = s.DualSignRound1(nil, "nonexistent-validator", []byte("msg"), 1, []byte("prf"))
require.Error(t, err, "DualSignRound1 must error for unconfigured validator")
require.Contains(t, err.Error(), "not configured for dual signing")
}
// TestCertBundle_VerifyPanics verifies the deprecated Verify method panics
// to prevent accidental use of the non-cryptographic path.
func TestCertBundle_VerifyPanics(t *testing.T) {
cert := &CertBundle{
BLSAgg: []byte{0x01, 0x02, 0x03},
PQCert: []byte{0x04, 0x05, 0x06},
}
require.Panics(t, func() { cert.Verify(nil) }, "Verify must panic to enforce VerifyWithKeys usage")
}
// TestCertBundle_VerifyWithKeys_Regression tests cryptographic KMAC256 verification.
func TestCertBundle_VerifyWithKeys_Regression(t *testing.T) {
blsKey := []byte("regression-bls-key-32bytes!!")
pqKey := []byte("regression-pq-key-32bytes!!!")
message := []byte("regression test")
cert := &CertBundle{
BLSAgg: kmac256(blsKey, message, kmacMACOutLen, customQuasarEventHorizonBLSMAC),
PQCert: kmac256(pqKey, message, kmacMACOutLen, customQuasarEventHorizonPQMAC),
Message: message,
}
require.True(t, cert.VerifyWithKeys(blsKey, pqKey), "valid cert passes KMAC256 check")
// Nil cert fails
var nilCert *CertBundle
require.False(t, nilCert.VerifyWithKeys(blsKey, pqKey), "nil cert must fail")
// Empty BLS fails
emptyBLS := &CertBundle{BLSAgg: []byte{}, PQCert: cert.PQCert, Message: message}
require.False(t, emptyBLS.VerifyWithKeys(blsKey, pqKey), "empty BLS must fail")
// Empty PQ fails
emptyPQ := &CertBundle{BLSAgg: cert.BLSAgg, PQCert: []byte{}, Message: message}
require.False(t, emptyPQ.VerifyWithKeys(blsKey, pqKey), "empty PQ must fail")
}