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.
This commit is contained in:
2026-08-29 22:53:03 -07:00
parent 1de772ad2d
commit 2210675e6e
9 changed files with 2373 additions and 47 deletions
+142 -5
View File
@@ -1,9 +1,20 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Operating a node.
//
// THREE SHAPES HERE ARE AN OBJECT ON THE JSON WIRE AND A LIST IN GO — the logger
// levels, the installed VMs, and what a reload found. A ZAP field is an offset
// and a map has no order to give one, so a reply carrying a map cannot cross
// between two processes at all. Each list is ordered by the key it was an object
// of; the JSON is unchanged.
package admin
import (
"encoding/json"
"slices"
"strings"
"github.com/luxfi/api/types"
"github.com/luxfi/ids"
)
@@ -43,9 +54,42 @@ type LogAndDisplayLevels struct {
DisplayLevel string `json:"displayLevel"`
}
// LoggerLevel is one logger and the two levels it is set to.
type LoggerLevel struct {
// Logger is the logger's name, the key it appears under on the JSON wire.
Logger string `json:"logger"`
// Levels are what it writes to its file and to the display.
Levels LogAndDisplayLevels `json:"levels"`
}
// LoggerLevels are the levels every named logger is set to: an object keyed by
// logger name on the JSON wire, a list ordered by that name here.
type LoggerLevels []LoggerLevel
func (l LoggerLevels) MarshalJSON() ([]byte, error) {
m := make(map[string]LogAndDisplayLevels, len(l))
for _, e := range l {
m[e.Logger] = e.Levels
}
return json.Marshal(m)
}
func (l *LoggerLevels) UnmarshalJSON(b []byte) error {
var m map[string]LogAndDisplayLevels
if err := json.Unmarshal(b, &m); err != nil {
return err
}
*l = make(LoggerLevels, 0, len(m))
for name, levels := range m {
*l = append(*l, LoggerLevel{Logger: name, Levels: levels})
}
slices.SortFunc(*l, func(x, y LoggerLevel) int { return strings.Compare(x.Logger, y.Logger) })
return nil
}
// LoggerLevelReply are the levels of the loggers.
type LoggerLevelReply struct {
LoggerLevels map[string]LogAndDisplayLevels `json:"loggerLevels"`
LoggerLevels LoggerLevels `json:"loggerLevels"`
}
// GetLoggerLevelArgs are the arguments for getting logger levels.
@@ -53,11 +97,77 @@ type GetLoggerLevelArgs struct {
LoggerName string `json:"loggerName"`
}
// LoadedVM is one VM a reload brought in and the names it answers to.
type LoadedVM struct {
// VM is the VM's id, the key it appears under on the JSON wire.
VM ids.ID `json:"vm"`
// Aliases are the names it also answers to.
Aliases []string `json:"aliases"`
}
// LoadedVMs are the VMs a reload brought in: an object keyed by VM id on the
// JSON wire, a list ordered by that id here.
type LoadedVMs []LoadedVM
func (l LoadedVMs) MarshalJSON() ([]byte, error) {
m := make(map[ids.ID][]string, len(l))
for _, e := range l {
m[e.VM] = e.Aliases
}
return json.Marshal(m)
}
func (l *LoadedVMs) UnmarshalJSON(b []byte) error {
var m map[ids.ID][]string
if err := json.Unmarshal(b, &m); err != nil {
return err
}
*l = make(LoadedVMs, 0, len(m))
for vm, aliases := range m {
*l = append(*l, LoadedVM{VM: vm, Aliases: aliases})
}
slices.SortFunc(*l, func(x, y LoadedVM) int { return x.VM.Compare(y.VM) })
return nil
}
// FailedVM is one VM a reload could not bring in, and why.
type FailedVM struct {
// VM is the VM's id, the key it appears under on the JSON wire.
VM ids.ID `json:"vm"`
// Error is what stopped it loading.
Error string `json:"error"`
}
// FailedVMs are the VMs a reload could not bring in: an object keyed by VM id
// on the JSON wire, a list ordered by that id here.
type FailedVMs []FailedVM
func (f FailedVMs) MarshalJSON() ([]byte, error) {
m := make(map[ids.ID]string, len(f))
for _, e := range f {
m[e.VM] = e.Error
}
return json.Marshal(m)
}
func (f *FailedVMs) UnmarshalJSON(b []byte) error {
var m map[ids.ID]string
if err := json.Unmarshal(b, &m); err != nil {
return err
}
*f = make(FailedVMs, 0, len(m))
for vm, why := range m {
*f = append(*f, FailedVM{VM: vm, Error: why})
}
slices.SortFunc(*f, func(x, y FailedVM) int { return x.VM.Compare(y.VM) })
return nil
}
// LoadVMsReply contains the response for LoadVMs.
type LoadVMsReply struct {
NewVMs map[ids.ID][]string `json:"newVMs"`
FailedVMs map[ids.ID]string `json:"failedVMs"`
ChainsRetried int `json:"chainsRetried"`
NewVMs LoadedVMs `json:"newVMs"`
FailedVMs FailedVMs `json:"failedVMs"`
ChainsRetried int `json:"chainsRetried"`
}
// DBGetArgs are the arguments for DBGet.
@@ -77,9 +187,36 @@ type VMInfo struct {
Path string `json:"path,omitempty"`
}
// InstalledVMs are the VMs on a node: an object keyed by VM id on the JSON
// wire, a list ordered by that id here. The id is on the element too — it is
// [VMInfo.ID] — so the key is the element's own name rather than a second one.
type InstalledVMs []VMInfo
func (v InstalledVMs) MarshalJSON() ([]byte, error) {
m := make(map[string]VMInfo, len(v))
for _, e := range v {
m[e.ID] = e
}
return json.Marshal(m)
}
func (v *InstalledVMs) UnmarshalJSON(b []byte) error {
var m map[string]VMInfo
if err := json.Unmarshal(b, &m); err != nil {
return err
}
*v = make(InstalledVMs, 0, len(m))
for id, info := range m {
info.ID = id
*v = append(*v, info)
}
slices.SortFunc(*v, func(x, y VMInfo) int { return strings.Compare(x.ID, y.ID) })
return nil
}
// ListVMsReply contains the response for ListVMs.
type ListVMsReply struct {
VMs map[string]VMInfo `json:"vms"`
VMs InstalledVMs `json:"vms"`
}
// SnapshotArgs are the arguments for Snapshot.
+476
View File
@@ -168,6 +168,47 @@ func (x *DBGetReply) UnmarshalZAP(data []byte) error {
return nil
}
// ---- FailedVM ----------------------------------------------------------
const (
failedVMVMAt = 0
failedVMErrorAt = 32
failedVMSize = 40
)
var _ interface {
MarshalZAP() ([]byte, error)
UnmarshalZAP([]byte) error
} = (*FailedVM)(nil)
// MarshalZAP writes FailedVM from constant offsets.
func (x *FailedVM) MarshalZAP() ([]byte, error) {
if x == nil {
return nil, nil
}
b := zap.NewBuilder(failedVMSize + 256)
ob := b.StartObject(failedVMSize)
ob.SetBytesFixed(failedVMVMAt, x.VM[:])
ob.SetText(failedVMErrorAt, string(x.Error))
ob.FinishAsRoot()
return b.Finish(), nil
}
// UnmarshalZAP reads FailedVM out of the buffer that arrived.
func (x *FailedVM) UnmarshalZAP(data []byte) error {
if x == nil || len(data) == 0 {
return nil
}
m, err := zap.Parse(data)
if err != nil {
return fmt.Errorf("FailedVM: %w", err)
}
o := m.Root()
copy(x.VM[:], o.BytesFixed(failedVMVMAt, 32))
x.Error = string(strings.Clone(o.Text(failedVMErrorAt)))
return nil
}
// ---- GetChainAliasesArgs -----------------------------------------------
const (
@@ -360,6 +401,69 @@ func (x *GetTrackedChainsReply) UnmarshalZAP(data []byte) error {
return nil
}
// ---- ListVMsReply ------------------------------------------------------
const (
listVMsReplyVMsAt = 0
listVMsReplySize = 8
)
var _ interface {
MarshalZAP() ([]byte, error)
UnmarshalZAP([]byte) error
} = (*ListVMsReply)(nil)
// MarshalZAP writes ListVMsReply from constant offsets.
func (x *ListVMsReply) MarshalZAP() ([]byte, error) {
if x == nil {
return nil, nil
}
b := zap.NewBuilder(listVMsReplySize + 256)
vMsAt, vMsN := 0, len(x.VMs)
if vMsN > 0 {
var blob []byte
for i := range x.VMs {
enc, err := x.VMs[i].MarshalZAP()
if err != nil {
return nil, err
}
var n [4]byte
binary.LittleEndian.PutUint32(n[:], uint32(len(enc)))
blob = append(blob, n[:]...)
blob = append(blob, enc...)
}
vMsAt = b.WriteBytes(blob)
}
ob := b.StartObject(listVMsReplySize)
if vMsN > 0 {
ob.SetList(listVMsReplyVMsAt, vMsAt, vMsN)
}
ob.FinishAsRoot()
return b.Finish(), nil
}
// UnmarshalZAP reads ListVMsReply out of the buffer that arrived.
func (x *ListVMsReply) UnmarshalZAP(data []byte) error {
if x == nil || len(data) == 0 {
return nil
}
m, err := zap.Parse(data)
if err != nil {
return fmt.Errorf("ListVMsReply: %w", err)
}
o := m.Root()
if l := o.List(listVMsReplyVMsAt); l.Len() > 0 {
rows := make(InstalledVMs, l.Len())
for i := range rows {
if err := rows[i].UnmarshalZAP(l.BytesAt(i)); err != nil {
return err
}
}
x.VMs = rows
}
return nil
}
// ---- LoadArgs ----------------------------------------------------------
const (
@@ -398,6 +502,314 @@ func (x *LoadArgs) UnmarshalZAP(data []byte) error {
return nil
}
// ---- LoadVMsReply ------------------------------------------------------
const (
loadVMsReplyNewVMsAt = 0
loadVMsReplyFailedVMsAt = 8
loadVMsReplyChainsRetriedAt = 16
loadVMsReplySize = 24
)
var _ interface {
MarshalZAP() ([]byte, error)
UnmarshalZAP([]byte) error
} = (*LoadVMsReply)(nil)
// MarshalZAP writes LoadVMsReply from constant offsets.
func (x *LoadVMsReply) MarshalZAP() ([]byte, error) {
if x == nil {
return nil, nil
}
b := zap.NewBuilder(loadVMsReplySize + 256)
newVMsAt, newVMsN := 0, len(x.NewVMs)
if newVMsN > 0 {
var blob []byte
for i := range x.NewVMs {
enc, err := x.NewVMs[i].MarshalZAP()
if err != nil {
return nil, err
}
var n [4]byte
binary.LittleEndian.PutUint32(n[:], uint32(len(enc)))
blob = append(blob, n[:]...)
blob = append(blob, enc...)
}
newVMsAt = b.WriteBytes(blob)
}
failedVMsAt, failedVMsN := 0, len(x.FailedVMs)
if failedVMsN > 0 {
var blob []byte
for i := range x.FailedVMs {
enc, err := x.FailedVMs[i].MarshalZAP()
if err != nil {
return nil, err
}
var n [4]byte
binary.LittleEndian.PutUint32(n[:], uint32(len(enc)))
blob = append(blob, n[:]...)
blob = append(blob, enc...)
}
failedVMsAt = b.WriteBytes(blob)
}
ob := b.StartObject(loadVMsReplySize)
ob.SetInt64(loadVMsReplyChainsRetriedAt, int64(x.ChainsRetried))
if newVMsN > 0 {
ob.SetList(loadVMsReplyNewVMsAt, newVMsAt, newVMsN)
}
if failedVMsN > 0 {
ob.SetList(loadVMsReplyFailedVMsAt, failedVMsAt, failedVMsN)
}
ob.FinishAsRoot()
return b.Finish(), nil
}
// UnmarshalZAP reads LoadVMsReply out of the buffer that arrived.
func (x *LoadVMsReply) UnmarshalZAP(data []byte) error {
if x == nil || len(data) == 0 {
return nil
}
m, err := zap.Parse(data)
if err != nil {
return fmt.Errorf("LoadVMsReply: %w", err)
}
o := m.Root()
if l := o.List(loadVMsReplyNewVMsAt); l.Len() > 0 {
rows := make(LoadedVMs, l.Len())
for i := range rows {
if err := rows[i].UnmarshalZAP(l.BytesAt(i)); err != nil {
return err
}
}
x.NewVMs = rows
}
if l := o.List(loadVMsReplyFailedVMsAt); l.Len() > 0 {
rows := make(FailedVMs, l.Len())
for i := range rows {
if err := rows[i].UnmarshalZAP(l.BytesAt(i)); err != nil {
return err
}
}
x.FailedVMs = rows
}
x.ChainsRetried = int(o.Int64(loadVMsReplyChainsRetriedAt))
return nil
}
// ---- LoadedVM ----------------------------------------------------------
const (
loadedVMVMAt = 0
loadedVMAliasesAt = 32
loadedVMSize = 40
)
var _ interface {
MarshalZAP() ([]byte, error)
UnmarshalZAP([]byte) error
} = (*LoadedVM)(nil)
// MarshalZAP writes LoadedVM from constant offsets.
func (x *LoadedVM) MarshalZAP() ([]byte, error) {
if x == nil {
return nil, nil
}
b := zap.NewBuilder(loadedVMSize + 256)
aliasesAt, aliasesN := 0, len(x.Aliases)
if aliasesN > 0 {
var blob []byte
for i := range x.Aliases {
enc := []byte(x.Aliases[i])
var n [4]byte
binary.LittleEndian.PutUint32(n[:], uint32(len(enc)))
blob = append(blob, n[:]...)
blob = append(blob, enc...)
}
aliasesAt = b.WriteBytes(blob)
}
ob := b.StartObject(loadedVMSize)
ob.SetBytesFixed(loadedVMVMAt, x.VM[:])
if aliasesN > 0 {
ob.SetList(loadedVMAliasesAt, aliasesAt, aliasesN)
}
ob.FinishAsRoot()
return b.Finish(), nil
}
// UnmarshalZAP reads LoadedVM out of the buffer that arrived.
func (x *LoadedVM) UnmarshalZAP(data []byte) error {
if x == nil || len(data) == 0 {
return nil
}
m, err := zap.Parse(data)
if err != nil {
return fmt.Errorf("LoadedVM: %w", err)
}
o := m.Root()
copy(x.VM[:], o.BytesFixed(loadedVMVMAt, 32))
if l := o.List(loadedVMAliasesAt); l.Len() > 0 {
rows := make([]string, l.Len())
for i := range rows {
rows[i] = string(l.BytesAt(i))
}
x.Aliases = rows
}
return nil
}
// ---- LogAndDisplayLevels -----------------------------------------------
const (
logAndDisplayLevelsLogLevelAt = 0
logAndDisplayLevelsDisplayLevelAt = 8
logAndDisplayLevelsSize = 16
)
var _ interface {
MarshalZAP() ([]byte, error)
UnmarshalZAP([]byte) error
} = (*LogAndDisplayLevels)(nil)
// MarshalZAP writes LogAndDisplayLevels from constant offsets.
func (x *LogAndDisplayLevels) MarshalZAP() ([]byte, error) {
if x == nil {
return nil, nil
}
b := zap.NewBuilder(logAndDisplayLevelsSize + 256)
ob := b.StartObject(logAndDisplayLevelsSize)
ob.SetText(logAndDisplayLevelsLogLevelAt, string(x.LogLevel))
ob.SetText(logAndDisplayLevelsDisplayLevelAt, string(x.DisplayLevel))
ob.FinishAsRoot()
return b.Finish(), nil
}
// UnmarshalZAP reads LogAndDisplayLevels out of the buffer that arrived.
func (x *LogAndDisplayLevels) UnmarshalZAP(data []byte) error {
if x == nil || len(data) == 0 {
return nil
}
m, err := zap.Parse(data)
if err != nil {
return fmt.Errorf("LogAndDisplayLevels: %w", err)
}
o := m.Root()
x.LogLevel = string(strings.Clone(o.Text(logAndDisplayLevelsLogLevelAt)))
x.DisplayLevel = string(strings.Clone(o.Text(logAndDisplayLevelsDisplayLevelAt)))
return nil
}
// ---- LoggerLevel -------------------------------------------------------
const (
loggerLevelLoggerAt = 0
loggerLevelLevelsAt = 8
loggerLevelSize = 16
)
var _ interface {
MarshalZAP() ([]byte, error)
UnmarshalZAP([]byte) error
} = (*LoggerLevel)(nil)
// MarshalZAP writes LoggerLevel from constant offsets.
func (x *LoggerLevel) MarshalZAP() ([]byte, error) {
if x == nil {
return nil, nil
}
b := zap.NewBuilder(loggerLevelSize + 256)
ob := b.StartObject(loggerLevelSize)
ob.SetText(loggerLevelLoggerAt, string(x.Logger))
innerLevels, err := x.Levels.MarshalZAP()
if err != nil {
return nil, err
}
ob.SetBytes(loggerLevelLevelsAt, innerLevels)
ob.FinishAsRoot()
return b.Finish(), nil
}
// UnmarshalZAP reads LoggerLevel out of the buffer that arrived.
func (x *LoggerLevel) UnmarshalZAP(data []byte) error {
if x == nil || len(data) == 0 {
return nil
}
m, err := zap.Parse(data)
if err != nil {
return fmt.Errorf("LoggerLevel: %w", err)
}
o := m.Root()
x.Logger = string(strings.Clone(o.Text(loggerLevelLoggerAt)))
if raw := o.Bytes(loggerLevelLevelsAt); len(raw) > 0 {
if err := x.Levels.UnmarshalZAP(raw); err != nil {
return err
}
}
return nil
}
// ---- LoggerLevelReply --------------------------------------------------
const (
loggerLevelReplyLoggerLevelsAt = 0
loggerLevelReplySize = 8
)
var _ interface {
MarshalZAP() ([]byte, error)
UnmarshalZAP([]byte) error
} = (*LoggerLevelReply)(nil)
// MarshalZAP writes LoggerLevelReply from constant offsets.
func (x *LoggerLevelReply) MarshalZAP() ([]byte, error) {
if x == nil {
return nil, nil
}
b := zap.NewBuilder(loggerLevelReplySize + 256)
loggerLevelsAt, loggerLevelsN := 0, len(x.LoggerLevels)
if loggerLevelsN > 0 {
var blob []byte
for i := range x.LoggerLevels {
enc, err := x.LoggerLevels[i].MarshalZAP()
if err != nil {
return nil, err
}
var n [4]byte
binary.LittleEndian.PutUint32(n[:], uint32(len(enc)))
blob = append(blob, n[:]...)
blob = append(blob, enc...)
}
loggerLevelsAt = b.WriteBytes(blob)
}
ob := b.StartObject(loggerLevelReplySize)
if loggerLevelsN > 0 {
ob.SetList(loggerLevelReplyLoggerLevelsAt, loggerLevelsAt, loggerLevelsN)
}
ob.FinishAsRoot()
return b.Finish(), nil
}
// UnmarshalZAP reads LoggerLevelReply out of the buffer that arrived.
func (x *LoggerLevelReply) UnmarshalZAP(data []byte) error {
if x == nil || len(data) == 0 {
return nil
}
m, err := zap.Parse(data)
if err != nil {
return fmt.Errorf("LoggerLevelReply: %w", err)
}
o := m.Root()
if l := o.List(loggerLevelReplyLoggerLevelsAt); l.Len() > 0 {
rows := make(LoggerLevels, l.Len())
for i := range rows {
if err := rows[i].UnmarshalZAP(l.BytesAt(i)); err != nil {
return err
}
}
x.LoggerLevels = rows
}
return nil
}
// ---- SetLoggerLevelArgs ------------------------------------------------
const (
@@ -636,3 +1048,67 @@ func (x *SnapshotReply) UnmarshalZAP(data []byte) error {
x.Version = uint64(o.Uint64(snapshotReplyVersionAt))
return nil
}
// ---- VMInfo ------------------------------------------------------------
const (
vMInfoIDAt = 0
vMInfoAliasesAt = 8
vMInfoPathAt = 16
vMInfoSize = 24
)
var _ interface {
MarshalZAP() ([]byte, error)
UnmarshalZAP([]byte) error
} = (*VMInfo)(nil)
// MarshalZAP writes VMInfo from constant offsets.
func (x *VMInfo) MarshalZAP() ([]byte, error) {
if x == nil {
return nil, nil
}
b := zap.NewBuilder(vMInfoSize + 256)
aliasesAt, aliasesN := 0, len(x.Aliases)
if aliasesN > 0 {
var blob []byte
for i := range x.Aliases {
enc := []byte(x.Aliases[i])
var n [4]byte
binary.LittleEndian.PutUint32(n[:], uint32(len(enc)))
blob = append(blob, n[:]...)
blob = append(blob, enc...)
}
aliasesAt = b.WriteBytes(blob)
}
ob := b.StartObject(vMInfoSize)
ob.SetText(vMInfoIDAt, string(x.ID))
ob.SetText(vMInfoPathAt, string(x.Path))
if aliasesN > 0 {
ob.SetList(vMInfoAliasesAt, aliasesAt, aliasesN)
}
ob.FinishAsRoot()
return b.Finish(), nil
}
// UnmarshalZAP reads VMInfo out of the buffer that arrived.
func (x *VMInfo) UnmarshalZAP(data []byte) error {
if x == nil || len(data) == 0 {
return nil
}
m, err := zap.Parse(data)
if err != nil {
return fmt.Errorf("VMInfo: %w", err)
}
o := m.Root()
x.ID = string(strings.Clone(o.Text(vMInfoIDAt)))
if l := o.List(vMInfoAliasesAt); l.Len() > 0 {
rows := make([]string, l.Len())
for i := range rows {
rows[i] = string(l.BytesAt(i))
}
x.Aliases = rows
}
x.Path = string(strings.Clone(o.Text(vMInfoPathAt)))
return nil
}
+94 -9
View File
@@ -1,27 +1,112 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Whether a node is up, ready, and well.
//
// TWO SHAPES HERE COULD NOT CROSS BETWEEN TWO PROCESSES and now do. The checks
// were a map, and a ZAP field is an offset a map has no order to give; they are
// a list ordered by check name. And a check's details were an `any`, which has
// no shape until it holds something — the disk check answers with an object, the
// BLS check with a string, and a chain's check with whatever it likes — so the
// details cross as the bytes of their own JSON, which is the one shape they all
// share. Neither change is visible to a caller: the list marshals to the object
// it replaced, and the raw bytes are written out exactly as they were produced.
package health
import "time"
import (
"encoding/json"
"slices"
"strings"
"time"
"github.com/luxfi/api/types"
)
// APIReply is the response for Readiness, Health, and Liveness.
type APIReply struct {
Checks map[string]Result `json:"checks"`
Healthy bool `json:"healthy"`
// Checks is what every check answered, ordered by name.
Checks Checks `json:"checks"`
// Healthy is whether every one of them passed.
Healthy bool `json:"healthy"`
}
// StatusCode is the HTTP status this answer carries: 200 when every check
// passed, 503 when one did not.
//
// It rides the value rather than being set beside it so the code a probe reads
// and the `healthy` a person reads are the same fact computed once. A caller
// that never speaks HTTP is unaffected — the value is identical on every wire.
func (r APIReply) StatusCode() int {
if r.Healthy {
return 200
}
return 503
}
// APIArgs is the arguments for Readiness, Health, and Liveness.
type APIArgs struct {
// Tags narrows the answer to checks carrying one of these tags. Empty asks
// for all of them.
Tags []string `json:"tags"`
}
// Check is one named check and what it answered.
type Check struct {
// Name is the check's name, the key it appears under on the JSON wire.
Name string `json:"name"`
// Result is what it answered.
Result Result `json:"result"`
}
// Checks is what every check answered: an object keyed by check name on the
// JSON wire, a list ordered by that name here.
type Checks []Check
func (c Checks) MarshalJSON() ([]byte, error) {
m := make(map[string]Result, len(c))
for _, e := range c {
m[e.Name] = e.Result
}
return json.Marshal(m)
}
func (c *Checks) UnmarshalJSON(b []byte) error {
var m map[string]Result
if err := json.Unmarshal(b, &m); err != nil {
return err
}
*c = make(Checks, 0, len(m))
for name, result := range m {
*c = append(*c, Check{Name: name, Result: result})
}
slices.SortFunc(*c, func(x, y Check) int { return strings.Compare(x.Name, y.Name) })
return nil
}
// Find is one check's result, and whether the reply carries it.
func (c Checks) Find(name string) (*Check, bool) {
for i := range c {
if c[i].Name == name {
return &c[i], true
}
}
return nil, false
}
// Result describes a health check result.
type Result struct {
Details interface{} `json:"message,omitempty"`
Error *string `json:"error,omitempty"`
Timestamp time.Time `json:"timestamp,omitempty"`
Duration time.Duration `json:"duration"`
ContiguousFailures int64 `json:"contiguousFailures,omitempty"`
TimeOfFirstFailure *time.Time `json:"timeOfFirstFailure,omitempty"`
// Details is whatever the check chose to say, as the JSON it said it in.
// A check answers with an object, a string or a list as it sees fit, so
// there is no struct to name and the bytes are the honest shape.
Details types.Raw `json:"message,omitempty"`
// Error is why the check failed, when it did.
Error *string `json:"error,omitempty"`
// Timestamp is when the check last ran.
Timestamp types.Time `json:"timestamp,omitempty"`
// Duration is how long it took.
Duration time.Duration `json:"duration"`
// ContiguousFailures is how many times running it has failed.
ContiguousFailures int64 `json:"contiguousFailures,omitempty"`
// TimeOfFirstFailure is when the current run of failures began.
TimeOfFirstFailure *types.Time `json:"timeOfFirstFailure,omitempty"`
}
+195
View File
@@ -5,7 +5,10 @@ package health
import (
"encoding/binary"
"fmt"
"strings"
"time"
types "github.com/luxfi/api/types"
zap "github.com/zap-proto/go"
)
@@ -66,3 +69,195 @@ func (x *APIArgs) UnmarshalZAP(data []byte) error {
}
return nil
}
// ---- APIReply ----------------------------------------------------------
const (
aPIReplyChecksAt = 0
aPIReplyHealthyAt = 8
aPIReplySize = 16
)
var _ interface {
MarshalZAP() ([]byte, error)
UnmarshalZAP([]byte) error
} = (*APIReply)(nil)
// MarshalZAP writes APIReply from constant offsets.
func (x *APIReply) MarshalZAP() ([]byte, error) {
if x == nil {
return nil, nil
}
b := zap.NewBuilder(aPIReplySize + 256)
checksAt, checksN := 0, len(x.Checks)
if checksN > 0 {
var blob []byte
for i := range x.Checks {
enc, err := x.Checks[i].MarshalZAP()
if err != nil {
return nil, err
}
var n [4]byte
binary.LittleEndian.PutUint32(n[:], uint32(len(enc)))
blob = append(blob, n[:]...)
blob = append(blob, enc...)
}
checksAt = b.WriteBytes(blob)
}
ob := b.StartObject(aPIReplySize)
ob.SetBool(aPIReplyHealthyAt, bool(x.Healthy))
if checksN > 0 {
ob.SetList(aPIReplyChecksAt, checksAt, checksN)
}
ob.FinishAsRoot()
return b.Finish(), nil
}
// UnmarshalZAP reads APIReply out of the buffer that arrived.
func (x *APIReply) UnmarshalZAP(data []byte) error {
if x == nil || len(data) == 0 {
return nil
}
m, err := zap.Parse(data)
if err != nil {
return fmt.Errorf("APIReply: %w", err)
}
o := m.Root()
if l := o.List(aPIReplyChecksAt); l.Len() > 0 {
rows := make(Checks, l.Len())
for i := range rows {
if err := rows[i].UnmarshalZAP(l.BytesAt(i)); err != nil {
return err
}
}
x.Checks = rows
}
x.Healthy = bool(o.Bool(aPIReplyHealthyAt))
return nil
}
// ---- Check -------------------------------------------------------------
const (
checkNameAt = 0
checkResultAt = 8
checkSize = 16
)
var _ interface {
MarshalZAP() ([]byte, error)
UnmarshalZAP([]byte) error
} = (*Check)(nil)
// MarshalZAP writes Check from constant offsets.
func (x *Check) MarshalZAP() ([]byte, error) {
if x == nil {
return nil, nil
}
b := zap.NewBuilder(checkSize + 256)
ob := b.StartObject(checkSize)
ob.SetText(checkNameAt, string(x.Name))
innerResult, err := x.Result.MarshalZAP()
if err != nil {
return nil, err
}
ob.SetBytes(checkResultAt, innerResult)
ob.FinishAsRoot()
return b.Finish(), nil
}
// UnmarshalZAP reads Check out of the buffer that arrived.
func (x *Check) UnmarshalZAP(data []byte) error {
if x == nil || len(data) == 0 {
return nil
}
m, err := zap.Parse(data)
if err != nil {
return fmt.Errorf("Check: %w", err)
}
o := m.Root()
x.Name = string(strings.Clone(o.Text(checkNameAt)))
if raw := o.Bytes(checkResultAt); len(raw) > 0 {
if err := x.Result.UnmarshalZAP(raw); err != nil {
return err
}
}
return nil
}
// ---- Result ------------------------------------------------------------
const (
resultDetailsAt = 0
resultErrorAt = 8
resultTimestampAt = 16
resultDurationAt = 24
resultContiguousFailuresAt = 32
resultTimeOfFirstFailureAt = 40
resultSize = 48
)
var _ interface {
MarshalZAP() ([]byte, error)
UnmarshalZAP([]byte) error
} = (*Result)(nil)
// MarshalZAP writes Result from constant offsets.
func (x *Result) MarshalZAP() ([]byte, error) {
if x == nil {
return nil, nil
}
b := zap.NewBuilder(resultSize + 256)
ob := b.StartObject(resultSize)
ob.SetBytes(resultDetailsAt, []byte(x.Details))
if x.Error != nil {
ob.SetText(resultErrorAt, string((*x.Error)))
}
innerTimestamp, err := x.Timestamp.MarshalZAP()
if err != nil {
return nil, err
}
ob.SetBytes(resultTimestampAt, innerTimestamp)
ob.SetInt64(resultDurationAt, int64(x.Duration))
ob.SetInt64(resultContiguousFailuresAt, int64(x.ContiguousFailures))
if x.TimeOfFirstFailure != nil {
innerTimeOfFirstFailure, err := x.TimeOfFirstFailure.MarshalZAP()
if err != nil {
return nil, err
}
ob.SetBytes(resultTimeOfFirstFailureAt, innerTimeOfFirstFailure)
}
ob.FinishAsRoot()
return b.Finish(), nil
}
// UnmarshalZAP reads Result out of the buffer that arrived.
func (x *Result) UnmarshalZAP(data []byte) error {
if x == nil || len(data) == 0 {
return nil
}
m, err := zap.Parse(data)
if err != nil {
return fmt.Errorf("Result: %w", err)
}
o := m.Root()
x.Details = types.Raw(append([]byte(nil), o.Bytes(resultDetailsAt)...))
if v := string(strings.Clone(o.Text(resultErrorAt))); v != "" {
x.Error = &v
}
if raw := o.Bytes(resultTimestampAt); len(raw) > 0 {
if err := x.Timestamp.UnmarshalZAP(raw); err != nil {
return err
}
}
x.Duration = time.Duration(o.Int64(resultDurationAt))
x.ContiguousFailures = int64(o.Int64(resultContiguousFailuresAt))
if raw := o.Bytes(resultTimeOfFirstFailureAt); len(raw) > 0 {
var v types.Time
if err := v.UnmarshalZAP(raw); err != nil {
return err
}
x.TimeOfFirstFailure = &v
}
return nil
}
+216 -27
View File
@@ -1,15 +1,24 @@
// 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 (
"net/netip"
"time"
"encoding/json"
"slices"
"github.com/luxfi/api/types"
"github.com/luxfi/ids"
"github.com/luxfi/math/set"
)
// PeerInfo is the JSON wire shape for a peer in the Peers RPC reply. It is
@@ -18,16 +27,26 @@ import (
// LP-201. Field tags are stable across the rename — JSON consumers see no
// difference.
type PeerInfo struct {
IP netip.AddrPort `json:"ip"`
PublicIP netip.AddrPort `json:"publicIP,omitempty"`
ID ids.NodeID `json:"nodeID"`
Version string `json:"version"`
LastSent time.Time `json:"lastSent"`
LastReceived time.Time `json:"lastReceived"`
ObservedUptime types.Uint32 `json:"observedUptime"`
TrackedChains set.Set[ids.ID] `json:"trackedChains"`
SupportedLPs set.Set[uint32] `json:"supportedLPs"`
ObjectedLPs set.Set[uint32] `json:"objectedLPs"`
// 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.
@@ -38,17 +57,50 @@ type ProofOfPossession struct {
// 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 map[string]string `json:"vmVersions"`
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.
@@ -81,7 +133,8 @@ type GetNetworkIDReply struct {
// GetNodeIPReply are the results from calling GetNodeIP.
type GetNodeIPReply struct {
IP netip.AddrPort `json:"ip"`
// IP is the address this node tells peers to reach it at.
IP types.Addr `json:"ip"`
}
// GetNetworkNameReply is the result from calling GetNetworkName.
@@ -91,6 +144,7 @@ type GetNetworkNameReply struct {
// 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"`
}
@@ -101,6 +155,7 @@ type GetBlockchainIDReply struct {
// 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"`
}
@@ -119,6 +174,7 @@ type PeersReply struct {
// 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"`
}
@@ -135,16 +191,61 @@ type UptimeResponse struct {
// LP is information about an LP proposal.
type LP struct {
SupportWeight types.Uint64 `json:"supportWeight"`
Supporters set.Set[ids.NodeID] `json:"supporters"`
ObjectWeight types.Uint64 `json:"objectWeight"`
Objectors set.Set[ids.NodeID] `json:"objectors"`
AbstainWeight types.Uint64 `json:"abstainWeight"`
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 map[uint32]*LP `json:"lps"`
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.
@@ -158,8 +259,96 @@ type GetTxFeeResponse struct {
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 map[ids.ID][]string `json:"vms"`
Fxs map[ids.ID]string `json:"fxs"`
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
}
+863 -6
View File
@@ -12,6 +12,97 @@ import (
zap "github.com/zap-proto/go"
)
// ---- ConsensusInfo -----------------------------------------------------
const (
consensusInfoModeAt = 0
consensusInfoBLSAt = 8
consensusInfoCoronaAt = 9
consensusInfoMLDSAAt = 10
consensusInfoPlatformVMAt = 11
consensusInfoSize = 16
)
var _ interface {
MarshalZAP() ([]byte, error)
UnmarshalZAP([]byte) error
} = (*ConsensusInfo)(nil)
// MarshalZAP writes ConsensusInfo from constant offsets.
func (x *ConsensusInfo) MarshalZAP() ([]byte, error) {
if x == nil {
return nil, nil
}
b := zap.NewBuilder(consensusInfoSize + 256)
ob := b.StartObject(consensusInfoSize)
ob.SetText(consensusInfoModeAt, string(x.Mode))
ob.SetBool(consensusInfoBLSAt, bool(x.BLS))
ob.SetBool(consensusInfoCoronaAt, bool(x.Corona))
ob.SetBool(consensusInfoMLDSAAt, bool(x.MLDSA))
ob.SetBool(consensusInfoPlatformVMAt, bool(x.PlatformVM))
ob.FinishAsRoot()
return b.Finish(), nil
}
// UnmarshalZAP reads ConsensusInfo out of the buffer that arrived.
func (x *ConsensusInfo) UnmarshalZAP(data []byte) error {
if x == nil || len(data) == 0 {
return nil
}
m, err := zap.Parse(data)
if err != nil {
return fmt.Errorf("ConsensusInfo: %w", err)
}
o := m.Root()
x.Mode = string(strings.Clone(o.Text(consensusInfoModeAt)))
x.BLS = bool(o.Bool(consensusInfoBLSAt))
x.Corona = bool(o.Bool(consensusInfoCoronaAt))
x.MLDSA = bool(o.Bool(consensusInfoMLDSAAt))
x.PlatformVM = bool(o.Bool(consensusInfoPlatformVMAt))
return nil
}
// ---- FxName ------------------------------------------------------------
const (
fxNameFxAt = 0
fxNameNameAt = 32
fxNameSize = 40
)
var _ interface {
MarshalZAP() ([]byte, error)
UnmarshalZAP([]byte) error
} = (*FxName)(nil)
// MarshalZAP writes FxName from constant offsets.
func (x *FxName) MarshalZAP() ([]byte, error) {
if x == nil {
return nil, nil
}
b := zap.NewBuilder(fxNameSize + 256)
ob := b.StartObject(fxNameSize)
ob.SetBytesFixed(fxNameFxAt, x.Fx[:])
ob.SetText(fxNameNameAt, string(x.Name))
ob.FinishAsRoot()
return b.Finish(), nil
}
// UnmarshalZAP reads FxName out of the buffer that arrived.
func (x *FxName) UnmarshalZAP(data []byte) error {
if x == nil || len(data) == 0 {
return nil
}
m, err := zap.Parse(data)
if err != nil {
return fmt.Errorf("FxName: %w", err)
}
o := m.Root()
copy(x.Fx[:], o.BytesFixed(fxNameFxAt, 32))
x.Name = string(strings.Clone(o.Text(fxNameNameAt)))
return nil
}
// ---- GetBlockchainIDArgs -----------------------------------------------
const (
@@ -236,11 +327,7 @@ func (x *GetNodeIPReply) MarshalZAP() ([]byte, error) {
}
b := zap.NewBuilder(getNodeIPReplySize + 256)
ob := b.StartObject(getNodeIPReplySize)
{
eb := zap.NewBuilder(zap.HeaderSize)
eb.StartObject(0).FinishAsRoot()
ob.SetBytes(getNodeIPReplyIPAt, eb.Finish())
}
ob.SetText(getNodeIPReplyIPAt, string(x.IP))
ob.FinishAsRoot()
return b.Finish(), nil
}
@@ -254,7 +341,98 @@ func (x *GetNodeIPReply) UnmarshalZAP(data []byte) error {
if err != nil {
return fmt.Errorf("GetNodeIPReply: %w", err)
}
_ = m
o := m.Root()
x.IP = types.Addr(strings.Clone(o.Text(getNodeIPReplyIPAt)))
return nil
}
// ---- GetNodeVersionReply -----------------------------------------------
const (
getNodeVersionReplyVersionAt = 0
getNodeVersionReplyDatabaseVersionAt = 8
getNodeVersionReplyRPCProtocolVersionAt = 16
getNodeVersionReplyGitCommitAt = 24
getNodeVersionReplyVMVersionsAt = 32
getNodeVersionReplyConsensusAt = 40
getNodeVersionReplySize = 48
)
var _ interface {
MarshalZAP() ([]byte, error)
UnmarshalZAP([]byte) error
} = (*GetNodeVersionReply)(nil)
// MarshalZAP writes GetNodeVersionReply from constant offsets.
func (x *GetNodeVersionReply) MarshalZAP() ([]byte, error) {
if x == nil {
return nil, nil
}
b := zap.NewBuilder(getNodeVersionReplySize + 256)
vMVersionsAt, vMVersionsN := 0, len(x.VMVersions)
if vMVersionsN > 0 {
var blob []byte
for i := range x.VMVersions {
enc, err := x.VMVersions[i].MarshalZAP()
if err != nil {
return nil, err
}
var n [4]byte
binary.LittleEndian.PutUint32(n[:], uint32(len(enc)))
blob = append(blob, n[:]...)
blob = append(blob, enc...)
}
vMVersionsAt = b.WriteBytes(blob)
}
ob := b.StartObject(getNodeVersionReplySize)
ob.SetText(getNodeVersionReplyVersionAt, string(x.Version))
ob.SetText(getNodeVersionReplyDatabaseVersionAt, string(x.DatabaseVersion))
ob.SetUint32(getNodeVersionReplyRPCProtocolVersionAt, uint32(x.RPCProtocolVersion))
ob.SetText(getNodeVersionReplyGitCommitAt, string(x.GitCommit))
if x.Consensus != nil {
innerConsensus, err := x.Consensus.MarshalZAP()
if err != nil {
return nil, err
}
ob.SetBytes(getNodeVersionReplyConsensusAt, innerConsensus)
}
if vMVersionsN > 0 {
ob.SetList(getNodeVersionReplyVMVersionsAt, vMVersionsAt, vMVersionsN)
}
ob.FinishAsRoot()
return b.Finish(), nil
}
// UnmarshalZAP reads GetNodeVersionReply out of the buffer that arrived.
func (x *GetNodeVersionReply) UnmarshalZAP(data []byte) error {
if x == nil || len(data) == 0 {
return nil
}
m, err := zap.Parse(data)
if err != nil {
return fmt.Errorf("GetNodeVersionReply: %w", err)
}
o := m.Root()
x.Version = string(strings.Clone(o.Text(getNodeVersionReplyVersionAt)))
x.DatabaseVersion = string(strings.Clone(o.Text(getNodeVersionReplyDatabaseVersionAt)))
x.RPCProtocolVersion = types.Uint32(o.Uint32(getNodeVersionReplyRPCProtocolVersionAt))
x.GitCommit = string(strings.Clone(o.Text(getNodeVersionReplyGitCommitAt)))
if l := o.List(getNodeVersionReplyVMVersionsAt); l.Len() > 0 {
rows := make(VMVersions, l.Len())
for i := range rows {
if err := rows[i].UnmarshalZAP(l.BytesAt(i)); err != nil {
return err
}
}
x.VMVersions = rows
}
if raw := o.Bytes(getNodeVersionReplyConsensusAt); len(raw) > 0 {
var v ConsensusInfo
if err := v.UnmarshalZAP(raw); err != nil {
return err
}
x.Consensus = &v
}
return nil
}
@@ -314,6 +492,97 @@ func (x *GetTxFeeResponse) UnmarshalZAP(data []byte) error {
return nil
}
// ---- GetVMsReply -------------------------------------------------------
const (
getVMsReplyVMsAt = 0
getVMsReplyFxsAt = 8
getVMsReplySize = 16
)
var _ interface {
MarshalZAP() ([]byte, error)
UnmarshalZAP([]byte) error
} = (*GetVMsReply)(nil)
// MarshalZAP writes GetVMsReply from constant offsets.
func (x *GetVMsReply) MarshalZAP() ([]byte, error) {
if x == nil {
return nil, nil
}
b := zap.NewBuilder(getVMsReplySize + 256)
vMsAt, vMsN := 0, len(x.VMs)
if vMsN > 0 {
var blob []byte
for i := range x.VMs {
enc, err := x.VMs[i].MarshalZAP()
if err != nil {
return nil, err
}
var n [4]byte
binary.LittleEndian.PutUint32(n[:], uint32(len(enc)))
blob = append(blob, n[:]...)
blob = append(blob, enc...)
}
vMsAt = b.WriteBytes(blob)
}
fxsAt, fxsN := 0, len(x.Fxs)
if fxsN > 0 {
var blob []byte
for i := range x.Fxs {
enc, err := x.Fxs[i].MarshalZAP()
if err != nil {
return nil, err
}
var n [4]byte
binary.LittleEndian.PutUint32(n[:], uint32(len(enc)))
blob = append(blob, n[:]...)
blob = append(blob, enc...)
}
fxsAt = b.WriteBytes(blob)
}
ob := b.StartObject(getVMsReplySize)
if vMsN > 0 {
ob.SetList(getVMsReplyVMsAt, vMsAt, vMsN)
}
if fxsN > 0 {
ob.SetList(getVMsReplyFxsAt, fxsAt, fxsN)
}
ob.FinishAsRoot()
return b.Finish(), nil
}
// UnmarshalZAP reads GetVMsReply out of the buffer that arrived.
func (x *GetVMsReply) UnmarshalZAP(data []byte) error {
if x == nil || len(data) == 0 {
return nil
}
m, err := zap.Parse(data)
if err != nil {
return fmt.Errorf("GetVMsReply: %w", err)
}
o := m.Root()
if l := o.List(getVMsReplyVMsAt); l.Len() > 0 {
rows := make(VMAliases, l.Len())
for i := range rows {
if err := rows[i].UnmarshalZAP(l.BytesAt(i)); err != nil {
return err
}
}
x.VMs = rows
}
if l := o.List(getVMsReplyFxsAt); l.Len() > 0 {
rows := make(FxNames, l.Len())
for i := range rows {
if err := rows[i].UnmarshalZAP(l.BytesAt(i)); err != nil {
return err
}
}
x.Fxs = rows
}
return nil
}
// ---- IsBootstrappedArgs ------------------------------------------------
const (
@@ -390,6 +659,426 @@ func (x *IsBootstrappedResponse) UnmarshalZAP(data []byte) error {
return nil
}
// ---- LP ----------------------------------------------------------------
const (
lPSupportWeightAt = 0
lPSupportersAt = 8
lPObjectWeightAt = 16
lPObjectorsAt = 24
lPAbstainWeightAt = 32
lPSize = 40
)
var _ interface {
MarshalZAP() ([]byte, error)
UnmarshalZAP([]byte) error
} = (*LP)(nil)
// MarshalZAP writes LP from constant offsets.
func (x *LP) MarshalZAP() ([]byte, error) {
if x == nil {
return nil, nil
}
b := zap.NewBuilder(lPSize + 256)
supportersAt, supportersN := 0, len(x.Supporters)
if supportersN > 0 {
var blob []byte
for i := range x.Supporters {
enc := x.Supporters[i][:]
var n [4]byte
binary.LittleEndian.PutUint32(n[:], uint32(len(enc)))
blob = append(blob, n[:]...)
blob = append(blob, enc...)
}
supportersAt = b.WriteBytes(blob)
}
objectorsAt, objectorsN := 0, len(x.Objectors)
if objectorsN > 0 {
var blob []byte
for i := range x.Objectors {
enc := x.Objectors[i][:]
var n [4]byte
binary.LittleEndian.PutUint32(n[:], uint32(len(enc)))
blob = append(blob, n[:]...)
blob = append(blob, enc...)
}
objectorsAt = b.WriteBytes(blob)
}
ob := b.StartObject(lPSize)
ob.SetUint64(lPSupportWeightAt, uint64(x.SupportWeight))
ob.SetUint64(lPObjectWeightAt, uint64(x.ObjectWeight))
ob.SetUint64(lPAbstainWeightAt, uint64(x.AbstainWeight))
if supportersN > 0 {
ob.SetList(lPSupportersAt, supportersAt, supportersN)
}
if objectorsN > 0 {
ob.SetList(lPObjectorsAt, objectorsAt, objectorsN)
}
ob.FinishAsRoot()
return b.Finish(), nil
}
// UnmarshalZAP reads LP out of the buffer that arrived.
func (x *LP) UnmarshalZAP(data []byte) error {
if x == nil || len(data) == 0 {
return nil
}
m, err := zap.Parse(data)
if err != nil {
return fmt.Errorf("LP: %w", err)
}
o := m.Root()
x.SupportWeight = types.Uint64(o.Uint64(lPSupportWeightAt))
if l := o.List(lPSupportersAt); l.Len() > 0 {
rows := make([]ids.NodeID, l.Len())
for i := range rows {
copy(rows[i][:], l.BytesAt(i))
}
x.Supporters = rows
}
x.ObjectWeight = types.Uint64(o.Uint64(lPObjectWeightAt))
if l := o.List(lPObjectorsAt); l.Len() > 0 {
rows := make([]ids.NodeID, l.Len())
for i := range rows {
copy(rows[i][:], l.BytesAt(i))
}
x.Objectors = rows
}
x.AbstainWeight = types.Uint64(o.Uint64(lPAbstainWeightAt))
return nil
}
// ---- LPStatus ----------------------------------------------------------
const (
lPStatusNumberAt = 0
lPStatusLPAt = 8
lPStatusSize = 16
)
var _ interface {
MarshalZAP() ([]byte, error)
UnmarshalZAP([]byte) error
} = (*LPStatus)(nil)
// MarshalZAP writes LPStatus from constant offsets.
func (x *LPStatus) MarshalZAP() ([]byte, error) {
if x == nil {
return nil, nil
}
b := zap.NewBuilder(lPStatusSize + 256)
ob := b.StartObject(lPStatusSize)
ob.SetUint32(lPStatusNumberAt, uint32(x.Number))
innerLP, err := x.LP.MarshalZAP()
if err != nil {
return nil, err
}
ob.SetBytes(lPStatusLPAt, innerLP)
ob.FinishAsRoot()
return b.Finish(), nil
}
// UnmarshalZAP reads LPStatus out of the buffer that arrived.
func (x *LPStatus) UnmarshalZAP(data []byte) error {
if x == nil || len(data) == 0 {
return nil
}
m, err := zap.Parse(data)
if err != nil {
return fmt.Errorf("LPStatus: %w", err)
}
o := m.Root()
x.Number = uint32(o.Uint32(lPStatusNumberAt))
if raw := o.Bytes(lPStatusLPAt); len(raw) > 0 {
if err := x.LP.UnmarshalZAP(raw); err != nil {
return err
}
}
return nil
}
// ---- LPsReply ----------------------------------------------------------
const (
lPsReplyLPsAt = 0
lPsReplySize = 8
)
var _ interface {
MarshalZAP() ([]byte, error)
UnmarshalZAP([]byte) error
} = (*LPsReply)(nil)
// MarshalZAP writes LPsReply from constant offsets.
func (x *LPsReply) MarshalZAP() ([]byte, error) {
if x == nil {
return nil, nil
}
b := zap.NewBuilder(lPsReplySize + 256)
lPsAt, lPsN := 0, len(x.LPs)
if lPsN > 0 {
var blob []byte
for i := range x.LPs {
enc, err := x.LPs[i].MarshalZAP()
if err != nil {
return nil, err
}
var n [4]byte
binary.LittleEndian.PutUint32(n[:], uint32(len(enc)))
blob = append(blob, n[:]...)
blob = append(blob, enc...)
}
lPsAt = b.WriteBytes(blob)
}
ob := b.StartObject(lPsReplySize)
if lPsN > 0 {
ob.SetList(lPsReplyLPsAt, lPsAt, lPsN)
}
ob.FinishAsRoot()
return b.Finish(), nil
}
// UnmarshalZAP reads LPsReply out of the buffer that arrived.
func (x *LPsReply) UnmarshalZAP(data []byte) error {
if x == nil || len(data) == 0 {
return nil
}
m, err := zap.Parse(data)
if err != nil {
return fmt.Errorf("LPsReply: %w", err)
}
o := m.Root()
if l := o.List(lPsReplyLPsAt); l.Len() > 0 {
rows := make(LPs, l.Len())
for i := range rows {
if err := rows[i].UnmarshalZAP(l.BytesAt(i)); err != nil {
return err
}
}
x.LPs = rows
}
return nil
}
// ---- Peer --------------------------------------------------------------
const (
peerPeerInfoAt = 0
peerBenchedAt = 8
peerSize = 16
)
var _ interface {
MarshalZAP() ([]byte, error)
UnmarshalZAP([]byte) error
} = (*Peer)(nil)
// MarshalZAP writes Peer from constant offsets.
func (x *Peer) MarshalZAP() ([]byte, error) {
if x == nil {
return nil, nil
}
b := zap.NewBuilder(peerSize + 256)
benchedAt, benchedN := 0, len(x.Benched)
if benchedN > 0 {
var blob []byte
for i := range x.Benched {
enc := []byte(x.Benched[i])
var n [4]byte
binary.LittleEndian.PutUint32(n[:], uint32(len(enc)))
blob = append(blob, n[:]...)
blob = append(blob, enc...)
}
benchedAt = b.WriteBytes(blob)
}
ob := b.StartObject(peerSize)
innerPeerInfo, err := x.PeerInfo.MarshalZAP()
if err != nil {
return nil, err
}
ob.SetBytes(peerPeerInfoAt, innerPeerInfo)
if benchedN > 0 {
ob.SetList(peerBenchedAt, benchedAt, benchedN)
}
ob.FinishAsRoot()
return b.Finish(), nil
}
// UnmarshalZAP reads Peer out of the buffer that arrived.
func (x *Peer) UnmarshalZAP(data []byte) error {
if x == nil || len(data) == 0 {
return nil
}
m, err := zap.Parse(data)
if err != nil {
return fmt.Errorf("Peer: %w", err)
}
o := m.Root()
if raw := o.Bytes(peerPeerInfoAt); len(raw) > 0 {
if err := x.PeerInfo.UnmarshalZAP(raw); err != nil {
return err
}
}
if l := o.List(peerBenchedAt); l.Len() > 0 {
rows := make([]string, l.Len())
for i := range rows {
rows[i] = string(l.BytesAt(i))
}
x.Benched = rows
}
return nil
}
// ---- PeerInfo ----------------------------------------------------------
const (
peerInfoIPAt = 0
peerInfoPublicIPAt = 8
peerInfoIDAt = 16
peerInfoVersionAt = 40
peerInfoLastSentAt = 48
peerInfoLastReceivedAt = 56
peerInfoObservedUptimeAt = 64
peerInfoTrackedChainsAt = 72
peerInfoSupportedLPsAt = 80
peerInfoObjectedLPsAt = 88
peerInfoSize = 96
)
var _ interface {
MarshalZAP() ([]byte, error)
UnmarshalZAP([]byte) error
} = (*PeerInfo)(nil)
// MarshalZAP writes PeerInfo from constant offsets.
func (x *PeerInfo) MarshalZAP() ([]byte, error) {
if x == nil {
return nil, nil
}
b := zap.NewBuilder(peerInfoSize + 256)
trackedChainsAt, trackedChainsN := 0, len(x.TrackedChains)
if trackedChainsN > 0 {
var blob []byte
for i := range x.TrackedChains {
enc := x.TrackedChains[i][:]
var n [4]byte
binary.LittleEndian.PutUint32(n[:], uint32(len(enc)))
blob = append(blob, n[:]...)
blob = append(blob, enc...)
}
trackedChainsAt = b.WriteBytes(blob)
}
supportedLPsAt, supportedLPsN := 0, len(x.SupportedLPs)
if supportedLPsN > 0 {
var blob []byte
for i := range x.SupportedLPs {
var full [8]byte
binary.LittleEndian.PutUint64(full[:], uint64(x.SupportedLPs[i]))
enc := full[:4]
var n [4]byte
binary.LittleEndian.PutUint32(n[:], uint32(len(enc)))
blob = append(blob, n[:]...)
blob = append(blob, enc...)
}
supportedLPsAt = b.WriteBytes(blob)
}
objectedLPsAt, objectedLPsN := 0, len(x.ObjectedLPs)
if objectedLPsN > 0 {
var blob []byte
for i := range x.ObjectedLPs {
var full [8]byte
binary.LittleEndian.PutUint64(full[:], uint64(x.ObjectedLPs[i]))
enc := full[:4]
var n [4]byte
binary.LittleEndian.PutUint32(n[:], uint32(len(enc)))
blob = append(blob, n[:]...)
blob = append(blob, enc...)
}
objectedLPsAt = b.WriteBytes(blob)
}
ob := b.StartObject(peerInfoSize)
ob.SetText(peerInfoIPAt, string(x.IP))
ob.SetText(peerInfoPublicIPAt, string(x.PublicIP))
ob.SetBytesFixed(peerInfoIDAt, x.ID[:])
ob.SetText(peerInfoVersionAt, string(x.Version))
innerLastSent, err := x.LastSent.MarshalZAP()
if err != nil {
return nil, err
}
ob.SetBytes(peerInfoLastSentAt, innerLastSent)
innerLastReceived, err := x.LastReceived.MarshalZAP()
if err != nil {
return nil, err
}
ob.SetBytes(peerInfoLastReceivedAt, innerLastReceived)
ob.SetUint32(peerInfoObservedUptimeAt, uint32(x.ObservedUptime))
if trackedChainsN > 0 {
ob.SetList(peerInfoTrackedChainsAt, trackedChainsAt, trackedChainsN)
}
if supportedLPsN > 0 {
ob.SetList(peerInfoSupportedLPsAt, supportedLPsAt, supportedLPsN)
}
if objectedLPsN > 0 {
ob.SetList(peerInfoObjectedLPsAt, objectedLPsAt, objectedLPsN)
}
ob.FinishAsRoot()
return b.Finish(), nil
}
// UnmarshalZAP reads PeerInfo out of the buffer that arrived.
func (x *PeerInfo) UnmarshalZAP(data []byte) error {
if x == nil || len(data) == 0 {
return nil
}
m, err := zap.Parse(data)
if err != nil {
return fmt.Errorf("PeerInfo: %w", err)
}
o := m.Root()
x.IP = types.Addr(strings.Clone(o.Text(peerInfoIPAt)))
x.PublicIP = types.Addr(strings.Clone(o.Text(peerInfoPublicIPAt)))
copy(x.ID[:], o.BytesFixed(peerInfoIDAt, 20))
x.Version = string(strings.Clone(o.Text(peerInfoVersionAt)))
if raw := o.Bytes(peerInfoLastSentAt); len(raw) > 0 {
if err := x.LastSent.UnmarshalZAP(raw); err != nil {
return err
}
}
if raw := o.Bytes(peerInfoLastReceivedAt); len(raw) > 0 {
if err := x.LastReceived.UnmarshalZAP(raw); err != nil {
return err
}
}
x.ObservedUptime = types.Uint32(o.Uint32(peerInfoObservedUptimeAt))
if l := o.List(peerInfoTrackedChainsAt); l.Len() > 0 {
rows := make([]ids.ID, l.Len())
for i := range rows {
copy(rows[i][:], l.BytesAt(i))
}
x.TrackedChains = rows
}
if l := o.List(peerInfoSupportedLPsAt); l.Len() > 0 {
rows := make([]uint32, l.Len())
for i := range rows {
var full [8]byte
copy(full[:], l.BytesAt(i))
rows[i] = uint32(binary.LittleEndian.Uint64(full[:]))
}
x.SupportedLPs = rows
}
if l := o.List(peerInfoObjectedLPsAt); l.Len() > 0 {
rows := make([]uint32, l.Len())
for i := range rows {
var full [8]byte
copy(full[:], l.BytesAt(i))
rows[i] = uint32(binary.LittleEndian.Uint64(full[:]))
}
x.ObjectedLPs = rows
}
return nil
}
// ---- PeersArgs ---------------------------------------------------------
const (
@@ -448,6 +1137,72 @@ func (x *PeersArgs) UnmarshalZAP(data []byte) error {
return nil
}
// ---- PeersReply --------------------------------------------------------
const (
peersReplyNumPeersAt = 0
peersReplyPeersAt = 8
peersReplySize = 16
)
var _ interface {
MarshalZAP() ([]byte, error)
UnmarshalZAP([]byte) error
} = (*PeersReply)(nil)
// MarshalZAP writes PeersReply from constant offsets.
func (x *PeersReply) MarshalZAP() ([]byte, error) {
if x == nil {
return nil, nil
}
b := zap.NewBuilder(peersReplySize + 256)
peersAt, peersN := 0, len(x.Peers)
if peersN > 0 {
var blob []byte
for i := range x.Peers {
enc, err := x.Peers[i].MarshalZAP()
if err != nil {
return nil, err
}
var n [4]byte
binary.LittleEndian.PutUint32(n[:], uint32(len(enc)))
blob = append(blob, n[:]...)
blob = append(blob, enc...)
}
peersAt = b.WriteBytes(blob)
}
ob := b.StartObject(peersReplySize)
ob.SetUint64(peersReplyNumPeersAt, uint64(x.NumPeers))
if peersN > 0 {
ob.SetList(peersReplyPeersAt, peersAt, peersN)
}
ob.FinishAsRoot()
return b.Finish(), nil
}
// UnmarshalZAP reads PeersReply out of the buffer that arrived.
func (x *PeersReply) UnmarshalZAP(data []byte) error {
if x == nil || len(data) == 0 {
return nil
}
m, err := zap.Parse(data)
if err != nil {
return fmt.Errorf("PeersReply: %w", err)
}
o := m.Root()
x.NumPeers = types.Uint64(o.Uint64(peersReplyNumPeersAt))
if l := o.List(peersReplyPeersAt); l.Len() > 0 {
rows := make([]Peer, l.Len())
for i := range rows {
if err := rows[i].UnmarshalZAP(l.BytesAt(i)); err != nil {
return err
}
}
x.Peers = rows
}
return nil
}
// ---- ProofOfPossession -------------------------------------------------
const (
@@ -529,3 +1284,105 @@ func (x *UptimeResponse) UnmarshalZAP(data []byte) error {
x.WeightedAveragePercentage = types.Float64(o.Float64(uptimeResponseWeightedAveragePercentageAt))
return nil
}
// ---- VMAlias -----------------------------------------------------------
const (
vMAliasVMAt = 0
vMAliasAliasesAt = 32
vMAliasSize = 40
)
var _ interface {
MarshalZAP() ([]byte, error)
UnmarshalZAP([]byte) error
} = (*VMAlias)(nil)
// MarshalZAP writes VMAlias from constant offsets.
func (x *VMAlias) MarshalZAP() ([]byte, error) {
if x == nil {
return nil, nil
}
b := zap.NewBuilder(vMAliasSize + 256)
aliasesAt, aliasesN := 0, len(x.Aliases)
if aliasesN > 0 {
var blob []byte
for i := range x.Aliases {
enc := []byte(x.Aliases[i])
var n [4]byte
binary.LittleEndian.PutUint32(n[:], uint32(len(enc)))
blob = append(blob, n[:]...)
blob = append(blob, enc...)
}
aliasesAt = b.WriteBytes(blob)
}
ob := b.StartObject(vMAliasSize)
ob.SetBytesFixed(vMAliasVMAt, x.VM[:])
if aliasesN > 0 {
ob.SetList(vMAliasAliasesAt, aliasesAt, aliasesN)
}
ob.FinishAsRoot()
return b.Finish(), nil
}
// UnmarshalZAP reads VMAlias out of the buffer that arrived.
func (x *VMAlias) UnmarshalZAP(data []byte) error {
if x == nil || len(data) == 0 {
return nil
}
m, err := zap.Parse(data)
if err != nil {
return fmt.Errorf("VMAlias: %w", err)
}
o := m.Root()
copy(x.VM[:], o.BytesFixed(vMAliasVMAt, 32))
if l := o.List(vMAliasAliasesAt); l.Len() > 0 {
rows := make([]string, l.Len())
for i := range rows {
rows[i] = string(l.BytesAt(i))
}
x.Aliases = rows
}
return nil
}
// ---- VMVersion ---------------------------------------------------------
const (
vMVersionVMAt = 0
vMVersionVersionAt = 8
vMVersionSize = 16
)
var _ interface {
MarshalZAP() ([]byte, error)
UnmarshalZAP([]byte) error
} = (*VMVersion)(nil)
// MarshalZAP writes VMVersion from constant offsets.
func (x *VMVersion) MarshalZAP() ([]byte, error) {
if x == nil {
return nil, nil
}
b := zap.NewBuilder(vMVersionSize + 256)
ob := b.StartObject(vMVersionSize)
ob.SetText(vMVersionVMAt, string(x.VM))
ob.SetText(vMVersionVersionAt, string(x.Version))
ob.FinishAsRoot()
return b.Finish(), nil
}
// UnmarshalZAP reads VMVersion out of the buffer that arrived.
func (x *VMVersion) UnmarshalZAP(data []byte) error {
if x == nil || len(data) == 0 {
return nil
}
m, err := zap.Parse(data)
if err != nil {
return fmt.Errorf("VMVersion: %w", err)
}
o := m.Root()
x.VM = string(strings.Clone(o.Text(vMVersionVMAt)))
x.Version = string(strings.Clone(o.Text(vMVersionVersionAt)))
return nil
}
+152
View File
@@ -0,0 +1,152 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Values that are one thing at the JSON edge and another on the ZAP wire.
//
// A ZAP field is an OFFSET and a WIDTH. Three shapes a Go program reaches for
// have neither, and each fails differently:
//
// - a MAP has no order, so it has no layout at all. [zip.LayoutOf] refuses it
// outright, which is the loud failure and the easy one.
// - a struct with only UNEXPORTED fields — time.Time, netip.AddrPort —
// derives an EMPTY layout instead. It is not refused: the value crosses,
// reports no error and carries nothing, which is the quiet failure and the
// dangerous one.
// - an INTERFACE has no shape to lay out until it holds something, and what it
// holds is a runtime fact.
//
// The types here are the second and third cases answered. The first is answered
// per map, in the package that owns it: an object on the JSON wire, a list
// ordered by key here.
//
// EVERY ONE OF THEM RENDERS THE JSON IT REPLACED, byte for byte. That is the
// whole constraint — these travel on APIs a network already answers, so the
// change is which bytes cross between two processes, never which bytes reach a
// caller.
package types
import (
"encoding/json"
"net/netip"
"time"
)
// Addr is a network address — an IP and a port — as the JSON wire has always
// carried it: the text netip.AddrPort writes for itself.
//
// It is text here rather than a netip.AddrPort because that type keeps its
// address, its zone and its port in unexported fields. Reflection cannot read
// them, so the derived layout is empty and a reply holding one arrives blank
// with no error to say so. GetNodeIPReply has exactly one field, so its entire
// answer was empty.
type Addr string
// AddrOf is ap as it crosses. It renders through netip's own marshaler, so the
// bytes are the ones that were already on the JSON wire — including the empty
// string an invalid address has always been written as.
func AddrOf(ap netip.AddrPort) Addr {
b, err := ap.MarshalText()
if err != nil {
return ""
}
return Addr(b)
}
// AddrPort is the address as a netip.AddrPort, or the zero value when the text
// does not parse — which is what an absent address already reads as.
func (a Addr) AddrPort() netip.AddrPort {
ap, err := netip.ParseAddrPort(string(a))
if err != nil {
return netip.AddrPort{}
}
return ap
}
// Time is an instant, as three numbers that have a layout.
//
// time.Time cannot cross for the same reason netip.AddrPort cannot: every field
// is unexported. The JSON is byte for byte what time.Time's own marshaler
// writes, because the rendering depends only on the instant and the offset —
// RFC 3339 spells a zero offset "Z" and any other one "+HH:MM" — so carrying
// the offset is enough to put the instant back in the zone it was read in.
type Time struct {
// Seconds is seconds since the epoch.
Seconds int64 `json:"seconds"`
// Nanos is the nanosecond within that second.
Nanos int32 `json:"nanos"`
// Offset is seconds east of UTC.
Offset int32 `json:"offset"`
}
// TimeOf is t as a value that can cross.
func TimeOf(t time.Time) Time {
_, offset := t.Zone()
return Time{Seconds: t.Unix(), Nanos: int32(t.Nanosecond()), Offset: int32(offset)}
}
// Time is the instant, in the zone it was read in.
func (t Time) Time() time.Time {
return time.Unix(t.Seconds, int64(t.Nanos)).In(time.FixedZone("", int(t.Offset)))
}
// IsZero reports whether this is the zero instant, which is what an absent time
// has always marshalled as.
func (t Time) IsZero() bool { return t == Time{} }
func (t Time) MarshalJSON() ([]byte, error) {
if t.IsZero() {
return json.Marshal(time.Time{})
}
return json.Marshal(t.Time())
}
func (t *Time) UnmarshalJSON(b []byte) error {
if string(b) == Null {
return nil
}
var when time.Time
if err := json.Unmarshal(b, &when); err != nil {
return err
}
*t = TimeOf(when)
return nil
}
// Raw is a value whose shape only the sender knows: it crosses as the bytes of
// its own JSON, and is written back out as those bytes.
//
// It is what an `any` field becomes. An interface has no layout until it holds
// something, and what it holds is decided per call — a health check answers with
// an object, a string or an array depending on which check it is — so there is
// no struct to name. The bytes are the honest shape, and they reach a caller
// exactly as they were produced.
type Raw []byte
// RawOf is v encoded once, at the point where its concrete type is still known.
func RawOf(v any) (Raw, error) {
if v == nil {
return nil, nil
}
b, err := json.Marshal(v)
if err != nil {
return nil, err
}
return Raw(b), nil
}
func (r Raw) MarshalJSON() ([]byte, error) {
if len(r) == 0 {
return []byte(Null), nil
}
return r, nil
}
func (r *Raw) UnmarshalJSON(b []byte) error {
if string(b) == Null {
*r = nil
return nil
}
*r = append((*r)[:0], b...)
return nil
}
+44
View File
@@ -658,3 +658,47 @@ func (x *JSONTxID) UnmarshalZAP(data []byte) error {
copy(x.TxID[:], o.BytesFixed(jSONTxIDTxIDAt, 32))
return nil
}
// ---- Time --------------------------------------------------------------
const (
timeSecondsAt = 0
timeNanosAt = 8
timeOffsetAt = 12
timeSize = 16
)
var _ interface {
MarshalZAP() ([]byte, error)
UnmarshalZAP([]byte) error
} = (*Time)(nil)
// MarshalZAP writes Time from constant offsets.
func (x *Time) MarshalZAP() ([]byte, error) {
if x == nil {
return nil, nil
}
b := zap.NewBuilder(timeSize + 256)
ob := b.StartObject(timeSize)
ob.SetInt64(timeSecondsAt, int64(x.Seconds))
ob.SetInt32(timeNanosAt, int32(x.Nanos))
ob.SetInt32(timeOffsetAt, int32(x.Offset))
ob.FinishAsRoot()
return b.Finish(), nil
}
// UnmarshalZAP reads Time out of the buffer that arrived.
func (x *Time) UnmarshalZAP(data []byte) error {
if x == nil || len(data) == 0 {
return nil
}
m, err := zap.Parse(data)
if err != nil {
return fmt.Errorf("Time: %w", err)
}
o := m.Root()
x.Seconds = int64(o.Int64(timeSecondsAt))
x.Nanos = int32(o.Int32(timeNanosAt))
x.Offset = int32(o.Int32(timeOffsetAt))
return nil
}
+191
View File
@@ -0,0 +1,191 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// The shapes that changed to cross a wire must not have changed the wire.
//
// A map became a list and an `any` became bytes so that a reply could cross
// between two processes at all — a ZAP field is an offset, and a map has no
// order to give one while an interface has no shape until it holds something.
// None of that is a caller's business: these APIs are answered by a live network
// today, so the JSON a caller reads must be the same bytes it always was.
//
// Each test below writes the NEW type and compares against the object the OLD
// one produced, spelled out as a literal. A literal rather than a round trip,
// because a round trip through one type proves only that it agrees with itself.
package api_test
import (
"encoding/json"
"testing"
"time"
"github.com/luxfi/api/admin"
"github.com/luxfi/api/health"
"github.com/luxfi/api/info"
"github.com/luxfi/api/types"
"github.com/luxfi/ids"
)
func id(b byte) ids.ID { return ids.ID{0: b} }
// same asserts v marshals to want, and that reading want back yields v — so the
// list is not merely writing the old shape but reading it too.
func same[T any](t *testing.T, v T, want string, back func(T) bool) {
t.Helper()
got, err := json.Marshal(v)
if err != nil {
t.Fatalf("marshal: %v", err)
}
if norm(t, string(got)) != norm(t, want) {
t.Errorf("marshalled\n %s\nwant\n %s", got, want)
}
var read T
if err := json.Unmarshal([]byte(want), &read); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if !back(read) {
t.Errorf("reading %s back did not reproduce the value", want)
}
}
func norm(t *testing.T, s string) string {
t.Helper()
var v any
if err := json.Unmarshal([]byte(s), &v); err != nil {
t.Fatalf("not JSON: %s", s)
}
b, _ := json.Marshal(v)
return string(b)
}
func TestVMVersionsIsStillAnObject(t *testing.T) {
same(t,
info.VMVersions{{VM: "platformvm", Version: "v1.2.3"}, {VM: "xvm", Version: "v1.2.4"}},
`{"platformvm":"v1.2.3","xvm":"v1.2.4"}`,
func(v info.VMVersions) bool {
return len(v) == 2 && v[0].VM == "platformvm" && v[1].Version == "v1.2.4"
})
}
func TestVMAliasesAndFxNamesAreStillObjects(t *testing.T) {
same(t,
info.VMAliases{{VM: id(1), Aliases: []string{"platformvm"}}},
`{"`+id(1).String()+`":["platformvm"]}`,
func(v info.VMAliases) bool { return len(v) == 1 && v[0].VM == id(1) })
same(t,
info.FxNames{{Fx: id(2), Name: "secp256k1fx"}},
`{"`+id(2).String()+`":"secp256k1fx"}`,
func(v info.FxNames) bool { return len(v) == 1 && v[0].Name == "secp256k1fx" })
}
func TestLPsAreStillAnObjectKeyedByNumber(t *testing.T) {
same(t,
info.LPs{{Number: 23, LP: info.LP{AbstainWeight: 7}}},
`{"23":{"supportWeight":"0","supporters":null,"objectWeight":"0","objectors":null,"abstainWeight":"7"}}`,
func(v info.LPs) bool { return len(v) == 1 && v[0].Number == 23 && v[0].LP.AbstainWeight == 7 })
}
func TestLoggerLevelsAreStillAnObject(t *testing.T) {
same(t,
admin.LoggerLevels{{Logger: "C", Levels: admin.LogAndDisplayLevels{LogLevel: "DEBUG", DisplayLevel: "INFO"}}},
`{"C":{"logLevel":"DEBUG","displayLevel":"INFO"}}`,
func(v admin.LoggerLevels) bool { return len(v) == 1 && v[0].Levels.LogLevel == "DEBUG" })
}
func TestInstalledVMsAreStillAnObjectKeyedByID(t *testing.T) {
same(t,
admin.InstalledVMs{{ID: "vm1", Aliases: []string{"a"}}},
`{"vm1":{"id":"vm1","aliases":["a"]}}`,
func(v admin.InstalledVMs) bool { return len(v) == 1 && v[0].ID == "vm1" })
}
func TestLoadedAndFailedVMsAreStillObjects(t *testing.T) {
same(t,
admin.LoadedVMs{{VM: id(3), Aliases: []string{"a"}}},
`{"`+id(3).String()+`":["a"]}`,
func(v admin.LoadedVMs) bool { return len(v) == 1 && v[0].VM == id(3) })
same(t,
admin.FailedVMs{{VM: id(4), Error: "no plugin"}},
`{"`+id(4).String()+`":"no plugin"}`,
func(v admin.FailedVMs) bool { return len(v) == 1 && v[0].Error == "no plugin" })
}
func TestHealthChecksAreStillAnObject(t *testing.T) {
when := types.TimeOf(time.Unix(1753479996, 0).UTC())
said, err := types.RawOf(map[string]any{"availableDiskBytes": 12})
if err != nil {
t.Fatal(err)
}
same(t,
health.Checks{{Name: "diskspace", Result: health.Result{Details: said, Timestamp: when, Duration: 1234}}},
`{"diskspace":{"message":{"availableDiskBytes":12},"timestamp":"2025-07-25T21:46:36Z","duration":1234}}`,
func(v health.Checks) bool { return len(v) == 1 && v[0].Name == "diskspace" })
}
// The details of a check are whatever it said. All three shapes mainnet answers
// with survive as themselves — an `any` that became bytes lost nothing.
func TestRawKeepsEveryShapeACheckAnswersWith(t *testing.T) {
for _, want := range []string{
`{"availableDiskBytes":12}`,
`"node is not a validator"`,
`["P","X","C"]`,
`null`,
} {
var r types.Raw
if err := json.Unmarshal([]byte(want), &r); err != nil {
t.Fatalf("unmarshal %s: %v", want, err)
}
got, err := json.Marshal(r)
if err != nil {
t.Fatalf("marshal %s: %v", want, err)
}
if string(got) != want {
t.Errorf("Raw turned %s into %s", want, got)
}
}
}
// An address is text on the JSON wire and always was — including the empty
// string an invalid one has always been written as.
func TestAddrIsTheTextNetipWrites(t *testing.T) {
for _, want := range []string{`"203.0.113.9:9651"`, `""`} {
var a types.Addr
if err := json.Unmarshal([]byte(want), &a); err != nil {
t.Fatalf("unmarshal %s: %v", want, err)
}
got, err := json.Marshal(a)
if err != nil {
t.Fatal(err)
}
if string(got) != want {
t.Errorf("Addr turned %s into %s", want, got)
}
}
}
// An instant is the RFC 3339 string time.Time writes, and reads back to the
// same instant in the same zone.
func TestTimeIsTheStringTimeTimeWrites(t *testing.T) {
when := time.Date(2026, 8, 29, 12, 0, 0, 0, time.FixedZone("", 2*3600))
got, err := json.Marshal(types.TimeOf(when))
if err != nil {
t.Fatal(err)
}
direct, err := json.Marshal(when)
if err != nil {
t.Fatal(err)
}
if string(got) != string(direct) {
t.Errorf("Time wrote %s; time.Time writes %s", got, direct)
}
var read types.Time
if err := json.Unmarshal(direct, &read); err != nil {
t.Fatal(err)
}
if !read.Time().Equal(when) {
t.Errorf("read back %s; want %s", read.Time(), when)
}
}