Files
api/info/types.go
T
zeekay 2210675e6e The info, admin and health replies cross the plane carrying their values
A ZAP field is an offset. Eight of these replies held a map, which has no
order to give one, so the reply could not cross between two processes at
all. Two more held values whose fields are every one unexported —
netip.AddrPort, time.Time — and those are worse: they are not refused, they
derive an EMPTY layout, so the reply crossed, reported no error and carried
nothing. GetNodeIPReply has one field, so its whole answer was blank. And a
health check says what it likes, so its details were an `any`, which has no
shape until it holds something.

Each map is now a list of a stated element type, ordered by the key it was
an object of. An address is the text netip already wrote for it, an instant
is three numbers, and a check details are the bytes of their own JSON —
the one shape an object, a string and a list all share.

The JSON does not move. Every list marshals to the object it replaced and
reads one back, and wire_test.go holds that against the old shape spelled
out as a literal rather than against itself. The order is new where there
was none: a map answered the same question two ways.

APIReply also states the status it carries, so the 503 a probe reads and
the healthy a person reads are one fact computed once.
2026-08-29 22:53:03 -07:00

355 lines
11 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// What a node will tell anyone who asks.
//
// SEVERAL SHAPES HERE ARE AN OBJECT ON THE JSON WIRE AND A LIST IN GO, and the
// reason is the same every time: a ZAP field is an offset, a map has no order to
// give one, so a reply carrying a map cannot cross between two processes at all.
// The list is ordered by the key it was an object of, which is an order a map
// never had — so the answer is now the same twice running as well.
//
// The JSON is unchanged. Each list marshals to the object it replaced, and reads
// one back.
package info
import (
"encoding/json"
"slices"
"github.com/luxfi/api/types"
"github.com/luxfi/ids"
)
// PeerInfo is the JSON wire shape for a peer in the Peers RPC reply. It is
// the local owner of this shape; previously this type came from
// github.com/luxfi/p2p/peer.Info, but that module is being archived per
// LP-201. Field tags are stable across the rename — JSON consumers see no
// difference.
type PeerInfo struct {
// IP is the address this node reaches the peer at.
IP types.Addr `json:"ip"`
// PublicIP is the address the peer says it is reachable at.
PublicIP types.Addr `json:"publicIP,omitempty"`
// ID is the peer's node id.
ID ids.NodeID `json:"nodeID"`
// Version is the node software the peer is running.
Version string `json:"version"`
// LastSent is when this node last sent the peer a message.
LastSent types.Time `json:"lastSent"`
// LastReceived is when this node last heard from the peer.
LastReceived types.Time `json:"lastReceived"`
// ObservedUptime is the percentage of time this node has seen the peer up.
ObservedUptime types.Uint32 `json:"observedUptime"`
// TrackedChains are the chains the peer serves, ordered by id.
TrackedChains []ids.ID `json:"trackedChains"`
// SupportedLPs are the LPs the peer votes for, ascending.
SupportedLPs []uint32 `json:"supportedLPs"`
// ObjectedLPs are the LPs the peer votes against, ascending.
ObjectedLPs []uint32 `json:"objectedLPs"`
}
// ProofOfPossession is a JSON-friendly representation of a BLS PoP.
type ProofOfPossession struct {
PublicKey string `json:"publicKey"`
ProofOfPossession string `json:"proofOfPossession"`
}
// GetNodeVersionReply are the results from calling GetNodeVersion.
type GetNodeVersionReply struct {
Version string `json:"version"`
DatabaseVersion string `json:"databaseVersion"`
RPCProtocolVersion types.Uint32 `json:"rpcProtocolVersion"`
GitCommit string `json:"gitCommit"`
VMVersions VMVersions `json:"vmVersions"`
// Consensus describes the active consensus configuration. Populated
// from the in-memory consensus engine state at request time. Omitted
// for legacy clients that don't wire it through.
Consensus *ConsensusInfo `json:"consensus,omitempty"`
}
// VMVersion is one VM and the version of it a node runs.
type VMVersion struct {
// VM is the VM's alias.
VM string `json:"vm"`
// Version is the release of it this node runs.
Version string `json:"version"`
}
// VMVersions is what a node runs of each VM: an object keyed by VM alias on the
// JSON wire, a list ordered by that alias here.
type VMVersions []VMVersion
func (v VMVersions) MarshalJSON() ([]byte, error) {
m := make(map[string]string, len(v))
for _, e := range v {
m[e.VM] = e.Version
}
return json.Marshal(m)
}
func (v *VMVersions) UnmarshalJSON(b []byte) error {
var m map[string]string
if err := json.Unmarshal(b, &m); err != nil {
return err
}
*v = make(VMVersions, 0, len(m))
for vm, version := range m {
*v = append(*v, VMVersion{VM: vm, Version: version})
}
slices.SortFunc(*v, func(x, y VMVersion) int { return cmpString(x.VM, y.VM) })
return nil
}
// ConsensusInfo summarises the consensus configuration of a running node so
// callers don't have to scrape the boot logs to learn whether Quasar is in
// triple/dual/classical mode.
type ConsensusInfo struct {
// Mode is one of "triple" (BLS + Corona + ML-DSA), "dual" (BLS +
// Corona), or "classical" (BLS only). Free-form so future modes
// don't bump the API version.
Mode string `json:"mode"`
// BLS is always true for production Quasar nodes.
BLS bool `json:"bls"`
// Corona is true when the post-quantum lattice threshold path is wired.
Corona bool `json:"corona"`
// MLDSA is true when ML-DSA-65 (FIPS 204) signature verification is wired.
MLDSA bool `json:"mlDSA"`
// PlatformVM is true when the production PlatformVM wiring is in use,
// false when running with the dev/stub PlatformVM.
PlatformVM bool `json:"platformVM"`
}
// GetNodeIDReply are the results from calling GetNodeID.
type GetNodeIDReply struct {
NodeID ids.NodeID `json:"nodeID"`
NodePOP *ProofOfPossession `json:"nodePOP"`
}
// GetNetworkIDReply are the results from calling GetNetworkID.
type GetNetworkIDReply struct {
NetworkID types.Uint32 `json:"networkID"`
}
// GetNodeIPReply are the results from calling GetNodeIP.
type GetNodeIPReply struct {
// IP is the address this node tells peers to reach it at.
IP types.Addr `json:"ip"`
}
// GetNetworkNameReply is the result from calling GetNetworkName.
type GetNetworkNameReply struct {
NetworkName string `json:"networkName"`
}
// GetBlockchainIDArgs are the arguments for calling GetBlockchainID.
type GetBlockchainIDArgs struct {
// Alias is the name the chain answers to, such as "X".
Alias string `json:"alias"`
}
// GetBlockchainIDReply are the results from calling GetBlockchainID.
type GetBlockchainIDReply struct {
BlockchainID ids.ID `json:"blockchainID"`
}
// PeersArgs are the arguments for calling Peers.
type PeersArgs struct {
// NodeIDs narrows the answer to these peers. Empty asks for all of them.
NodeIDs []ids.NodeID `json:"nodeIDs"`
}
// Peer is information about a peer in the network.
type Peer struct {
PeerInfo
Benched []string `json:"benched"`
}
// PeersReply are the results from calling Peers.
type PeersReply struct {
NumPeers types.Uint64 `json:"numPeers"`
Peers []Peer `json:"peers"`
}
// IsBootstrappedArgs are the arguments for calling IsBootstrapped.
type IsBootstrappedArgs struct {
// Chain is the alias or id of the chain to ask about.
Chain string `json:"chain"`
}
// IsBootstrappedResponse are the results from calling IsBootstrapped.
type IsBootstrappedResponse struct {
IsBootstrapped bool `json:"isBootstrapped"`
}
// UptimeResponse are the results from calling Uptime.
type UptimeResponse struct {
RewardingStakePercentage types.Float64 `json:"rewardingStakePercentage"`
WeightedAveragePercentage types.Float64 `json:"weightedAveragePercentage"`
}
// LP is information about an LP proposal.
type LP struct {
SupportWeight types.Uint64 `json:"supportWeight"`
// Supporters are the peers voting for it, ordered by node id.
Supporters []ids.NodeID `json:"supporters"`
ObjectWeight types.Uint64 `json:"objectWeight"`
// Objectors are the peers voting against it, ordered by node id.
Objectors []ids.NodeID `json:"objectors"`
AbstainWeight types.Uint64 `json:"abstainWeight"`
}
// LPStatus is one LP proposal and where the network's stake stands on it.
type LPStatus struct {
// Number is the LP's number, the key it appears under on the JSON wire.
Number uint32 `json:"number"`
// LP is the stake for, against and abstaining.
LP LP `json:"lp"`
}
// LPs is where the network stands on every LP: an object keyed by LP number on
// the JSON wire, a list in ascending number here.
type LPs []LPStatus
func (l LPs) MarshalJSON() ([]byte, error) {
m := make(map[uint32]LP, len(l))
for _, e := range l {
m[e.Number] = e.LP
}
return json.Marshal(m)
}
func (l *LPs) UnmarshalJSON(b []byte) error {
var m map[uint32]LP
if err := json.Unmarshal(b, &m); err != nil {
return err
}
*l = make(LPs, 0, len(m))
for number, lp := range m {
*l = append(*l, LPStatus{Number: number, LP: lp})
}
slices.SortFunc(*l, func(x, y LPStatus) int { return cmpUint32(x.Number, y.Number) })
return nil
}
// LPsReply are the results from calling LPs.
type LPsReply struct {
LPs LPs `json:"lps"`
}
// Find is the status of one LP, and whether the reply carries it.
func (l LPs) Find(number uint32) (*LPStatus, bool) {
for i := range l {
if l[i].Number == number {
return &l[i], true
}
}
return nil, false
}
// GetTxFeeResponse are the results from calling GetTxFee.
type GetTxFeeResponse struct {
TxFee types.Uint64 `json:"txFee"`
CreateAssetTxFee types.Uint64 `json:"createAssetTxFee"`
CreateNetworkTxFee types.Uint64 `json:"createNetworkTxFee"`
TransformChainTxFee types.Uint64 `json:"transformChainTxFee"`
CreateChainTxFee types.Uint64 `json:"createChainTxFee"`
AddNetworkValidatorFee types.Uint64 `json:"addNetworkValidatorFee"`
AddNetworkDelegatorFee types.Uint64 `json:"addNetworkDelegatorFee"`
}
// VMAlias is one VM and the names it answers to.
type VMAlias struct {
// VM is the VM's id.
VM ids.ID `json:"vm"`
// Aliases are the names it also answers to.
Aliases []string `json:"aliases"`
}
// VMAliases are the VMs installed on a node: an object keyed by VM id on the
// JSON wire, a list ordered by that id here.
type VMAliases []VMAlias
func (v VMAliases) MarshalJSON() ([]byte, error) {
m := make(map[ids.ID][]string, len(v))
for _, e := range v {
m[e.VM] = e.Aliases
}
return json.Marshal(m)
}
func (v *VMAliases) UnmarshalJSON(b []byte) error {
var m map[ids.ID][]string
if err := json.Unmarshal(b, &m); err != nil {
return err
}
*v = make(VMAliases, 0, len(m))
for vm, aliases := range m {
*v = append(*v, VMAlias{VM: vm, Aliases: aliases})
}
slices.SortFunc(*v, func(x, y VMAlias) int { return x.VM.Compare(y.VM) })
return nil
}
// FxName is one feature extension and what it is called.
type FxName struct {
// Fx is the extension's id.
Fx ids.ID `json:"fx"`
// Name is what it is called.
Name string `json:"name"`
}
// FxNames are the feature extensions a node knows: an object keyed by id on the
// JSON wire, a list ordered by that id here.
type FxNames []FxName
func (f FxNames) MarshalJSON() ([]byte, error) {
m := make(map[ids.ID]string, len(f))
for _, e := range f {
m[e.Fx] = e.Name
}
return json.Marshal(m)
}
func (f *FxNames) UnmarshalJSON(b []byte) error {
var m map[ids.ID]string
if err := json.Unmarshal(b, &m); err != nil {
return err
}
*f = make(FxNames, 0, len(m))
for fx, name := range m {
*f = append(*f, FxName{Fx: fx, Name: name})
}
slices.SortFunc(*f, func(x, y FxName) int { return x.Fx.Compare(y.Fx) })
return nil
}
// GetVMsReply contains the response metadata for GetVMs.
type GetVMsReply struct {
VMs VMAliases `json:"vms"`
Fxs FxNames `json:"fxs"`
}
// cmpString and cmpUint32 order the lists above. Written out rather than reached
// for from cmp so the ordering a reply is in is stated where the reply is.
func cmpString(x, y string) int {
switch {
case x < y:
return -1
case x > y:
return 1
}
return 0
}
func cmpUint32(x, y uint32) int {
switch {
case x < y:
return -1
case x > y:
return 1
}
return 0
}