mirror of
https://github.com/luxfi/mpc.git
synced 2026-09-24 00:26:24 +00:00
245 lines
6.8 KiB
Go
245 lines
6.8 KiB
Go
package client
|
|
|
|
import (
|
|
"crypto/ed25519"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"filippo.io/age"
|
|
"github.com/fystack/mpcium/pkg/event"
|
|
"github.com/fystack/mpcium/pkg/eventconsumer"
|
|
"github.com/fystack/mpcium/pkg/logger"
|
|
"github.com/fystack/mpcium/pkg/messaging"
|
|
"github.com/fystack/mpcium/pkg/mpc"
|
|
"github.com/fystack/mpcium/pkg/types"
|
|
"github.com/nats-io/nats.go"
|
|
)
|
|
|
|
const (
|
|
GenerateWalletSuccessTopic = "mpc.mpc_keygen_success.*" // wildcard to listen to all success events
|
|
)
|
|
|
|
type MPCClient interface {
|
|
CreateWallet(walletID string) error
|
|
OnWalletCreationResult(callback func(event mpc.KeygenSuccessEvent)) error
|
|
|
|
SignTransaction(msg *types.SignTxMessage) error
|
|
OnSignResult(callback func(event event.SigningResultEvent)) error
|
|
}
|
|
|
|
type mpcClient struct {
|
|
signingStream messaging.StreamPubsub
|
|
pubsub messaging.PubSub
|
|
genKeySuccessQueue messaging.MessageQueue
|
|
signResultQueue 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
|
|
signingStream, err := messaging.NewJetStreamPubSub(opts.NatsConn, "mpc-signing", []string{
|
|
"mpc.signing_request.*",
|
|
})
|
|
if err != nil {
|
|
logger.Fatal("Failed to create JetStream PubSub", err)
|
|
}
|
|
|
|
pubsub := messaging.NewNATSPubSub(opts.NatsConn)
|
|
|
|
manager := messaging.NewNATsMessageQueueManager("mpc", []string{
|
|
"mpc.mpc_keygen_success.*",
|
|
"mpc.signing_result.*",
|
|
}, opts.NatsConn)
|
|
|
|
genKeySuccessQueue := manager.NewMessageQueue("mpc_keygen_success")
|
|
signResultQueue := manager.NewMessageQueue("signing_result")
|
|
|
|
return &mpcClient{
|
|
signingStream: signingStream,
|
|
pubsub: pubsub,
|
|
genKeySuccessQueue: genKeySuccessQueue,
|
|
signResultQueue: signResultQueue,
|
|
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.pubsub.Publish(eventconsumer.MPCGenerateEvent, 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 mpc.KeygenSuccessEvent)) error {
|
|
err := c.genKeySuccessQueue.Dequeue(GenerateWalletSuccessTopic, func(msg []byte) error {
|
|
var event mpc.KeygenSuccessEvent
|
|
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.signingStream.Publish(event.SigningRequestEventTopic, 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
|
|
}
|