fix(keygen): emit full compressed ECDSA pubkey (33B), not bare X — fixes ~50% wrong addresses (#19)

The CGGMP21 keygen session serialized the group public key as the bare 32-byte
X-coordinate (PublicPoint().XScalar()), dropping the Y parity. pubKeyToEVMAddress
and pubKeyToBtcAddress then GUESS even-Y (0x02 prefix) when handed a 32-byte key,
so for every ODD-Y key (~50%) they derive the WRONG address — the sibling curve
point — which does NOT match the key the ring actually signs with. A threshold
signature over a digest then recovers to a different address than keygen reported,
making the wallet unusable (funds sent to the reported address are unspendable).

Fix: serialize PublicPoint().MarshalBinary() (33-byte compressed, 0x02/0x03 || X)
— exactly what signing_session_lss.go already uses for recovery — so keygen's
address == the signing key's address. pubKeyToEVMAddress (case 33),
pubKeyToBtcAddress (case 33), and CalculateRecoveryByte all already accept the
33-byte compressed form; the 32-byte path stays only as a legacy fallback.

Root-caused live: direct ring keygen returned evm_address 0x84e0…f894 for
ecdsa_pub_key X=5b0e9ad0…; a direct threshold sign recovered pubkey 03·5b0e9ad0…
(odd Y) → real address 0x10B8…15A9. The +1 parity bit is the whole bug.

Co-authored-by: hanzo-dev <[email protected]>
This commit is contained in:
2026-07-07 21:35:20 -07:00
committed by GitHub
co-authored by hanzo-dev
parent acdd6b6dfd
commit d625ef533a
+12 -5
View File
@@ -395,13 +395,20 @@ func (s *cggmp21KeygenSession) publishResult() {
return
}
// Convert public key to hex
// Use the X coordinate as a simple representation
// Serialize the FULL public-key POINT (33-byte compressed: 0x02/0x03 || X),
// NOT the bare 32-byte X-scalar. MarshalBinary preserves the Y parity, which
// the address/recovery paths need. Emitting only X loses parity, and
// pubKeyToEVMAddress/pubKeyToBtcAddress then GUESS even-Y (0x02) — deriving the
// WRONG address (the sibling curve point) for ~50% of keys (every odd-Y key),
// so the reported address does not match the key the ring actually signs with.
// The signing session already uses PublicPoint().MarshalBinary() (see
// signing_session_lss.go); keygen MUST serialize identically so keygen address
// == signing key. pubKeyToEVMAddress/pubKeyToBtcAddress + CalculateRecoveryByte
// all accept the 33-byte compressed form.
var pubKeyHex string
if s.config != nil && s.config.PublicPoint() != nil {
if xScalar := s.config.PublicPoint().XScalar(); xScalar != nil {
xBytes, _ := xScalar.MarshalBinary()
pubKeyHex = fmt.Sprintf("%x", xBytes)
if pubBytes, err := s.config.PublicPoint().MarshalBinary(); err == nil && len(pubBytes) > 0 {
pubKeyHex = fmt.Sprintf("%x", pubBytes)
}
}