Files
node/message/ops.go
T
zeekay b72f9edb80 message: prove the op table is a bijection onto the router op space
Kills the bug CLASS behind the α-of-K finality wedge (v1.30.65), not just the
one Gossip instance. The chain router (node/chain_router.go) forwards an inbound
message only if message.ToConsensusOp maps it, then dispatches on the router op;
the node op table and the consensus router op enum are two halves of ONE routing
contract. When they diverged — router.Gossip existed but no node op mapped to it
— every broadcast vote was dropped as "unhandled message op" and finality
wedged, with nothing to catch it.

- ToConsensusOp now returns the router constants (byte(router.Get) ...) instead
  of 0..11 magic numbers, so values cannot drift and the table reads as the
  correspondence it is. Bumps consensus v1.25.29 -> v1.25.30 (adds router.NumOps,
  the single source of truth for the op count).

- message/ops_test.go TestToConsensusOp_TableAlignedWithRouter is now EXHAUSTIVE
  and BIDIRECTIONAL: it reconstructs the inverse mapping over the whole node op
  space and requires a bijection onto [0, router.NumOps). A router op with no
  node preimage (the original bug), a collision, a wrong target, or a stray
  mapping all go RED. Pinned to router.NumOps, so a future op added to one table
  but not the other fails the test instead of at runtime. Proven RED against the
  original bug (drop the GossipOp case -> router.Gossip loses its preimage ->
  fail); folds in the old Gossip-specific and hand-listed table checks.

Build rc=0 (GOWORK=off go build ./...); message tests green; finality fix
(GossipOp -> router.Gossip -> blockHandler.Gossip) unchanged and still covered.
2026-06-25 22:10:09 -07:00

413 lines
11 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 (
"encoding/binary"
"errors"
"fmt"
"github.com/luxfi/consensus/core/router"
"github.com/luxfi/math/set"
"github.com/luxfi/node/proto/p2p"
)
// Op is an opcode
type Op byte
// Types of messages that may be sent between nodes
// Note: If you add a new parseable Op below, you must add it to either
// [UnrequestedOps] or [FailedToResponseOps].
const (
// Handshake:
PingOp Op = iota
PongOp
HandshakeOp
GetPeerListOp
PeerListOp
// State sync:
GetStateSummaryFrontierOp
GetStateSummaryFrontierFailedOp
StateSummaryFrontierOp
GetAcceptedStateSummaryOp
GetAcceptedStateSummaryFailedOp
AcceptedStateSummaryOp
// Bootstrapping:
GetAcceptedFrontierOp
GetAcceptedFrontierFailedOp
AcceptedFrontierOp
GetAcceptedOp
GetAcceptedFailedOp
AcceptedOp
GetAncestorsOp
GetAncestorsFailedOp
AncestorsOp
// Consensus:
GetOp
GetFailedOp
PutOp
PushQueryOp
PullQueryOp
QueryFailedOp
QbitOp // Authenticated preference signal (formerly ChitsOp)
// Application:
RequestOp
ErrorOp
ResponseOp
GossipOp
// Internal:
ConnectedOp
DisconnectedOp
NotifyOp
GossipRequestOp
// BFT
BFTOp
)
var (
// UnrequestedOps are operations that are expected to be seen without having
// requested them. For example, a peer may receive a Get request for a block
// without having sent a message previously.
UnrequestedOps = set.Of(
GetAcceptedFrontierOp,
GetAcceptedOp,
GetAncestorsOp,
GetOp,
PushQueryOp,
PullQueryOp,
RequestOp,
GossipOp,
GetStateSummaryFrontierOp,
GetAcceptedStateSummaryOp,
BFTOp,
)
// FailedToResponseOps maps response failure messages to their successful
// counterparts.
FailedToResponseOps = map[Op]Op{
GetStateSummaryFrontierFailedOp: StateSummaryFrontierOp,
GetAcceptedStateSummaryFailedOp: AcceptedStateSummaryOp,
GetAcceptedFrontierFailedOp: AcceptedFrontierOp,
GetAcceptedFailedOp: AcceptedOp,
GetAncestorsFailedOp: AncestorsOp,
GetFailedOp: PutOp,
QueryFailedOp: QbitOp,
ErrorOp: ResponseOp,
}
errUnknownMessageType = errors.New("unknown message type")
)
func (op Op) String() string {
switch op {
// Handshake
case PingOp:
return "ping"
case PongOp:
return "pong"
case HandshakeOp:
return "handshake"
case GetPeerListOp:
return "get_peerlist"
case PeerListOp:
return "peerlist"
// State sync
case GetStateSummaryFrontierOp:
return "get_state_summary_frontier"
case GetStateSummaryFrontierFailedOp:
return "get_state_summary_frontier_failed"
case StateSummaryFrontierOp:
return "state_summary_frontier"
case GetAcceptedStateSummaryOp:
return "get_accepted_state_summary"
case GetAcceptedStateSummaryFailedOp:
return "get_accepted_state_summary_failed"
case AcceptedStateSummaryOp:
return "accepted_state_summary"
// Bootstrapping
case GetAcceptedFrontierOp:
return "get_accepted_frontier"
case GetAcceptedFrontierFailedOp:
return "get_accepted_frontier_failed"
case AcceptedFrontierOp:
return "accepted_frontier"
case GetAcceptedOp:
return "get_accepted"
case GetAcceptedFailedOp:
return "get_accepted_failed"
case AcceptedOp:
return "accepted"
case GetAncestorsOp:
return "get_ancestors"
case GetAncestorsFailedOp:
return "get_ancestors_failed"
case AncestorsOp:
return "ancestors"
// Consensus
case GetOp:
return "get"
case GetFailedOp:
return "get_failed"
case PutOp:
return "put"
case PushQueryOp:
return "push_query"
case PullQueryOp:
return "pull_query"
case QueryFailedOp:
return "query_failed"
case QbitOp:
return "qbit"
// Application
case RequestOp:
return "app_request"
case ErrorOp:
return "app_error"
case ResponseOp:
return "app_response"
case GossipOp:
return "app_gossip"
// Internal
case ConnectedOp:
return "connected"
case DisconnectedOp:
return "disconnected"
case NotifyOp:
return "notify"
case GossipRequestOp:
return "gossip_request"
// BFT
case BFTOp:
return "bft"
default:
return "unknown"
}
}
func Unwrap(m *p2p.Message) (fmt.Stringer, error) {
switch msg := m.GetMessage().(type) {
// Handshake:
case *p2p.Message_Ping:
return msg.Ping, nil
case *p2p.Message_Pong:
return msg.Pong, nil
case *p2p.Message_Handshake:
return msg.Handshake, nil
case *p2p.Message_GetPeerList:
return msg.GetPeerList, nil
case *p2p.Message_PeerList_:
return msg.PeerList_, nil
// State sync:
case *p2p.Message_GetStateSummaryFrontier:
return msg.GetStateSummaryFrontier, nil
case *p2p.Message_StateSummaryFrontier_:
return msg.StateSummaryFrontier_, nil
case *p2p.Message_GetAcceptedStateSummary:
return msg.GetAcceptedStateSummary, nil
case *p2p.Message_AcceptedStateSummary_:
return msg.AcceptedStateSummary_, nil
// Bootstrapping:
case *p2p.Message_GetAcceptedFrontier:
return msg.GetAcceptedFrontier, nil
case *p2p.Message_AcceptedFrontier_:
return msg.AcceptedFrontier_, nil
case *p2p.Message_GetAccepted:
return msg.GetAccepted, nil
case *p2p.Message_Accepted_:
return msg.Accepted_, nil
case *p2p.Message_GetAncestors:
return msg.GetAncestors, nil
case *p2p.Message_Ancestors_:
return msg.Ancestors_, nil
// Consensus:
case *p2p.Message_Get:
return msg.Get, nil
case *p2p.Message_Put:
return msg.Put, nil
case *p2p.Message_PushQuery:
return msg.PushQuery, nil
case *p2p.Message_PullQuery:
return msg.PullQuery, nil
case *p2p.Message_Chits:
return msg.Chits, nil
// Application:
case *p2p.Message_Request:
return msg.Request, nil
case *p2p.Message_Response:
return msg.Response, nil
case *p2p.Message_Error:
return msg.Error, nil
case *p2p.Message_Gossip:
return msg.Gossip, nil
// BFT
case *p2p.Message_BFT:
return extractBFT(msg), nil
default:
return nil, fmt.Errorf("%w: %T", errUnknownMessageType, msg)
}
}
// ToConsensusOp maps a wire message Op to the consensus router op the chain
// router (node/chain_router.go) dispatches on, returning false for ops that are
// not consensus-routed (the router drops those as "unhandled message op"). The
// returned values ARE the router constants, so they cannot drift in value; the
// mapping must also stay a bijection onto the router op space [0, router.NumOps)
// — proven by message/ops_test.go. A missing case silently drops that op: that
// is the α-of-K finality wedge, where GossipOp had no case and every vote
// vanished before reaching blockHandler.Gossip -> engine.HandleIncomingVote.
func ToConsensusOp(op Op) (byte, bool) {
switch op {
case GetAcceptedFrontierOp:
return byte(router.GetAcceptedFrontier), true
case AcceptedFrontierOp:
return byte(router.AcceptedFrontier), true
case GetAcceptedOp:
return byte(router.GetAccepted), true
case AcceptedOp:
return byte(router.Accepted), true
case GetOp:
return byte(router.Get), true
case PutOp:
return byte(router.Put), true
case PushQueryOp:
return byte(router.PushQuery), true
case PullQueryOp:
return byte(router.PullQuery), true
case QbitOp:
return byte(router.Vote), true // votes ride the Qbit wire op
case GetAncestorsOp:
return byte(router.GetContext), true // wire op is still GetAncestors
case AncestorsOp:
return byte(router.Context), true // wire op is still Ancestors
case GossipOp:
return byte(router.Gossip), true // α-of-K vote/cert transport
default:
return 0, false
}
}
// GetContainerBytes extracts the container/body bytes from various message types
func GetContainerBytes(msg fmt.Stringer) []byte {
switch m := msg.(type) {
case *p2p.Put:
return m.Container
case *p2p.PushQuery:
return m.Container
case *p2p.Ancestors:
// Encode all containers with length prefix so handlers can decode
// multiple blocks from a single byte slice.
if len(m.Containers) == 0 {
return nil
}
totalLen := 0
for _, container := range m.Containers {
totalLen += 4 + len(container)
}
encoded := make([]byte, 0, totalLen)
for _, container := range m.Containers {
var lenBuf [4]byte
binary.BigEndian.PutUint32(lenBuf[:], uint32(len(container)))
encoded = append(encoded, lenBuf[:]...)
encoded = append(encoded, container...)
}
return encoded
case *p2p.Gossip:
return m.AppBytes
case *p2p.Request:
return m.AppBytes
case *p2p.Response:
return m.AppBytes
// Request messages with container IDs for P-Chain sync
case *p2p.GetAccepted:
containerIDs := m.GetContainerIds()
if len(containerIDs) == 0 {
return nil
}
result := make([]byte, 0, len(containerIDs)*32)
for _, id := range containerIDs {
result = append(result, id...)
}
return result
case *p2p.Get:
return m.GetContainerId()
case *p2p.GetAncestors:
return m.GetContainerId()
case *p2p.PullQuery:
return m.GetContainerId()
case *p2p.Chits:
// For Qbit messages (formerly Chits), return the PreferredId (the block being voted for)
return m.GetPreferredId()
case *p2p.AcceptedFrontier:
return m.GetContainerId()
case *p2p.Accepted:
containerIDs := m.GetContainerIds()
if len(containerIDs) == 0 {
return nil
}
result := make([]byte, 0, len(containerIDs)*32)
for _, id := range containerIDs {
result = append(result, id...)
}
return result
default:
return nil
}
}
func ToOp(m *p2p.Message) (Op, error) {
switch msg := m.GetMessage().(type) {
case *p2p.Message_Ping:
return PingOp, nil
case *p2p.Message_Pong:
return PongOp, nil
case *p2p.Message_Handshake:
return HandshakeOp, nil
case *p2p.Message_GetPeerList:
return GetPeerListOp, nil
case *p2p.Message_PeerList_:
return PeerListOp, nil
case *p2p.Message_GetStateSummaryFrontier:
return GetStateSummaryFrontierOp, nil
case *p2p.Message_StateSummaryFrontier_:
return StateSummaryFrontierOp, nil
case *p2p.Message_GetAcceptedStateSummary:
return GetAcceptedStateSummaryOp, nil
case *p2p.Message_AcceptedStateSummary_:
return AcceptedStateSummaryOp, nil
case *p2p.Message_GetAcceptedFrontier:
return GetAcceptedFrontierOp, nil
case *p2p.Message_AcceptedFrontier_:
return AcceptedFrontierOp, nil
case *p2p.Message_GetAccepted:
return GetAcceptedOp, nil
case *p2p.Message_Accepted_:
return AcceptedOp, nil
case *p2p.Message_GetAncestors:
return GetAncestorsOp, nil
case *p2p.Message_Ancestors_:
return AncestorsOp, nil
case *p2p.Message_Get:
return GetOp, nil
case *p2p.Message_Put:
return PutOp, nil
case *p2p.Message_PushQuery:
return PushQueryOp, nil
case *p2p.Message_PullQuery:
return PullQueryOp, nil
case *p2p.Message_Chits:
return QbitOp, nil
case *p2p.Message_Request:
return RequestOp, nil
case *p2p.Message_Response:
return ResponseOp, nil
case *p2p.Message_Error:
return ErrorOp, nil
case *p2p.Message_Gossip:
return GossipOp, nil
case *p2p.Message_BFT:
return BFTOp, nil
default:
return 0, fmt.Errorf("%w: %T", errUnknownMessageType, msg)
}
}