mirror of
https://github.com/luxfi/node.git
synced 2026-10-30 15:24:05 +00:00
The module already called itself github.com/luxfi/node while containing no node in Go: cmd/luxd was twenty-one lines that printed the size of a few types, and the Go column of the matrix measured a VM plugin. A repo that fetches the thing it claims to be has not replaced it, so the Go sources move in — 1515 files, the daemon and everything under it — and the build reaches outside this module for nothing. Rust and C++ sit beside it unchanged and are still measured against it by the differential. That is the point of one repo: the reference implementation and the two ports are the same checkout, so parity is a thing you can run rather than a thing you assert. Verified: builds, and the closure carries no lux-private — checked at build time rather than believed. The daemon starts twelve chains on a local network (P X C Q Z A B G K M F, plus D through its plugin) and reports healthy.
374 lines
13 KiB
Go
374 lines
13 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
// The questions this node answers, and the one place each answer is written.
|
|
//
|
|
// A handler here IS the operation. There is no second copy of it behind a
|
|
// JSON-RPC method table: registering it yields the REST route, the OpenAPI
|
|
// document, the MCP tool, the CLI command, the generated SDK and the by-name call
|
|
// plane, all from this one registration and the doc comment above it.
|
|
//
|
|
// EVERY OPERATION IS A GET, and that is the whole of its authorization. The
|
|
// node's rule reads the method (server/http/authorize.go) and info is what a node
|
|
// tells anyone; nothing here changes the node.
|
|
//
|
|
// # Filtering a read over REST
|
|
//
|
|
// A GET carries no body (RFC 9110), so an argument that narrows a read has to
|
|
// ride the URL. peers takes a list of node ids and lps takes none; a list is
|
|
// spelled comma-separated in one value — ?nodeIDs=NodeID-a,NodeID-b — which is
|
|
// `style: form, explode: false`, what the document already publishes for a
|
|
// repeated parameter. Each id reads itself from that text, so the filter a
|
|
// caller sends over MCP or the call plane is the same filter a URL can carry.
|
|
|
|
package info
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"slices"
|
|
|
|
apiinfo "github.com/luxfi/api/info"
|
|
apitypes "github.com/luxfi/api/types"
|
|
"github.com/luxfi/constants"
|
|
"github.com/luxfi/ids"
|
|
"github.com/luxfi/log"
|
|
"github.com/luxfi/math/set"
|
|
"github.com/luxfi/utxo/nftfx"
|
|
"github.com/luxfi/utxo/propertyfx"
|
|
"github.com/luxfi/utxo/secp256k1fx"
|
|
"github.com/zap-proto/zip"
|
|
|
|
"github.com/luxfi/node/upgrade"
|
|
"github.com/luxfi/node/version"
|
|
)
|
|
|
|
//go:generate go run github.com/zap-proto/zip/cmd/zipdoc
|
|
|
|
// Ops is this service's typed operations. The paths are relative to where the
|
|
// app is mounted, which the node decides — a service does not name its own
|
|
// address.
|
|
func (i *Info) Ops() *zip.App {
|
|
app := zip.New(zip.Config{
|
|
AppName: "info",
|
|
Logger: i.log,
|
|
DisableStartupMessage: true,
|
|
OpenAPI: zip.OpenAPIConfig{
|
|
Title: "Lux node info",
|
|
Description: "What a Lux node tells anyone who asks: what it is running, what network it is on, who it is connected to, and what its chains cost.",
|
|
Version: i.release(),
|
|
},
|
|
})
|
|
zip.Get(app, "/node/version", i.nodeVersion)
|
|
zip.Get(app, "/node/id", i.identity)
|
|
zip.Get(app, "/node/ip", i.address)
|
|
zip.Get(app, "/network/id", i.networkID)
|
|
zip.Get(app, "/network/name", i.networkName)
|
|
zip.Get(app, "/chain/id", i.chainID)
|
|
zip.Get(app, "/chain/bootstrapped", i.bootstrapped)
|
|
zip.Get(app, "/chains", i.chains)
|
|
zip.Get(app, "/peers", i.peers)
|
|
zip.Get(app, "/lps", i.lps)
|
|
zip.Get(app, "/vms", i.vms)
|
|
zip.Get(app, "/upgrades", i.upgrades)
|
|
zip.Get(app, "/uptime", i.uptime)
|
|
zip.Get(app, "/fees", i.fees)
|
|
return app
|
|
}
|
|
|
|
// release is the node's version for the document's info block, or the empty
|
|
// string when this service was built without one — a test, or a caller that has
|
|
// not wired Parameters through.
|
|
func (i *Info) release() string {
|
|
if i.Version == nil {
|
|
return ""
|
|
}
|
|
return i.Version.String()
|
|
}
|
|
|
|
// NodeVersion is what this node is running: its own release, the database format
|
|
// it reads, the VM protocol it speaks, and the consensus it is configured for.
|
|
//
|
|
// Response: {"version": "luxd/1.36.178", "databaseVersion": "v1.4.5", "rpcProtocolVersion": "39", "gitCommit": "", "vmVersions": {}}
|
|
func (i *Info) nodeVersion(_ context.Context, _ *struct{}) (*apiinfo.GetNodeVersionReply, error) {
|
|
i.log.Debug("API called",
|
|
log.String("service", "info"),
|
|
log.String("method", "getNodeVersion"),
|
|
)
|
|
|
|
reply := &apiinfo.GetNodeVersionReply{
|
|
Version: i.Version.String(),
|
|
DatabaseVersion: version.CurrentDatabase.String(),
|
|
RPCProtocolVersion: apitypes.Uint32(version.RPCChainVMProtocol),
|
|
GitCommit: version.GitCommit,
|
|
VMVersions: apiinfo.VMVersions{},
|
|
}
|
|
if i.Consensus != nil {
|
|
consensus := *i.Consensus
|
|
reply.Consensus = &consensus
|
|
}
|
|
return reply, nil
|
|
}
|
|
|
|
// Identity is this node's id, with the proof it holds the staking key that id is
|
|
// derived from.
|
|
//
|
|
// Response: {"nodeID": "NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg", "nodePOP": {"publicKey": "0x8f95423f7142d00a48e1014a3de8d28907d420dc33b3052a6dee03a3f2941a393c2351e354704ca66a3fc29870282e15", "proofOfPossession": "0x86a3ab4c45cfe31cae34c1d06f212434ac71b1be6cfe046c80c162e057614a94a5bc9f1ded1a7029deb0ba4ca7c9b71411e293438691be79c2dbf19d1ca7c3eadb9c756246fc5de5b7b89511c7d7dda6"}}
|
|
func (i *Info) identity(_ context.Context, _ *struct{}) (*apiinfo.GetNodeIDReply, error) {
|
|
i.log.Debug("API called",
|
|
log.String("service", "info"),
|
|
log.String("method", "getNodeID"),
|
|
)
|
|
|
|
pop, err := toAPIProofOfPossession(i.NodePOP)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &apiinfo.GetNodeIDReply{NodeID: i.NodeID, NodePOP: pop}, nil
|
|
}
|
|
|
|
// Address is where this node tells its peers to reach it.
|
|
//
|
|
// Response: {"ip": "203.0.113.9:9651"}
|
|
func (i *Info) address(_ context.Context, _ *struct{}) (*apiinfo.GetNodeIPReply, error) {
|
|
i.log.Debug("API called",
|
|
log.String("service", "info"),
|
|
log.String("method", "getNodeIP"),
|
|
)
|
|
|
|
return &apiinfo.GetNodeIPReply{IP: apitypes.AddrOf(i.myIP.Get())}, nil
|
|
}
|
|
|
|
// NetworkID is the number of the network this node is on.
|
|
//
|
|
// Response: {"networkID": "96369"}
|
|
func (i *Info) networkID(_ context.Context, _ *struct{}) (*apiinfo.GetNetworkIDReply, error) {
|
|
i.log.Debug("API called",
|
|
log.String("service", "info"),
|
|
log.String("method", "getNetworkID"),
|
|
)
|
|
|
|
return &apiinfo.GetNetworkIDReply{NetworkID: apitypes.Uint32(i.Parameters.NetworkID)}, nil
|
|
}
|
|
|
|
// NetworkName is the name of the network this node is on.
|
|
//
|
|
// Response: {"networkName": "mainnet"}
|
|
func (i *Info) networkName(_ context.Context, _ *struct{}) (*apiinfo.GetNetworkNameReply, error) {
|
|
i.log.Debug("API called",
|
|
log.String("service", "info"),
|
|
log.String("method", "getNetworkName"),
|
|
)
|
|
|
|
return &apiinfo.GetNetworkNameReply{NetworkName: constants.NetworkName(i.Parameters.NetworkID)}, nil
|
|
}
|
|
|
|
// ChainID resolves a chain's alias to the id it names.
|
|
//
|
|
// Example: {"alias": "X"}
|
|
// Response: {"blockchainID": "2oYMBNV4eNHyqk2fjjV5nVQLDbtmNJzq5s3qs3Lo6ftnC6FByM"}
|
|
func (i *Info) chainID(_ context.Context, in *apiinfo.GetBlockchainIDArgs) (*apiinfo.GetBlockchainIDReply, error) {
|
|
i.log.Debug("API called",
|
|
log.String("service", "info"),
|
|
log.String("method", "getBlockchainID"),
|
|
)
|
|
|
|
id, err := i.chainManager.Lookup(in.Alias)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &apiinfo.GetBlockchainIDReply{BlockchainID: id}, nil
|
|
}
|
|
|
|
// Bootstrapped reports whether a chain has finished bootstrapping on this node.
|
|
//
|
|
// Example: {"chain": "X"}
|
|
// Response: {"isBootstrapped": true}
|
|
func (i *Info) bootstrapped(_ context.Context, in *apiinfo.IsBootstrappedArgs) (*apiinfo.IsBootstrappedResponse, error) {
|
|
i.log.Debug("API called",
|
|
log.String("service", "info"),
|
|
log.String("method", "isBootstrapped"),
|
|
log.String("chain", in.Chain),
|
|
)
|
|
|
|
if in.Chain == "" {
|
|
return nil, errNoChainProvided
|
|
}
|
|
chainID, err := i.chainManager.Lookup(in.Chain)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("there is no chain with alias/ID '%s'", in.Chain)
|
|
}
|
|
return &apiinfo.IsBootstrappedResponse{IsBootstrapped: i.chainManager.IsBootstrapped(chainID)}, nil
|
|
}
|
|
|
|
// Chains are the chains this node is running, which is a subset of the chains
|
|
// the P-Chain knows about — platform.getBlockchains answers for those.
|
|
//
|
|
// Response: {"chains": [{"id": "11111111111111111111111111111111LpoYY", "name": "P-Chain", "vmID": "11111111111111111111111111111111LpoYY", "bootstrapped": true}]}
|
|
func (i *Info) chains(_ context.Context, _ *struct{}) (*GetChainsReply, error) {
|
|
i.log.Debug("API called",
|
|
log.String("service", "info"),
|
|
log.String("method", "getChains"),
|
|
)
|
|
|
|
return &GetChainsReply{Chains: i.chainManager.GetChains()}, nil
|
|
}
|
|
|
|
// Peers are the nodes this one is connected to. A list of node ids narrows the
|
|
// answer to those, and no list asks for all of them.
|
|
//
|
|
// Example: {"nodeIDs": []}
|
|
// Response: {"numPeers": "1", "peers": [{"ip": "203.0.113.9:9651", "publicIP": "203.0.113.9:9651", "nodeID": "NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg", "version": "luxd/1.36.178", "lastSent": "2026-08-29T00:00:00Z", "lastReceived": "2026-08-29T00:00:00Z", "observedUptime": "99", "trackedChains": [], "supportedLPs": [], "objectedLPs": [], "benched": []}]}
|
|
func (i *Info) peers(_ context.Context, in *apiinfo.PeersArgs) (*apiinfo.PeersReply, error) {
|
|
i.log.Debug("API called",
|
|
log.String("service", "info"),
|
|
log.String("method", "peers"),
|
|
)
|
|
|
|
found := i.networking.PeerInfo(in.NodeIDs)
|
|
peers := make([]apiinfo.Peer, len(found))
|
|
for index, peer := range found {
|
|
peers[index] = apiinfo.Peer{
|
|
PeerInfo: toP2PPeerInfo(peer),
|
|
// Benching is not wired on this node, so no chain is benched.
|
|
Benched: []string{},
|
|
}
|
|
}
|
|
return &apiinfo.PeersReply{
|
|
Peers: peers,
|
|
NumPeers: apitypes.Uint64(len(peers)),
|
|
}, nil
|
|
}
|
|
|
|
// LPs is where the network's stake stands on every current Lux Proposal.
|
|
//
|
|
// Response: {"lps": {"23": {"supportWeight": "0", "supporters": [], "objectWeight": "0", "objectors": [], "abstainWeight": "1000000000000"}}}
|
|
func (i *Info) lps(_ context.Context, _ *struct{}) (*apiinfo.LPsReply, error) {
|
|
i.log.Debug("API called",
|
|
log.String("service", "info"),
|
|
log.String("method", "lps"),
|
|
)
|
|
|
|
totalWeight, err := i.validators.TotalWeight(constants.PrimaryNetworkID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// A peer's own votes are not carried on the connection this node holds, so
|
|
// every validator abstains: the weight is the whole primary network's.
|
|
lps := make(apiinfo.LPs, 0, constants.CurrentLPs.Len())
|
|
for number := range constants.CurrentLPs {
|
|
lps = append(lps, apiinfo.LPStatus{
|
|
Number: number,
|
|
LP: apiinfo.LP{
|
|
Supporters: []ids.NodeID{},
|
|
Objectors: []ids.NodeID{},
|
|
AbstainWeight: apitypes.Uint64(totalWeight),
|
|
},
|
|
})
|
|
}
|
|
slices.SortFunc(lps, func(x, y apiinfo.LPStatus) int { return int(x.Number) - int(y.Number) })
|
|
return &apiinfo.LPsReply{LPs: lps}, nil
|
|
}
|
|
|
|
// VMs are the virtual machines installed on this node, and the feature
|
|
// extensions its UTXO chains understand.
|
|
//
|
|
// Response: {"vms": {"mgj786NP7uDwBCcq6YwThhaN8FLyybkCa4zBWTQbNgmK6k9A6": ["platformvm"]}, "fxs": {"spqBHsy2UBGpjaQJezEywUy1AB98eVjJ3WQ38x1vpjsx4xPkY": "secp256k1fx"}}
|
|
func (i *Info) vms(ctx context.Context, _ *struct{}) (*apiinfo.GetVMsReply, error) {
|
|
i.log.Debug("API called",
|
|
log.String("service", "info"),
|
|
log.String("method", "getVMs"),
|
|
)
|
|
|
|
vmIDs, err := i.VMManager.ListFactories(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
installed := make(apiinfo.VMAliases, 0, len(vmIDs))
|
|
for _, vmID := range vmIDs {
|
|
entry := apiinfo.VMAlias{VM: vmID}
|
|
if alias, err := i.VMManager.PrimaryAlias(ctx, vmID); err == nil && alias != vmID.String() {
|
|
entry.Aliases = []string{alias}
|
|
}
|
|
installed = append(installed, entry)
|
|
}
|
|
slices.SortFunc(installed, func(x, y apiinfo.VMAlias) int { return x.VM.Compare(y.VM) })
|
|
|
|
fxs := apiinfo.FxNames{
|
|
{Fx: secp256k1fx.ID, Name: secp256k1fx.Name},
|
|
{Fx: nftfx.ID, Name: nftfx.Name},
|
|
{Fx: propertyfx.ID, Name: propertyfx.Name},
|
|
}
|
|
slices.SortFunc(fxs, func(x, y apiinfo.FxName) int { return x.Fx.Compare(y.Fx) })
|
|
|
|
return &apiinfo.GetVMsReply{VMs: installed, Fxs: fxs}, nil
|
|
}
|
|
|
|
// Upgrades is the upgrade schedule this node runs.
|
|
//
|
|
// Response: {"xChainStopVertexID": "jrGWDh5Po9FMj54depyunNixpia5PN4aAYxfmNzU8n752Rjga", "epochDuration": 300000000000}
|
|
func (i *Info) upgrades(_ context.Context, _ *struct{}) (*upgrade.Config, error) {
|
|
i.log.Debug("API called",
|
|
log.String("service", "info"),
|
|
log.String("method", "upgrades"),
|
|
)
|
|
|
|
schedule := i.Parameters.Upgrades
|
|
return &schedule, nil
|
|
}
|
|
|
|
// Uptime is how much of the network, by stake, reports having seen this node up.
|
|
//
|
|
// Response: {"rewardingStakePercentage": "100.0000", "weightedAveragePercentage": "99.9999"}
|
|
func (i *Info) uptime(_ context.Context, _ *struct{}) (*apiinfo.UptimeResponse, error) {
|
|
i.log.Debug("API called",
|
|
log.String("service", "info"),
|
|
log.String("method", "uptime"),
|
|
)
|
|
|
|
result, err := i.networking.NodeUptime()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("couldn't get node uptime: %w", err)
|
|
}
|
|
return &apiinfo.UptimeResponse{
|
|
WeightedAveragePercentage: apitypes.Float64(result.WeightedAveragePercentage),
|
|
RewardingStakePercentage: apitypes.Float64(result.RewardingStakePercentage),
|
|
}, nil
|
|
}
|
|
|
|
// Fees are the transaction fees this node charges, in nLUX. Deprecated: the
|
|
// P-Chain's fees are dynamic and platform.getFeeConfig is the live answer.
|
|
//
|
|
// Response: {"txFee": "1000000", "createAssetTxFee": "10000000", "createNetworkTxFee": "1000000000", "transformChainTxFee": "10000000000", "createChainTxFee": "1000000000", "addNetworkValidatorFee": "1000000", "addNetworkDelegatorFee": "1000000"}
|
|
func (i *Info) fees(_ context.Context, _ *struct{}) (*apiinfo.GetTxFeeResponse, error) {
|
|
i.log.Warn("deprecated API called",
|
|
log.String("service", "info"),
|
|
log.String("method", "getTxFee"),
|
|
)
|
|
|
|
reply := defaultGetTxFeeResponse
|
|
if i.Parameters.NetworkID == constants.MainnetID {
|
|
reply = mainnetGetTxFeeResponse
|
|
}
|
|
reply.TxFee = apitypes.Uint64(i.TxFee)
|
|
reply.CreateAssetTxFee = apitypes.Uint64(i.CreateAssetTxFee)
|
|
return &reply, nil
|
|
}
|
|
|
|
// sortedIDs and sortedNumbers are a set as the wire carries it: a list, in an
|
|
// order, because a map has neither. The JSON was already an array — an
|
|
// unordered one, so a caller could read the same answer two ways.
|
|
func sortedIDs(s set.Set[ids.ID]) []ids.ID {
|
|
out := s.List()
|
|
slices.SortFunc(out, func(x, y ids.ID) int { return x.Compare(y) })
|
|
return out
|
|
}
|
|
|
|
func sortedNumbers(s set.Set[uint32]) []uint32 {
|
|
out := s.List()
|
|
slices.Sort(out)
|
|
return out
|
|
}
|