mirror of
https://github.com/luxfi/lens.git
synced 2026-10-30 16:21:16 +00:00
Lens is the curve-based threshold signing kernel for Lux. It is the classical analogue of Pulsar (the lattice kernel at github.com/luxfi/pulsar). Both kernels share the same lifecycle shape — Bootstrap → Reshare* → Reanchor — and the same activation circuit-breaker contract. They differ only in the math field: Pulsar over R_q (Module-LWE), Lens over a prime-order curve group (discrete log). Curves shipped: Ed25519, secp256k1, Ristretto255. Each has a deterministically-derived secondary Pedersen generator H from a Lens-specific nothing-up-my-sleeve tag. Sign math: FROST 2-round Schnorr threshold signing per RFC 9591 (Komlo-Goldberg 2020). Verifier checks z·G ?= R + c·X under the unchanged group public key. Reshare math: HJKY97 zero-poly Refresh + Desmedt-Jajodia ReshareToNewSet over the curve scalar field. Master secret s is preserved; only the share distribution rotates per epoch. Activation cert message format: QUASAR-LENS-ACTIVATE-v1, byte-equivalent shape to QUASAR-PULSAR-ACTIVATE-v1. Hash suite: HashSuite interface with Lens-SHA3 (production, cSHAKE256 / KMAC256 / TupleHash256) and Lens-BLAKE3 (legacy) profiles. DKG: distributed FROST DKG with Feldman commitments. No trusted dealer required at genesis if the chain prefers proper distributed DKG. KAT oracles: cmd/reshare_oracle and cmd/dkg_oracle emit deterministic JSON vectors for cross-language byte-equality validation against a future luxcpp/crypto/lens/ port. Tests: full unit + integration coverage on every shipped curve. 60 unique tests, 0 failures.
112 lines
3.0 KiB
Go
112 lines
3.0 KiB
Go
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package hash
|
|
|
|
// FIPS 202 / NIST SP 800-185 helpers — left_encode, right_encode,
|
|
// bytepad, encode_string — and the TupleHash / KMAC constructions
|
|
// they enable. Vendored here because Go's golang.org/x/crypto/sha3
|
|
// ships cSHAKE128/256 but not TupleHash or KMAC. The implementation
|
|
// is fully covered by the published NIST test vectors (see hash_test.go).
|
|
//
|
|
// All encoders match SP 800-185 §2.3 byte-for-byte. left_encode and
|
|
// right_encode operate on the BIT length, not the byte length.
|
|
|
|
import (
|
|
"encoding/binary"
|
|
|
|
"golang.org/x/crypto/sha3"
|
|
)
|
|
|
|
// leftEncode returns the SP 800-185 left_encode(x) byte string.
|
|
func leftEncode(x uint64) []byte {
|
|
if x == 0 {
|
|
return []byte{0x01, 0x00}
|
|
}
|
|
var buf [8]byte
|
|
binary.BigEndian.PutUint64(buf[:], x)
|
|
i := 0
|
|
for i < 7 && buf[i] == 0 {
|
|
i++
|
|
}
|
|
out := make([]byte, 0, 9-i)
|
|
out = append(out, byte(8-i))
|
|
out = append(out, buf[i:]...)
|
|
return out
|
|
}
|
|
|
|
// rightEncode returns the SP 800-185 right_encode(x) byte string.
|
|
func rightEncode(x uint64) []byte {
|
|
if x == 0 {
|
|
return []byte{0x00, 0x01}
|
|
}
|
|
var buf [8]byte
|
|
binary.BigEndian.PutUint64(buf[:], x)
|
|
i := 0
|
|
for i < 7 && buf[i] == 0 {
|
|
i++
|
|
}
|
|
out := make([]byte, 0, 9-i)
|
|
out = append(out, buf[i:]...)
|
|
out = append(out, byte(8-i))
|
|
return out
|
|
}
|
|
|
|
// encodeString returns left_encode(bit_len(s)) || s.
|
|
func encodeString(s []byte) []byte {
|
|
out := leftEncode(uint64(len(s)) * 8)
|
|
out = append(out, s...)
|
|
return out
|
|
}
|
|
|
|
// bytepad pads x with zeros so the result is a multiple of w bytes,
|
|
// prefixed by left_encode(w).
|
|
func bytepad(x []byte, w int) []byte {
|
|
prefix := leftEncode(uint64(w))
|
|
out := make([]byte, 0, len(prefix)+len(x)+w)
|
|
out = append(out, prefix...)
|
|
out = append(out, x...)
|
|
for len(out)%w != 0 {
|
|
out = append(out, 0x00)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// kmac256 returns KMAC256(K, X, outLen, S) per SP 800-185 §4.
|
|
func kmac256(key, msg []byte, outLen int, customization string) []byte {
|
|
x := bytepad(encodeString(key), 136)
|
|
x = append(x, msg...)
|
|
x = append(x, rightEncode(uint64(outLen)*8)...)
|
|
|
|
h := sha3.NewCShake256([]byte("KMAC"), []byte(customization))
|
|
_, _ = h.Write(x)
|
|
out := make([]byte, outLen)
|
|
_, _ = h.Read(out)
|
|
return out
|
|
}
|
|
|
|
// tupleHash256 returns TupleHash256(parts, outLen, S) per SP 800-185 §5.
|
|
func tupleHash256(parts [][]byte, outLen int, customization string) []byte {
|
|
var x []byte
|
|
for _, p := range parts {
|
|
x = append(x, encodeString(p)...)
|
|
}
|
|
x = append(x, rightEncode(uint64(outLen)*8)...)
|
|
|
|
h := sha3.NewCShake256([]byte("TupleHash"), []byte(customization))
|
|
_, _ = h.Write(x)
|
|
out := make([]byte, outLen)
|
|
_, _ = h.Read(out)
|
|
return out
|
|
}
|
|
|
|
// cshake256Stream is the bare cSHAKE256 XOF: customization S is the
|
|
// only label; N is empty.
|
|
func cshake256Stream(customization string, transcript []byte, outLen int) []byte {
|
|
h := sha3.NewCShake256(nil, []byte(customization))
|
|
_, _ = h.Write(transcript)
|
|
out := make([]byte, outLen)
|
|
_, _ = h.Read(out)
|
|
return out
|
|
}
|