mirror of
https://github.com/luxfi/mpc.git
synced 2026-09-24 00:26:24 +00:00
security(mpc): rip trusted-dealer threshold-BLS custody — H-2
BLS is consensus-only. The mpcd validator-key custody path generated a
threshold-BLS group key via a TRUSTED DEALER (blsThreshold.TrustedDealer:
one node, the lowest sorted party ID, generated the entire group secret
key and all shares, then distributed them) — a single point of full-key
compromise, with no DKG and no rogue-key PoP. That violates the
public/leaderless/permissionless invariant "no single node can sign or
extract a key".
Orphan-safe: this path was incomplete (no proof/sign-off doc unlike
cmp/frost; no deployment manifest provisions validator-BLS; BLS keygen
had no dispatch caller) and NOT live custody. A validator's real BLS
consensus key is its own NATIVE per-validator crypto/bls key (PoP +
aggregation, the consensus fast-path), held at the node — never
threshold-custodied here. Corona PQ-threshold custody (dealerless DKG)
stays. So a validator keyset = {native BLS (consensus, no custody) +
corona PQ-threshold (custodied)}.
Delete: bls_keygen_session.go, bls_signing_session.go, bls_config_marshal.go,
bls_session_test.go, signing_handler_bls.go. Remove KeyTypeBLS (mpc + types),
SessionTypeBLS, CreateBLS{KeyGen,Sign}Session, the BLS signing/keytype
dispatch cases, the keygen-result BLSPubKey field, and the bls: key-share
prefix. handlers_kms.go: drop the BLS-custody half of ValidatorKeySet and
the generate/sign/rotate BLS branches; KMS custodies corona only.
Verified: GOWORK=off go build ./... + go vet green; pkg/mpc, pkg/api,
pkg/types tests pass. Zero residual refs to deleted BLS symbols.
This commit is contained in:
+12
-21
@@ -14,12 +14,15 @@ import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
// ValidatorKeySet holds MPC wallet references for a validator's BLS and Corona keys.
|
||||
// ValidatorKeySet holds the MPC wallet reference for a validator's custodied
|
||||
// Corona PQ-threshold key. The validator's BLS consensus key is deliberately
|
||||
// NOT here: BLS is consensus-only and lives as the validator's own native
|
||||
// per-validator crypto/bls key (PoP + signature aggregation, the consensus
|
||||
// fast-path), never threshold-custodied by this service. Only the Corona
|
||||
// PQ-threshold key is custodied (dealerless DKG).
|
||||
type ValidatorKeySet struct {
|
||||
ValidatorID string `json:"validatorId"`
|
||||
BLSWalletID string `json:"blsWalletId"`
|
||||
CoronaWalletID string `json:"coronaWalletId"`
|
||||
BLSPublicKey string `json:"blsPublicKey"`
|
||||
CoronaPublicKey string `json:"coronaPublicKey"`
|
||||
Threshold int `json:"threshold"`
|
||||
Parties int `json:"parties"`
|
||||
@@ -89,8 +92,11 @@ func (s *Server) handleKMSGenerate(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
blsWalletID := fmt.Sprintf("validator-%s-bls", req.ValidatorID)
|
||||
blsResult, err := s.mpc.TriggerKeygen(orgID, blsWalletID)
|
||||
// Custody only the Corona PQ-threshold key (dealerless DKG). The validator's
|
||||
// BLS consensus key is native (held by the validator's node), never
|
||||
// threshold-custodied here.
|
||||
coronaWalletID := fmt.Sprintf("validator-%s-corona", req.ValidatorID)
|
||||
coronaResult, err := s.mpc.TriggerKeygen(orgID, coronaWalletID)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "already exists") {
|
||||
writeError(w, http.StatusConflict, err.Error())
|
||||
@@ -100,19 +106,10 @@ func (s *Server) handleKMSGenerate(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
coronaWalletID := fmt.Sprintf("validator-%s-corona", req.ValidatorID)
|
||||
coronaResult, err := s.mpc.TriggerKeygen(orgID, coronaWalletID)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
ks := &ValidatorKeySet{
|
||||
ValidatorID: req.ValidatorID,
|
||||
BLSWalletID: blsResult.WalletID,
|
||||
CoronaWalletID: coronaResult.WalletID,
|
||||
BLSPublicKey: blsResult.ECDSAPubKey,
|
||||
CoronaPublicKey: coronaResult.EDDSAPubKey,
|
||||
Threshold: req.Threshold,
|
||||
Parties: req.Parties,
|
||||
@@ -186,12 +183,10 @@ func (s *Server) handleKMSSign(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var walletID string
|
||||
switch req.KeyType {
|
||||
case "bls":
|
||||
walletID = ks.BLSWalletID
|
||||
case "corona":
|
||||
walletID = ks.CoronaWalletID
|
||||
default:
|
||||
writeError(w, http.StatusBadRequest, "key_type must be 'bls' or 'corona'")
|
||||
writeError(w, http.StatusBadRequest, "key_type must be 'corona'")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -229,10 +224,6 @@ func (s *Server) handleKMSRotate(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.mpc.TriggerReshare(orgID, ks.BLSWalletID, req.NewThreshold, req.NewParticipants); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, fmt.Sprintf("bls reshare: %v", err))
|
||||
return
|
||||
}
|
||||
if err := s.mpc.TriggerReshare(orgID, ks.CoronaWalletID, req.NewThreshold, req.NewParticipants); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, fmt.Sprintf("corona reshare: %v", err))
|
||||
return
|
||||
|
||||
@@ -11,7 +11,6 @@ type KeygenResultEvent struct {
|
||||
ECDSAPubKey []byte `json:"ecdsa_pub_key"`
|
||||
EDDSAPubKey []byte `json:"eddsa_pub_key"`
|
||||
SR25519PubKey []byte `json:"sr25519_pub_key,omitempty"`
|
||||
BLSPubKey []byte `json:"bls_pub_key,omitempty"`
|
||||
|
||||
ResultType ResultType `json:"result_type"`
|
||||
ErrorReason string `json:"error_reason"`
|
||||
|
||||
@@ -236,8 +236,6 @@ func (ec *eventConsumer) consumeTxSigningEvent() error {
|
||||
ec.handleSigningEventFROST(&msg, natMsg)
|
||||
case types.KeyTypeSR25519:
|
||||
ec.handleSigningEventSR25519(&msg, natMsg)
|
||||
case types.KeyTypeBLS:
|
||||
ec.handleSigningEventBLS(&msg, natMsg)
|
||||
default:
|
||||
logger.Error("Unsupported key type for signing", nil,
|
||||
"walletID", msg.WalletID,
|
||||
@@ -637,8 +635,6 @@ func sessionTypeFromKeyType(keyType types.KeyType) (mpc.SessionType, error) {
|
||||
return mpc.SessionTypeEDDSA, nil
|
||||
case types.KeyTypeSR25519:
|
||||
return mpc.SessionTypeSR25519, nil
|
||||
case types.KeyTypeBLS:
|
||||
return mpc.SessionTypeBLS, nil
|
||||
default:
|
||||
logger.Warn("Unsupported key type", "keyType", keyType)
|
||||
return "", fmt.Errorf("unsupported key type: %v", keyType)
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
package eventconsumer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
|
||||
"github.com/luxfi/mpc/pkg/logger"
|
||||
"github.com/luxfi/mpc/pkg/mpc"
|
||||
"github.com/luxfi/mpc/pkg/types"
|
||||
)
|
||||
|
||||
// handleSigningEventBLS handles signing events for BLS protocol (BLS12-381 threshold)
|
||||
func (ec *eventConsumer) handleSigningEventBLS(msg *types.SignTxMessage, natMsg *nats.Msg) {
|
||||
// Check for duplicate session and track if new
|
||||
if ec.checkDuplicateSession(msg.OrgID, msg.WalletID, msg.TxID) {
|
||||
duplicateErr := fmt.Errorf("duplicate signing request detected for walletID=%s txID=%s", msg.WalletID, msg.TxID)
|
||||
ec.handleSigningSessionError(
|
||||
msg.WalletID,
|
||||
msg.TxID,
|
||||
msg.NetworkInternalCode,
|
||||
duplicateErr,
|
||||
"Duplicate session",
|
||||
natMsg,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Get key info to determine signers
|
||||
keyInfo, err := ec.keyinfoStore.Get(msg.WalletID)
|
||||
if err != nil {
|
||||
ec.handleSigningSessionError(
|
||||
msg.WalletID,
|
||||
msg.TxID,
|
||||
msg.NetworkInternalCode,
|
||||
err,
|
||||
"Failed to get key info",
|
||||
natMsg,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Create BLS signing session
|
||||
session, err := ec.node.CreateBLSSignSession(
|
||||
msg.TxID, // Use TxID as sessionID
|
||||
msg.WalletID,
|
||||
msg.Tx, // Use transaction bytes as message hash
|
||||
keyInfo.ParticipantPeerIDs, // Use all participants as signers
|
||||
ec.signingResultQueue,
|
||||
msg.OrgID,
|
||||
)
|
||||
if err != nil {
|
||||
// Check if the error is due to node not being in participant list
|
||||
if errors.Is(err, mpc.ErrNotInParticipantList) {
|
||||
logger.Info("Node is not in participant list for this wallet, skipping signing",
|
||||
"walletID", msg.WalletID,
|
||||
"txID", msg.TxID,
|
||||
"nodeID", ec.node.ID(),
|
||||
)
|
||||
return // Skip signing instead of treating as error
|
||||
}
|
||||
|
||||
logger.Error("Failed to create BLS signing session", err)
|
||||
ec.handleSigningSessionError(
|
||||
msg.WalletID,
|
||||
msg.TxID,
|
||||
msg.NetworkInternalCode,
|
||||
err,
|
||||
"Failed to create signing session",
|
||||
natMsg,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Mark session as already processed
|
||||
ec.addSession(msg.OrgID, msg.WalletID, msg.TxID)
|
||||
|
||||
// Initialize the session
|
||||
session.Init()
|
||||
|
||||
ctx, done := context.WithCancel(context.Background())
|
||||
|
||||
// Monitor for errors
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case err := <-session.ErrChan():
|
||||
if err != nil {
|
||||
ec.handleSigningSessionError(
|
||||
msg.WalletID,
|
||||
msg.TxID,
|
||||
msg.NetworkInternalCode,
|
||||
err,
|
||||
"Failed to sign tx",
|
||||
natMsg,
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Start listening to incoming messages
|
||||
session.ListenToIncomingMessageAsync()
|
||||
|
||||
// Small delay to ensure all nodes are ready
|
||||
time.Sleep(DefaultSessionStartupDelay * time.Millisecond)
|
||||
|
||||
// Start processing outbound messages
|
||||
go session.ProcessOutboundMessage()
|
||||
|
||||
// Wait for completion
|
||||
go func() {
|
||||
logger.Info("Waiting for BLS signing session to finish", "sessionID", msg.TxID, "walletID", msg.WalletID)
|
||||
result := session.WaitForFinish()
|
||||
logger.Info("BLS signing session WaitForFinish returned", "sessionID", msg.TxID, "result", result)
|
||||
done()
|
||||
ec.sendReplyToRemoveMsg(natMsg)
|
||||
logger.Info("BLS signing session completed", "result", result)
|
||||
}()
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
package mpc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"github.com/luxfi/crypto/bls"
|
||||
"github.com/luxfi/threshold/pkg/party"
|
||||
blsThreshold "github.com/luxfi/threshold/protocols/bls"
|
||||
)
|
||||
|
||||
// blsConfigMarshal is the CBOR-friendly representation of a BLS threshold Config.
|
||||
// MUST use CBOR: bls.PublicKey and bls.SecretKey do not have JSON marshalers.
|
||||
type blsConfigMarshal struct {
|
||||
ID party.ID
|
||||
Threshold int
|
||||
TotalParties int
|
||||
PublicKey []byte // 48 bytes (compressed G1 point)
|
||||
SecretShare []byte // 32 bytes (BLS secret key)
|
||||
VerificationKeys []blsVerificationKeyMarshal
|
||||
}
|
||||
|
||||
type blsVerificationKeyMarshal struct {
|
||||
ID party.ID
|
||||
Key []byte // 48 bytes (compressed G1 point)
|
||||
}
|
||||
|
||||
// MarshalBLSConfig serializes a BLS threshold Config to CBOR bytes.
|
||||
func MarshalBLSConfig(config *blsThreshold.Config) ([]byte, error) {
|
||||
if config == nil {
|
||||
return nil, fmt.Errorf("config is nil")
|
||||
}
|
||||
|
||||
// Marshal public key
|
||||
pkBytes := bls.PublicKeyToCompressedBytes(config.PublicKey)
|
||||
if pkBytes == nil {
|
||||
return nil, fmt.Errorf("failed to marshal public key")
|
||||
}
|
||||
|
||||
// Marshal secret share
|
||||
skBytes := bls.SecretKeyToBytes(config.SecretShare)
|
||||
if skBytes == nil {
|
||||
return nil, fmt.Errorf("failed to marshal secret share")
|
||||
}
|
||||
|
||||
// Marshal verification keys
|
||||
vks := make([]blsVerificationKeyMarshal, 0, len(config.VerificationKeys))
|
||||
for id, pk := range config.VerificationKeys {
|
||||
vkBytes := bls.PublicKeyToCompressedBytes(pk)
|
||||
if vkBytes == nil {
|
||||
return nil, fmt.Errorf("failed to marshal verification key for %s", id)
|
||||
}
|
||||
vks = append(vks, blsVerificationKeyMarshal{
|
||||
ID: id,
|
||||
Key: vkBytes,
|
||||
})
|
||||
}
|
||||
|
||||
cm := &blsConfigMarshal{
|
||||
ID: config.ID,
|
||||
Threshold: config.Threshold,
|
||||
TotalParties: config.TotalParties,
|
||||
PublicKey: pkBytes,
|
||||
SecretShare: skBytes,
|
||||
VerificationKeys: vks,
|
||||
}
|
||||
|
||||
return cbor.Marshal(cm)
|
||||
}
|
||||
|
||||
// UnmarshalBLSConfig deserializes CBOR bytes to a BLS threshold Config.
|
||||
func UnmarshalBLSConfig(data []byte) (*blsThreshold.Config, error) {
|
||||
cm := &blsConfigMarshal{}
|
||||
if err := cbor.Unmarshal(data, cm); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal BLS config: %w", err)
|
||||
}
|
||||
|
||||
// Unmarshal public key
|
||||
pk, err := bls.PublicKeyFromCompressedBytes(cm.PublicKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal public key: %w", err)
|
||||
}
|
||||
|
||||
// Unmarshal secret share
|
||||
sk, err := bls.SecretKeyFromBytes(cm.SecretShare)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal secret share: %w", err)
|
||||
}
|
||||
|
||||
// Unmarshal verification keys
|
||||
vks := make(map[party.ID]*bls.PublicKey, len(cm.VerificationKeys))
|
||||
for _, vk := range cm.VerificationKeys {
|
||||
key, err := bls.PublicKeyFromCompressedBytes(vk.Key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal verification key for %s: %w", vk.ID, err)
|
||||
}
|
||||
vks[vk.ID] = key
|
||||
}
|
||||
|
||||
return &blsThreshold.Config{
|
||||
ID: cm.ID,
|
||||
Threshold: cm.Threshold,
|
||||
TotalParties: cm.TotalParties,
|
||||
PublicKey: pk,
|
||||
SecretShare: sk,
|
||||
VerificationKeys: vks,
|
||||
}, nil
|
||||
}
|
||||
@@ -1,404 +0,0 @@
|
||||
package mpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/luxfi/crypto/bls"
|
||||
"github.com/luxfi/threshold/pkg/party"
|
||||
blsThreshold "github.com/luxfi/threshold/protocols/bls"
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/luxfi/mpc/pkg/event"
|
||||
"github.com/luxfi/mpc/pkg/identity"
|
||||
"github.com/luxfi/mpc/pkg/keyinfo"
|
||||
"github.com/luxfi/mpc/pkg/kvstore"
|
||||
"github.com/luxfi/mpc/pkg/messaging"
|
||||
"github.com/luxfi/mpc/pkg/types"
|
||||
"github.com/luxfi/mpc/pkg/utils"
|
||||
)
|
||||
|
||||
// blsShareMessage is sent from the dealer to each party with their config share.
|
||||
type blsShareMessage struct {
|
||||
SecretShare []byte `json:"secret_share"`
|
||||
PublicKey []byte `json:"public_key"`
|
||||
Threshold int `json:"threshold"`
|
||||
TotalParties int `json:"total_parties"`
|
||||
VerificationKeys map[party.ID][]byte `json:"verification_keys"`
|
||||
}
|
||||
|
||||
// BLSKeygenSession interface for BLS keygen
|
||||
type BLSKeygenSession interface {
|
||||
Session
|
||||
// GetPublicKey returns the BLS group public key after keygen completes
|
||||
GetPublicKey() []byte
|
||||
}
|
||||
|
||||
type blsKeygenSession struct {
|
||||
session
|
||||
config *blsThreshold.Config
|
||||
resultMutex sync.Mutex
|
||||
done bool
|
||||
resultErr error
|
||||
}
|
||||
|
||||
func newBLSKeygenSession(
|
||||
walletID string,
|
||||
pubSub messaging.PubSub,
|
||||
selfPartyID party.ID,
|
||||
partyIDs []party.ID,
|
||||
threshold int,
|
||||
kvstore kvstore.KVStore,
|
||||
keyinfoStore keyinfo.Store,
|
||||
resultQueue messaging.MessageQueue,
|
||||
identityStore identity.Store,
|
||||
orgID string,
|
||||
) *blsKeygenSession {
|
||||
return &blsKeygenSession{
|
||||
session: session{
|
||||
walletID: walletID,
|
||||
orgID: orgID,
|
||||
pubSub: pubSub,
|
||||
selfPartyID: selfPartyID,
|
||||
partyIDs: partyIDs,
|
||||
subscriberList: []messaging.Subscription{},
|
||||
rounds: 1, // BLS keygen is single-round (dealer distributes)
|
||||
outCh: make(chan msg, 100),
|
||||
errCh: make(chan error, 10),
|
||||
finishCh: make(chan bool, 1),
|
||||
externalFinishChan: make(chan string, 1),
|
||||
threshold: threshold,
|
||||
kvstore: kvstore,
|
||||
keyinfoStore: keyinfoStore,
|
||||
resultQueue: resultQueue,
|
||||
logger: zerolog.New(utils.ZerologConsoleWriter()).With().Timestamp().Logger(),
|
||||
processing: newDedupMap(),
|
||||
processingLock: sync.Mutex{},
|
||||
topicComposer: &TopicComposer{
|
||||
ComposeBroadcastTopic: func() string {
|
||||
return fmt.Sprintf("keygen:broadcast:bls:%s", walletID)
|
||||
},
|
||||
ComposeDirectTopic: func(nodeID string) string {
|
||||
return fmt.Sprintf("keygen:direct:bls:%s:%s", nodeID, walletID)
|
||||
},
|
||||
},
|
||||
identityStore: identityStore,
|
||||
},
|
||||
done: false,
|
||||
}
|
||||
}
|
||||
|
||||
// isDealer returns true if this node is the dealer (lowest sorted party ID).
|
||||
func (s *blsKeygenSession) isDealer() bool {
|
||||
sorted := make([]string, len(s.partyIDs))
|
||||
for i, id := range s.partyIDs {
|
||||
sorted[i] = string(id)
|
||||
}
|
||||
sort.Strings(sorted)
|
||||
return sorted[0] == string(s.selfPartyID)
|
||||
}
|
||||
|
||||
// ListenToIncomingMessageAsync subscribes to BLS keygen messages
|
||||
func (s *blsKeygenSession) ListenToIncomingMessageAsync() {
|
||||
// Subscribe to direct messages (non-dealer nodes receive their share here)
|
||||
directTopic := s.topicComposer.ComposeDirectTopic(extractNodeID(string(s.selfPartyID)))
|
||||
directSub, err := s.pubSub.Subscribe(directTopic, func(m *nats.Msg) {
|
||||
s.logger.Debug().
|
||||
Str("topic", directTopic).
|
||||
Int("size", len(m.Data)).
|
||||
Msg("BLS: Received direct message")
|
||||
s.ProcessInboundMessage(m.Data)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
s.logger.Error().Err(err).Msgf("BLS: Failed to subscribe to direct topic %s", directTopic)
|
||||
s.errCh <- err
|
||||
return
|
||||
}
|
||||
|
||||
s.subscriberList = append(s.subscriberList, directSub)
|
||||
|
||||
s.logger.Info().
|
||||
Str("direct", directTopic).
|
||||
Bool("isDealer", s.isDealer()).
|
||||
Msg("BLS: Listening to incoming messages")
|
||||
}
|
||||
|
||||
func (s *blsKeygenSession) Init() {
|
||||
s.logger.Info().
|
||||
Str("walletID", s.walletID).
|
||||
Int("threshold", s.threshold).
|
||||
Int("partyCount", len(s.partyIDs)).
|
||||
Str("selfPartyID", string(s.selfPartyID)).
|
||||
Bool("isDealer", s.isDealer()).
|
||||
Msg("[BLS] Initializing BLS keygen session")
|
||||
}
|
||||
|
||||
func (s *blsKeygenSession) ProcessInboundMessage(msgBytes []byte) {
|
||||
s.processingLock.Lock()
|
||||
defer s.processingLock.Unlock()
|
||||
|
||||
// Unmarshal wire format
|
||||
inboundMessage := &types.Message{}
|
||||
if err := json.Unmarshal(msgBytes, inboundMessage); err != nil {
|
||||
s.logger.Error().Err(err).Msg("BLS: ProcessInboundMessage unmarshal error")
|
||||
return
|
||||
}
|
||||
|
||||
// Verify Ed25519 signature on the wire message
|
||||
if err := s.verifyInboundSignature(inboundMessage); err != nil {
|
||||
s.logger.Warn().Err(err).Str("sender", inboundMessage.SenderNodeID).Msg("Dropping message with invalid signature")
|
||||
return
|
||||
}
|
||||
|
||||
// Deduplication
|
||||
msgHashStr := fmt.Sprintf("%x", utils.GetMessageHash(inboundMessage.Body))
|
||||
if s.processing.seen(msgHashStr) {
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the share message
|
||||
var shareMsg blsShareMessage
|
||||
if err := json.Unmarshal(inboundMessage.Body, &shareMsg); err != nil {
|
||||
s.logger.Error().Err(err).Msg("BLS: Failed to unmarshal share message")
|
||||
return
|
||||
}
|
||||
|
||||
// Reconstruct the BLS config from the share message
|
||||
sk, err := bls.SecretKeyFromBytes(shareMsg.SecretShare)
|
||||
if err != nil {
|
||||
s.logger.Error().Err(err).Msg("BLS: Failed to unmarshal secret share")
|
||||
s.errCh <- err
|
||||
return
|
||||
}
|
||||
|
||||
pk, err := bls.PublicKeyFromCompressedBytes(shareMsg.PublicKey)
|
||||
if err != nil {
|
||||
s.logger.Error().Err(err).Msg("BLS: Failed to unmarshal public key")
|
||||
s.errCh <- err
|
||||
return
|
||||
}
|
||||
|
||||
vks := make(map[party.ID]*bls.PublicKey, len(shareMsg.VerificationKeys))
|
||||
for id, vkBytes := range shareMsg.VerificationKeys {
|
||||
vk, err := bls.PublicKeyFromCompressedBytes(vkBytes)
|
||||
if err != nil {
|
||||
s.logger.Error().Err(err).Str("partyID", string(id)).Msg("BLS: Failed to unmarshal verification key")
|
||||
s.errCh <- err
|
||||
return
|
||||
}
|
||||
vks[id] = vk
|
||||
}
|
||||
|
||||
s.resultMutex.Lock()
|
||||
s.config = blsThreshold.NewConfig(
|
||||
s.selfPartyID,
|
||||
shareMsg.Threshold,
|
||||
shareMsg.TotalParties,
|
||||
sk,
|
||||
pk,
|
||||
vks,
|
||||
)
|
||||
s.done = true
|
||||
s.resultMutex.Unlock()
|
||||
|
||||
s.finishCh <- true
|
||||
}
|
||||
|
||||
func (s *blsKeygenSession) ProcessOutboundMessage() {
|
||||
s.logger.Info().Msgf("BLS: ProcessOutboundMessage started: %s", s.walletID)
|
||||
|
||||
if s.isDealer() {
|
||||
// Dealer: generate shares and distribute
|
||||
s.runDealer()
|
||||
}
|
||||
|
||||
// Wait for finish (dealer finishes after distributing, non-dealer after receiving)
|
||||
select {
|
||||
case <-s.finishCh:
|
||||
s.logger.Info().Msg("BLS: Received finish message during ProcessOutboundMessage")
|
||||
s.publishResult()
|
||||
return
|
||||
case err := <-s.errCh:
|
||||
s.logger.Error().Err(err).Msg("BLS: Received error during ProcessOutboundMessage")
|
||||
s.resultMutex.Lock()
|
||||
s.resultErr = err
|
||||
s.resultMutex.Unlock()
|
||||
s.publishResult()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (s *blsKeygenSession) runDealer() {
|
||||
ctx := context.Background()
|
||||
|
||||
dealer := &blsThreshold.TrustedDealer{
|
||||
Threshold: s.threshold,
|
||||
TotalParties: len(s.partyIDs),
|
||||
}
|
||||
|
||||
shares, groupPK, err := dealer.GenerateShares(ctx, s.partyIDs)
|
||||
if err != nil {
|
||||
s.logger.Error().Err(err).Msg("BLS: Dealer failed to generate shares")
|
||||
s.resultMutex.Lock()
|
||||
s.resultErr = err
|
||||
s.resultMutex.Unlock()
|
||||
s.finishCh <- true
|
||||
return
|
||||
}
|
||||
|
||||
// Get verification keys
|
||||
vks := blsThreshold.GetVerificationKeys(shares)
|
||||
|
||||
// Serialize verification keys once
|
||||
vkBytes := make(map[party.ID][]byte, len(vks))
|
||||
for id, vk := range vks {
|
||||
vkBytes[id] = bls.PublicKeyToCompressedBytes(vk)
|
||||
}
|
||||
|
||||
pkBytes := bls.PublicKeyToCompressedBytes(groupPK)
|
||||
|
||||
// Send each party its share
|
||||
for _, partyID := range s.partyIDs {
|
||||
sk := shares[partyID]
|
||||
shareMsg := blsShareMessage{
|
||||
SecretShare: bls.SecretKeyToBytes(sk),
|
||||
PublicKey: pkBytes,
|
||||
Threshold: s.threshold,
|
||||
TotalParties: len(s.partyIDs),
|
||||
VerificationKeys: vkBytes,
|
||||
}
|
||||
|
||||
body, err := json.Marshal(shareMsg)
|
||||
if err != nil {
|
||||
s.logger.Error().Err(err).Str("partyID", string(partyID)).Msg("BLS: Failed to marshal share for party")
|
||||
s.resultMutex.Lock()
|
||||
s.resultErr = err
|
||||
s.resultMutex.Unlock()
|
||||
s.finishCh <- true
|
||||
return
|
||||
}
|
||||
|
||||
if partyID == s.selfPartyID {
|
||||
// Dealer stores its own config directly
|
||||
s.resultMutex.Lock()
|
||||
s.config = blsThreshold.NewConfig(
|
||||
s.selfPartyID,
|
||||
s.threshold,
|
||||
len(s.partyIDs),
|
||||
sk,
|
||||
groupPK,
|
||||
vks,
|
||||
)
|
||||
s.done = true
|
||||
s.resultMutex.Unlock()
|
||||
continue
|
||||
}
|
||||
|
||||
// Send to other parties via direct message
|
||||
wireMsg := &types.Message{
|
||||
SessionID: s.walletID,
|
||||
SenderID: string(s.selfPartyID),
|
||||
RecipientIDs: []string{string(partyID)},
|
||||
Body: body,
|
||||
IsBroadcast: false,
|
||||
}
|
||||
|
||||
s.sendMsg(wireMsg)
|
||||
|
||||
s.logger.Info().
|
||||
Str("partyID", string(partyID)).
|
||||
Msg("BLS: Sent share to party")
|
||||
}
|
||||
|
||||
// Dealer is done
|
||||
s.finishCh <- true
|
||||
}
|
||||
|
||||
func (s *blsKeygenSession) publishResult() {
|
||||
s.resultMutex.Lock()
|
||||
defer s.resultMutex.Unlock()
|
||||
|
||||
if s.resultErr != nil {
|
||||
s.logger.Error().Err(s.resultErr).Msg("BLS: keygen failed with error")
|
||||
failureEvent := event.CreateKeygenFailure(
|
||||
s.walletID,
|
||||
map[string]any{
|
||||
"error": s.resultErr.Error(),
|
||||
"protocol": "BLS",
|
||||
},
|
||||
)
|
||||
evtData, _ := json.Marshal(failureEvent)
|
||||
if err := s.resultQueue.Enqueue(fmt.Sprintf("mpc.mpc_keygen_result.%s", s.walletID), evtData, nil); err != nil {
|
||||
s.logger.Error().Err(err).Msg("BLS: failed to publish keygen failure event")
|
||||
}
|
||||
// Always send to externalFinishChan so WaitForFinish() doesn't block forever
|
||||
s.externalFinishChan <- ""
|
||||
return
|
||||
}
|
||||
|
||||
if s.config == nil {
|
||||
s.logger.Error().Msg("BLS: No config available after keygen completion")
|
||||
s.externalFinishChan <- ""
|
||||
return
|
||||
}
|
||||
|
||||
// Save key share with bls prefix using CBOR
|
||||
shareBytes, err := MarshalBLSConfig(s.config)
|
||||
if err != nil {
|
||||
s.logger.Error().Err(err).Msg("BLS: Failed to marshal key share")
|
||||
s.externalFinishChan <- ""
|
||||
return
|
||||
}
|
||||
|
||||
blsKey := fmt.Sprintf("bls:%s", s.walletID)
|
||||
if err := s.kvstore.Put(OrgScopedKey(s.orgID, blsKey), shareBytes); err != nil {
|
||||
s.logger.Error().Err(err).Msgf("BLS: Failed to save key share for wallet %s", s.walletID)
|
||||
s.externalFinishChan <- ""
|
||||
return
|
||||
}
|
||||
|
||||
// Get public key hex
|
||||
var pubKeyHex string
|
||||
pkBytes := bls.PublicKeyToCompressedBytes(s.config.PublicKey)
|
||||
if len(pkBytes) > 0 {
|
||||
pubKeyHex = fmt.Sprintf("%x", pkBytes)
|
||||
s.logger.Info().
|
||||
Int("configPubKeyLen", len(pkBytes)).
|
||||
Str("pubKeyHex", pubKeyHex).
|
||||
Msg("[BLS-PUBLISH] PublicKey available")
|
||||
} else {
|
||||
s.logger.Warn().Msg("[BLS-PUBLISH] PublicKey is empty!")
|
||||
}
|
||||
|
||||
// Notify via external finish channel
|
||||
s.externalFinishChan <- pubKeyHex
|
||||
|
||||
s.logger.Info().
|
||||
Str("walletID", s.walletID).
|
||||
Str("publicKey", pubKeyHex).
|
||||
Msg("BLS keygen completed successfully")
|
||||
}
|
||||
|
||||
func (s *blsKeygenSession) Stop() {
|
||||
close(s.outCh)
|
||||
close(s.errCh)
|
||||
}
|
||||
|
||||
func (s *blsKeygenSession) WaitForFinish() string {
|
||||
return <-s.externalFinishChan
|
||||
}
|
||||
|
||||
// GetPublicKey returns the BLS group public key after keygen completes
|
||||
func (s *blsKeygenSession) GetPublicKey() []byte {
|
||||
s.resultMutex.Lock()
|
||||
defer s.resultMutex.Unlock()
|
||||
if s.config != nil && s.config.PublicKey != nil {
|
||||
return bls.PublicKeyToCompressedBytes(s.config.PublicKey)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,219 +0,0 @@
|
||||
package mpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/crypto/bls"
|
||||
"github.com/luxfi/threshold/pkg/party"
|
||||
blsThreshold "github.com/luxfi/threshold/protocols/bls"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestKeyTypeBLSConstant(t *testing.T) {
|
||||
assert.Equal(t, KeyType("bls"), KeyTypeBLS)
|
||||
}
|
||||
|
||||
func TestSessionTypeBLSConstant(t *testing.T) {
|
||||
assert.Equal(t, SessionType("bls"), SessionTypeBLS)
|
||||
}
|
||||
|
||||
func TestBLSConfigMarshalRoundtrip(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
partyA := party.ID("node-a")
|
||||
partyB := party.ID("node-b")
|
||||
partyC := party.ID("node-c")
|
||||
parties := []party.ID{partyA, partyB, partyC}
|
||||
|
||||
// Generate real BLS threshold shares
|
||||
dealer := &blsThreshold.TrustedDealer{
|
||||
Threshold: 2,
|
||||
TotalParties: 3,
|
||||
}
|
||||
|
||||
shares, groupPK, err := dealer.GenerateShares(ctx, parties)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, groupPK)
|
||||
|
||||
vks := blsThreshold.GetVerificationKeys(shares)
|
||||
|
||||
// Create config for party A
|
||||
config := blsThreshold.NewConfig(partyA, 2, 3, shares[partyA], groupPK, vks)
|
||||
require.NotNil(t, config)
|
||||
|
||||
// Marshal
|
||||
data, err := MarshalBLSConfig(config)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, data)
|
||||
|
||||
// Unmarshal
|
||||
config2, err := UnmarshalBLSConfig(data)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config2)
|
||||
|
||||
// Verify roundtrip
|
||||
assert.Equal(t, config.ID, config2.ID)
|
||||
assert.Equal(t, config.Threshold, config2.Threshold)
|
||||
assert.Equal(t, config.TotalParties, config2.TotalParties)
|
||||
|
||||
// Verify public key roundtrip
|
||||
pkBytes1 := bls.PublicKeyToCompressedBytes(config.PublicKey)
|
||||
pkBytes2 := bls.PublicKeyToCompressedBytes(config2.PublicKey)
|
||||
assert.Equal(t, pkBytes1, pkBytes2)
|
||||
|
||||
// Verify secret share roundtrip
|
||||
skBytes1 := bls.SecretKeyToBytes(config.SecretShare)
|
||||
skBytes2 := bls.SecretKeyToBytes(config2.SecretShare)
|
||||
assert.Equal(t, skBytes1, skBytes2)
|
||||
|
||||
// Verify verification keys roundtrip
|
||||
for id, vk := range config.VerificationKeys {
|
||||
vk2, ok := config2.VerificationKeys[id]
|
||||
require.True(t, ok, "missing verification key for %s", id)
|
||||
vkBytes1 := bls.PublicKeyToCompressedBytes(vk)
|
||||
vkBytes2 := bls.PublicKeyToCompressedBytes(vk2)
|
||||
assert.Equal(t, vkBytes1, vkBytes2, "verification key mismatch for %s", id)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBLSConfigMarshalNilConfig(t *testing.T) {
|
||||
_, err := MarshalBLSConfig(nil)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "config is nil")
|
||||
}
|
||||
|
||||
func TestBLSConfigUnmarshalInvalidData(t *testing.T) {
|
||||
_, err := UnmarshalBLSConfig([]byte("invalid cbor data"))
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestBLSSignAndVerifyWithMarshaledConfig(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
parties := []party.ID{
|
||||
party.ID("party1"),
|
||||
party.ID("party2"),
|
||||
party.ID("party3"),
|
||||
}
|
||||
|
||||
// Generate threshold shares
|
||||
dealer := &blsThreshold.TrustedDealer{
|
||||
Threshold: 2,
|
||||
TotalParties: 3,
|
||||
}
|
||||
|
||||
shares, groupPK, err := dealer.GenerateShares(ctx, parties)
|
||||
require.NoError(t, err)
|
||||
|
||||
vks := blsThreshold.GetVerificationKeys(shares)
|
||||
|
||||
// Create configs, marshal/unmarshal each one (simulating storage roundtrip)
|
||||
configs := make(map[party.ID]*blsThreshold.Config, 3)
|
||||
for _, id := range parties {
|
||||
original := blsThreshold.NewConfig(id, 2, 3, shares[id], groupPK, vks)
|
||||
|
||||
// Marshal and unmarshal to simulate BadgerDB storage
|
||||
data, err := MarshalBLSConfig(original)
|
||||
require.NoError(t, err)
|
||||
|
||||
restored, err := UnmarshalBLSConfig(data)
|
||||
require.NoError(t, err)
|
||||
|
||||
configs[id] = restored
|
||||
}
|
||||
|
||||
// Sign with each party
|
||||
message := []byte("threshold BLS signing test via CBOR roundtrip")
|
||||
sigShares := make([]*blsThreshold.SignatureShare, 0, 3)
|
||||
for _, id := range parties {
|
||||
share, err := configs[id].Sign(message)
|
||||
require.NoError(t, err, "party %s signing failed", id)
|
||||
require.NotNil(t, share.Signature)
|
||||
sigShares = append(sigShares, share)
|
||||
}
|
||||
|
||||
// Verify partial signatures
|
||||
for _, share := range sigShares {
|
||||
valid := configs[parties[0]].VerifyPartialSignature(share, message)
|
||||
require.True(t, valid, "partial signature from %s should be valid", share.PartyID)
|
||||
}
|
||||
|
||||
// Aggregate with threshold (2 of 3)
|
||||
aggSig, err := blsThreshold.AggregateSignatures(sigShares[:2], 2)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, aggSig)
|
||||
|
||||
// Verify aggregate signature
|
||||
valid := configs[parties[0]].VerifyAggregateSignature(message, aggSig)
|
||||
require.True(t, valid, "aggregate signature should verify against group key")
|
||||
|
||||
// Wrong message should fail
|
||||
require.False(t, configs[parties[0]].VerifyAggregateSignature([]byte("wrong message"), aggSig))
|
||||
}
|
||||
|
||||
func TestBLSKeygenSessionCreation(t *testing.T) {
|
||||
walletID := "test-wallet-bls"
|
||||
partyIDs := []party.ID{"node-a", "node-b", "node-c"}
|
||||
selfPartyID := party.ID("node-a")
|
||||
|
||||
session := newBLSKeygenSession(
|
||||
walletID,
|
||||
nil, // pubSub - nil for unit test
|
||||
selfPartyID,
|
||||
partyIDs,
|
||||
2, // threshold
|
||||
nil, // kvstore
|
||||
nil, // keyinfoStore
|
||||
nil, // resultQueue
|
||||
nil, // identityStore
|
||||
"", // orgID
|
||||
)
|
||||
|
||||
require.NotNil(t, session)
|
||||
assert.Equal(t, walletID, session.walletID)
|
||||
assert.Equal(t, selfPartyID, session.selfPartyID)
|
||||
assert.Equal(t, 2, session.threshold)
|
||||
assert.Equal(t, 1, session.rounds) // BLS keygen is single-round
|
||||
assert.False(t, session.done)
|
||||
assert.Nil(t, session.config)
|
||||
assert.Nil(t, session.resultErr)
|
||||
}
|
||||
|
||||
func TestBLSKeygenSessionIsDealer(t *testing.T) {
|
||||
partyIDs := []party.ID{"node-c", "node-a", "node-b"}
|
||||
|
||||
// node-a is the smallest sorted, so it should be the dealer
|
||||
sessionA := newBLSKeygenSession(
|
||||
"wallet1",
|
||||
nil,
|
||||
party.ID("node-a"),
|
||||
partyIDs,
|
||||
2,
|
||||
nil, nil, nil, nil, "",
|
||||
)
|
||||
assert.True(t, sessionA.isDealer())
|
||||
|
||||
// node-b is not the smallest
|
||||
sessionB := newBLSKeygenSession(
|
||||
"wallet1",
|
||||
nil,
|
||||
party.ID("node-b"),
|
||||
partyIDs,
|
||||
2,
|
||||
nil, nil, nil, nil, "",
|
||||
)
|
||||
assert.False(t, sessionB.isDealer())
|
||||
|
||||
// node-c is not the smallest
|
||||
sessionC := newBLSKeygenSession(
|
||||
"wallet1",
|
||||
nil,
|
||||
party.ID("node-c"),
|
||||
partyIDs,
|
||||
2,
|
||||
nil, nil, nil, nil, "",
|
||||
)
|
||||
assert.False(t, sessionC.isDealer())
|
||||
}
|
||||
@@ -1,384 +0,0 @@
|
||||
package mpc
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/luxfi/crypto/bls"
|
||||
"github.com/luxfi/threshold/pkg/party"
|
||||
blsThreshold "github.com/luxfi/threshold/protocols/bls"
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/luxfi/mpc/pkg/encoding"
|
||||
"github.com/luxfi/mpc/pkg/event"
|
||||
"github.com/luxfi/mpc/pkg/identity"
|
||||
"github.com/luxfi/mpc/pkg/keyinfo"
|
||||
"github.com/luxfi/mpc/pkg/kvstore"
|
||||
"github.com/luxfi/mpc/pkg/messaging"
|
||||
"github.com/luxfi/mpc/pkg/types"
|
||||
"github.com/luxfi/mpc/pkg/utils"
|
||||
)
|
||||
|
||||
// blsSignShareMessage is broadcast by each signer with their partial signature.
|
||||
type blsSignShareMessage struct {
|
||||
PartyID party.ID `json:"party_id"`
|
||||
Signature []byte `json:"signature"` // BLS signature bytes (96 bytes, compressed G2)
|
||||
}
|
||||
|
||||
// BLSSignSession is the interface for BLS signing sessions
|
||||
type BLSSignSession interface {
|
||||
Session
|
||||
}
|
||||
|
||||
type blsSigningSession struct {
|
||||
session
|
||||
config *blsThreshold.Config
|
||||
messageHash []byte
|
||||
signerIDs []party.ID
|
||||
|
||||
resultMutex sync.Mutex
|
||||
done bool
|
||||
resultErr error
|
||||
|
||||
// Collected signature shares from all signers
|
||||
shares []*blsThreshold.SignatureShare
|
||||
sharesLock sync.Mutex
|
||||
}
|
||||
|
||||
func newBLSSigningSession(
|
||||
sessionID string,
|
||||
walletID string,
|
||||
messageHash []byte,
|
||||
pubSub messaging.PubSub,
|
||||
selfPartyID party.ID,
|
||||
signerIDs []party.ID,
|
||||
kvstore kvstore.KVStore,
|
||||
keyinfoStore keyinfo.Store,
|
||||
resultQueue messaging.MessageQueue,
|
||||
identityStore identity.Store,
|
||||
orgID string,
|
||||
) (*blsSigningSession, error) {
|
||||
// Load and unmarshal key share inside withSecretErasure so that raw
|
||||
// share bytes on the stack are zeroed after parsing completes.
|
||||
blsKey := fmt.Sprintf("bls:%s", walletID)
|
||||
var config *blsThreshold.Config
|
||||
var loadErr error
|
||||
withSecretErasure(func() {
|
||||
shareBytes, err := GetKeyShareWithFallback(kvstore, orgID, blsKey)
|
||||
if err != nil {
|
||||
loadErr = fmt.Errorf("failed to get BLS key share: %w", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
for i := range shareBytes {
|
||||
shareBytes[i] = 0
|
||||
}
|
||||
}()
|
||||
config, err = UnmarshalBLSConfig(shareBytes)
|
||||
if err != nil {
|
||||
loadErr = fmt.Errorf("failed to unmarshal BLS key share: %w", err)
|
||||
}
|
||||
})
|
||||
if loadErr != nil {
|
||||
return nil, loadErr
|
||||
}
|
||||
|
||||
return &blsSigningSession{
|
||||
session: session{
|
||||
walletID: walletID,
|
||||
sessionID: sessionID,
|
||||
pubSub: pubSub,
|
||||
selfPartyID: selfPartyID,
|
||||
partyIDs: signerIDs,
|
||||
subscriberList: []messaging.Subscription{},
|
||||
rounds: 1, // BLS signing is single-round (sign + aggregate)
|
||||
outCh: make(chan msg, 100),
|
||||
errCh: make(chan error, 10),
|
||||
finishCh: make(chan bool, 1),
|
||||
externalFinishChan: make(chan string, 1),
|
||||
threshold: config.Threshold,
|
||||
kvstore: kvstore,
|
||||
keyinfoStore: keyinfoStore,
|
||||
resultQueue: resultQueue,
|
||||
logger: zerolog.New(utils.ZerologConsoleWriter()).With().Timestamp().Logger(),
|
||||
processing: newDedupMap(),
|
||||
processingLock: sync.Mutex{},
|
||||
topicComposer: &TopicComposer{
|
||||
ComposeBroadcastTopic: func() string {
|
||||
return fmt.Sprintf("sign:broadcast:bls:%s", sessionID)
|
||||
},
|
||||
ComposeDirectTopic: func(nodeID string) string {
|
||||
return fmt.Sprintf("sign:direct:bls:%s:%s", nodeID, sessionID)
|
||||
},
|
||||
},
|
||||
identityStore: identityStore,
|
||||
},
|
||||
config: config,
|
||||
messageHash: messageHash,
|
||||
signerIDs: signerIDs,
|
||||
shares: make([]*blsThreshold.SignatureShare, 0, len(signerIDs)),
|
||||
done: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListenToIncomingMessageAsync subscribes to BLS signing messages
|
||||
func (s *blsSigningSession) ListenToIncomingMessageAsync() {
|
||||
// Subscribe to broadcast messages (all signers broadcast their partial sig)
|
||||
broadcastTopic := s.topicComposer.ComposeBroadcastTopic()
|
||||
broadcastSub, err := s.pubSub.Subscribe(broadcastTopic, func(m *nats.Msg) {
|
||||
s.logger.Debug().
|
||||
Str("topic", broadcastTopic).
|
||||
Int("size", len(m.Data)).
|
||||
Msg("BLS: Received broadcast message")
|
||||
s.ProcessInboundMessage(m.Data)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
s.logger.Error().Err(err).Msgf("BLS: Failed to subscribe to broadcast topic %s", broadcastTopic)
|
||||
s.errCh <- err
|
||||
return
|
||||
}
|
||||
|
||||
s.subscriberList = append(s.subscriberList, broadcastSub)
|
||||
|
||||
s.logger.Info().
|
||||
Str("broadcast", broadcastTopic).
|
||||
Msg("BLS: Listening to incoming signing messages")
|
||||
}
|
||||
|
||||
func (s *blsSigningSession) Init() {
|
||||
s.logger.Info().
|
||||
Str("sessionID", s.sessionID).
|
||||
Str("walletID", s.walletID).
|
||||
Hex("messageHash", s.messageHash).
|
||||
Interface("signerIDs", s.signerIDs).
|
||||
Msg("Initializing BLS signing session")
|
||||
}
|
||||
|
||||
func (s *blsSigningSession) ProcessInboundMessage(msgBytes []byte) {
|
||||
s.processingLock.Lock()
|
||||
defer s.processingLock.Unlock()
|
||||
|
||||
// Unmarshal wire format
|
||||
inboundMessage := &types.Message{}
|
||||
if err := json.Unmarshal(msgBytes, inboundMessage); err != nil {
|
||||
s.logger.Error().Err(err).Msg("BLS: ProcessInboundMessage unmarshal error")
|
||||
return
|
||||
}
|
||||
|
||||
// Verify Ed25519 signature on the wire message
|
||||
if err := s.verifyInboundSignature(inboundMessage); err != nil {
|
||||
s.logger.Warn().Err(err).Str("sender", inboundMessage.SenderNodeID).Msg("Dropping message with invalid signature")
|
||||
return
|
||||
}
|
||||
|
||||
// Deduplication
|
||||
msgHashStr := fmt.Sprintf("%x", utils.GetMessageHash(inboundMessage.Body))
|
||||
if s.processing.seen(msgHashStr) {
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the signature share message
|
||||
var shareMsg blsSignShareMessage
|
||||
if err := json.Unmarshal(inboundMessage.Body, &shareMsg); err != nil {
|
||||
s.logger.Error().Err(err).Msg("BLS: Failed to unmarshal signature share")
|
||||
return
|
||||
}
|
||||
|
||||
// Reconstruct the BLS signature
|
||||
sig, err := bls.SignatureFromBytes(shareMsg.Signature)
|
||||
if err != nil {
|
||||
s.logger.Error().Err(err).Str("partyID", string(shareMsg.PartyID)).Msg("BLS: Invalid signature share")
|
||||
return
|
||||
}
|
||||
|
||||
// Verify the partial signature before accepting
|
||||
share := &blsThreshold.SignatureShare{
|
||||
PartyID: shareMsg.PartyID,
|
||||
Signature: sig,
|
||||
}
|
||||
|
||||
if !s.config.VerifyPartialSignature(share, s.messageHash) {
|
||||
s.logger.Warn().
|
||||
Str("partyID", string(shareMsg.PartyID)).
|
||||
Msg("BLS: Partial signature verification failed, rejecting")
|
||||
return
|
||||
}
|
||||
|
||||
s.sharesLock.Lock()
|
||||
s.shares = append(s.shares, share)
|
||||
shareCount := len(s.shares)
|
||||
s.sharesLock.Unlock()
|
||||
|
||||
s.logger.Info().
|
||||
Str("partyID", string(shareMsg.PartyID)).
|
||||
Int("shareCount", shareCount).
|
||||
Int("threshold", s.config.Threshold).
|
||||
Msg("BLS: Accepted verified signature share")
|
||||
|
||||
// Check if we have enough shares to aggregate
|
||||
if shareCount >= s.config.Threshold {
|
||||
s.resultMutex.Lock()
|
||||
if !s.done {
|
||||
s.done = true
|
||||
s.resultMutex.Unlock()
|
||||
s.finishCh <- true
|
||||
} else {
|
||||
s.resultMutex.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *blsSigningSession) ProcessOutboundMessage() {
|
||||
s.logger.Info().Msgf("BLS ProcessOutboundMessage started: %s", s.sessionID)
|
||||
|
||||
// Sign locally with our share and broadcast
|
||||
share, err := s.config.Sign(s.messageHash)
|
||||
if err != nil {
|
||||
s.logger.Error().Err(err).Msg("BLS: Failed to create partial signature")
|
||||
s.resultMutex.Lock()
|
||||
s.resultErr = err
|
||||
s.resultMutex.Unlock()
|
||||
s.publishResult()
|
||||
return
|
||||
}
|
||||
|
||||
// Add our own share
|
||||
s.sharesLock.Lock()
|
||||
s.shares = append(s.shares, share)
|
||||
shareCount := len(s.shares)
|
||||
s.sharesLock.Unlock()
|
||||
|
||||
// Broadcast our signature share
|
||||
shareMsg := blsSignShareMessage{
|
||||
PartyID: s.selfPartyID,
|
||||
Signature: bls.SignatureToBytes(share.Signature),
|
||||
}
|
||||
body, err := json.Marshal(shareMsg)
|
||||
if err != nil {
|
||||
s.logger.Error().Err(err).Msg("BLS: Failed to marshal signature share")
|
||||
s.resultMutex.Lock()
|
||||
s.resultErr = err
|
||||
s.resultMutex.Unlock()
|
||||
s.publishResult()
|
||||
return
|
||||
}
|
||||
|
||||
wireMsg := &types.Message{
|
||||
SessionID: s.sessionID,
|
||||
SenderID: string(s.selfPartyID),
|
||||
Body: body,
|
||||
IsBroadcast: true,
|
||||
}
|
||||
s.sendMsg(wireMsg)
|
||||
|
||||
s.logger.Info().
|
||||
Str("partyID", string(s.selfPartyID)).
|
||||
Int("shareCount", shareCount).
|
||||
Msg("BLS: Broadcast our signature share")
|
||||
|
||||
// Check if our share alone meets threshold (unlikely but possible for t=1)
|
||||
if shareCount >= s.config.Threshold {
|
||||
s.resultMutex.Lock()
|
||||
if !s.done {
|
||||
s.done = true
|
||||
s.resultMutex.Unlock()
|
||||
s.finishCh <- true
|
||||
} else {
|
||||
s.resultMutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for enough shares to aggregate
|
||||
select {
|
||||
case <-s.finishCh:
|
||||
s.logger.Info().Msg("BLS: Received finish - aggregating signatures")
|
||||
s.publishResult()
|
||||
return
|
||||
case err := <-s.errCh:
|
||||
s.logger.Error().Err(err).Msg("BLS: Received error during ProcessOutboundMessage")
|
||||
s.resultMutex.Lock()
|
||||
s.resultErr = err
|
||||
s.resultMutex.Unlock()
|
||||
s.publishResult()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (s *blsSigningSession) publishResult() {
|
||||
s.resultMutex.Lock()
|
||||
defer s.resultMutex.Unlock()
|
||||
|
||||
if s.resultErr != nil {
|
||||
failureEvent := event.CreateSignFailure(
|
||||
s.sessionID,
|
||||
s.walletID,
|
||||
map[string]any{
|
||||
"error": s.resultErr.Error(),
|
||||
"protocol": "BLS",
|
||||
},
|
||||
)
|
||||
evtData, _ := encoding.StructToJsonBytes(failureEvent)
|
||||
if err := s.resultQueue.Enqueue(fmt.Sprintf("%s.%s", event.SigningResultTopicBase, s.walletID), evtData, nil); err != nil {
|
||||
s.logger.Error().Err(err).Msg("BLS: failed to publish sign failure event")
|
||||
}
|
||||
s.externalFinishChan <- ""
|
||||
return
|
||||
}
|
||||
|
||||
// Aggregate collected shares
|
||||
s.sharesLock.Lock()
|
||||
shares := make([]*blsThreshold.SignatureShare, len(s.shares))
|
||||
copy(shares, s.shares)
|
||||
s.sharesLock.Unlock()
|
||||
|
||||
aggSig, err := blsThreshold.AggregateSignatures(shares, s.config.Threshold)
|
||||
if err != nil {
|
||||
s.logger.Error().Err(err).Msg("BLS: Failed to aggregate signatures")
|
||||
s.externalFinishChan <- ""
|
||||
return
|
||||
}
|
||||
|
||||
// Verify the aggregated signature
|
||||
if !s.config.VerifyAggregateSignature(s.messageHash, aggSig) {
|
||||
s.logger.Error().Msg("BLS: Aggregated signature verification failed")
|
||||
s.externalFinishChan <- ""
|
||||
return
|
||||
}
|
||||
|
||||
sigBytes := bls.SignatureToBytes(aggSig)
|
||||
|
||||
s.logger.Info().
|
||||
Str("sessionID", s.sessionID).
|
||||
Str("walletID", s.walletID).
|
||||
Int("sigLen", len(sigBytes)).
|
||||
Msg("BLS signing completed successfully")
|
||||
|
||||
// Create success event - use the Signature field (same as EdDSA pattern)
|
||||
successEvent := event.SigningResultEvent{
|
||||
ResultType: event.ResultTypeSuccess,
|
||||
WalletID: s.walletID,
|
||||
TxID: s.sessionID,
|
||||
Signature: sigBytes,
|
||||
}
|
||||
|
||||
evtData, _ := encoding.StructToJsonBytes(successEvent)
|
||||
if err := s.resultQueue.Enqueue(fmt.Sprintf("%s.%s", event.SigningResultTopicBase, s.walletID), evtData, nil); err != nil {
|
||||
s.logger.Error().Err(err).Msg("BLS: failed to publish sign success event")
|
||||
}
|
||||
|
||||
// Send to externalFinishChan so WaitForFinish() unblocks
|
||||
s.externalFinishChan <- hex.EncodeToString(sigBytes)
|
||||
}
|
||||
|
||||
func (s *blsSigningSession) Stop() {
|
||||
close(s.outCh)
|
||||
close(s.errCh)
|
||||
}
|
||||
|
||||
func (s *blsSigningSession) WaitForFinish() string {
|
||||
return <-s.externalFinishChan
|
||||
}
|
||||
@@ -6,5 +6,4 @@ const (
|
||||
KeyTypeSecp256k1 KeyType = "secp256k1"
|
||||
KeyTypeEd25519 KeyType = "ed25519"
|
||||
KeyTypeSR25519 KeyType = "sr25519"
|
||||
KeyTypeBLS KeyType = "bls"
|
||||
)
|
||||
|
||||
@@ -181,7 +181,7 @@ func (k *KMSEnabledKVStore) Delete(key string) error {
|
||||
|
||||
// isKeyShare checks if a key represents an MPC key share that requires KMS encryption.
|
||||
// Key shares are stored under org-scoped paths ("org:<orgID>:<walletID>") or with
|
||||
// known key-type prefixes (frost:, bls:, sr25519:, tfhe:). Metadata keys that do not
|
||||
// known key-type prefixes (frost:, sr25519:, tfhe:). Metadata keys that do not
|
||||
// match these patterns pass through to regular storage unencrypted.
|
||||
func isKeyShare(key string) bool {
|
||||
// Org-scoped keys are always key shares
|
||||
@@ -189,7 +189,7 @@ func isKeyShare(key string) bool {
|
||||
return true
|
||||
}
|
||||
// Known key-type prefixed shares
|
||||
for _, prefix := range []string{"frost:", "bls:", "sr25519:", "tfhe:"} {
|
||||
for _, prefix := range []string{"frost:", "sr25519:", "tfhe:"} {
|
||||
if strings.HasPrefix(key, prefix) {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -270,91 +270,6 @@ func (p *Node) CreateSR25519SignSession(
|
||||
return session, nil
|
||||
}
|
||||
|
||||
// CreateBLSKeyGenSession creates a BLS threshold keygen session
|
||||
func (p *Node) CreateBLSKeyGenSession(
|
||||
walletID string,
|
||||
threshold int,
|
||||
resultQueue messaging.MessageQueue,
|
||||
orgID string,
|
||||
) (BLSKeygenSession, error) {
|
||||
if !p.peerRegistry.ArePeersReady() {
|
||||
return nil, fmt.Errorf(
|
||||
"peers are not ready yet. ready: %d, expected: %d",
|
||||
p.peerRegistry.GetReadyPeersCount(),
|
||||
len(p.peerIDs)+1,
|
||||
)
|
||||
}
|
||||
|
||||
readyPeerIDs := p.peerRegistry.GetReadyPeersIncludeSelf()
|
||||
// BLS uses version 0 for raw party IDs without suffixes
|
||||
selfPartyID, allPartyIDs := p.generatePartyIDs(PurposeKeygen, readyPeerIDs, 0)
|
||||
|
||||
session := newBLSKeygenSession(
|
||||
walletID,
|
||||
p.pubSub,
|
||||
selfPartyID,
|
||||
allPartyIDs,
|
||||
threshold,
|
||||
p.kvstore,
|
||||
p.keyinfoStore,
|
||||
resultQueue,
|
||||
p.identityStore,
|
||||
orgID,
|
||||
)
|
||||
|
||||
// Note: Init() is called by the caller (keygen handler)
|
||||
return session, nil
|
||||
}
|
||||
|
||||
// CreateBLSSignSession creates a BLS threshold signing session
|
||||
func (p *Node) CreateBLSSignSession(
|
||||
sessionID string,
|
||||
walletID string,
|
||||
messageHash []byte,
|
||||
signerPeerIDs []string,
|
||||
resultQueue messaging.MessageQueue,
|
||||
orgID string,
|
||||
) (BLSSignSession, error) {
|
||||
// Check if we have enough signers
|
||||
keyInfo, err := p.keyinfoStore.Get(walletID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get key info: %w", err)
|
||||
}
|
||||
|
||||
if len(signerPeerIDs) < keyInfo.Threshold+1 {
|
||||
return nil, ErrNotEnoughParticipants
|
||||
}
|
||||
|
||||
// Check if this node is in the signer list
|
||||
if !contains(signerPeerIDs, p.nodeID) {
|
||||
return nil, ErrNotInParticipantList
|
||||
}
|
||||
|
||||
// Generate party IDs for signers
|
||||
// BLS uses version 0 for raw party IDs without suffixes
|
||||
selfPartyID, signerPartyIDs := p.generatePartyIDs(PurposeKeygen, signerPeerIDs, 0)
|
||||
|
||||
session, err := newBLSSigningSession(
|
||||
sessionID,
|
||||
walletID,
|
||||
messageHash,
|
||||
p.pubSub,
|
||||
selfPartyID,
|
||||
signerPartyIDs,
|
||||
p.kvstore,
|
||||
p.keyinfoStore,
|
||||
resultQueue,
|
||||
p.identityStore,
|
||||
orgID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Note: Init() is called by the caller (sign handler)
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (p *Node) CreateSignSession(
|
||||
sessionID string,
|
||||
walletID string,
|
||||
|
||||
@@ -100,7 +100,6 @@ const (
|
||||
SessionTypeECDSA SessionType = "ecdsa"
|
||||
SessionTypeEDDSA SessionType = "eddsa"
|
||||
SessionTypeSR25519 SessionType = "sr25519"
|
||||
SessionTypeBLS SessionType = "bls"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//
|
||||
// The orchestrator is the integration point for the
|
||||
// "MPC node signs only if it independently verifies the policy bundle"
|
||||
// control. Every signing entry point — CGGMP21, FROST, LSS, BLS,
|
||||
// control. Every signing entry point — CGGMP21, FROST, LSS,
|
||||
// SR25519 — must call SignGate.AuthorizeSign before its session emits
|
||||
// a share. Failure FAILS CLOSED: no fallback to "trust the coordinator".
|
||||
//
|
||||
|
||||
@@ -8,7 +8,6 @@ const (
|
||||
KeyTypeSecp256k1 KeyType = "secp256k1"
|
||||
KeyTypeEd25519 KeyType = "ed25519"
|
||||
KeyTypeSR25519 KeyType = "sr25519"
|
||||
KeyTypeBLS KeyType = "bls"
|
||||
)
|
||||
|
||||
// InitiatorMessage is anything that carries a payload to verify and its signature.
|
||||
|
||||
Reference in New Issue
Block a user