From 1c0d65bd5ff96e932813f07ee5113c3a5d941395 Mon Sep 17 00:00:00 2001 From: athen Date: Wed, 30 Jul 2025 17:11:23 +0200 Subject: [PATCH 01/23] fix typo --- INSTALLATION.md | 2 +- SECURITY.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/INSTALLATION.md b/INSTALLATION.md index d523a20..c13e662 100644 --- a/INSTALLATION.md +++ b/INSTALLATION.md @@ -287,7 +287,7 @@ mpcium start -n node2 --- -## Apendix +## Appendix ### Decrypt initiator private key with age diff --git a/SECURITY.md b/SECURITY.md index 3333a75..6bd039f 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -5,7 +5,7 @@ Mpcium implements a threshold signature scheme with industry-standard security practices to protect cryptographic operations: 1. **Distributed Trust**: No single entity possesses complete private keys -2. **Threshold Cryptography**: Requires t+1 nodes to participate in signing operations +2. **Threshold Cryptography**: Requires t-out-of-n nodes to participate in signing operations 3. **End-to-End Verification**: All communications are signed and verified 4. **Defense in Depth**: Multiple layers of encryption and verification From 9e5bc4a0389243aa0181e876205e0091283bef18 Mon Sep 17 00:00:00 2001 From: athen Date: Mon, 4 Aug 2025 19:55:45 +0200 Subject: [PATCH 02/23] draft impl for diffie hellman exchange --- cmd/mpcium/main.go | 22 ++++ config.yaml.template | 2 +- pkg/identity/identity.go | 44 +++++++ pkg/mpc/key_exchange_session.go | 202 ++++++++++++++++++++++++++++++++ pkg/mpc/session.go | 2 + pkg/types/tss.go | 28 ++++- 6 files changed, 298 insertions(+), 2 deletions(-) create mode 100644 pkg/mpc/key_exchange_session.go diff --git a/cmd/mpcium/main.go b/cmd/mpcium/main.go index e47edfc..1973be1 100644 --- a/cmd/mpcium/main.go +++ b/cmd/mpcium/main.go @@ -176,6 +176,10 @@ func runNode(ctx context.Context, c *cli.Command) error { ) defer mpcNode.Close() + + + + eventConsumer := eventconsumer.NewEventConsumer( mpcNode, pubsub, @@ -220,6 +224,24 @@ func runNode(ctx context.Context, c *cli.Command) error { } }() + + + + //Start---negotiate p2p secret symmetric key via DH Key Exchange session + dhSession := mpc.NewECDHSession(nodeID, peerNodeIDs, pubsub, directMessaging, identityStore) + if err := dhSession.StartKeyExchange(); err != nil { + logger.Fatal("Failed to start DH key exchange", err) + } + if err := dhSession.WaitForCompletion(); err != nil { + logger.Fatal("DH key exchange failed", err) + } + logger.Info("DH key exchange completed successfully") + //End---negotiate p2p secret symmetric key via DH Key Exchange session + + + + + var wg sync.WaitGroup errChan := make(chan error, 2) diff --git a/config.yaml.template b/config.yaml.template index e7ba7d2..1f05f9b 100644 --- a/config.yaml.template +++ b/config.yaml.template @@ -5,7 +5,7 @@ consul: mpc_threshold: 2 environment: development -badger_password: "your_badger_password" +badger_password: "F))ysJp?E]ol&I;^" event_initiator_pubkey: "event_initiator_pubkey" max_concurrent_keygen: 2 db_path: "." diff --git a/pkg/identity/identity.go b/pkg/identity/identity.go index 68b9176..b29be8c 100644 --- a/pkg/identity/identity.go +++ b/pkg/identity/identity.go @@ -37,6 +37,10 @@ type Store interface { VerifyInitiatorMessage(msg types.InitiatorMessage) error SignMessage(msg *types.TssMessage) ([]byte, error) VerifyMessage(msg *types.TssMessage) error + + + SignEcdhMessage(msg *types.ECDHMessage) ([]byte, error) + VerifySignature(msg *types.ECDHMessage) error } // fileStore implements the Store interface using the filesystem @@ -219,6 +223,7 @@ func (s *fileStore) GetPublicKey(nodeID string) ([]byte, error) { return nil, fmt.Errorf("public key not found for node ID: %s", nodeID) } +//TODO: EncryptSign each message func (s *fileStore) SignMessage(msg *types.TssMessage) ([]byte, error) { // Get deterministic bytes for signing msgBytes, err := msg.MarshalForSigning() @@ -230,6 +235,7 @@ func (s *fileStore) SignMessage(msg *types.TssMessage) ([]byte, error) { return signature, nil } +//TODO: P2p message should be kept secret to the NATs server // VerifyMessage verifies a TSS message's signature using the sender's public key func (s *fileStore) VerifyMessage(msg *types.TssMessage) error { if msg.Signature == nil { @@ -259,6 +265,44 @@ func (s *fileStore) VerifyMessage(msg *types.TssMessage) error { return nil } +//Sign ECDH key exchange message +func (s *fileStore) SignEcdhMessage(msg *types.ECDHMessage) ([]byte, error) { + // Get deterministic bytes for signing + msgBytes, err := msg.MarshalForSigning() + if err != nil { + return nil, fmt.Errorf("failed to marshal message for signing: %w", err) + } + + signature := ed25519.Sign(s.privateKey, msgBytes) + return signature, nil +} + +//Verify ECDH key exchange message +func (s *fileStore) VerifySignature(msg *types.ECDHMessage) error { + if msg.Signature == nil { + return fmt.Errorf("ECDH message has no signature") + } + + // Get the sender's public key + senderPk, err := s.GetPublicKey(msg.From) + if err != nil { + return fmt.Errorf("failed to get sender's public key: %w", err) + } + + // Get deterministic bytes for verification + msgBytes, err := msg.MarshalForSigning() + if err != nil { + return fmt.Errorf("failed to marshal message for verification: %w", err) + } + + // Verify the signature + if !ed25519.Verify(senderPk, msgBytes, msg.Signature) { + return fmt.Errorf("invalid signature") + } + + return nil +} + // VerifyInitiatorMessage verifies that a message was signed by the known initiator func (s *fileStore) VerifyInitiatorMessage(msg types.InitiatorMessage) error { // Get the raw message that was signed diff --git a/pkg/mpc/key_exchange_session.go b/pkg/mpc/key_exchange_session.go new file mode 100644 index 0000000..db3cc02 --- /dev/null +++ b/pkg/mpc/key_exchange_session.go @@ -0,0 +1,202 @@ +// pkg/mpc/ecdh_session.go +package mpc + +import ( + "crypto/ecdh" + "crypto/rand" + "crypto/sha256" + "golang.org/x/crypto/hkdf" + + "fmt" + "time" + + "github.com/fystack/mpcium/pkg/identity" + "github.com/fystack/mpcium/pkg/logger" + "github.com/fystack/mpcium/pkg/messaging" + "github.com/fystack/mpcium/pkg/types" + + "github.com/nats-io/nats.go" + "encoding/json" + +) + +type ECDHSession struct { + nodeID string + peerIDs []string + + pubSub messaging.PubSub + + direct messaging.DirectMessaging + identityStore identity.Store + symmetricKeys map[string][]byte // peerID -> symmetric key + + privateKey *ecdh.PrivateKey + publicKey *ecdh.PublicKey + + exchangeComplete chan struct{} + errCh chan error +} + +func NewECDHSession( + nodeID string, + peerIDs []string, + pubSub messaging.PubSub, + direct messaging.DirectMessaging, + identityStore identity.Store, +) *ECDHSession { + return &ECDHSession{ + nodeID: nodeID, + peerIDs: peerIDs, + pubSub: pubSub, + direct: direct, + identityStore: identityStore, + symmetricKeys: make(map[string][]byte), + exchangeComplete: make(chan struct{}), + errCh: make(chan error), + } +} + +func (e *ECDHSession) StartKeyExchange() error { + + // Generate an ephemeral ECDH key pair + privateKey, err := ecdh.P256().GenerateKey(rand.Reader) + if err != nil { + return fmt.Errorf("failed to generate ECDH key pair: %w", err) + } + + e.privateKey = privateKey + e.publicKey = privateKey.PublicKey() + + //Subscribe to ECDH messages + sub, err := e.pubSub.Subscribe(fmt.Sprintf("ecdh:exchange:%s", e.nodeID), func(natMsg *nats.Msg) { + + + + var ecdhMsg types.ECDHMessage + if err := json.Unmarshal(natMsg.Data, &ecdhMsg); err != nil { + return + } + + logger.Info("Received ECDH message from peer node", "1st", ecdhMsg.From) + + //TODO: consider how to avoid replay attack + if err := e.identityStore.VerifySignature(&ecdhMsg); err != nil { + e.errCh <- err + return + } + + logger.Info("Proceed onto messsage processing", "2nd", ecdhMsg.From) + + //Perform ECDH key exchange + if err := e.processECDHMessage(&ecdhMsg); err != nil { + e.errCh <- err + return + } + //Check if exchange is complete + if len(e.symmetricKeys) == len(e.peerIDs) { + close(e.exchangeComplete) + return + } + }) + defer sub.Unsubscribe() // Use sub to clean up + + if err != nil { + return err + } + + if err != nil { + return fmt.Errorf("failed to subscribe to ECDH topic: %w", err) + } + + // Broadcast public DH key to all other peers + return e.broadcastPublicKey() +} + +func (e *ECDHSession) broadcastPublicKey() error { + publicKeyBytes := e.publicKey.Bytes() + + for _, peerID := range e.peerIDs { + msg := types.ECDHMessage{ + From: e.nodeID, + To: peerID, + PublicKey: publicKeyBytes, + Timestamp: time.Now(), + } + + //Sign the message using existing identity store + signature, err := e.identityStore.SignEcdhMessage(&msg) + if err != nil { + return fmt.Errorf("failed to sign ECDH message: %w", err) + } + + msg.Signature = signature + signedMsgBytes, _ := json.Marshal(msg) + + if err := e.direct.Send(fmt.Sprintf("ecdh:exchange:%s", peerID), signedMsgBytes); err != nil { + return fmt.Errorf("failed to send public DH message to %s: %w", peerID, err) + } + } + return nil +} + +func (e *ECDHSession) processECDHMessage(msg *types.ECDHMessage) error { + peerPublicKey, err := ecdh.P256().NewPublicKey(msg.PublicKey) + if err != nil { + return fmt.Errorf("invalid peer public key: %w", err) + } + + // Perform ECDH + sharedSecret, err := e.privateKey.ECDH(peerPublicKey) + if err != nil { + return fmt.Errorf("ECDH failed: %w", err) + } + + // Derive symmetric key using HKDF + symmetricKey := e.deriveSymmetricKey(sharedSecret, msg.From) + e.symmetricKeys[msg.From] = symmetricKey + + return nil +} + +func (e *ECDHSession) GetSymmetricKey(peerID string) ([]byte, bool) { + key, exists := e.symmetricKeys[peerID] + return key, exists +} + +// deriveSymmetricKey derives a symmetric key from the shared secret and peer ID using HKDF. +func (e *ECDHSession) deriveSymmetricKey(sharedSecret []byte, peerID string) []byte { + // Use SHA256 as the hash function for HKDF + hash := sha256.New + // Info parameter can include context-specific data; here we use the peerID + var info []byte + if e.nodeID < peerID { + info = []byte(e.nodeID + peerID) + } else { + info = []byte(peerID + e.nodeID) + } + //TODO: Salt can be nil or a random value; here we use nil for simplicity + var salt []byte + + // Create an HKDF instance + hkdf := hkdf.New(hash, sharedSecret, salt, info) + + // Derive a 32-byte symmetric key (suitable for AES-256) + symmetricKey := make([]byte, 32) + _, err := hkdf.Read(symmetricKey) + if err != nil { + // In a production environment, handle this error appropriately + panic(err) // Simplified for example; replace with proper error handling + } + return symmetricKey +} + +func (e *ECDHSession) WaitForCompletion() error { + select { + case <-e.exchangeComplete: + return nil + case err := <-e.errCh: + return err + case <-time.After(30 * time.Second): + return fmt.Errorf("ECDH key exchange timeout") + } +} \ No newline at end of file diff --git a/pkg/mpc/session.go b/pkg/mpc/session.go index ece9c18..0e0e682 100644 --- a/pkg/mpc/session.go +++ b/pkg/mpc/session.go @@ -88,6 +88,7 @@ func (s *session) PartyCount() int { return len(s.partyIDs) } +//TODO: Does AEAD encryption for each message so NATs server learns nothing func (s *session) handleTssMessage(keyshare tss.Message) { data, routing, err := keyshare.WireBytes() if err != nil { @@ -133,6 +134,7 @@ func (s *session) handleTssMessage(keyshare tss.Message) { } } +//TODO: the logic of receiving message should be modified func (s *session) receiveTssMessage(rawMsg []byte) { msg, err := types.UnmarshalTssMessage(rawMsg) if err != nil { diff --git a/pkg/types/tss.go b/pkg/types/tss.go index c2d54df..7df91cc 100644 --- a/pkg/types/tss.go +++ b/pkg/types/tss.go @@ -5,6 +5,7 @@ package types import ( "encoding/json" "sort" + "time" "github.com/bnb-chain/tss-lib/v2/tss" ) @@ -22,6 +23,15 @@ type TssMessage struct { Signature []byte `json:"signature"` } +type ECDHMessage struct { + From string `json:"from"` + To string `json:"to"` + + PublicKey []byte `json:"public_key"` + Timestamp time.Time `json:"timestamp"` + Signature []byte `json:"signature"` +} + func NewTssMessage( walletID string, msgBytes []byte, @@ -110,6 +120,22 @@ func UnmarshalStartMessage(msgBytes []byte) (*StartMessage, error) { return msg, nil } + +// MarshalForSigning returns the deterministic JSON bytes for signing +func (msg *ECDHMessage) MarshalForSigning() ([]byte, error) { + // Create a map with ordered keys + signingData := map[string]interface{}{ + "From": msg.From, + "To": msg.To, + "PublicKey": msg.PublicKey, + "Timestamp": msg.Timestamp, + } + + // Use json.Marshal with sorted keys + return json.Marshal(signingData) +} + + // MarshalForSigning returns the deterministic JSON bytes for signing func (msg *TssMessage) MarshalForSigning() ([]byte, error) { // Create a map with ordered keys @@ -135,4 +161,4 @@ func getPartyIDs(parties []*tss.PartyID) []string { } sort.Strings(ids) // Ensure deterministic order return ids -} +} \ No newline at end of file From f7f618180da4785a483c84c7d1bb97213464935f Mon Sep 17 00:00:00 2001 From: athen Date: Wed, 6 Aug 2025 15:48:07 +0200 Subject: [PATCH 03/23] New Feature: add p2p channel among nodes, enabling authenticated encryption --- cmd/mpcium/main.go | 29 +--- pkg/identity/identity.go | 127 ++++++++++++++- pkg/messaging/point2point.go | 12 +- pkg/messaging/pubsub.go | 3 +- pkg/mpc/ecdsa_keygen_session.go | 4 +- pkg/mpc/ecdsa_resharing_session.go | 4 +- pkg/mpc/ecdsa_signing_session.go | 4 +- pkg/mpc/eddsa_keygen_session.go | 4 +- pkg/mpc/eddsa_resharing_session.go | 4 +- pkg/mpc/eddsa_signing_session.go | 4 +- pkg/mpc/key_exchange_session.go | 243 +++++++++++++---------------- pkg/mpc/node.go | 21 ++- pkg/mpc/registry.go | 10 +- pkg/mpc/session.go | 152 ++++++++++++------ 14 files changed, 384 insertions(+), 237 deletions(-) diff --git a/cmd/mpcium/main.go b/cmd/mpcium/main.go index 1973be1..803c593 100644 --- a/cmd/mpcium/main.go +++ b/cmd/mpcium/main.go @@ -159,7 +159,7 @@ func runNode(ctx context.Context, c *cli.Command) error { reshareResultQueue := mqManager.NewMessageQueue("mpc_reshare_result") defer reshareResultQueue.Close() - logger.Info("Node is running", "peerID", nodeID, "name", nodeName) + logger.Info("Node is running", "id", nodeID, "name", nodeName) peerNodeIDs := GetPeerIDs(peers) peerRegistry := mpc.NewRegistry(nodeID, peerNodeIDs, consulClient.KV()) @@ -176,10 +176,6 @@ func runNode(ctx context.Context, c *cli.Command) error { ) defer mpcNode.Close() - - - - eventConsumer := eventconsumer.NewEventConsumer( mpcNode, pubsub, @@ -207,7 +203,8 @@ func runNode(ctx context.Context, c *cli.Command) error { } logger.Info("[READY] Node is ready", "nodeID", nodeID) appContext, cancel := context.WithCancel(context.Background()) - // Setup signal handling to cancel context on termination signals. + + //Setup signal handling to cancel context on termination signals. go func() { sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) @@ -224,24 +221,6 @@ func runNode(ctx context.Context, c *cli.Command) error { } }() - - - - //Start---negotiate p2p secret symmetric key via DH Key Exchange session - dhSession := mpc.NewECDHSession(nodeID, peerNodeIDs, pubsub, directMessaging, identityStore) - if err := dhSession.StartKeyExchange(); err != nil { - logger.Fatal("Failed to start DH key exchange", err) - } - if err := dhSession.WaitForCompletion(); err != nil { - logger.Fatal("DH key exchange failed", err) - } - logger.Info("DH key exchange completed successfully") - //End---negotiate p2p secret symmetric key via DH Key Exchange session - - - - - var wg sync.WaitGroup errChan := make(chan error, 2) @@ -272,7 +251,6 @@ func runNode(ctx context.Context, c *cli.Command) error { logger.Info("All consumers have finished") close(errChan) }() - for err := range errChan { if err != nil { logger.Error("Consumer error received", err) @@ -280,6 +258,7 @@ func runNode(ctx context.Context, c *cli.Command) error { return err } } + return nil } diff --git a/pkg/identity/identity.go b/pkg/identity/identity.go index b29be8c..dada327 100644 --- a/pkg/identity/identity.go +++ b/pkg/identity/identity.go @@ -1,7 +1,10 @@ package identity import ( + "crypto/aes" + "crypto/cipher" "crypto/ed25519" + "crypto/rand" "encoding/hex" "encoding/json" "errors" @@ -38,9 +41,15 @@ type Store interface { SignMessage(msg *types.TssMessage) ([]byte, error) VerifyMessage(msg *types.TssMessage) error - + // New ECDH methods SignEcdhMessage(msg *types.ECDHMessage) ([]byte, error) VerifySignature(msg *types.ECDHMessage) error + + SetSymmetricKey(peerID string, key []byte) + GetSymmetricKey(peerID string) ([]byte, error) + + EncryptMessage(plaintext []byte, peerID string) ([]byte, error) + DecryptMessage(cipher []byte, peerID string) ([]byte, error) } // fileStore implements the Store interface using the filesystem @@ -55,8 +64,24 @@ type fileStore struct { // Cached private key privateKey []byte initiatorPubKey []byte + + //Cached ecdh symmetric key + symmetricKeys map[string][]byte } +// ECDHStore implements the Store interface with ECDH +// type ECDHStore struct { +// symmetricKeys map[string][]byte +// mu sync.RWMutex +// } + +// // initializes a new ECDHStore. +// func NewECDHStore() *ECDHStore { +// return &ECDHStore{ +// symmetricKeys: make(map[string][]byte), +// } +// } + // NewFileStore creates a new identity store func NewFileStore(identityDir, nodeName string, decrypt bool) (*fileStore, error) { if err := os.MkdirAll(identityDir, 0750); err != nil { @@ -101,6 +126,8 @@ func NewFileStore(identityDir, nodeName string, decrypt bool) (*fileStore, error publicKeys: make(map[string][]byte), privateKey: privateKey, initiatorPubKey: initiatorPubKey, + + symmetricKeys: make(map[string][]byte), } // Check that each node in peers.json has an identity file @@ -211,6 +238,25 @@ func loadPrivateKey(identityDir, nodeName string, decrypt bool) (string, error) } } +// Set SymmetricKey: adds or updates a symmetric key for a given peer ID. +func (s *fileStore) SetSymmetricKey(peerID string, key []byte) { + s.mu.Lock() + defer s.mu.Unlock() + s.symmetricKeys[peerID] = key +} + +// Get SymmetricKey: retrieves a peer node's dh symmetric-key by its ID +func (s *fileStore) GetSymmetricKey(peerID string) ([]byte, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + if key, exists := s.symmetricKeys[peerID]; exists { + return key, nil + } + + return nil, fmt.Errorf("SymmetricKey key not found for node ID: %s", peerID) +} + // GetPublicKey retrieves a node's public key by its ID func (s *fileStore) GetPublicKey(nodeID string) ([]byte, error) { s.mu.RLock() @@ -223,7 +269,6 @@ func (s *fileStore) GetPublicKey(nodeID string) ([]byte, error) { return nil, fmt.Errorf("public key not found for node ID: %s", nodeID) } -//TODO: EncryptSign each message func (s *fileStore) SignMessage(msg *types.TssMessage) ([]byte, error) { // Get deterministic bytes for signing msgBytes, err := msg.MarshalForSigning() @@ -235,7 +280,6 @@ func (s *fileStore) SignMessage(msg *types.TssMessage) ([]byte, error) { return signature, nil } -//TODO: P2p message should be kept secret to the NATs server // VerifyMessage verifies a TSS message's signature using the sender's public key func (s *fileStore) VerifyMessage(msg *types.TssMessage) error { if msg.Signature == nil { @@ -265,7 +309,80 @@ func (s *fileStore) VerifyMessage(msg *types.TssMessage) error { return nil } -//Sign ECDH key exchange message +// encryptAEAD encrypts plaintext using AES-GCM with authentication. +func encryptAEAD(symmetricKey []byte, plaintext []byte) ([]byte, error) { + // Create AES cipher block + block, err := aes.NewCipher(symmetricKey) + if err != nil { + return nil, fmt.Errorf("failed to create AES cipher: %w", err) + } + + // Generate a random 12-byte nonce + nonce := make([]byte, 12) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return nil, fmt.Errorf("failed to generate nonce: %w", err) + } + + // Create AES-GCM cipher + aead, err := cipher.NewGCM(block) + if err != nil { + return nil, fmt.Errorf("failed to create GCM: %w", err) + } + + // Encrypt with no additional data (nil) + ciphertext := aead.Seal(nil, nonce, plaintext, nil) + + // Prepend nonce to ciphertext for decryption + return append(nonce, ciphertext...), nil +} + +// decryptAEAD decrypts ciphertext using AES-GCM with authentication. +func decryptAEAD(symmetricKey []byte, ciphertext []byte) ([]byte, error) { + // Create AES cipher block + block, err := aes.NewCipher(symmetricKey) + if err != nil { + return nil, fmt.Errorf("failed to create AES cipher: %w", err) + } + + // Create AES-GCM cipher + aead, err := cipher.NewGCM(block) + if err != nil { + return nil, fmt.Errorf("failed to create GCM: %w", err) + } + + // Extract nonce (first 12 bytes) and ciphertext + if len(ciphertext) < 12 { + return nil, errors.New("ciphertext too short") + } + nonce := ciphertext[:12] + ciphertext = ciphertext[12:] + + // Decrypt with no additional data (nil) + plaintext, err := aead.Open(nil, nonce, ciphertext, nil) + if err != nil { + return nil, fmt.Errorf("decryption failed: %w", err) + } + + return plaintext, nil +} + +func (s *fileStore) EncryptMessage(plaintext []byte, peerID string) ([]byte, error) { + key, err := s.GetSymmetricKey(peerID) + if err != nil || key == nil { + return nil, fmt.Errorf("no symmetric key for peer %s", peerID) + } + return encryptAEAD(key, plaintext) +} + +func (s *fileStore) DecryptMessage(cipher []byte, peerID string) ([]byte, error) { + key, err := s.GetSymmetricKey(peerID) + if err != nil || key == nil { + return nil, fmt.Errorf("no symmetric key for peer %s", peerID) + } + return decryptAEAD(key, cipher) +} + +// Sign ECDH key exchange message func (s *fileStore) SignEcdhMessage(msg *types.ECDHMessage) ([]byte, error) { // Get deterministic bytes for signing msgBytes, err := msg.MarshalForSigning() @@ -277,7 +394,7 @@ func (s *fileStore) SignEcdhMessage(msg *types.ECDHMessage) ([]byte, error) { return signature, nil } -//Verify ECDH key exchange message +// Verify ECDH key exchange message func (s *fileStore) VerifySignature(msg *types.ECDHMessage) error { if msg.Signature == nil { return fmt.Errorf("ECDH message has no signature") diff --git a/pkg/messaging/point2point.go b/pkg/messaging/point2point.go index 6919c05..665fbff 100644 --- a/pkg/messaging/point2point.go +++ b/pkg/messaging/point2point.go @@ -9,8 +9,8 @@ import ( ) type DirectMessaging interface { - Listen(target string, handler func(data []byte)) (Subscription, error) - Send(target string, data []byte) error + Listen(topic string, handler func(data []byte)) (Subscription, error) + Send(topic string, data []byte) error } type natsDirectMessaging struct { @@ -23,11 +23,11 @@ func NewNatsDirectMessaging(natsConn *nats.Conn) DirectMessaging { } } -func (d *natsDirectMessaging) Send(id string, message []byte) error { +func (d *natsDirectMessaging) Send(topic string, message []byte) error { var retryCount = 0 err := retry.Do( func() error { - _, err := d.natsConn.Request(id, message, 3*time.Second) + _, err := d.natsConn.Request(topic, message, 3*time.Second) if err != nil { return err } @@ -44,8 +44,8 @@ func (d *natsDirectMessaging) Send(id string, message []byte) error { return err } -func (d *natsDirectMessaging) Listen(id string, handler func(data []byte)) (Subscription, error) { - sub, err := d.natsConn.Subscribe(id, func(m *nats.Msg) { +func (d *natsDirectMessaging) Listen(topic string, handler func(data []byte)) (Subscription, error) { + sub, err := d.natsConn.Subscribe(topic, func(m *nats.Msg) { handler(m.Data) if err := m.Respond([]byte("OK")); err != nil { logger.Error("Failed to respond to message", err) diff --git a/pkg/messaging/pubsub.go b/pkg/messaging/pubsub.go index 57547ea..8e4fd0e 100644 --- a/pkg/messaging/pubsub.go +++ b/pkg/messaging/pubsub.go @@ -51,8 +51,7 @@ func (n *natsPubSub) PublishWithReply(topic, reply string, data []byte, headers } func (n *natsPubSub) Subscribe(topic string, handler func(msg *nats.Msg)) (Subscription, error) { - // TODO: Handle subscription - // handle more fields in msg + //Handle subscription: handle more fields in msg sub, err := n.natsConn.Subscribe(topic, func(msg *nats.Msg) { handler(msg) }) diff --git a/pkg/mpc/ecdsa_keygen_session.go b/pkg/mpc/ecdsa_keygen_session.go index e9ea00a..36f8a24 100644 --- a/pkg/mpc/ecdsa_keygen_session.go +++ b/pkg/mpc/ecdsa_keygen_session.go @@ -61,8 +61,8 @@ func newECDSAKeygenSession( ComposeBroadcastTopic: func() string { return fmt.Sprintf("keygen:broadcast:ecdsa:%s", walletID) }, - ComposeDirectTopic: func(nodeID string) string { - return fmt.Sprintf("keygen:direct:ecdsa:%s:%s", nodeID, walletID) + ComposeDirectTopic: func(fromRouteID string, toRouteID string) string { + return fmt.Sprintf("keygen:direct:ecdsa:%s:%s:%s", fromRouteID, toRouteID, walletID) }, }, composeKey: func(walletID string) string { diff --git a/pkg/mpc/ecdsa_resharing_session.go b/pkg/mpc/ecdsa_resharing_session.go index 0b19be7..84c06da 100644 --- a/pkg/mpc/ecdsa_resharing_session.go +++ b/pkg/mpc/ecdsa_resharing_session.go @@ -68,8 +68,8 @@ func NewECDSAReshareSession( ComposeBroadcastTopic: func() string { return fmt.Sprintf("resharing:broadcast:ecdsa:%s", walletID) }, - ComposeDirectTopic: func(nodeID string) string { - return fmt.Sprintf("resharing:direct:ecdsa:%s:%s", nodeID, walletID) + ComposeDirectTopic: func(fromRouteID string, toRouteID string) string { + return fmt.Sprintf("resharing:direct:ecdsa:%s:%s:%s", fromRouteID, toRouteID, walletID) }, }, composeKey: func(walletID string) string { diff --git a/pkg/mpc/ecdsa_signing_session.go b/pkg/mpc/ecdsa_signing_session.go index 76cba2c..cd3d9ed 100644 --- a/pkg/mpc/ecdsa_signing_session.go +++ b/pkg/mpc/ecdsa_signing_session.go @@ -71,8 +71,8 @@ func newECDSASigningSession( ComposeBroadcastTopic: func() string { return fmt.Sprintf("sign:ecdsa:broadcast:%s:%s", walletID, txID) }, - ComposeDirectTopic: func(nodeID string) string { - return fmt.Sprintf("sign:ecdsa:direct:%s:%s", nodeID, txID) + ComposeDirectTopic: func(fromRouteID string, toRouteID string) string { + return fmt.Sprintf("sign:ecdsa:direct:%s:%s:%s", fromRouteID, toRouteID, txID) }, }, composeKey: func(waleltID string) string { diff --git a/pkg/mpc/eddsa_keygen_session.go b/pkg/mpc/eddsa_keygen_session.go index a4fe030..9855f13 100644 --- a/pkg/mpc/eddsa_keygen_session.go +++ b/pkg/mpc/eddsa_keygen_session.go @@ -49,8 +49,8 @@ func newEDDSAKeygenSession( ComposeBroadcastTopic: func() string { return fmt.Sprintf("keygen:broadcast:eddsa:%s", walletID) }, - ComposeDirectTopic: func(nodeID string) string { - return fmt.Sprintf("keygen:direct:eddsa:%s:%s", nodeID, walletID) + ComposeDirectTopic: func(fromRouteID string, toRouteID string) string { + return fmt.Sprintf("keygen:direct:eddsa:%s:%s:%s", fromRouteID, toRouteID, walletID) }, }, composeKey: func(waleltID string) string { diff --git a/pkg/mpc/eddsa_resharing_session.go b/pkg/mpc/eddsa_resharing_session.go index 9135fbc..c2e206e 100644 --- a/pkg/mpc/eddsa_resharing_session.go +++ b/pkg/mpc/eddsa_resharing_session.go @@ -58,8 +58,8 @@ func NewEDDSAReshareSession( ComposeBroadcastTopic: func() string { return fmt.Sprintf("reshare:broadcast:eddsa:%s", walletID) }, - ComposeDirectTopic: func(nodeID string) string { - return fmt.Sprintf("reshare:direct:eddsa:%s:%s", nodeID, walletID) + ComposeDirectTopic: func(fromRouteID string, toRouteID string) string { + return fmt.Sprintf("reshare:direct:eddsa:%s:%s:%s", fromRouteID, toRouteID, walletID) }, }, composeKey: func(walletID string) string { diff --git a/pkg/mpc/eddsa_signing_session.go b/pkg/mpc/eddsa_signing_session.go index 4538730..4ebd879 100644 --- a/pkg/mpc/eddsa_signing_session.go +++ b/pkg/mpc/eddsa_signing_session.go @@ -62,8 +62,8 @@ func newEDDSASigningSession( ComposeBroadcastTopic: func() string { return fmt.Sprintf("sign:eddsa:broadcast:%s:%s", walletID, txID) }, - ComposeDirectTopic: func(nodeID string) string { - return fmt.Sprintf("sign:eddsa:direct:%s:%s", nodeID, txID) + ComposeDirectTopic: func(fromRouteID string, toRouteID string) string { + return fmt.Sprintf("sign:eddsa:direct:%s:%s:%s", fromRouteID, toRouteID, txID) }, }, composeKey: func(waleltID string) string { diff --git a/pkg/mpc/key_exchange_session.go b/pkg/mpc/key_exchange_session.go index db3cc02..a766031 100644 --- a/pkg/mpc/key_exchange_session.go +++ b/pkg/mpc/key_exchange_session.go @@ -2,168 +2,148 @@ package mpc import ( - "crypto/ecdh" - "crypto/rand" + "crypto/ecdh" + "crypto/rand" "crypto/sha256" + "golang.org/x/crypto/hkdf" - "fmt" - "time" - - "github.com/fystack/mpcium/pkg/identity" + "fmt" + "time" + + "github.com/fystack/mpcium/pkg/identity" "github.com/fystack/mpcium/pkg/logger" - "github.com/fystack/mpcium/pkg/messaging" - "github.com/fystack/mpcium/pkg/types" + "github.com/fystack/mpcium/pkg/messaging" + "github.com/fystack/mpcium/pkg/types" - "github.com/nats-io/nats.go" "encoding/json" + "github.com/nats-io/nats.go" ) type ECDHSession struct { - nodeID string - peerIDs []string + nodeID string + peerIDs []string + + pubSub messaging.PubSub - pubSub messaging.PubSub + ecdhSub messaging.Subscription - direct messaging.DirectMessaging - identityStore identity.Store - symmetricKeys map[string][]byte // peerID -> symmetric key + identityStore identity.Store + symmetricKeys map[string][]byte // peerID -> symmetric key - privateKey *ecdh.PrivateKey - publicKey *ecdh.PublicKey + privateKey *ecdh.PrivateKey + publicKey *ecdh.PublicKey - exchangeComplete chan struct{} - errCh chan error + exchangeComplete chan struct{} + errCh chan error } func NewECDHSession( - nodeID string, - peerIDs []string, - pubSub messaging.PubSub, - direct messaging.DirectMessaging, - identityStore identity.Store, + nodeID string, + peerIDs []string, + pubSub messaging.PubSub, + identityStore identity.Store, ) *ECDHSession { - return &ECDHSession{ - nodeID: nodeID, - peerIDs: peerIDs, - pubSub: pubSub, - direct: direct, - identityStore: identityStore, - symmetricKeys: make(map[string][]byte), - exchangeComplete: make(chan struct{}), - errCh: make(chan error), - } + return &ECDHSession{ + nodeID: nodeID, + peerIDs: peerIDs, + pubSub: pubSub, + identityStore: identityStore, + // symmetricKeys: make(map[string][]byte), + exchangeComplete: make(chan struct{}), + errCh: make(chan error), + } } func (e *ECDHSession) StartKeyExchange() error { + // Generate an ephemeral ECDH key pair + privateKey, err := ecdh.P256().GenerateKey(rand.Reader) + if err != nil { + return fmt.Errorf("failed to generate ECDH key pair: %w", err) + } - // Generate an ephemeral ECDH key pair - privateKey, err := ecdh.P256().GenerateKey(rand.Reader) - if err != nil { - return fmt.Errorf("failed to generate ECDH key pair: %w", err) - } - - e.privateKey = privateKey - e.publicKey = privateKey.PublicKey() - - //Subscribe to ECDH messages - sub, err := e.pubSub.Subscribe(fmt.Sprintf("ecdh:exchange:%s", e.nodeID), func(natMsg *nats.Msg) { - - + e.privateKey = privateKey + e.publicKey = privateKey.PublicKey() + //Subscribe to ECDH messages + sub, err := e.pubSub.Subscribe(fmt.Sprintf("ecdh:exchange:%s", e.nodeID), func(natMsg *nats.Msg) { var ecdhMsg types.ECDHMessage - if err := json.Unmarshal(natMsg.Data, &ecdhMsg); err != nil { - return - } + if err := json.Unmarshal(natMsg.Data, &ecdhMsg); err != nil { + return + } - logger.Info("Received ECDH message from peer node", "1st", ecdhMsg.From) + logger.Info("Received ECDH message from", "node", ecdhMsg.From) //TODO: consider how to avoid replay attack - if err := e.identityStore.VerifySignature(&ecdhMsg); err != nil { - e.errCh <- err - return - } - - logger.Info("Proceed onto messsage processing", "2nd", ecdhMsg.From) - - //Perform ECDH key exchange - if err := e.processECDHMessage(&ecdhMsg); err != nil { - e.errCh <- err - return - } - //Check if exchange is complete - if len(e.symmetricKeys) == len(e.peerIDs) { - close(e.exchangeComplete) - return - } + if err := e.identityStore.VerifySignature(&ecdhMsg); err != nil { + e.errCh <- err + return + } + + peerPublicKey, err := ecdh.P256().NewPublicKey(ecdhMsg.PublicKey) + if err != nil { + e.errCh <- err + return + } + // Perform ECDH + sharedSecret, err := e.privateKey.ECDH(peerPublicKey) + if err != nil { + e.errCh <- err + return + } + + // Derive symmetric key using HKDF + symmetricKey := e.deriveSymmetricKey(sharedSecret, ecdhMsg.From) + e.identityStore.SetSymmetricKey(ecdhMsg.From, symmetricKey) + + //Check if exchange is complete + // if len(e.identityStore.symmetricKeys) == len(e.peerIDs)-1 { + // logger.Info("Finished ECDH Key Exchange") + // close(e.exchangeComplete) + // return + // } }) - defer sub.Unsubscribe() // Use sub to clean up - + e.ecdhSub = sub + if err != nil { - return err + return fmt.Errorf("failed to subscribe to ECDH topic: %w", err) } - - if err != nil { - return fmt.Errorf("failed to subscribe to ECDH topic: %w", err) - } - - // Broadcast public DH key to all other peers - return e.broadcastPublicKey() -} - -func (e *ECDHSession) broadcastPublicKey() error { - publicKeyBytes := e.publicKey.Bytes() - - for _, peerID := range e.peerIDs { - msg := types.ECDHMessage{ - From: e.nodeID, - To: peerID, - PublicKey: publicKeyBytes, - Timestamp: time.Now(), - } - - //Sign the message using existing identity store - signature, err := e.identityStore.SignEcdhMessage(&msg) - if err != nil { - return fmt.Errorf("failed to sign ECDH message: %w", err) - } - - msg.Signature = signature - signedMsgBytes, _ := json.Marshal(msg) - - if err := e.direct.Send(fmt.Sprintf("ecdh:exchange:%s", peerID), signedMsgBytes); err != nil { - return fmt.Errorf("failed to send public DH message to %s: %w", peerID, err) - } - } - return nil + return nil } -func (e *ECDHSession) processECDHMessage(msg *types.ECDHMessage) error { - peerPublicKey, err := ecdh.P256().NewPublicKey(msg.PublicKey) - if err != nil { - return fmt.Errorf("invalid peer public key: %w", err) - } - - // Perform ECDH - sharedSecret, err := e.privateKey.ECDH(peerPublicKey) - if err != nil { - return fmt.Errorf("ECDH failed: %w", err) - } - - // Derive symmetric key using HKDF - symmetricKey := e.deriveSymmetricKey(sharedSecret, msg.From) - e.symmetricKeys[msg.From] = symmetricKey - - return nil +func (e *ECDHSession) BroadcastPublicKey() error { + publicKeyBytes := e.publicKey.Bytes() + for _, peerID := range e.peerIDs { + if peerID != e.nodeID { + msg := types.ECDHMessage{ + From: e.nodeID, + To: peerID, + PublicKey: publicKeyBytes, + Timestamp: time.Now(), + } + //Sign the message using existing identity store + signature, err := e.identityStore.SignEcdhMessage(&msg) + if err != nil { + return fmt.Errorf("failed to sign ECDH message: %w", err) + } + msg.Signature = signature + signedMsgBytes, _ := json.Marshal(msg) + + if err := e.pubSub.Publish(fmt.Sprintf("ecdh:exchange:%s", peerID), signedMsgBytes); err != nil { + return fmt.Errorf("failed to send public DH message to %s: %w", peerID, err) + } + } + } + return nil } func (e *ECDHSession) GetSymmetricKey(peerID string) ([]byte, bool) { - key, exists := e.symmetricKeys[peerID] - return key, exists + key, exists := e.symmetricKeys[peerID] + return key, exists } -// deriveSymmetricKey derives a symmetric key from the shared secret and peer ID using HKDF. +// derives a symmetric key from the shared secret and peer ID using HKDF. func (e *ECDHSession) deriveSymmetricKey(sharedSecret []byte, peerID string) []byte { // Use SHA256 as the hash function for HKDF hash := sha256.New @@ -176,7 +156,7 @@ func (e *ECDHSession) deriveSymmetricKey(sharedSecret []byte, peerID string) []b } //TODO: Salt can be nil or a random value; here we use nil for simplicity var salt []byte - + // Create an HKDF instance hkdf := hkdf.New(hash, sharedSecret, salt, info) @@ -189,14 +169,3 @@ func (e *ECDHSession) deriveSymmetricKey(sharedSecret []byte, peerID string) []b } return symmetricKey } - -func (e *ECDHSession) WaitForCompletion() error { - select { - case <-e.exchangeComplete: - return nil - case err := <-e.errCh: - return err - case <-time.After(30 * time.Second): - return fmt.Errorf("ECDH key exchange timeout") - } -} \ No newline at end of file diff --git a/pkg/mpc/node.go b/pkg/mpc/node.go index 4daf65d..df2857d 100644 --- a/pkg/mpc/node.go +++ b/pkg/mpc/node.go @@ -7,6 +7,7 @@ import ( "math/big" "slices" "strconv" + "strings" "time" "github.com/bnb-chain/tss-lib/v2/ecdsa/keygen" @@ -43,12 +44,17 @@ type Node struct { identityStore identity.Store peerRegistry PeerRegistry + dhSession *ECDHSession } func PartyIDToRoutingDest(partyID *tss.PartyID) string { return string(partyID.KeyInt().Bytes()) } +func PartyIDToNodeID(partyID *tss.PartyID) string { + return strings.Split(string(partyID.KeyInt().Bytes()), ":")[0] +} + func ComparePartyIDs(x, y *tss.PartyID) bool { return bytes.Equal(x.KeyInt().Bytes(), y.KeyInt().Bytes()) } @@ -71,6 +77,12 @@ func NewNode( elapsed := time.Since(start) logger.Info("Starting new node, preparams is generated successfully!", "elapsed", elapsed.Milliseconds()) + //each node initiates the DH key exchange listener at the beginning and invoke message sending when all peers are ready + dhSession := NewECDHSession(nodeID, peerIDs, pubSub, identityStore) + if err := dhSession.StartKeyExchange(); err != nil { + logger.Fatal("Failed to start DH key exchange", err) + } + node := &Node{ nodeID: nodeID, peerIDs: peerIDs, @@ -80,10 +92,17 @@ func NewNode( keyinfoStore: keyinfoStore, peerRegistry: peerRegistry, identityStore: identityStore, + dhSession: dhSession, } node.ecdsaPreParams = node.generatePreParams() - go peerRegistry.WatchPeersReady() + initTasks := func() { + if err := dhSession.BroadcastPublicKey(); err != nil { + logger.Fatal("DH key broadcast failed", err) + } + } + + go peerRegistry.WatchPeersReady(initTasks) return node } diff --git a/pkg/mpc/registry.go b/pkg/mpc/registry.go index 688a306..59f911d 100644 --- a/pkg/mpc/registry.go +++ b/pkg/mpc/registry.go @@ -19,7 +19,7 @@ const ( type PeerRegistry interface { Ready() error ArePeersReady() bool - WatchPeersReady() + WatchPeersReady(callback func()) // Resign is called by the node when it is going to shutdown Resign() error GetReadyPeersCount() int64 @@ -66,7 +66,7 @@ func (r *registry) readyKey(nodeID string) string { return fmt.Sprintf("ready/%s", nodeID) } -func (r *registry) registerReadyPairs(peerIDs []string) { +func (r *registry) registerReadyPairs(peerIDs []string, callback func()) { for _, peerID := range peerIDs { ready, exist := r.readyMap[peerID] if !exist { @@ -85,6 +85,8 @@ func (r *registry) registerReadyPairs(peerIDs []string) { r.ready = true r.mu.Unlock() logger.Info("ALL PEERS ARE READY! Starting to accept MPC requests") + + time.AfterFunc(10*time.Second, callback) } } @@ -107,7 +109,7 @@ func (r *registry) Ready() error { return nil } -func (r *registry) WatchPeersReady() { +func (r *registry) WatchPeersReady(callback func()) { ticker := time.NewTicker(ReadinessCheckPeriod) go r.logReadyStatus() // first tick is executed immediately @@ -141,7 +143,7 @@ func (r *registry) WatchPeersReady() { } } - r.registerReadyPairs(newReadyPeerIDs) + r.registerReadyPairs(newReadyPeerIDs, callback) } } diff --git a/pkg/mpc/session.go b/pkg/mpc/session.go index 0e0e682..dc00807 100644 --- a/pkg/mpc/session.go +++ b/pkg/mpc/session.go @@ -3,6 +3,7 @@ package mpc import ( "encoding/json" "fmt" + "strings" "sync" "github.com/bnb-chain/tss-lib/v2/ecdsa/keygen" @@ -34,7 +35,7 @@ var ( type TopicComposer struct { ComposeBroadcastTopic func() string - ComposeDirectTopic func(nodeID string) string + ComposeDirectTopic func(fromRouteID string, toRouteID string) string } type KeyComposerFn func(id string) string @@ -45,9 +46,10 @@ type Session interface { } type session struct { - walletID string - pubSub messaging.PubSub - direct messaging.DirectMessaging + walletID string + pubSub messaging.PubSub + direct messaging.DirectMessaging + threshold int participantPeerIDs []string selfPartyID *tss.PartyID @@ -59,11 +61,12 @@ type session struct { version int // preParams is nil for EDDSA session - preParams *keygen.LocalPreParams - kvstore kvstore.KVStore - keyinfoStore keyinfo.Store - broadcastSub messaging.Subscription - directSub messaging.Subscription + preParams *keygen.LocalPreParams + kvstore kvstore.KVStore + keyinfoStore keyinfo.Store + broadcastSub messaging.Subscription + directSubs []messaging.Subscription + resultQueue messaging.MessageQueue identityStore identity.Store @@ -88,7 +91,7 @@ func (s *session) PartyCount() int { return len(s.partyIDs) } -//TODO: Does AEAD encryption for each message so NATs server learns nothing +// update: use AEAD encryption for each message so NATs server learns nothing func (s *session) handleTssMessage(keyshare tss.Message) { data, routing, err := keyshare.WireBytes() if err != nil { @@ -97,56 +100,100 @@ func (s *session) handleTssMessage(keyshare tss.Message) { } tssMsg := types.NewTssMessage(s.walletID, data, routing.IsBroadcast, routing.From, routing.To) - signature, err := s.identityStore.SignMessage(&tssMsg) - if err != nil { - s.ErrCh <- fmt.Errorf("failed to sign message: %w", err) - return - } - tssMsg.Signature = signature - msg, err := types.MarshalTssMessage(&tssMsg) - if err != nil { - s.ErrCh <- fmt.Errorf("failed to marshal tss message: %w", err) - return - } + toIDs := make([]string, len(routing.To)) for i, id := range routing.To { toIDs[i] = id.String() } logger.Debug(fmt.Sprintf("%s Sending message", s.sessionType), "from", s.selfPartyID.String(), "to", toIDs, "isBroadcast", routing.IsBroadcast) + + //Note: Differentiate broadcast and p2p messages + + //Broadcast message if routing.IsBroadcast && len(routing.To) == 0 { - err := s.pubSub.Publish(s.topicComposer.ComposeBroadcastTopic(), msg) + //attach signature + signature, err := s.identityStore.SignMessage(&tssMsg) + if err != nil { + s.ErrCh <- fmt.Errorf("failed to sign message: %w", err) + return + } + tssMsg.Signature = signature + msg, err := types.MarshalTssMessage(&tssMsg) + if err != nil { + s.ErrCh <- fmt.Errorf("failed to marshal tss message: %w", err) + return + } + + err = s.pubSub.Publish(s.topicComposer.ComposeBroadcastTopic(), msg) if err != nil { s.ErrCh <- err return } - } else { + } else { //p2p message + //without signature + msg, err := types.MarshalTssMessage(&tssMsg) + if err != nil { + s.ErrCh <- fmt.Errorf("failed to marshal tss message: %w", err) + return + } + + fromRouteID := PartyIDToRoutingDest(s.selfPartyID) + for _, to := range routing.To { - nodeID := PartyIDToRoutingDest(to) - topic := s.topicComposer.ComposeDirectTopic(nodeID) - err := s.direct.Send(topic, msg) + toRouteID := PartyIDToRoutingDest(to) + + cipher, err := s.identityStore.EncryptMessage(msg, PartyIDToNodeID(to)) + if err != nil { + logger.Error("AuthEncrypt Error: %w", err) + } + topic := s.topicComposer.ComposeDirectTopic(fromRouteID, toRouteID) + err = s.direct.Send(topic, cipher) if err != nil { logger.Error("Failed to send direct message to", err, "topic", topic) - s.ErrCh <- fmt.Errorf("Failed to send direct message to %s", topic) + s.ErrCh <- fmt.Errorf("failed to send direct message to %s", topic) } - } + } +} + +func (s *session) receiveP2PTssMessage(topic string, cipher []byte) { + senderID := extractSenderIdFromDirectTopic(topic) + + plaintext, err := s.identityStore.DecryptMessage(cipher, senderID) + + if err != nil { + s.ErrCh <- fmt.Errorf("failed to decrypt message: %w, tampered message", err) + return + } + msg, err := types.UnmarshalTssMessage(plaintext) + if err != nil { + s.ErrCh <- fmt.Errorf("failed to unmarshal message: %w", err) + return } + + s.receiveTssMessage(msg) } -//TODO: the logic of receiving message should be modified -func (s *session) receiveTssMessage(rawMsg []byte) { +func (s *session) receiveBroadcastTssMessage(rawMsg []byte) { + msg, err := types.UnmarshalTssMessage(rawMsg) if err != nil { - s.ErrCh <- fmt.Errorf("Failed to unmarshal message: %w", err) + s.ErrCh <- fmt.Errorf("failed to unmarshal message: %w", err) return } + err = s.identityStore.VerifyMessage(msg) if err != nil { s.ErrCh <- fmt.Errorf("Failed to verify message: %w, tampered message", err) return } + s.receiveTssMessage(msg) +} + +// update: the logic of receiving message should be modified +func (s *session) receiveTssMessage(msg *types.TssMessage) { toIDs := make([]string, len(msg.To)) for i, id := range msg.To { toIDs[i] = id.String() @@ -175,7 +222,6 @@ func (s *session) receiveTssMessage(rawMsg []byte) { logger.Error("Failed to update party", err, "walletID", s.walletID) return } - } } @@ -183,7 +229,7 @@ func (s *session) ListenToIncomingMessageAsync() { go func() { sub, err := s.pubSub.Subscribe(s.topicComposer.ComposeBroadcastTopic(), func(natMsg *nats.Msg) { msg := natMsg.Data - s.receiveTssMessage(msg) + s.receiveBroadcastTssMessage(msg) }) if err != nil { @@ -194,16 +240,23 @@ func (s *session) ListenToIncomingMessageAsync() { s.broadcastSub = sub }() - nodeID := PartyIDToRoutingDest(s.selfPartyID) - targetID := s.topicComposer.ComposeDirectTopic(nodeID) - sub, err := s.direct.Listen(targetID, func(msg []byte) { - go s.receiveTssMessage(msg) // async for avoid timeout - }) - if err != nil { - s.ErrCh <- fmt.Errorf("Failed to subscribe to direct topic %s: %w", targetID, err) + //subscribe all possible p2p messages from all other potential nodes + for _, fromId := range s.partyIDs { + if fromId != s.selfPartyID { + fromRouteID := PartyIDToRoutingDest(fromId) + toRouteID := PartyIDToRoutingDest(s.selfPartyID) + + topic := s.topicComposer.ComposeDirectTopic(fromRouteID, toRouteID) + //Note: need to pass send's id into the listening handler + sub, err := s.direct.Listen(topic, func(cipher []byte) { + go s.receiveP2PTssMessage(topic, cipher) // async for avoid timeout + }) + if err != nil { + s.ErrCh <- fmt.Errorf("Failed to subscribe to direct topic %s: %w", topic, err) + } + s.directSubs = append(s.directSubs, sub) + } } - s.directSub = sub - } func (s *session) Close() error { @@ -211,10 +264,14 @@ func (s *session) Close() error { if err != nil { return err } - err = s.directSub.Unsubscribe() - if err != nil { - return err + + for _, sub := range s.directSubs { + err = sub.Unsubscribe() + if err != nil { + return err + } } + return nil } @@ -267,3 +324,8 @@ func walletIDWithVersion(walletID string, version int) string { } return walletID } + +// according to current format of a topic, this is how we could extract the node ID of the sender +func extractSenderIdFromDirectTopic(topic string) string { + return strings.Split(topic, ":")[3] +} From 57c51dd1aa1344513bcfae2392593112ba27a28d Mon Sep 17 00:00:00 2001 From: athen Date: Wed, 6 Aug 2025 16:55:10 +0200 Subject: [PATCH 04/23] decrease delay --- pkg/mpc/registry.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/mpc/registry.go b/pkg/mpc/registry.go index 59f911d..5dd8bb7 100644 --- a/pkg/mpc/registry.go +++ b/pkg/mpc/registry.go @@ -86,7 +86,7 @@ func (r *registry) registerReadyPairs(peerIDs []string, callback func()) { r.mu.Unlock() logger.Info("ALL PEERS ARE READY! Starting to accept MPC requests") - time.AfterFunc(10*time.Second, callback) + time.AfterFunc(5*time.Second, callback) } } From 6d908f71bdededa3cdbe0f09a8d25528f69e8260 Mon Sep 17 00:00:00 2001 From: athen Date: Wed, 6 Aug 2025 17:08:08 +0200 Subject: [PATCH 05/23] suppress warning --- pkg/identity/identity.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/identity/identity.go b/pkg/identity/identity.go index dada327..aff2b18 100644 --- a/pkg/identity/identity.go +++ b/pkg/identity/identity.go @@ -317,7 +317,7 @@ func encryptAEAD(symmetricKey []byte, plaintext []byte) ([]byte, error) { return nil, fmt.Errorf("failed to create AES cipher: %w", err) } - // Generate a random 12-byte nonce + // Generate a random 12-byte nonce (not hardcoded, populated by crypto/rand) nonce := make([]byte, 12) if _, err := io.ReadFull(rand.Reader, nonce); err != nil { return nil, fmt.Errorf("failed to generate nonce: %w", err) From 071ac87ce936ccc0855bf47e9d842150e0447793 Mon Sep 17 00:00:00 2001 From: athen Date: Wed, 6 Aug 2025 17:21:07 +0200 Subject: [PATCH 06/23] increase delay for starting client --- e2e/reshare_test.go | 2 +- e2e/sign_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/e2e/reshare_test.go b/e2e/reshare_test.go index 86cf670..08ce09c 100644 --- a/e2e/reshare_test.go +++ b/e2e/reshare_test.go @@ -92,7 +92,7 @@ func testKeyGenerationForResharing(t *testing.T, suite *E2ETestSuite) { require.NoError(t, err, "Failed to setup keygen result listener") // Add a small delay to ensure the result listener is fully set up - time.Sleep(2 * time.Second) + time.Sleep(10 * time.Second) // Trigger key generation for all wallets for _, walletID := range walletIDs { diff --git a/e2e/sign_test.go b/e2e/sign_test.go index c5d454f..fe197c9 100644 --- a/e2e/sign_test.go +++ b/e2e/sign_test.go @@ -150,7 +150,7 @@ func testKeyGenerationForSigning(t *testing.T, suite *E2ETestSuite) { require.NoError(t, err, "Failed to setup keygen result listener") // Add a small delay to ensure the result listener is fully set up - time.Sleep(2 * time.Second) + time.Sleep(10 * time.Second) // Trigger key generation for all wallets for _, walletID := range walletIDs { From fa21efbc67624495fbc5edd12e0c71de1785e5d8 Mon Sep 17 00:00:00 2001 From: athen Date: Wed, 6 Aug 2025 17:48:50 +0200 Subject: [PATCH 07/23] increase delay in test case, supporess github security warning --- e2e/reshare_test.go | 4 ++-- pkg/identity/identity.go | 15 ++++++++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/e2e/reshare_test.go b/e2e/reshare_test.go index 08ce09c..f865e5d 100644 --- a/e2e/reshare_test.go +++ b/e2e/reshare_test.go @@ -179,7 +179,7 @@ func testResharingAllNodes(t *testing.T, suite *E2ETestSuite) { require.NoError(t, err, "Failed to setup resharing result listener") // Wait for listener setup - time.Sleep(2 * time.Second) + time.Sleep(10 * time.Second) // Test resharing for both key types for i, walletID := range suite.walletIDs { @@ -360,7 +360,7 @@ func testSigningAfterResharing(t *testing.T, suite *E2ETestSuite) { require.NoError(t, err, "Failed to setup signing result listener") // Wait for listener setup - time.Sleep(2 * time.Second) + time.Sleep(10 * time.Second) // Test messages to sign testMessages := []string{ diff --git a/pkg/identity/identity.go b/pkg/identity/identity.go index aff2b18..a199967 100644 --- a/pkg/identity/identity.go +++ b/pkg/identity/identity.go @@ -309,6 +309,14 @@ func (s *fileStore) VerifyMessage(msg *types.TssMessage) error { return nil } +func generateNonce(nonceSize int) ([]byte, error) { + nonce := make([]byte, nonceSize) + if _, err := rand.Read(nonce); err != nil { + return nil, err + } + return nonce, nil +} + // encryptAEAD encrypts plaintext using AES-GCM with authentication. func encryptAEAD(symmetricKey []byte, plaintext []byte) ([]byte, error) { // Create AES cipher block @@ -318,8 +326,10 @@ func encryptAEAD(symmetricKey []byte, plaintext []byte) ([]byte, error) { } // Generate a random 12-byte nonce (not hardcoded, populated by crypto/rand) - nonce := make([]byte, 12) - if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + // nonce := make([]byte, 12) + + nonce, err := generateNonce(12) + if err != nil { return nil, fmt.Errorf("failed to generate nonce: %w", err) } @@ -329,7 +339,6 @@ func encryptAEAD(symmetricKey []byte, plaintext []byte) ([]byte, error) { return nil, fmt.Errorf("failed to create GCM: %w", err) } - // Encrypt with no additional data (nil) ciphertext := aead.Seal(nil, nonce, plaintext, nil) // Prepend nonce to ciphertext for decryption From 0f69eaf95249b32843664ee23221ece89c399e0e Mon Sep 17 00:00:00 2001 From: athen Date: Sun, 10 Aug 2025 16:22:40 +0200 Subject: [PATCH 08/23] fix p2p feature correctness --- cmd/mpcium/main.go | 1 + pkg/identity/identity.go | 73 +++++++++------- pkg/messaging/point2point.go | 7 +- pkg/mpc/ecdsa_keygen_session.go | 4 +- pkg/mpc/ecdsa_resharing_session.go | 42 ++++++++- pkg/mpc/ecdsa_signing_session.go | 4 +- pkg/mpc/eddsa_keygen_session.go | 4 +- pkg/mpc/eddsa_resharing_session.go | 42 +++++++-- pkg/mpc/eddsa_signing_session.go | 6 +- pkg/mpc/key_exchange_session.go | 131 ++++++++++++++--------------- pkg/mpc/node.go | 6 +- pkg/mpc/session.go | 72 ++++++++++------ pkg/types/tss.go | 23 ++--- 13 files changed, 253 insertions(+), 162 deletions(-) diff --git a/cmd/mpcium/main.go b/cmd/mpcium/main.go index 803c593..7d34e85 100644 --- a/cmd/mpcium/main.go +++ b/cmd/mpcium/main.go @@ -221,6 +221,7 @@ func runNode(ctx context.Context, c *cli.Command) error { } }() + //TODO: I think it makes more sense to start these consumers only after P2P channel were succesfully built var wg sync.WaitGroup errChan := make(chan error, 2) diff --git a/pkg/identity/identity.go b/pkg/identity/identity.go index a199967..52e49a5 100644 --- a/pkg/identity/identity.go +++ b/pkg/identity/identity.go @@ -25,6 +25,9 @@ import ( "github.com/spf13/viper" ) +const AES_GCM_Nonce_Size = 12 +const AES_SYMMETRICKEY_Size = 32 + // NodeIdentity represents a node's identity information type NodeIdentity struct { NodeName string `json:"node_name"` @@ -41,12 +44,12 @@ type Store interface { SignMessage(msg *types.TssMessage) ([]byte, error) VerifyMessage(msg *types.TssMessage) error - // New ECDH methods SignEcdhMessage(msg *types.ECDHMessage) ([]byte, error) VerifySignature(msg *types.ECDHMessage) error SetSymmetricKey(peerID string, key []byte) GetSymmetricKey(peerID string) ([]byte, error) + CheckSymmetricKeyComplete(desired int) bool EncryptMessage(plaintext []byte, peerID string) ([]byte, error) DecryptMessage(cipher []byte, peerID string) ([]byte, error) @@ -69,19 +72,6 @@ type fileStore struct { symmetricKeys map[string][]byte } -// ECDHStore implements the Store interface with ECDH -// type ECDHStore struct { -// symmetricKeys map[string][]byte -// mu sync.RWMutex -// } - -// // initializes a new ECDHStore. -// func NewECDHStore() *ECDHStore { -// return &ECDHStore{ -// symmetricKeys: make(map[string][]byte), -// } -// } - // NewFileStore creates a new identity store func NewFileStore(identityDir, nodeName string, decrypt bool) (*fileStore, error) { if err := os.MkdirAll(identityDir, 0750); err != nil { @@ -120,14 +110,26 @@ func NewFileStore(identityDir, nodeName string, decrypt bool) (*fileStore, error return nil, fmt.Errorf("failed to parse peers.json: %w", err) } + mNodeId, exists := peers[nodeName] + if !exists { + return nil, fmt.Errorf("cannot find nodeID in peers.json for", nodeName) + } + + initSymmetricKeys := make(map[string][]byte) + // Generate a random 32-byte symmetric key (suitable for AES-256) + tmpKey, err := generateRandom(AES_SYMMETRICKEY_Size) + if err != nil { + return nil, fmt.Errorf("failed to generate random symmetric key:", err) + } + initSymmetricKeys[mNodeId] = tmpKey + store := &fileStore{ identityDir: identityDir, currentNodeName: nodeName, publicKeys: make(map[string][]byte), privateKey: privateKey, initiatorPubKey: initiatorPubKey, - - symmetricKeys: make(map[string][]byte), + symmetricKeys: initSymmetricKeys, } // Check that each node in peers.json has an identity file @@ -257,6 +259,10 @@ func (s *fileStore) GetSymmetricKey(peerID string) ([]byte, error) { return nil, fmt.Errorf("SymmetricKey key not found for node ID: %s", peerID) } +func (s *fileStore) CheckSymmetricKeyComplete(desired int) bool { + return len(s.symmetricKeys) == desired +} + // GetPublicKey retrieves a node's public key by its ID func (s *fileStore) GetPublicKey(nodeID string) ([]byte, error) { s.mu.RLock() @@ -309,7 +315,7 @@ func (s *fileStore) VerifyMessage(msg *types.TssMessage) error { return nil } -func generateNonce(nonceSize int) ([]byte, error) { +func generateRandom(nonceSize int) ([]byte, error) { nonce := make([]byte, nonceSize) if _, err := rand.Read(nonce); err != nil { return nil, err @@ -325,15 +331,11 @@ func encryptAEAD(symmetricKey []byte, plaintext []byte) ([]byte, error) { return nil, fmt.Errorf("failed to create AES cipher: %w", err) } - // Generate a random 12-byte nonce (not hardcoded, populated by crypto/rand) - // nonce := make([]byte, 12) - - nonce, err := generateNonce(12) + nonce, err := generateRandom(AES_GCM_Nonce_Size) if err != nil { return nil, fmt.Errorf("failed to generate nonce: %w", err) } - // Create AES-GCM cipher aead, err := cipher.NewGCM(block) if err != nil { return nil, fmt.Errorf("failed to create GCM: %w", err) @@ -341,30 +343,26 @@ func encryptAEAD(symmetricKey []byte, plaintext []byte) ([]byte, error) { ciphertext := aead.Seal(nil, nonce, plaintext, nil) - // Prepend nonce to ciphertext for decryption return append(nonce, ciphertext...), nil } // decryptAEAD decrypts ciphertext using AES-GCM with authentication. func decryptAEAD(symmetricKey []byte, ciphertext []byte) ([]byte, error) { - // Create AES cipher block block, err := aes.NewCipher(symmetricKey) if err != nil { return nil, fmt.Errorf("failed to create AES cipher: %w", err) } - // Create AES-GCM cipher aead, err := cipher.NewGCM(block) if err != nil { return nil, fmt.Errorf("failed to create GCM: %w", err) } - // Extract nonce (first 12 bytes) and ciphertext - if len(ciphertext) < 12 { + if len(ciphertext) < AES_GCM_Nonce_Size { return nil, errors.New("ciphertext too short") } - nonce := ciphertext[:12] - ciphertext = ciphertext[12:] + nonce := ciphertext[:AES_GCM_Nonce_Size] + ciphertext = ciphertext[AES_GCM_Nonce_Size:] // Decrypt with no additional data (nil) plaintext, err := aead.Open(nil, nonce, ciphertext, nil) @@ -377,17 +375,30 @@ func decryptAEAD(symmetricKey []byte, ciphertext []byte) ([]byte, error) { func (s *fileStore) EncryptMessage(plaintext []byte, peerID string) ([]byte, error) { key, err := s.GetSymmetricKey(peerID) - if err != nil || key == nil { + if err != nil { + return nil, err + } + + if key == nil { return nil, fmt.Errorf("no symmetric key for peer %s", peerID) } + return encryptAEAD(key, plaintext) } func (s *fileStore) DecryptMessage(cipher []byte, peerID string) ([]byte, error) { key, err := s.GetSymmetricKey(peerID) - if err != nil || key == nil { + + if err != nil { + return nil, err + } + + if key == nil { return nil, fmt.Errorf("no symmetric key for peer %s", peerID) } + + // logger.Info("check decryption key", "final", key) + return decryptAEAD(key, cipher) } diff --git a/pkg/messaging/point2point.go b/pkg/messaging/point2point.go index 665fbff..12514a2 100644 --- a/pkg/messaging/point2point.go +++ b/pkg/messaging/point2point.go @@ -24,8 +24,7 @@ func NewNatsDirectMessaging(natsConn *nats.Conn) DirectMessaging { } func (d *natsDirectMessaging) Send(topic string, message []byte) error { - var retryCount = 0 - err := retry.Do( + return retry.Do( func() error { _, err := d.natsConn.Request(topic, message, 3*time.Second) if err != nil { @@ -37,11 +36,9 @@ func (d *natsDirectMessaging) Send(topic string, message []byte) error { retry.Delay(50*time.Millisecond), retry.DelayType(retry.FixedDelay), retry.OnRetry(func(n uint, err error) { - logger.Error("Failed to send direct message message", err, "retryCount", retryCount) + logger.Error("Failed to send direct message", err, "attempt", n+1, "topic", topic) }), ) - - return err } func (d *natsDirectMessaging) Listen(topic string, handler func(data []byte)) (Subscription, error) { diff --git a/pkg/mpc/ecdsa_keygen_session.go b/pkg/mpc/ecdsa_keygen_session.go index 36f8a24..acbc497 100644 --- a/pkg/mpc/ecdsa_keygen_session.go +++ b/pkg/mpc/ecdsa_keygen_session.go @@ -61,8 +61,8 @@ func newECDSAKeygenSession( ComposeBroadcastTopic: func() string { return fmt.Sprintf("keygen:broadcast:ecdsa:%s", walletID) }, - ComposeDirectTopic: func(fromRouteID string, toRouteID string) string { - return fmt.Sprintf("keygen:direct:ecdsa:%s:%s:%s", fromRouteID, toRouteID, walletID) + ComposeDirectTopic: func(fromID string, toID string) string { + return fmt.Sprintf("keygen:direct:ecdsa:%s:%s:%s", fromID, toID, walletID) }, }, composeKey: func(walletID string) string { diff --git a/pkg/mpc/ecdsa_resharing_session.go b/pkg/mpc/ecdsa_resharing_session.go index 84c06da..c4b126d 100644 --- a/pkg/mpc/ecdsa_resharing_session.go +++ b/pkg/mpc/ecdsa_resharing_session.go @@ -21,11 +21,13 @@ type ReshareSession interface { Init() Reshare(done func()) GetPubKeyResult() []byte + GetExtraPeerIDs() []string } type ecdsaReshareSession struct { *session isNewParty bool + oldPeerIDs []string newPeerIDs []string reshareParams *tss.ReSharingParameters endCh chan *keygen.LocalPartySaveData @@ -50,6 +52,12 @@ func NewECDSAReshareSession( isNewParty bool, version int, ) *ecdsaReshareSession { + + realPartyIDs := oldPartyIDs + if isNewParty { + realPartyIDs = newPartyIDs + } + session := session{ walletID: walletID, pubSub: pubSub, @@ -57,7 +65,7 @@ func NewECDSAReshareSession( threshold: threshold, participantPeerIDs: participantPeerIDs, selfPartyID: selfID, - partyIDs: newPartyIDs, + partyIDs: realPartyIDs, outCh: make(chan tss.Message), ErrCh: make(chan error), preParams: preParams, @@ -68,8 +76,8 @@ func NewECDSAReshareSession( ComposeBroadcastTopic: func() string { return fmt.Sprintf("resharing:broadcast:ecdsa:%s", walletID) }, - ComposeDirectTopic: func(fromRouteID string, toRouteID string) string { - return fmt.Sprintf("resharing:direct:ecdsa:%s:%s:%s", fromRouteID, toRouteID, walletID) + ComposeDirectTopic: func(fromID string, toID string) string { + return fmt.Sprintf("resharing:direct:ecdsa:%s:%s:%s", fromID, toID, walletID) }, }, composeKey: func(walletID string) string { @@ -90,17 +98,43 @@ func NewECDSAReshareSession( len(newPartyIDs), newThreshold, ) + + var oldPeerIDs []string + for _, partyId := range oldPartyIDs { + oldPeerIDs = append(oldPeerIDs, PartyIDToNodeID(partyId)) + } + return &ecdsaReshareSession{ session: &session, reshareParams: reshareParams, isNewParty: isNewParty, + oldPeerIDs: oldPeerIDs, newPeerIDs: newPeerIDs, endCh: make(chan *keygen.LocalPartySaveData), } } +func (s *ecdsaReshareSession) GetExtraPeerIDs() []string { + // difference returns elements in A that are not in B. + difference := func(A, B []string) []string { + seen := make(map[string]bool) + for _, b := range B { + seen[b] = true + } + var result []string + for _, a := range A { + if !seen[a] { + result = append(result, a) + } + } + return result + } + + return difference(s.oldPeerIDs, s.newPeerIDs) +} + func (s *ecdsaReshareSession) Init() { - logger.Infof("Initializing resharing session with partyID: %s, newPartyIDs %s", s.selfPartyID, s.partyIDs) + logger.Infof("Initializing ecdsa resharing session with partyID: %s, newPartyIDs %s", s.selfPartyID, s.partyIDs) var share keygen.LocalPartySaveData if s.isNewParty { diff --git a/pkg/mpc/ecdsa_signing_session.go b/pkg/mpc/ecdsa_signing_session.go index ee792b8..d8fc3b5 100644 --- a/pkg/mpc/ecdsa_signing_session.go +++ b/pkg/mpc/ecdsa_signing_session.go @@ -72,8 +72,8 @@ func newECDSASigningSession( ComposeBroadcastTopic: func() string { return fmt.Sprintf("sign:ecdsa:broadcast:%s:%s", walletID, txID) }, - ComposeDirectTopic: func(fromRouteID string, toRouteID string) string { - return fmt.Sprintf("sign:ecdsa:direct:%s:%s:%s", fromRouteID, toRouteID, txID) + ComposeDirectTopic: func(fromID string, toID string) string { + return fmt.Sprintf("sign:ecdsa:direct:%s:%s:%s", fromID, toID, txID) }, }, composeKey: func(waleltID string) string { diff --git a/pkg/mpc/eddsa_keygen_session.go b/pkg/mpc/eddsa_keygen_session.go index 9855f13..fd832e3 100644 --- a/pkg/mpc/eddsa_keygen_session.go +++ b/pkg/mpc/eddsa_keygen_session.go @@ -49,8 +49,8 @@ func newEDDSAKeygenSession( ComposeBroadcastTopic: func() string { return fmt.Sprintf("keygen:broadcast:eddsa:%s", walletID) }, - ComposeDirectTopic: func(fromRouteID string, toRouteID string) string { - return fmt.Sprintf("keygen:direct:eddsa:%s:%s:%s", fromRouteID, toRouteID, walletID) + ComposeDirectTopic: func(fromID string, toID string) string { + return fmt.Sprintf("keygen:direct:eddsa:%s:%s:%s", fromID, toID, walletID) }, }, composeKey: func(waleltID string) string { diff --git a/pkg/mpc/eddsa_resharing_session.go b/pkg/mpc/eddsa_resharing_session.go index c2e206e..607f97f 100644 --- a/pkg/mpc/eddsa_resharing_session.go +++ b/pkg/mpc/eddsa_resharing_session.go @@ -18,6 +18,7 @@ import ( type eddsaReshareSession struct { *session isNewParty bool + oldPeerIDs []string newPeerIDs []string reshareParams *tss.ReSharingParameters endCh chan *keygen.LocalPartySaveData @@ -41,6 +42,12 @@ func NewEDDSAReshareSession( isNewParty bool, version int, ) *eddsaReshareSession { + + realPartyIDs := oldPartyIDs + if isNewParty { + realPartyIDs = newPartyIDs + } + session := session{ walletID: walletID, pubSub: pubSub, @@ -49,7 +56,7 @@ func NewEDDSAReshareSession( version: version, participantPeerIDs: participantPeerIDs, selfPartyID: selfID, - partyIDs: newPartyIDs, + partyIDs: realPartyIDs, outCh: make(chan tss.Message), ErrCh: make(chan error), kvstore: kvstore, @@ -58,8 +65,8 @@ func NewEDDSAReshareSession( ComposeBroadcastTopic: func() string { return fmt.Sprintf("reshare:broadcast:eddsa:%s", walletID) }, - ComposeDirectTopic: func(fromRouteID string, toRouteID string) string { - return fmt.Sprintf("reshare:direct:eddsa:%s:%s:%s", fromRouteID, toRouteID, walletID) + ComposeDirectTopic: func(fromID string, toID string) string { + return fmt.Sprintf("reshare:direct:eddsa:%s:%s:%s", fromID, toID, walletID) }, }, composeKey: func(walletID string) string { @@ -82,17 +89,42 @@ func NewEDDSAReshareSession( newThreshold, ) + var oldPeerIDs []string + for _, partyId := range oldPartyIDs { + oldPeerIDs = append(oldPeerIDs, PartyIDToNodeID(partyId)) + } + return &eddsaReshareSession{ session: &session, reshareParams: reshareParams, isNewParty: isNewParty, + oldPeerIDs: oldPeerIDs, newPeerIDs: newPeerIDs, endCh: make(chan *keygen.LocalPartySaveData), } } +func (s *eddsaReshareSession) GetExtraPeerIDs() []string { + // difference returns elements in A that are not in B. + difference := func(A, B []string) []string { + seen := make(map[string]bool) + for _, b := range B { + seen[b] = true + } + var result []string + for _, a := range A { + if !seen[a] { + result = append(result, a) + } + } + return result + } + + return difference(s.oldPeerIDs, s.newPeerIDs) +} + func (s *eddsaReshareSession) Init() { - logger.Infof("Initializing resharing session with partyID: %s, peerIDs %s", s.selfPartyID, s.partyIDs) + logger.Infof("Initializing eddsa resharing session with partyID: %s, peerIDs %s", s.selfPartyID, s.partyIDs) var share keygen.LocalPartySaveData if s.isNewParty { // Initialize empty share data for new party @@ -105,7 +137,7 @@ func (s *eddsaReshareSession) Init() { } } s.party = resharing.NewLocalParty(s.reshareParams, share, s.outCh, s.endCh) - logger.Infof("[INITIALIZED] Initialized resharing session successfully partyID: %s, peerIDs %s, walletID %s, oldThreshold = %d, newThreshold = %d", + logger.Infof("[INITIALIZED] Initialized eddsa resharing session successfully partyID: %s, peerIDs %s, walletID %s, oldThreshold = %d, newThreshold = %d", s.selfPartyID, s.partyIDs, s.walletID, s.threshold, s.reshareParams.NewThreshold()) } diff --git a/pkg/mpc/eddsa_signing_session.go b/pkg/mpc/eddsa_signing_session.go index 123cd33..d70b242 100644 --- a/pkg/mpc/eddsa_signing_session.go +++ b/pkg/mpc/eddsa_signing_session.go @@ -63,8 +63,8 @@ func newEDDSASigningSession( ComposeBroadcastTopic: func() string { return fmt.Sprintf("sign:eddsa:broadcast:%s:%s", walletID, txID) }, - ComposeDirectTopic: func(fromRouteID string, toRouteID string) string { - return fmt.Sprintf("sign:eddsa:direct:%s:%s:%s", fromRouteID, toRouteID, txID) + ComposeDirectTopic: func(fromID string, toID string) string { + return fmt.Sprintf("sign:eddsa:direct:%s:%s:%s", fromID, toID, txID) }, }, composeKey: func(waleltID string) string { @@ -73,7 +73,7 @@ func newEDDSASigningSession( getRoundFunc: GetEddsaMsgRound, resultQueue: resultQueue, identityStore: identityStore, - idempotentKey: idempotentKey, + idempotentKey: idempotentKey, }, endCh: make(chan *common.SignatureData), txID: txID, diff --git a/pkg/mpc/key_exchange_session.go b/pkg/mpc/key_exchange_session.go index a766031..19bf6ce 100644 --- a/pkg/mpc/key_exchange_session.go +++ b/pkg/mpc/key_exchange_session.go @@ -1,4 +1,3 @@ -// pkg/mpc/ecdh_session.go package mpc import ( @@ -21,20 +20,19 @@ import ( "github.com/nats-io/nats.go" ) -type ECDHSession struct { - nodeID string - peerIDs []string - - pubSub messaging.PubSub - - ecdhSub messaging.Subscription - - identityStore identity.Store - symmetricKeys map[string][]byte // peerID -> symmetric key - - privateKey *ecdh.PrivateKey - publicKey *ecdh.PublicKey +type ECDHSession interface { + StartKeyExchange() error + BroadcastPublicKey() error +} +type ecdhSession struct { + nodeID string + peerIDs []string + pubSub messaging.PubSub + ecdhSub messaging.Subscription + identityStore identity.Store + privateKey *ecdh.PrivateKey + publicKey *ecdh.PublicKey exchangeComplete chan struct{} errCh chan error } @@ -44,19 +42,18 @@ func NewECDHSession( peerIDs []string, pubSub messaging.PubSub, identityStore identity.Store, -) *ECDHSession { - return &ECDHSession{ - nodeID: nodeID, - peerIDs: peerIDs, - pubSub: pubSub, - identityStore: identityStore, - // symmetricKeys: make(map[string][]byte), +) *ecdhSession { + return &ecdhSession{ + nodeID: nodeID, + peerIDs: peerIDs, + pubSub: pubSub, + identityStore: identityStore, exchangeComplete: make(chan struct{}), errCh: make(chan error), } } -func (e *ECDHSession) StartKeyExchange() error { +func (e *ecdhSession) StartKeyExchange() error { // Generate an ephemeral ECDH key pair privateKey, err := ecdh.P256().GenerateKey(rand.Reader) if err != nil { @@ -66,13 +63,17 @@ func (e *ECDHSession) StartKeyExchange() error { e.privateKey = privateKey e.publicKey = privateKey.PublicKey() - //Subscribe to ECDH messages - sub, err := e.pubSub.Subscribe(fmt.Sprintf("ecdh:exchange:%s", e.nodeID), func(natMsg *nats.Msg) { + // Subscribe to ECDH broadcast + sub, err := e.pubSub.Subscribe("ecdh:exchange", func(natMsg *nats.Msg) { var ecdhMsg types.ECDHMessage if err := json.Unmarshal(natMsg.Data, &ecdhMsg); err != nil { return } + if ecdhMsg.From == e.nodeID { + return + } + logger.Info("Received ECDH message from", "node", ecdhMsg.From) //TODO: consider how to avoid replay attack @@ -97,12 +98,12 @@ func (e *ECDHSession) StartKeyExchange() error { symmetricKey := e.deriveSymmetricKey(sharedSecret, ecdhMsg.From) e.identityStore.SetSymmetricKey(ecdhMsg.From, symmetricKey) - //Check if exchange is complete - // if len(e.identityStore.symmetricKeys) == len(e.peerIDs)-1 { - // logger.Info("Finished ECDH Key Exchange") - // close(e.exchangeComplete) - // return - // } + requiredKeyCount := len(e.peerIDs) + + if e.identityStore.CheckSymmetricKeyComplete(requiredKeyCount) { + logger.Info("Completed ECDH!", "symmetricKeyAmount", requiredKeyCount) + logger.Info("PEER IS READY! Starting to accept MPC requests") + } }) e.ecdhSub = sub @@ -112,60 +113,56 @@ func (e *ECDHSession) StartKeyExchange() error { return nil } -func (e *ECDHSession) BroadcastPublicKey() error { +func (e *ecdhSession) BroadcastPublicKey() error { publicKeyBytes := e.publicKey.Bytes() - for _, peerID := range e.peerIDs { - if peerID != e.nodeID { - msg := types.ECDHMessage{ - From: e.nodeID, - To: peerID, - PublicKey: publicKeyBytes, - Timestamp: time.Now(), - } - //Sign the message using existing identity store - signature, err := e.identityStore.SignEcdhMessage(&msg) - if err != nil { - return fmt.Errorf("failed to sign ECDH message: %w", err) - } - msg.Signature = signature - signedMsgBytes, _ := json.Marshal(msg) - - if err := e.pubSub.Publish(fmt.Sprintf("ecdh:exchange:%s", peerID), signedMsgBytes); err != nil { - return fmt.Errorf("failed to send public DH message to %s: %w", peerID, err) - } - } + + msg := types.ECDHMessage{ + From: e.nodeID, + PublicKey: publicKeyBytes, + Timestamp: time.Now(), + } + //Sign the message using existing identity store + signature, err := e.identityStore.SignEcdhMessage(&msg) + if err != nil { + return fmt.Errorf("failed to sign ECDH message: %w", err) + } + msg.Signature = signature + signedMsgBytes, _ := json.Marshal(msg) + + logger.Info("Starting to broadcast DH key") + + if err := e.pubSub.Publish("ecdh:exchange", signedMsgBytes); err != nil { + return fmt.Errorf("%s failed to publish DH message because %w", e.nodeID, err) } + return nil } -func (e *ECDHSession) GetSymmetricKey(peerID string) ([]byte, bool) { - key, exists := e.symmetricKeys[peerID] - return key, exists +func deriveConsistentInfo(a, b string) []byte { + if a < b { + return []byte(a + b) + } + return []byte(b + a) } // derives a symmetric key from the shared secret and peer ID using HKDF. -func (e *ECDHSession) deriveSymmetricKey(sharedSecret []byte, peerID string) []byte { - // Use SHA256 as the hash function for HKDF +func (e *ecdhSession) deriveSymmetricKey(sharedSecret []byte, peerID string) []byte { hash := sha256.New - // Info parameter can include context-specific data; here we use the peerID - var info []byte - if e.nodeID < peerID { - info = []byte(e.nodeID + peerID) - } else { - info = []byte(peerID + e.nodeID) - } - //TODO: Salt can be nil or a random value; here we use nil for simplicity + + // Info parameter can include context-specific data; here we use a pair of party IDs + info := deriveConsistentInfo(e.nodeID, peerID) + + // Salt can be nil or a random value; here we use nil var salt []byte - // Create an HKDF instance hkdf := hkdf.New(hash, sharedSecret, salt, info) // Derive a 32-byte symmetric key (suitable for AES-256) symmetricKey := make([]byte, 32) _, err := hkdf.Read(symmetricKey) if err != nil { - // In a production environment, handle this error appropriately - panic(err) // Simplified for example; replace with proper error handling + e.errCh <- err + return nil } return symmetricKey } diff --git a/pkg/mpc/node.go b/pkg/mpc/node.go index a7a3d18..f6da00c 100644 --- a/pkg/mpc/node.go +++ b/pkg/mpc/node.go @@ -44,7 +44,7 @@ type Node struct { identityStore identity.Store peerRegistry PeerRegistry - dhSession *ECDHSession + dhSession *ecdhSession } func PartyIDToRoutingDest(partyID *tss.PartyID) string { @@ -96,13 +96,13 @@ func NewNode( } node.ecdsaPreParams = node.generatePreParams() - initTasks := func() { + ecdhTask := func() { if err := dhSession.BroadcastPublicKey(); err != nil { logger.Fatal("DH key broadcast failed", err) } } - go peerRegistry.WatchPeersReady(initTasks) + go peerRegistry.WatchPeersReady(ecdhTask) return node } diff --git a/pkg/mpc/session.go b/pkg/mpc/session.go index d990892..7e88277 100644 --- a/pkg/mpc/session.go +++ b/pkg/mpc/session.go @@ -36,13 +36,14 @@ var ( type TopicComposer struct { ComposeBroadcastTopic func() string - ComposeDirectTopic func(fromRouteID string, toRouteID string) string + ComposeDirectTopic func(fromID string, toID string) string } type KeyComposerFn func(id string) string type Session interface { ListenToIncomingMessageAsync() + ListenAsyncWithExtra(extraIDs []string) ErrChan() <-chan error } @@ -139,16 +140,14 @@ func (s *session) handleTssMessage(keyshare tss.Message) { return } - fromRouteID := PartyIDToRoutingDest(s.selfPartyID) - + fromID := PartyIDToNodeID(s.selfPartyID) for _, to := range routing.To { - toRouteID := PartyIDToRoutingDest(to) - - cipher, err := s.identityStore.EncryptMessage(msg, PartyIDToNodeID(to)) + toNodeID := PartyIDToNodeID(to) + cipher, err := s.identityStore.EncryptMessage(msg, toNodeID) if err != nil { logger.Error("AuthEncrypt Error: %w", err) } - topic := s.topicComposer.ComposeDirectTopic(fromRouteID, toRouteID) + topic := s.topicComposer.ComposeDirectTopic(fromID, toNodeID) err = s.direct.Send(topic, cipher) if err != nil { logger.Error("Failed to send direct message to", err, "topic", topic) @@ -161,6 +160,12 @@ func (s *session) handleTssMessage(keyshare tss.Message) { func (s *session) receiveP2PTssMessage(topic string, cipher []byte) { senderID := extractSenderIdFromDirectTopic(topic) + if senderID == "" { + s.ErrCh <- fmt.Errorf("failed to extract senderID from direct topic: the direct topic format is wrong") + return + } + + // logger.Info("Debug werid crash message", "topic: ", topic) plaintext, err := s.identityStore.DecryptMessage(cipher, senderID) if err != nil { @@ -242,22 +247,35 @@ func (s *session) ListenToIncomingMessageAsync() { s.broadcastSub = sub }() - //subscribe all possible p2p messages from all other potential nodes - for _, fromId := range s.partyIDs { - if fromId != s.selfPartyID { - fromRouteID := PartyIDToRoutingDest(fromId) - toRouteID := PartyIDToRoutingDest(s.selfPartyID) - - topic := s.topicComposer.ComposeDirectTopic(fromRouteID, toRouteID) - //Note: need to pass send's id into the listening handler - sub, err := s.direct.Listen(topic, func(cipher []byte) { - go s.receiveP2PTssMessage(topic, cipher) // async for avoid timeout - }) - if err != nil { - s.ErrCh <- fmt.Errorf("Failed to subscribe to direct topic %s: %w", topic, err) - } - s.directSubs = append(s.directSubs, sub) + //subscribe all possible p2p messages from all nodes, due to the nature of tss-lib, this even includes oneself + toID := PartyIDToNodeID(s.selfPartyID) + for _, fromPartyId := range s.partyIDs { + fromID := PartyIDToNodeID(fromPartyId) + logger.Info("Check-listening", "id", fromID) + topic := s.topicComposer.ComposeDirectTopic(fromID, toID) + sub, err := s.direct.Listen(topic, func(cipher []byte) { + go s.receiveP2PTssMessage(topic, cipher) // async for avoid timeout + }) + if err != nil { + s.ErrCh <- fmt.Errorf("Failed to subscribe to direct topic %s: %w", topic, err) } + + s.directSubs = append(s.directSubs, sub) + } +} + +func (s *session) ListenAsyncWithExtra(extraIDs []string) { + //subscribe potential p2p messages from addtional nodes (just for resharing) + toID := PartyIDToNodeID(s.selfPartyID) + for _, fromId := range extraIDs { + topic := s.topicComposer.ComposeDirectTopic(fromId, toID) + sub, err := s.direct.Listen(topic, func(cipher []byte) { + go s.receiveP2PTssMessage(topic, cipher) // async for avoid timeout + }) + if err != nil { + s.ErrCh <- fmt.Errorf("Failed to subscribe to direct topic %s: %w", topic, err) + } + s.directSubs = append(s.directSubs, sub) } } @@ -327,7 +345,13 @@ func walletIDWithVersion(walletID string, version int) string { return walletID } -// according to current format of a topic, this is how we could extract the node ID of the sender func extractSenderIdFromDirectTopic(topic string) string { - return strings.Split(topic, ":")[3] + strs := strings.Split(topic, ":") + + // according to direct topic format, there will be 6 slices, senderId is the 4th one + if len(strs) == 6 { + return strs[3] + } + + return "" } diff --git a/pkg/types/tss.go b/pkg/types/tss.go index 7df91cc..3b118e4 100644 --- a/pkg/types/tss.go +++ b/pkg/types/tss.go @@ -5,7 +5,7 @@ package types import ( "encoding/json" "sort" - "time" + "time" "github.com/bnb-chain/tss-lib/v2/tss" ) @@ -24,12 +24,10 @@ type TssMessage struct { } type ECDHMessage struct { - From string `json:"from"` - To string `json:"to"` - - PublicKey []byte `json:"public_key"` - Timestamp time.Time `json:"timestamp"` - Signature []byte `json:"signature"` + From string `json:"from"` + PublicKey []byte `json:"public_key"` + Timestamp time.Time `json:"timestamp"` + Signature []byte `json:"signature"` } func NewTssMessage( @@ -120,22 +118,19 @@ func UnmarshalStartMessage(msgBytes []byte) (*StartMessage, error) { return msg, nil } - // MarshalForSigning returns the deterministic JSON bytes for signing func (msg *ECDHMessage) MarshalForSigning() ([]byte, error) { // Create a map with ordered keys signingData := map[string]interface{}{ - "From": msg.From, - "To": msg.To, - "PublicKey": msg.PublicKey, - "Timestamp": msg.Timestamp, + "From": msg.From, + "PublicKey": msg.PublicKey, + "Timestamp": msg.Timestamp, } // Use json.Marshal with sorted keys return json.Marshal(signingData) } - // MarshalForSigning returns the deterministic JSON bytes for signing func (msg *TssMessage) MarshalForSigning() ([]byte, error) { // Create a map with ordered keys @@ -161,4 +156,4 @@ func getPartyIDs(parties []*tss.PartyID) []string { } sort.Strings(ids) // Ensure deterministic order return ids -} \ No newline at end of file +} From 94838c32c456bd297dd86ba3e31162b71d3d54da Mon Sep 17 00:00:00 2001 From: athen Date: Sun, 10 Aug 2025 16:28:48 +0200 Subject: [PATCH 09/23] fix p2p feature --- pkg/eventconsumer/event_consumer.go | 25 ++++++++++++++++++------- pkg/mpc/registry.go | 2 -- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/pkg/eventconsumer/event_consumer.go b/pkg/eventconsumer/event_consumer.go index 59e2b92..d2e52f7 100644 --- a/pkg/eventconsumer/event_consumer.go +++ b/pkg/eventconsumer/event_consumer.go @@ -636,15 +636,27 @@ func (ec *eventConsumer) consumeReshareEvent() error { ResultType: event.ResultTypeSuccess, } - var wg sync.WaitGroup - ctx := context.Background() + if oldSession != nil { + oldSession.Init() + logger.Info("Check-listening", "start", "oldSession") + oldSession.ListenToIncomingMessageAsync() + } + + if newSession != nil { + newSession.Init() + logger.Info("Check-listening", "start", "newSession") + newSession.ListenToIncomingMessageAsync() + + extraOldCommiteePeers := newSession.GetExtraPeerIDs() + newSession.ListenAsyncWithExtra(extraOldCommiteePeers) + } ec.warmUpSession() + var wg sync.WaitGroup + ctx := context.Background() if oldSession != nil { ctxOld, doneOld := context.WithCancel(ctx) - oldSession.Init() - oldSession.ListenToIncomingMessageAsync() go oldSession.Reshare(doneOld) wg.Add(1) @@ -664,12 +676,11 @@ func (ec *eventConsumer) consumeReshareEvent() error { }() } + logger.Info("Start new resharing session", "walletID", walletID) + if newSession != nil { ctxNew, doneNew := context.WithCancel(ctx) - newSession.Init() - newSession.ListenToIncomingMessageAsync() go newSession.Reshare(doneNew) - wg.Add(1) go func() { defer wg.Done() diff --git a/pkg/mpc/registry.go b/pkg/mpc/registry.go index 5dd8bb7..2dd4754 100644 --- a/pkg/mpc/registry.go +++ b/pkg/mpc/registry.go @@ -84,8 +84,6 @@ func (r *registry) registerReadyPairs(peerIDs []string, callback func()) { r.mu.Lock() r.ready = true r.mu.Unlock() - logger.Info("ALL PEERS ARE READY! Starting to accept MPC requests") - time.AfterFunc(5*time.Second, callback) } From 3eaf6c97e223dfbc2236f7c8162fcbab84533985 Mon Sep 17 00:00:00 2001 From: athen Date: Sun, 10 Aug 2025 17:10:12 +0200 Subject: [PATCH 10/23] fix format error --- pkg/identity/identity.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/identity/identity.go b/pkg/identity/identity.go index 52e49a5..798a775 100644 --- a/pkg/identity/identity.go +++ b/pkg/identity/identity.go @@ -112,14 +112,14 @@ func NewFileStore(identityDir, nodeName string, decrypt bool) (*fileStore, error mNodeId, exists := peers[nodeName] if !exists { - return nil, fmt.Errorf("cannot find nodeID in peers.json for", nodeName) + return nil, fmt.Errorf("cannot find nodeID %s in peers.json for", nodeName) } initSymmetricKeys := make(map[string][]byte) // Generate a random 32-byte symmetric key (suitable for AES-256) tmpKey, err := generateRandom(AES_SYMMETRICKEY_Size) if err != nil { - return nil, fmt.Errorf("failed to generate random symmetric key:", err) + return nil, fmt.Errorf("failed to generate random symmetric key: %w", err) } initSymmetricKeys[mNodeId] = tmpKey From 03dd98c36082ccda8e12fb36abcf46969cab8032 Mon Sep 17 00:00:00 2001 From: athen Date: Sun, 10 Aug 2025 17:17:38 +0200 Subject: [PATCH 11/23] revert old testing config --- e2e/base_test.go | 4 ++-- e2e/config.test.yaml.template | 2 +- e2e/setup_test_identities.sh | 1 - 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/e2e/base_test.go b/e2e/base_test.go index 2587484..6d38588 100644 --- a/e2e/base_test.go +++ b/e2e/base_test.go @@ -218,7 +218,7 @@ func (s *E2ETestSuite) RegisterPeers(t *testing.T) { // Use mpcium register-peers command instead of manual registration t.Log("Running mpcium-cli register-peers...") nodeDir := filepath.Join(s.testDir, "test_node0") - cmd := exec.Command("/home/nan/go/bin/mpcium-cli", "register-peers") + cmd := exec.Command("mpcium-cli", "register-peers") cmd.Dir = nodeDir cmd.Env = append(os.Environ(), "MPCIUM_CONFIG=config.yaml") @@ -274,7 +274,7 @@ func (s *E2ETestSuite) StartNodes(t *testing.T) { nodeDir := filepath.Join(s.testDir, nodeName) // Start node process - cmd := exec.Command("/home/nan/go/bin/mpcium", "start", "-n", nodeName) + cmd := exec.Command("mpcium", "start", "-n", nodeName) cmd.Dir = nodeDir cmd.Env = append(os.Environ(), "MPCIUM_CONFIG=config.yaml") diff --git a/e2e/config.test.yaml.template b/e2e/config.test.yaml.template index 1a7ce6f..8d37279 100644 --- a/e2e/config.test.yaml.template +++ b/e2e/config.test.yaml.template @@ -10,4 +10,4 @@ nats: url: nats://localhost:4223 max_concurrent_keygen: 1 max_concurrent_signing: 10 -session_warm_up_delay_ms: 1000 +session_warm_up_delay_ms: 500 diff --git a/e2e/setup_test_identities.sh b/e2e/setup_test_identities.sh index 59f106a..46b62f6 100755 --- a/e2e/setup_test_identities.sh +++ b/e2e/setup_test_identities.sh @@ -2,7 +2,6 @@ # E2E Test Identity Setup Script # This script sets up identities for testing with separate test database paths -export PATH=$PATH:/home/nan/go/bin/ set -e # Number of test nodes From 6c43ac29250e7eefd57f0fc823d3a73c9f480ece Mon Sep 17 00:00:00 2001 From: athen Date: Mon, 11 Aug 2025 15:23:50 +0200 Subject: [PATCH 12/23] correct log & remove useless comment --- pkg/eventconsumer/keygen_consumer.go | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/pkg/eventconsumer/keygen_consumer.go b/pkg/eventconsumer/keygen_consumer.go index 1d85402..d8c26bf 100644 --- a/pkg/eventconsumer/keygen_consumer.go +++ b/pkg/eventconsumer/keygen_consumer.go @@ -103,22 +103,6 @@ func (sc *keygenConsumer) Run(ctx context.Context) error { return sc.Close() } -// The handleSigningEvent function in sign_consumer.go acts as a bridge between the JetStream-based event queue and the MPC (Multi-Party Computation) signing system -// Creates a reply channel: It generates a unique inbox address using nats.NewInbox() to receive the signing response. -// Sets up response handling: It creates a synchronous subscription to listen for replies on this inbox. -// Forwards the signing request: It publishes the original signing event data to the MPCSigningEventTopic with the reply inbox attached, which triggers the MPC signing process. -// Polls for completion: It enters a polling loop that checks for a reply message, continuing until either: -// A reply is received (successful signing) -// An error occurs (failed signing) -// The timeout is reached (30 seconds) -// Completes the transaction: It either acknowledges (Ack) the message if signing was successful or negatively acknowledges (Nak) it if there was a timeout or error. -// MPC Session Interaction -// The signing consumer doesn't directly interact with MPC sessions. Instead: -// It publishes the signing request to the MPCSigningEventTopic, which is consumed by the eventconsumer.consumeTxSigningEvent handler. -// This handler creates the appropriate signing session (SigningSession for ECDSA or EDDSASigningSession for EdDSA) via the MPC node's creation methods. -// The MPC signing sessions manage the distributed cryptographic operations across multiple nodes, handling message routing, party updates, and signature verification. -// When signing completes, the session publishes the result to a queue and calls the onSuccess callback, which sends a reply to the inbox that the KeygenConsumer is monitoring. -// The reply signals completion, allowing the KeygenConsumer to acknowledge the original message. func (sc *keygenConsumer) handleKeygenEvent(msg jetstream.Msg) { if !sc.peerRegistry.ArePeersReady() { @@ -165,7 +149,7 @@ func (sc *keygenConsumer) handleKeygenEvent(msg jetstream.Msg) { break } if replyMsg != nil { - logger.Info("KeygenConsumer: Completed signing event; reply received") + logger.Info("KeygenConsumer: Completed Keygen event; reply received") if ackErr := msg.Ack(); ackErr != nil { logger.Error("KeygenConsumer: ACK failed", ackErr) } @@ -173,7 +157,7 @@ func (sc *keygenConsumer) handleKeygenEvent(msg jetstream.Msg) { } } - logger.Warn("KeygenConsumer: Timeout waiting for signing event response") + logger.Warn("KeygenConsumer: Timeout waiting for Keygen event response") _ = msg.Nak() } From 2dd7f6cd8c415539b5fbbf098ffdfca0c370e792 Mon Sep 17 00:00:00 2001 From: athen Date: Mon, 11 Aug 2025 17:53:15 +0200 Subject: [PATCH 13/23] avoid p2p message via nats message layer --- pkg/identity/identity.go | 15 +---------- pkg/messaging/point2point.go | 31 ++++++++++++++++++++-- pkg/mpc/key_exchange_session.go | 2 +- pkg/mpc/session.go | 47 +++++++++++++++++++++------------ 4 files changed, 61 insertions(+), 34 deletions(-) diff --git a/pkg/identity/identity.go b/pkg/identity/identity.go index 798a775..d1dacdd 100644 --- a/pkg/identity/identity.go +++ b/pkg/identity/identity.go @@ -110,26 +110,13 @@ func NewFileStore(identityDir, nodeName string, decrypt bool) (*fileStore, error return nil, fmt.Errorf("failed to parse peers.json: %w", err) } - mNodeId, exists := peers[nodeName] - if !exists { - return nil, fmt.Errorf("cannot find nodeID %s in peers.json for", nodeName) - } - - initSymmetricKeys := make(map[string][]byte) - // Generate a random 32-byte symmetric key (suitable for AES-256) - tmpKey, err := generateRandom(AES_SYMMETRICKEY_Size) - if err != nil { - return nil, fmt.Errorf("failed to generate random symmetric key: %w", err) - } - initSymmetricKeys[mNodeId] = tmpKey - store := &fileStore{ identityDir: identityDir, currentNodeName: nodeName, publicKeys: make(map[string][]byte), privateKey: privateKey, initiatorPubKey: initiatorPubKey, - symmetricKeys: initSymmetricKeys, + symmetricKeys: make(map[string][]byte), } // Check that each node in peers.json has an identity file diff --git a/pkg/messaging/point2point.go b/pkg/messaging/point2point.go index 12514a2..da44d15 100644 --- a/pkg/messaging/point2point.go +++ b/pkg/messaging/point2point.go @@ -1,6 +1,8 @@ package messaging import ( + "fmt" + "sync" "time" "github.com/avast/retry-go" @@ -10,20 +12,41 @@ import ( type DirectMessaging interface { Listen(topic string, handler func(data []byte)) (Subscription, error) - Send(topic string, data []byte) error + SendToOther(topic string, data []byte) error + SendToSelf(topic string, data []byte) error } type natsDirectMessaging struct { natsConn *nats.Conn + handlers map[string][]func([]byte) + mu sync.Mutex } func NewNatsDirectMessaging(natsConn *nats.Conn) DirectMessaging { return &natsDirectMessaging{ natsConn: natsConn, + handlers: make(map[string][]func([]byte)), } } -func (d *natsDirectMessaging) Send(topic string, message []byte) error { +// SendToSelf locally sends a message to the same node, invoking all handlers for the topic, avoiding mediating through the message layer. +func (d *natsDirectMessaging) SendToSelf(topic string, message []byte) error { + d.mu.Lock() + handlers, ok := d.handlers[topic] + d.mu.Unlock() + + if !ok || len(handlers) == 0 { + return fmt.Errorf("no handlers found for topic %s", topic) + } + + for _, handler := range handlers { + handler(message) + } + + return nil +} + +func (d *natsDirectMessaging) SendToOther(topic string, message []byte) error { return retry.Do( func() error { _, err := d.natsConn.Request(topic, message, 3*time.Second) @@ -52,5 +75,9 @@ func (d *natsDirectMessaging) Listen(topic string, handler func(data []byte)) (S return nil, err } + d.mu.Lock() + d.handlers[topic] = append(d.handlers[topic], handler) + d.mu.Unlock() + return &natsSubscription{subscription: sub}, nil } diff --git a/pkg/mpc/key_exchange_session.go b/pkg/mpc/key_exchange_session.go index 19bf6ce..d3c829a 100644 --- a/pkg/mpc/key_exchange_session.go +++ b/pkg/mpc/key_exchange_session.go @@ -98,7 +98,7 @@ func (e *ecdhSession) StartKeyExchange() error { symmetricKey := e.deriveSymmetricKey(sharedSecret, ecdhMsg.From) e.identityStore.SetSymmetricKey(ecdhMsg.From, symmetricKey) - requiredKeyCount := len(e.peerIDs) + requiredKeyCount := len(e.peerIDs) - 1 if e.identityStore.CheckSymmetricKeyComplete(requiredKeyCount) { logger.Info("Completed ECDH!", "symmetricKeyAmount", requiredKeyCount) diff --git a/pkg/mpc/session.go b/pkg/mpc/session.go index 7e88277..de61968 100644 --- a/pkg/mpc/session.go +++ b/pkg/mpc/session.go @@ -140,18 +140,27 @@ func (s *session) handleTssMessage(keyshare tss.Message) { return } - fromID := PartyIDToNodeID(s.selfPartyID) + selfID := PartyIDToNodeID(s.selfPartyID) for _, to := range routing.To { toNodeID := PartyIDToNodeID(to) - cipher, err := s.identityStore.EncryptMessage(msg, toNodeID) - if err != nil { - logger.Error("AuthEncrypt Error: %w", err) - } - topic := s.topicComposer.ComposeDirectTopic(fromID, toNodeID) - err = s.direct.Send(topic, cipher) - if err != nil { - logger.Error("Failed to send direct message to", err, "topic", topic) - s.ErrCh <- fmt.Errorf("failed to send direct message to %s", topic) + topic := s.topicComposer.ComposeDirectTopic(selfID, toNodeID) + if selfID == toNodeID { + logger.Debug("---------Detected toself p2p message---------") + err := s.direct.SendToSelf(topic, msg) + if err != nil { + logger.Error("Failed in SendToSelf direct message", err, "topic", topic) + s.ErrCh <- fmt.Errorf("failed to send direct message to %s", topic) + } + } else { + cipher, err := s.identityStore.EncryptMessage(msg, toNodeID) + if err != nil { + logger.Error("AuthEncrypt Error: %w", err) + } + err = s.direct.SendToOther(topic, cipher) + if err != nil { + logger.Error("Failed in SendToOther direct message", err, "topic", topic) + s.ErrCh <- fmt.Errorf("failed to send direct message to %s", topic) + } } } } @@ -165,12 +174,17 @@ func (s *session) receiveP2PTssMessage(topic string, cipher []byte) { return } - // logger.Info("Debug werid crash message", "topic: ", topic) - plaintext, err := s.identityStore.DecryptMessage(cipher, senderID) + var plaintext []byte + var err error - if err != nil { - s.ErrCh <- fmt.Errorf("failed to decrypt message: %w, tampered message", err) - return + if senderID == PartyIDToNodeID(s.selfPartyID) { + plaintext = cipher // to self, no decryption needed + } else { + plaintext, err = s.identityStore.DecryptMessage(cipher, senderID) + if err != nil { + s.ErrCh <- fmt.Errorf("failed to decrypt message: %w, tampered message", err) + return + } } msg, err := types.UnmarshalTssMessage(plaintext) @@ -247,11 +261,10 @@ func (s *session) ListenToIncomingMessageAsync() { s.broadcastSub = sub }() - //subscribe all possible p2p messages from all nodes, due to the nature of tss-lib, this even includes oneself + //subscribe all possible p2p messages from all nodes, per the design of tss-lib, this includes oneself toID := PartyIDToNodeID(s.selfPartyID) for _, fromPartyId := range s.partyIDs { fromID := PartyIDToNodeID(fromPartyId) - logger.Info("Check-listening", "id", fromID) topic := s.topicComposer.ComposeDirectTopic(fromID, toID) sub, err := s.direct.Listen(topic, func(cipher []byte) { go s.receiveP2PTssMessage(topic, cipher) // async for avoid timeout From f9d0a68dce11f9e7d6e37f740bc6acf4fb6d83b7 Mon Sep 17 00:00:00 2001 From: athen Date: Wed, 13 Aug 2025 09:15:54 +0200 Subject: [PATCH 14/23] change to modern x25519 curve for ecdh --- pkg/eventconsumer/event_consumer.go | 2 ++ pkg/mpc/key_exchange_session.go | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/eventconsumer/event_consumer.go b/pkg/eventconsumer/event_consumer.go index 57bbd99..65ec6c6 100644 --- a/pkg/eventconsumer/event_consumer.go +++ b/pkg/eventconsumer/event_consumer.go @@ -655,6 +655,8 @@ func (ec *eventConsumer) consumeReshareEvent() error { } newSession.ListenToIncomingMessageAsync() + // In resharing process, we need to ensure that the new session is aware of the old committee peers. + // Then new committee peers can start listening to the old committee peers, and thus enable receiving direct messages from them. extraOldCommiteePeers := newSession.GetExtraPeerIDs() newSession.ListenAsyncWithExtra(extraOldCommiteePeers) } diff --git a/pkg/mpc/key_exchange_session.go b/pkg/mpc/key_exchange_session.go index d3c829a..dfedf07 100644 --- a/pkg/mpc/key_exchange_session.go +++ b/pkg/mpc/key_exchange_session.go @@ -55,7 +55,7 @@ func NewECDHSession( func (e *ecdhSession) StartKeyExchange() error { // Generate an ephemeral ECDH key pair - privateKey, err := ecdh.P256().GenerateKey(rand.Reader) + privateKey, err := ecdh.X25519().GenerateKey(rand.Reader) if err != nil { return fmt.Errorf("failed to generate ECDH key pair: %w", err) } @@ -82,7 +82,7 @@ func (e *ecdhSession) StartKeyExchange() error { return } - peerPublicKey, err := ecdh.P256().NewPublicKey(ecdhMsg.PublicKey) + peerPublicKey, err := ecdh.X25519().NewPublicKey(ecdhMsg.PublicKey) if err != nil { e.errCh <- err return From 5ee33fc2c60ed0491dec89a02ca6518101ca7517 Mon Sep 17 00:00:00 2001 From: anhthii Date: Wed, 13 Aug 2025 12:59:31 +0700 Subject: [PATCH 15/23] Minor cleanup --- cmd/mpcium/main.go | 2 +- pkg/identity/identity.go | 3 --- pkg/messaging/point2point.go | 3 ++- pkg/mpc/key_exchange_session.go | 6 +----- pkg/mpc/node.go | 3 +-- 5 files changed, 5 insertions(+), 12 deletions(-) diff --git a/cmd/mpcium/main.go b/cmd/mpcium/main.go index 7d34e85..6099f68 100644 --- a/cmd/mpcium/main.go +++ b/cmd/mpcium/main.go @@ -159,7 +159,7 @@ func runNode(ctx context.Context, c *cli.Command) error { reshareResultQueue := mqManager.NewMessageQueue("mpc_reshare_result") defer reshareResultQueue.Close() - logger.Info("Node is running", "id", nodeID, "name", nodeName) + logger.Info("Node is running", "ID", nodeID, "name", nodeName) peerNodeIDs := GetPeerIDs(peers) peerRegistry := mpc.NewRegistry(nodeID, peerNodeIDs, consulClient.KV()) diff --git a/pkg/identity/identity.go b/pkg/identity/identity.go index d1dacdd..e29d3f7 100644 --- a/pkg/identity/identity.go +++ b/pkg/identity/identity.go @@ -383,9 +383,6 @@ func (s *fileStore) DecryptMessage(cipher []byte, peerID string) ([]byte, error) if key == nil { return nil, fmt.Errorf("no symmetric key for peer %s", peerID) } - - // logger.Info("check decryption key", "final", key) - return decryptAEAD(key, cipher) } diff --git a/pkg/messaging/point2point.go b/pkg/messaging/point2point.go index da44d15..6ba202b 100644 --- a/pkg/messaging/point2point.go +++ b/pkg/messaging/point2point.go @@ -29,7 +29,8 @@ func NewNatsDirectMessaging(natsConn *nats.Conn) DirectMessaging { } } -// SendToSelf locally sends a message to the same node, invoking all handlers for the topic, avoiding mediating through the message layer. +// SendToSelf locally sends a message to the same node, invoking all handlers for the topic +// avoiding mediating through the message layer. func (d *natsDirectMessaging) SendToSelf(topic string, message []byte) error { d.mu.Lock() handlers, ok := d.handlers[topic] diff --git a/pkg/mpc/key_exchange_session.go b/pkg/mpc/key_exchange_session.go index dfedf07..3c492b4 100644 --- a/pkg/mpc/key_exchange_session.go +++ b/pkg/mpc/key_exchange_session.go @@ -73,9 +73,7 @@ func (e *ecdhSession) StartKeyExchange() error { if ecdhMsg.From == e.nodeID { return } - logger.Info("Received ECDH message from", "node", ecdhMsg.From) - //TODO: consider how to avoid replay attack if err := e.identityStore.VerifySignature(&ecdhMsg); err != nil { e.errCh <- err @@ -87,7 +85,6 @@ func (e *ecdhSession) StartKeyExchange() error { e.errCh <- err return } - // Perform ECDH sharedSecret, err := e.privateKey.ECDH(peerPublicKey) if err != nil { e.errCh <- err @@ -99,9 +96,8 @@ func (e *ecdhSession) StartKeyExchange() error { e.identityStore.SetSymmetricKey(ecdhMsg.From, symmetricKey) requiredKeyCount := len(e.peerIDs) - 1 - if e.identityStore.CheckSymmetricKeyComplete(requiredKeyCount) { - logger.Info("Completed ECDH!", "symmetricKeyAmount", requiredKeyCount) + logger.Info("Completed ECDH!", "symmetricKeyCount", requiredKeyCount) logger.Info("PEER IS READY! Starting to accept MPC requests") } }) diff --git a/pkg/mpc/node.go b/pkg/mpc/node.go index 4f3289d..25060d7 100644 --- a/pkg/mpc/node.go +++ b/pkg/mpc/node.go @@ -76,8 +76,7 @@ func NewNode( start := time.Now() elapsed := time.Since(start) logger.Info("Starting new node, preparams is generated successfully!", "elapsed", elapsed.Milliseconds()) - - //each node initiates the DH key exchange listener at the beginning and invoke message sending when all peers are ready + // Each node initiates the DH key exchange listener at the beginning and invoke message sending when all peers are ready dhSession := NewECDHSession(nodeID, peerIDs, pubSub, identityStore) if err := dhSession.StartKeyExchange(); err != nil { logger.Fatal("Failed to start DH key exchange", err) From 318762ed56bb8cb2623615353284afb210f47f70 Mon Sep 17 00:00:00 2001 From: anhthii Date: Wed, 13 Aug 2025 13:02:47 +0700 Subject: [PATCH 16/23] Consistent naming convention Id -> ID --- pkg/mpc/session.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/mpc/session.go b/pkg/mpc/session.go index 5bd9c69..56e623c 100644 --- a/pkg/mpc/session.go +++ b/pkg/mpc/session.go @@ -167,7 +167,7 @@ func (s *session) handleTssMessage(keyshare tss.Message) { } func (s *session) receiveP2PTssMessage(topic string, cipher []byte) { - senderID := extractSenderIdFromDirectTopic(topic) + senderID := extractSenderIDFromDirectTopic(topic) if senderID == "" { s.ErrCh <- fmt.Errorf("failed to extract senderID from direct topic: the direct topic format is wrong") @@ -263,8 +263,8 @@ func (s *session) ListenToIncomingMessageAsync() { //subscribe all possible p2p messages from all nodes, per the design of tss-lib, this includes oneself toID := PartyIDToNodeID(s.selfPartyID) - for _, fromPartyId := range s.partyIDs { - fromID := PartyIDToNodeID(fromPartyId) + for _, fromPartyID := range s.partyIDs { + fromID := PartyIDToNodeID(fromPartyID) topic := s.topicComposer.ComposeDirectTopic(fromID, toID) sub, err := s.direct.Listen(topic, func(cipher []byte) { go s.receiveP2PTssMessage(topic, cipher) // async for avoid timeout @@ -280,8 +280,8 @@ func (s *session) ListenToIncomingMessageAsync() { func (s *session) ListenAsyncWithExtra(extraIDs []string) { //subscribe potential p2p messages from addtional nodes (just for resharing) toID := PartyIDToNodeID(s.selfPartyID) - for _, fromId := range extraIDs { - topic := s.topicComposer.ComposeDirectTopic(fromId, toID) + for _, fromID := range extraIDs { + topic := s.topicComposer.ComposeDirectTopic(fromID, toID) sub, err := s.direct.Listen(topic, func(cipher []byte) { go s.receiveP2PTssMessage(topic, cipher) // async for avoid timeout }) @@ -364,10 +364,10 @@ func walletIDWithVersion(walletID string, version int) string { return walletID } -func extractSenderIdFromDirectTopic(topic string) string { +func extractSenderIDFromDirectTopic(topic string) string { strs := strings.Split(topic, ":") - // according to direct topic format, there will be 6 slices, senderId is the 4th one + // according to direct topic format, there will be 6 slices, senderID is the 4th one if len(strs) == 6 { return strs[3] } From 345f92dc55b0cd1303a572b90a59cf4b026f9c4d Mon Sep 17 00:00:00 2001 From: anhthii Date: Wed, 13 Aug 2025 14:02:19 +0700 Subject: [PATCH 17/23] Wait for ECDH session to complete before starting consumers --- cmd/mpcium/main.go | 13 ++++++- pkg/mpc/key_exchange_session.go | 66 +++++++++++++++++++++++++++------ pkg/mpc/node.go | 4 ++ pkg/types/tss.go | 34 ----------------- pkg/types/tss_test.go | 24 ------------ 5 files changed, 70 insertions(+), 71 deletions(-) diff --git a/cmd/mpcium/main.go b/cmd/mpcium/main.go index 6099f68..1cc6e2f 100644 --- a/cmd/mpcium/main.go +++ b/cmd/mpcium/main.go @@ -202,8 +202,14 @@ func runNode(ctx context.Context, c *cli.Command) error { logger.Error("Failed to mark peer registry as ready", err) } logger.Info("[READY] Node is ready", "nodeID", nodeID) - appContext, cancel := context.WithCancel(context.Background()) + logger.Info("Waiting for ECDH key exchange to complete...", "nodeID", nodeID) + if err := mpcNode.GetDHSession().WaitForExchangeComplete(); err != nil { + logger.Fatal("ECDH exchange failed", err) + } + + logger.Info("ECDH key exchange completed successfully, starting consumers...", "nodeID", nodeID) + appContext, cancel := context.WithCancel(context.Background()) //Setup signal handling to cancel context on termination signals. go func() { sigChan := make(chan os.Signal, 1) @@ -219,9 +225,12 @@ func runNode(ctx context.Context, c *cli.Command) error { if err := signingConsumer.Close(); err != nil { logger.Error("Failed to close signing consumer", err) } + + if err := mpcNode.GetDHSession().Close(); err != nil { + logger.Error("Failed to close ECDH session", err) + } }() - //TODO: I think it makes more sense to start these consumers only after P2P channel were succesfully built var wg sync.WaitGroup errChan := make(chan error, 2) diff --git a/pkg/mpc/key_exchange_session.go b/pkg/mpc/key_exchange_session.go index 3c492b4..ecf6356 100644 --- a/pkg/mpc/key_exchange_session.go +++ b/pkg/mpc/key_exchange_session.go @@ -17,12 +17,21 @@ import ( "encoding/json" + "sync" + "github.com/nats-io/nats.go" ) +const ( + ECDHExchangeTopic = "ecdh:exchange" + ECDHExchangeTimeout = 2 * time.Minute +) + type ECDHSession interface { StartKeyExchange() error BroadcastPublicKey() error + WaitForExchangeComplete() error + Close() error } type ecdhSession struct { @@ -35,6 +44,8 @@ type ecdhSession struct { publicKey *ecdh.PublicKey exchangeComplete chan struct{} errCh chan error + exchangeDone bool + mu sync.RWMutex } func NewECDHSession( @@ -48,8 +59,8 @@ func NewECDHSession( peerIDs: peerIDs, pubSub: pubSub, identityStore: identityStore, - exchangeComplete: make(chan struct{}), - errCh: make(chan error), + exchangeComplete: make(chan struct{}, 1), + errCh: make(chan error, 1), } } @@ -64,7 +75,7 @@ func (e *ecdhSession) StartKeyExchange() error { e.publicKey = privateKey.PublicKey() // Subscribe to ECDH broadcast - sub, err := e.pubSub.Subscribe("ecdh:exchange", func(natMsg *nats.Msg) { + sub, err := e.pubSub.Subscribe(ECDHExchangeTopic, func(natMsg *nats.Msg) { var ecdhMsg types.ECDHMessage if err := json.Unmarshal(natMsg.Data, &ecdhMsg); err != nil { return @@ -96,22 +107,38 @@ func (e *ecdhSession) StartKeyExchange() error { e.identityStore.SetSymmetricKey(ecdhMsg.From, symmetricKey) requiredKeyCount := len(e.peerIDs) - 1 + logger.Info("ECDH progress", "peer", ecdhMsg.From, "required", requiredKeyCount) + if e.identityStore.CheckSymmetricKeyComplete(requiredKeyCount) { - logger.Info("Completed ECDH!", "symmetricKeyCount", requiredKeyCount) - logger.Info("PEER IS READY! Starting to accept MPC requests") + logger.Info("Completed ECDH!", "symmetric key counts of peers", requiredKeyCount) + logger.Info("ALL PEERS ARE READY! Starting to accept MPC requests") + + e.mu.Lock() + e.exchangeDone = true + e.mu.Unlock() + + e.exchangeComplete <- struct{}{} } }) - e.ecdhSub = sub + e.ecdhSub = sub if err != nil { return fmt.Errorf("failed to subscribe to ECDH topic: %w", err) } return nil } +func (s *ecdhSession) Close() error { + err := s.ecdhSub.Unsubscribe() + if err != nil { + return err + } + + return nil +} + func (e *ecdhSession) BroadcastPublicKey() error { publicKeyBytes := e.publicKey.Bytes() - msg := types.ECDHMessage{ From: e.nodeID, PublicKey: publicKeyBytes, @@ -125,15 +152,32 @@ func (e *ecdhSession) BroadcastPublicKey() error { msg.Signature = signature signedMsgBytes, _ := json.Marshal(msg) - logger.Info("Starting to broadcast DH key") - - if err := e.pubSub.Publish("ecdh:exchange", signedMsgBytes); err != nil { + logger.Info("Starting to broadcast DH key", "nodeID", e.nodeID) + if err := e.pubSub.Publish(ECDHExchangeTopic, signedMsgBytes); err != nil { return fmt.Errorf("%s failed to publish DH message because %w", e.nodeID, err) } - return nil } +func (e *ecdhSession) WaitForExchangeComplete() error { + e.mu.RLock() + if e.exchangeDone { + e.mu.RUnlock() + return nil + } + e.mu.RUnlock() + timeout := time.After(ECDHExchangeTimeout) // 2 minutes timeout + + select { + case <-e.exchangeComplete: + return nil + case err := <-e.errCh: + return err + case <-timeout: + return fmt.Errorf("ECDH exchange timeout!") + } +} + func deriveConsistentInfo(a, b string) []byte { if a < b { return []byte(a + b) diff --git a/pkg/mpc/node.go b/pkg/mpc/node.go index 25060d7..c7b1bfe 100644 --- a/pkg/mpc/node.go +++ b/pkg/mpc/node.go @@ -482,6 +482,10 @@ func (p *Node) Close() { } } +func (p *Node) GetDHSession() ECDHSession { + return p.dhSession +} + func (p *Node) generatePreParams() []*keygen.LocalPreParams { start := time.Now() // Try to load from kvstore diff --git a/pkg/types/tss.go b/pkg/types/tss.go index 3b118e4..ed574d7 100644 --- a/pkg/types/tss.go +++ b/pkg/types/tss.go @@ -1,11 +1,7 @@ -// The Licensed Work is (c) 2022 Sygma -// SPDX-License-Identifier: LGPL-3.0-only package types import ( "encoding/json" - "sort" - "time" "github.com/bnb-chain/tss-lib/v2/tss" ) @@ -23,13 +19,6 @@ type TssMessage struct { Signature []byte `json:"signature"` } -type ECDHMessage struct { - From string `json:"from"` - PublicKey []byte `json:"public_key"` - Timestamp time.Time `json:"timestamp"` - Signature []byte `json:"signature"` -} - func NewTssMessage( walletID string, msgBytes []byte, @@ -118,19 +107,6 @@ func UnmarshalStartMessage(msgBytes []byte) (*StartMessage, error) { return msg, nil } -// MarshalForSigning returns the deterministic JSON bytes for signing -func (msg *ECDHMessage) MarshalForSigning() ([]byte, error) { - // Create a map with ordered keys - signingData := map[string]interface{}{ - "From": msg.From, - "PublicKey": msg.PublicKey, - "Timestamp": msg.Timestamp, - } - - // Use json.Marshal with sorted keys - return json.Marshal(signingData) -} - // MarshalForSigning returns the deterministic JSON bytes for signing func (msg *TssMessage) MarshalForSigning() ([]byte, error) { // Create a map with ordered keys @@ -147,13 +123,3 @@ func (msg *TssMessage) MarshalForSigning() ([]byte, error) { // Use json.Marshal with sorted keys return json.Marshal(signingData) } - -// Helper function to get sorted party IDs -func getPartyIDs(parties []*tss.PartyID) []string { - ids := make([]string, len(parties)) - for i, party := range parties { - ids[i] = party.Id - } - sort.Strings(ids) // Ensure deterministic order - return ids -} diff --git a/pkg/types/tss_test.go b/pkg/types/tss_test.go index 702ac07..9fa7fc6 100644 --- a/pkg/types/tss_test.go +++ b/pkg/types/tss_test.go @@ -170,27 +170,3 @@ func TestUnmarshalStartMessage_InvalidJSON(t *testing.T) { _, err := UnmarshalStartMessage(invalidJSON) assert.Error(t, err) } - -func TestGetPartyIDs(t *testing.T) { - parties := []*tss.PartyID{ - { - MessageWrapper_PartyID: &tss.MessageWrapper_PartyID{ - Id: "party3", - }, - }, - { - MessageWrapper_PartyID: &tss.MessageWrapper_PartyID{ - Id: "party1", - }, - }, - { - MessageWrapper_PartyID: &tss.MessageWrapper_PartyID{ - Id: "party2", - }, - }, - } - - ids := getPartyIDs(parties) - expected := []string{"party1", "party2", "party3"} - assert.Equal(t, expected, ids) -} From 2ea0d3275207cd420ea125b00d0a930fc53b3c37 Mon Sep 17 00:00:00 2001 From: anhthii Date: Wed, 13 Aug 2025 15:08:42 +0700 Subject: [PATCH 18/23] Code refactoring, better naming convention --- .gitignore | 4 ++ pkg/eventconsumer/event_consumer.go | 4 +- pkg/mpc/ecdsa_resharing_session.go | 11 ++-- pkg/mpc/eddsa_resharing_session.go | 9 ++- pkg/mpc/node.go | 63 +------------------ pkg/mpc/node_test.go | 45 -------------- pkg/mpc/party_id.go | 71 ++++++++++++++++++++++ pkg/mpc/session.go | 93 +++++++++++++++++------------ pkg/types/ecdh.go | 26 ++++++++ 9 files changed, 173 insertions(+), 153 deletions(-) create mode 100644 pkg/mpc/party_id.go create mode 100644 pkg/types/ecdh.go diff --git a/.gitignore b/.gitignore index b09913b..5832d41 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,7 @@ e2e/coverage.html e2e/logs/ # Generated config file (template is tracked) e2e/config.test.yaml +node0 +node1 +node2 +config.yaml diff --git a/pkg/eventconsumer/event_consumer.go b/pkg/eventconsumer/event_consumer.go index 65ec6c6..8b88ff2 100644 --- a/pkg/eventconsumer/event_consumer.go +++ b/pkg/eventconsumer/event_consumer.go @@ -657,8 +657,8 @@ func (ec *eventConsumer) consumeReshareEvent() error { // In resharing process, we need to ensure that the new session is aware of the old committee peers. // Then new committee peers can start listening to the old committee peers, and thus enable receiving direct messages from them. - extraOldCommiteePeers := newSession.GetExtraPeerIDs() - newSession.ListenAsyncWithExtra(extraOldCommiteePeers) + legacyCommitteePeers := newSession.GetLegacyCommitteePeers() + newSession.ListenToPeersAsync(legacyCommitteePeers) } ec.warmUpSession() diff --git a/pkg/mpc/ecdsa_resharing_session.go b/pkg/mpc/ecdsa_resharing_session.go index 3641b19..4cf3845 100644 --- a/pkg/mpc/ecdsa_resharing_session.go +++ b/pkg/mpc/ecdsa_resharing_session.go @@ -21,7 +21,7 @@ type ReshareSession interface { Init() error Reshare(done func()) GetPubKeyResult() []byte - GetExtraPeerIDs() []string + GetLegacyCommitteePeers() []string } type ecdsaReshareSession struct { @@ -101,7 +101,7 @@ func NewECDSAReshareSession( var oldPeerIDs []string for _, partyId := range oldPartyIDs { - oldPeerIDs = append(oldPeerIDs, PartyIDToNodeID(partyId)) + oldPeerIDs = append(oldPeerIDs, partyIDToNodeID(partyId)) } return &ecdsaReshareSession{ @@ -114,8 +114,11 @@ func NewECDSAReshareSession( } } -func (s *ecdsaReshareSession) GetExtraPeerIDs() []string { - // difference returns elements in A that are not in B. +// GetLegacyCommitteePeers returns peer IDs that were part of the old committee +// but are NOT part of the new committee after resharing. +// These peers are still relevant during resharing because +// they must send final share data to the new committee. +func (s *ecdsaReshareSession) GetLegacyCommitteePeers() []string { difference := func(A, B []string) []string { seen := make(map[string]bool) for _, b := range B { diff --git a/pkg/mpc/eddsa_resharing_session.go b/pkg/mpc/eddsa_resharing_session.go index 7f0857f..70a59c2 100644 --- a/pkg/mpc/eddsa_resharing_session.go +++ b/pkg/mpc/eddsa_resharing_session.go @@ -91,7 +91,7 @@ func NewEDDSAReshareSession( var oldPeerIDs []string for _, partyId := range oldPartyIDs { - oldPeerIDs = append(oldPeerIDs, PartyIDToNodeID(partyId)) + oldPeerIDs = append(oldPeerIDs, partyIDToNodeID(partyId)) } return &eddsaReshareSession{ @@ -104,8 +104,11 @@ func NewEDDSAReshareSession( } } -func (s *eddsaReshareSession) GetExtraPeerIDs() []string { - // difference returns elements in A that are not in B. +// GetLegacyCommitteePeers returns peer IDs that were part of the old committee +// but are NOT part of the new committee after resharing. +// These peers are still relevant during resharing because +// they must send final share data to the new committee. +func (s *eddsaReshareSession) GetLegacyCommitteePeers() []string { difference := func(A, B []string) []string { seen := make(map[string]bool) for _, b := range B { diff --git a/pkg/mpc/node.go b/pkg/mpc/node.go index c7b1bfe..96da3c5 100644 --- a/pkg/mpc/node.go +++ b/pkg/mpc/node.go @@ -1,13 +1,9 @@ package mpc import ( - "bytes" "encoding/json" "fmt" - "math/big" "slices" - "strconv" - "strings" "time" "github.com/bnb-chain/tss-lib/v2/ecdsa/keygen" @@ -18,7 +14,6 @@ import ( "github.com/fystack/mpcium/pkg/kvstore" "github.com/fystack/mpcium/pkg/logger" "github.com/fystack/mpcium/pkg/messaging" - "github.com/google/uuid" ) const ( @@ -44,23 +39,7 @@ type Node struct { identityStore identity.Store peerRegistry PeerRegistry - dhSession *ecdhSession -} - -func PartyIDToRoutingDest(partyID *tss.PartyID) string { - return string(partyID.KeyInt().Bytes()) -} - -func PartyIDToNodeID(partyID *tss.PartyID) string { - return strings.Split(string(partyID.KeyInt().Bytes()), ":")[0] -} - -func ComparePartyIDs(x, y *tss.PartyID) bool { - return bytes.Equal(x.KeyInt().Bytes(), y.KeyInt().Bytes()) -} - -func ComposeReadyKey(nodeID string) string { - return fmt.Sprintf("ready/%s", nodeID) + dhSession ECDHSession } func NewNode( @@ -435,44 +414,8 @@ func (p *Node) CreateReshareSession( } } -// generatePartyIDs generates the party IDs for the given purpose and version -// It returns the self party ID and all party IDs -// It also sorts the party IDs in place -func (n *Node) generatePartyIDs( - label string, - readyPeerIDs []string, - version int, -) (self *tss.PartyID, all []*tss.PartyID) { - // Pre-allocate slice with exact size needed - partyIDs := make([]*tss.PartyID, 0, len(readyPeerIDs)) - - // Create all party IDs in one pass - for _, peerID := range readyPeerIDs { - partyID := createPartyID(peerID, label, version) - if peerID == n.nodeID { - self = partyID - } - partyIDs = append(partyIDs, partyID) - } - - // Sort party IDs in place - all = tss.SortPartyIDs(partyIDs, 0) - return -} - -// createPartyID creates a new party ID for the given node ID, label and version -// It returns the party ID: random string -// Moniker: for routing messages -// Key: for mpc internal use (need persistent storage) -func createPartyID(nodeID string, label string, version int) *tss.PartyID { - partyID := uuid.NewString() - var key *big.Int - if version == BackwardCompatibleVersion { - key = big.NewInt(0).SetBytes([]byte(nodeID)) - } else { - key = big.NewInt(0).SetBytes([]byte(nodeID + ":" + strconv.Itoa(version))) - } - return tss.NewPartyID(partyID, label, key) +func ComposeReadyKey(nodeID string) string { + return fmt.Sprintf("ready/%s", nodeID) } func (p *Node) Close() { diff --git a/pkg/mpc/node_test.go b/pkg/mpc/node_test.go index cfd7294..37792e9 100644 --- a/pkg/mpc/node_test.go +++ b/pkg/mpc/node_test.go @@ -6,12 +6,6 @@ import ( "github.com/stretchr/testify/assert" ) -func TestPartyIDToNodeID(t *testing.T) { - partyID := createPartyID("4d8cb873-dc86-4776-b6f6-cf5c668f6468", "keygen", 1) - nodeID := PartyIDToRoutingDest(partyID) - assert.Equal(t, nodeID, "4d8cb873-dc86-4776-b6f6-cf5c668f6468:1", "NodeID should be equal") -} - func TestCreatePartyID_Structure(t *testing.T) { sessionID := "test-session-123" keyType := "keygen" @@ -46,29 +40,6 @@ func TestCreatePartyID_DifferentVersions(t *testing.T) { assert.NotEqual(t, partyID0.Key, partyID1.Key) } -func TestPartyIDToRoutingDest_BackwardCompatible(t *testing.T) { - sessionID := "test-session-789" - keyType := "signing" - - partyID := createPartyID(sessionID, keyType, BackwardCompatibleVersion) - nodeID := PartyIDToRoutingDest(partyID) - - // For backward compatible version, should just be the sessionID - assert.Equal(t, sessionID, nodeID) -} - -func TestPartyIDToRoutingDest_DefaultVersion(t *testing.T) { - sessionID := "test-session-999" - keyType := "signing" - - partyID := createPartyID(sessionID, keyType, DefaultVersion) - nodeID := PartyIDToRoutingDest(partyID) - - // For default version, should include the version number - expected := sessionID + ":1" - assert.Equal(t, expected, nodeID) -} - func TestCreatePartyID_EmptyValues(t *testing.T) { // Test with empty session ID partyID := createPartyID("", "keygen", 0) @@ -81,22 +52,6 @@ func TestCreatePartyID_EmptyValues(t *testing.T) { assert.Equal(t, "", partyID.Moniker) } -func TestPartyIDToRoutingDest_Consistency(t *testing.T) { - sessionID := "consistent-session" - keyType := "keygen" - version := 3 - - // Create the same party ID multiple times - partyID1 := createPartyID(sessionID, keyType, version) - partyID2 := createPartyID(sessionID, keyType, version) - - nodeID1 := PartyIDToRoutingDest(partyID1) - nodeID2 := PartyIDToRoutingDest(partyID2) - - // Should produce consistent results based on sessionID and version - assert.Equal(t, nodeID1, nodeID2, "Same parameters should produce same routing destinations") -} - func TestCreatePartyID_UniqueIDs(t *testing.T) { sessionID := "test-session" keyType := "keygen" diff --git a/pkg/mpc/party_id.go b/pkg/mpc/party_id.go new file mode 100644 index 0000000..e0c6b19 --- /dev/null +++ b/pkg/mpc/party_id.go @@ -0,0 +1,71 @@ +package mpc + +import ( + "bytes" + "fmt" + "math/big" + "strings" + + "github.com/bnb-chain/tss-lib/v2/tss" + "github.com/google/uuid" +) + +// generatePartyIDs generates the party IDs for the given purpose and version +// It returns the self party ID and all party IDs +// It also sorts the party IDs in place +func (n *Node) generatePartyIDs( + label string, + readyPeerIDs []string, + version int, +) (self *tss.PartyID, all []*tss.PartyID) { + // Pre-allocate slice with exact size needed + partyIDs := make([]*tss.PartyID, 0, len(readyPeerIDs)) + + // Create all party IDs in one pass + for _, peerID := range readyPeerIDs { + partyID := createPartyID(peerID, label, version) + if peerID == n.nodeID { + self = partyID + } + partyIDs = append(partyIDs, partyID) + } + + // Sort party IDs in place + all = tss.SortPartyIDs(partyIDs, 0) + return +} + +// createPartyID creates a new party ID for the given node ID, label and version +// It returns the party ID: random string +// Moniker: for routing messages +// Key: for mpc internal use (need persistent storage) +func createPartyID(nodeID string, label string, version int) *tss.PartyID { + partyID := uuid.NewString() + var key *big.Int + if version == BackwardCompatibleVersion { + key = new(big.Int).SetBytes([]byte(nodeID)) + } else { + key = new(big.Int).SetBytes([]byte(fmt.Sprintf("%s:%d", nodeID, version))) + } + return tss.NewPartyID(partyID, label, key) +} + +func partyIDToNodeID(partyID *tss.PartyID) string { + if partyID == nil { + return "" + } + nodeID, _, _ := strings.Cut(string(partyID.KeyInt().Bytes()), ":") + return strings.TrimSpace(nodeID) +} + +func partyIDsToNodeIDs(pids []*tss.PartyID) []string { + out := make([]string, 0, len(pids)) + for _, p := range pids { + out = append(out, partyIDToNodeID(p)) + } + return out +} + +func comparePartyIDs(x, y *tss.PartyID) bool { + return bytes.Equal(x.KeyInt().Bytes(), y.KeyInt().Bytes()) +} diff --git a/pkg/mpc/session.go b/pkg/mpc/session.go index 56e623c..1b0ffc0 100644 --- a/pkg/mpc/session.go +++ b/pkg/mpc/session.go @@ -43,7 +43,7 @@ type KeyComposerFn func(id string) string type Session interface { ListenToIncomingMessageAsync() - ListenAsyncWithExtra(extraIDs []string) + ListenToPeersAsync(peerIDs []string) ErrChan() <-chan error } @@ -140,9 +140,9 @@ func (s *session) handleTssMessage(keyshare tss.Message) { return } - selfID := PartyIDToNodeID(s.selfPartyID) + selfID := partyIDToNodeID(s.selfPartyID) for _, to := range routing.To { - toNodeID := PartyIDToNodeID(to) + toNodeID := partyIDToNodeID(to) topic := s.topicComposer.ComposeDirectTopic(selfID, toNodeID) if selfID == toNodeID { logger.Debug("---------Detected toself p2p message---------") @@ -177,7 +177,7 @@ func (s *session) receiveP2PTssMessage(topic string, cipher []byte) { var plaintext []byte var err error - if senderID == PartyIDToNodeID(s.selfPartyID) { + if senderID == partyIDToNodeID(s.selfPartyID) { plaintext = cipher // to self, no decryption needed } else { plaintext, err = s.identityStore.DecryptMessage(cipher, senderID) @@ -225,11 +225,23 @@ func (s *session) receiveTssMessage(msg *types.TssMessage) { s.ErrCh <- errors.Wrap(err, "Broken TSS Share") return } - logger.Debug("Received message", "round", round.RoundMsg, "isBroadcast", msg.IsBroadcast, "to", toIDs, "from", msg.From.String(), "self", s.selfPartyID.String()) + logger.Debug( + "Received message", + "round", + round.RoundMsg, + "isBroadcast", + msg.IsBroadcast, + "to", + toIDs, + "from", + msg.From.String(), + "self", + s.selfPartyID.String(), + ) isBroadcast := msg.IsBroadcast && len(msg.To) == 0 var isToSelf bool for _, to := range msg.To { - if ComparePartyIDs(to, s.selfPartyID) { + if comparePartyIDs(to, s.selfPartyID) { isToSelf = true break } @@ -246,50 +258,53 @@ func (s *session) receiveTssMessage(msg *types.TssMessage) { } } -func (s *session) ListenToIncomingMessageAsync() { +func (s *session) subscribeDirectTopicAsync(topic string) error { + t := topic // avoid capturing the changing loop variable + sub, err := s.direct.Listen(t, func(cipher []byte) { + // async to avoid timeouts in handlers + go s.receiveP2PTssMessage(t, cipher) + }) + if err != nil { + return fmt.Errorf("Failed to subscribe to direct topic %s: %w", t, err) + } + s.directSubs = append(s.directSubs, sub) + return nil +} + +func (s *session) subscribeFromPeersAsync(fromIDs []string) { + toID := partyIDToNodeID(s.selfPartyID) + for _, fromID := range fromIDs { + topic := s.topicComposer.ComposeDirectTopic(fromID, toID) + if err := s.subscribeDirectTopicAsync(topic); err != nil { + s.ErrCh <- err + } + } +} + +func (s *session) subscribeBroadcastAsync() { go func() { - sub, err := s.pubSub.Subscribe(s.topicComposer.ComposeBroadcastTopic(), func(natMsg *nats.Msg) { - msg := natMsg.Data - s.receiveBroadcastTssMessage(msg) + topic := s.topicComposer.ComposeBroadcastTopic() + sub, err := s.pubSub.Subscribe(topic, func(natMsg *nats.Msg) { + s.receiveBroadcastTssMessage(natMsg.Data) }) - if err != nil { - s.ErrCh <- fmt.Errorf("Failed to subscribe to broadcast topic %s: %w", s.topicComposer.ComposeBroadcastTopic(), err) + s.ErrCh <- fmt.Errorf("Failed to subscribe to broadcast topic %s: %w", topic, err) return } - s.broadcastSub = sub }() +} - //subscribe all possible p2p messages from all nodes, per the design of tss-lib, this includes oneself - toID := PartyIDToNodeID(s.selfPartyID) - for _, fromPartyID := range s.partyIDs { - fromID := PartyIDToNodeID(fromPartyID) - topic := s.topicComposer.ComposeDirectTopic(fromID, toID) - sub, err := s.direct.Listen(topic, func(cipher []byte) { - go s.receiveP2PTssMessage(topic, cipher) // async for avoid timeout - }) - if err != nil { - s.ErrCh <- fmt.Errorf("Failed to subscribe to direct topic %s: %w", topic, err) - } +func (s *session) ListenToIncomingMessageAsync() { + // 1) broadcast + s.subscribeBroadcastAsync() - s.directSubs = append(s.directSubs, sub) - } + // 2) direct from peers in this session's partyIDs (includes self) + s.subscribeFromPeersAsync(partyIDsToNodeIDs(s.partyIDs)) } -func (s *session) ListenAsyncWithExtra(extraIDs []string) { - //subscribe potential p2p messages from addtional nodes (just for resharing) - toID := PartyIDToNodeID(s.selfPartyID) - for _, fromID := range extraIDs { - topic := s.topicComposer.ComposeDirectTopic(fromID, toID) - sub, err := s.direct.Listen(topic, func(cipher []byte) { - go s.receiveP2PTssMessage(topic, cipher) // async for avoid timeout - }) - if err != nil { - s.ErrCh <- fmt.Errorf("Failed to subscribe to direct topic %s: %w", topic, err) - } - s.directSubs = append(s.directSubs, sub) - } +func (s *session) ListenToPeersAsync(peerIDs []string) { + s.subscribeFromPeersAsync(peerIDs) } func (s *session) Close() error { diff --git a/pkg/types/ecdh.go b/pkg/types/ecdh.go new file mode 100644 index 0000000..cfc4295 --- /dev/null +++ b/pkg/types/ecdh.go @@ -0,0 +1,26 @@ +package types + +import ( + "encoding/json" + "time" +) + +type ECDHMessage struct { + From string `json:"from"` + PublicKey []byte `json:"public_key"` + Timestamp time.Time `json:"timestamp"` + Signature []byte `json:"signature"` +} + +// MarshalForSigning returns the deterministic JSON bytes for signing +func (msg *ECDHMessage) MarshalForSigning() ([]byte, error) { + // Create a map with ordered keys + signingData := map[string]interface{}{ + "from": msg.From, + "publicKey": msg.PublicKey, + "timestamp": msg.Timestamp, + } + + // Use json.Marshal with sorted keys + return json.Marshal(signingData) +} From fc8bab722234276e02399acf3879d708b39ac2fd Mon Sep 17 00:00:00 2001 From: anhthii Date: Wed, 13 Aug 2025 15:21:14 +0700 Subject: [PATCH 19/23] Refactor aes encryption --- pkg/encryption/aes.go | 52 ++++++++++++++++++++++ pkg/eventconsumer/keygen_consumer.go | 4 +- pkg/identity/identity.go | 65 ++-------------------------- 3 files changed, 58 insertions(+), 63 deletions(-) diff --git a/pkg/encryption/aes.go b/pkg/encryption/aes.go index bc9f386..c4e4f02 100644 --- a/pkg/encryption/aes.go +++ b/pkg/encryption/aes.go @@ -4,8 +4,12 @@ import ( "crypto/aes" "crypto/cipher" "crypto/rand" + "errors" + "fmt" ) +const AESGCMNonceSize = 12 + func EncryptAESGCM(plain, key []byte) (ciphertext, nonce []byte, err error) { block, err := aes.NewCipher(key) if err != nil { @@ -34,3 +38,51 @@ func DecryptAESGCM(ciphertext, key, nonce []byte) ([]byte, error) { } return aead.Open(nil, nonce, ciphertext, nil) } + +// EncryptAESGCMWithNonceEmbed encrypts plaintext and embeds the nonce at the start of the returned slice. +func EncryptAESGCMWithNonceEmbed(plaintext, key []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, fmt.Errorf("failed to create AES cipher: %w", err) + } + + aead, err := cipher.NewGCM(block) + if err != nil { + return nil, fmt.Errorf("failed to create GCM: %w", err) + } + + nonce := make([]byte, AESGCMNonceSize) + if _, err := rand.Read(nonce); err != nil { + return nil, fmt.Errorf("failed to generate nonce: %w", err) + } + + ciphertext := aead.Seal(nil, nonce, plaintext, nil) + return append(nonce, ciphertext...), nil +} + +// DecryptAESGCMWithNonceEmbed decrypts ciphertext where the nonce is embedded at the start of the slice. +func DecryptAESGCMWithNonceEmbed(data, key []byte) ([]byte, error) { + if len(data) < AESGCMNonceSize { + return nil, errors.New("ciphertext too short") + } + + nonce := data[:AESGCMNonceSize] + ciphertext := data[AESGCMNonceSize:] + + block, err := aes.NewCipher(key) + if err != nil { + return nil, fmt.Errorf("failed to create AES cipher: %w", err) + } + + aead, err := cipher.NewGCM(block) + if err != nil { + return nil, fmt.Errorf("failed to create GCM: %w", err) + } + + plaintext, err := aead.Open(nil, nonce, ciphertext, nil) + if err != nil { + return nil, fmt.Errorf("decryption failed: %w", err) + } + + return plaintext, nil +} diff --git a/pkg/eventconsumer/keygen_consumer.go b/pkg/eventconsumer/keygen_consumer.go index d8c26bf..2ad36a8 100644 --- a/pkg/eventconsumer/keygen_consumer.go +++ b/pkg/eventconsumer/keygen_consumer.go @@ -149,7 +149,7 @@ func (sc *keygenConsumer) handleKeygenEvent(msg jetstream.Msg) { break } if replyMsg != nil { - logger.Info("KeygenConsumer: Completed Keygen event; reply received") + logger.Info("KeygenConsumer: Completed keygen event; reply received") if ackErr := msg.Ack(); ackErr != nil { logger.Error("KeygenConsumer: ACK failed", ackErr) } @@ -157,7 +157,7 @@ func (sc *keygenConsumer) handleKeygenEvent(msg jetstream.Msg) { } } - logger.Warn("KeygenConsumer: Timeout waiting for Keygen event response") + logger.Warn("KeygenConsumer: Timeout waiting for keygen event response") _ = msg.Nak() } diff --git a/pkg/identity/identity.go b/pkg/identity/identity.go index e29d3f7..2fb9882 100644 --- a/pkg/identity/identity.go +++ b/pkg/identity/identity.go @@ -1,8 +1,6 @@ package identity import ( - "crypto/aes" - "crypto/cipher" "crypto/ed25519" "crypto/rand" "encoding/hex" @@ -20,14 +18,12 @@ import ( "golang.org/x/term" "github.com/fystack/mpcium/pkg/common/pathutil" + "github.com/fystack/mpcium/pkg/encryption" "github.com/fystack/mpcium/pkg/logger" "github.com/fystack/mpcium/pkg/types" "github.com/spf13/viper" ) -const AES_GCM_Nonce_Size = 12 -const AES_SYMMETRICKEY_Size = 32 - // NodeIdentity represents a node's identity information type NodeIdentity struct { NodeName string `json:"node_name"` @@ -64,12 +60,9 @@ type fileStore struct { publicKeys map[string][]byte mu sync.RWMutex - // Cached private key privateKey []byte initiatorPubKey []byte - - //Cached ecdh symmetric key - symmetricKeys map[string][]byte + symmetricKeys map[string][]byte } // NewFileStore creates a new identity store @@ -310,56 +303,6 @@ func generateRandom(nonceSize int) ([]byte, error) { return nonce, nil } -// encryptAEAD encrypts plaintext using AES-GCM with authentication. -func encryptAEAD(symmetricKey []byte, plaintext []byte) ([]byte, error) { - // Create AES cipher block - block, err := aes.NewCipher(symmetricKey) - if err != nil { - return nil, fmt.Errorf("failed to create AES cipher: %w", err) - } - - nonce, err := generateRandom(AES_GCM_Nonce_Size) - if err != nil { - return nil, fmt.Errorf("failed to generate nonce: %w", err) - } - - aead, err := cipher.NewGCM(block) - if err != nil { - return nil, fmt.Errorf("failed to create GCM: %w", err) - } - - ciphertext := aead.Seal(nil, nonce, plaintext, nil) - - return append(nonce, ciphertext...), nil -} - -// decryptAEAD decrypts ciphertext using AES-GCM with authentication. -func decryptAEAD(symmetricKey []byte, ciphertext []byte) ([]byte, error) { - block, err := aes.NewCipher(symmetricKey) - if err != nil { - return nil, fmt.Errorf("failed to create AES cipher: %w", err) - } - - aead, err := cipher.NewGCM(block) - if err != nil { - return nil, fmt.Errorf("failed to create GCM: %w", err) - } - - if len(ciphertext) < AES_GCM_Nonce_Size { - return nil, errors.New("ciphertext too short") - } - nonce := ciphertext[:AES_GCM_Nonce_Size] - ciphertext = ciphertext[AES_GCM_Nonce_Size:] - - // Decrypt with no additional data (nil) - plaintext, err := aead.Open(nil, nonce, ciphertext, nil) - if err != nil { - return nil, fmt.Errorf("decryption failed: %w", err) - } - - return plaintext, nil -} - func (s *fileStore) EncryptMessage(plaintext []byte, peerID string) ([]byte, error) { key, err := s.GetSymmetricKey(peerID) if err != nil { @@ -370,7 +313,7 @@ func (s *fileStore) EncryptMessage(plaintext []byte, peerID string) ([]byte, err return nil, fmt.Errorf("no symmetric key for peer %s", peerID) } - return encryptAEAD(key, plaintext) + return encryption.EncryptAESGCMWithNonceEmbed(plaintext, key) } func (s *fileStore) DecryptMessage(cipher []byte, peerID string) ([]byte, error) { @@ -383,7 +326,7 @@ func (s *fileStore) DecryptMessage(cipher []byte, peerID string) ([]byte, error) if key == nil { return nil, fmt.Errorf("no symmetric key for peer %s", peerID) } - return decryptAEAD(key, cipher) + return encryption.DecryptAESGCMWithNonceEmbed(cipher, key) } // Sign ECDH key exchange message From bf76a2014f130128ad4eae354159bbc8cf1915ac Mon Sep 17 00:00:00 2001 From: anhthii Date: Wed, 13 Aug 2025 15:31:58 +0700 Subject: [PATCH 20/23] Error handling for ecdh session --- cmd/mpcium/main.go | 24 +++++++++++++++++++++--- pkg/eventconsumer/event_consumer.go | 8 ++++---- pkg/mpc/key_exchange_session.go | 5 +++++ pkg/mpc/node.go | 10 +++++++--- 4 files changed, 37 insertions(+), 10 deletions(-) diff --git a/cmd/mpcium/main.go b/cmd/mpcium/main.go index 1cc6e2f..ee6c6d2 100644 --- a/cmd/mpcium/main.go +++ b/cmd/mpcium/main.go @@ -176,6 +176,9 @@ func runNode(ctx context.Context, c *cli.Command) error { ) defer mpcNode.Close() + // ECDH session for DH key exchange + ecdhSession := mpcNode.GetECDHSession() + eventConsumer := eventconsumer.NewEventConsumer( mpcNode, pubsub, @@ -204,7 +207,7 @@ func runNode(ctx context.Context, c *cli.Command) error { logger.Info("[READY] Node is ready", "nodeID", nodeID) logger.Info("Waiting for ECDH key exchange to complete...", "nodeID", nodeID) - if err := mpcNode.GetDHSession().WaitForExchangeComplete(); err != nil { + if err := ecdhSession.WaitForExchangeComplete(); err != nil { logger.Fatal("ECDH exchange failed", err) } @@ -226,13 +229,13 @@ func runNode(ctx context.Context, c *cli.Command) error { logger.Error("Failed to close signing consumer", err) } - if err := mpcNode.GetDHSession().Close(); err != nil { + if err := ecdhSession.Close(); err != nil { logger.Error("Failed to close ECDH session", err) } }() var wg sync.WaitGroup - errChan := make(chan error, 2) + errChan := make(chan error, 3) wg.Add(1) go func() { @@ -256,6 +259,21 @@ func runNode(ctx context.Context, c *cli.Command) error { logger.Info("Signing consumer finished successfully") }() + go func() { + for { + select { + case <-appContext.Done(): + return + case err := <-ecdhSession.ErrChan(): + if err != nil { + logger.Error("ECDH session error", err) + errChan <- fmt.Errorf("ecdh session error: %w", err) + return + } + } + } + }() + go func() { wg.Wait() logger.Info("All consumers have finished") diff --git a/pkg/eventconsumer/event_consumer.go b/pkg/eventconsumer/event_consumer.go index 8b88ff2..c84e684 100644 --- a/pkg/eventconsumer/event_consumer.go +++ b/pkg/eventconsumer/event_consumer.go @@ -654,11 +654,11 @@ func (ec *eventConsumer) consumeReshareEvent() error { return } newSession.ListenToIncomingMessageAsync() - // In resharing process, we need to ensure that the new session is aware of the old committee peers. - // Then new committee peers can start listening to the old committee peers, and thus enable receiving direct messages from them. - legacyCommitteePeers := newSession.GetLegacyCommitteePeers() - newSession.ListenToPeersAsync(legacyCommitteePeers) + // Then new committee peers can start listening to the old committee peers + // and thus enable receiving direct messages from them. + extraOldCommiteePeers := newSession.GetLegacyCommitteePeers() + newSession.ListenToPeersAsync(extraOldCommiteePeers) } ec.warmUpSession() diff --git a/pkg/mpc/key_exchange_session.go b/pkg/mpc/key_exchange_session.go index ecf6356..496dc6c 100644 --- a/pkg/mpc/key_exchange_session.go +++ b/pkg/mpc/key_exchange_session.go @@ -31,6 +31,7 @@ type ECDHSession interface { StartKeyExchange() error BroadcastPublicKey() error WaitForExchangeComplete() error + ErrChan() <-chan error Close() error } @@ -128,6 +129,10 @@ func (e *ecdhSession) StartKeyExchange() error { return nil } +func (s *ecdhSession) ErrChan() <-chan error { + return s.errCh +} + func (s *ecdhSession) Close() error { err := s.ecdhSub.Unsubscribe() if err != nil { diff --git a/pkg/mpc/node.go b/pkg/mpc/node.go index 96da3c5..a881554 100644 --- a/pkg/mpc/node.go +++ b/pkg/mpc/node.go @@ -39,7 +39,7 @@ type Node struct { identityStore identity.Store peerRegistry PeerRegistry - dhSession ECDHSession + ecdhSession ECDHSession } func NewNode( @@ -70,7 +70,7 @@ func NewNode( keyinfoStore: keyinfoStore, peerRegistry: peerRegistry, identityStore: identityStore, - dhSession: dhSession, + ecdhSession: dhSession, } node.ecdsaPreParams = node.generatePreParams() @@ -425,8 +425,12 @@ func (p *Node) Close() { } } +func (p *Node) GetECDHSession() ECDHSession { + return p.ecdhSession +} + func (p *Node) GetDHSession() ECDHSession { - return p.dhSession + return p.ecdhSession } func (p *Node) generatePreParams() []*keygen.LocalPreParams { From 1e3021dd19c60789196361ba67fc58e0ab918e4b Mon Sep 17 00:00:00 2001 From: anhthii Date: Wed, 13 Aug 2025 15:45:37 +0700 Subject: [PATCH 21/23] Refactor session --- pkg/mpc/session.go | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/pkg/mpc/session.go b/pkg/mpc/session.go index 1b0ffc0..b1a76b5 100644 --- a/pkg/mpc/session.go +++ b/pkg/mpc/session.go @@ -108,14 +108,19 @@ func (s *session) handleTssMessage(keyshare tss.Message) { for i, id := range routing.To { toIDs[i] = id.String() } - logger.Debug(fmt.Sprintf("%s Sending message", s.sessionType), "from", s.selfPartyID.String(), "to", toIDs, "isBroadcast", routing.IsBroadcast) - - //Note: Differentiate broadcast and p2p messages + logger.Debug( + fmt.Sprintf("%s Sending message", s.sessionType), + "from", + s.selfPartyID.String(), + "to", + toIDs, + "isBroadcast", + routing.IsBroadcast, + ) - //Broadcast message + // Broadcast message if routing.IsBroadcast && len(routing.To) == 0 { - //attach signature - signature, err := s.identityStore.SignMessage(&tssMsg) + signature, err := s.identityStore.SignMessage(&tssMsg) // attach signature if err != nil { s.ErrCh <- fmt.Errorf("failed to sign message: %w", err) return @@ -132,9 +137,9 @@ func (s *session) handleTssMessage(keyshare tss.Message) { s.ErrCh <- err return } - } else { //p2p message - //without signature - msg, err := types.MarshalTssMessage(&tssMsg) + } else { + // p2p message + msg, err := types.MarshalTssMessage(&tssMsg) // without signature if err != nil { s.ErrCh <- fmt.Errorf("failed to marshal tss message: %w", err) return @@ -145,7 +150,6 @@ func (s *session) handleTssMessage(keyshare tss.Message) { toNodeID := partyIDToNodeID(to) topic := s.topicComposer.ComposeDirectTopic(selfID, toNodeID) if selfID == toNodeID { - logger.Debug("---------Detected toself p2p message---------") err := s.direct.SendToSelf(topic, msg) if err != nil { logger.Error("Failed in SendToSelf direct message", err, "topic", topic) @@ -154,12 +158,13 @@ func (s *session) handleTssMessage(keyshare tss.Message) { } else { cipher, err := s.identityStore.EncryptMessage(msg, toNodeID) if err != nil { - logger.Error("AuthEncrypt Error: %w", err) + s.ErrCh <- fmt.Errorf("encrypt tss message error %w", err) + logger.Error("Encrypt tss message error", err, "topic", topic) } err = s.direct.SendToOther(topic, cipher) if err != nil { logger.Error("Failed in SendToOther direct message", err, "topic", topic) - s.ErrCh <- fmt.Errorf("failed to send direct message to %s", topic) + s.ErrCh <- fmt.Errorf("failed to send direct message to %w", err) } } } @@ -168,7 +173,6 @@ func (s *session) handleTssMessage(keyshare tss.Message) { func (s *session) receiveP2PTssMessage(topic string, cipher []byte) { senderID := extractSenderIDFromDirectTopic(topic) - if senderID == "" { s.ErrCh <- fmt.Errorf("failed to extract senderID from direct topic: the direct topic format is wrong") return @@ -186,7 +190,6 @@ func (s *session) receiveP2PTssMessage(topic string, cipher []byte) { return } } - msg, err := types.UnmarshalTssMessage(plaintext) if err != nil { s.ErrCh <- fmt.Errorf("failed to unmarshal message: %w", err) @@ -380,11 +383,10 @@ func walletIDWithVersion(walletID string, version int) string { } func extractSenderIDFromDirectTopic(topic string) string { - strs := strings.Split(topic, ":") - - // according to direct topic format, there will be 6 slices, senderID is the 4th one - if len(strs) == 6 { - return strs[3] + // E.g: keygen:direct:ecdsa::: + parts := strings.SplitN(topic, ":", 5) + if len(parts) >= 4 { + return parts[3] } return "" From 0049c7cafecaff1e6de38b9f14b614f46ed20f2e Mon Sep 17 00:00:00 2001 From: anhthii Date: Wed, 13 Aug 2025 16:45:52 +0700 Subject: [PATCH 22/23] Fix security warning on AES encryption --- pkg/encryption/aes.go | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/pkg/encryption/aes.go b/pkg/encryption/aes.go index c4e4f02..ba31b45 100644 --- a/pkg/encryption/aes.go +++ b/pkg/encryption/aes.go @@ -8,8 +8,6 @@ import ( "fmt" ) -const AESGCMNonceSize = 12 - func EncryptAESGCM(plain, key []byte) (ciphertext, nonce []byte, err error) { block, err := aes.NewCipher(key) if err != nil { @@ -51,7 +49,7 @@ func EncryptAESGCMWithNonceEmbed(plaintext, key []byte) ([]byte, error) { return nil, fmt.Errorf("failed to create GCM: %w", err) } - nonce := make([]byte, AESGCMNonceSize) + nonce := make([]byte, aead.NonceSize()) if _, err := rand.Read(nonce); err != nil { return nil, fmt.Errorf("failed to generate nonce: %w", err) } @@ -62,23 +60,23 @@ func EncryptAESGCMWithNonceEmbed(plaintext, key []byte) ([]byte, error) { // DecryptAESGCMWithNonceEmbed decrypts ciphertext where the nonce is embedded at the start of the slice. func DecryptAESGCMWithNonceEmbed(data, key []byte) ([]byte, error) { - if len(data) < AESGCMNonceSize { - return nil, errors.New("ciphertext too short") - } - - nonce := data[:AESGCMNonceSize] - ciphertext := data[AESGCMNonceSize:] - block, err := aes.NewCipher(key) if err != nil { return nil, fmt.Errorf("failed to create AES cipher: %w", err) } - aead, err := cipher.NewGCM(block) if err != nil { return nil, fmt.Errorf("failed to create GCM: %w", err) } + nonceSize := aead.NonceSize() + if len(data) < nonceSize { + return nil, errors.New("ciphertext too short") + } + + nonce := data[:nonceSize] + ciphertext := data[nonceSize:] + plaintext, err := aead.Open(nil, nonce, ciphertext, nil) if err != nil { return nil, fmt.Errorf("decryption failed: %w", err) From 170245c9dc1f3ca57dd68fd8dc7fc52a9bef8385 Mon Sep 17 00:00:00 2001 From: anhthii Date: Wed, 13 Aug 2025 16:59:28 +0700 Subject: [PATCH 23/23] Update ci/cd --- .github/workflows/ci.yml | 86 +++++++++------------------------ .github/workflows/e2e-tests.yml | 2 +- 2 files changed, 25 insertions(+), 63 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 48a8329..d090edb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,9 @@ on: pull_request: branches: ["*"] +env: + GO_VERSION: "1.24" + jobs: test: runs-on: ubuntu-latest @@ -15,19 +18,10 @@ jobs: uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: - go-version: "1.23" - - - name: Cache Go modules - uses: actions/cache@v3 - with: - path: | - ~/.cache/go-build - ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go- + go-version: ${{ env.GO_VERSION }} + cache: true - name: Install dependencies run: go mod download @@ -50,9 +44,13 @@ jobs: uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: - go-version: "1.23" + go-version: ${{ env.GO_VERSION }} + cache: true + + - name: Clean Go build cache + run: go clean -cache -modcache - name: Run golangci-lint uses: golangci/golangci-lint-action@v3 @@ -74,19 +72,10 @@ jobs: uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: - go-version: "1.23" - - - name: Cache Go modules - uses: actions/cache@v3 - with: - path: | - ~/.cache/go-build - ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go- + go-version: ${{ env.GO_VERSION }} + cache: true - name: Install dependencies run: go mod download @@ -153,19 +142,10 @@ jobs: uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: - go-version: "1.23" - - - name: Cache Go modules - uses: actions/cache@v3 - with: - path: | - ~/.cache/go-build - ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go- + go-version: ${{ env.GO_VERSION }} + cache: true - name: Install dependencies run: go mod download @@ -200,19 +180,10 @@ jobs: uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v4 - with: - go-version: "1.23" - - - name: Cache Go modules - uses: actions/cache@v3 + uses: actions/setup-go@v5 with: - path: | - ~/.cache/go-build - ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go- + go-version: ${{ env.GO_VERSION }} + cache: true - name: Install dependencies run: go mod download @@ -293,19 +264,10 @@ jobs: uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v4 - with: - go-version: "1.23" - - - name: Cache Go modules - uses: actions/cache@v3 + uses: actions/setup-go@v5 with: - path: | - ~/.cache/go-build - ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go- + go-version: ${{ env.GO_VERSION }} + cache: true - name: Build mpcium run: go build -v ./cmd/mpcium diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 8714318..e3443f3 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -7,7 +7,7 @@ on: branches: [master] env: - GO_VERSION: "1.23" + GO_VERSION: "1.24" CGO_ENABLED: 0 DOCKER_BUILDKIT: 1 GO_BUILD_FLAGS: -trimpath -ldflags="-s -w"