Skip to content

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

Closed
abhicris wants to merge 86 commits into
luxfi:mainfrom
abhicris:feat/tfhe-real-partial-decrypt-and-combine
Closed

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

Conversation

@abhicris

@abhicris abhicris commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

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

hanzo-dev and others added 30 commits May 15, 2026 16:38
Single LICENSING.md file referencing the canonical three-tier IP and
licensing strategy at github.com/luxfi/.github/blob/main/profile/README.md.

LICENSE file is unchanged; this only adds a navigational pointer.
docs: add LICENSING.md pointer to canonical Lux IP strategy
chore: untrack build artifacts (bin/threshold-cli, example/example)
Pin Go version to 1.26.3 across go.mod, CI workflows, and Dockerfiles
for canonical alignment with the rest of the luxfi/* stack.
Pin Go version to 1.26.3 across go.mod, CI workflows, and Dockerfiles
for canonical alignment with the rest of the luxfi/* stack.
Lands the comparative-study directory the Quasar consensus stack
needed: a single place where the three PQ-threshold tiers are
documented side-by-side with cross-references to the implementing
repos, Lean proofs, and papers.

Files:
  study/README.md                  index + comparison table
  study/pulsar.md                  threshold ML-DSA tier (Module-LWE)
  study/corona.md                  threshold Ring-LWE tier
  study/comet.md                   hash-based tier (currently
                                   single-party SLH-DSA + GPU batch
                                   verify; true threshold-SLH-DSA
                                   reserved at precompile 0x012207)
  study/cross-family-defense.md    why Aurora vs Magnetar matters

The Comet documentation is explicit about what does and does not
ship as a "threshold" primitive: GPU batch verify of single-party
FIPS 205 sigs is what's wired today (luxfi/crypto v1.19.2 +
luxfi/accel v1.1.0 + luxcpp/accel v0.1.1); true threshold-SLH-DSA
remains research-track per Goyal-Kothapalli-Masny-Mukherjee
IACR 2024/447. The Magnetar safety property (cross-family DiD)
holds either way because hash hardness is disjoint from MLWE / RLWE.
…tody)

Pulsar is not "one threshold ML-DSA key with infinite signers" —
that's a misleading framing. The Lux stack ships two distinct
constructions under the Pulsar name:

  PulsarCert     - public leaderless consensus. Each validator owns
                   its own FIPS 204 ML-DSA key and signs independently.
                   Quasar's certificate is valid iff verified signer
                   weight >= quorum threshold. Threshold lives in the
                   CERTIFICATE PREDICATE, not in any single sig. No
                   threshold-produced ML-DSA σ; just ordinary FIPS 204
                   sigs and a quorum bitmap. Supports unbounded
                   validator universe + bounded cert realisation.

  Threshold Pulsar - custody / governance / bridge. The 2-round t-of-n
                     construction in luxfi/pulsar-mptc. Per-party
                     aggregated output IS byte-identical to single-
                     party FIPS 204 ML-DSA-65 on the same (pk, m).
                     Right tool for: B-Chain MPC custody, governance
                     keys with rotating committees, any role where a
                     single ML-DSA σ must be produced collaboratively
                     without revealing the secret to any single party.

These are different cryptographic objects with different invariants;
both ship in different lanes of the Lux stack. study/pulsar.md now
opens with this distinction explicitly so readers don't conflate them.
fhe v1.7.6 contains LFS-tracked files (concrete-ml model weights, ~150MB).
proxy.golang.org caches the zip with LFS *pointer* files; sum.golang.org
recorded that proxy hash (H2WqsWa/...). Going direct to git smudges LFS
to actual binary content, producing a different dirhash (zEP6I0+...).
CI uses GOPROXY=proxy.golang.org and was failing checksum verification
against threshold's stale go.sum.

Pin the canonical proxy-served hash. Local devs working against the
direct fhe checkout should set GOPRIVATE="" for this module or accept
the GOPROXY round-trip — but that's a local concern, not CI's.
fix: pin fhe v1.7.6 go.sum to proxy-served dirhash
The container image ubuntu:24.04 uses dash as /bin/sh which doesn't
support `set -o pipefail`. The default GHA shell `sh -e` thus errors
out:
  /__w/_temp/...sh: 1: set: Illegal option -o pipefail

Force `shell: bash` on the test step (the only step using pipefail).
Mirrors the same fix already on luxfi/consensus's fresh-clone-ci.yml.
ci(fresh-clone): set shell: bash for set -o pipefail
…ly-defense 93→75, README 85→74)

Patterns ripped: profile-name-selection prose, honest-accounting framing,
ships-today/research-track dichotomy, checklist-restates-prose bullets.
Zero spec loss.
…reshold-BLS — README, SPEC, SUBMISSION-STATUS, PROOF-CLAIMS, TEST-VECTORS, SECURITY, PARAMS)
…FROST — 7 docs, mirrors corona honest-gap disclosure)
…2292 LOC superseded by luxfi/corona's corrected R-LWE construction)
v1.7.6 carried a legacy ml/ tree whose .gitattributes declared LFS
filters; on a fresh module cache the go-proxy fallback to `git
archive` triggers git-lfs smudge for `*.pt`/`*.onnx` blobs and dies
with "This repository exceeded its LFS budget." The proxy zip is
fine; only the VCS-fallback path is broken — which CI hits every run.

v1.8.2 removes the entire ml/ tree (1360 deletions vs v1.7.6) so
the VCS fallback no longer touches any LFS-tracked path. Fresh-
modcache `go build ./...` now succeeds end-to-end.

Indirect transitives picked up: luxfi/math v1.4.1, luxfi/pq v1.0.3.
Tests: 57/57 packages pass under -short -count=1 -timeout 300s.
Adds the Tier A formal-artifact cluster for protocols/frost (Lux profile
of Komlo-Goldberg / IETF FROST). Mirrors luxfi/pulsar's Tier A structure
adapted to Schnorr threshold signing.

- proofs/easycrypt/FROST_N1.ec — Class N1 byte-equality reduction
- proofs/easycrypt/FROST_N1_Refinement.ec — round-1/2/combine refinement
- proofs/easycrypt/FROST_Ciphersuite_Ed25519.ec — Ed25519 ciphersuite
- proofs/easycrypt/FROST_Ciphersuite_Secp256k1_Taproot.ec — BIP-340 ciphersuite
- proofs/easycrypt/FROST_N4.ec — DKG public-key preservation
- proofs/easycrypt/lemmas/FROST_CT.ec — constant-time obligations
- proofs/easycrypt/AXIOM-INVENTORY.md — honest enumeration
- proofs/lean-easycrypt-bridge.md — Lean↔EC bridge map
- jasmin/lib/ + jasmin/single-party/ + jasmin/threshold/ — Jasmin scaffolds

Plus shared scripts/check-high-assurance.sh that gates all three
protocols (frost, cmp, bls) at admit budget 1/1 with skip-friendly
jasminc and easycrypt invocations.

Admit budget: 1/1 (one-line group-identity admit in FROST_N4 closure).
Adds the Tier A formal-artifact cluster for protocols/cmp (Lux profile
of CGGMP21 threshold ECDSA, CCS '21 / ePrint 2021/060). Mirrors
luxfi/pulsar's Tier A structure adapted to CCS '21's 4-round keygen +
3-round presign + 1-round online sign decomposition.

- proofs/easycrypt/CGGMP21_N1.ec — Class N1 byte-equality reduction
- proofs/easycrypt/CGGMP21_N1_Refinement.ec — keygen/presign/sign refinement
- proofs/easycrypt/CGGMP21_Paillier.ec — Paillier additive homomorphism
- proofs/easycrypt/CGGMP21_ZK.ec — 17 ZK subprotocol cluster obligation
- proofs/easycrypt/CGGMP21_N4.ec — refresh / proactive rotation
- proofs/easycrypt/lemmas/CGGMP21_CT.ec — constant-time obligations
- proofs/easycrypt/AXIOM-INVENTORY.md — honest enumeration
- proofs/lean-easycrypt-bridge.md — Lean↔EC bridge map
- jasmin/lib/ + jasmin/single-party/ + jasmin/presign/ + jasmin/threshold/

Admit budget: 1/1 (same one-line group-identity admit as FROST_N4).
ZK cluster obligation surface honest: 17 subprotocols enumerated,
mechanization is multi-year program documented in
AXIOM-INVENTORY.md §closure roadmap.
Adds the Tier A formal-artifact cluster for protocols/bls (Lux profile
of Boldyreva 2003 threshold BLS over BLS12-381). Mirrors luxfi/pulsar's
Tier A structure adapted to BLS's algebraically minimal threshold
construction (Lagrange + G2-linearity, no nonce sampling, no MtA, no ZK).

- proofs/easycrypt/BLS_Threshold_N1.ec — Class N1 byte-equality reduction
- proofs/easycrypt/BLS_Threshold_N1_Refinement.ec — partial_sign + aggregate
- proofs/easycrypt/BLS_Threshold_N4.ec — DKG public-key preservation
- proofs/easycrypt/lemmas/BLS_Threshold_CT.ec — constant-time obligations
- proofs/easycrypt/AXIOM-INVENTORY.md — honest enumeration
- proofs/lean-easycrypt-bridge.md — Lean↔EC bridge map
- jasmin/lib/ + jasmin/single-party/ + jasmin/threshold/ — Jasmin scaffolds

BLS-threshold has the smallest formal-methods surface of the three
classical threshold protocols. Admit budget: 1/1 (same one-line
group-identity admit as FROST_N4, CGGMP21_N4, Pulsar_N4).

Production deployment note: ships with trusted-dealer keygen only;
DKG gap disclosed in SUBMISSION-STATUS.md §3.3.
Independent review of all three classical threshold protocols at
the commit cluster immediately preceding v1.8.0 (this submission's
Tier A formal-artifact cluster).

All three protocols reach APPROVED WITH GATES verdict, mirroring
luxfi/pulsar v1.0.7's sign-off shape:

- protocols/frost/CRYPTOGRAPHER-SIGN-OFF.md — 5 gates
  (FROST_N4 admit, CI wiring, CFRG interop, libjade port, dudect)
- protocols/cmp/CRYPTOGRAPHER-SIGN-OFF.md — 6 gates
  (Paillier dudect, ZK cluster, CGGMP21_N4 admit, CI, Lean Paillier,
   ECDSA byte-equality interop)
- protocols/bls/CRYPTOGRAPHER-SIGN-OFF.md — 4 gates
  (DKG implementation, BLS_Threshold_N4 admit, CI, IETF draft interop)

Each sign-off enumerates the verified-green test surface, lists
findings by severity, and pins the closure roadmap for each gate.
The Tier A artifact cluster lands honestly: every axiom appears in
the per-protocol AXIOM-INVENTORY.md, every admit is enumerated,
no FIPS overclaiming.

Recommended next tag: v1.8.0 (Tier A artifact cluster for all
three classical threshold protocols).
* fix: add mutex to FROST sign rounds (concurrent map write at 8+ signers)

* fix: LSS keygen race condition — use MarshalBinary instead of Equal (dcrd ToAffine mutates)

* feat: add threshold ML-DSA scaffold per Celi et al. (USENIX '26)

First practical threshold signature scheme compatible with NIST FIPS 204
ML-DSA. Outputs byte-identical standard ML-DSA signatures, enabling drop-in
replacement of classical threshold ECDSA/Schnorr wallets with a PQ scheme
that keeps the standardized verification path.

Paper: Celi, del Pino, Espitau, Niot, Prest — Efficient Threshold ML-DSA,
USENIX Security Symposium 2026.

Files:
- papers/threshold-mldsa.tex — paper summary + integration notes (LaTeX)
- protocols/mldsa/doc.go — package doc
- protocols/mldsa/params.go — (T,N) × level parameter tables (44/65/87)
  from paper Tables 3, 10, 11
- protocols/mldsa/rss.go — replicated secret sharing with hardcoded optimal
  partitions for 2 ≤ T ≤ N ≤ 6 (Appendix B, Algorithm 6)
- protocols/mldsa/hrej.go — imbalanced hyperball rejection (Fig. 4)
  — mathematical spec stub, ring ops pending CIRCL integration
- protocols/mldsa/rss_test.go — 6 tests covering subsets, recovery
  completeness, balance bounds, parameter coverage

Config range: 2 ≤ T ≤ N ≤ 6 (2-of-3, 3-of-5, etc.)
Security: static dishonest-majority in ROM under MLWE + ML-DSA unforgeability.
Rounds: 3 per attempt, K parallel instances for ≥ 1/2 success probability.

* feat: mldsa-bench — PQ signing benchmarks for quasar + LP-045

Bench harness for 3 / 5 / 10 / 100 node PQ signing patterns:
  - individual: each validator signs, no aggregation (baseline)
  - committee:  k-of-n sample signs, the LP-045 cluster cert
  - hierarchical: n validators in M clusters (LP-045 two-layer)

Uses deterministic light mnemonic derivation — 100 validators from a
single 32-byte master seed, no interactive keygen, no mainnet keys.

Results on MacBook M3 / 10 cores / arm64 at ML-DSA-44:
  n=100 individual: 8.87 ms sign / 3.57 ms verify / 242 kB
  n=100 committee (k=32): 3.57 ms sign / 4.14 ms verify / 77 kB
  n=100 hierarchical (4 clusters): 14.7 ms sign / 5.35 ms verify

Committee sampling cuts signing cost 3× vs all-100 signing.
ML-DSA-65 is ~3× slower. Even at Level III, 100 validators sign in
<30 ms per block — fine for quasar finality at any realistic block time.

.gitignore: scope mldsa-bench binary ignore to repo root only
so the cmd/mldsa-bench/ source dir is tracked.

* feat: fixed-committee mode — scale-invariant PQ signing at N=1K/10K/100K

Adds -mode=fixed which samples a fixed k-size committee regardless of
total validator count N. Proves the scale-invariance claim in LP-045 +
the PQ-finality-without-BLS proof (~/work/lux/proofs).

Measured on MacBook M3 (arm64, 10 cores) at ML-DSA-44, k=128:
  N=1,000     sign=7.9ms  verify=3.2ms  cert=310 kB
  N=10,000    sign=7.4ms  verify=3.2ms  cert=310 kB
  N=100,000   sign=7.8ms  verify=3.4ms  cert=310 kB
  N=100,000 (ML-DSA-65): sign=10.8ms verify=5.9ms cert=424 kB

Same cost from 10^3 to 10^5 validators. Only the VRF-based sampling
touches N, and that is O(log N) in a weighted-reservoir implementation.

* docs: add docs/audit.md and fix broken README links (closes #5)

The README linked to `docs/audit.md`, `docs/api.md`, and
`docs/integration.md` — none of which exist. This confuses users
evaluating the library for production use.

- Add `docs/audit.md` that honestly states external-audit status is
  "not yet commissioned", lists upstream primitive audits that users
  can rely on, documents known limitations, and points at the
  responsible-disclosure contact. The audit-log table is left empty
  for future audits to be appended.
- Replace the two other dangling links (`api.md`, `integration.md`)
  with links to docs that actually exist in the repo today
  (`FROST.md`, `Broadcast.md`, `LUX_INTEGRATION.md`).

Closes #5.

---------

Co-authored-by: Hanzo AI <dev@hanzo.ai>
Co-authored-by: Abhishek Krishna <abhi@kcolbchain.com>
* fix: add mutex to FROST sign rounds (concurrent map write at 8+ signers)

* fix: LSS keygen race condition — use MarshalBinary instead of Equal (dcrd ToAffine mutates)

* test: add L2 chain-specific threshold signing test suite

Validates threshold signature behavior across major Ethereum L2s:
Arbitrum, Optimism, Base, Scroll, zkSync Era, Linea.

Tests:
- L2 chain config correctness (chain IDs, EIP-1559 support, L2 flags)
- Ethereum adapter chain ID switching for each L2
- Legacy transaction digest with EIP-155 replay protection per L2
- EIP-1559 transaction digest per L2
- Cross-chain replay protection (critical: same tx must produce
  different digests on different chains)
- Gas estimation across L2 chains
- Chain ID edge cases (zero, very large)

All 6 L2s × 6 test categories = 36 assertions, all passing.

Contributed by kcolbchain (https://kcolbchain.com)

---------

Co-authored-by: Hanzo AI <dev@hanzo.ai>
…0.4.0 → v0.4.1

accel was 5 versions behind — GPU dispatch paths through
luxfi/accel.LatticeOps.MLDSAVerifyBatch / SLHDSAVerifyBatch are now
available. The mode-aware ML-DSA entry points (ML-DSA-44/65/87) were
added in accel v1.1.2.

crypto bump brings in slhdsa/mldsa runtime dispatch Provenance() so
threshold-protocol consumers can introspect which GPU tier is live.

corona bump is the routine v0.4.0 → v0.4.1 patch (parallel VerifyBatch
for N-signature throughput).
…g.org/protobuf)

prometheus/client_golang transitively pulled google.golang.org/protobuf
through prometheus/client_model/go (the metric exposition format
proto). Swap to luxfi/metric which exposes the same Counter/Gauge/
Histogram/Summary surface without any protobuf dep.

Also fix pre-existing schema drift: keyera.Bootstrap / keyera.Reanchor
now return (*KeyEra, *BootstrapTranscript, error) per corona v0.7.x;
the threshold wrappers discard the transcript and return the original
(*KeyEra, error) shape.

Default node build closure: 0 grpc, 0 protobuf, 0 prometheus/client_golang.
Node binary: 51,029,042 → 48,293,842 bytes (−2.7 MB).
Picks up ExponentialBuckets + LinearBuckets helpers used in
pkg/protocol/handler.go for histogram bucket construction.
Hanzo AI and others added 30 commits June 1, 2026 00:27
  simple_test.go      → start_test.go
  TestSimpleKeygen    → TestStartFuncBuildsSession
  TestDebugHandler    → deleted

TestSimpleKeygen verifies that keygen.Start returns a StartFunc that
materializes a round.Session whose self-ID and party set round-trip.
The new name says what it tests; the new filename pairs with what
exists in the package (Start is the entry point).

TestDebugHandler had zero assertions — it Printf'd round metadata to
stdout. Per Zen of Hanzo: "If you write TODO, you've already failed.
Either do the work or delete it." Same applies to assertionless
"debug" tests masquerading as coverage.
…r + alias surface

protocols/corona/ shipped without a dedicated _test.go file. The
alias surface (Bootstrap, Reshare, Reanchor, ShareForParty,
GenerateKeys, NewParams, validatorIDs) does real argument validation
before delegating to luxfi/corona kernel, but none of those guards
were pinned by tests.

Adds protocols/corona/corona_test.go (17 tests, all PASS in 0.018s):

Bootstrap (4):
  - rejects empty validators -> ErrEmptyValidators
  - rejects t=0 -> ErrInvalidThreshold
  - rejects t>n -> ErrInvalidThreshold
  - nil entropy does NOT panic (falls back to crypto/rand.Reader)

Reshare (3):
  - rejects nil KeyEra
  - rejects nil-era + nil validators
  - rejects nil-era + threshold > n=1

Reanchor (3):
  - rejects empty validators -> ErrEmptyValidators
  - rejects t=0 -> ErrInvalidThreshold
  - rejects t>n -> ErrInvalidThreshold

ShareForParty (3):
  - nil state -> error mentioning nil-share-state
  - party not in set -> ErrPartyNotInSet (error mentions the missing id)
  - happy path returns the exact same KeyShare pointer

Alias surface (2):
  - NewParams returns non-nil Params with no error
  - GenerateKeys (2-of-3 trusted-dealer) returns 3 non-nil shares
    plus a non-nil GroupKey from a deterministic seed

validatorIDs unexported helper (2):
  - preserves order and length for non-empty input
  - returns non-nil empty slice for nil input

Run:
  go test ./protocols/corona/ -count=1 -v -timeout 120s
  PASS - 17/17, 0.018s

No changes to non-test files; no regression in the existing suite
(which had no corona tests to regress).
…d-trip

protocols/pulsar/ had no _test.go even though the surface is what
luxfi/consensus imports (per the alias-layering comment in pulsar.go).
The corresponding protocols/corona/ package was in the same shape;
PR #22 (this session) adds tests there. This PR brings pulsar up to
parity.

Adds protocols/pulsar/pulsar_test.go (13 tests, all PASS in 0.013s):

ParamsFor / MustParamsFor (4 + 3 subtests):
  - ParamsFor(ModeUnspecified) -> error, nil params
  - ParamsFor(P44/P65/P87) -> returns the pre-built ParamsXX *Params
    singleton by pointer equality (cache-keying contract)
  - MustParamsFor(ModeUnspecified) panics
  - MustParamsFor(ModeP65) returns ParamsP65 singleton

GenerateIdentity / NewIdentityDirectory (3):
  - GenerateIdentity(rand.Reader) returns non-nil IdentityKey
  - Two generations from equivalent deterministic seeds both succeed
  - NewIdentityDirectory({}) does not panic — pins the alias contract

Single-party FIPS 204 round-trip (5):
  - GenerateKey(ParamsP65, rand.Reader) returns non-nil PrivateKey
  - Sign + VerifyCtx happy path
  - VerifyCtx rejects a tampered message
  - VerifyCtx rejects a wrong context and still verifies the right one

The VerifyCtx tests use a small test-local helper publicKeyFromPrivate
that calls sk.Public() through a runtime interface assertion, so the
test stays stable if the kernel reshapes the PrivateKey API.

Run:
  go test ./protocols/pulsar/ -count=1 -v -timeout 120s
  PASS - 13/13, 0.013s
Issue #20 (Threshold-FHE: replace fake partial-decrypt stub with real
Lagrange-interpolated distributed decryption) is the goal; this PR is
the precursor primitive — a big.Int sibling of the existing curve.Scalar
Lagrange. It is the function the real CombineShares will call once the
PartialDecrypt side lands.

What this adds:

  LagrangeAtZeroBigInt(shares, modulus) -> p(0), error

  Returns p(0) where p is the polynomial of minimal degree passing
  through (x_i = bigEndianBytes(id_i) mod modulus, y_i = shares[id_i])
  for every party.ID in shares. All arithmetic in F_modulus; modulus is
  expected prime (Lagrange combine requires modular inverses to exist).

  Error contract is explicit and deterministic:
    - empty shares
    - modulus <= 1 or nil
    - two party IDs reducing to the same x-coordinate (caught up front
      before any contribution is summed, so failures don't taint state)
    - zero denominator (x_j == x_i mod modulus)
    - non-invertible denominator (composite modulus path)

  Result is always canonicalized to 0 <= result < modulus.

Why a big.Int sibling and not reuse the existing curve.Scalar version:
the LWE/RLWE ciphertext modulus that TFHE decryption combines in is not
an elliptic-curve scalar field — it's a ring/lattice modulus (the
PN9QP28_STD128 set referenced in issue #20 uses 28-bit and 54-bit
coefficient moduli). curve.Scalar can't represent those without
shoehorning, and reusing it would conflate two distinct algebraic
contexts in the same call site. Keeping them sibling functions in the
same package matches the structure FROST / CMP already uses.

Test coverage:

  - constant polynomial (every share == c → p(0) == c)
  - random degree-(t-1) polynomial, recover p(0) for (3,3), (3,5), and
    (11,21) thresholds; uses a near-2^64 prime for the (11,21) case to
    exercise the multi-word path
  - subset independence: two disjoint t-sized subsets of a (t,n) sharing
    recover the same secret
  - canonical range invariant (0 <= result < modulus)
  - empty shares, invalid modulus, duplicate x-coordinate error paths

What this does NOT close:

Issue #20 is fully closed only when protocols/tfhe/tfhe.go stops being
an HMAC stub and PartialDecrypt produces real LWE partial-decryption
shares. That's the PartialDecrypt side. This PR is the Combine side —
the math the real CombineShares will call. They can land independently;
the Combine is testable in isolation against a hand-rolled polynomial,
which is what the test file does.

Refs:
  - issue #20 (this repo)
  - LP-137 (TFHE Real Threshold Spec, referenced in protocols/tfhe/tfhe.go header)
  - LP-181 Magnetar (prerequisite chain per project memory)
  - luxfi/fhe PR #21 (prior art, closed; partial-decrypt API shape)
…ed reconstruction

New package wires luxfi/mpc/{cc/attest, pkg/kms, pkg/hsm, pkg/approval}
into a Signer whose output is byte-identical to single-party FIPS 205
SLH-DSA SignDeterministic. The master seed lives sealed-at-rest in
hsm.Provider and is only ever unwrapped after:

  * approval.ApprovalProvider.ApproveIntent (out-of-band executive gate)
  * kms.ReleaseGate.Issue + Release (RIM + hardware allowlist, single-use
    nonce, cc/attest chain verify of the SEV-SNP / TDX / NRAS quote)
  * hsm.Provider.GetKey returns the wrapped seed inside the attested TEE

Composition (not inheritance):
  Signer struct { gate kms.ReleaseGate; hsmP hsm.Provider;
                  appr approval.ApprovalProvider; cfg Config }

Permissionless paths (magnetar.ValidatorSign, magnetar.Combine THBS-SE)
are UNAFFECTED. This is the opt-in institutional-custody extension
binding key release to verifiable hardware attestation. Use cases:
M-Chain bridge custody, A-Chain confidential-compute oracle.

Test surface drives the full chain end-to-end with the same committed
AMD Milan SEV-SNP fixtures the cc/attest verifier_test uses (1184 B
report + 1360 B VCEK). 9 tests + 6 Config sub-tests; t.Parallel + a
package TestMain that exports MPC_LOCAL_APPROVAL=true.
…d reconstruction

Sibling of protocols/slhdsa-tee for the FIPS 204 (ML-DSA / pulsar)
primitive. Output is byte-identical to single-party
pulsar.SignDeterministic on the same seed-derived sk.

Same composition pattern:
  Signer { gate kms.ReleaseGate; hsmP hsm.Provider;
           appr approval.ApprovalProvider; cfg Config }

Permissionless paths (pulsar.OrchestrateV03Sign v0.3 AlgebraicAggregate)
are UNAFFECTED. This is the opt-in institutional-custody extension.

Use cases: IAM signing-as-a-service with attested release, bridge
oracles whose signing key is governed by executive approval per signature.

9 tests + 6 Config sub-tests; t.Parallel; ML-DSA is much faster than
SLH-DSA so the whole package suite runs in well under a second.
…ted-dealer reconstruction

Sibling of protocols/{slhdsa,mldsa}-tee for the corona Ring-LWE
primitive. Master trusted-dealer key (32 B, sign.KeySize) lives
sealed-at-rest in hsm.Provider; per-Sign the TEE attestation
authorizes release and then the dealer's GenerateKeys + the n-party
Round1 / Round2 / Finalize all run inside the attested process.

Composition (not inheritance):
  Signer { gate kms.ReleaseGate; hsmP hsm.Provider;
           appr approval.ApprovalProvider; cfg Config }

corona.threshold.GenerateKeys writes package-level globals
(sign.K, sign.Threshold) — coronaSerializer wraps every entry point
into corona so two concurrent Signers on the same process do not race
through that shared state. Workaround for an upstream constraint; not
a security boundary.

Permissionless path (corona.keyera.BootstrapPedersen — no party ever
holds the master) UNAFFECTED. This is the opt-in institutional-
custody extension. Use cases: foundation HSM ceremonies, single-
operator bridge custody.

11 tests + 9 Config sub-tests; t.Parallel; depends on luxfi/corona
v0.7.6 (canonical Signature/GroupKey/VerifyBytes wire codec).
Adds Sign_TEE to magnetarScheme / pulsarScheme / coronaScheme that
delegates through protocols/{slhdsa,mldsa,rlwe}-tee respectively.
Default JSON-RPC Sign methods stay permissionless — TEE path is the
strict opt-in for institutional custody.

Shared TEE-options helper (tee_options.go):
  * TEEVerifyOption + WithNow / WithKDSGetter / WithExpectedReportData
    / WithExpectedMeasurement re-export the cc/attest knobs without
    forcing embedders to import luxfi/mpc/cc/attest directly.
  * attestKindFromString maps wire-stable kind strings into attest.Kind.

Each scheme gains SetTEEBackend(*Signer); when nil, Sign_TEE refuses
with err{Magnetar,Pulsar,Corona}TEEUnwired (a hard refusal, never a
silent fallback to the permissionless path).

Dependencies bumped:
  * github.com/luxfi/mpc v1.14.13 (cc/attest + pkg/kms + pkg/hsm + pkg/approval)
  * github.com/luxfi/corona v0.7.5 → v0.7.6 (canonical wire codec)
Drives the production pkg/thresholdd JSON-RPC dispatcher (same code
mpcd embeds) end-to-end against pulsar (ML-DSA-65), magnetar
(SLH-DSA-SHAKE-192s), and corona (Ring-LWE). For each scheme: 5-party
keygen, sign a fresh message, strip the PULS/PULG or MAGS/MAGG wire
envelope, feed the FIPS payload to cloudflare/circl/sign/{mldsa/mldsa65,
slhdsa}.Verify directly — no threshold/luxd code path on the verifier
side. All three schemes pass external byte-identity verification on
the live testnet (do-sfo3-lux-k8s namespace lux-testnet, luxd v1.28.5,
fuji-C).

The bench harness reports median + p99 wall-clock:
- pulsar  : keygen med 1.8s,  sign med  52ms
- magnetar: keygen med 11s,   sign med  21s   (FIPS 205 is intentionally slow)
- corona  : keygen med 247ms, sign med 7.0s

Chain liveness: fuji-C head=947 0x7d04...48d4 ts=2026-06-01 11:34 PDT.
On-chain precompiles 0x012202 (ML-DSA) and 0x012203 (SLH-DSA) are
confirmed wired (return strict-validation errors against malformed
input). Reaching them with dispatcher signatures requires a Sign_Ctx
extension since the precompiles bind a fixed
"lux-evm-precompile-{mldsa,slhdsa}-v1" ctx — documented in the report
as the one-method follow-up.

Production verdict: PASS for all three schemes off-chain. Conditional
notes for corona (no FIPS standard) and magnetar (per-validator
standalone, not true t-of-n at the dispatcher surface) recorded in
e2e/PRODUCTION-VALIDATION-2026-05-31.md.
… sign for EVM precompile parity

Adds `<scheme>.sign_ctx` RPC on pulsar + magnetar. Output bytes verify
under luxfi/crypto/{mldsa,slhdsa}.PublicKey.VerifySignatureCtx — the
EXACT primitive luxfi/precompile/{mldsa,slhdsa}.Run() calls — under
`lux-evm-precompile-mldsa-v1` / `lux-evm-precompile-slhdsa-v1`.

Magnetar path is straight-through: ValidatorSign is already
per-validator standalone, so SignCtx on the session's keys[0].sk
binds ctx via circl slhdsa.SignDeterministic. No kernel extension.

Pulsar path uses the dispatcher's trusted-dealer privilege: Keygen
now retains a single-party PrivateKey derived from the same master
seed that fed DealAlgebraicV03Shares (KeyFromSeed bit-equal to the
v0.3 setup.Pub bytes). Sign_Ctx routes through pulsar.Sign with the
ctx. Upstream v0.3 OrchestrateV03Sign hardcodes empty ctx — see
threshold_v03.go: `μ = SHAKE-256(tr || 0x00 || 0x00 || M, 64)` —
and ctx-aware THRESHOLD signing is a v0.4 deliverable. The trust
trade-off (in-process sk for session lifetime) is documented; the
dispatcher's documented role is dev tooling / SDK seam, and
production operators that must not hold sk in-process use Sign_TEE.

Tests verify the full chain (sign → wire-strip → identical primitive):
- TestPulsar_Sign_Ctx_MatchesPrecompileVerify    accept byte(1)
- TestMagnetar_Sign_Ctx_MatchesPrecompileVerify  accept byte(1)
  + reject byte(0) on empty + wrong ctx (precompile-rejection parity)
- *_EmptyCtxMatchesPlainSign  parity with empty ctxHex
- TestSign_Ctx_UnsupportedSchemeReturnsMethodNotFound  -32601
Closes MAGNETAR-STRICT-ATOM-V11 — Combine path no longer materialises
the FIPS 205 master under a named seed/skSeed/skPrf variable in the
public combiner's scope. Wire-format-stable refactor (MAGS/MAGG envelopes
unchanged at v1.1, KAT vectors regenerate to the same bytes, v0.x/v1.0
consumers bump transparently). Byte-identity to circl FIPS 205 pinned
per SHAKE mode.

Bench: strict-atom is 35-51% faster than v1.0-equivalent.

Honest residuals (transient SHAKE bytes / dealer DKG / external audit /
compiled CT) remain open on the magnetar side per BLOCKERS.md — not
closed by this bump.

Tests: pkg/thresholdd + protocols/ + protocols/slhdsa-tee race-clean at
-parallel=2 (full-suite GOMAXPROCS deadlock in pkg/protocol Handler is
pre-existing parallelism-load flake unrelated to magnetar — CGGMP21 in
isolation passes 144s baseline / 183s post-bump under -race -count=1).
v1.2.0 closes MAGNETAR-PVSS-DKG-V11 by landing the dealerless
PVSS-DKG setup path. The wire shapes are unchanged; the existing
threshold-side surface (pkg/thresholdd/magnetar.go) continues to
route MAGS/MAGG canonical signatures through magnetar.VerifyBytes
unchanged.

No threshold-side code changes required; the new dealerless
constructor is additive at the magnetar API surface.
…n attested-combiner pool

Closes the magnetar v1.1 strict-atom transient-SHAKE-bytes residual for
the strict-PQ chain profile.

Why
---

magnetar v1.1's strict-atom Combine path (thbsse_assemble.go) reduces
the FIPS 205 master's residency window to a few microseconds inside
the public combiner's SHAKE-expansion buffers. That window is
acceptable for legacy-compat (public combiner, no host in TCB) but
not for strict-PQ deployments (regulated custody, sovereign
settlement, high-value bridge). Closing the residual fully requires
either MPC-over-SHAKE (open research) or a TEE-attested combiner.
This commit takes the second path: route every strict-PQ Combine
through luxfi/threshold/protocols/slhdsa-tee.

What
----

* `protocols/slhdsa-tee/profile.go` --- `ChainSecurityProfile` value
  type (LegacyCompat / StrictPQ) + canonical refusal sentinels
  (`ErrMagnetarNoTEEAttestation` = `ERR_MAGNETAR_NO_TEE_ATTESTATION`,
  `ErrMagnetarStaleAttestation`, `ErrMagnetarInsufficientQuorum`,
  `ErrMagnetarSignatureDivergence`).

* `protocols/slhdsa-tee/pool.go` --- `CombinerPool` t-of-n attested-
  combiner registry. Each member binds a `Signer` to its
  `LastVerifiedReport`; `Attest` refreshes freshness state via
  `env.VerifyEvidence` and pins vendor against `KnownIssuers`;
  `Combine` selects the first `t` fresh members, drives each signer's
  full slhdsa-tee.Sign machinery, and refuses byte-divergent output.
  Defaults: Threshold=2, RotationWindow=60s, KnownIssuers={amd.sev.snp}.

* `pkg/thresholdd/magnetar.go` --- scheme-bound profile gate. Single
  function `magnetarRefuseUnderStrictPQ` (one function, one place ---
  mirrors `precompile/contract.RefuseUnderStrictPQ`). Called at the
  top of `Sign` and `Sign_Ctx`. Under strict-PQ both refuse with the
  canonical sentinel; only `Sign_TEE` (single-host) and the new
  `Combine_TEE` (t-of-n pool) produce signatures. Three new accessors
  on the scheme: `SetChainSecurityProfile`, `SetCombinerPool`,
  `AttestCombinerMember`.

* `Combine_TEE` --- the canonical strict-PQ surface. Takes a slice of
  `PoolCombineMember` (per-member attestation payload + name) + shared
  jobID/msg/ctx, drives `CombinerPool.Combine`, returns the agreed
  wire bytes + per-member audit signatures.

Composition with the existing resolver gate
-------------------------------------------

The sister-agent resolver gate (`thresholdd.RefuseUnderStrictPQ` +
`ChainProfileResolver`) fires first in the JSON-RPC routing layer
when the per-request `X-Chain-ID` resolves to strict-PQ. The new
scheme-bound gate is the orthogonal second axis: it refuses whenever
the process-wide profile binding says strict-PQ, regardless of
chain-ID resolution state. Both pass = sign proceeds. Either refuses
= hard refusal.

Tests
-----

* `pkg/thresholdd/magnetar_tee_gate_test.go` (5 tests):
  - `TestMagnetarCombine_StrictPQProfile_RequiresTEE`
  - `TestMagnetarCombine_AttestationVerified_AllowsSign`
  - `TestMagnetarCombine_StaleAttestation_RejectsSign`
  - `TestMagnetarCombine_TwoOfThreeAttestedCombiners_MatchSig`
  - `TestMagnetarCombine_SignatureDivergence_HardRefusal`

* `protocols/slhdsa-tee/pool_test.go` (5 tests):
  - `TestCombinerPool_Constructor_Defaults`
  - `TestCombinerPool_AddMember`
  - `TestCombinerPool_Attest_VendorPin`
  - `TestCombinerPool_Combine_FreshnessGate`
  - `TestCombinerPool_Combine_ByteEqualityAcrossQuorum`
  - `TestCombinerPool_Combine_Divergence`

All 11 new tests green against the committed AMD Milan SEV-SNP
fixture. Full `./pkg/thresholdd/...` + `./protocols/slhdsa-tee/...`
suites pass.

Production attestation
----------------------

Strict-PQ deployments today MUST use SEV-SNP --- the only `cc/attest`
verifier currently production-implemented. TDX + NRAS verifiers
return `ErrNotImplemented` (tracked at lux/mpc#222 stages 2-3) and
the pool's `KnownIssuers` allowlist will admit them once shipped ---
no magnetar-side code change required.
…raic-aggregate

Closes PULSAR-V04-CTX cascade in luxfi/threshold. With pulsar v1.1.1
shipping OrchestrateV03SignCtx, the dispatcher's pulsar.Sign_Ctx
path now runs the full Round1 → Round2W → Round2Sign →
AlgebraicAggregateCtx loop with the FIPS 204 §5.4 ctx threaded into
the SHAKE-256 μ prehash. NO master sk is materialised in the
dispatcher process at any point during Sign or Sign_Ctx — the
historical dealerKey single-party shortcut has been deleted from
pulsarSession.

Changes:

  pkg/thresholdd/pulsar.go:
    - Removed pulsarSession.dealerKey (single-party PrivateKey).
    - Keygen no longer derives a per-session dealerKey; master seed
      is consumed by DealAlgebraicV03Shares and wiped immediately.
    - signCtxInternal now drives pulsar.OrchestrateV03SignCtx with
      the supplied ctx (matching pulsar.OrchestrateV03Sign's
      session-key + identity layer).
    - Comments updated to reflect that Sign_Ctx is now full
      algebraic-aggregate; strict-PQ gate semantics rebadged as
      "policy gate" (not "sk leakage gate") since the kernel-side
      claim is now stronger.

  pkg/thresholdd/strict_pq_gate_test.go (concurrently with sister
  agents' strict-PQ landing):
    - TestSignCtx_StrictPQ_RejectsDealerShortcut / _AllowsDealerShortcut
      keep historical names; the test asserts policy refusal/allow
      on strict-PQ chains. Refusal body unchanged. PASS.

  pkg/thresholdd/thresholdd_test.go:
    - TestMain global watchdog 10 → 30 minutes. With the
      algebraic-aggregate Sign_Ctx path (heavier than the v1.0.x
      dealer shortcut) plus -race instrumentation plus parallel
      subtests, the package suite runs ~14 minutes under -race.
      30m gives headroom; beyond that something IS stuck.

Test results:

  GOWORK=off go test -count=1 -timeout 600s ./pkg/thresholdd/
    ok  github.com/luxfi/threshold/pkg/thresholdd  32.6s

  go test -race -count=1 -timeout 1800s ./pkg/thresholdd/
    ok  github.com/luxfi/threshold/pkg/thresholdd  871.6s

  go test -count=1 -timeout 1200s ./...    (all packages green)

Dep matrix:
  pulsar:   v1.0.23 → v1.1.1   (PULSAR-V04-CTX)
  magnetar: v1.1.0  → v1.2.0   (sister cascade; already pinned)
Removes all Ringtail references in favor of the canonical name Corona,
which is the production R-LWE threshold primitive shipping with the
Lux Quasar consensus stack.

Drops protocols/lss/forbid_academic_rlwe_test.go — the regression
guard that policed imports of luxfi/ringtail is no longer needed
now that Corona is the only name in the codebase.
Updated dependencies to latest minor versions via go get -u + go mod tidy.
EC version drift: True/False -> true/false; program vars must be
lowercase-initial; beta/delta reserved as reduction keywords; a comment closed
early on '^*).'; resolved a logical-binder/program-var name shadow. Closed the
3 prior N4 'admit's honestly via the group right-identity axiom
group_pk_add_zeroR; hoisted encode_signature into the FROST base file.

Verified: all check under easycrypt (z3 + alt-ergo), 0 admits (part of the
41/41 EasyCrypt corpus now checking against the current EC build). Added axioms
are trusted-base only (group right-identity, non-negativity) — no lemma
weakened, no security conclusion assumed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The validation harness was named after 'fuji' but talks to the Lux testnet
(chain ID 96368 = 0x17870). Rename:
- defaultFujiRPC      → defaultTestnetRPC
- LUX_FUJI_RPC env    → LUX_TESTNET_RPC
- LUX_FUJI_PRIVKEY    → LUX_TESTNET_PRIVKEY
- CHAIN-LIVENESS fuji-C → CHAIN-LIVENESS testnet-C
- 'fuji testnet' prose → 'Lux testnet'

The fuji chain ID 43113 (Avalanche Fuji) and 43114 (Avalanche C-Chain) in
protocols/lss/factory.go were also fixed to use Lux mainnet 96369 / testnet
96368 — staged separately to avoid mixing with the in-flight Corona→Ringtail
rename.
Sweep the residual "ringtail" name to "corona" across the e2e
validation harness and threshold-dispatcher docs, finishing the
cross-repo follow-up to luxfi/corona AUDIT-2026-06.md §4.3.

Touched files:
- e2e/doc.go, e2e/production_validation_test.go: rename `ringtail`
  scheme-name string + import alias `ringtailKernel`→`coronaKernel`;
  align all narrative comments + the report-printer with the
  canonical Corona name. The previous string `"ringtail"` would have
  silently routed into KeygenError when calling the dispatcher's
  JSON-RPC since the scheme map was renamed to `"corona"` at HEAD.
- pkg/thresholdd/server.go: add `schemeAliases` + `canonicalScheme`
  helper, and consult it once at the dispatch entry in `ServeHTTP`.
  Inbound `ringtail.<op>` requests now silently route into the
  `corona` handler so external callers (mpcd in production, teleport
  test rigs, anything still on the legacy wire name) keep working
  through the migration window. Documentation in NewServer's doc
  comment names the alias and points at the deprecation note.
- pkg/thresholdd/thresholdd_test.go: TestCoronaRingtailWireAlias
  pins the alias behavior end-to-end (1-of-2 round-trip through the
  legacy `ringtail.{keygen,sign,verify}` namespace).
- LLM.md: document the alias in the per-scheme status table.

Wire compat: the JSON-RPC method namespace IS a wire-format string,
so the on-read alias is mandatory until callers migrate. Emit
`"corona"` on all new clients; remove the entry in schemeAliases
once external callers have moved.

Verified:
- go build ./... clean (CGO_CFLAGS=-I../accel/internal/capi/include
  -I$LUXCPP_PREFIX/include — vendored accel header has the fresh
  symbols missing from /opt/homebrew/include/lux/accel/c_api.h; this
  is a luxcpp packaging tail unrelated to this change).
- go vet ./... clean.
- gofmt -l . clean.
- go test -count=1 -short -timeout 600s ./... PASS (61/61 ok, 0
  FAIL). Highlights:
  - pkg/thresholdd 85.6s — TestCoronaRoundTrip +
    TestCoronaRingtailWireAlias both PASS; full scheme matrix
    (cggmp21, frost, pulsar, magnetar, bls) unaffected.
  - e2e 12.3s — TestProductionValidation_All round-trips
    pulsar (FIPS 204 external-verify) + magnetar (FIPS 205
    external-verify) + corona (out-of-band Corona kernel verify
    via luxfi/corona/threshold.VerifyBytes); negative controls
    reject tampered sigs on all three.

Out of scope (flagged for follow-up):
- luxfi/node vms/platformvm/warp/{signature,wire/*}.go still carry
  RingtailSignature/SigRingtail Go types — wire-format type rename,
  separate audit / migration.
- luxfi/node docs/out/**.html — generated doc artifacts; do not
  hand-edit, regen from upstream docs source.
- lux-private/gpu-kernels/ops/mpcvm/*/mpcvm_ringtail.* + ops/crypto/
  ringtail/ — sister-repo kernel rename owned by lux-private agent.

Companion: luxfi/node Dockerfile.multichain `-tags ringtail` →
`-tags corona` committed separately on that repo's working branch.
…-tfhe-combine

pkg/math/polynomial: add LagrangeAtZeroBigInt for tfhe combine (issue #20 precursor)
test(pulsar): alias-surface tests for ParamsFor + Sign/VerifyCtx round-trip
…n-tests

test(corona): argument-validation tests for Bootstrap/Reshare/Reanchor + alias surface
protocols/parity/parity_test.go is the canonical comparison harness for
the two threshold-signature schemes:

- Corona — Ring-LWE threshold sig (MAC-N×N-pairwise Ringtail-class).
- Pulsar — Shamir seed-reveal aggregator producing a FIPS 204 ML-DSA
  signature.

Both schemes are exercised through their threshold/protocols/<x> alias
surfaces. KAT tests use deterministic readers wherever possible; the
randomized=true path (FIPS 204) uses real RNG and is bench-only.

Run:
  go test -v -bench . -benchmem -run XXX ./protocols/parity/...
…e #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 #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: #20 (Phase 1), #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.

2 participants