mirror of
https://github.com/luxfi/node.git
synced 2026-07-27 03:39:39 +00:00
fix(consensus): route GossipOp votes to the engine — un-wedge α-of-K finality
The α-of-K quorum vote/cert transport rides on app-gossip (GossipOp), but GossipOp was never added to the chain router's consensus-op table (message.ToConsensusOp) nor to blockHandler.HandleInbound's switch. So every inbound vote was dropped at node/chain_router.go as "unhandled message op" before it could reach blockHandler.Gossip -> engine.HandleIncomingVote. Blocks ride PutOp (mapped) and propagated+verified on all validators; votes ride GossipOp (unmapped) and vanished. Each node held only its own self-vote, the cert never reached alpha, and every chain wedged at height N: "verified but never accepted", no vote/cert/accept activity. This is the fork divergence from avalanchego, where votes are Chits — a first-class consensus op always in the router table (snow/engine/snowman engine.go Chits -> voter -> topological.RecordPoll -> accept). Lux moved votes onto app-gossip but left the node-side op routing incomplete; the consensus repo already reserved router.Gossip=11 'routed via the blockHandler Gossip method' (core/router/router.go) — only the node wiring was missing. Fix (node-only; safety core untouched — this is purely liveness): - message.ToConsensusOp: map GossipOp -> 11 (router.Gossip) - blockHandler.HandleInbound: add case handler.Gossip -> b.Gossip(...) b.Gossip already demuxes the quorum envelope (Mode-gated) into HandleIncomingVote / HandleIncomingCert; the signed-vote verify + verified-2/3 -stake cert assembly are unchanged, so honest votes now reach the assembler without weakening VerifyWeighted or the unforgeable cert. Regression guard (message/ops_test.go): GossipOp must map to router.Gossip, the full op table must stay aligned with the consensus router, and an inbound vote gossip must survive router extraction (op + envelope bytes) intact.
This commit is contained in:
@@ -3103,6 +3103,14 @@ func (b *blockHandler) HandleInbound(ctx context.Context, msg handler.Message) e
|
||||
case handler.Context:
|
||||
// Context contains prerequisite blocks - process each one via Put
|
||||
return b.handleContext(ctx, msg.NodeID, msg.RequestID, msg.Message)
|
||||
case handler.Gossip:
|
||||
// App-gossip envelope carrying an α-of-K quorum vote or finality cert.
|
||||
// Route to Gossip, which demuxes the quorum envelope into
|
||||
// engine.HandleIncomingVote / HandleIncomingCert (the vote TRANSPORT that
|
||||
// drives finality). Without this case the router delivers the message here
|
||||
// but the switch drops it, so no vote is ever counted and the chain wedges.
|
||||
// Non-quorum gossip falls through inside Gossip to a plain block Put.
|
||||
return b.Gossip(ctx, msg.NodeID, msg.Message)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -272,6 +272,15 @@ func ToConsensusOp(op Op) (byte, bool) {
|
||||
return 9, true // GetContext (wire protocol still uses GetAncestors)
|
||||
case AncestorsOp:
|
||||
return 10, true // Context (wire protocol still uses Ancestors)
|
||||
case GossipOp:
|
||||
// Gossip (= router.Gossip). The α-of-K quorum vote/cert transport rides on
|
||||
// app-gossip. Without this mapping the chain router drops every inbound vote
|
||||
// as "unhandled message op" before it reaches blockHandler.Gossip ->
|
||||
// engine.HandleIncomingVote, so the finality cert never assembles and the
|
||||
// chain wedges (blocks verify but never accept). Routed to blockHandler's
|
||||
// Gossip method, which demuxes the quorum envelope. MUST match the value the
|
||||
// consensus side decodes it as (TestToConsensusOp_TableAlignedWithRouter).
|
||||
return 11, true // Gossip
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package message
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/luxfi/consensus/core/router"
|
||||
"github.com/luxfi/ids"
|
||||
)
|
||||
|
||||
// TestToConsensusOp_GossipIsRouted is the regression guard for the finality
|
||||
// wedge: the α-of-K quorum vote/cert transport rides on app-gossip (GossipOp).
|
||||
// The chain router (node/chain_router.go) gates EVERY inbound message through
|
||||
// ToConsensusOp; an op that does not map is dropped as "unhandled message op"
|
||||
// BEFORE it can reach the engine. GossipOp was missing from the table, so every
|
||||
// broadcast vote was silently dropped at the router — blocks (PutOp) propagated
|
||||
// and verified, but no vote ever reached HandleIncomingVote, the cert never
|
||||
// assembled, and the chain wedged at "verified but never accepted".
|
||||
//
|
||||
// This asserts GossipOp maps to the consensus router's Gossip op so votes are
|
||||
// delivered to blockHandler.Gossip -> engine.HandleIncomingVote.
|
||||
func TestToConsensusOp_GossipIsRouted(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
got, ok := ToConsensusOp(GossipOp)
|
||||
require.True(ok, "GossipOp MUST map to a consensus router op — votes ride app-gossip; "+
|
||||
"an unmapped GossipOp is dropped by the chain router and finality wedges")
|
||||
require.Equal(byte(router.Gossip), got,
|
||||
"GossipOp must map to router.Gossip so blockHandler routes it to engine.HandleIncomingVote")
|
||||
}
|
||||
|
||||
// TestToConsensusOp_TableAlignedWithRouter pins the full message-op -> router-op
|
||||
// table. Every op the chain router forwards to a consensus handler must map to
|
||||
// the SAME byte the consensus side decodes it as (handler.Op == router.Op). A
|
||||
// drift here re-breaks routing for that op exactly as the missing GossipOp broke
|
||||
// vote delivery.
|
||||
func TestToConsensusOp_TableAlignedWithRouter(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
in Op
|
||||
want router.Op
|
||||
}{
|
||||
{"GetAcceptedFrontier", GetAcceptedFrontierOp, router.GetAcceptedFrontier},
|
||||
{"AcceptedFrontier", AcceptedFrontierOp, router.AcceptedFrontier},
|
||||
{"GetAccepted", GetAcceptedOp, router.GetAccepted},
|
||||
{"Accepted", AcceptedOp, router.Accepted},
|
||||
{"Get", GetOp, router.Get},
|
||||
{"Put", PutOp, router.Put},
|
||||
{"PushQuery", PushQueryOp, router.PushQuery},
|
||||
{"PullQuery", PullQueryOp, router.PullQuery},
|
||||
{"Qbit/Vote", QbitOp, router.Vote},
|
||||
{"GetAncestors/GetContext", GetAncestorsOp, router.GetContext},
|
||||
{"Ancestors/Context", AncestorsOp, router.Context},
|
||||
{"Gossip", GossipOp, router.Gossip},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
got, ok := ToConsensusOp(tc.in)
|
||||
require.True(ok, "%s must map to a consensus router op", tc.name)
|
||||
require.Equal(byte(tc.want), got, "%s maps to the wrong router op", tc.name)
|
||||
}
|
||||
}
|
||||
|
||||
// TestInboundGossip_DeliveredNotDropped reproduces the chain router's inbound
|
||||
// extraction for a quorum vote envelope (the exact steps node/chain_router.go
|
||||
// performs before dispatch) and asserts the vote SURVIVES routing: the op maps
|
||||
// to router.Gossip AND the envelope bytes are recovered intact as the container.
|
||||
// This is the seam the finality wedge lived in — a vote that the router dropped
|
||||
// here never reached the engine. The bytes that come out here are what
|
||||
// blockHandler.Gossip demuxes into engine.HandleIncomingVote.
|
||||
func TestInboundGossip_DeliveredNotDropped(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
chainID := ids.GenerateTestID()
|
||||
nodeID := ids.GenerateTestNodeID()
|
||||
// Stand-in for an encodeQuorumGossip envelope: magic + kind + blockID + vote.
|
||||
envelope := []byte("LXQ\x01" + "<signed-vote-payload>")
|
||||
|
||||
inbound := InboundGossip(chainID, envelope, nodeID)
|
||||
|
||||
// 1) The router maps the op (else "unhandled message op" -> dropped).
|
||||
consensusOp, ok := ToConsensusOp(inbound.Op())
|
||||
require.True(ok, "inbound vote gossip must route to a consensus op")
|
||||
require.Equal(byte(router.Gossip), consensusOp)
|
||||
|
||||
// 2) The router recovers the envelope as the handler container bytes.
|
||||
got := GetContainerBytes(inbound.Message())
|
||||
require.Equal(envelope, got, "the quorum envelope must survive router extraction intact")
|
||||
}
|
||||
Reference in New Issue
Block a user