mirror of
https://github.com/luxfi/consensus.git
synced 2026-09-24 00:26:58 +00:00
chain: a cert is not the source of its own bar, and an asker does not size a buffer
NovaSignerFloor saturates at NovaQuorum(minBFTCommittee) = 3 for every K, so a committee of nine was asked for three signatures. With no stake source wired nothing downstream re-derives the majority either, leaving the cert's own declared Threshold as the entire bar — a security parameter chosen by the party it constrains. Quasar's weighted verifier already refuses that and pins its own minimum; the engine cert had no floor at all. Three consequences, each with a test: four of nine finalizes where the majority is five; two disjoint triples both certify different blocks at one height with no validator equivocating and none misbehaving; and a three-vote cert for a losing sibling clears verification and jails three validators who each signed one block once — jailing a majority of them halts the chain more cheaply than the fork the cert claims to prove. Votes are broadcast, so anyone on the gossip path holds enough of them. The floor is now the live committee's majority, checked against the votes the cert CARRIES rather than the number it claims to need, at both doors. Leaving catch-up behind would have kept the whole hole open one path over. Separately: From reserved make([]Certified, 0, max) with max straight from the asker, so a peer writing MaxInt cost a makeslice panic in one packet. It was the only peer-sized quantity here without a cap — vote_count is checked against the remaining frame, sig_len against the buffer, the Merkle step count at 64. Nothing past the served window can be answered anyway, and the run is built by appending, so capping the reservation changes no reply. Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -247,7 +247,11 @@ func (rt *Runtime) VerifyCatchupCertificate(ctx context.Context, blockBytes, cer
|
||||
chainID := t.chainID
|
||||
floor := t.consensus.Alpha()
|
||||
if cert.Tier == Nova {
|
||||
floor = NovaSignerFloor(t.consensus.K())
|
||||
// The live committee's majority. NovaSignerFloor saturates at 3 for every
|
||||
// K, so it asked a committee of nine for three signatures — and this door
|
||||
// is the mirror of the gossip one, so leaving it behind would keep the
|
||||
// whole hole open on the catch-up path. One invariant, two doors.
|
||||
floor = NovaQuorum(t.consensus.K())
|
||||
}
|
||||
setRootSource := t.setRootSource
|
||||
t.mu.RUnlock()
|
||||
@@ -255,7 +259,9 @@ func (rt *Runtime) VerifyCatchupCertificate(ctx context.Context, blockBytes, cer
|
||||
if cert.Position.ChainID != chainID ||
|
||||
cert.Position.Height != blk.Height() ||
|
||||
certCanonical(cert) != canonicalIDOf(blk) ||
|
||||
(floor > 0 && cert.Threshold < uint32(floor)) {
|
||||
// The votes carried, not the number claimed: a self-declared threshold is
|
||||
// a bar chosen by the party it constrains.
|
||||
(floor > 0 && (cert.Threshold < uint32(floor) || cert.VoterCount() < floor)) {
|
||||
return ErrCatchupCertRejected
|
||||
}
|
||||
|
||||
|
||||
@@ -83,6 +83,20 @@ func (rt *Runtime) From(ctx context.Context, height uint64, max int) (Run, error
|
||||
return Run{Base: height}, nil
|
||||
}
|
||||
|
||||
// The asker names max, so the reservation cannot be sized from it. A peer that
|
||||
// writes MaxInt costs a `makeslice: cap out of range` panic in one packet, and
|
||||
// this is the only peer-sized quantity in the package that was not capped at
|
||||
// the read — vote_count is checked against the remaining frame, sig_len
|
||||
// against the buffer, the Merkle step count at 64, and the served-cert window
|
||||
// is a constant.
|
||||
//
|
||||
// Nothing beyond that window can be served anyway, so a larger request is not
|
||||
// a bigger answer; it is only a bigger allocation. The loop below stops at the
|
||||
// first height this node cannot answer, so capping the RESERVATION changes no
|
||||
// reply — the run is built by appending, not by filling.
|
||||
if max > maxServedCerts {
|
||||
max = maxServedCerts
|
||||
}
|
||||
run := Run{Base: height, Chain: make([]Certified, 0, max)}
|
||||
for h := height; h < height+uint64(max); h++ {
|
||||
id, ok := rt.finalizedAt(h)
|
||||
|
||||
@@ -201,9 +201,21 @@ func (rt *Runtime) HandleIncomingCert(certBytes []byte) bool {
|
||||
// count majority every legitimate cert already carries, so the filter is unweakened.
|
||||
floor := t.consensus.Alpha() // Quasar ⅔ count floor
|
||||
if cert.Tier == Nova {
|
||||
floor = NovaSignerFloor(t.consensus.K()) // Nova signer floor
|
||||
// The MAJORITY of the live committee, not NovaSignerFloor. That floor
|
||||
// saturates at NovaQuorum(minBFTCommittee) = 3 for every K, so on a
|
||||
// committee of nine it asked for three — and with no stake source wired
|
||||
// nothing downstream re-derives the majority, leaving the cert's own
|
||||
// declared Threshold as the whole bar. Three relayed votes then finalize
|
||||
// on any committee, two disjoint triples both certify at one height with
|
||||
// no validator equivocating, and a three-vote cert for a losing sibling
|
||||
// jails three validators who each signed one block once.
|
||||
floor = NovaQuorum(t.consensus.K())
|
||||
}
|
||||
if floor > 0 && cert.Threshold < uint32(floor) {
|
||||
// Against the votes the cert ACTUALLY carries, not the number it claims to
|
||||
// need. A self-declared threshold is a security parameter chosen by the party
|
||||
// the parameter is meant to constrain; quasar's own weighted verifier already
|
||||
// refuses to let a cert be the sole source of its own bar.
|
||||
if floor > 0 && (cert.Threshold < uint32(floor) || cert.VoterCount() < floor) {
|
||||
if !rt.config.Logger.IsZero() {
|
||||
rt.config.Logger.Warn("incoming cert: threshold below chain floor for its tier",
|
||||
log.Stringer("blockID", cert.Position.BlockID),
|
||||
|
||||
Reference in New Issue
Block a user