mirror of
https://github.com/luxfi/threshold.git
synced 2026-10-30 15:15:53 +00:00
The generic FROST challenge is BLAKE3 over big-endian scalars. RFC 8032 needs SHA-512(R || A || M) reduced mod L with S encoded little-endian, so a threshold signature over curve.Ed25519 was rejected by every real verifier — which is why mpcd returned an empty eddsa_pub_key and SOL and TON had no signable key. This follows the shape already used twice here: taproot and sr25519 each carry a scheme through round1 -> round3, and Ed25519 is a third. StartSignCommon and StartSignSR25519Common keep their exact signatures and both forward to an unexported startSign, so those two paths are behaviourally untouched. Only the SCALAR needed reversing. Ed25519Point.MarshalBinary was already the canonical compressed Edwards encoding; Ed25519Scalar.MarshalBinary is big-endian and is flipped at the signature boundary alone. Verification delegates to crypto/ed25519 rather than reimplementing the equation. An in-house verifier only ever proves we agree with ourselves — exactly how a scheme that is not really Ed25519 passes its own tests. Round 3 gates on it, so a signature can only leave the protocol if the verifier Solana runs accepts it. Validated against all four RFC 8032 §7.1 vectors, each first re-derived with ed25519.Sign so a transcription typo fails loudly instead of silently weakening the suite; the production ed25519Challenge has ONE implementation and both round 2 and the vector test call it. SECURITY, and it is the reason to take this seriously: keygen now rejects non-prime-order points at Ed25519Point.UnmarshalBinary. The zksch proof on a polynomial's constant term does NOT exclude torsion — z*G = C + e*phi can be satisfied with torsion in phi by grinding C, since only e mod 8 matters, ~8 attempts. The payoff is not forgery, it is a BRICK: the group key carries torsion, the address base58-encodes normally and accepts deposits, and no quorum can ever sign, because z*G is always torsion-free. Unmarshal is the single wire entry point (cbor decodes coefficients through BinaryUnmarshaler), and the test splices an order-2 point into a real serialized commitment to prove it lands there. Ed25519-specific: secp256k1 has cofactor 1 and Ristretto255 has no torsion by construction. Go's verifier is cofactorless and does not torsion-check A; ed25519-dalek's verify_strict, which Solana runs, does. FROST satisfies both — R, A and [S]B are all torsion-free. 115 frost tests green.
88 lines
3.2 KiB
Go
88 lines
3.2 KiB
Go
package sign
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/luxfi/threshold/internal/round"
|
|
"github.com/luxfi/threshold/pkg/math/curve"
|
|
"github.com/luxfi/threshold/pkg/party"
|
|
"github.com/luxfi/threshold/pkg/protocol"
|
|
"github.com/luxfi/threshold/protocols/frost/keygen"
|
|
)
|
|
|
|
const (
|
|
// Frost Sign with Threshold.
|
|
protocolID = "frost/sign-threshold"
|
|
protocolIDTaproot = "frost/sign-threshold-taproot"
|
|
protocolIDSR25519 = "frost/sign-threshold-sr25519"
|
|
protocolIDEd25519 = "frost/sign-threshold-ed25519"
|
|
// This protocol has 3 concrete rounds.
|
|
protocolRounds round.Number = 3
|
|
)
|
|
|
|
func StartSignCommon(taproot bool, result *keygen.Config, signers []party.ID, messageHash []byte) protocol.StartFunc {
|
|
return startSign(taproot, false, false, nil, result, signers, messageHash)
|
|
}
|
|
|
|
func StartSignSR25519Common(taproot, sr25519 bool, signingContext []byte, result *keygen.Config, signers []party.ID, messageHash []byte) protocol.StartFunc {
|
|
return startSign(taproot, sr25519, false, signingContext, result, signers, messageHash)
|
|
}
|
|
|
|
// StartSignEd25519Common produces an RFC 8032 PureEdDSA signature.
|
|
//
|
|
// result must come from a key generation over curve.Ed25519, and message is the
|
|
// raw message: PureEdDSA signs M itself, so callers must not pre-hash it.
|
|
func StartSignEd25519Common(result *keygen.Config, signers []party.ID, message []byte) protocol.StartFunc {
|
|
return startSign(false, false, true, nil, result, signers, message)
|
|
}
|
|
|
|
func startSign(taproot, sr25519, ed25519 bool, signingContext []byte, result *keygen.Config, signers []party.ID, messageHash []byte) protocol.StartFunc {
|
|
return func(sessionID []byte) (round.Session, error) {
|
|
// For FROST signing, we use the original threshold from keygen
|
|
// The signers list should be at least threshold+1 parties
|
|
// but we still use the original threshold for protocol validation
|
|
signThreshold := result.Threshold
|
|
|
|
// Validate we have enough signers - FROST requires exactly threshold (t) signers, not t+1
|
|
if len(signers) < signThreshold {
|
|
return nil, fmt.Errorf("insufficient signers: need at least %d, got %d", signThreshold, len(signers))
|
|
}
|
|
|
|
info := round.Info{
|
|
FinalRoundNumber: protocolRounds,
|
|
SelfID: result.ID,
|
|
PartyIDs: signers,
|
|
Threshold: len(signers) - 1, // Session threshold is n-1 where n is number of participants
|
|
Group: result.PublicKey.Curve(),
|
|
}
|
|
if sr25519 {
|
|
info.ProtocolID = protocolIDSR25519
|
|
} else if ed25519 {
|
|
if _, ok := result.PublicKey.Curve().(curve.Ed25519); !ok {
|
|
return nil, fmt.Errorf("sign.StartSign: ed25519 signing requires an ed25519 key, got %q", result.PublicKey.Curve().Name())
|
|
}
|
|
info.ProtocolID = protocolIDEd25519
|
|
} else if taproot {
|
|
info.ProtocolID = protocolIDTaproot
|
|
} else {
|
|
info.ProtocolID = protocolID
|
|
}
|
|
|
|
helper, err := round.NewSession(info, sessionID, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("sign.StartSign: %w", err)
|
|
}
|
|
return &round1{
|
|
Base: helper,
|
|
taproot: taproot,
|
|
sr25519: sr25519,
|
|
ed25519: ed25519,
|
|
signingContext: signingContext,
|
|
M: messageHash,
|
|
Y: result.PublicKey,
|
|
YShares: result.VerificationShares.Points,
|
|
sI: result.PrivateShare,
|
|
}, nil
|
|
}
|
|
}
|