Files
zeekayandHanzo Dev 62d5f28c22 A chain address is built in one place
The node moved every chain route from /v1/bc/<alias> to /v1/chain/<alias>
and deleted the old prefix, so every chain call answered 404. The address
was spelled out at 112 sites across 34 files and constants.ChainAliasPrefix
was used at none of them, which is why nothing failed to compile when the
segment moved.

route.Chain(uri, alias) composes it now, and route.Alias reads one back,
replacing three hand-rolled parsers — one of which took the alias by
position in the split path and only worked because the new segment happens
to leave the component count unchanged.

TestChainAddressBuiltOnlyHere walks every .go file in the module and fails
on a chain address in a string literal, so the segment cannot drift back;
prose is free to name a route. TestGuardCatchesEachShape plants each
forbidden shape and requires the guard to find it.

The endpoint box and the eleven-chain table were the same list written
twice; there is one list, rendered once, measured rather than hand-padded.
The generated CLI reference and the faucet manifest compose through the
same function.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-30 15:05:42 -07:00

342 lines
10 KiB
Go

// Copyright (C) 2022-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package utils
import (
"crypto/rand"
"encoding/hex"
"encoding/pem"
"errors"
"fmt"
"net/url"
"os"
"path/filepath"
"time"
"github.com/luxfi/cli/pkg/route"
"github.com/luxfi/constants"
"github.com/luxfi/crypto/bls"
"github.com/luxfi/crypto/bls/signer/localsigner"
"github.com/luxfi/crypto/mldsa"
"github.com/luxfi/crypto/secp256k1"
evmclient "github.com/luxfi/evm/plugin/evm/client"
"github.com/luxfi/ids"
"github.com/luxfi/sdk/platformvm"
luxtls "github.com/luxfi/tls"
)
// SplitRPCURI parses a chain's RPC URL, such as the C-Chain's at
// http://127.0.0.1:9650/v1/chain/C/rpc, into the node endpoint
// ("http://127.0.0.1:9650") and the chain alias ("C").
func SplitRPCURI(rpcURL string) (string, string, error) {
u, err := url.Parse(rpcURL)
if err != nil {
return "", "", fmt.Errorf("invalid RPC URL: %w", err)
}
blockchainID, ok := route.Alias(u.Path)
if !ok {
return "", "", fmt.Errorf("could not extract blockchain ID from URL: %s", rpcURL)
}
// Build the network endpoint (scheme + host)
networkEndpoint := fmt.Sprintf("%s://%s", u.Scheme, u.Host)
return networkEndpoint, blockchainID, nil
}
func NewBlsSecretKeyBytes() ([]byte, error) {
blsSignerKey, err := localsigner.New()
if err != nil {
return nil, err
}
return blsSignerKey.ToBytes(), nil
}
func ToNodeID(certBytes []byte) (ids.NodeID, error) {
block, _ := pem.Decode(certBytes)
if block == nil {
return ids.EmptyNodeID, fmt.Errorf("failed to decode certificate")
}
cert, err := luxtls.ParseCertificate(block.Bytes)
if err != nil {
return ids.EmptyNodeID, err
}
idsCert := &ids.Certificate{
Raw: cert.Raw,
PublicKey: cert.PublicKey,
}
return ids.NodeIDFromCert(idsCert), nil
}
func ToBLSPoP(keyBytes []byte) (
[]byte, // bls public key
[]byte, // bls proof of possession
error,
) {
localSigner, err := localsigner.FromBytes(keyBytes)
if err != nil {
return nil, nil, err
}
// LocalSigner has the secret key as a private field, but we can get the public key
// and sign a proof of possession directly
pk := localSigner.PublicKey()
pkBytes := bls.PublicKeyToCompressedBytes(pk)
sig, err := localSigner.SignProofOfPossession(pkBytes)
if err != nil {
return nil, nil, err
}
sigBytes := bls.SignatureToBytes(sig)
return pkBytes, sigBytes, nil
}
// GetNodeParams returns node id, bls public key and bls proof of possession
func GetNodeParams(nodeDir string) (
ids.NodeID,
[]byte, // bls public key
[]byte, // bls proof of possession
error,
) {
certBytes, err := os.ReadFile(filepath.Join(nodeDir, constants.StakerCertFileName)) //nolint:gosec // G304: Reading staker cert from node directory
if err != nil {
return ids.EmptyNodeID, nil, nil, err
}
nodeID, err := ToNodeID(certBytes)
if err != nil {
return ids.EmptyNodeID, nil, nil, err
}
blsKeyBytes, err := os.ReadFile(filepath.Join(nodeDir, constants.BLSKeyFileName)) //nolint:gosec // G304: Reading BLS key from node directory
if err != nil {
return ids.EmptyNodeID, nil, nil, err
}
blsPub, blsPoP, err := ToBLSPoP(blsKeyBytes)
if err != nil {
return ids.EmptyNodeID, nil, nil, err
}
return nodeID, blsPub, blsPoP, nil
}
func GetRemainingValidationTime(networkEndpoint string, nodeID ids.NodeID, chainID ids.ID, startTime time.Time) (time.Duration, error) {
ctx, cancel := GetAPIContext()
defer cancel()
platformCli := platformvm.NewClient(networkEndpoint)
vs, err := platformCli.GetCurrentValidators(ctx, chainID, nil)
cancel()
if err != nil {
return 0, err
}
for _, v := range vs {
if v.NodeID == nodeID {
return time.Unix(int64(v.EndTime), 0).Sub(startTime), nil //nolint:gosec // G115: EndTime is positive Unix timestamp
}
}
return 0, errors.New("nodeID not found in validator set: " + nodeID.String())
}
// GetL1ValidatorUptimeSeconds returns the uptime of the L1 validator
func GetL1ValidatorUptimeSeconds(rpcURL string, nodeID ids.NodeID) (uint64, error) {
ctx, cancel := GetAPIContext()
defer cancel()
networkEndpoint, blockchainID, err := SplitRPCURI(rpcURL)
if err != nil {
return 0, err
}
evmCli := evmclient.NewClient(networkEndpoint, blockchainID)
validators, err := evmCli.GetCurrentValidators(ctx, []ids.NodeID{nodeID})
if err != nil {
return 0, err
}
if len(validators) > 0 {
deductibleSeconds := uint64(constants.ValidatorUptimeDeductible.Seconds())
if validators[0].UptimeSeconds > deductibleSeconds {
return validators[0].UptimeSeconds - deductibleSeconds, nil
}
return 0, nil
}
return 0, errors.New("nodeID not found in validator set: " + nodeID.String())
}
// NewRingSigKeyBytes generates a new secp256k1 private key and returns it as bytes
// Note: "Ring-signature" key derivation name - we use standard secp256k1 for now
func NewRingSigKeyBytes() ([]byte, error) {
privKey, err := secp256k1.NewPrivateKey()
if err != nil {
return nil, fmt.Errorf("failed to generate secp256k1 key: %w", err)
}
return privKey.Bytes(), nil
}
// ToRingSigPublicKey converts secp256k1 private key bytes to public key bytes
func ToRingSigPublicKey(keyBytes []byte) ([]byte, error) {
privKey, err := secp256k1.ToPrivateKey(keyBytes)
if err != nil {
return nil, fmt.Errorf("failed to parse secp256k1 private key: %w", err)
}
return privKey.PublicKey().Bytes(), nil
}
// NewMLDSAKeyBytes generates a new ML-DSA private key and returns it as bytes
// Uses MLDSA65 (192-bit security, NIST Level 3) as the default
func NewMLDSAKeyBytes() ([]byte, error) {
privKey, err := mldsa.GenerateKey(rand.Reader, mldsa.MLDSA65)
if err != nil {
return nil, fmt.Errorf("failed to generate ML-DSA key: %w", err)
}
return privKey.Bytes(), nil
}
// ToMLDSAPublicKey converts ML-DSA private key bytes to public key bytes
func ToMLDSAPublicKey(keyBytes []byte) ([]byte, error) {
privKey, err := mldsa.PrivateKeyFromBytes(mldsa.MLDSA65, keyBytes)
if err != nil {
return nil, fmt.Errorf("failed to parse ML-DSA private key: %w", err)
}
return privKey.PublicKey.Bytes(), nil
}
// QuantumKeys holds all quantum-safe keys for a validator
type QuantumKeys struct {
BLSSecretKey []byte
BLSPublicKey []byte
BLSPoP []byte
RingSigSecretKey []byte
RingSigPublicKey []byte
MLDSASecretKey []byte
MLDSAPublicKey []byte
}
// GenerateAllQuantumKeys generates BLS, Corona, and ML-DSA keys for a validator
func GenerateAllQuantumKeys() (*QuantumKeys, error) {
keys := &QuantumKeys{}
var err error
// Generate BLS key
keys.BLSSecretKey, err = NewBlsSecretKeyBytes()
if err != nil {
return nil, fmt.Errorf("BLS key generation failed: %w", err)
}
keys.BLSPublicKey, keys.BLSPoP, err = ToBLSPoP(keys.BLSSecretKey)
if err != nil {
return nil, fmt.Errorf("BLS public key derivation failed: %w", err)
}
// Generate Corona key
keys.RingSigSecretKey, err = NewRingSigKeyBytes()
if err != nil {
return nil, fmt.Errorf("corona key generation failed: %w", err)
}
keys.RingSigPublicKey, err = ToRingSigPublicKey(keys.RingSigSecretKey)
if err != nil {
return nil, fmt.Errorf("corona public key derivation failed: %w", err)
}
// Generate ML-DSA key
keys.MLDSASecretKey, err = NewMLDSAKeyBytes()
if err != nil {
return nil, fmt.Errorf("ML-DSA key generation failed: %w", err)
}
keys.MLDSAPublicKey, err = ToMLDSAPublicKey(keys.MLDSASecretKey)
if err != nil {
return nil, fmt.Errorf("ML-DSA public key derivation failed: %w", err)
}
return keys, nil
}
// SaveQuantumKeys saves all quantum keys to the specified directory
func SaveQuantumKeys(nodeDir string, keys *QuantumKeys) error {
// Save BLS key
blsPath := filepath.Join(nodeDir, constants.BLSKeyFileName)
if err := os.WriteFile(blsPath, keys.BLSSecretKey, 0o600); err != nil {
return fmt.Errorf("failed to save BLS key: %w", err)
}
// Save Corona key (hex encoded)
ringSigPath := filepath.Join(nodeDir, constants.RingSigKeyFileName)
ringSigHex := hex.EncodeToString(keys.RingSigSecretKey)
if err := os.WriteFile(ringSigPath, []byte(ringSigHex), 0o600); err != nil {
return fmt.Errorf("failed to save Corona key: %w", err)
}
// Save ML-DSA key (hex encoded)
mldsaPath := filepath.Join(nodeDir, constants.MLDSAKeyFileName)
mldsaHex := hex.EncodeToString(keys.MLDSASecretKey)
if err := os.WriteFile(mldsaPath, []byte(mldsaHex), 0o600); err != nil {
return fmt.Errorf("failed to save ML-DSA key: %w", err)
}
return nil
}
// LoadQuantumKeys loads all quantum keys from the specified directory
func LoadQuantumKeys(nodeDir string) (*QuantumKeys, error) {
keys := &QuantumKeys{}
var err error
// Load BLS key
blsPath := filepath.Join(nodeDir, constants.BLSKeyFileName)
keys.BLSSecretKey, err = os.ReadFile(blsPath) //nolint:gosec // G304: Reading from node's key directory
if err != nil {
return nil, fmt.Errorf("failed to load BLS key: %w", err)
}
keys.BLSPublicKey, keys.BLSPoP, err = ToBLSPoP(keys.BLSSecretKey)
if err != nil {
return nil, fmt.Errorf("failed to derive BLS public key: %w", err)
}
// Load Corona key
ringSigPath := filepath.Join(nodeDir, constants.RingSigKeyFileName)
ringSigHex, err := os.ReadFile(ringSigPath) //nolint:gosec // G304: Reading from node's key directory
if err != nil {
return nil, fmt.Errorf("failed to load Corona key: %w", err)
}
keys.RingSigSecretKey, err = hex.DecodeString(string(ringSigHex))
if err != nil {
return nil, fmt.Errorf("failed to decode Corona key: %w", err)
}
keys.RingSigPublicKey, err = ToRingSigPublicKey(keys.RingSigSecretKey)
if err != nil {
return nil, fmt.Errorf("failed to derive Corona public key: %w", err)
}
// Load ML-DSA key
mldsaPath := filepath.Join(nodeDir, constants.MLDSAKeyFileName)
mldsaHex, err := os.ReadFile(mldsaPath) //nolint:gosec // G304: Reading from node's key directory
if err != nil {
return nil, fmt.Errorf("failed to load ML-DSA key: %w", err)
}
keys.MLDSASecretKey, err = hex.DecodeString(string(mldsaHex))
if err != nil {
return nil, fmt.Errorf("failed to decode ML-DSA key: %w", err)
}
keys.MLDSAPublicKey, err = ToMLDSAPublicKey(keys.MLDSASecretKey)
if err != nil {
return nil, fmt.Errorf("failed to derive ML-DSA public key: %w", err)
}
return keys, nil
}
// GetQuantumNodeParams returns node id and all quantum public keys
func GetQuantumNodeParams(nodeDir string) (
ids.NodeID,
*QuantumKeys,
error,
) {
certBytes, err := os.ReadFile(filepath.Join(nodeDir, constants.StakerCertFileName)) //nolint:gosec // G304: Reading from node's directory
if err != nil {
return ids.EmptyNodeID, nil, err
}
nodeID, err := ToNodeID(certBytes)
if err != nil {
return ids.EmptyNodeID, nil, err
}
keys, err := LoadQuantumKeys(nodeDir)
if err != nil {
return ids.EmptyNodeID, nil, err
}
return nodeID, keys, nil
}