Files
zeekayandHanzo Dev df0b9934b3 Pass the vote sender to the engine; state the BLS signature length
Local, unreleased; go.mod still pins luxfi/consensus v1.36.94.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-24 17:29:55 -07:00

564 lines
25 KiB
Go
Raw Permalink 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-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// quorum.go — the node-layer wiring that makes the consensus engine's α-of-K
// quorum-cert finality LIVE. The consensus engine (luxfi/consensus
// engine/chain) defines the quorum RULE and the vote/cert topology interfaces;
// THIS file supplies the concrete node implementations the engine needs:
//
// - blsVoteVerifier — verifies a validator's BLS signature over the canonical
// vote message against the chain's validator set (the engine is scheme-
// agnostic; the node injects BLS).
// - blsVoteSigner — signs THIS node's accept votes with its staking BLS key
// so its signature can be collected into a cert.
// - validatorStakeSource — supplies per-validator stake so finality is a
// ⅔-by-STAKE supermajority, not a raw voter count.
// - networkGossiper.BroadcastVote / GossipCert — the QuorumGossiper transport:
// a follower broadcasts its signed vote to ALL validators and any node that
// collects α distinct signed votes gossips the assembled cert, so finality
// never hinges on one node's inbound Chits (liveness; no proposer-freeze).
//
// Inbound votes/certs arrive as app-gossip and are demuxed in blockHandler.Gossip
// (see manager.go) into engine.HandleIncomingVote / HandleIncomingCert.
package chains
import (
"context"
"errors"
"sync/atomic"
"time"
consensusconfig "github.com/luxfi/consensus/config"
consensuschain "github.com/luxfi/consensus/engine/chain"
"github.com/luxfi/constants"
"github.com/luxfi/crypto/bls"
"github.com/luxfi/ids"
"github.com/luxfi/log"
validators "github.com/luxfi/validators"
)
// ConsensusOverrides carries the consensus parameters an operator set EXPLICITLY
// on the command line. A nil field means "not set", so the network default from
// selectConsensusParams stands.
//
// It exists because parameter selection cannot be a pure function of
// (sybilProtection, networkID) alone: a small value network falls to the default
// branch and gets DefaultParams — K=20, α=14, BetaVirtuous=14 — so an
// uncontested block
// needed FOURTEEN consecutive clean polls and the chain froze at height 3248
// while --consensus-sample-size=5 --consensus-quorum-size=4
// --consensus-commit-threshold=2 sat in the node config reaching nothing. That is
// the same arithmetic isLocalDevNetwork already carves devnet/localnet out for
// ("α=14 affirmative votes are unreachable with 3-4 validators"); the carve-out
// keys on networkID, so it cannot help a sovereign L1 whose networkID is its
// chainID.
//
// Separating the two concerns keeps the policy honest: the networkID switch is
// the DEFAULT (mainnet and testnet keep their tuned sets untouched), and an
// explicit operator value is the OVERRIDE. Nothing changes for a node that
// passes no consensus flags.
type ConsensusOverrides struct {
K *int
AlphaPreference *int
AlphaConfidence *int
Beta *int
ConcurrentRepolls *int
OptimalProcessing *int
MaxOutstandingItems *int
MaxItemProcessingTime *time.Duration
ConvergenceSettleWindow *time.Duration
}
// ApplyTo returns p with every explicitly-set override applied. A nil receiver
// or an all-nil struct returns p unchanged.
func (o *ConsensusOverrides) ApplyTo(p consensusconfig.Parameters) consensusconfig.Parameters {
if o == nil {
return p
}
if o.K != nil {
p.K = *o.K
}
if o.AlphaPreference != nil {
p.AlphaPreference = *o.AlphaPreference
}
if o.AlphaConfidence != nil {
p.AlphaConfidence = *o.AlphaConfidence
}
if o.Beta != nil {
// Beta alone is not enough. A block with no conflict takes the VIRTUOUS
// path, so acceptance is gated by BetaVirtuous: lowering Beta while
// BetaVirtuous keeps its default leaves uncontested blocks needing the
// default's run of clean polls. BetaRogue is raised to match when it
// would otherwise sit below Beta, never lowered.
b := *o.Beta
p.Beta = uint32(b)
p.BetaVirtuous = b
if p.BetaRogue < b {
p.BetaRogue = b
}
}
if o.ConcurrentRepolls != nil {
p.ConcurrentRepolls = *o.ConcurrentRepolls
}
if o.OptimalProcessing != nil {
p.OptimalProcessing = *o.OptimalProcessing
}
if o.MaxOutstandingItems != nil {
p.MaxOutstandingItems = *o.MaxOutstandingItems
}
if o.MaxItemProcessingTime != nil {
p.MaxItemProcessingTime = *o.MaxItemProcessingTime
}
if o.ConvergenceSettleWindow != nil {
p.ConvergenceSettleWindow = *o.ConvergenceSettleWindow
}
return p
}
// isLocalDevNetwork reports whether networkID is an explicitly-local developer
// network: devnet (3) or localnet (1337). These are EXACT IDs (per luxfi/constants
// convention), NOT a range — a custom value L1 with a high networkID (e.g. an
// L1 whose chainID == networkID) is a VALUE network and must NOT match here.
// Local dev networks are the only IDs that run the minimal-BFT committee
// (LocalBFTParams, K=4) instead of the large-committee Default (K=20), because a
// handful of localhost validators cannot reach an α=14-of-K=20 quorum.
func isLocalDevNetwork(networkID uint32) bool {
return networkID == constants.DevnetID || networkID == constants.LocalID
}
// selectConsensusParams picks the consensus parameters for a chain.
//
// - sybilProtection == false (--dev / single-node): K=1, the sole validator's
// accept is the 1-of-1 quorum (no peer signatures).
// - sybilProtection == true, VALUE network: a large BYZANTINE-fault-tolerant
// param set — NEVER LocalParams() (K=3/α=2, f=0, which a single Byzantine
// validator forks). Mainnet→K=21, Testnet→K=11, every
// other value net→Default K=20.
// - sybilProtection == true, LOCAL DEV network (devnet 3 / localnet 1337):
// LocalBFTParams() (K=4/α=3, f=1) — the MINIMAL real-BFT committee. Default
// K=20 is unsatisfiable on a few localhost validators: α=14 affirmative votes
// are unreachable with 3-4 validators, so no block ever finalizes and the
// P-Chain freezes at height 0 (no C-Chain is ever created). K=4 makes quorum
// reachable (3 of 4) while staying genuinely BFT — it still clears
// ValidateForValueNetwork (K≥4, f≥1) and the multi-node-is-BFT rule, so a
// value network's safety is untouched. (A local devnet should run
// ≥4 validators to realise f=1; with 3 it degrades to near-unanimous f=0,
// which is safe though not live under a fault.)
//
// All branches satisfy the 2α−K ≥ f+1 overlap bound; the manager call site also
// asserts ValidateForValueNetwork as a fail-closed backstop (K=4 passes it).
func selectConsensusParams(sybilProtection bool, networkID uint32) consensusconfig.Parameters {
if !sybilProtection {
return consensusconfig.SingleValidatorParams()
}
switch networkID {
case constants.MainnetID:
return consensusconfig.MainnetParams()
case constants.TestnetID:
return consensusconfig.TestnetParams()
default:
if isLocalDevNetwork(networkID) {
return consensusconfig.LocalBFTParams()
}
return consensusconfig.DefaultParams()
}
}
// shouldWrapInProposerVM decides whether a linear chain.ChainVM is wrapped in
// proposervm to enforce single-proposer-per-height block production (the
// proposer window). It is the SINGLE policy gate (the manager calls it once);
// keeping it a pure function makes the policy unit-testable without standing up
// a whole chain. All three conditions must hold:
//
// - k > 1: a multi-validator quorum. K==1 (single-node --dev) has exactly one
// proposer already, so there is no equivocation to prevent and no schedule
// to compute (proposervm would only add a wrapper with no safety value).
// - chainID is NOT the P-Chain: the P-Chain publishes its OWN height-indexed
// validators.State DURING its createChain, AFTER the chainRuntime snapshot
// proposervm's windower reads — so its windower would see an empty set and
// fall back to anyone-can-propose with a P-chain-height-0 stamp. The P-Chain
// keeps the existing newPChainHeightVM path (which DOES get the live state).
// - inner is NOT DAG-native (no Linearize): a linearized DAG VM (X-Chain) is
// driven by a push-notification bridge, and a push bridge does not compose
// with proposervm's pull/window model. It keeps the existing path.
//
// The C-Chain and sovereign-L1 EVM chains satisfy all three (multi-validator,
// not the P-Chain, not DAG) — exactly the chains where two proposers at one
// height is possible, which is what the proposer window rules out.
func shouldWrapInProposerVM(k int, chainID ids.ID, innerIsDAGNative bool) bool {
return k > 1 && chainID != constants.PlatformChainID && !innerIsDAGNative
}
// --- BLS vote verifier -------------------------------------------------------
// blsVoteVerifier verifies a validator's BLS signature over the canonical vote
// message. The validator's BLS public key is resolved FROM THE HEIGHT-INDEXED
// validators.State AT THE BLOCK'S P-CHAIN EPOCH HEIGHT — the SAME
// height-pinned source the set-root and the ⅔-by-stake tally read from
// (validatorSetAtHeight). It is NOT resolved from the CURRENT validator map.
//
// Why the epoch, not the current map: during an async validator-set change a
// validator can be present in the set@H (it legitimately signed the block being
// voted on at height H) yet ALREADY GONE from the current map. Resolving its
// pubkey from the current map drops its valid vote, and if that validator holds
// >⅓ of the stake-at-H the stable set falls below ⅔ and block H NEVER finalizes
// (this is not self-healing for that block — the skew is permanent for H). Pinning
// pubkey resolution to set@H — alongside membership, set-root, and stake — makes
// the cert internally consistent at exactly one epoch.
//
// An unknown validator at the epoch, a validator with no BLS key, or a bad
// signature all yield false (never an error/panic) — a cert with such a voter is
// simply invalid, the fail-closed contract the engine requires. A nil state (the
// no-op on a non-quorum node) yields false for every voter; a K>1 chain is
// guarded against ever reaching here with a nil/no-op state (manager.go).
type blsVoteVerifier struct {
state validators.State
networkID ids.ID
// log and the counters below exist because a verification failure here is the
// quietest way a chain can die: every node-side counter stays healthy, votes
// are broadcast and received in the thousands, and the cert simply never
// reaches alpha. Sampled so a fully-failing fleet cannot flood the log.
log log.Logger
nOK atomic.Uint64
nNoVdr atomic.Uint64
nNoPK atomic.Uint64
nBadPK atomic.Uint64
nBadSig atomic.Uint64
nVerFail atomic.Uint64
}
// sample logs the first few of each outcome and then every 500th, so a
// systematic failure is unmissable without being unbounded.
func (v *blsVoteVerifier) sample(c *atomic.Uint64, reason string, nodeID ids.NodeID, epochHeight uint64) {
n := c.Add(1)
if v.log == nil || (n > 3 && n%500 != 0) {
return
}
v.log.Warn("vote signature REJECTED — this vote cannot count toward the quorum",
log.String("reason", reason),
log.Stringer("voter", nodeID),
log.Uint64("epochHeight", epochHeight),
log.Uint64("count", n))
}
// blsPublicKeyFromValidatorBytes parses a validator's registered BLS public key
// in EITHER supported G1 encoding.
//
// This is not defensive coding. A validator set may carry 96-byte UNCOMPRESSED
// G1 keys, and a verifier that calls only PublicKeyFromCompressedBytes (48-byte
// compressed, bls.PublicKeyLen) fails to decompress every one of them. VerifyVote
// then returns false before reaching bls.Verify, so no vote counts toward the
// quorum and alpha is unreachable by construction — while every other counter
// reads healthy, because blocks still build, gossip, and answer chits.
//
// The stored keys live in chain state and cannot be re-encoded without a genesis
// change, so the verifier has to read what is actually there. Safety is unchanged:
// an unparseable or wrong key still yields a key whose bls.Verify FAILS, so this
// widens what can be PARSED, never what is ACCEPTED.
func blsPublicKeyFromValidatorBytes(b []byte) (*bls.PublicKey, error) {
switch len(b) {
case bls.PublicKeyLen: // 48 — compressed G1
return bls.PublicKeyFromCompressedBytes(b)
case 2 * bls.PublicKeyLen: // 96 — uncompressed G1
pk := bls.PublicKeyFromValidUncompressedBytes(b)
if pk == nil {
return nil, errBadValidatorPublicKey
}
return pk, nil
default:
return nil, errBadValidatorPublicKey
}
}
var errBadValidatorPublicKey = errors.New("validator BLS public key is neither 48-byte compressed nor 96-byte uncompressed G1")
func newBLSVoteVerifier(state validators.State, networkID ids.ID) *blsVoteVerifier {
return &blsVoteVerifier{state: state, networkID: networkID}
}
// VerifyVote implements consensuschain.VoteVerifier. epochHeight is the block's
// P-chain height; the voter's pubkey is read from the set IN FORCE AT that height.
// SignatureLen is the length of every vote signature this verifier accepts: a
// compressed BLS12-381 G2 point. The engine parks a vote for a block it lacks only at
// this length.
func (v *blsVoteVerifier) SignatureLen() int { return bls.SignatureLen }
func (v *blsVoteVerifier) VerifyVote(nodeID ids.NodeID, message []byte, sig []byte, epochHeight uint64) bool {
out, ok := validatorSetAtHeight(v.state, v.networkID, epochHeight)[nodeID]
if !ok || out == nil {
v.sample(&v.nNoVdr, "voter not in the validator set at this epoch height", nodeID, epochHeight)
return false
}
if len(out.PublicKey) == 0 {
v.sample(&v.nNoPK, "voter has no BLS public key at this epoch height", nodeID, epochHeight)
return false
}
pk, err := blsPublicKeyFromValidatorBytes(out.PublicKey)
if err != nil || pk == nil {
v.sample(&v.nBadPK, "voter public key failed to decompress", nodeID, epochHeight)
return false
}
signature, err := bls.SignatureFromBytes(sig)
if err != nil || signature == nil {
v.sample(&v.nBadSig, "signature bytes failed to parse", nodeID, epochHeight)
return false
}
if !bls.Verify(pk, signature, message) {
// The signature is well-formed and the key is right, so the SIGNED MESSAGE
// differs: signer and verifier disagree on the block POSITION.
// CanonicalVoteMessage binds (ChainID, Height, Round, BlockID, ParentID) —
// a Round or ParentID mismatch fails here while every counter stays green.
v.sample(&v.nVerFail, "BLS verify failed — signer/verifier disagree on the vote message (position mismatch)", nodeID, epochHeight)
return false
}
if n := v.nOK.Add(1); v.log != nil && (n <= 3 || n%1000 == 0) {
v.log.Info("vote signature verified",
log.Stringer("voter", nodeID), log.Uint64("epochHeight", epochHeight), log.Uint64("count", n))
}
return true
}
var _ consensuschain.VoteVerifier = (*blsVoteVerifier)(nil)
// --- BLS vote signer ---------------------------------------------------------
// blsVoteSigner signs this node's accept votes with its staking BLS key.
type blsVoteSigner struct {
signer bls.Signer
}
func newBLSVoteSigner(signer bls.Signer) *blsVoteSigner {
if signer == nil {
return nil
}
return &blsVoteSigner{signer: signer}
}
// SignVote implements consensuschain.VoteSigner.
func (s *blsVoteSigner) SignVote(message []byte) ([]byte, error) {
sig, err := s.signer.Sign(message)
if err != nil {
return nil, err
}
return bls.SignatureToBytes(sig), nil
}
var _ consensuschain.VoteSigner = (*blsVoteSigner)(nil)
// --- height-pinned epoch read -------------------------------------
// validatorSetAtHeight reads the validator set IN FORCE AT a value-chain height
// from the height-indexed validators.State. This is the SINGLE source of epoch
// truth shared by the stake source and the set-root source: both membership and
// weights are read at the SAME height H so a cert's signed set-root and its
// ⅔-by-stake tally are measured against the identical set.
//
// Determinism across nodes is the whole point. validators.State.GetValidatorSet
// returns the set the network already agreed on at height H (P-chain / L1-staking
// consensus), so every honest node computes the same set — and therefore the
// same set-root and the same tally — for a given H, INDEPENDENT of async
// current-map skew during a validator-set change. (The previous Manager.GetMap()
// read hashed the CURRENT map, which diverges between the signer and the
// assembler across that skew window → mismatched canonical messages → dropped
// votes → finality stall at every staking change.)
//
// A nil state, a lookup error, or an empty set yields a nil map, which the
// callers fold into their fail-soft answers (0 weight / Empty root). An error is
// SYMMETRIC across nodes (a committed height H reads the same on every node, or
// fails the same way), so the degraded answer is uniform — it never makes one
// node's view disagree with another's, which is the property that matters here.
func validatorSetAtHeight(state validators.State, networkID ids.ID, height uint64) map[ids.NodeID]*validators.GetValidatorOutput {
if state == nil {
return nil
}
set, err := state.GetValidatorSet(context.Background(), height, networkID)
if err != nil || len(set) == 0 {
return nil
}
return set
}
// --- stake source, height-pinned ---------------------------------------------
// validatorStakeSource supplies validator voting weights so the engine can
// require a ⅔-by-stake supermajority for finality. Weights are read
// from the HEIGHT-INDEXED validators.State at the cert-position height, the same
// height the set-root commits to. Reading the tally at the same epoch
// as the signed membership means a validator whose vote is in the cert (its
// signature verifies against the height-H set-root) also contributes its height-H
// weight to the tally — eliminating the second skew (a current-map weight read
// could drop a legitimately-signed quorum when membership changed between sign
// and tally).
type validatorStakeSource struct {
state validators.State
networkID ids.ID
}
func newValidatorStakeSource(state validators.State, networkID ids.ID) *validatorStakeSource {
return &validatorStakeSource{state: state, networkID: networkID}
}
// Weight implements consensuschain.StakeSource. Returns the validator's stake in
// the set IN FORCE AT height — deterministic across nodes for a given height. An
// unknown validator (or a fail-soft empty read) yields 0, which cannot inflate
// the numerator.
func (s *validatorStakeSource) Weight(nodeID ids.NodeID, height uint64) uint64 {
out, ok := validatorSetAtHeight(s.state, s.networkID, height)[nodeID]
if !ok || out == nil {
return 0
}
return out.Light
}
// SignerStake implements consensuschain.StakeSource. The stake of the set IN
// FORCE AT height that can actually SIGN — a member holding no key is left out,
// because its stake can never reach the numerator and counting it in the
// denominator only raises a bar no quorum could then clear.
func (s *validatorStakeSource) SignerStake(height uint64) uint64 {
var total uint64
for _, out := range validatorSetAtHeight(s.state, s.networkID, height) {
if out != nil && len(out.PublicKey) > 0 {
total += out.Light
}
}
return total
}
// CarriedStake implements consensuschain.StakeSource. The stake the chain
// CARRIES at height: every member, keyed or not. No floor is read against it,
// and none may be — it exists so the two numbers can be compared. A chain whose
// signable stake has fallen to a sliver of what it carries still certifies at
// two thirds of the sliver, correctly and invisibly; reporting both is what
// lets that be seen before it is an incident.
func (s *validatorStakeSource) CarriedStake(height uint64) uint64 {
var total uint64
for _, out := range validatorSetAtHeight(s.state, s.networkID, height) {
if out != nil {
total += out.Light
}
}
return total
}
// SignerCount implements consensuschain.StakeSource. The number of DISTINCT
// validators in the set IN FORCE AT height that can sign — the round-scoped view-change's BFT
// committee size (it sizes its POL/precommit quorum to bftAlpha over this count,
// NOT the oversized sample K). Read from the SAME height-indexed set as
// Weight/SignerStake so every node computes the identical committee and the
// count-quorum matches the ⅔-by-stake set exactly.
func (s *validatorStakeSource) SignerCount(height uint64) int {
n := 0
for _, out := range validatorSetAtHeight(s.state, s.networkID, height) {
if out != nil && len(out.PublicKey) > 0 {
n++
}
}
return n
}
var _ consensuschain.StakeSource = (*validatorStakeSource)(nil)
// --- validator-set-root source (MEDIUM: epoch binding) -----------------------
// validatorSetRootSource computes the deterministic commitment to the validator
// set IN FORCE AT a value-chain height — the value the engine stamps into every
// vote's VotePosition.ValidatorSetRoot so a cert is pinned to the exact set it
// was certified under. It is the node side of the MEDIUM fix: it turns the
// "⅔-by-stake measured at the cert-position epoch" property into an ENFORCED
// invariant (a cross-epoch cert fails verification because every signature was
// over this root).
//
// HEIGHT-PINNED. The root is read from the HEIGHT-INDEXED
// validators.State at the value-chain block height, NOT from the Manager's
// CURRENT map. At a given height H, GetValidatorSet returns the set the network
// already agreed on, so every honest node — signer and assembler alike — computes
// the IDENTICAL root for H, independent of async current-map skew during a
// validator-set change. Reading the current map (the prior bug) let the signer
// and the assembler hold different maps across that skew window → different roots
// → the canonical signed message differed → signatures failed verification →
// votes dropped → finality stalled at every staking change.
//
// The commitment is a SHA-256 over the set serialized in a canonical order
// (validators sorted by NodeID, each as nodeID || light || len(pubkey) ||
// pubkey) — see validators.SetRoot. Sorting by NodeID + length-prefixing the
// pubkey makes the encoding canonical and unambiguous; the byte layout is
// UNCHANGED from the prior implementation, so the wire format and the engine's
// epoch-binding contract are preserved (only the SOURCE of the set changed from
// the current map to the height-indexed set).
type validatorSetRootSource struct {
state validators.State
networkID ids.ID
}
func newValidatorSetRootSource(state validators.State, networkID ids.ID) *validatorSetRootSource {
return &validatorSetRootSource{state: state, networkID: networkID}
}
// ValidatorSetRoot implements consensuschain.ValidatorSetRootSource. Returns the
// commitment to the weighted set IN FORCE AT height (deterministic across nodes).
// Returns ids.Empty when the state is absent or the set is empty (the explicit
// "unbound" answer, consistent with the engine default); a height-read error is
// symmetric across nodes, so the Empty fallback is uniform and never creates a
// cross-node root disagreement.
func (s *validatorSetRootSource) ValidatorSetRoot(height uint64) ids.ID {
return validators.SetRoot(validatorSetAtHeight(s.state, s.networkID, height))
}
var _ consensuschain.ValidatorSetRootSource = (*validatorSetRootSource)(nil)
// --- app-gossip envelope for votes/certs -------------------------------------
// The QuorumGossiper transport rides on app-gossip. A single framed envelope
// carries either a signed vote or an assembled cert. The receiver (blockHandler
// .Gossip) demuxes on the magic + kind and routes to the engine. A payload
// without the magic is a plain block gossip (legacy Put path), so the demux is
// backward-compatible.
//
// Layout (big-endian):
//
// magic:4 ("LXQ\x01") kind:1 blockID:32 payload:...
//
// kind 1 = signed vote (payload = engine encodeSignedVote: nodeID+sig)
// kind 2 = finality cert (payload = engine cert MarshalBinary)
// (round-scoped view-change prevotes are engine-INTERNAL since consensus v1.36 —
//
// the node no longer frames or routes a prevote kind)
var quorumGossipMagic = [4]byte{'L', 'X', 'Q', 0x01}
const (
quorumKindVote byte = 1
quorumKindCert byte = 2
// kind 3 (prevote) was DELETED with the v1.36 view-change rip-out (174af3c31); Nova sampling
// decides and the ⅔ Quasar attestation rides quorumKindVote. Do not reuse 3 — keep the braid dead.
)
// ErrNotQuorumGossip signals a payload is not a quorum envelope (so the caller
// falls through to the block-gossip path).
var ErrNotQuorumGossip = errors.New("chains: not a quorum gossip envelope")
// encodeQuorumGossip frames a vote/cert payload for app-gossip.
func encodeQuorumGossip(kind byte, blockID ids.ID, payload []byte) []byte {
buf := make([]byte, 0, 4+1+32+len(payload))
buf = append(buf, quorumGossipMagic[:]...)
buf = append(buf, kind)
buf = append(buf, blockID[:]...)
buf = append(buf, payload...)
return buf
}
// decodeQuorumGossip parses an envelope. Returns ErrNotQuorumGossip if the magic
// is absent (a normal block gossip) — fail-soft so the legacy path is preserved.
func decodeQuorumGossip(data []byte) (kind byte, blockID ids.ID, payload []byte, err error) {
if len(data) < 4+1+32 || [4]byte{data[0], data[1], data[2], data[3]} != quorumGossipMagic {
return 0, ids.Empty, nil, ErrNotQuorumGossip
}
kind = data[4]
copy(blockID[:], data[5:5+32])
payload = data[5+32:]
if kind != quorumKindVote && kind != quorumKindCert {
return 0, ids.Empty, nil, ErrNotQuorumGossip
}
return kind, blockID, payload, nil
}