mirror of
https://github.com/luxfi/mpc.git
synced 2026-09-24 00:26:24 +00:00
Solana addresses were correct and receive-only: ConsensusKeyInfoStore.Save was the only RegisterKey caller and hardcoded "secp256k1", and TriggerSign read the curve from that field, so no product entry point could reach Ed25519 signing. The hardcode is not fixed, it is UNREPRESENTABLE. transport.KeyInfo.KeyType is deleted — the record carries one public key per curve and a PublicKeyFor(curve) — and RegisterKey lost its keyType parameter, so there is no longer a place to state a curve wrongly. A scalar KeyType was the wrong shape anyway: one keygen ceremony produces BOTH a secp256k1 and an Ed25519 key, so "what curve is this wallet" has no single answer. The request carries the network instead, and ONE table maps it to a curve, with NO default arm — KeyTypeForNetwork returns ErrUnknownNetwork rather than guessing. That is the point: the old default returned secp256k1 for anything unrecognized, which is how a Solana request would have been signed with the wrong key. /sign's 32-byte clamp becomes the SCHEME's rule rather than the endpoint's: secp256k1 signs a digest and requires exactly 32 bytes, Ed25519 signs the message at any length. /sign no longer accepts key_type at all — two ways to say the curve is the misuse being removed — and Network is bound into the idempotency fingerprint, so the same bytes for SOL and ETH conflict instead of sharing one cached signature from a different key. ⚠ The trap this nearly shipped with: every pre-existing KeyInfo record has EMPTY public keys, because the old Save wrote "", "". A naive "does this wallet have this curve" gate would have refused every existing wallet on every curve. The check is gated on RecordsCurves() — a record naming no curve denies none, and the missing share stays the thing that fails closed. Verified end to end, not by mirroring: a Solana request through ConsensusMPCBackend.TriggerSign with a real 2-of-3 FROST ring produces a signature crypto/ed25519.Verify accepts, over a 63-byte message so any reintroduced clamp or pre-hash fails it; ETH/EVM/BTC/LUX still route to secp256k1 and must RECOVER to the wallet's key. Four mutations tried, all caught. Behavioral risk worth naming: an unrecognized chain string now fails its transaction closed rather than signing with a default. That is correct, and the real fix is validating chain at POST /v1/transactions.
410 lines
14 KiB
Go
410 lines
14 KiB
Go
package e2e
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/luxfi/mpc/pkg/event"
|
|
"github.com/luxfi/mpc/pkg/logger"
|
|
"github.com/luxfi/mpc/pkg/types"
|
|
)
|
|
|
|
// TestXRPLSigning tests MPC signing specifically for XRP Ledger transactions.
|
|
// XRPL uses secp256k1 (ECDSA) signatures like Bitcoin and Ethereum.
|
|
func TestXRPLSigning(t *testing.T) {
|
|
suite := NewE2ETestSuite(".")
|
|
logger.Init("dev", true)
|
|
|
|
// Comprehensive cleanup before starting tests
|
|
t.Log("Performing pre-test cleanup...")
|
|
suite.CleanupTestEnvironment(t)
|
|
|
|
// Ensure cleanup happens even if test fails
|
|
defer func() {
|
|
t.Log("Performing post-test cleanup...")
|
|
suite.Cleanup(t)
|
|
}()
|
|
|
|
// Setup infrastructure
|
|
t.Run("Setup", func(t *testing.T) {
|
|
t.Log("Running make clean to ensure clean build...")
|
|
err := suite.RunMakeClean()
|
|
require.NoError(t, err, "Failed to run make clean")
|
|
t.Log("make clean completed")
|
|
|
|
t.Log("Starting setupInfrastructure...")
|
|
suite.SetupInfrastructure(t)
|
|
t.Log("setupInfrastructure completed")
|
|
|
|
t.Log("Starting setupTestNodes...")
|
|
suite.SetupTestNodes(t)
|
|
t.Log("setupTestNodes completed")
|
|
|
|
// Load config after setup script runs
|
|
err = suite.LoadConfig()
|
|
require.NoError(t, err, "Failed to load config after setup")
|
|
|
|
t.Log("Starting registerPeers...")
|
|
suite.RegisterPeers(t)
|
|
t.Log("registerPeers completed")
|
|
|
|
t.Log("Starting setupMPCClient...")
|
|
suite.SetupMPCClient(t)
|
|
t.Log("setupMPCClient completed")
|
|
|
|
t.Log("Starting startNodes...")
|
|
suite.StartNodes(t)
|
|
t.Log("startNodes completed")
|
|
})
|
|
|
|
// Test key generation for XRPL wallet
|
|
t.Run("XRPLKeyGeneration", func(t *testing.T) {
|
|
testXRPLKeyGeneration(t, suite)
|
|
})
|
|
|
|
// Test XRPL signing with different transaction types
|
|
t.Run("XRPLSigningPayment", func(t *testing.T) {
|
|
testXRPLPaymentSigning(t, suite)
|
|
})
|
|
|
|
t.Run("XRPLSigningTrustSet", func(t *testing.T) {
|
|
testXRPLTrustSetSigning(t, suite)
|
|
})
|
|
|
|
t.Run("XRPLSigningOfferCreate", func(t *testing.T) {
|
|
testXRPLOfferCreateSigning(t, suite)
|
|
})
|
|
}
|
|
|
|
func testXRPLKeyGeneration(t *testing.T, suite *E2ETestSuite) {
|
|
t.Log("Testing key generation for XRPL wallet...")
|
|
|
|
if suite.mpcClient == nil {
|
|
t.Fatal("MPC client is not initialized. Make sure Setup subtest runs first.")
|
|
}
|
|
|
|
suite.WaitForNodesReady(t)
|
|
|
|
// Generate wallet ID for XRPL testing
|
|
walletID := uuid.New().String()
|
|
suite.walletIDs = append(suite.walletIDs, walletID)
|
|
|
|
t.Logf("Generated XRPL wallet ID: %s", walletID)
|
|
|
|
// Setup result listener
|
|
err := suite.mpcClient.OnWalletCreationResult(func(result event.KeygenResultEvent) {
|
|
t.Logf("Received keygen result for wallet %s: %s", result.WalletID, result.ResultType)
|
|
suite.keygenResults[result.WalletID] = &result
|
|
|
|
if result.ResultType == event.ResultTypeError {
|
|
t.Logf("Keygen failed for wallet %s: %s (%s)", result.WalletID, result.ErrorReason, result.ErrorCode)
|
|
} else {
|
|
t.Logf("Keygen succeeded for wallet %s", result.WalletID)
|
|
}
|
|
})
|
|
require.NoError(t, err, "Failed to setup keygen result listener")
|
|
|
|
time.Sleep(2 * time.Second)
|
|
|
|
// Trigger key generation
|
|
t.Logf("Triggering key generation for XRPL wallet %s", walletID)
|
|
err = suite.mpcClient.CreateWallet(walletID)
|
|
require.NoError(t, err, "Failed to trigger key generation for wallet %s", walletID)
|
|
|
|
// Wait for key generation to complete
|
|
t.Log("Waiting for key generation to complete...")
|
|
timeout := time.NewTimer(keygenTimeout)
|
|
defer timeout.Stop()
|
|
|
|
ticker := time.NewTicker(5 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-timeout.C:
|
|
t.Logf("Timeout waiting for keygen results. Received %d results", len(suite.keygenResults))
|
|
goto checkResults
|
|
case <-ticker.C:
|
|
t.Logf("Still waiting... Received %d keygen results", len(suite.keygenResults))
|
|
if result, exists := suite.keygenResults[walletID]; exists {
|
|
if result.ResultType != event.ResultTypeError {
|
|
goto checkResults
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
checkResults:
|
|
result, exists := suite.keygenResults[walletID]
|
|
if !exists {
|
|
t.Fatalf("No keygen result received for XRPL wallet %s", walletID)
|
|
}
|
|
|
|
if result.ResultType == event.ResultTypeError {
|
|
t.Fatalf("Keygen failed for XRPL wallet %s: %s (%s)", walletID, result.ErrorReason, result.ErrorCode)
|
|
}
|
|
|
|
t.Logf("XRPL keygen succeeded for wallet %s", result.WalletID)
|
|
assert.NotEmpty(t, result.ECDSAPubKey, "ECDSA public key should not be empty (required for XRPL secp256k1)")
|
|
t.Log("XRPL key generation completed successfully")
|
|
}
|
|
|
|
func testXRPLPaymentSigning(t *testing.T, suite *E2ETestSuite) {
|
|
t.Log("Testing XRPL Payment transaction signing...")
|
|
|
|
if len(suite.walletIDs) == 0 {
|
|
t.Fatal("No wallets available for signing. Make sure key generation ran first.")
|
|
}
|
|
|
|
walletID := suite.walletIDs[0]
|
|
|
|
// Setup signing result listener
|
|
signingResults := make(map[string]*event.SigningResultEvent)
|
|
err := suite.mpcClient.OnSignResult(func(result event.SigningResultEvent) {
|
|
t.Logf("Received signing result for tx %s: %s", result.TxID, result.ResultType)
|
|
signingResults[result.TxID] = &result
|
|
})
|
|
require.NoError(t, err, "Failed to setup signing result listener")
|
|
|
|
time.Sleep(1 * time.Second)
|
|
|
|
// Sample XRPL Payment transaction (serialized for signing)
|
|
// In real usage, this would be a properly serialized XRPL transaction blob
|
|
xrplPaymentTx := []byte(`{"TransactionType":"Payment","Account":"rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh","Destination":"rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe","Amount":"1000000"}`)
|
|
|
|
txID := uuid.New().String()
|
|
signTxMsg := &types.SignTxMessage{
|
|
WalletID: walletID,
|
|
TxID: txID,
|
|
Tx: xrplPaymentTx,
|
|
KeyType: types.KeyTypeSecp256k1, // XRPL uses secp256k1
|
|
NetworkInternalCode: string(types.NetworkXRPL),
|
|
}
|
|
|
|
t.Logf("Signing XRPL Payment transaction with wallet %s", walletID)
|
|
err = suite.mpcClient.SignTransaction(signTxMsg)
|
|
require.NoError(t, err, "Failed to trigger XRPL signing")
|
|
|
|
// Wait for signing result
|
|
result := waitForXRPLSigningResult(t, txID, signingResults)
|
|
validateXRPLSigningResult(t, walletID, result)
|
|
|
|
t.Log("XRPL Payment signing completed successfully")
|
|
}
|
|
|
|
func testXRPLTrustSetSigning(t *testing.T, suite *E2ETestSuite) {
|
|
t.Log("Testing XRPL TrustSet transaction signing...")
|
|
|
|
if len(suite.walletIDs) == 0 {
|
|
t.Fatal("No wallets available for signing. Make sure key generation ran first.")
|
|
}
|
|
|
|
walletID := suite.walletIDs[0]
|
|
|
|
// Setup signing result listener
|
|
signingResults := make(map[string]*event.SigningResultEvent)
|
|
err := suite.mpcClient.OnSignResult(func(result event.SigningResultEvent) {
|
|
t.Logf("Received signing result for tx %s: %s", result.TxID, result.ResultType)
|
|
signingResults[result.TxID] = &result
|
|
})
|
|
require.NoError(t, err, "Failed to setup signing result listener")
|
|
|
|
time.Sleep(1 * time.Second)
|
|
|
|
// Sample XRPL TrustSet transaction
|
|
xrplTrustSetTx := []byte(`{"TransactionType":"TrustSet","Account":"rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh","LimitAmount":{"currency":"USD","issuer":"rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe","value":"1000"}}`)
|
|
|
|
txID := uuid.New().String()
|
|
signTxMsg := &types.SignTxMessage{
|
|
WalletID: walletID,
|
|
TxID: txID,
|
|
Tx: xrplTrustSetTx,
|
|
KeyType: types.KeyTypeSecp256k1,
|
|
NetworkInternalCode: string(types.NetworkXRPL),
|
|
}
|
|
|
|
t.Logf("Signing XRPL TrustSet transaction with wallet %s", walletID)
|
|
err = suite.mpcClient.SignTransaction(signTxMsg)
|
|
require.NoError(t, err, "Failed to trigger XRPL signing")
|
|
|
|
// Wait for signing result
|
|
result := waitForXRPLSigningResult(t, txID, signingResults)
|
|
validateXRPLSigningResult(t, walletID, result)
|
|
|
|
t.Log("XRPL TrustSet signing completed successfully")
|
|
}
|
|
|
|
func testXRPLOfferCreateSigning(t *testing.T, suite *E2ETestSuite) {
|
|
t.Log("Testing XRPL OfferCreate transaction signing...")
|
|
|
|
if len(suite.walletIDs) == 0 {
|
|
t.Fatal("No wallets available for signing. Make sure key generation ran first.")
|
|
}
|
|
|
|
walletID := suite.walletIDs[0]
|
|
|
|
// Setup signing result listener
|
|
signingResults := make(map[string]*event.SigningResultEvent)
|
|
err := suite.mpcClient.OnSignResult(func(result event.SigningResultEvent) {
|
|
t.Logf("Received signing result for tx %s: %s", result.TxID, result.ResultType)
|
|
signingResults[result.TxID] = &result
|
|
})
|
|
require.NoError(t, err, "Failed to setup signing result listener")
|
|
|
|
time.Sleep(1 * time.Second)
|
|
|
|
// Sample XRPL OfferCreate transaction (DEX order)
|
|
xrplOfferCreateTx := []byte(`{"TransactionType":"OfferCreate","Account":"rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh","TakerGets":"1000000","TakerPays":{"currency":"USD","issuer":"rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe","value":"1"}}`)
|
|
|
|
txID := uuid.New().String()
|
|
signTxMsg := &types.SignTxMessage{
|
|
WalletID: walletID,
|
|
TxID: txID,
|
|
Tx: xrplOfferCreateTx,
|
|
KeyType: types.KeyTypeSecp256k1,
|
|
NetworkInternalCode: string(types.NetworkXRPL),
|
|
}
|
|
|
|
t.Logf("Signing XRPL OfferCreate transaction with wallet %s", walletID)
|
|
err = suite.mpcClient.SignTransaction(signTxMsg)
|
|
require.NoError(t, err, "Failed to trigger XRPL signing")
|
|
|
|
// Wait for signing result
|
|
result := waitForXRPLSigningResult(t, txID, signingResults)
|
|
validateXRPLSigningResult(t, walletID, result)
|
|
|
|
t.Log("XRPL OfferCreate signing completed successfully")
|
|
}
|
|
|
|
// waitForXRPLSigningResult waits for a signing result with the given txID
|
|
func waitForXRPLSigningResult(t *testing.T, txID string, signingResults map[string]*event.SigningResultEvent) *event.SigningResultEvent {
|
|
timeout := time.NewTimer(signingTimeout)
|
|
defer timeout.Stop()
|
|
|
|
ticker := time.NewTicker(2 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-timeout.C:
|
|
t.Fatalf("Timeout waiting for XRPL signing result for tx %s", txID)
|
|
case <-ticker.C:
|
|
if result, exists := signingResults[txID]; exists {
|
|
return result
|
|
}
|
|
t.Logf("Still waiting for signing result for tx %s...", txID)
|
|
}
|
|
}
|
|
}
|
|
|
|
// validateXRPLSigningResult validates an XRPL signing result
|
|
func validateXRPLSigningResult(t *testing.T, walletID string, result *event.SigningResultEvent) {
|
|
logger.Info("Received XRPL signing result", "walletID", walletID, "result", result)
|
|
|
|
if result.ResultType == event.ResultTypeError {
|
|
t.Fatalf("XRPL signing failed for wallet %s: %s (%s)", walletID, result.ErrorReason, result.ErrorCode)
|
|
}
|
|
|
|
t.Logf("XRPL signing succeeded for wallet %s (tx: %s)", walletID, result.TxID)
|
|
|
|
// XRPL uses ECDSA/secp256k1, so we expect R, S, and SignatureRecovery
|
|
assert.NotEmpty(t, result.R, "XRPL ECDSA R value should not be empty")
|
|
assert.NotEmpty(t, result.S, "XRPL ECDSA S value should not be empty")
|
|
assert.NotEmpty(t, result.SignatureRecovery, "XRPL signature recovery should not be empty")
|
|
|
|
// Verify signature can be composed
|
|
composedSig, err := ComposeSignature(result.SignatureRecovery, result.R, result.S)
|
|
if err != nil {
|
|
t.Errorf("Failed to compose XRPL ECDSA signature: %v", err)
|
|
} else {
|
|
t.Logf("Successfully composed XRPL signature: %d bytes", len(composedSig))
|
|
assert.Equal(t, 65, len(composedSig), "Composed XRPL signature should be 65 bytes")
|
|
|
|
// Log signature components for debugging
|
|
t.Logf("XRPL signature components - R: %d bytes, S: %d bytes, V: %d bytes",
|
|
len(result.R), len(result.S), len(result.SignatureRecovery))
|
|
}
|
|
}
|
|
|
|
// TestXRPLNetworkCodeValidation tests that XRPL network codes are correctly recognized
|
|
func TestXRPLNetworkCodeValidation(t *testing.T) {
|
|
t.Run("XRPLMainnet", func(t *testing.T) {
|
|
assert.True(t, types.IsNetworkSupported(string(types.NetworkXRPL)), "XRPL mainnet should be supported")
|
|
kt, err := types.KeyTypeForNetwork(types.NetworkXRPL)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, types.KeyTypeSecp256k1, kt, "XRPL should use secp256k1")
|
|
})
|
|
|
|
t.Run("XRPLTestnet", func(t *testing.T) {
|
|
assert.True(t, types.IsNetworkSupported(string(types.NetworkXRPLTestnet)), "XRPL testnet should be supported")
|
|
kt, err := types.KeyTypeForNetwork(types.NetworkXRPLTestnet)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, types.KeyTypeSecp256k1, kt, "XRPL testnet should use secp256k1")
|
|
})
|
|
|
|
t.Run("XRPLDevnet", func(t *testing.T) {
|
|
assert.True(t, types.IsNetworkSupported(string(types.NetworkXRPLDevnet)), "XRPL devnet should be supported")
|
|
kt, err := types.KeyTypeForNetwork(types.NetworkXRPLDevnet)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, types.KeyTypeSecp256k1, kt, "XRPL devnet should use secp256k1")
|
|
})
|
|
}
|
|
|
|
// TestLUXNetworkCodeValidation tests that LUX network codes are correctly recognized
|
|
func TestLUXNetworkCodeValidation(t *testing.T) {
|
|
t.Run("LUXMainnet", func(t *testing.T) {
|
|
assert.True(t, types.IsNetworkSupported(string(types.NetworkLUX)), "LUX mainnet should be supported")
|
|
kt, err := types.KeyTypeForNetwork(types.NetworkLUX)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, types.KeyTypeSecp256k1, kt, "LUX should use secp256k1")
|
|
})
|
|
|
|
t.Run("LUXTestnet", func(t *testing.T) {
|
|
assert.True(t, types.IsNetworkSupported(string(types.NetworkLUXTestnet)), "LUX testnet should be supported")
|
|
kt, err := types.KeyTypeForNetwork(types.NetworkLUXTestnet)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, types.KeyTypeSecp256k1, kt, "LUX testnet should use secp256k1")
|
|
})
|
|
}
|
|
|
|
// TestAllSupportedNetworks validates all network codes are properly configured
|
|
func TestAllSupportedNetworks(t *testing.T) {
|
|
networks := []struct {
|
|
code types.NetworkCode
|
|
name string
|
|
keyType types.KeyType
|
|
}{
|
|
{types.NetworkBTC, "BTC", types.KeyTypeSecp256k1},
|
|
{types.NetworkBTCTestnet, "BTC-testnet", types.KeyTypeSecp256k1},
|
|
{types.NetworkETH, "ETH", types.KeyTypeSecp256k1},
|
|
{types.NetworkETHSepolia, "ETH-sepolia", types.KeyTypeSecp256k1},
|
|
{types.NetworkETHGoerli, "ETH-goerli", types.KeyTypeSecp256k1},
|
|
{types.NetworkSOL, "SOL", types.KeyTypeEd25519},
|
|
{types.NetworkSOLDevnet, "SOL-devnet", types.KeyTypeEd25519},
|
|
{types.NetworkSOLTestnet, "SOL-testnet", types.KeyTypeEd25519},
|
|
{types.NetworkXRPL, "XRPL", types.KeyTypeSecp256k1},
|
|
{types.NetworkXRPLTestnet, "XRPL-testnet", types.KeyTypeSecp256k1},
|
|
{types.NetworkXRPLDevnet, "XRPL-devnet", types.KeyTypeSecp256k1},
|
|
{types.NetworkLUX, "LUX", types.KeyTypeSecp256k1},
|
|
{types.NetworkLUXTestnet, "LUX-testnet", types.KeyTypeSecp256k1},
|
|
{types.NetworkTON, "TON", types.KeyTypeEd25519},
|
|
{types.NetworkTONTestnet, "TON-testnet", types.KeyTypeEd25519},
|
|
}
|
|
|
|
for _, tc := range networks {
|
|
t.Run(fmt.Sprintf("Network_%s", tc.name), func(t *testing.T) {
|
|
assert.True(t, types.IsNetworkSupported(string(tc.code)),
|
|
"%s should be supported", tc.name)
|
|
kt, err := types.KeyTypeForNetwork(tc.code)
|
|
assert.NoError(t, err, "%s should have a recorded curve", tc.name)
|
|
assert.Equal(t, tc.keyType, kt,
|
|
"%s should use %s", tc.name, tc.keyType)
|
|
})
|
|
}
|
|
}
|