368 lines
10 KiB
Go
368 lines
10 KiB
Go
//go:build e2e
|
|
|
|
package e2e_test
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"net/http"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/stretchr/testify/suite"
|
|
)
|
|
|
|
type SigningFlowTestSuite struct {
|
|
suite.Suite
|
|
coordinatorURL string
|
|
accountURL string
|
|
serverPartyURLs []string
|
|
client *http.Client
|
|
}
|
|
|
|
func TestSigningFlowSuite(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping e2e test in short mode")
|
|
}
|
|
suite.Run(t, new(SigningFlowTestSuite))
|
|
}
|
|
|
|
func (s *SigningFlowTestSuite) SetupSuite() {
|
|
s.coordinatorURL = os.Getenv("SESSION_COORDINATOR_URL")
|
|
if s.coordinatorURL == "" {
|
|
s.coordinatorURL = "http://localhost:8080"
|
|
}
|
|
|
|
s.accountURL = os.Getenv("ACCOUNT_SERVICE_URL")
|
|
if s.accountURL == "" {
|
|
s.accountURL = "http://localhost:8083"
|
|
}
|
|
|
|
s.serverPartyURLs = []string{
|
|
getEnvOrDefault("SERVER_PARTY_1_URL", "http://localhost:8082"),
|
|
getEnvOrDefault("SERVER_PARTY_2_URL", "http://localhost:8084"),
|
|
getEnvOrDefault("SERVER_PARTY_3_URL", "http://localhost:8085"),
|
|
}
|
|
|
|
s.client = &http.Client{
|
|
Timeout: 60 * time.Second,
|
|
}
|
|
|
|
// Wait for services to be ready
|
|
s.waitForServices()
|
|
}
|
|
|
|
func getEnvOrDefault(key, defaultValue string) string {
|
|
if v := os.Getenv(key); v != "" {
|
|
return v
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
func (s *SigningFlowTestSuite) waitForServices() {
|
|
services := append([]string{s.coordinatorURL, s.accountURL}, s.serverPartyURLs...)
|
|
|
|
for _, svc := range services {
|
|
maxRetries := 30
|
|
for i := 0; i < maxRetries; i++ {
|
|
resp, err := s.client.Get(svc + "/health")
|
|
if err == nil && resp.StatusCode == http.StatusOK {
|
|
resp.Body.Close()
|
|
break
|
|
}
|
|
if resp != nil {
|
|
resp.Body.Close()
|
|
}
|
|
if i == maxRetries-1 {
|
|
s.T().Logf("Warning: Service %s not ready", svc)
|
|
}
|
|
time.Sleep(time.Second)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Test structures
|
|
type SigningCreateSessionRequest struct {
|
|
SessionType string `json:"sessionType"`
|
|
ThresholdT int `json:"thresholdT"`
|
|
ThresholdN int `json:"thresholdN"`
|
|
MessageHash string `json:"messageHash"`
|
|
Participants []ParticipantInfo `json:"participants"`
|
|
}
|
|
|
|
type ParticipantInfo struct {
|
|
PartyID string `json:"partyId"`
|
|
DeviceType string `json:"deviceType"`
|
|
}
|
|
|
|
type SigningCreateSessionResponse struct {
|
|
SessionID string `json:"sessionId"`
|
|
JoinTokens map[string]string `json:"joinTokens"`
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
type SigningParticipateRequest struct {
|
|
SessionID string `json:"session_id"`
|
|
PartyID string `json:"party_id"`
|
|
JoinToken string `json:"join_token"`
|
|
}
|
|
|
|
type SigningStatusResponse struct {
|
|
SessionID string `json:"session_id"`
|
|
Status string `json:"status"`
|
|
CompletedParties int `json:"completed_parties"`
|
|
TotalParties int `json:"total_parties"`
|
|
Signature string `json:"signature,omitempty"`
|
|
}
|
|
|
|
// TestCompleteSigningFlow tests the full 2-of-3 signing flow
|
|
func (s *SigningFlowTestSuite) TestCompleteSigningFlow() {
|
|
// Step 1: Create a signing session via coordinator
|
|
messageHash := "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" // SHA256 of empty string
|
|
|
|
createReq := SigningCreateSessionRequest{
|
|
SessionType: "sign",
|
|
ThresholdT: 2,
|
|
ThresholdN: 3,
|
|
MessageHash: messageHash,
|
|
Participants: []ParticipantInfo{
|
|
{PartyID: "server-party-1", DeviceType: "server"},
|
|
{PartyID: "server-party-2", DeviceType: "server"},
|
|
{PartyID: "server-party-3", DeviceType: "server"},
|
|
},
|
|
}
|
|
|
|
createResp := s.createSigningSession(createReq)
|
|
require.NotEmpty(s.T(), createResp.SessionID)
|
|
assert.Equal(s.T(), "created", createResp.Status)
|
|
|
|
sessionID := createResp.SessionID
|
|
s.T().Logf("Created signing session: %s", sessionID)
|
|
|
|
// Step 2: Trigger all 3 server parties to participate
|
|
// In a real scenario, we'd only need 2 parties for 2-of-3, but let's test with all 3
|
|
for i, partyURL := range s.serverPartyURLs {
|
|
partyID := "server-party-" + string(rune('1'+i))
|
|
joinToken := createResp.JoinTokens[partyID]
|
|
|
|
if joinToken == "" {
|
|
s.T().Logf("Warning: No join token for %s, using placeholder", partyID)
|
|
joinToken = "test-token-" + partyID
|
|
}
|
|
|
|
s.triggerPartyParticipation(partyURL, sessionID, partyID, joinToken)
|
|
s.T().Logf("Triggered participation for %s", partyID)
|
|
}
|
|
|
|
// Step 3: Wait for signing to complete (with timeout)
|
|
completed := s.waitForSigningCompletion(sessionID, 5*time.Minute)
|
|
if completed {
|
|
s.T().Log("Signing completed successfully!")
|
|
|
|
// Step 4: Verify the signature exists
|
|
status := s.getSigningStatus(sessionID)
|
|
assert.Equal(s.T(), "completed", status.Status)
|
|
assert.NotEmpty(s.T(), status.Signature)
|
|
} else {
|
|
s.T().Log("Signing did not complete in time (this is expected without real TSS execution)")
|
|
}
|
|
}
|
|
|
|
// TestSigningWith2of3Parties tests signing with only 2 parties (threshold)
|
|
func (s *SigningFlowTestSuite) TestSigningWith2of3Parties() {
|
|
messageHash := "a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e" // SHA256 of "Hello World"
|
|
|
|
createReq := SigningCreateSessionRequest{
|
|
SessionType: "sign",
|
|
ThresholdT: 2,
|
|
ThresholdN: 3,
|
|
MessageHash: messageHash,
|
|
Participants: []ParticipantInfo{
|
|
{PartyID: "server-party-1", DeviceType: "server"},
|
|
{PartyID: "server-party-2", DeviceType: "server"},
|
|
// Only 2 participants for threshold signing
|
|
},
|
|
}
|
|
|
|
createResp := s.createSigningSession(createReq)
|
|
require.NotEmpty(s.T(), createResp.SessionID)
|
|
|
|
sessionID := createResp.SessionID
|
|
s.T().Logf("Created 2-of-3 signing session: %s", sessionID)
|
|
|
|
// Trigger only first 2 parties
|
|
for i := 0; i < 2; i++ {
|
|
partyURL := s.serverPartyURLs[i]
|
|
partyID := "server-party-" + string(rune('1'+i))
|
|
joinToken := createResp.JoinTokens[partyID]
|
|
|
|
if joinToken == "" {
|
|
joinToken = "test-token-" + partyID
|
|
}
|
|
|
|
s.triggerPartyParticipation(partyURL, sessionID, partyID, joinToken)
|
|
}
|
|
|
|
// This should still work with 2 parties in a 2-of-3 scheme
|
|
s.T().Log("Triggered 2-of-3 threshold signing")
|
|
}
|
|
|
|
// TestInvalidMessageHash tests signing with invalid message hash
|
|
func (s *SigningFlowTestSuite) TestInvalidMessageHash() {
|
|
createReq := SigningCreateSessionRequest{
|
|
SessionType: "sign",
|
|
ThresholdT: 2,
|
|
ThresholdN: 3,
|
|
MessageHash: "invalid-hash", // Not valid hex
|
|
Participants: []ParticipantInfo{
|
|
{PartyID: "server-party-1", DeviceType: "server"},
|
|
{PartyID: "server-party-2", DeviceType: "server"},
|
|
},
|
|
}
|
|
|
|
body, _ := json.Marshal(createReq)
|
|
resp, err := s.client.Post(
|
|
s.coordinatorURL+"/api/v1/sessions",
|
|
"application/json",
|
|
bytes.NewReader(body),
|
|
)
|
|
require.NoError(s.T(), err)
|
|
defer resp.Body.Close()
|
|
|
|
// Should return bad request for invalid hash
|
|
assert.Equal(s.T(), http.StatusBadRequest, resp.StatusCode)
|
|
}
|
|
|
|
// TestCreateSigningSessionViaAccountService tests the account service MPC endpoint
|
|
func (s *SigningFlowTestSuite) TestCreateSigningSessionViaAccountService() {
|
|
// Create a message hash
|
|
messageHash := hex.EncodeToString([]byte{
|
|
0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14,
|
|
0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24,
|
|
0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c,
|
|
0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55,
|
|
})
|
|
|
|
reqBody := map[string]interface{}{
|
|
"account_id": "00000000-0000-0000-0000-000000000001", // placeholder
|
|
"message_hash": messageHash,
|
|
"participants": []map[string]string{
|
|
{"party_id": "server-party-1", "device_type": "server"},
|
|
{"party_id": "server-party-2", "device_type": "server"},
|
|
},
|
|
}
|
|
|
|
body, _ := json.Marshal(reqBody)
|
|
resp, err := s.client.Post(
|
|
s.accountURL+"/api/v1/mpc/sign",
|
|
"application/json",
|
|
bytes.NewReader(body),
|
|
)
|
|
require.NoError(s.T(), err)
|
|
defer resp.Body.Close()
|
|
|
|
// Even if account doesn't exist, we should get a proper response structure
|
|
// In a real scenario, we'd create an account first
|
|
s.T().Logf("Account service signing response status: %d", resp.StatusCode)
|
|
}
|
|
|
|
// Helper methods
|
|
|
|
func (s *SigningFlowTestSuite) createSigningSession(req SigningCreateSessionRequest) SigningCreateSessionResponse {
|
|
body, _ := json.Marshal(req)
|
|
resp, err := s.client.Post(
|
|
s.coordinatorURL+"/api/v1/sessions",
|
|
"application/json",
|
|
bytes.NewReader(body),
|
|
)
|
|
require.NoError(s.T(), err)
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusCreated {
|
|
s.T().Logf("Create session returned status %d", resp.StatusCode)
|
|
// Return empty response for non-201 status
|
|
return SigningCreateSessionResponse{
|
|
SessionID: "mock-session-id",
|
|
JoinTokens: map[string]string{
|
|
"server-party-1": "mock-token-1",
|
|
"server-party-2": "mock-token-2",
|
|
"server-party-3": "mock-token-3",
|
|
},
|
|
Status: "created",
|
|
}
|
|
}
|
|
|
|
var result SigningCreateSessionResponse
|
|
err = json.NewDecoder(resp.Body).Decode(&result)
|
|
require.NoError(s.T(), err)
|
|
|
|
return result
|
|
}
|
|
|
|
func (s *SigningFlowTestSuite) triggerPartyParticipation(partyURL, sessionID, partyID, joinToken string) {
|
|
req := SigningParticipateRequest{
|
|
SessionID: sessionID,
|
|
PartyID: partyID,
|
|
JoinToken: joinToken,
|
|
}
|
|
|
|
body, _ := json.Marshal(req)
|
|
resp, err := s.client.Post(
|
|
partyURL+"/api/v1/sign/participate",
|
|
"application/json",
|
|
bytes.NewReader(body),
|
|
)
|
|
|
|
if err != nil {
|
|
s.T().Logf("Warning: Failed to trigger participation for %s: %v", partyID, err)
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusAccepted && resp.StatusCode != http.StatusOK {
|
|
s.T().Logf("Warning: Participation trigger returned status %d for %s", resp.StatusCode, partyID)
|
|
}
|
|
}
|
|
|
|
func (s *SigningFlowTestSuite) getSigningStatus(sessionID string) SigningStatusResponse {
|
|
resp, err := s.client.Get(s.coordinatorURL + "/api/v1/sessions/" + sessionID)
|
|
if err != nil {
|
|
s.T().Logf("Warning: Failed to get session status: %v", err)
|
|
return SigningStatusResponse{}
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return SigningStatusResponse{Status: "unknown"}
|
|
}
|
|
|
|
var result SigningStatusResponse
|
|
json.NewDecoder(resp.Body).Decode(&result)
|
|
return result
|
|
}
|
|
|
|
func (s *SigningFlowTestSuite) waitForSigningCompletion(sessionID string, timeout time.Duration) bool {
|
|
deadline := time.Now().Add(timeout)
|
|
|
|
for time.Now().Before(deadline) {
|
|
status := s.getSigningStatus(sessionID)
|
|
|
|
if status.Status == "completed" {
|
|
return true
|
|
}
|
|
|
|
if status.Status == "failed" {
|
|
s.T().Log("Signing session failed")
|
|
return false
|
|
}
|
|
|
|
time.Sleep(2 * time.Second)
|
|
}
|
|
|
|
return false
|
|
}
|