`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>
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.
- 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.
- Update github.com/luxfi/log to v1.4.1 for correct Level type
- Remove broken replace directive from go.mod
- Fix nil pointer panic in NewHandler when logger is nil
- Adds explicit nil check before calling logger.IsZero()
- Fix CalculateThreshold test to use single-parameter API (n -> floor(n/2)+1)
- Add mockThresholdProvider for local testing (fhe.NewLocalThresholdProvider doesn't exist)
- All TFHE tests now pass
- Support x-only format (64 bytes) for BIP-340/Taproot compatibility
- Support compressed format (65 bytes) for general use
- Add NewSignature constructor
- Update dependencies (luxfi/log)
Ported changes from taurusgroup/multi-party-sig upstream:
1. Security fix: Increased ZKModIterations from 12 to 128
- Improves security of Paillier-Blum modulus validation
- Aligns with theoretical statistical security parameter requirements
2. Extended OT monochrome check improvement (based on paper revision)
- Added doubleFieldElement type for proper GF(2^k) multiplication
- New helper functions: randFe, pluckColumnToFieldElement,
pluckBitsToFieldElements, transposeToFieldSizeElements, adjustBatchSize
- Improved constant-time equality check
- Better batch size padding for security
3. Comment/typo fixes from upstream
Adapted to local naming conventions (Delta vs _Delta, _KDelta vs _K_Delta)
while preserving the security improvements from the upstream implementation.
Reference: https://eprint.iacr.org/2015/546
- Rename Avalanche chain references to Lux in LSS adapters
- Update chain symbol from AVAX to LUX
- Remove obsolete .old and .bak test files
- Update README to reflect Lux branding