mirror of
https://github.com/luxfi/vm.git
synced 2026-07-27 03:39:13 +00:00
feat(rpc): close the ZAP caps handshake — plugin advertises CapQuasarExport + Quasar handlers; delete dead parallel scheme
The generic ZAP VM server never set InitializeResponse.Capabilities and had no MsgSetQuasarFinalized/ MsgQuasarHeight handlers, so CapQuasarExport was dead-on-arrival (every plugin reported Nova-only and the node's Quasar-export bridge never wired for plugin VMs). newZAPVMServer now probes the wrapped VM once for the quasarExporter interface (SetLastQuasarFinalized/LastQuasarHeight — the real C-Chain EVM VM's method set), handleInitialize sets Capabilities accordingly, and the two Quasar handlers forward to the VM (refuse loudly on a non-capable VM, no nil-panic). Deleted the parallel, never-wired capabilities.go scheme (CurrentCapabilitiesVersion/VMKind/VMFeature — zero importers confirmed repo-wide); InitializeResponse.Capabilities is the one caps mechanism. Co-authored-by: Hanzo Dev <dev@hanzo.ai>
This commit is contained in:
@@ -1,175 +0,0 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package rpc
|
||||
|
||||
// VMKind represents the type of consensus data structure the VM supports.
|
||||
type VMKind string
|
||||
|
||||
const (
|
||||
// VMKindChain indicates a linear blockchain (single-parent blocks)
|
||||
VMKindChain VMKind = "chain"
|
||||
// VMKindDAG indicates a directed acyclic graph (multi-parent vertices)
|
||||
VMKindDAG VMKind = "dag"
|
||||
// VMKindHybrid indicates support for both chain and DAG
|
||||
VMKindHybrid VMKind = "hybrid"
|
||||
)
|
||||
|
||||
// VMFeature represents optional features a VM can support.
|
||||
type VMFeature string
|
||||
|
||||
const (
|
||||
// Core block/vertex operations
|
||||
FeatureBuildBlock VMFeature = "build_block"
|
||||
FeatureParseBlock VMFeature = "parse_block"
|
||||
FeatureGetBlock VMFeature = "get_block"
|
||||
FeatureVerifyBlock VMFeature = "verify_block"
|
||||
FeatureAcceptBlock VMFeature = "accept_block"
|
||||
FeatureRejectBlock VMFeature = "reject_block"
|
||||
FeatureSetPreference VMFeature = "set_preference"
|
||||
|
||||
// DAG-specific operations
|
||||
FeatureBuildVertex VMFeature = "build_vertex"
|
||||
FeatureParseVertex VMFeature = "parse_vertex"
|
||||
FeatureGetVertex VMFeature = "get_vertex"
|
||||
FeatureVerifyVertex VMFeature = "verify_vertex"
|
||||
FeatureAcceptVertex VMFeature = "accept_vertex"
|
||||
FeatureRejectVertex VMFeature = "reject_vertex"
|
||||
FeatureFrontierOps VMFeature = "frontier_ops"
|
||||
|
||||
// State and sync
|
||||
FeatureStateSync VMFeature = "state_sync"
|
||||
FeatureStateSummary VMFeature = "state_summary"
|
||||
FeatureGetAncestors VMFeature = "get_ancestors"
|
||||
|
||||
// Batched operations
|
||||
FeatureBatchedParse VMFeature = "batched_parse"
|
||||
FeatureBatchedVerify VMFeature = "batched_verify"
|
||||
|
||||
// Context-aware operations
|
||||
FeatureWithContext VMFeature = "with_context"
|
||||
|
||||
// Messaging
|
||||
FeatureSender VMFeature = "sender"
|
||||
FeatureHandler VMFeature = "handler"
|
||||
)
|
||||
|
||||
// Capabilities describes what a VM supports.
|
||||
type Capabilities struct {
|
||||
// Kind indicates chain, dag, or hybrid
|
||||
Kind VMKind `json:"kind"`
|
||||
|
||||
// Features lists supported optional features
|
||||
Features []VMFeature `json:"features"`
|
||||
|
||||
// Version of the capability protocol
|
||||
Version uint32 `json:"version"`
|
||||
|
||||
// MinProtocolVersion is the minimum RPC protocol version supported
|
||||
MinProtocolVersion uint32 `json:"min_protocol_version"`
|
||||
|
||||
// MaxProtocolVersion is the maximum RPC protocol version supported
|
||||
MaxProtocolVersion uint32 `json:"max_protocol_version"`
|
||||
}
|
||||
|
||||
// CurrentCapabilitiesVersion is the current version of the capabilities protocol.
|
||||
const CurrentCapabilitiesVersion uint32 = 1
|
||||
|
||||
// NewChainCapabilities returns default capabilities for a chain VM.
|
||||
func NewChainCapabilities() *Capabilities {
|
||||
return &Capabilities{
|
||||
Kind: VMKindChain,
|
||||
Version: CurrentCapabilitiesVersion,
|
||||
MinProtocolVersion: 42,
|
||||
MaxProtocolVersion: 42,
|
||||
Features: []VMFeature{
|
||||
FeatureBuildBlock,
|
||||
FeatureParseBlock,
|
||||
FeatureGetBlock,
|
||||
FeatureVerifyBlock,
|
||||
FeatureAcceptBlock,
|
||||
FeatureRejectBlock,
|
||||
FeatureSetPreference,
|
||||
FeatureGetAncestors,
|
||||
FeatureSender,
|
||||
FeatureHandler,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewDAGCapabilities returns default capabilities for a DAG VM.
|
||||
func NewDAGCapabilities() *Capabilities {
|
||||
return &Capabilities{
|
||||
Kind: VMKindDAG,
|
||||
Version: CurrentCapabilitiesVersion,
|
||||
MinProtocolVersion: 42,
|
||||
MaxProtocolVersion: 42,
|
||||
Features: []VMFeature{
|
||||
FeatureBuildVertex,
|
||||
FeatureParseVertex,
|
||||
FeatureGetVertex,
|
||||
FeatureVerifyVertex,
|
||||
FeatureAcceptVertex,
|
||||
FeatureRejectVertex,
|
||||
FeatureFrontierOps,
|
||||
FeatureGetAncestors,
|
||||
FeatureSender,
|
||||
FeatureHandler,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewHybridCapabilities returns capabilities for a VM that supports both chain and DAG.
|
||||
func NewHybridCapabilities() *Capabilities {
|
||||
caps := NewChainCapabilities()
|
||||
caps.Kind = VMKindHybrid
|
||||
|
||||
// Add DAG features
|
||||
dagCaps := NewDAGCapabilities()
|
||||
caps.Features = append(caps.Features, dagCaps.Features...)
|
||||
|
||||
return caps
|
||||
}
|
||||
|
||||
// SupportsFeature returns true if the VM supports the given feature.
|
||||
func (c *Capabilities) SupportsFeature(feature VMFeature) bool {
|
||||
for _, f := range c.Features {
|
||||
if f == feature {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// SupportsChain returns true if the VM supports chain operations.
|
||||
func (c *Capabilities) SupportsChain() bool {
|
||||
return c.Kind == VMKindChain || c.Kind == VMKindHybrid
|
||||
}
|
||||
|
||||
// SupportsDAG returns true if the VM supports DAG operations.
|
||||
func (c *Capabilities) SupportsDAG() bool {
|
||||
return c.Kind == VMKindDAG || c.Kind == VMKindHybrid
|
||||
}
|
||||
|
||||
// WithFeature adds a feature and returns the capabilities for chaining.
|
||||
func (c *Capabilities) WithFeature(feature VMFeature) *Capabilities {
|
||||
if !c.SupportsFeature(feature) {
|
||||
c.Features = append(c.Features, feature)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// WithStateSync adds state sync capability.
|
||||
func (c *Capabilities) WithStateSync() *Capabilities {
|
||||
return c.WithFeature(FeatureStateSync).WithFeature(FeatureStateSummary)
|
||||
}
|
||||
|
||||
// WithBatchedOps adds batched operation capability.
|
||||
func (c *Capabilities) WithBatchedOps() *Capabilities {
|
||||
return c.WithFeature(FeatureBatchedParse).WithFeature(FeatureBatchedVerify)
|
||||
}
|
||||
|
||||
// WithContext adds context-aware operation capability.
|
||||
func (c *Capabilities) WithContext() *Capabilities {
|
||||
return c.WithFeature(FeatureWithContext)
|
||||
}
|
||||
+88
-2
@@ -18,8 +18,8 @@ import (
|
||||
zapwire "github.com/luxfi/api/zap"
|
||||
"github.com/luxfi/consensus/engine/chain/block"
|
||||
"github.com/luxfi/database"
|
||||
"github.com/luxfi/database/zapdb"
|
||||
"github.com/luxfi/database/prefixdb"
|
||||
"github.com/luxfi/database/zapdb"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
"github.com/luxfi/metric"
|
||||
@@ -101,6 +101,24 @@ func Serve(ctx context.Context, logger log.Logger, vm chain.ChainVM) (retErr err
|
||||
return err
|
||||
}
|
||||
|
||||
// quasarExporter is the OPTIONAL VM-side surface behind zapwire.CapQuasarExport.
|
||||
// A concrete ChainVM (the C-Chain EVM) implements it to track a Quasar
|
||||
// (⅔-by-stake) EXPORT-FINAL height distinct from its reorgable local accept tip.
|
||||
// It is NOT part of the generic chain.ChainVM contract: the plugin server PROBES
|
||||
// the wrapped VM for it once at construction and, when present, advertises
|
||||
// CapQuasarExport at the Initialize handshake AND serves the MsgSetQuasarFinalized
|
||||
// / MsgQuasarHeight round-trips. The method set mirrors the node-side client
|
||||
// (github.com/luxfi/node/vms/rpcchainvm/zap.Client) so the two ends compose
|
||||
// across the ZAP boundary. Absent it, the node keeps this chain Nova-only.
|
||||
type quasarExporter interface {
|
||||
// SetLastQuasarFinalized records that the block at height reached ⅔-stake
|
||||
// EXPORT finality (drives the VM's finalized/safe tags + warp export gate).
|
||||
SetLastQuasarFinalized(height uint64)
|
||||
// LastQuasarHeight returns the VM's accept-tip-clamped EXPORT-FINAL height
|
||||
// (0 before the first export forms).
|
||||
LastQuasarHeight() uint64
|
||||
}
|
||||
|
||||
// zapVMServer implements zapwire.Handler for a VM
|
||||
type zapVMServer struct {
|
||||
vm chain.ChainVM
|
||||
@@ -108,6 +126,13 @@ type zapVMServer struct {
|
||||
allowShutdown *bool
|
||||
db database.Database // persistent database, closed on shutdown
|
||||
|
||||
// quasarVM is the wrapped VM viewed through the OPTIONAL quasarExporter
|
||||
// surface, or nil if the VM does not implement it. Resolved ONCE by probing vm
|
||||
// at construction (a VM's method set is fixed for its life), then read by
|
||||
// capabilities() and the two Quasar handlers. nil ⇒ the server never sets
|
||||
// CapQuasarExport and refuses the Quasar messages, so the node stays Nova-only.
|
||||
quasarVM quasarExporter
|
||||
|
||||
// pendingBlock caches the last built block to prevent rebuilding
|
||||
// with a new timestamp while consensus is voting on it.
|
||||
// Cleared on BlockAccept or BlockReject.
|
||||
@@ -117,11 +142,32 @@ type zapVMServer struct {
|
||||
|
||||
func newZAPVMServer(vm chain.ChainVM, logger log.Logger) *zapVMServer {
|
||||
allowShutdown := false
|
||||
return &zapVMServer{
|
||||
s := &zapVMServer{
|
||||
vm: vm,
|
||||
logger: logger,
|
||||
allowShutdown: &allowShutdown,
|
||||
}
|
||||
// Probe the wrapped VM ONCE for the OPTIONAL Quasar export surface. A hit here
|
||||
// is what turns CapQuasarExport from dead-on-arrival into a real capability: the
|
||||
// bit is advertised at Initialize and the two Quasar handlers dispatch to this
|
||||
// VM. A miss leaves quasarVM nil → generic VM, no export surface.
|
||||
if qvm, ok := vm.(quasarExporter); ok {
|
||||
s.quasarVM = qvm
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// capabilities returns the OPTIONAL cross-boundary add-on bitfield this server
|
||||
// advertises in InitializeResponse.Capabilities, derived from the one-time probe
|
||||
// of the wrapped VM. A generic VM implements no add-ons → 0 → the node stays on
|
||||
// the generic path. This is the SINGLE source of the advertised bitfield; every
|
||||
// bit set here MUST have a matching handler wired in Handle.
|
||||
func (s *zapVMServer) capabilities() uint64 {
|
||||
var caps uint64
|
||||
if s.quasarVM != nil {
|
||||
caps |= zapwire.CapQuasarExport
|
||||
}
|
||||
return caps
|
||||
}
|
||||
|
||||
// Handle implements zapwire.Handler
|
||||
@@ -176,6 +222,10 @@ func (s *zapVMServer) Handle(ctx context.Context, msgType zapwire.MessageType, p
|
||||
return s.handleGetBlockIDAtHeight(ctx, payload)
|
||||
case zapwire.MsgNewHTTPHandler:
|
||||
return s.handleNewHTTPHandler(ctx)
|
||||
case zapwire.MsgSetQuasarFinalized:
|
||||
return s.handleSetQuasarFinalized(ctx, payload)
|
||||
case zapwire.MsgQuasarHeight:
|
||||
return s.handleQuasarHeight(ctx)
|
||||
default:
|
||||
s.logger.Warn("unknown ZAP message type", "msgType", msgType)
|
||||
return msgType, nil, nil
|
||||
@@ -277,6 +327,9 @@ func (s *zapVMServer) handleInitialize(ctx context.Context, payload []byte) (zap
|
||||
Height: lastBlock.Height(),
|
||||
Bytes: lastBlock.Bytes(),
|
||||
Timestamp: lastBlock.Timestamp().UnixNano(),
|
||||
// Capabilities is probed from the wrapped VM (0 for a generic VM). The
|
||||
// node reads it once here to decide which OPTIONAL add-ons to wire.
|
||||
Capabilities: s.capabilities(),
|
||||
}
|
||||
|
||||
buf := zapwire.GetBuffer()
|
||||
@@ -288,6 +341,7 @@ func (s *zapVMServer) handleInitialize(ctx context.Context, payload []byte) (zap
|
||||
s.logger.Info("ZAP VM initialized successfully",
|
||||
"lastAcceptedID", lastAccepted,
|
||||
"height", lastBlock.Height(),
|
||||
"quasarExport", s.quasarVM != nil,
|
||||
)
|
||||
|
||||
return zapwire.MsgInitialize, result, nil
|
||||
@@ -831,6 +885,38 @@ func (s *zapVMServer) handleNewHTTPHandler(ctx context.Context) (zapwire.Message
|
||||
return zapwire.MsgNewHTTPHandler, result, nil
|
||||
}
|
||||
|
||||
// handleSetQuasarFinalized applies a node-pushed Quasar (⅔-by-stake) EXPORT-FINAL
|
||||
// height to the wrapped VM. Only reachable when the VM advertised CapQuasarExport
|
||||
// (quasarVM != nil); a stray message on a non-capable VM is refused, not silently
|
||||
// dropped. The response is empty — the node-side caller is fire-and-forget.
|
||||
func (s *zapVMServer) handleSetQuasarFinalized(ctx context.Context, payload []byte) (zapwire.MessageType, []byte, error) {
|
||||
if s.quasarVM == nil {
|
||||
return zapwire.MsgSetQuasarFinalized, nil, fmt.Errorf("vm does not support quasar export")
|
||||
}
|
||||
req := &zapwire.SetQuasarFinalizedRequest{}
|
||||
if err := req.Decode(zapwire.NewReader(payload)); err != nil {
|
||||
return zapwire.MsgSetQuasarFinalized, nil, fmt.Errorf("decode set-quasar-finalized: %w", err)
|
||||
}
|
||||
s.quasarVM.SetLastQuasarFinalized(req.Height)
|
||||
return zapwire.MsgSetQuasarFinalized, nil, nil
|
||||
}
|
||||
|
||||
// handleQuasarHeight returns the wrapped VM's accept-tip-clamped Quasar
|
||||
// EXPORT-FINAL height (0 before the first export). Only reachable when the VM
|
||||
// advertised CapQuasarExport.
|
||||
func (s *zapVMServer) handleQuasarHeight(ctx context.Context) (zapwire.MessageType, []byte, error) {
|
||||
if s.quasarVM == nil {
|
||||
return zapwire.MsgQuasarHeight, nil, fmt.Errorf("vm does not support quasar export")
|
||||
}
|
||||
resp := &zapwire.QuasarHeightResponse{Height: s.quasarVM.LastQuasarHeight()}
|
||||
buf := zapwire.GetBuffer()
|
||||
resp.Encode(buf)
|
||||
result := make([]byte, len(buf.Bytes()))
|
||||
copy(result, buf.Bytes())
|
||||
zapwire.PutBuffer(buf)
|
||||
return zapwire.MsgQuasarHeight, result, nil
|
||||
}
|
||||
|
||||
func errorToZAP(err error) zapwire.Error {
|
||||
if err == nil {
|
||||
return zapwire.ErrorUnspecified
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
//go:build !grpc
|
||||
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
zapwire "github.com/luxfi/api/zap"
|
||||
"github.com/luxfi/log"
|
||||
"github.com/luxfi/vm/chain"
|
||||
)
|
||||
|
||||
// baseVM embeds chain.ChainVM so it satisfies the full generic contract with no
|
||||
// boilerplate. The probe + Quasar handlers under test never invoke the embedded
|
||||
// (nil) methods, so leaving them unimplemented is safe for these unit tests.
|
||||
type baseVM struct{ chain.ChainVM }
|
||||
|
||||
// quasarVMStub is a ChainVM that ALSO implements the OPTIONAL quasarExporter
|
||||
// surface the server probes for at construction.
|
||||
type quasarVMStub struct {
|
||||
baseVM
|
||||
finalized uint64
|
||||
height uint64
|
||||
}
|
||||
|
||||
func (q *quasarVMStub) SetLastQuasarFinalized(h uint64) { q.finalized = h }
|
||||
func (q *quasarVMStub) LastQuasarHeight() uint64 { return q.height }
|
||||
|
||||
// Compile-time contracts: both stubs are ChainVMs; only the quasar stub is a
|
||||
// quasarExporter (so the server's probe distinguishes them).
|
||||
var (
|
||||
_ chain.ChainVM = baseVM{}
|
||||
_ chain.ChainVM = (*quasarVMStub)(nil)
|
||||
_ quasarExporter = (*quasarVMStub)(nil)
|
||||
)
|
||||
|
||||
func TestCapabilitiesProbe(t *testing.T) {
|
||||
logger := log.NewTestLogger(log.DebugLevel)
|
||||
|
||||
// Generic VM: no optional add-ons → Capabilities 0 → node stays Nova-only.
|
||||
if got := newZAPVMServer(baseVM{}, logger).capabilities(); got != 0 {
|
||||
t.Fatalf("generic VM must advertise 0 capabilities, got %d", got)
|
||||
}
|
||||
|
||||
// Quasar-capable VM: probe hits → CapQuasarExport advertised.
|
||||
if got := newZAPVMServer(&quasarVMStub{}, logger).capabilities(); got != zapwire.CapQuasarExport {
|
||||
t.Fatalf("quasar VM must advertise CapQuasarExport (%d), got %d", zapwire.CapQuasarExport, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuasarHandlersRoundTrip(t *testing.T) {
|
||||
logger := log.NewTestLogger(log.DebugLevel)
|
||||
stub := &quasarVMStub{height: 99}
|
||||
s := newZAPVMServer(stub, logger)
|
||||
ctx := context.Background()
|
||||
|
||||
// MsgSetQuasarFinalized forwards the height into the wrapped VM.
|
||||
req := &zapwire.SetQuasarFinalizedRequest{Height: 42}
|
||||
buf := zapwire.GetBuffer()
|
||||
req.Encode(buf)
|
||||
payload := append([]byte(nil), buf.Bytes()...)
|
||||
zapwire.PutBuffer(buf)
|
||||
|
||||
mt, resp, err := s.handleSetQuasarFinalized(ctx, payload)
|
||||
if err != nil {
|
||||
t.Fatalf("handleSetQuasarFinalized: %v", err)
|
||||
}
|
||||
if mt != zapwire.MsgSetQuasarFinalized || resp != nil {
|
||||
t.Fatalf("set-quasar-finalized reply: mt=%d resp=%v", mt, resp)
|
||||
}
|
||||
if stub.finalized != 42 {
|
||||
t.Fatalf("SetLastQuasarFinalized not applied: got %d want 42", stub.finalized)
|
||||
}
|
||||
|
||||
// MsgQuasarHeight returns the wrapped VM's clamped height.
|
||||
mt, resp, err = s.handleQuasarHeight(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("handleQuasarHeight: %v", err)
|
||||
}
|
||||
if mt != zapwire.MsgQuasarHeight {
|
||||
t.Fatalf("quasar-height reply type: got %d", mt)
|
||||
}
|
||||
var hr zapwire.QuasarHeightResponse
|
||||
if err := hr.Decode(zapwire.NewReader(resp)); err != nil {
|
||||
t.Fatalf("decode QuasarHeightResponse: %v", err)
|
||||
}
|
||||
if hr.Height != 99 {
|
||||
t.Fatalf("quasar height: got %d want 99", hr.Height)
|
||||
}
|
||||
}
|
||||
|
||||
// TestQuasarHandlersRefusedWhenUnsupported: a stray Quasar message on a generic
|
||||
// VM is refused with an error, not silently dropped or a nil-pointer panic.
|
||||
func TestQuasarHandlersRefusedWhenUnsupported(t *testing.T) {
|
||||
s := newZAPVMServer(baseVM{}, log.NewTestLogger(log.DebugLevel))
|
||||
if _, _, err := s.handleQuasarHeight(context.Background()); err == nil {
|
||||
t.Fatal("handleQuasarHeight on a non-quasar VM must error")
|
||||
}
|
||||
if _, _, err := s.handleSetQuasarFinalized(context.Background(), nil); err == nil {
|
||||
t.Fatal("handleSetQuasarFinalized on a non-quasar VM must error")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user