chains: bridge the consensus EXPORT (Quasar) frontier into the VM; drop dead view-change braid (v1.36)

Wire the two-tier consensus export boundary through the node so the EVM `finalized`/`safe` tags and
the warp export gate track the ⅔-stake Quasar tip, never the reorgable Nova/accept tip:

- Set NetworkConfig.QuasarObserver to push each EXPORT (Quasar) frontier advance into the raw inner
  VM (SetLastQuasarFinalized) — the eth/warp backends live there, not on the proposervm wrapper.
  Interface-gated: only a VM exposing the export sink (the C-Chain EVM) participates.
- On boot, re-seed the consensus export frontier from the VM's DURABLE Quasar height
  (SyncQuasarFrontier) so GetQuasarTip/QuasarHeight do not regress on restart.

Also remove the node's dead references to the v1.36-deleted Tendermint braid (the consensus engine
dropped it in 174af3c31), which no longer compile against the v1.36 engine:
- the LUX_CONSENSUS_VIEW_CHANGE opt-in (params.ViewChange is gone — Nova is the sole decider),
- the quorumKindPrevote gossip kind + BroadcastPrevote + HandleIncomingPrevote routing (no prevotes;
  the ⅔ Quasar attestation rides the ordinary accept-vote gossip). Keep the braid dead.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
This commit is contained in:
zeekay
2026-07-10 04:59:06 -07:00
co-authored by Hanzo Dev
parent a93e5cdd9d
commit 9c2468671e
2 changed files with 40 additions and 45 deletions
+35 -41
View File
@@ -1199,25 +1199,10 @@ func (m *manager) buildChain(chainParams ChainParameters, sb nets.Net) (*chainIn
return nil, fmt.Errorf("refusing to start multi-node chain %s with non-BFT consensus params: %w", chainParams.ID, err)
}
}
// Round-scoped view-change (restores liveness under competing siblings + a zero-margin
// quorum — the 415→416 freeze). OPT-IN per deployment via LUX_CONSENSUS_VIEW_CHANGE=true
// so devnet/testnet can enable it without a mainnet default (mainnet is owner-gated). Only
// meaningful on a multi-validator (K>1) chain; K==1 has no competing proposers. The engine
// itself fail-secure HALTS the view-change if the committee fails the 2α−n>f bound, so
// enabling it can never weaken safety — at worst it halts (never forks).
if consensusParams.K > 1 && strings.EqualFold(os.Getenv("LUX_CONSENSUS_VIEW_CHANGE"), "true") {
consensusParams.ViewChange = true
// NOTE: presetK/presetAlpha are the sample preset (MainnetParams K=21/α=15),
// NOT the finality committee. The α-of-K cert and the view-change POL/precommit are
// sized to the LIVE validator set at runtime via effectiveCommittee/bftCommittee
// (e.g. 5 validators → K=5/α=4); the engine logs the effective (K,α) whenever it
// re-clamps ("committee re-clamped … newK/newAlpha"). Do not read presetK as the quorum.
m.Log.Info("round-scoped view-change ENABLED for chain",
log.Stringer("chainID", chainParams.ID),
log.Int("presetK", consensusParams.K),
log.Int("presetAlpha", consensusParams.AlphaConfidence),
log.String("note", "finality committee sized to the live validator set at runtime (see engine committee-clamp log for effective K/alpha)"))
}
// v1.36 "Nova": the round-scoped VIEW-CHANGE (prevote/POL/lock) was DELETED from the
// consensus engine (174af3c31). Nova metastable sampling is the sole decider and the ⅔
// Quasar attestation trails it — there is no view-change to opt into, so the former
// LUX_CONSENSUS_VIEW_CHANGE env gate is gone. Keep the braid dead.
_, innerIsDAGNative := vmTyped.(interface {
Linearize(context.Context, ids.ID, chan<- vm.Message) error
})
@@ -1535,7 +1520,34 @@ func (m *manager) buildChain(chainParams ChainParameters, sb nets.Net) (*chainIn
log.Stringer("networkID", networkID))
}
}
consensusEngine := consensuschain.NewRuntime(netCfg)
// EXPORT-FRONTIER BRIDGE (two-tier consensus, v1.36). VM.Accept now advances the
// local NOVA (bare-majority) accept tip, which is reorgable and MUST NOT be exported.
// Push each EXPORT (Quasar, ⅔-by-stake) frontier advance into the VM so the EVM
// `finalized`/`safe` block tags and the warp cross-chain export gate resolve to the
// Quasar tip, NEVER the Nova tip (the "semantic collapse" the split exists to prevent).
// Interface-gated: only a VM that exposes the export sink participates (the C-Chain
// EVM); other VMs are Nova-only with no export surface. Push into the RAW inner VM
// (vmTyped) — the eth backend / warp backend live there, not on the proposervm wrapper.
if qvm, ok := vmTyped.(interface{ SetLastQuasarFinalized(uint64) }); ok {
netCfg.QuasarObserver = func(_ ids.ID, height uint64) {
qvm.SetLastQuasarFinalized(height)
}
m.Log.Info("wired EXPORT-frontier (quasar) bridge into the VM (finalized/safe + warp gate track ⅔-stake finality)",
log.Stringer("chainID", chainParams.ID))
}
consensusEngine := consensuschain.NewRuntime(netCfg)
// Re-seed the consensus EXPORT frontier from the VM's DURABLE Quasar height so
// GetQuasarTip / QuasarHeight do not regress on restart (the in-memory frontier resets
// to (Empty,0) until a fresh ⅔-stake cert re-forms; the VM persisted the export height).
// Advance-only; the observer above refines it as new certs land this session.
if qvm, ok := vmTyped.(interface{ LastQuasarHeight() uint64 }); ok {
if h := qvm.LastQuasarHeight(); h > 0 {
consensusEngine.SyncQuasarFrontier(ids.Empty, h)
m.Log.Info("re-seeded consensus export (quasar) frontier from the VM's durable height on boot",
log.Stringer("chainID", chainParams.ID), log.Uint64("quasarHeight", h))
}
}
// Start the consensus engine with a LIFETIME context (not a timeout):
// engine.Start parents all four long-running loops (poll, vote, pipeline,
@@ -3904,10 +3916,6 @@ func (b *blockHandler) Gossip(ctx context.Context, nodeID ids.NodeID, msg []byte
b.engine.HandleIncomingVote(blockID, payload)
case quorumKindCert:
b.engine.HandleIncomingCert(payload)
case quorumKindPrevote:
// Round-scoped view-change prevote: the engine decodes+verifies
// (height,round,canonical,sig) from the payload and tallies it toward a POL.
b.engine.HandleIncomingPrevote(payload)
}
return nil
}
@@ -4222,23 +4230,9 @@ func (g *networkGossiper) BroadcastVote(chainID ids.ID, networkID ids.ID, blockI
return g.net.Gossip(msg, nil, g.networkID, -1, 0, 0).Len()
}
// BroadcastPrevote sends this node's signed ROUND-SCOPED view-change prevote (the
// non-binding preference signal) for `canonical` at (height, round) to ALL validators,
// framed in a quorum envelope (kind 3) and decoded by blockHandler.Gossip into
// engine.HandleIncomingPrevote. Prevotes never finalize anything — they drive the POL +
// the lock/unlock rule that lets a competing-sibling split RE-CONVERGE (liveness under a
// down proposer + zero-margin quorum). Only emitted when the chain runs params.ViewChange.
func (g *networkGossiper) BroadcastPrevote(chainID ids.ID, networkID ids.ID, height uint64, round uint32, canonical ids.ID, voteBytes []byte) int {
if g.net == nil || g.msgCreator == nil {
return 0
}
envelope := encodeQuorumGossip(quorumKindPrevote, canonical, voteBytes)
msg, err := g.msgCreator.Gossip(chainID, envelope)
if err != nil {
return 0
}
return g.net.Gossip(msg, nil, g.networkID, -1, 0, 0).Len()
}
// v1.36 "Nova": BroadcastPrevote was DELETED — the round-scoped view-change (prevote/POL/lock)
// it fed no longer exists in the consensus engine (174af3c31). Nova sampling decides; the ⅔
// Quasar attestation (a plain accept-vote, gossiped via BroadcastVote) trails it. Keep the braid dead.
// GossipCert broadcasts an assembled α-of-K finality cert to ALL validators so
// followers finalize blockID on a verifiable proof (HandleIncomingCert), not a
+5 -4
View File
@@ -382,9 +382,10 @@ func hashValidatorSet(set map[ids.NodeID]*validators.GetValidatorOutput) ids.ID
var quorumGossipMagic = [4]byte{'L', 'X', 'Q', 0x01}
const (
quorumKindVote byte = 1
quorumKindCert byte = 2
quorumKindPrevote byte = 3
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
@@ -410,7 +411,7 @@ func decodeQuorumGossip(data []byte) (kind byte, blockID ids.ID, payload []byte,
kind = data[4]
copy(blockID[:], data[5:5+32])
payload = data[5+32:]
if kind != quorumKindVote && kind != quorumKindCert && kind != quorumKindPrevote {
if kind != quorumKindVote && kind != quorumKindCert {
return 0, ids.Empty, nil, ErrNotQuorumGossip
}
return kind, blockID, payload, nil