Skip to content

Commit d8c0fff

Browse files
committed
feat: optional multi-authorizer checks for key generation, signing, and resharing
2 parents 75062c0 + ab26645 commit d8c0fff

File tree

14 files changed

+654
-198
lines changed

14 files changed

+654
-198
lines changed

cmd/mpcium/main.go

Lines changed: 9 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,6 @@ func runNode(ctx context.Context, c *cli.Command) error {
129129
if err != nil {
130130
logger.Fatal("Failed to connect to NATS", err)
131131
}
132-
defer natsConn.Close()
133132

134133
pubsub := messaging.NewNATSPubSub(natsConn)
135134
keygenBroker, err := messaging.NewJetStreamBroker(ctx, natsConn, event.KeygenBrokerStream, []string{
@@ -162,7 +161,7 @@ func runNode(ctx context.Context, c *cli.Command) error {
162161
logger.Info("Node is running", "ID", nodeID, "name", nodeName)
163162

164163
peerNodeIDs := GetPeerIDs(peers)
165-
peerRegistry := mpc.NewRegistry(nodeID, peerNodeIDs, consulClient.KV(), directMessaging)
164+
peerRegistry := mpc.NewRegistry(nodeID, peerNodeIDs, consulClient.KV(), directMessaging, pubsub, identityStore)
166165

167166
mpcNode := mpc.NewNode(
168167
nodeID,
@@ -176,9 +175,6 @@ func runNode(ctx context.Context, c *cli.Command) error {
176175
)
177176
defer mpcNode.Close()
178177

179-
// ECDH session for DH key exchange
180-
ecdhSession := mpcNode.GetECDHSession()
181-
182178
eventConsumer := eventconsumer.NewEventConsumer(
183179
mpcNode,
184180
pubsub,
@@ -197,21 +193,16 @@ func runNode(ctx context.Context, c *cli.Command) error {
197193

198194
timeoutConsumer.Run()
199195
defer timeoutConsumer.Close()
200-
keygenConsumer := eventconsumer.NewKeygenConsumer(natsConn, keygenBroker, pubsub, peerRegistry)
201-
signingConsumer := eventconsumer.NewSigningConsumer(natsConn, signingBroker, pubsub, peerRegistry)
196+
keygenConsumer := eventconsumer.NewKeygenConsumer(natsConn, keygenBroker, pubsub, peerRegistry, genKeyResultQueue)
197+
signingConsumer := eventconsumer.NewSigningConsumer(natsConn, signingBroker, pubsub, peerRegistry, singingResultQueue)
202198

203199
// Make the node ready before starting the signing consumer
204200
if err := peerRegistry.Ready(); err != nil {
205201
logger.Error("Failed to mark peer registry as ready", err)
206202
}
207203
logger.Info("[READY] Node is ready", "nodeID", nodeID)
208204

209-
logger.Info("Waiting for ECDH key exchange to complete...", "nodeID", nodeID)
210-
if err := ecdhSession.WaitForExchangeComplete(); err != nil {
211-
logger.Fatal("ECDH exchange failed", err)
212-
}
213-
214-
logger.Info("ECDH key exchange completed successfully, starting consumers...", "nodeID", nodeID)
205+
logger.Info("Starting consumers", "nodeID", nodeID)
215206
appContext, cancel := context.WithCancel(context.Background())
216207
//Setup signal handling to cancel context on termination signals.
217208
go func() {
@@ -221,6 +212,11 @@ func runNode(ctx context.Context, c *cli.Command) error {
221212
logger.Warn("Shutdown signal received, canceling context...")
222213
cancel()
223214

215+
// Resign from peer registry first (before closing NATS)
216+
if err := peerRegistry.Resign(); err != nil {
217+
logger.Error("Failed to resign from peer registry", err)
218+
}
219+
224220
// Gracefully close consumers
225221
if err := keygenConsumer.Close(); err != nil {
226222
logger.Error("Failed to close keygen consumer", err)
@@ -229,10 +225,6 @@ func runNode(ctx context.Context, c *cli.Command) error {
229225
logger.Error("Failed to close signing consumer", err)
230226
}
231227

232-
if err := ecdhSession.Close(); err != nil {
233-
logger.Error("Failed to close ECDH session", err)
234-
}
235-
236228
err := natsConn.Drain()
237229
if err != nil {
238230
logger.Error("Failed to drain NATS connection", err)
@@ -264,21 +256,6 @@ func runNode(ctx context.Context, c *cli.Command) error {
264256
logger.Info("Signing consumer finished successfully")
265257
}()
266258

267-
go func() {
268-
for {
269-
select {
270-
case <-appContext.Done():
271-
return
272-
case err := <-ecdhSession.ErrChan():
273-
if err != nil {
274-
logger.Error("ECDH session error", err)
275-
errChan <- fmt.Errorf("ecdh session error: %w", err)
276-
return
277-
}
278-
}
279-
}
280-
}()
281-
282259
go func() {
283260
wg.Wait()
284261
logger.Info("All consumers have finished")

config.prod.yaml.template

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,19 @@ backup_dir: backups
1717
max_concurrent_keygen: 2
1818
max_concurrent_signing: 10
1919
session_warm_up_delay_ms: 100
20+
21+
# Authorization (optional)
22+
authorization:
23+
enabled: false
24+
default_threshold: 0
25+
operation_policies:
26+
keygen:
27+
required_authorizers: 0
28+
authorizer_ids: []
29+
signing:
30+
required_authorizers: 0
31+
authorizer_ids: []
32+
reshare:
33+
required_authorizers: 0
34+
authorizer_ids: []
35+
authorizers: {}

config.yaml.template

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,19 @@ backup_dir: backups
1414
max_concurrent_keygen: 2
1515
max_concurrent_signing: 10
1616
session_warm_up_delay_ms: 100
17+
18+
# Authorization (optional)
19+
authorization:
20+
enabled: false
21+
default_threshold: 0
22+
operation_policies:
23+
keygen:
24+
required_authorizers: 0
25+
authorizer_ids: []
26+
signing:
27+
required_authorizers: 0
28+
authorizer_ids: []
29+
reshare:
30+
required_authorizers: 0
31+
authorizer_ids: []
32+
authorizers: {}

pkg/event/types.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ const (
9191
// Context and cancellation errors
9292
ErrorCodeContextCancelled ErrorCode = "ERROR_CONTEXT_CANCELLED"
9393
ErrorCodeOperationAborted ErrorCode = "ERROR_OPERATION_ABORTED"
94+
ErrorCodeNotMajority ErrorCode = "ERROR_NOT_MAJORITY"
95+
ErrorCodeClusterNotReady ErrorCode = "ERROR_CLUSTER_NOT_READY"
9496
)
9597

9698
// GetErrorCodeFromError attempts to categorize a generic error into a specific error code

pkg/eventconsumer/event_consumer.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,13 @@ func (ec *eventConsumer) handleKeyGenEvent(natMsg *nats.Msg) {
166166
return
167167
}
168168

169+
// Optional multi-authorizer check for keygen
170+
if err := ec.identityStore.AuthorizeInitiatorMessage("keygen", &msg); err != nil {
171+
logger.Error("Authorization failed for keygen", err)
172+
ec.handleKeygenSessionError(msg.WalletID, err, "Authorization failed for keygen", natMsg)
173+
return
174+
}
175+
169176
walletID := msg.WalletID
170177
ecdsaSession, err := ec.node.CreateKeyGenSession(mpc.SessionTypeECDSA, walletID, ec.mpcThreshold, ec.genKeyResultQueue)
171178
if err != nil {
@@ -353,6 +360,12 @@ func (ec *eventConsumer) handleSigningEvent(natMsg *nats.Msg) {
353360
return
354361
}
355362

363+
// Optional multi-authorizer check for signing
364+
if err := ec.identityStore.AuthorizeInitiatorMessage("signing", &msg); err != nil {
365+
logger.Error("Authorization failed for signing", err)
366+
return
367+
}
368+
356369
logger.Info(
357370
"Received signing event",
358371
"waleltID",
@@ -589,6 +602,13 @@ func (ec *eventConsumer) consumeReshareEvent() error {
589602
return
590603
}
591604

605+
// Optional multi-authorizer check for reshare
606+
if err := ec.identityStore.AuthorizeInitiatorMessage("reshare", &msg); err != nil {
607+
logger.Error("Authorization failed for reshare", err)
608+
ec.handleReshareSessionError(msg.WalletID, msg.KeyType, msg.NewThreshold, err, "Authorization failed for reshare", natMsg)
609+
return
610+
}
611+
592612
walletID := msg.WalletID
593613
keyType := msg.KeyType
594614

pkg/eventconsumer/events.go

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,30 @@ type InitiatorMessage interface {
1717
Sig() []byte
1818
// InitiatorID returns the ID whose public key we have to look up.
1919
InitiatorID() string
20+
// AuthorizerSigs returns the optional list of authorizer signatures.
21+
AuthorizerSigs() []AuthorizerSignature
22+
}
23+
24+
// AuthorizerSignature represents an approval signature from an external authorizer.
25+
type AuthorizerSignature struct {
26+
AuthorizerID string `json:"authorizer_id"`
27+
Signature []byte `json:"signature"`
2028
}
2129

2230
type GenerateKeyMessage struct {
23-
WalletID string `json:"wallet_id"`
24-
Signature []byte `json:"signature"`
31+
WalletID string `json:"wallet_id"`
32+
Signature []byte `json:"signature"`
33+
AuthorizerSignatures []AuthorizerSignature `json:"authorizer_signatures,omitempty"`
2534
}
2635

2736
type SignTxMessage struct {
28-
KeyType KeyType `json:"key_type"`
29-
WalletID string `json:"wallet_id"`
30-
NetworkInternalCode string `json:"network_internal_code"`
31-
TxID string `json:"tx_id"`
32-
Tx []byte `json:"tx"`
33-
Signature []byte `json:"signature"`
37+
KeyType KeyType `json:"key_type"`
38+
WalletID string `json:"wallet_id"`
39+
NetworkInternalCode string `json:"network_internal_code"`
40+
TxID string `json:"tx_id"`
41+
Tx []byte `json:"tx"`
42+
Signature []byte `json:"signature"`
43+
AuthorizerSignatures []AuthorizerSignature `json:"authorizer_signatures,omitempty"`
3444
}
3545

3646
func (m *SignTxMessage) Raw() ([]byte, error) {
@@ -59,6 +69,10 @@ func (m *SignTxMessage) InitiatorID() string {
5969
return m.TxID
6070
}
6171

72+
func (m *SignTxMessage) AuthorizerSigs() []AuthorizerSignature {
73+
return m.AuthorizerSignatures
74+
}
75+
6276
func (m *GenerateKeyMessage) Raw() ([]byte, error) {
6377
return []byte(m.WalletID), nil
6478
}
@@ -70,3 +84,7 @@ func (m *GenerateKeyMessage) Sig() []byte {
7084
func (m *GenerateKeyMessage) InitiatorID() string {
7185
return m.WalletID
7286
}
87+
88+
func (m *GenerateKeyMessage) AuthorizerSigs() []AuthorizerSignature {
89+
return m.AuthorizerSignatures
90+
}

pkg/eventconsumer/keygen_consumer.go

Lines changed: 67 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@ package eventconsumer
22

33
import (
44
"context"
5+
"encoding/json"
6+
"errors"
57
"fmt"
68
"time"
79

810
"github.com/fystack/mpcium/pkg/event"
911
"github.com/fystack/mpcium/pkg/logger"
1012
"github.com/fystack/mpcium/pkg/messaging"
1113
"github.com/fystack/mpcium/pkg/mpc"
14+
"github.com/fystack/mpcium/pkg/types"
1215
"github.com/google/uuid"
1316
"github.com/nats-io/nats.go"
1417
"github.com/nats-io/nats.go/jetstream"
@@ -31,22 +34,30 @@ type KeygenConsumer interface {
3134

3235
// keygenConsumer implements KeygenConsumer.
3336
type keygenConsumer struct {
34-
natsConn *nats.Conn
35-
pubsub messaging.PubSub
36-
jsBroker messaging.MessageBroker
37-
peerRegistry mpc.PeerRegistry
37+
natsConn *nats.Conn
38+
pubsub messaging.PubSub
39+
jsBroker messaging.MessageBroker
40+
peerRegistry mpc.PeerRegistry
41+
keygenResultQueue messaging.MessageQueue
3842

3943
// jsSub holds the JetStream subscription, so it can be cleaned up during Close().
4044
jsSub messaging.MessageSubscription
4145
}
4246

4347
// NewKeygenConsumer returns a new instance of KeygenConsumer.
44-
func NewKeygenConsumer(natsConn *nats.Conn, jsBroker messaging.MessageBroker, pubsub messaging.PubSub, peerRegistry mpc.PeerRegistry) KeygenConsumer {
48+
func NewKeygenConsumer(
49+
natsConn *nats.Conn,
50+
jsBroker messaging.MessageBroker,
51+
pubsub messaging.PubSub,
52+
peerRegistry mpc.PeerRegistry,
53+
keygenResultQueue messaging.MessageQueue,
54+
) KeygenConsumer {
4555
return &keygenConsumer{
46-
natsConn: natsConn,
47-
pubsub: pubsub,
48-
jsBroker: jsBroker,
49-
peerRegistry: peerRegistry,
56+
natsConn: natsConn,
57+
pubsub: pubsub,
58+
jsBroker: jsBroker,
59+
peerRegistry: peerRegistry,
60+
keygenResultQueue: keygenResultQueue,
5061
}
5162
}
5263

@@ -60,6 +71,9 @@ func (sc *keygenConsumer) waitForAllPeersReadyToGenKey(ctx context.Context) erro
6071
for {
6172
select {
6273
case <-ctx.Done():
74+
if ctx.Err() == context.Canceled {
75+
return nil
76+
}
6377
return ctx.Err()
6478
case <-ticker.C:
6579
allPeersReady := sc.peerRegistry.ArePeersReady()
@@ -80,6 +94,9 @@ func (sc *keygenConsumer) waitForAllPeersReadyToGenKey(ctx context.Context) erro
8094
func (sc *keygenConsumer) Run(ctx context.Context) error {
8195
// Wait for sufficient peers before starting to consume messages
8296
if err := sc.waitForAllPeersReadyToGenKey(ctx); err != nil {
97+
if err == context.Canceled {
98+
return nil
99+
}
83100
return fmt.Errorf("failed to wait for sufficient peers: %w", err)
84101
}
85102

@@ -104,9 +121,22 @@ func (sc *keygenConsumer) Run(ctx context.Context) error {
104121
}
105122

106123
func (sc *keygenConsumer) handleKeygenEvent(msg jetstream.Msg) {
124+
raw := msg.Data()
125+
var keygenMsg types.GenerateKeyMessage
126+
sessionID := msg.Headers().Get("SessionID")
127+
128+
err := json.Unmarshal(raw, &keygenMsg)
129+
if err != nil {
130+
logger.Error("SigningConsumer: Failed to unmarshal keygen message", err)
131+
sc.handleKeygenError(keygenMsg, event.ErrorCodeUnmarshalFailure, err, sessionID)
132+
_ = msg.Ack()
133+
return
134+
}
107135

108136
if !sc.peerRegistry.ArePeersReady() {
109-
logger.Warn("KeygenConsumer: Not all peers are ready to sign, skipping message processing")
137+
logger.Warn("KeygenConsumer: Not all peers are ready to gen key, skipping message processing")
138+
sc.handleKeygenError(keygenMsg, event.ErrorCodeClusterNotReady, errors.New("not all peers are ready"), sessionID)
139+
_ = msg.Ack()
110140
return
111141
}
112142

@@ -161,6 +191,33 @@ func (sc *keygenConsumer) handleKeygenEvent(msg jetstream.Msg) {
161191
_ = msg.Nak()
162192
}
163193

194+
func (sc *keygenConsumer) handleKeygenError(keygenMsg types.GenerateKeyMessage, errorCode event.ErrorCode, err error, sessionID string) {
195+
keygenResult := event.KeygenResultEvent{
196+
ResultType: event.ResultTypeError,
197+
ErrorCode: string(errorCode),
198+
WalletID: keygenMsg.WalletID,
199+
ErrorReason: err.Error(),
200+
}
201+
202+
keygenResultBytes, err := json.Marshal(keygenResult)
203+
if err != nil {
204+
logger.Error("Failed to marshal keygen result event", err,
205+
"walletID", keygenResult.WalletID,
206+
)
207+
return
208+
}
209+
210+
topic := fmt.Sprintf(mpc.TypeGenerateWalletResultFmt, keygenResult.WalletID)
211+
err = sc.keygenResultQueue.Enqueue(topic, keygenResultBytes, &messaging.EnqueueOptions{
212+
IdempotententKey: buildIdempotentKey(keygenMsg.WalletID, sessionID, mpc.TypeGenerateWalletResultFmt),
213+
})
214+
if err != nil {
215+
logger.Error("Failed to enqueue keygen result event", err,
216+
"walletID", keygenMsg.WalletID,
217+
)
218+
}
219+
}
220+
164221
// Close unsubscribes from the JetStream subject and cleans up resources.
165222
func (sc *keygenConsumer) Close() error {
166223
if sc.jsSub != nil {

0 commit comments

Comments
 (0)