Files
zeekayandHanzo Dev 019248c82a protocols/tfhe: threshold FHE
A committee of n members at threshold t decrypts without any member holding
the key. Keygen is dealerless: each member samples its own contribution and
publishes a share of the collective public key. Decrypt combines masked
partial decryptions, bound by digest to the ciphertext, and never
reconstructs the key.

Recombination uses the integer Lagrange coefficients directly and clears no
denominator, so quorums whose coefficients are not integers are refused
rather than answered wrongly. Decrypt searches the contributions on hand for
one that recombines exactly; the largest positions always do.

Custody and correctness are proved in lux/proofs/fhe/threshold-custody.tex.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-17 13:34:52 -07:00

286 lines
7.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// SPDX-License-Identifier: BSD-3-Clause
package tfhe
import (
"testing"
"github.com/luxfi/fhe"
"github.com/luxfi/lattice/v7/utils/sampling"
)
// TestKernel verifies that the canonical 2-of-3
// flow recovers the plaintext bit. This is the smoke test: real shares,
// real partials, real Lagrange combine, real bit decoding.
func TestKernel(t *testing.T) {
for _, value := range []bool{false, true} {
kernelRoundTrip(t, fhe.PN10QP27, 2, 3, value)
}
}
// TestWide covers an asymmetric setup. Uses
// PN11QP54 because PN10QP27's modulus is too small to support t=5 with
// secure noise flooding (Λ_max · σ · √t · 6 > Q/16 at PN10).
func TestWide(t *testing.T) {
for _, value := range []bool{false, true} {
kernelRoundTrip(t, fhe.PN11QP54, 5, 9, value)
}
}
// TestLarge covers a larger committee at PN11QP54.
func TestLarge(t *testing.T) {
if testing.Short() {
t.Skip("skipping 11-of-21 round-trip under -short")
}
for _, value := range []bool{false, true} {
kernelRoundTrip(t, fhe.PN11QP54, 11, 21, value)
}
}
func kernelRoundTrip(t *testing.T, lit fhe.ParametersLiteral, threshold, total int, value bool) {
t.Helper()
params, err := fhe.NewParametersFromLiteral(lit)
if err != nil {
t.Fatalf("params: %v", err)
}
kgen := fhe.NewKeyGenerator(params)
sk, _ := kgen.GenKeyPair()
// Distribute shares.
shares, err := Split(sk, params, threshold, total)
if err != nil {
t.Fatalf("share: %v", err)
}
if got, want := len(shares), total; got != want {
t.Fatalf("share count: got %d want %d", got, want)
}
// Encrypt with the master key (collective public key in the
// production flow; either yields the same threshold-decryptable
// ciphertext).
enc := fhe.NewEncryptor(params, sk)
ct := enc.Encrypt(value)
// Each party in a chosen subset of size `threshold` produces a
// partial. We choose the *last* t parties to avoid lucky alignment
// with the first-t shares carrying obviously-small Lagrange weights.
subset := shares[total-threshold:]
partials := make([]*Partial, threshold)
for i, share := range subset {
share := share
prng, err := sampling.NewPRNG()
if err != nil {
t.Fatalf("prng: %v", err)
}
p, err := share.Partial(ct, params, threshold, prng)
if err != nil {
t.Fatalf("partial[%d]: %v", i, err)
}
if p.Index != share.Index {
t.Fatalf("partial index mismatch: got %d want %d", p.Index, share.Index)
}
partials[i] = p
}
got, err := Combine(ct, partials, params)
if err != nil {
t.Fatalf("combine: %v", err)
}
if got != value {
t.Fatalf("round trip: got %v want %v (subset of %d, threshold %d)", got, value, total, threshold)
}
}
// TestShort confirms that 1 of 3
// partials cannot recover the plaintext (negative correctness test).
//
// A single partial is c_1*s_0 + e_0, unrelated to the plaintext.
func TestShort(t *testing.T) {
params, err := fhe.NewParametersFromLiteral(fhe.PN10QP27)
if err != nil {
t.Fatalf("params: %v", err)
}
kgen := fhe.NewKeyGenerator(params)
sk, _ := kgen.GenKeyPair()
shares, err := Split(sk, params, 2, 3)
if err != nil {
t.Fatalf("share: %v", err)
}
enc := fhe.NewEncryptor(params, sk)
ct := enc.Encrypt(true)
prng, _ := sampling.NewPRNG()
share := shares[0]
p, err := share.Partial(ct, params, 1, prng)
if err != nil {
t.Fatalf("partial: %v", err)
}
// With one partial the Lagrange basis is 1, so the result is c_0 + d_0.
disagreements := 0
trials := 32
for i := 0; i < trials; i++ {
value := i&1 == 0
ct2 := enc.Encrypt(value)
prng2, _ := sampling.NewPRNG()
p2, err := share.Partial(ct2, params, 1, prng2)
if err != nil {
t.Fatalf("partial[%d]: %v", i, err)
}
got, err := Combine(ct2, []*Partial{p2}, params)
if err != nil {
t.Fatalf("combine[%d]: %v", i, err)
}
if got != value {
disagreements++
}
}
_ = p
if disagreements == 0 {
t.Fatalf("below-threshold combine should disagree with plaintext; got 0/%d disagreements", trials)
}
}
// TestSplit verifies that no party's
// stored share equals the master secret key polynomial.
func TestSplit(t *testing.T) {
params, err := fhe.NewParametersFromLiteral(fhe.PN10QP27)
if err != nil {
t.Fatalf("params: %v", err)
}
kgen := fhe.NewKeyGenerator(params)
sk, _ := kgen.GenKeyPair()
shares, err := Split(sk, params, 2, 3)
if err != nil {
t.Fatalf("share: %v", err)
}
// Lift sk to standard form to compare against shares (the share
// representation).
ringQ := params.ParamsLWE().RingQ()
skStd := ringQ.NewPoly()
ringQ.IMForm(sk.SKLWE.Value.Q, skStd)
ringQ.INTT(skStd, skStd)
for _, share := range shares {
for i := 0; i < ringQ.N(); i++ {
if share.Coeffs[i] == skStd.Coeffs[0][i] && share.Coeffs[i] != 0 {
// A single coordinate matching is statistically possible.
// We only flag if the entire vector matches.
}
}
match := true
for i := 0; i < ringQ.N(); i++ {
if share.Coeffs[i] != skStd.Coeffs[0][i] {
match = false
break
}
}
if match {
t.Fatalf("share at index %d equals master secret coefficients", share.Index)
}
}
}
// TestDuplicate verifies the dedup guard.
func TestDuplicate(t *testing.T) {
params, err := fhe.NewParametersFromLiteral(fhe.PN10QP27)
if err != nil {
t.Fatalf("params: %v", err)
}
kgen := fhe.NewKeyGenerator(params)
sk, _ := kgen.GenKeyPair()
shares, err := Split(sk, params, 2, 3)
if err != nil {
t.Fatalf("share: %v", err)
}
enc := fhe.NewEncryptor(params, sk)
ct := enc.Encrypt(true)
prng, _ := sampling.NewPRNG()
share := shares[0]
p, err := share.Partial(ct, params, 1, prng)
if err != nil {
t.Fatalf("partial: %v", err)
}
// The same partial twice must error before combining.
if _, err := combine(ct.Ciphertext, []*Partial{p, p}, params.ParamsLWE()); err == nil {
t.Fatal("expected duplicate-index error")
}
}
// TestModulus guards against cross-parameter mixing.
func TestModulus(t *testing.T) {
params, err := fhe.NewParametersFromLiteral(fhe.PN10QP27)
if err != nil {
t.Fatalf("params: %v", err)
}
kgen := fhe.NewKeyGenerator(params)
sk, _ := kgen.GenKeyPair()
shares, err := Split(sk, params, 2, 3)
if err != nil {
t.Fatalf("share: %v", err)
}
enc := fhe.NewEncryptor(params, sk)
ct := enc.Encrypt(true)
prng, _ := sampling.NewPRNG()
share := shares[0]
p, err := share.Partial(ct, params, 1, prng)
if err != nil {
t.Fatalf("partial: %v", err)
}
p.Q = p.Q ^ 1
if _, err := combine(ct.Ciphertext, []*Partial{p}, params.ParamsLWE()); err == nil {
t.Fatal("expected q-mismatch error")
}
}
// TestDeterminism verifies that
// running the combine on the SAME partials yields the SAME plaintext.
// (PartialDecrypt is non-deterministic due to fresh noise; combine is
// deterministic in its inputs.)
func TestDeterminism(t *testing.T) {
params, err := fhe.NewParametersFromLiteral(fhe.PN10QP27)
if err != nil {
t.Fatalf("params: %v", err)
}
kgen := fhe.NewKeyGenerator(params)
sk, _ := kgen.GenKeyPair()
shares, err := Split(sk, params, 2, 3)
if err != nil {
t.Fatalf("share: %v", err)
}
enc := fhe.NewEncryptor(params, sk)
ct := enc.Encrypt(true)
prng1, _ := sampling.NewPRNG()
prng2, _ := sampling.NewPRNG()
share0 := shares[0]
share1 := shares[1]
p0, err := share0.Partial(ct, params, 2, prng1)
if err != nil {
t.Fatalf("partial 0: %v", err)
}
p1, err := share1.Partial(ct, params, 2, prng2)
if err != nil {
t.Fatalf("partial 1: %v", err)
}
// Run combine twice on the same partials.
got1, err := Combine(ct, []*Partial{p0, p1}, params)
if err != nil {
t.Fatalf("combine 1: %v", err)
}
got2, err := Combine(ct, []*Partial{p0, p1}, params)
if err != nil {
t.Fatalf("combine 2: %v", err)
}
if got1 != got2 {
t.Fatalf("combine non-deterministic in inputs: %v vs %v", got1, got2)
}
if !got1 {
t.Fatalf("plaintext recovery failed: got false want true")
}
}