Skip to content

tfhe: Phase 1 real Lagrange-interpolated distributed decryption (issue #20)#25

Open
abhicris wants to merge 1 commit into
luxfi:mainfrom
abhicris:feat/tfhe-real-partial-decrypt-and-combine
Open

tfhe: Phase 1 real Lagrange-interpolated distributed decryption (issue #20)#25
abhicris wants to merge 1 commit into
luxfi:mainfrom
abhicris:feat/tfhe-real-partial-decrypt-and-combine

Conversation

@abhicris
Copy link
Copy Markdown
Contributor

@abhicris abhicris commented Jun 6, 2026

Phase 1 of issue #20. Replaces the HMAC-stub PartialDecrypt / CombineShares in protocols/tfhe/tfhe.go with a real Shamir-over-F_QLWE distributed decryption that never materialises the master key on any party.

Builds on the polynomial.LagrangeAtZeroBigInt helper added in #24 specifically as the precursor for this PR.

What lands

New file protocols/tfhe/partial_decrypt.go with the real surface:

  • DealRealKeyShares(ctx, params, threshold, parties) — trusted-dealer keygen. Generates the master FHE keypair, Shamir-shares each LWE-secret-key polynomial coefficient over F_QLWE, evaluates the shares at every party's x-coordinate, and zeroises the master coefficient buffer before returning. No party's RealKeyShare carries the master key.
  • RealKeyShare.PartialDecrypt(ct, flood) — computes <a, s_i> against the LWE half of an fhe.Ciphertext by MulCoeffsMontgomery + INTT, matching rlwe.Decryptor's convention exactly. Optional flood-noise term plumbed through the API.
  • CombineShares(params, ct, partials, threshold) — uses polynomial.LagrangeAtZeroBigInt (the precursor from pkg/math/polynomial: add LagrangeAtZeroBigInt for tfhe combine (issue #20 precursor) #24) to interpolate the partials at x=0 in F_QLWE, adds the constant term of b, and rounds against the {Q/8, 7Q/8} bit encoding.

Plus a binding-hash check so partials produced against ciphertext A cannot be silently combined with partials from ciphertext B.

Scheme (textbook AJL+12-style, single-bit form)

Each LWE-secret-key polynomial coefficient s[k] ∈ Z_QLWE is Shamir-shared in F_QLWE. By Shamir linearity over the same field, for any authorised subset T of size ≥ threshold:

s = Σ_{i∈T} λ_i(T) · s_i   (mod QLWE, coefficient-wise)

For an LWE ciphertext (a, b) with b = Δm + e − a·s (mod QLWE), each party returns p_i = <a, s_i> (the constant term of the ring product, as rlwe.Decryptor extracts it). The combiner runs:

m_noisy = (b₀ + Σ_{i∈T} λ_i(T) · p_i)   (mod QLWE)

and rounds against the bit encoding to recover the plaintext bit. The whole reduction is exact mod QLWE — noise structure is preserved because no operation leaves F_QLWE.

Tests

Parametrised M-of-N harness in partial_decrypt_test.go:

Test N M Notes
TestPartialDecrypt_3of2 3 2 smoke
TestPartialDecrypt_5of3 5 3 issue #20 acceptance
TestPartialDecrypt_7of4 7 4 mid-validator
TestPartialDecrypt_21of11 21 11 issue #20 production-validator-scale acceptance
TestPartialDecrypt_STD128_5of3 5 3 spot-check against PN9QP28_STD128 (OpenFHE STD128_LMKCDEY)

Plus three structural / negative tests:

  • TestPartialDecrypt_RejectsInsufficient — combine refuses with < threshold partials.
  • TestPartialDecrypt_BindingHashRejectsMisroute — partials bound to one ciphertext cannot be reused against another.
  • TestPartialDecrypt_MasterKeyNotMaterialisedOnParty — structural check that no two parties hold the same first coefficient (catches the legacy UnderlyingKey: masterSK regression).

Every test passes locally on Go 1.26.3 with both PN10QP27 and PN9QP28_STD128.

Deferred to Phase 2 (NOT in this PR)

Documented in the package doc and tracked under issue #20:

  1. Formal noise-growth proof bounds for PN9QP28_STD128. The Phase 1 API plumbs the flood term through PartialDecrypt but defaults to no flooding. Phase 2 wires in a χ_flood sampler and adds noise-budget assertions in the test harness against the STD128 parameter set.
  2. Active-adversary verification. A malicious party producing a wrong p_i is not currently detected. Phase 2 adds Feldman/Pedersen VSS commitments on partial shares so the combiner can reject deviating partials before combine.
  3. Public-DKG variant. Phase 1 ships trusted-dealer keygen only. Phase 2 hooks the DKG path per LP-181/Magnetar so no single dealer ever holds the master key.

The existing fake-threshold path in tfhe.go (the one gated by ALLOW_FAKE_TFHE_FOR_TESTING_ONLY=1) is intentionally preserved for now so downstream consumers keep compiling. Deletion of that env var + path follows in a later PR once consumers migrate to the new API surface.

Refs

…luxfi#20)

Adds RealKeyShare / PartialDecrypt / CombineShares to protocols/tfhe,
replacing the HMAC-stub gated by ALLOW_FAKE_TFHE_FOR_TESTING_ONLY with
genuine M-of-N threshold decryption that never materialises the master
key on any party.

Scheme (textbook AJL+12-style RLWE threshold decryption, single-bit):

  - DealRealKeyShares performs trusted-dealer keygen, then Shamir-shares
    each LWE-secret-key polynomial coefficient over F_QLWE and zeroises
    the master polynomial before returning.

  - RealKeyShare.PartialDecrypt computes <a, s_i> against the LWE
    half of an fhe.Ciphertext via MulCoeffsMontgomery + INTT, matching
    the rlwe.Decryptor convention exactly. Optional flood-noise term
    plumbed through the API (sampler deferred to Phase 2).

  - CombineShares uses polynomial.LagrangeAtZeroBigInt (added in PR luxfi#24
    for this exact purpose) to interpolate the partials at x=0 in
    F_QLWE, adds the constant term of b, and rounds against the bit
    encoding.

Tests: parametrised M-of-N harness covering N=5/M=3 and N=21/M=11
(the production-validator scale from the acceptance criteria), plus
N=3/M=2, N=7/M=4, and a PN9QP28_STD128 spot-check. Additional tests
cover insufficient-quorum refusal, cross-ciphertext partial-mix
rejection, and a structural check that no two parties share the same
first coefficient (catches the legacy master-key-replication regression).

Deferred to Phase 2 (documented in package doc + PR body):

  - Formal noise-growth proof bounds for PN9QP28_STD128
  - Active-adversary verification (Feldman VSS on partials)
  - Public-DKG variant (LP-181/Magnetar track)

The existing fake-threshold path in tfhe.go (UnderlyingKey: masterSK,
HMAC PartialDecrypt) is preserved for downstream compile compatibility;
the ALLOW_FAKE_TFHE_FOR_TESTING_ONLY env-var deletion follows in a
later PR once downstream consumers migrate to the real API surface.

Refs: luxfi#20 (Phase 1), luxfi#24 (LagrangeAtZeroBigInt precursor)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant