mirror of
https://github.com/luxfi/utxo.git
synced 2026-10-30 15:13:57 +00:00
Adds four new Fx (feature-extension) packages for X-Chain UTXO signing schemes beyond secp256k1: - ed25519fx — Ed25519 (EdDSA on Curve25519) - schnorrfx — Schnorr (classical) - secp256r1fx — P-256 / NIST - slhdsafx — SLH-DSA (FIPS 205 hash-based PQ) Plus mldsafx updates to match (ML-DSA / FIPS 204 lattice PQ). Each fx package exports Fx, Credential, Keychain, OutputOwners, Input following the existing secp256k1fx pattern. Gitignore .DS_Store.
123 lines
3.5 KiB
Go
123 lines
3.5 KiB
Go
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package slhdsafx
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/luxfi/crypto/hash"
|
|
"github.com/luxfi/formatting"
|
|
"github.com/luxfi/ids"
|
|
)
|
|
|
|
var (
|
|
ErrNilOutputOwners = errors.New("nil SLH-DSA output owners")
|
|
ErrNilPublicKey = errors.New("nil SLH-DSA public key")
|
|
ErrThresholdExceeded = errors.New("threshold exceeds number of addresses")
|
|
ErrOutputUnoptimized = errors.New("output representation should be optimized")
|
|
ErrOutputNotSpendable = errors.New("output not yet spendable")
|
|
ErrInvalidPubKeyLength = errors.New("invalid SLH-DSA public key length")
|
|
)
|
|
|
|
// OutputOwners describes who can spend an output locked with SLH-DSA keys.
|
|
type OutputOwners struct {
|
|
// Level indicates the SLH-DSA parameter set for all addresses
|
|
Level SecurityLevel `serialize:"true" json:"securityLevel"`
|
|
// Locktime is the Unix timestamp after which this output can be spent
|
|
Locktime uint64 `serialize:"true" json:"locktime"`
|
|
// Threshold is the number of signatures required to spend
|
|
Threshold uint32 `serialize:"true" json:"threshold"`
|
|
// Addrs are the SLH-DSA public keys that can sign to spend
|
|
Addrs [][]byte `serialize:"true" json:"addresses"`
|
|
}
|
|
|
|
// Verify validates the output owners structure
|
|
func (out *OutputOwners) Verify() error {
|
|
if out == nil {
|
|
return ErrNilOutputOwners
|
|
}
|
|
|
|
switch {
|
|
case out.Threshold > uint32(len(out.Addrs)):
|
|
return ErrThresholdExceeded
|
|
case out.Threshold == 0 && len(out.Addrs) > 0:
|
|
return ErrOutputUnoptimized
|
|
}
|
|
|
|
expectedPKLen := out.Level.PubKeyLen()
|
|
if expectedPKLen == 0 {
|
|
return ErrInvalidSecLevel
|
|
}
|
|
|
|
for i, addr := range out.Addrs {
|
|
if len(addr) != expectedPKLen {
|
|
return fmt.Errorf("%w: address %d has length %d, expected %d for %s",
|
|
ErrInvalidPubKeyLength, i, len(addr), expectedPKLen, out.Level)
|
|
}
|
|
}
|
|
|
|
// Verify sorted order
|
|
for i := 1; i < len(out.Addrs); i++ {
|
|
if string(out.Addrs[i-1]) >= string(out.Addrs[i]) {
|
|
return fmt.Errorf("addresses not sorted at index %d", i)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Addresses returns short IDs derived from the SLH-DSA public keys.
|
|
// Public keys are hashed via SHA256+RIPEMD160 to produce 20-byte addresses.
|
|
func (out *OutputOwners) Addresses() []ids.ShortID {
|
|
addrs := make([]ids.ShortID, len(out.Addrs))
|
|
for i, pk := range out.Addrs {
|
|
addrBytes := hash.PubkeyBytesToAddress(pk)
|
|
addr, err := ids.ToShortID(addrBytes)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("hash160 produced wrong length: %v", err))
|
|
}
|
|
addrs[i] = addr
|
|
}
|
|
return addrs
|
|
}
|
|
|
|
// MarshalJSON marshals the output owners to JSON
|
|
func (out *OutputOwners) MarshalJSON() ([]byte, error) {
|
|
addrs := make([]string, len(out.Addrs))
|
|
for i, addr := range out.Addrs {
|
|
addrStr, err := formatting.Encode(formatting.HexNC, addr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("couldn't encode address %d: %w", i, err)
|
|
}
|
|
addrs[i] = addrStr
|
|
}
|
|
|
|
return json.Marshal(map[string]interface{}{
|
|
"securityLevel": out.Level.String(),
|
|
"locktime": out.Locktime,
|
|
"threshold": out.Threshold,
|
|
"addresses": addrs,
|
|
})
|
|
}
|
|
|
|
// Equals returns true if the provided owners create the same condition
|
|
func (out *OutputOwners) Equals(other *OutputOwners) bool {
|
|
if out == other {
|
|
return true
|
|
}
|
|
if out == nil || other == nil || out.Level != other.Level ||
|
|
out.Locktime != other.Locktime || out.Threshold != other.Threshold ||
|
|
len(out.Addrs) != len(other.Addrs) {
|
|
return false
|
|
}
|
|
for i, addr := range out.Addrs {
|
|
if string(addr) != string(other.Addrs[i]) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|