Files
node/message/ops_test.go
T
zeekay f504b3424f 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.
2026-06-25 21:24:14 -07:00

96 lines
4.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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")
}