mirror of
https://github.com/luxfi/ids.git
synced 2026-10-30 16:22:36 +00:00
Replaces github.com/luxfi/codec/wrappers.Packer with direct
encoding/binary.BigEndian writes. Byte-for-byte equal preimage, so
LP-77 validationID derivation hashes do not change.
Adds id_packing_test.go with:
- referencePacker mirroring the historical codec/wrappers.Packer
byte layout (PackLong / PackInt / PackFixedBytes)
- byte-equal cross-checks for Prefix + Append against the reference
- golden vector locking the LP-77 ConvertSubnetToL1Tx layout
Drops github.com/luxfi/codec from go.mod (the only consumer in this
module). Lets warp + api fully decompose from codec/wrappers downstream.
295 lines
7.4 KiB
Go
295 lines
7.4 KiB
Go
// Copyright (C) 2020-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package ids
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/luxfi/crypto/cb58"
|
|
"github.com/luxfi/crypto/hash"
|
|
"github.com/mr-tron/base58/base58"
|
|
)
|
|
|
|
const (
|
|
// uint32Len is the byte length of a big-endian-packed uint32.
|
|
uint32Len = 4
|
|
// uint64Len is the byte length of a big-endian-packed uint64.
|
|
uint64Len = 8
|
|
)
|
|
|
|
// Sortable is the interface for types that can be compared for ordering.
|
|
type Sortable[T any] interface {
|
|
Compare(T) int
|
|
}
|
|
|
|
const (
|
|
IDLen = 32
|
|
nullStr = "null"
|
|
)
|
|
|
|
var (
|
|
// Empty is a useful all zero value
|
|
Empty = ID{}
|
|
|
|
errMissingQuotes = errors.New("first and last characters should be quotes")
|
|
|
|
_ Sortable[ID] = ID{}
|
|
)
|
|
|
|
// ID wraps a 32 byte hash used as an identifier
|
|
type ID [IDLen]byte
|
|
|
|
// ToID attempt to convert a byte slice into an id
|
|
func ToID(bytes []byte) (ID, error) {
|
|
return hash.ToHash256(bytes)
|
|
}
|
|
|
|
// FromString is the inverse of ID.String()
|
|
func FromString(idStr string) (ID, error) {
|
|
// Check if this is a well-known native chain string
|
|
if id, ok := NativeChainFromString(idStr); ok {
|
|
return id, nil
|
|
}
|
|
|
|
bytes, err := cb58.Decode(idStr)
|
|
if err != nil {
|
|
return ID{}, err
|
|
}
|
|
return ToID(bytes)
|
|
}
|
|
|
|
// FromStringWithForce is like FromString but can force ignore checksum errors
|
|
func FromStringWithForce(idStr string, forceIgnoreChecksum bool) (ID, error) {
|
|
// Check if this is a well-known native chain string
|
|
if id, ok := NativeChainFromString(idStr); ok {
|
|
return id, nil
|
|
}
|
|
|
|
bytes, err := cb58.Decode(idStr)
|
|
if err != nil {
|
|
// If force flag is set and it's a checksum error, try raw base58 decode
|
|
if forceIgnoreChecksum && err == cb58.ErrBadChecksum {
|
|
// Decode raw base58 and take first 32 bytes
|
|
rawBytes, decodeErr := base58.Decode(idStr)
|
|
if decodeErr == nil && len(rawBytes) >= IDLen {
|
|
var id ID
|
|
copy(id[:], rawBytes[:IDLen])
|
|
return id, nil
|
|
}
|
|
}
|
|
return ID{}, err
|
|
}
|
|
return ToID(bytes)
|
|
}
|
|
|
|
// FromStringOrPanic is the same as FromString, but will panic on error
|
|
func FromStringOrPanic(idStr string) ID {
|
|
id, err := FromString(idStr)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return id
|
|
}
|
|
|
|
func (id ID) MarshalJSON() ([]byte, error) {
|
|
// Check if this is a well-known native chain ID
|
|
if nativeStr := NativeChainString(id); nativeStr != "" {
|
|
return []byte(`"` + nativeStr + `"`), nil
|
|
}
|
|
|
|
str, err := cb58.Encode(id[:])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return []byte(`"` + str + `"`), nil
|
|
}
|
|
|
|
func (id *ID) UnmarshalJSON(b []byte) error {
|
|
str := string(b)
|
|
if str == nullStr { // If "null", do nothing
|
|
return nil
|
|
} else if len(str) < 2 {
|
|
return errMissingQuotes
|
|
}
|
|
|
|
lastIndex := len(str) - 1
|
|
if str[0] != '"' || str[lastIndex] != '"' {
|
|
return errMissingQuotes
|
|
}
|
|
|
|
// Handle empty string - treat as zero ID
|
|
innerStr := str[1:lastIndex]
|
|
if innerStr == "" {
|
|
*id = Empty
|
|
return nil
|
|
}
|
|
|
|
// Check if this is a well-known native chain string
|
|
if nativeID, ok := NativeChainFromString(innerStr); ok {
|
|
*id = nativeID
|
|
return nil
|
|
}
|
|
|
|
// Parse CB58 formatted string to bytes
|
|
bytes, err := cb58.Decode(innerStr)
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't decode ID to bytes: %w", err)
|
|
}
|
|
*id, err = ToID(bytes)
|
|
return err
|
|
}
|
|
|
|
// UnmarshalText decodes an unquoted CB58/native ID string. It is the inverse
|
|
// of MarshalText, which returns the unquoted id.String(). Used by Go's
|
|
// encoding/json when ID appears as a map key (json decodes map keys via
|
|
// TextUnmarshaler, not via JSON), as well as by encoding/xml, flag.Value,
|
|
// and any other TextUnmarshaler consumer.
|
|
//
|
|
// Historical note: this previously delegated to UnmarshalJSON, which
|
|
// required the input to be quoted. That broke json.Unmarshal of any
|
|
// map[ids.ID]V because the stdlib passes UNQUOTED keys to UnmarshalText
|
|
// (TextUnmarshaler contract). The asymmetry surfaced as
|
|
// "first and last characters should be quotes" on
|
|
// --chain-aliases-file and --chain-aliases-file-content inputs.
|
|
func (id *ID) UnmarshalText(text []byte) error {
|
|
str := string(text)
|
|
if str == nullStr || str == "" {
|
|
*id = Empty
|
|
return nil
|
|
}
|
|
if nativeID, ok := NativeChainFromString(str); ok {
|
|
*id = nativeID
|
|
return nil
|
|
}
|
|
bytes, err := cb58.Decode(str)
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't decode ID to bytes: %w", err)
|
|
}
|
|
*id, err = ToID(bytes)
|
|
return err
|
|
}
|
|
|
|
// Prefix this id to create a more selective id. This can be used to store
|
|
// multiple values under the same key. For example:
|
|
// prefix1(id) -> confidence
|
|
// prefix2(id) -> vertex
|
|
// This will return a new id and not modify the original id.
|
|
//
|
|
// Wire format (byte-for-byte equal to the historical
|
|
// codec/wrappers.Packer encoding): prefix0 || prefix1 || ... || id, each
|
|
// prefix big-endian uint64, id raw 32-byte fixed bytes.
|
|
func (id ID) Prefix(prefixes ...uint64) ID {
|
|
buf := make([]byte, len(prefixes)*uint64Len+IDLen)
|
|
off := 0
|
|
for _, prefix := range prefixes {
|
|
binary.BigEndian.PutUint64(buf[off:], prefix)
|
|
off += uint64Len
|
|
}
|
|
copy(buf[off:], id[:])
|
|
return hash.ComputeHash256Array(buf)
|
|
}
|
|
|
|
// Append this id with the provided suffixes and re-hash the result. This
|
|
// returns a new ID and does not modify the original ID.
|
|
//
|
|
// This is used to generate LP-77 validationIDs.
|
|
//
|
|
// Ref: https://github.com/luxfi/LPs/tree/e333b335c34c8692d84259d21bd07b2bb849dc2c/LPs/77-reinventing-subnets#convertsubnettol1tx
|
|
//
|
|
// Wire format (byte-for-byte equal to the historical
|
|
// codec/wrappers.Packer encoding): id || suffix0 || suffix1 || ..., id raw
|
|
// 32-byte fixed bytes, each suffix big-endian uint32.
|
|
func (id ID) Append(suffixes ...uint32) ID {
|
|
buf := make([]byte, IDLen+len(suffixes)*uint32Len)
|
|
copy(buf, id[:])
|
|
off := IDLen
|
|
for _, suffix := range suffixes {
|
|
binary.BigEndian.PutUint32(buf[off:], suffix)
|
|
off += uint32Len
|
|
}
|
|
return hash.ComputeHash256Array(buf)
|
|
}
|
|
|
|
// XOR this id and the provided id and return the resulting id.
|
|
//
|
|
// Note: this id is not modified.
|
|
func (id ID) XOR(other ID) ID {
|
|
for i, b := range other {
|
|
id[i] ^= b
|
|
}
|
|
return id
|
|
}
|
|
|
|
// Bit returns the bit value at the ith index of the byte array. Returns 0 or 1
|
|
func (id ID) Bit(i uint) int {
|
|
byteIndex := i / BitsPerByte
|
|
bitIndex := i % BitsPerByte
|
|
|
|
b := id[byteIndex]
|
|
|
|
// b = [7, 6, 5, 4, 3, 2, 1, 0]
|
|
|
|
b >>= bitIndex
|
|
|
|
// b = [0, ..., bitIndex + 1, bitIndex]
|
|
// 1 = [0, 0, 0, 0, 0, 0, 0, 1]
|
|
|
|
b &= 1
|
|
|
|
// b = [0, 0, 0, 0, 0, 0, 0, bitIndex]
|
|
|
|
return int(b)
|
|
}
|
|
|
|
// Hex returns a hex encoded string of this id.
|
|
func (id ID) Hex() string {
|
|
return hex.EncodeToString(id[:])
|
|
}
|
|
|
|
// ToShortID converts this ID to a ShortID by taking the first 20 bytes
|
|
func (id ID) ToShortID() ShortID {
|
|
var shortID ShortID
|
|
copy(shortID[:], id[:ShortIDLen])
|
|
return shortID
|
|
}
|
|
|
|
func (id ID) String() string {
|
|
// Check if this is a well-known native chain ID
|
|
if nativeStr := NativeChainString(id); nativeStr != "" {
|
|
return nativeStr
|
|
}
|
|
|
|
// We assume that the maximum size of a byte slice that
|
|
// can be stringified is at least the length of an ID
|
|
s, _ := cb58.Encode(id[:])
|
|
return s
|
|
}
|
|
|
|
func (id ID) MarshalText() ([]byte, error) {
|
|
return []byte(id.String()), nil
|
|
}
|
|
|
|
func (id ID) Compare(other ID) int {
|
|
return bytes.Compare(id[:], other[:])
|
|
}
|
|
|
|
// IsZero returns true if the ID is all zeros
|
|
func (id ID) IsZero() bool {
|
|
return id == Empty
|
|
}
|
|
|
|
// GenerateNodeIDFromBytes generates a node ID from bytes
|
|
func GenerateNodeIDFromBytes(bytes []byte) ID {
|
|
return hash.ComputeHash256Array(bytes)
|
|
}
|
|
|
|
// Checksum256 computes SHA256 checksum and returns an ID
|
|
func Checksum256(data []byte) ID {
|
|
return hash.ComputeHash256Array(data)
|
|
}
|