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.
162 lines
5.1 KiB
Go
162 lines
5.1 KiB
Go
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package reshare
|
|
|
|
// Activation certificate (post-reshare circuit-breaker).
|
|
//
|
|
// Resharing is meaningless without a hard go/no-go switch at the chain
|
|
// level: if the new committee cannot collectively sign under the
|
|
// UNCHANGED group public key, then the shares it holds do not actually
|
|
// reconstruct the master secret, and rotating to those shares would
|
|
// brick the chain. The activation certificate is the proof that the
|
|
// new committee CAN sign — and therefore the chain accepts the new
|
|
// epoch.
|
|
//
|
|
// Activation message canonical bytes (signed by the new committee
|
|
// under the UNCHANGED GroupKey):
|
|
//
|
|
// "QUASAR-LENS-ACTIVATE-v1" ||
|
|
// transcript_hash (32 bytes; from TranscriptInputs.Hash)
|
|
// reshare_transcript_hash (32 bytes; from ReshareTranscript.Hash)
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/luxfi/lens/hash"
|
|
)
|
|
|
|
// ActivationMessage is the canonical bytes-to-be-signed for the
|
|
// post-reshare circuit-breaker.
|
|
type ActivationMessage struct {
|
|
Transcript TranscriptInputs
|
|
ReshareTranscript ReshareTranscript
|
|
}
|
|
|
|
// ReshareTranscript is the structured digest of the resharing exchange.
|
|
type ReshareTranscript struct {
|
|
CommitDigests map[int][32]byte
|
|
ComplaintHashes [][32]byte
|
|
DisqualifiedSenders []int
|
|
QualifiedQuorum []int
|
|
}
|
|
|
|
// Hash returns the canonical 32-byte digest of the ReshareTranscript.
|
|
// suite=nil resolves to the production default.
|
|
func (rt *ReshareTranscript) Hash(suite hash.HashSuite) [32]byte {
|
|
s := hash.Resolve(suite)
|
|
parts := buildExchangeTranscriptParts(rt)
|
|
return s.TranscriptHash(parts...)
|
|
}
|
|
|
|
func buildExchangeTranscriptParts(rt *ReshareTranscript) [][]byte {
|
|
enc32 := func(v uint32) []byte {
|
|
var b [4]byte
|
|
b[0] = byte(v >> 24)
|
|
b[1] = byte(v >> 16)
|
|
b[2] = byte(v >> 8)
|
|
b[3] = byte(v)
|
|
return b[:]
|
|
}
|
|
parts := [][]byte{
|
|
[]byte("lens.reshare.exchange-transcript.v1"),
|
|
}
|
|
|
|
commitParties := make([]int, 0, len(rt.CommitDigests))
|
|
for id := range rt.CommitDigests {
|
|
commitParties = append(commitParties, id)
|
|
}
|
|
insertionSortInts(commitParties)
|
|
parts = append(parts, []byte("commit_count"), enc32(uint32(len(commitParties))))
|
|
for _, id := range commitParties {
|
|
parts = append(parts, []byte("commit_party"), enc32(uint32(id)))
|
|
d := rt.CommitDigests[id]
|
|
parts = append(parts, []byte("commit_digest"), append([]byte(nil), d[:]...))
|
|
}
|
|
|
|
parts = append(parts, []byte("complaint_count"), enc32(uint32(len(rt.ComplaintHashes))))
|
|
for _, ch := range rt.ComplaintHashes {
|
|
parts = append(parts, []byte("complaint_hash"), append([]byte(nil), ch[:]...))
|
|
}
|
|
|
|
dq := append([]int(nil), rt.DisqualifiedSenders...)
|
|
insertionSortInts(dq)
|
|
parts = append(parts, []byte("disqualified_count"), enc32(uint32(len(dq))))
|
|
for _, id := range dq {
|
|
parts = append(parts, []byte("disqualified_id"), enc32(uint32(id)))
|
|
}
|
|
|
|
q := append([]int(nil), rt.QualifiedQuorum...)
|
|
insertionSortInts(q)
|
|
parts = append(parts, []byte("qualified_count"), enc32(uint32(len(q))))
|
|
for _, id := range q {
|
|
parts = append(parts, []byte("qualified_id"), enc32(uint32(id)))
|
|
}
|
|
return parts
|
|
}
|
|
|
|
// SignableBytes returns the canonical bytes the new committee
|
|
// threshold-signs to produce an activation cert.
|
|
func (a *ActivationMessage) SignableBytes(suite hash.HashSuite) []byte {
|
|
var buf bytes.Buffer
|
|
buf.WriteString("QUASAR-LENS-ACTIVATE-v1")
|
|
t := a.Transcript.Hash(suite)
|
|
buf.Write(t[:])
|
|
rth := a.ReshareTranscript.Hash(suite)
|
|
buf.Write(rth[:])
|
|
return buf.Bytes()
|
|
}
|
|
|
|
// ActivationCert wraps the threshold signature emitted by the new
|
|
// committee over an ActivationMessage.
|
|
type ActivationCert struct {
|
|
Message ActivationMessage
|
|
Signature []byte
|
|
}
|
|
|
|
// Errors returned by the activation circuit-breaker.
|
|
var (
|
|
ErrActivationFailed = errors.New("reshare: activation cert failed to verify under group public key")
|
|
ErrTranscriptMismatch = errors.New("reshare: activation transcript hash does not match local view")
|
|
)
|
|
|
|
// VerifyActivation runs the chain-level activation check.
|
|
// suite=nil resolves to the production default (Lens-SHA3).
|
|
func VerifyActivation(
|
|
cert *ActivationCert,
|
|
localTranscriptHash [32]byte,
|
|
localExchangeHash [32]byte,
|
|
suite hash.HashSuite,
|
|
verify func(message []byte, signature []byte) bool,
|
|
) error {
|
|
if cert == nil {
|
|
return errors.New("reshare: nil activation cert")
|
|
}
|
|
expectedTranscript := cert.Message.Transcript.Hash(suite)
|
|
if expectedTranscript != localTranscriptHash {
|
|
return fmt.Errorf("%w: cert says %x, chain says %x",
|
|
ErrTranscriptMismatch, expectedTranscript, localTranscriptHash)
|
|
}
|
|
expectedExchange := cert.Message.ReshareTranscript.Hash(suite)
|
|
if expectedExchange != localExchangeHash {
|
|
return fmt.Errorf("%w: exchange hash mismatch", ErrTranscriptMismatch)
|
|
}
|
|
if !verify(cert.Message.SignableBytes(suite), cert.Signature) {
|
|
return ErrActivationFailed
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// insertionSortInts is a hand-rolled O(n^2) sort suitable for the
|
|
// small slices this package operates on (committee size ≤ 32 in any
|
|
// realistic deployment).
|
|
func insertionSortInts(s []int) {
|
|
for i := 1; i < len(s); i++ {
|
|
for j := i; j > 0 && s[j-1] > s[j]; j-- {
|
|
s[j-1], s[j] = s[j], s[j-1]
|
|
}
|
|
}
|
|
}
|