Files
threshold/protocols/lss/adapters/adapters_test.go
zeekay 7ec0a63610 lss: gate research-preview Corona adapter behind -tags=researchpreview
Closes a HIGH-severity production-routing risk identified in the
pulsar/corona audit (2026-05-31).

The file protocols/lss/adapters/corona.go was a paper-grade
demonstrator that named itself "Corona" — colliding with the real
luxfi/corona Ring-LWE primitive. Anyone invoking the LSS factory with
`lss.NewLSS(lss.Corona, ...)` got the toy adapter, not the production
primitive. The toy adapter has:

 - A textbook-LWE "DKG" that sums each party's s_i into a combined
   master secret at the aggregator. NO Lagrange threshold sharing.
   The threshold parameter is decorative.

 - Q = 12289 (< 2^14) — far below any meaningful LWE security bound.

 - A Box-Muller-from-uniform-mod-Q Gaussian sampler using
   floating-point math on secret-dependent inputs. NOT constant-time.
   Side-channel leakage is GUARANTEED.

 - No matching verifier.

This commit:

  - adapters/corona.go     +build researchpreview
  - adapters/corona_test.go  +build researchpreview
  - adapters/corona_external_test.go  new gated external-test file
  - factory_corona_prod.go    new — production refuses with a routing
                              error pointing at
                              luxfi/threshold/protocols/corona →
                              luxfi/corona
  - factory_corona_research.go  new — research-preview returns the toy

Tests and benchmarks in adapters_test.go and full_coverage_test.go
that referenced NewCoronaAdapter / GetRecommendedParams have moved to
corona_external_test.go (gated). Production `go test ./protocols/lss/
adapters/...` passes; research `go test -tags researchpreview` also
passes.

Production users that need PQ threshold signing must route through
luxfi/threshold/protocols/corona (which wraps luxfi/corona): real
Ring-LWE with Pedersen-DKG, canonical wire codec (corona@42838bd), and
dudect-validated CT hot paths.

(cherry picked from commit 06e790c4a8c4a89a1b4a8aa2673fb02192dfb8c5)
2026-05-31 22:53:11 -07:00

629 lines
18 KiB
Go

package adapters_test
import (
"crypto/rand"
"encoding/hex"
"fmt"
"math/big"
"testing"
"github.com/luxfi/threshold/pkg/math/curve"
"github.com/luxfi/threshold/pkg/party"
"github.com/luxfi/threshold/protocols/lss/adapters"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestChainCompatibilityMatrix tests all chain adapters with their supported signature types
func TestChainCompatibilityMatrix(t *testing.T) {
tests := []struct {
chain string
signatureTypes []adapters.SignatureType
testData interface{}
}{
{
chain: "xrpl",
signatureTypes: []adapters.SignatureType{adapters.SignatureECDSA, adapters.SignatureEdDSA},
testData: []byte("test xrpl transaction"),
},
{
chain: "ethereum",
signatureTypes: []adapters.SignatureType{adapters.SignatureECDSA},
testData: &adapters.LegacyTransaction{
Nonce: 1,
GasPrice: big.NewInt(20000000000),
GasLimit: 21000,
Value: big.NewInt(1000000000000000000),
},
},
{
chain: "bitcoin",
signatureTypes: []adapters.SignatureType{adapters.SignatureECDSA, adapters.SignatureSchnorr},
testData: &adapters.LegacyBitcoinTx{
Version: 2,
LockTime: 0,
SigHash: adapters.SigHashAll,
},
},
{
chain: "solana",
signatureTypes: []adapters.SignatureType{adapters.SignatureEdDSA},
testData: &adapters.SolanaMessage{
NumRequiredSignatures: 1,
RecentBlockhash: [32]byte{1, 2, 3},
},
},
}
for _, tt := range tests {
for _, sigType := range tt.signatureTypes {
testName := fmt.Sprintf("%s_%v", tt.chain, sigType)
t.Run(testName, func(t *testing.T) {
testChainAdapter(t, tt.chain, sigType, tt.testData)
})
}
}
}
func testChainAdapter(t *testing.T, chain string, sigType adapters.SignatureType, testData interface{}) {
// Create adapter
factory := &adapters.AdapterFactory{}
adapter := factory.NewAdapter(chain, sigType)
require.NotNil(t, adapter, "adapter should be created for %s", chain)
// Test digest computation
digest, err := adapter.Digest(testData)
require.NoError(t, err)
assert.NotEmpty(t, digest)
assert.Len(t, digest, 32, "digest should be 32 bytes")
// Create mock shares
shares := createMockShares(t, sigType, 3, 2)
// Test partial signing
var partialSigs []adapters.PartialSig
for _, share := range shares[:2] { // Use threshold number
partial, signErr := adapter.SignEC(digest, share)
require.NoError(t, signErr)
assert.NotNil(t, partial)
partialSigs = append(partialSigs, partial)
}
// Test aggregation
fullSig, err := adapter.AggregateEC(partialSigs)
require.NoError(t, err)
assert.NotNil(t, fullSig)
// Test encoding
encoded, err := adapter.Encode(fullSig)
require.NoError(t, err)
assert.NotEmpty(t, encoded)
// Verify encoding format based on chain
verifyEncodingFormat(t, chain, sigType, encoded)
}
// TestXRPLSpecificFeatures tests XRPL-specific requirements
func TestXRPLSpecificFeatures(t *testing.T) {
t.Run("STX_SMT_Prefixes", func(t *testing.T) {
// Test single-signing prefix
xrplSingle, err := adapters.NewXRPLAdapter(adapters.SignatureECDSA, false)
require.NoError(t, err)
txBlob := []byte("test transaction")
digestSingle, err := xrplSingle.Digest(txBlob)
require.NoError(t, err)
// Test multi-signing prefix
xrplMulti, err := adapters.NewXRPLAdapter(adapters.SignatureECDSA, true)
require.NoError(t, err)
digestMulti, err := xrplMulti.Digest(txBlob)
require.NoError(t, err)
// Digests should be different due to different prefixes
assert.NotEqual(t, digestSingle, digestMulti)
})
t.Run("Ed25519_Prefix", func(t *testing.T) {
xrpl, err := adapters.NewXRPLAdapter(adapters.SignatureEdDSA, false)
require.NoError(t, err)
// Create mock public key (using Secp256k1 as Ed25519 not available)
pubKey := curve.Secp256k1{}.NewBasePoint()
formatted := xrpl.FormatPublicKey(pubKey)
// Should start with ED prefix
decoded, err := hex.DecodeString(formatted)
require.NoError(t, err)
assert.Equal(t, byte(0xED), decoded[0])
})
t.Run("Low_S_Normalization", func(t *testing.T) {
xrpl, err := adapters.NewXRPLAdapter(adapters.SignatureECDSA, false)
require.NoError(t, err)
// Create high S value
group := curve.Secp256k1{}
orderMod := group.Order()
order := orderMod.Big()
halfOrder := new(big.Int).Div(order, big.NewInt(2))
// highS would be used for testing high S normalization
// highS := new(big.Int).Add(halfOrder, big.NewInt(100))
// Create partial signature
partial := &adapters.ECDSAPartialSig{
PartyID: "test",
R: group.NewScalar(),
S: group.NewScalar(),
}
// Aggregate should normalize to low S
full, err := xrpl.AggregateEC([]adapters.PartialSig{partial})
require.NoError(t, err)
ecdsaSig := full.(*adapters.ECDSAFullSig)
// Check S is normalized (would need proper scalar conversion)
// For now, just verify signature was created
_ = ecdsaSig.S
// sInt := new(big.Int) // Would convert S to big.Int for comparison
assert.NotNil(t, ecdsaSig.S, "S should be present")
_ = halfOrder // halfOrder would be used for S normalization check
})
}
// TestEthereumSpecificFeatures tests Ethereum-specific requirements
func TestEthereumSpecificFeatures(t *testing.T) {
t.Run("EIP155_ChainID", func(t *testing.T) {
eth := adapters.NewEthereumAdapter()
// Test different chain IDs
chainIDs := []*big.Int{
big.NewInt(1), // Mainnet
big.NewInt(5), // Goerli
big.NewInt(137), // Polygon
big.NewInt(42161), // Arbitrum
}
for _, chainID := range chainIDs {
eth.SetChainID(chainID)
tx := &adapters.LegacyTransaction{
Nonce: 1,
GasPrice: big.NewInt(20000000000),
GasLimit: 21000,
}
digest, err := eth.Digest(tx)
require.NoError(t, err)
assert.NotEmpty(t, digest)
}
})
t.Run("TypedTransactions", func(t *testing.T) {
eth := adapters.NewEthereumAdapter()
// Test EIP-1559 transaction
eip1559 := &adapters.EIP1559Transaction{
ChainID: big.NewInt(1),
Nonce: 1,
MaxPriorityFeePerGas: big.NewInt(1000000000),
MaxFeePerGas: big.NewInt(30000000000),
GasLimit: 21000,
}
digest1559, err := eth.Digest(eip1559)
require.NoError(t, err)
assert.Len(t, digest1559, 32)
// Test EIP-4844 blob transaction
eip4844 := &adapters.EIP4844Transaction{
ChainID: big.NewInt(1),
MaxFeePerBlobGas: big.NewInt(1000000000),
BlobVersionedHashes: [][32]byte{{1}, {2}},
}
digest4844, err := eth.Digest(eip4844)
require.NoError(t, err)
assert.Len(t, digest4844, 32)
})
}
// TestBitcoinTaprootFeatures tests Bitcoin Taproot-specific features
func TestBitcoinTaprootFeatures(t *testing.T) {
t.Run("BIP340_Schnorr", func(t *testing.T) {
btc := adapters.NewBitcoinAdapter(adapters.SignatureSchnorr)
// Create Taproot transaction
taproot := &adapters.TaprootTx{
SegwitTx: adapters.SegwitTx{
Version: 2,
Amount: 100000000,
Sequence: 0xfffffffd,
SigHash: adapters.SigHashDefault,
},
ScriptPath: false, // Key path spend
}
digest, err := btc.Digest(taproot)
require.NoError(t, err)
assert.Len(t, digest, 32)
// Test with tweak
// Note: Taproot tweak is applied to the private key during signing,
// not to the message digest, so the digest should be the same
tweak := make([]byte, 32)
rand.Read(tweak)
btc.SetTaprootTweak(tweak)
digestTweaked, err := btc.Digest(taproot)
require.NoError(t, err)
assert.Equal(t, digest, digestTweaked, "digest should be the same, tweak is applied to key")
})
t.Run("ScriptPath_Spending", func(t *testing.T) {
btc := adapters.NewBitcoinAdapter(adapters.SignatureSchnorr)
// Create script path spend
taproot := &adapters.TaprootTx{
ScriptPath: true,
TapScript: []byte{0x51}, // OP_1
ControlBlock: make([]byte, 33),
}
digest, err := btc.Digest(taproot)
require.NoError(t, err)
assert.NotEmpty(t, digest)
})
}
// TestSolanaFeatures tests Solana-specific features
func TestSolanaFeatures(t *testing.T) {
t.Run("Ed25519_Only", func(t *testing.T) {
sol := adapters.NewSolanaAdapter()
// Solana only supports Ed25519
config := &adapters.UnifiedConfig{
SignatureScheme: adapters.SignatureEdDSA,
Group: curve.Ed25519{},
}
err := sol.ValidateConfig(config)
require.NoError(t, err)
// Should reject ECDSA
config.SignatureScheme = adapters.SignatureECDSA
err = sol.ValidateConfig(config)
assert.Error(t, err)
})
t.Run("CompactArray_Encoding", func(t *testing.T) {
sol := adapters.NewSolanaAdapter()
msg := &adapters.SolanaMessage{
NumRequiredSignatures: 2,
AccountKeys: [][32]byte{{1}, {2}, {3}},
Instructions: []*adapters.SolanaInstruction{
{
ProgramIDIndex: 0,
AccountIndices: []byte{1, 2},
Data: []byte{0x02, 0x00, 0x00, 0x00},
},
},
}
digest, err := sol.Digest(msg)
require.NoError(t, err)
assert.NotEmpty(t, digest)
})
t.Run("PDA_Derivation", func(t *testing.T) {
sol := adapters.NewSolanaAdapter()
programID := [32]byte{1, 2, 3}
seeds := [][]byte{
[]byte("threshold"),
[]byte("wallet"),
}
pda, bump, err := sol.ComputeProgramDerivedAddress(programID, seeds)
require.NoError(t, err)
assert.NotEqual(t, [32]byte{}, pda)
assert.Greater(t, bump, byte(0))
})
}
// TestCoronaPQAdapter (research preview only) lives in
// corona_external_test.go behind -tags=researchpreview, mirroring the
// LSS-side adapter gate. Production builds: route post-quantum
// threshold through luxfi/threshold/protocols/corona instead. See the
// disclosure block at the top of adapters/corona.go.
// Benchmark tests
func BenchmarkAdapters(b *testing.B) {
xrplECDSA, err := adapters.NewXRPLAdapter(adapters.SignatureECDSA, false)
if err != nil {
b.Fatal(err)
}
xrplEdDSA, err := adapters.NewXRPLAdapter(adapters.SignatureEdDSA, false)
if err != nil {
b.Fatal(err)
}
benchmarks := []struct {
name string
adapter adapters.SignerAdapter
sigType adapters.SignatureType
}{
{"XRPL_ECDSA", xrplECDSA, adapters.SignatureECDSA},
{"XRPL_EdDSA", xrplEdDSA, adapters.SignatureEdDSA},
{"Ethereum", adapters.NewEthereumAdapter(), adapters.SignatureECDSA},
{"Bitcoin_ECDSA", adapters.NewBitcoinAdapter(adapters.SignatureECDSA), adapters.SignatureECDSA},
{"Bitcoin_Schnorr", adapters.NewBitcoinAdapter(adapters.SignatureSchnorr), adapters.SignatureSchnorr},
{"Solana", adapters.NewSolanaAdapter(), adapters.SignatureEdDSA},
// Corona_128 benchmark moved to corona_external_test.go (under
// -tags=researchpreview) — see TestCoronaPQAdapter disclosure.
}
for _, bench := range benchmarks {
b.Run(bench.name+"_Digest", func(b *testing.B) {
data := make([]byte, 256)
rand.Read(data)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = bench.adapter.Digest(data)
}
})
b.Run(bench.name+"_Sign", func(b *testing.B) {
digest := make([]byte, 32)
rand.Read(digest)
share := createMockShares(b, bench.sigType, 1, 1)[0]
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = bench.adapter.SignEC(digest, share)
}
})
b.Run(bench.name+"_Aggregate", func(b *testing.B) {
shares := createMockShares(b, bench.sigType, 5, 3)
digest := make([]byte, 32)
var partials []adapters.PartialSig
for _, share := range shares[:3] {
partial, _ := bench.adapter.SignEC(digest, share)
partials = append(partials, partial)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = bench.adapter.AggregateEC(partials)
}
})
}
}
// Helper functions
func createMockShares(t testing.TB, sigType adapters.SignatureType, n, threshold int) []adapters.Share {
var shares []adapters.Share
for i := 0; i < n; i++ {
// For testing purposes, use scalar values for all share types
// Real Corona shares would be CoronaSecretShare structs
scalar := curve.Secp256k1{}.NewScalar()
shares = append(shares, adapters.Share{
ID: party.ID(fmt.Sprintf("party_%d", i)),
Value: scalar,
Index: i,
})
}
return shares
}
func verifyEncodingFormat(t *testing.T, chain string, sigType adapters.SignatureType, encoded []byte) {
switch chain {
case "xrpl":
if sigType == adapters.SignatureECDSA {
// DER format check
assert.Equal(t, byte(0x30), encoded[0], "ECDSA should be DER encoded")
} else {
// Ed25519: 64 bytes
assert.Len(t, encoded, 64, "Ed25519 signature should be 64 bytes")
}
case "ethereum":
// r(32) + s(32) + v(1) = 65 bytes
assert.Len(t, encoded, 65, "Ethereum signature should be 65 bytes")
case "bitcoin":
if sigType == adapters.SignatureECDSA {
// DER format
assert.Equal(t, byte(0x30), encoded[0])
} else {
// BIP340: 64 bytes
assert.Len(t, encoded, 64, "Schnorr signature should be 64 bytes")
}
case "solana":
// Ed25519: 64 bytes
assert.Len(t, encoded, 64, "Solana signature should be 64 bytes")
}
}
// TestCrossChainCompatibility tests that keys can be used across compatible chains
func TestCrossChainCompatibility(t *testing.T) {
// Test that the same ECDSA key works on multiple chains
ecdsaChains := []string{"xrpl", "ethereum", "bitcoin"}
// Create unified config with ECDSA
config := &adapters.UnifiedConfig{
ID: "alice",
Threshold: 2,
SignatureScheme: adapters.SignatureECDSA,
Group: curve.Secp256k1{},
SecretShare: curve.Secp256k1{}.NewScalar(),
PublicKey: curve.Secp256k1{}.NewBasePoint(),
}
factory := &adapters.AdapterFactory{}
for _, chain := range ecdsaChains {
adapter := factory.NewAdapter(chain, adapters.SignatureECDSA)
err := adapter.ValidateConfig(config)
assert.NoError(t, err, "ECDSA config should work on %s", chain)
}
// Test Ed25519 chains
ed25519Chains := []string{"xrpl", "solana"}
config.SignatureScheme = adapters.SignatureEdDSA
config.Group = curve.Ed25519{}
config.SecretShare = curve.Ed25519{}.NewScalar()
config.PublicKey = curve.Ed25519{}.NewBasePoint()
for _, chain := range ed25519Chains {
adapter := factory.NewAdapter(chain, adapters.SignatureEdDSA)
err := adapter.ValidateConfig(config)
assert.NoError(t, err, "Ed25519 config should work on %s", chain)
}
}
// TestEndToEndThresholdSignature tests complete threshold signature flow
func TestEndToEndThresholdSignature(t *testing.T) {
// Simulate 3-of-5 threshold signing
n := 5
threshold := 3
// Test each supported chain
chains := []struct {
name string
sigType adapters.SignatureType
}{
{"xrpl", adapters.SignatureECDSA},
{"ethereum", adapters.SignatureECDSA},
{"bitcoin", adapters.SignatureSchnorr},
{"solana", adapters.SignatureEdDSA},
}
for _, chain := range chains {
t.Run(chain.name, func(t *testing.T) {
factory := &adapters.AdapterFactory{}
adapter := factory.NewAdapter(chain.name, chain.sigType)
// Create shares
shares := createMockShares(t, chain.sigType, n, threshold)
// Create message
message := []byte(fmt.Sprintf("test message for %s", chain.name))
digest, err := adapter.Digest(message)
require.NoError(t, err)
// Each party creates partial signature
var partials []adapters.PartialSig
for i := 0; i < threshold; i++ {
partial, signErr := adapter.SignEC(digest, shares[i])
require.NoError(t, signErr)
partials = append(partials, partial)
}
// Aggregate signatures
fullSig, err := adapter.AggregateEC(partials)
require.NoError(t, err)
// Encode for wire format
encoded, err := adapter.Encode(fullSig)
require.NoError(t, err)
// Verify encoding is valid
assert.NotEmpty(t, encoded)
t.Logf("%s signature size: %d bytes", chain.name, len(encoded))
})
}
}
// TestAdapterErrorHandling tests error cases
func TestAdapterErrorHandling(t *testing.T) {
t.Run("InvalidDigestInput", func(t *testing.T) {
adapter, err := adapters.NewXRPLAdapter(adapters.SignatureECDSA, false)
require.NoError(t, err)
// Invalid input type
_, err = adapter.Digest(123)
assert.Error(t, err)
})
t.Run("InsufficientPartialSignatures", func(t *testing.T) {
adapter := adapters.NewEthereumAdapter()
// Empty partial signatures
_, err := adapter.AggregateEC([]adapters.PartialSig{})
assert.Error(t, err)
})
t.Run("MismatchedSignatureType", func(t *testing.T) {
adapter := adapters.NewSolanaAdapter()
// Try to use ECDSA signature with Ed25519 adapter
ecdsaSig := &adapters.ECDSAFullSig{
R: curve.Secp256k1{}.NewScalar(),
S: curve.Secp256k1{}.NewScalar(),
}
_, err := adapter.Encode(ecdsaSig)
assert.Error(t, err)
})
t.Run("InvalidConfiguration", func(t *testing.T) {
adapter := adapters.NewBitcoinAdapter(adapters.SignatureSchnorr)
// Wrong signature scheme for Bitcoin
config := &adapters.UnifiedConfig{
SignatureScheme: adapters.SignatureEdDSA, // Bitcoin doesn't support EdDSA
Group: curve.Secp256k1{},
}
err := adapter.ValidateConfig(config)
assert.Error(t, err)
})
}
// TestParallelSigning tests concurrent signing operations
func TestParallelSigning(t *testing.T) {
adapter, err := adapters.NewXRPLAdapter(adapters.SignatureECDSA, false)
require.NoError(t, err)
shares := createMockShares(t, adapters.SignatureECDSA, 10, 6)
message := []byte("parallel test message")
digest, err := adapter.Digest(message)
require.NoError(t, err)
// Sign in parallel
type result struct {
partial adapters.PartialSig
err error
}
results := make(chan result, 6)
for i := 0; i < 6; i++ {
go func(share adapters.Share) {
partial, signErr := adapter.SignEC(digest, share)
results <- result{partial, signErr}
}(shares[i])
}
// Collect results
var partials []adapters.PartialSig
for i := 0; i < 6; i++ {
res := <-results
require.NoError(t, res.err)
partials = append(partials, res.partial)
}
// Aggregate
fullSig, err := adapter.AggregateEC(partials)
require.NoError(t, err)
assert.NotNil(t, fullSig)
}