`go build ./...` failed with "missing go.sum entry for module providing
package" (pkg/thresholdd) — the go.mod was out of sync with the imports, so
individual `go get`s just walk further down the chain; tidy is the fix.
Also patches the panic-driven denial of service in the AWS SDK for Go v2
EventStream decoder, at minimum patched versions: eventstream v1.7.8,
service/s3 v1.97.3.
Verified: go build ./... goes from exit 1 to exit 0.
A quorum written as `"threshold": 3` is ambiguous — it reads as either the
signer count or the polynomial degree, and the two differ by one. That
ambiguity is what produced degree-0 (1-of-n) custody keys from configs that
said 3-of-5.
Policy now implements TextMarshaler/TextUnmarshaler over the operator form, so
any config, genesis blob or API payload that carries a quorum carries it in the
one representation that cannot be misread. Decode of an absent or undeployable
policy is an error rather than a zero value: {0,0} would reach cmp.Keygen as
degree -1, and a wrong-degree key that already holds funds has no recovery path
short of a resharing ceremony.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The stack had two different numbers called "threshold". cmp.Keygen and
frost.Keygen take t, the polynomial degree, and need t+1 signers. Operators,
dashboards and runbooks say k-of-n. Provisioning a "3-of-5" wallet by passing
threshold=3 produces a 4-of-5, silently — nothing errors, and the mistake only
surfaces when three custodians hold a ceremony and cannot sign a key that
already holds value.
pkg/quorum is now the single place those two numbers meet. Policy{K,N} states
what operators mean; Degree() == K-1 is the only k->t conversion in the tree.
Callers pass policy.Degree() and never compute the off-by-one themselves.
Proofs, all against the real protocols rather than mocks:
protocols/quorum_3of5_test.go — five-party CGGMP21 DKG at the policy degree,
three parties sign, and the signature both verifies under the group key and
ECRECOVERS to the group's EVM address. Recovery is the part that matters for
custody: a key can verify correctly and still resolve to an address no one
controls. Four different 3-subsets all sign for one address. Negatives: every
2-of-5 subset is refused, and a forged third share cannot produce a signature
that verifies. FROST covered at the same policy.
protocols/safe_3of5_test.go — the custody claim end to end. Real SafeL2 v1.5.0
bytecode behind the real SafeProxyFactory on the luxfi/geth EVM, owned solely
by the 3-of-5 group address. The digest comes from the deployed Safe's own
getTransactionHash, so the test cannot sign something the contract would not
check. execTransaction succeeds, value moves, the nonce advances, a replay is
rejected, and a valid signature from a different 3-of-5 key is rejected.
Fixes found along the way:
protocols/lss/adapters/evm.go — GenerateEVMAddress hashed the COMPRESSED
point, i.e. 32 bytes of X, where the EVM requires Keccak over the 64-byte
uncompressed X||Y. Every address it produced was well-formed and belonged to
no key; value sent to one is unspendable. It had no callers, so this was a
loaded gun rather than a live wound. Now decompresses, and reports failure
instead of returning a plausible wrong answer.
internal/test/harness.go — WithTimeout moved the harness deadline but not the
handler's own ProtocolTimeout, which was hard-coded to five minutes, so a
raised timeout silently did nothing and a CGGMP21 DKG under -race died at
five minutes blaming the context. The harness now owns one deadline. Also
removes a duplicate RunProtocolWithTimeout.
protocols/cmp/cmp_threshold_test.go — cases labelled "3-of-5" passed degree 3
and signed with four parties. They were 4-of-5. Labels now derive from the
policy, so the name and the behaviour are one fact.
go.sum — was missing entries for luxfi/metric and mattn/go-isatty; the FROST
packages did not compile from a clean checkout.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Env-gated integration test (LUX_DEVNET_RPC + LUX_DEVNET_PK): signs an EIP-155
legacy tx with the deployer key via SigEthereum, recovers the sender locally,
broadcasts it, and asserts the receipt lands with status=success. EIP-155
(chainID-protected) is used deliberately — Lux rejects unprotected pre-155 txs,
which is exactly why the KMS /sign format matters.
Verified on devnet chainID 31337: SigEthereum-signed tx mined, status=success.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
SigEthereum had ZERO callers (test or otherwise) and two latent hazards that
made it unsafe to wire: it mutated its receiver's S (Negate) and R
(UnmarshalBinary) in place — curve.Scalar/Point are pointer-backed, so a
value receiver still aliases — and it did a pointless post-hoc R rewrite.
Rewrite: copy S before low-S normalization (no aliasing), validate the R/S
encodings, and derive V from the compressed-R Y-parity byte (flipped when S is
negated). Output is the canonical 65-byte R.x‖S‖V (V in {0,1}), EIP-2 low-S,
directly accepted by luxfi/evm ecrecover.
Tests (SDKROOT="$(xcrun --show-sdk-path)" go test ./pkg/ecdsa):
- EVMRecoverable: 256 random signatures — each emits low-S, recovers to the
signer's pubkey via decred secp256k1 RecoverCompact (the routine luxfi/mpc
and the EVM's ecrecover use), and leaves the receiver unmutated.
- NormalizesHighS: forces the high-S branch and proves the low-S flip still
recovers to the signer.
This is the shared helper the mpcd signing sessions now wire for canonical
EVM-ready threshold-ECDSA signatures.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Regression guard for the KMS/MPC threshold fix (mpc 1e1d318): a CGGMP21 key at
polynomial degree t=2 (keyInfo.Threshold for a 3-of-5 wallet) MUST reject any
2-signer committee at cmp.Sign -> CanSign -> ValidThreshold(2,2)=false, before
any round, and MUST produce a verifying signature with any 3 signers. This is
the line between genuine threshold security and the degree-0 (1-of-n) bug.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Item5, re-targeted onto SHIPPED threshold v1.12.1 (cloud go.mod pin), not
the stale on-disk main.
The LSS Dilithium adapter (dilithium.go: sampleSecret, sampleError,
sampleMask) and the true-threshold ML-DSA Shamir adapter
(mldsa_threshold.go: sampleMaskingVector) sampled secret-key, error, and
masking-polynomial coefficients with math/rand, a non-cryptographic PRNG.
A predictable mask y is the lattice-signature analogue of ECDSA nonce
reuse and can lead to key recovery. Both adapters are gated out of the
production Corona/LSS path (factory_corona_prod.go) and unreferenced by
any consumer, but the footgun is loaded for whoever wires them in next.
Route all such sampling through a single crypto/rand-backed randIntn
helper (csprng.go). crypto/rand.Int already does unbiased rejection
sampling internally, so this is the one and only sampling path.
Regression tests (csprng_test.go): randIntn bounds/panic behavior,
freshness (non-determinism) of sampled secrets/masks across independent
DKG and signing runs, and an AST-based source scan (TestPackage_NoMath
RandForSecrets) that fails the build if math/rand (v1 or v2) is imported
into this package again.
No go.mod/go.sum change.
Consolidates all threshold-signature IMPLEMENTATIONS under luxfi/threshold
(alongside cmp/frost/lss/corona/pulsar). The Scheme/Signer/Aggregator/
Verifier INTERFACE stays in luxfi/crypto/threshold — it is the shared
low-level contract that crypto/signer + hsm depend on, so it cannot move
up without a module cycle. One home for impls, one home for the contract.
scheme/bls/ carries the native BLS Scheme + the dealerless Pedersen-VSS
DKG (RunDKG). It implements crypto/threshold.Scheme using crypto/bls
primitives. Consumers blank-import luxfi/threshold/scheme/bls to register.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
- protocols/lss/lss_pulsar_test.go: corona v0.8.0 Signer.Round1 now
returns (*Round1Data, error); capture the error (was assign-1-of-2).
- protocols/parity/parity_test.go: DELETED. It benchmarked the removed
v0.1 pulsar.ThresholdSigner/Combine small-committee API (ripped in H-1,
gone in pulsar v1.2.0). No backwards compat — dead bench for deleted
code is removed, not resurrected.
- pkg/thresholdd/{doc,profile,thresholdd_test}.go: correct stale comments
that named the removed v0.3 DealAlgebraicV03Shares/OrchestrateV03SignCtx
symbols; describe the dealerless committee-DKG + reconstruct-at-combine
reality and why the strict-PQ gate stays load-bearing.
corona v0.8.0 bakes the FULL-COMMITTEE Lagrange coefficient into every
share (keyera.computeFullCommitteeLagrange over all n; kernel sets
party.Lambda = share.Lambda, no per-subset recompute). The Lagrange
identity Σλ_i·f(i)=f(0) holds only with all n contributing, so the
threshold sign requires the full committee — t is the Shamir RECOVERY
threshold, signing is n-of-n. Dispatcher signed with only t shares →
incomplete Lagrange basis → self-verify failed. Fix: signers = all n
(matches every corona kernel round-trip test). TestCoronaRoundTrip green.
This package wires the github.com/luxfi/corona Module-LWE kernel
(Ringtail/Raccoon, q=0x1000000004A01), but carried stale "Pulsar"
labels from when Corona was historically named Pulsar:
- package doc claimed it wired the Pulsar kernel (github.com/luxfi/pulsar)
and was "the equivalent of protocols/corona" — self-contradictory; the
imports are github.com/luxfi/corona/{keyera,threshold}. Rewrote the doc
to state the scheme accurately and note the Corona-vs-Pulsar distinction
(both Module-LWE; Pulsar is FIPS-204/ML-DSA q=8380417, Corona is
Ringtail/Raccoon q=0x1000000004A01).
- error strings "pulsar:" → "corona:" (sentinel identities unchanged;
substrings asserted by tests preserved).
- exported aliases PulsarKeyEraID/PulsarGroupID → CoronaKeyEraID/
CoronaGroupID (the corona kernel already uses the Corona* names; these
wrappers had zero external consumers across ~/work/lux).
No wire format, modulus, or test vector changed. Doc-, string-, and
identifier-only. corona pkg builds + tests green.
The one canonical RefuseUnderStrictPQ gate (decomplected — magnetar's
duplicate slhdsatee gate removed with the TEE extraction). Drop the
chainID=="" short-circuit so empty/unknown chains consult the resolver;
production wires def=ProfileStrictPQ → refuse. Proven by the policy matrix.
TEE-backed threshold custody was a threshold-internal, fenced opt-in
surface reachable only from tests (never on the ZAP dispatcher wire).
Decomplect it out: the dealerless/permissionless signing path is the
production default with zero TEE dependency.
Removed from the threshold core:
- protocols/{mldsa,rlwe,slhdsa}-tee -> moved to github.com/luxfi/tee
- pkg/thresholdd/tee_options.go (cc/attest + go-sev-guest glue)
- the optional Sign_TEE / SetTEEBackend surface on the pulsar, corona
and magnetar dispatcher schemes (teeBackend fields + unwired-error
sentinels)
- magnetar's duplicate slhdsatee-typed strict-PQ gate
(profile/magnetarRefuseUnderStrictPQ/SetChainSecurityProfile/
SetCombinerPool/Combine_TEE/AttestCombinerMember/PoolCombineMember)
- the dispatcher-only TEE test files + their testdata
The permissionless keygen/sign/sign_ctx/verify paths are byte-unchanged.
The ONE canonical strict-PQ gate (profile.go RefuseUnderStrictPQ, applied
at Sign_Ctx_Profile) is retained; magnetar's second, TEE-typed gate is
gone -- one gate, one place. go mod tidy drops go-sev-guest AND luxfi/mpc
(both were TEE-only deps; the core had zero real imports of either).
Dependency direction is one-way: luxfi/tee depends on the cores
(pulsar/corona/magnetar/mpc), never the reverse.
Build + vet + dispatcher tests green; no consensus/mpc/corona consumer
referenced the removed surface.
- rm protocols/tfhe (ALLOW_FAKE_TFHE stub holding the full master key per
party; real threshold-FHE is luxfi/fhe). Zero external importers.
- RefuseUnderStrictPQ: drop the chainID=="" short-circuit so empty/unknown
chains consult the resolver. Production wires def=ProfileStrictPQ → empty/
unknown DEFAULT-REFUSE. Proven by TestRefuseUnderStrictPQ_PolicyMatrix
(empty/unknown REFUSED on a strict-default node).
The threshold/protocols/pulsar alias is the surface consensus reaches
into the pulsar reference kernel. It re-exported the v0.1
reconstruct-then-sign API — ThresholdSigner, NewThresholdSigner,
Combine, and the Round1Message/Round2Message wire types. That path
reconstructs the full ML-DSA signing key in aggregator memory (H-1);
its only production consumer was consensus's quasar wave_signer, now
deleted.
Removed those five re-exports. The alias now exposes only identity,
DKG, single-party sign/verify, and parameter surfaces — no
reconstruct-then-sign forward. Live-path threshold signing runs through
the corona dealerless kernel (protocols/corona), not this alias.
Verified: GOWORK=off go build ./... exit 0 (alias + full module);
gofmt clean; no threshold-internal consumer of the removed symbols.
Same upstream same-version tag re-cut churn picked up across the rip
branches (cf. mpc e3dcc3f). go mod verify clean; build green. Was the
'wip in-flight checkpoint' placeholder; this is the actual change.
The thresholdd daemon registered a "bls" scheme whose keygen ran a
TrustedDealer in-process (one node mints the whole group secret key) —
the same single-point-of-compromise trusted-dealer BLS custody removed
from mpc. BLS is consensus-only (native per-validator keys), never a
custodied threshold key. Remove the daemon's bls scheme so the dispatcher
can no longer mint a trusted-dealer BLS key.
- Delete pkg/thresholdd/bls.go (blsScheme / TrustedDealer keygen).
- server.go: drop s.schemes["bls"] + the bls keygen/sign/verify procs.
- zap_schema.go: drop ProcBLS{Keygen,Sign,Verify} opcodes.
- Tests: delete the bls-specific round-trip + benchmarks; rewire the
auth-gate tests (which used bls only as a fast keygen driver) to frost;
drop bls from the sign_ctx unsupported-scheme loop.
- Scrub stale bls mentions from doc comments.
NOT touched: protocols/bls (the library primitive) stays — luxfi/chains
(a non-owned repo) consumes bls.TrustedDealer/AggregateSignatures
directly. Deleting protocols/bls must be a coordinated change with the
chains owner; flagged, not forced here.
Verified: GOWORK=off go build ./... green; affected thresholdd tests
(auth-gate, ProcOpcode, ZapShares, FrostRoundTrip, sign_ctx unsupported)
pass without -short. Zero bls refs remain in thresholdd.
Per CTO directive — no wire compat, no alias, no shim. The R-LWE
threshold signature scheme is named Corona across all live Lux
code. Pre-2026-06 callers using "corona" must migrate to "corona"
on the procedure-routing surface.
- Drop schemeAliases map + canonicalScheme() from types.go.
- Delete TestCoronaCoronaWireAlias from thresholdd_test.go.
- protocols/parity test comment sweep.
Forward-only; no migration window, no fallback path.
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 Corona-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/...
Sweep the residual "corona" 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 `corona`
scheme-name string + import alias `coronaKernel`→`coronaKernel`;
align all narrative comments + the report-printer with the
canonical Corona name. The previous string `"corona"` 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 `corona.<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: TestCoronaCoronaWireAlias
pins the alias behavior end-to-end (1-of-2 round-trip through the
legacy `corona.{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 +
TestCoronaCoronaWireAlias 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
CoronaSignature/SigCorona 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_corona.* + ops/crypto/
corona/ — sister-repo kernel rename owned by lux-private agent.
Companion: luxfi/node Dockerfile.multichain `-tags corona` →
`-tags corona` committed separately on that repo's working branch.
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→Corona
rename.
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: Hanzo Dev <dev@hanzo.ai>
Removes all Corona 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/corona is no longer needed
now that Corona is the only name in the codebase.
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)
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.