mirror of
https://github.com/luxfi/netrunner.git
synced 2026-10-30 15:24:46 +00:00
Per the project rule 'ZAP internal, ZIP edge': netrunner's 26-method
ControlService + PingService move off google.golang.org/grpc and
grpc-gateway onto a luxfi/zap envelope. Default builds now have ZERO
google.golang.org/grpc transitive deps.
New package: netrunner/zaprpc/
- protocol.go: MsgType (uint8 range, encoded in ZAP flags upper byte).
One stable wire ID per method. Append-only.
- dispatch.go: typed Bind[Req, Resp](d, msg, handler) — register a
'func(ctx, *Req) (*Resp, error)' handler. JSON encode/decode +
envelope framing happen here, handlers stay business-logic-only.
- server.go: thin wrapper around zap.Node hosting a Dispatcher.
- client.go: typed Call[Req, Resp](ctx, c, msg, req) — same DX as the
old gRPC client but transport is ZAP. Includes connect-retry around
the boot race.
server/bind.go: one-shot registration of all 26 RPC methods onto a
Dispatcher. The canonical map of 'what this server exposes'.
server/server.go: gRPC server + grpc-gateway HTTP bridge are gone.
Run() now starts the ZAP server and waits for rootCtx.Done. The
'soldier-on' isClientCanceled helper goes with the gRPC stream code.
client/client.go: full rewrite. Same Client interface (28 methods),
implementation is now one-line zaprpc.Call per method. StreamStatus
becomes a client-driven Status() poll on the requested interval —
ZAP has no native server-streaming, but the observable contract
(channel of ClusterInfo) is unchanged.
Deleted:
- rpcpb/rpc_grpc.pb.go (gRPC service stubs)
- rpcpb/rpc.pb.gw.go (grpc-gateway HTTP bridge)
The rpcpb message types (rpc.pb.go) stay — they're plain Go structs
with json: tags, used as the JSON payload format inside ZAP envelopes.
A future cleanup pass can replace them with hand-written structs to
drop google.golang.org/protobuf from the dep tree too.
go.mod cleanup: drop github.com/grpc-ecosystem/grpc-gateway/v2,
google.golang.org/grpc, google.golang.org/genproto/googleapis/api.
Pin luxfi/proto v1.0.0 (was luxfi/protocol v0.0.4 — stale rename
artifact). Add luxfi/zap v0.2.0.
Tests: zaprpc/zaprpc_test.go round-trips request/response and proves
the error path propagates through the envelope.
No backwards compat — forwards perfection per the project rule.
177 lines
5.3 KiB
Go
177 lines
5.3 KiB
Go
// Copyright (C) 2021-2026, Lux Industries Inc. All rights reserved.
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
// Package zaprpc carries the netrunner control-plane RPC over luxfi/zap.
|
|
//
|
|
// One MsgType per RPC method; payloads are JSON inside a ZAP envelope:
|
|
//
|
|
// envelope := zap.NewBuilder(...)
|
|
// root := envelope.StartObject(envelopeSize)
|
|
// root.SetBytes(FieldPayload, payloadJSON) // request or response body
|
|
// root.SetText(FieldError, errMsg) // empty on success
|
|
// root.FinishAsRoot()
|
|
// wire := envelope.FinishWithFlags(MsgType << 8)
|
|
//
|
|
// JSON is the body format so any rpcpb.* struct round-trips via its existing
|
|
// `json:` tags — no extra schema to maintain and no `google.golang.org/protobuf`
|
|
// on the wire (per the project rule: ZAP internal, ZIP edge).
|
|
package zaprpc
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/luxfi/zap"
|
|
)
|
|
|
|
// MsgType identifies a netrunner RPC method on the ZAP wire.
|
|
//
|
|
// Stable wire IDs: never renumber, append-only. Bumping changes the wire
|
|
// protocol and forces clients to upgrade in lockstep with the server.
|
|
//
|
|
// Constraint: ZAP carries the method tag in the upper 8 bits of its 16-bit
|
|
// flags header, so MsgType values must fit in uint8 (0..255). 1..26 are
|
|
// the current netrunner control-plane methods; 27..255 are free for future
|
|
// additions; 0 is reserved (unused).
|
|
type MsgType uint16
|
|
|
|
const (
|
|
MsgPing MsgType = 1
|
|
MsgRPCVersion MsgType = 2
|
|
MsgStart MsgType = 3
|
|
MsgCreateBlockchains MsgType = 4
|
|
MsgTransformElasticChains MsgType = 5
|
|
MsgAddPermissionlessValidator MsgType = 6
|
|
MsgRemoveChainValidator MsgType = 7
|
|
MsgCreateChains MsgType = 8
|
|
MsgHealth MsgType = 9
|
|
MsgURIs MsgType = 10
|
|
MsgWaitForHealthy MsgType = 11
|
|
MsgStatus MsgType = 12
|
|
MsgRemoveNode MsgType = 13
|
|
MsgAddNode MsgType = 14
|
|
MsgRestartNode MsgType = 15
|
|
MsgPauseNode MsgType = 16
|
|
MsgResumeNode MsgType = 17
|
|
MsgStop MsgType = 18
|
|
MsgAttachPeer MsgType = 19
|
|
MsgSendOutboundMessage MsgType = 20
|
|
MsgSaveSnapshot MsgType = 21
|
|
MsgSaveHotSnapshot MsgType = 22
|
|
MsgLoadSnapshot MsgType = 23
|
|
MsgRemoveSnapshot MsgType = 24
|
|
MsgGetSnapshotNames MsgType = 25
|
|
MsgStreamStatus MsgType = 26 // server→client status push, one msg per tick
|
|
)
|
|
|
|
// Envelope field layout — keep the size 16 (multiple of 8 for alignment).
|
|
const (
|
|
envelopeSize = 16
|
|
FieldPayload int = 0 // bytes — JSON-encoded request or response body
|
|
FieldError int = 8 // text — non-empty on RPC-level error
|
|
)
|
|
|
|
// Encode wraps a JSON-marshalable value in a ZAP envelope tagged with msgType.
|
|
// Empty errStr means "success".
|
|
func Encode(msgType MsgType, body any, errStr string) ([]byte, error) {
|
|
var payload []byte
|
|
if body != nil {
|
|
var err error
|
|
payload, err = json.Marshal(body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("zaprpc encode: %w", err)
|
|
}
|
|
}
|
|
|
|
cap := envelopeSize + 64 + len(payload) + len(errStr)
|
|
b := zap.NewBuilder(cap)
|
|
root := b.StartObject(envelopeSize)
|
|
root.SetBytes(FieldPayload, payload)
|
|
root.SetText(FieldError, errStr)
|
|
root.FinishAsRoot()
|
|
return b.FinishWithFlags(uint16(msgType) << 8), nil
|
|
}
|
|
|
|
// Decode reads a ZAP envelope. dest may be nil for void responses.
|
|
// Returns the RPC-level error from the envelope (non-nil if errStr was set).
|
|
func Decode(msg *zap.Message, dest any) error {
|
|
root := msg.Root()
|
|
if errStr := root.Text(FieldError); errStr != "" {
|
|
return errors.New(errStr)
|
|
}
|
|
payload := root.Bytes(FieldPayload)
|
|
if dest == nil || len(payload) == 0 {
|
|
return nil
|
|
}
|
|
if err := json.Unmarshal(payload, dest); err != nil {
|
|
return fmt.Errorf("zaprpc decode: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// MsgTypeOf extracts the MsgType from a ZAP message's flags.
|
|
func MsgTypeOf(msg *zap.Message) MsgType {
|
|
return MsgType(msg.Flags() >> 8)
|
|
}
|
|
|
|
// String renders the canonical name for telemetry/logging.
|
|
func (m MsgType) String() string {
|
|
switch m {
|
|
case MsgPing:
|
|
return "Ping"
|
|
case MsgRPCVersion:
|
|
return "RPCVersion"
|
|
case MsgStart:
|
|
return "Start"
|
|
case MsgCreateBlockchains:
|
|
return "CreateBlockchains"
|
|
case MsgTransformElasticChains:
|
|
return "TransformElasticChains"
|
|
case MsgAddPermissionlessValidator:
|
|
return "AddPermissionlessValidator"
|
|
case MsgRemoveChainValidator:
|
|
return "RemoveChainValidator"
|
|
case MsgCreateChains:
|
|
return "CreateChains"
|
|
case MsgHealth:
|
|
return "Health"
|
|
case MsgURIs:
|
|
return "URIs"
|
|
case MsgWaitForHealthy:
|
|
return "WaitForHealthy"
|
|
case MsgStatus:
|
|
return "Status"
|
|
case MsgRemoveNode:
|
|
return "RemoveNode"
|
|
case MsgAddNode:
|
|
return "AddNode"
|
|
case MsgRestartNode:
|
|
return "RestartNode"
|
|
case MsgPauseNode:
|
|
return "PauseNode"
|
|
case MsgResumeNode:
|
|
return "ResumeNode"
|
|
case MsgStop:
|
|
return "Stop"
|
|
case MsgAttachPeer:
|
|
return "AttachPeer"
|
|
case MsgSendOutboundMessage:
|
|
return "SendOutboundMessage"
|
|
case MsgSaveSnapshot:
|
|
return "SaveSnapshot"
|
|
case MsgSaveHotSnapshot:
|
|
return "SaveHotSnapshot"
|
|
case MsgLoadSnapshot:
|
|
return "LoadSnapshot"
|
|
case MsgRemoveSnapshot:
|
|
return "RemoveSnapshot"
|
|
case MsgGetSnapshotNames:
|
|
return "GetSnapshotNames"
|
|
case MsgStreamStatus:
|
|
return "StreamStatus"
|
|
default:
|
|
return fmt.Sprintf("Msg(%d)", uint16(m))
|
|
}
|
|
}
|