Files
mpc/pkg/address/address_test.go
zeekay cb77fab979 Ed25519 keygen is real: sol_address comes from KeygenEd25519 and nothing else
luxfi/threshold v1.12.7 added FROST(Ed25519, SHA-512), so Solana and TON finally
have a signable curve. The sessions named "EdDSA" were calling KeygenTaproot —
BIP-340 over secp256k1 — whose key is also 32 bytes, sails through the
len(EDDSAPubKey)==32 gate, and base58-encodes into a real-looking Solana address
the ring can never sign for.

⚠ A LIVE WRONG-ADDRESS BUG went with it: handlers_wallets served
addresses["ton"] = *wallet.SolAddress. A TON address is not a base58 pubkey — it
is workchain plus a hash of the wallet contract's StateInit, base64url with a
CRC16 — so the API was handing out an address that decodes to nothing. Deleted
rather than guessed; TON returns when its encoding is real.

The keygen tests used to call frost.KeygenEd25519 directly, which validated the
LIBRARY and not the daemon: reintroducing KeygenTaproot left every package
green. They now go through the production session, and so does a test that
constructs the real signing session and proves the caller's bytes reach the
protocol unhashed. Both original bugs reintroduced together fail 11 tests.

secp256k1 keygen changes from fail-open to fail-closed, and that is the one
behavioural change to the existing EVM/BTC path. An empty pubkey previously
meant any of nil config, marshal failure, kvstore.Put failure or keyinfo-save
failure — and was returned as SUCCESS, announcing addresses while fewer than
t+1 nodes hold a share. Applying fail-closed to Solana while leaving the
higher-value leg fail-open is incoherent.

Also: the KMS share classifier still keyed off the retired "frost:" prefix; the
CGGMP21 session published its own local failure onto the same first-wins subject
as the ring's verdict, so one node's failure could beat the ring's success; and
one 120s deadline shared across two sequential ceremonies starved the cheap leg.

NOT YET SPENDABLE. ConsensusKeyInfoStore.Save is the only RegisterKey caller and
hardcodes "secp256k1", and TriggerSign reads the curve from that field, so no
product entry point can reach Ed25519 signing. The address is correct; signing
it needs a wire-contract change shared with lux/wallet.
2026-08-06 13:23:18 -07:00

131 lines
4.5 KiB
Go

package address
import (
"crypto/ed25519"
"crypto/rand"
"testing"
"github.com/luxfi/threshold/pkg/math/curve"
"github.com/mr-tron/base58"
"github.com/stretchr/testify/require"
)
// TestSolanaRoundTripsARealEd25519Key pins the encoding itself: a Solana address
// is the base58 of the raw 32-byte key, nothing added and nothing hashed.
func TestSolanaRoundTripsARealEd25519Key(t *testing.T) {
for i := 0; i < 64; i++ {
pub, _, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
addr, err := Solana(pub)
require.NoError(t, err, "a stdlib Ed25519 key must always yield an address")
decoded, err := base58.Decode(addr)
require.NoError(t, err)
require.Equal(t, []byte(pub), decoded, "the address must decode back to the key exactly")
}
}
// TestSolanaRefusesWrongLength covers truncation and padding, the failure modes
// that come from a mis-sized buffer rather than a wrong curve.
func TestSolanaRefusesWrongLength(t *testing.T) {
pub, _, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
for name, key := range map[string][]byte{
"nil": nil,
"empty": {},
"truncated": pub[:31],
"too_long": append([]byte(pub), 0x00),
"secp256k1_compressed_33_bytes": append([]byte{0x02}, pub...),
} {
t.Run(name, func(t *testing.T) {
addr, err := Solana(key)
require.ErrorIs(t, err, ErrNotEd25519)
require.Empty(t, addr, "an error must never be accompanied by an address")
})
}
}
// TestSolanaRefusesTorsionPoints is the subtle one. These decode as perfectly
// good curve points, so nothing short of a subgroup check rejects them — and a
// torsion-tainted key makes the Schnorr verification equation unsatisfiable, so
// the address would take deposits and never spend.
func TestSolanaRefusesTorsionPoints(t *testing.T) {
// Canonical small-order points of edwards25519 (RFC 8032 §5.1.7 / the
// standard small-order set): identity, the order-2 point, and the two
// order-4 points.
smallOrder := map[string]string{
"identity": "0100000000000000000000000000000000000000000000000000000000000000",
"order_2": "ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f",
"order_4_a": "0000000000000000000000000000000000000000000000000000000000000080",
"order_4_b": "0000000000000000000000000000000000000000000000000000000000000000",
"order_8_mix1": "26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05",
"order_8_mix2": "c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a",
}
for name, hexKey := range smallOrder {
t.Run(name, func(t *testing.T) {
key := mustHex(t, hexKey)
// Every one of these must be refused. The identity is included
// deliberately: it passes the prime-order test ([L]O = O) and is the
// worst case of the set, because a wallet keyed to it is spendable
// by anybody rather than by nobody.
addr, err := Solana(key)
require.Error(t, err, "small-order point %s must never yield an address", name)
require.ErrorIs(t, err, ErrNotEd25519)
require.Empty(t, addr, "an error must never be accompanied by an address")
})
}
}
// TestRequireEd25519AgreesWithTheDKGDecoder makes the address gate and the
// ceremony gate the same gate. If they ever diverge, a key could be good enough
// to mint an address but not good enough to sign with, or the reverse.
func TestRequireEd25519AgreesWithTheDKGDecoder(t *testing.T) {
for i := 0; i < 256; i++ {
candidate := make([]byte, 32)
_, err := rand.Read(candidate)
require.NoError(t, err)
addrErr := RequireEd25519(candidate)
point := (curve.Ed25519{}).NewPoint()
dkgErr := point.UnmarshalBinary(candidate)
if dkgErr == nil && point.IsIdentity() {
// The one intended divergence: the DKG decoder accepts the identity,
// the address gate does not. Asserted rather than tolerated so the
// difference stays deliberate.
require.Error(t, addrErr, "identity must be refused by the address gate")
continue
}
require.Equal(t, dkgErr == nil, addrErr == nil,
"address gate and DKG decoder disagree on %x", candidate)
}
}
func mustHex(t *testing.T, s string) []byte {
t.Helper()
out := make([]byte, len(s)/2)
for i := 0; i < len(out); i++ {
var b int
for j := 0; j < 2; j++ {
c := s[i*2+j]
var v int
switch {
case c >= '0' && c <= '9':
v = int(c - '0')
case c >= 'a' && c <= 'f':
v = int(c-'a') + 10
default:
t.Fatalf("bad hex %q", s)
}
b = b<<4 | v
}
out[i] = byte(b)
}
return out
}