mirror of
https://github.com/luxfi/ids.git
synced 2026-10-30 16:22:36 +00:00
UnmarshalText delegated to UnmarshalJSON, so it demanded quotes around a value that was never JSON. Every caller that keeps the TextUnmarshaler contract met "first and last characters should be quotes": encoding/json handing over a map KEY, encoding/xml, flag.Value, and a URL binder reading a query value — where the failure is silent and the node id arrives as the zero node with a 200 on the answer. ID.UnmarshalText was corrected this way already, and its comment records the same symptom on --chain-aliases-file. This is that correction on the other two, with the reading in ONE place: UnmarshalText holds it and UnmarshalJSON unquotes and calls it, so the two spellings cannot drift.
126 lines
3.7 KiB
Go
126 lines
3.7 KiB
Go
// Copyright (C) 2020-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package ids
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/luxfi/crypto/hash"
|
|
)
|
|
|
|
const (
|
|
NodeIDPrefix = "NodeID-"
|
|
NodeIDLen = ShortIDLen
|
|
)
|
|
|
|
var (
|
|
EmptyNodeID = NodeID{}
|
|
|
|
errShortNodeID = errors.New("insufficient NodeID length")
|
|
|
|
_ Sortable[NodeID] = NodeID{}
|
|
)
|
|
|
|
type NodeID ShortID
|
|
|
|
func (id NodeID) String() string {
|
|
return ShortID(id).PrefixedString(NodeIDPrefix)
|
|
}
|
|
|
|
func (id NodeID) Bytes() []byte {
|
|
return id[:]
|
|
}
|
|
|
|
func (id NodeID) MarshalJSON() ([]byte, error) {
|
|
return []byte(`"` + id.String() + `"`), nil
|
|
}
|
|
|
|
func (id NodeID) MarshalText() ([]byte, error) {
|
|
return []byte(id.String()), nil
|
|
}
|
|
|
|
// UnmarshalJSON reads the same prefixed word, quoted. The reading itself is in
|
|
// UnmarshalText and is not written twice.
|
|
func (id *NodeID) UnmarshalJSON(b []byte) error {
|
|
str := string(b)
|
|
if str == nullStr { // If "null", do nothing
|
|
return nil
|
|
}
|
|
if len(str) < 2 || str[0] != '"' || str[len(str)-1] != '"' {
|
|
return errMissingQuotes
|
|
}
|
|
return id.UnmarshalText([]byte(str[1 : len(str)-1]))
|
|
}
|
|
|
|
// UnmarshalText reads the prefixed word ITSELF, unquoted — which is what a
|
|
// TextUnmarshaler is handed, by encoding/json for a map key, by encoding/xml,
|
|
// by flag.Value and by a URL binder. It delegated to UnmarshalJSON, so every
|
|
// one of those callers met "first and last characters should be quotes" for a
|
|
// value that was never JSON, and a node id named in a URL arrived as the zero
|
|
// node. [ID.UnmarshalText] was already corrected this way; this is the same
|
|
// correction on the other two.
|
|
func (id *NodeID) UnmarshalText(text []byte) error {
|
|
str := string(text)
|
|
if str == nullStr || str == "" {
|
|
return nil
|
|
}
|
|
if len(str) <= len(NodeIDPrefix) {
|
|
return fmt.Errorf("%w: expected to be > %d", errShortNodeID, len(NodeIDPrefix))
|
|
}
|
|
var err error
|
|
*id, err = NodeIDFromString(str)
|
|
return err
|
|
}
|
|
|
|
func (id NodeID) Compare(other NodeID) int {
|
|
return bytes.Compare(id[:], other[:])
|
|
}
|
|
|
|
// ToNodeID attempt to convert a byte slice into a node id
|
|
func ToNodeID(bytes []byte) (NodeID, error) {
|
|
nodeID, err := ToShortID(bytes)
|
|
return NodeID(nodeID), err
|
|
}
|
|
|
|
func NodeIDFromCert(cert *Certificate) NodeID {
|
|
return hash.ComputeHash160Array(
|
|
hash.ComputeHash256(cert.Raw),
|
|
)
|
|
}
|
|
|
|
// NodeIDFromString is the inverse of NodeID.String()
|
|
func NodeIDFromString(nodeIDStr string) (NodeID, error) {
|
|
asShort, err := ShortFromPrefixedString(nodeIDStr, NodeIDPrefix)
|
|
if err != nil {
|
|
return NodeID{}, err
|
|
}
|
|
return NodeID(asShort), nil
|
|
}
|
|
|
|
// NodeIDPrefix for the legacy ML-DSA NodeID derivation. Retained for
|
|
// backwards compatibility with chains that registered validators under the
|
|
// pre-v1 derivation; new strict-PQ chains use the canonical SHAKE256-384
|
|
// derivation in node_id_scheme.go (NodeIDScheme.DeriveMLDSA), which
|
|
// domain-separates by chain id AND scheme byte and produces a 48-byte
|
|
// commitment whose 20-byte prefix is the NodeID.
|
|
const NodeIDMLDSADomainPrefix = "LuxNodeID/v1"
|
|
|
|
// NodeIDFromMLDSA derives a 20-byte NodeID from an ML-DSA public key
|
|
// using the legacy (pre-chain-binding) derivation. Kept stable for
|
|
// existing call sites; new strict-PQ chains MUST use
|
|
// NodeIDScheme.DeriveMLDSA or TypedNodeIDFromMLDSA, which bind the
|
|
// chain id and scheme byte into the digest and return the full
|
|
// 48-byte SHAKE256-384 commitment alongside the truncated NodeID.
|
|
//
|
|
// NodeID = RIPEMD160(SHA256("LuxNodeID/v1" || mldsa_pubkey_bytes))
|
|
func NodeIDFromMLDSA(mldsaPubKey []byte) NodeID {
|
|
prefix := []byte(NodeIDMLDSADomainPrefix)
|
|
data := make([]byte, len(prefix)+len(mldsaPubKey))
|
|
copy(data, prefix)
|
|
copy(data[len(prefix):], mldsaPubKey)
|
|
return hash.ComputeHash160Array(hash.ComputeHash256(data))
|
|
}
|