Files
mpc/pkg/client/client.go
T
zeekay b45ac639ac Fix remaining import formatting issues
- Run goimports on all pkg files
- Ensure consistent import grouping throughout codebase
2025-08-02 22:30:28 -05:00

304 lines
8.7 KiB
Go

package client
import (
"context"
"crypto/ed25519"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"filippo.io/age"
"github.com/nats-io/nats.go"
"github.com/luxfi/mpc/pkg/event"
"github.com/luxfi/mpc/pkg/eventconsumer"
"github.com/luxfi/mpc/pkg/logger"
"github.com/luxfi/mpc/pkg/messaging"
"github.com/luxfi/mpc/pkg/types"
)
const (
GenerateWalletSuccessTopic = "mpc.mpc_keygen_result.*" // wildcard to listen to all success events
ResharingSuccessTopic = "mpc.mpc_reshare_result.*" // wildcard to listen to all success events
)
type MPCClient interface {
CreateWallet(walletID string) error
OnWalletCreationResult(callback func(event event.KeygenResultEvent)) error
SignTransaction(msg *types.SignTxMessage) error
OnSignResult(callback func(event event.SigningResultEvent)) error
Resharing(msg *types.ResharingMessage) error
OnResharingResult(callback func(event event.ResharingResultEvent)) error
}
type mpcClient struct {
signingBroker messaging.MessageBroker
keygenBroker messaging.MessageBroker
pubsub messaging.PubSub
genKeySuccessQueue messaging.MessageQueue
signResultQueue messaging.MessageQueue
reshareSuccessQueue messaging.MessageQueue
privKey ed25519.PrivateKey
}
// Options defines configuration options for creating a new MPCClient
type Options struct {
// NATS connection
NatsConn *nats.Conn
// Key path options
KeyPath string // Path to unencrypted key (default: "./event_initiator.key")
// Encryption options
Encrypted bool // Whether the key is encrypted
Password string // Password for encrypted key
}
// NewMPCClient creates a new MPC client using the provided options.
// It reads the Ed25519 private key from disk and sets up messaging connections.
// If the key is encrypted (.age file), decryption options must be provided in the config.
func NewMPCClient(opts Options) MPCClient {
// Set default paths if not provided
if opts.KeyPath == "" {
opts.KeyPath = filepath.Join(".", "event_initiator.key")
}
if strings.HasSuffix(opts.KeyPath, ".age") {
opts.Encrypted = true
}
var privHexBytes []byte
var err error
// Check if key file exists
if _, err := os.Stat(opts.KeyPath); err == nil {
if opts.Encrypted {
// Encrypted key exists, try to decrypt it
if opts.Password == "" {
logger.Fatal("Encrypted key found but no decryption option provided", nil)
}
// Read encrypted file
encryptedBytes, err := os.ReadFile(opts.KeyPath)
if err != nil {
logger.Fatal("Failed to read encrypted private key file", err)
}
// Decrypt the key using the provided password
privHexBytes, err = decryptPrivateKey(encryptedBytes, opts.Password)
if err != nil {
logger.Fatal("Failed to decrypt private key", err)
}
} else {
// Unencrypted key exists, read it normally
privHexBytes, err = os.ReadFile(opts.KeyPath)
if err != nil {
logger.Fatal("Failed to read private key file", err)
}
}
} else {
logger.Fatal("No private key file found", nil)
}
privHex := string(privHexBytes)
// Decode private key from hex
privSeed, err := hex.DecodeString(privHex)
if err != nil {
fmt.Println("Failed to decode private key hex:", err)
os.Exit(1)
}
// Reconstruct full Ed25519 private key from seed
priv := ed25519.NewKeyFromSeed(privSeed)
// 2) Create the PubSub for both publish & subscribe
signingBroker, err := messaging.NewJetStreamBroker(context.Background(), opts.NatsConn, "mpc-signing", []string{
"mpc.signing_request.*",
})
if err != nil {
logger.Fatal("Failed to create signing jetstream broker", err)
}
keygenBroker, err := messaging.NewJetStreamBroker(context.Background(), opts.NatsConn, "mpc-keygen", []string{
"mpc.keygen_request.*",
})
if err != nil {
logger.Fatal("Failed to create keygen jetstream broker", err)
}
pubsub := messaging.NewNATSPubSub(opts.NatsConn)
manager := messaging.NewNATsMessageQueueManager("mpc", []string{
"mpc.mpc_keygen_result.*",
"mpc.mpc_signing_result.*",
"mpc.mpc_reshare_result.*",
}, opts.NatsConn)
genKeySuccessQueue := manager.NewMessageQueue("mpc_keygen_result")
signResultQueue := manager.NewMessageQueue("mpc_signing_result")
reshareSuccessQueue := manager.NewMessageQueue("mpc_reshare_result")
return &mpcClient{
signingBroker: signingBroker,
keygenBroker: keygenBroker,
pubsub: pubsub,
genKeySuccessQueue: genKeySuccessQueue,
signResultQueue: signResultQueue,
reshareSuccessQueue: reshareSuccessQueue,
privKey: priv,
}
}
// decryptPrivateKey decrypts the encrypted private key using the provided password
func decryptPrivateKey(encryptedData []byte, password string) ([]byte, error) {
// Create an age identity (decryption key) from the password
identity, err := age.NewScryptIdentity(password)
if err != nil {
return nil, fmt.Errorf("failed to create identity from password: %w", err)
}
// Create a reader from the encrypted data
decrypter, err := age.Decrypt(strings.NewReader(string(encryptedData)), identity)
if err != nil {
return nil, fmt.Errorf("failed to create decrypter: %w", err)
}
// Read the decrypted data
decryptedData, err := io.ReadAll(decrypter)
if err != nil {
return nil, fmt.Errorf("failed to read decrypted data: %w", err)
}
return decryptedData, nil
}
// CreateWallet generates a GenerateKeyMessage, signs it, and publishes it.
func (c *mpcClient) CreateWallet(walletID string) error {
// build the message
msg := &types.GenerateKeyMessage{
WalletID: walletID,
}
// compute the canonical raw bytes
raw, err := msg.Raw()
if err != nil {
return fmt.Errorf("CreateWallet: raw payload error: %w", err)
}
// sign
msg.Signature = ed25519.Sign(c.privKey, raw)
bytes, err := json.Marshal(msg)
if err != nil {
return fmt.Errorf("CreateWallet: marshal error: %w", err)
}
if err := c.keygenBroker.PublishMessage(context.Background(), event.KeygenRequestTopic, bytes); err != nil {
return fmt.Errorf("CreateWallet: publish error: %w", err)
}
return nil
}
// The callback will be invoked whenever a wallet creation result is received.
func (c *mpcClient) OnWalletCreationResult(callback func(event event.KeygenResultEvent)) error {
err := c.genKeySuccessQueue.Dequeue(GenerateWalletSuccessTopic, func(msg []byte) error {
var event event.KeygenResultEvent
err := json.Unmarshal(msg, &event)
if err != nil {
return err
}
callback(event)
return nil
})
if err != nil {
return fmt.Errorf("OnWalletCreationResult: subscribe error: %w", err)
}
return nil
}
// SignTransaction builds a SignTxMessage, signs it, and publishes it.
func (c *mpcClient) SignTransaction(msg *types.SignTxMessage) error {
// compute the canonical raw bytes (omitting Signature field)
raw, err := msg.Raw()
if err != nil {
return fmt.Errorf("SignTransaction: raw payload error: %w", err)
}
// sign
msg.Signature = ed25519.Sign(c.privKey, raw)
bytes, err := json.Marshal(msg)
if err != nil {
return fmt.Errorf("SignTransaction: marshal error: %w", err)
}
if err := c.signingBroker.PublishMessage(context.Background(), event.SigningRequestTopic, bytes); err != nil {
return fmt.Errorf("SignTransaction: publish error: %w", err)
}
return nil
}
func (c *mpcClient) OnSignResult(callback func(event event.SigningResultEvent)) error {
err := c.signResultQueue.Dequeue(event.SigningResultCompleteTopic, func(msg []byte) error {
var event event.SigningResultEvent
err := json.Unmarshal(msg, &event)
if err != nil {
return err
}
callback(event)
return nil
})
if err != nil {
return fmt.Errorf("OnSignResult: subscribe error: %w", err)
}
return nil
}
func (c *mpcClient) Resharing(msg *types.ResharingMessage) error {
// compute the canonical raw bytes
raw, err := msg.Raw()
if err != nil {
return fmt.Errorf("Resharing: raw payload error: %w", err)
}
// sign
msg.Signature = ed25519.Sign(c.privKey, raw)
bytes, err := json.Marshal(msg)
if err != nil {
return fmt.Errorf("Resharing: marshal error: %w", err)
}
if err := c.pubsub.Publish(eventconsumer.MPCReshareEvent, bytes); err != nil {
return fmt.Errorf("Resharing: publish error: %w", err)
}
return nil
}
func (c *mpcClient) OnResharingResult(callback func(event event.ResharingResultEvent)) error {
err := c.reshareSuccessQueue.Dequeue(ResharingSuccessTopic, func(msg []byte) error {
logger.Info("Received reshare success message", "raw", string(msg))
var event event.ResharingResultEvent
err := json.Unmarshal(msg, &event)
if err != nil {
logger.Error("Failed to unmarshal reshare success event", err, "raw", string(msg))
return err
}
logger.Info("Deserialized reshare success event", "event", event)
callback(event)
return nil
})
if err != nil {
return fmt.Errorf("OnResharingResult: subscribe error: %w", err)
}
return nil
}