tfhe: Phase 1 real Lagrange-interpolated distributed decryption (issue #20)#25
Closed
abhicris wants to merge 86 commits into
Closed
tfhe: Phase 1 real Lagrange-interpolated distributed decryption (issue #20)#25abhicris wants to merge 86 commits into
abhicris wants to merge 86 commits into
Conversation
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)
…GMP21 threshold-ECDSA — 7 docs)
…h-clone-bash-shell
…h-clone-bash-shell
…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.
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 1 of issue #20. Replaces the HMAC-stub
PartialDecrypt/CombineSharesinprotocols/tfhe/tfhe.gowith a real Shamir-over-F_QLWE distributed decryption that never materialises the master key on any party.Builds on the
polynomial.LagrangeAtZeroBigInthelper added in #24 specifically as the precursor for this PR.What lands
New file
protocols/tfhe/partial_decrypt.gowith the real surface:DealRealKeyShares(ctx, params, threshold, parties)— trusted-dealer keygen. Generates the master FHE keypair, Shamir-shares each LWE-secret-key polynomial coefficient overF_QLWE, evaluates the shares at every party's x-coordinate, and zeroises the master coefficient buffer before returning. No party'sRealKeySharecarries the master key.RealKeyShare.PartialDecrypt(ct, flood)— computes<a, s_i>against the LWE half of anfhe.CiphertextbyMulCoeffsMontgomery+INTT, matchingrlwe.Decryptor's convention exactly. Optional flood-noise term plumbed through the API.CombineShares(params, ct, partials, threshold)— usespolynomial.LagrangeAtZeroBigInt(the precursor from pkg/math/polynomial: add LagrangeAtZeroBigInt for tfhe combine (issue #20 precursor) #24) to interpolate the partials atx=0inF_QLWE, adds the constant term ofb, 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_QLWEis Shamir-shared in F_QLWE. By Shamir linearity over the same field, for any authorised subset T of size ≥ threshold:For an LWE ciphertext
(a, b)withb = Δm + e − a·s (mod QLWE), each party returnsp_i = <a, s_i>(the constant term of the ring product, asrlwe.Decryptorextracts it). The combiner runs: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:TestPartialDecrypt_3of2TestPartialDecrypt_5of3TestPartialDecrypt_7of4TestPartialDecrypt_21of11TestPartialDecrypt_STD128_5of3PN9QP28_STD128(OpenFHE STD128_LMKCDEY)Plus three structural / negative tests:
TestPartialDecrypt_RejectsInsufficient— combine refuses with< thresholdpartials.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 legacyUnderlyingKey: masterSKregression).Every test passes locally on Go 1.26.3 with both
PN10QP27andPN9QP28_STD128.Deferred to Phase 2 (NOT in this PR)
Documented in the package doc and tracked under issue #20:
PN9QP28_STD128. The Phase 1 API plumbs thefloodterm throughPartialDecryptbut 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.p_iis not currently detected. Phase 2 adds Feldman/Pedersen VSS commitments on partial shares so the combiner can reject deviating partials before combine.The existing fake-threshold path in
tfhe.go(the one gated byALLOW_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
polynomial.LagrangeAtZeroBigIntfor exactly this combine)