Files
zeekay 2c63fd34fc lens: classical curve threshold kernel — sister to Pulsar
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.
2026-03-03 12:00:00 -08:00

130 lines
4.8 KiB
Go

// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package reshare
// Transcript binding for Verifiable Secret Resharing.
//
// Every resharing message — Round 1 commits + private share deliveries,
// Round 1.5 commit-digest broadcasts, Round 2 complaints, and the final
// activation certificate — is bound to a transcript hash that commits
// to the (chain_id, network_id, group_id, era, generations, epochs,
// sets, thresholds, group_pk_hash, nebula_root, hash_suite_id,
// implementation_version, variant) tuple. The transcript hash is the
// single point of equality the new committee uses when it threshold-
// signs the activation message under the unchanged GroupKey: the chain
// accepts the new epoch only when the activation signature verifies
// against this hash.
//
// Domain separation. Every input field is unambiguously length-prefixed
// (TupleHash framing under Lens-SHA3, hand-rolled length prefixes under
// the legacy Lens-BLAKE3 suite). The customization tag is
// "LENS-TRANSCRIPT-v1" — distinct from any DKG transcript tag and from
// any Lens Sign transcript tag.
import (
"bytes"
"encoding/binary"
"github.com/luxfi/lens/hash"
)
// TranscriptPersonalization is the legacy BLAKE3 personalization
// string. Kept as an exported symbol because some external consumers
// (cross-language KAT loaders) reference it; the canonical transcript
// binding is now produced via the HashSuite layer.
const TranscriptPersonalization = "lens.reshare.transcript.v1"
// TranscriptInputs holds the public binding fields for one resharing
// invocation. All fields are mandatory; the transcript hash is well-
// defined only when every byte below is fixed.
type TranscriptInputs struct {
ChainID []byte
NetworkID []byte
GroupID []byte
KeyEraID uint64
OldGeneration uint64
NewGeneration uint64
OldEpochID uint64
NewEpochID uint64
OldSetHash [32]byte
NewSetHash [32]byte
ThresholdOld uint32
ThresholdNew uint32
GroupPublicKeyHash [32]byte
NebulaRoot [32]byte
HashSuiteID string
ImplementationVersion string
Variant string // "refresh" or "reshare"
CurveName string // "ed25519" / "secp256k1" / "ristretto255"
}
// Hash returns the canonical 32-byte transcript binding for the inputs
// under the supplied HashSuite. nil resolves to the production default
// (Lens-SHA3); pass hash.NewLensBLAKE3() to reproduce legacy bytes.
func (t *TranscriptInputs) Hash(suite hash.HashSuite) [32]byte {
s := hash.Resolve(suite)
parts := buildTranscriptParts(t)
return s.TranscriptHash(parts...)
}
// buildTranscriptParts canonicalizes a TranscriptInputs into the
// ordered byte-string list that drives TranscriptHash.
func buildTranscriptParts(t *TranscriptInputs) [][]byte {
enc64 := func(v uint64) []byte {
var b [8]byte
binary.BigEndian.PutUint64(b[:], v)
return b[:]
}
enc32 := func(v uint32) []byte {
var b [4]byte
binary.BigEndian.PutUint32(b[:], v)
return b[:]
}
return [][]byte{
[]byte("variant"), []byte(t.Variant),
[]byte("curve"), []byte(t.CurveName),
[]byte("chain_id"), t.ChainID,
[]byte("network_id"), t.NetworkID,
[]byte("group_id"), t.GroupID,
[]byte("key_era_id"), enc64(t.KeyEraID),
[]byte("old_generation"), enc64(t.OldGeneration),
[]byte("new_generation"), enc64(t.NewGeneration),
[]byte("old_epoch_id"), enc64(t.OldEpochID),
[]byte("new_epoch_id"), enc64(t.NewEpochID),
[]byte("old_set_hash"), t.OldSetHash[:],
[]byte("new_set_hash"), t.NewSetHash[:],
[]byte("threshold_old"), enc32(t.ThresholdOld),
[]byte("threshold_new"), enc32(t.ThresholdNew),
[]byte("group_public_key_hash"), t.GroupPublicKeyHash[:],
[]byte("nebula_root"), t.NebulaRoot[:],
[]byte("hash_suite_id"), []byte(t.HashSuiteID),
[]byte("implementation_version"), []byte(t.ImplementationVersion),
}
}
// ValidatorSetHash returns the canonical hash of a validator set.
// suite=nil resolves to the production default (Lens-SHA3).
func ValidatorSetHash(publicKeys [][]byte, suite hash.HashSuite) [32]byte {
s := hash.Resolve(suite)
sorted := make([][]byte, len(publicKeys))
copy(sorted, publicKeys)
for i := 1; i < len(sorted); i++ {
for j := i; j > 0 && bytes.Compare(sorted[j-1], sorted[j]) > 0; j-- {
sorted[j-1], sorted[j] = sorted[j], sorted[j-1]
}
}
parts := make([][]byte, 0, 2+len(sorted))
parts = append(parts, []byte("lens.reshare.validator-set.v1"))
var nBuf [4]byte
binary.BigEndian.PutUint32(nBuf[:], uint32(len(sorted)))
parts = append(parts, nBuf[:])
for _, pk := range sorted {
if len(pk) > 0xFFFF {
panic("reshare: validator public key longer than 65535 bytes")
}
parts = append(parts, pk)
}
return s.TranscriptHash(parts...)
}