Skip to content

Commit bfd2ebf

Browse files
committed
feat: cggmp21 presign pool with auto cleanup
1 parent 01af87c commit bfd2ebf

6 files changed

Lines changed: 260 additions & 195 deletions

File tree

INSTALLATION.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,8 @@ Update `config.yaml`:
168168
event_initiator_pubkey: "09be5d070816aadaa1b6638cad33e819a8aed7101626f6bf1e0b427412c3408a"
169169
```
170170
171+
> 💡 **Note**: If you plan to use the presign pool worker (see [Presign Pool Worker](#presign-pool-worker) section), you'll need the `event_initiator.key` file (or `event_initiator.key.age` if encrypted) to be available. The private key file is generated alongside the identity file.
172+
171173
---
172174

173175
## Configure Node Identities
@@ -274,6 +276,49 @@ mpcium start -n node2
274276
275277
---
276278
279+
## Presign Pool Worker
280+
281+
The presign pool worker automatically maintains a pool of presignatures for hot wallets, ensuring they're ready for immediate use.
282+
283+
### Setup
284+
285+
To enable the presign pool worker on a node:
286+
287+
1. **Copy the event initiator private key** to the node directory:
288+
289+
If you generated the initiator with encryption:
290+
```bash
291+
# Decrypt the key first
292+
age --decrypt -o event_initiator.key event_initiator.key.age
293+
```
294+
295+
Then copy it to the node directory:
296+
```bash
297+
cp event_initiator.key node0/
298+
```
299+
300+
If you generated the initiator without encryption:
301+
```bash
302+
cp event_initiator.key node0/
303+
```
304+
305+
2. **Start the node with the `--presign-pool-worker` flag**:
306+
307+
```bash
308+
cd node0
309+
mpcium start -n node0 --presign-pool-worker
310+
```
311+
312+
> ⚠️ **Important**: Only one node in the cluster should run the presign pool worker. The node running this worker must have the `event_initiator.key` file in its working directory.
313+
314+
### How It Works
315+
316+
- The worker monitors hot wallet activity and automatically generates presignatures when needed
317+
- It maintains a pool of presignatures between `MinPoolSize` (default: 5) and `MaxPoolSize` (default: 20)
318+
- The worker subscribes to hot wallet events and proactively refills the presignature pool
319+
320+
---
321+
277322
## Production Deployment (High Security)
278323
279324
1. Use production-grade **NATS** and **Consul** clusters.

pkg/eventconsumer/event_consumer.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -663,7 +663,7 @@ func (ec *eventConsumer) consumePresignEvent() error {
663663
}
664664

665665
ctx := context.Background()
666-
success, err := session.Presign(ctx, msg.WalletID)
666+
success, err := session.Presign(ctx, msg.TxID)
667667
if err != nil {
668668
ec.handlePresignSessionError(msg.WalletID,
669669
err, "Presign operation failed",
@@ -708,7 +708,7 @@ func (ec *eventConsumer) handlePresignSessionSuccess(walletID string, txID strin
708708
}
709709

710710
err = ec.presignResultQueue.Enqueue(event.PresignResultTopic, presignResultBytes, &messaging.EnqueueOptions{
711-
IdempotententKey: composePresignIdempotentKey(walletID, natMsg),
711+
IdempotententKey: composePresignIdempotentKey(txID, natMsg),
712712
})
713713
if err != nil {
714714
logger.Error("Failed to enqueue presign result event", err,
@@ -717,7 +717,7 @@ func (ec *eventConsumer) handlePresignSessionSuccess(walletID string, txID strin
717717
)
718718
}
719719
// Presign events don't use reply inboxes, so no need to send reply
720-
logger.Info("[COMPLETED PRESIGN] Presign completed successfully", "walletID", walletID)
720+
logger.Info("[COMPLETED PRESIGN] Presign completed successfully", "walletID", walletID, "txID", txID)
721721
}
722722

723723
// handlePresignSessionError handles errors that occur during presign operations

pkg/mpc/node.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ func (p *Node) CreateTaurusSession(
159159
switch protocol {
160160
case types.ProtocolCGGMP21:
161161
tr := taurus.NewNATSTransport(walletID, selfPartyID, act, taurus.CGGMP21, p.pubSub, p.direct, p.identityStore)
162-
session = taurus.NewCGGMP21Session(walletID, selfPartyID, allPartyIDs, threshold, nil, tr, p.kvstore, p.keyinfoStore)
162+
session = taurus.NewCGGMP21Session(walletID, selfPartyID, allPartyIDs, threshold, p.presignInfoStore, tr, p.kvstore, p.keyinfoStore)
163163
case types.ProtocolTaproot:
164164
tr := taurus.NewNATSTransport(walletID, selfPartyID, act, taurus.FROSTTaproot, p.pubSub, p.direct, p.identityStore)
165165
session = taurus.NewTaprootSession(walletID, selfPartyID, allPartyIDs, threshold, tr, p.kvstore, p.keyinfoStore)

pkg/mpc/taurus/cggmp21.go

Lines changed: 78 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
cryptoEcdsa "crypto/ecdsa"
66
"crypto/sha256"
7+
"encoding/binary"
78
"errors"
89
"fmt"
910
"math/big"
@@ -145,40 +146,26 @@ func (p *CGGMP21Session) Sign(ctx context.Context, msg *big.Int) ([]byte, error)
145146
if p.savedData == nil {
146147
return nil, errors.New("no key loaded")
147148
}
149+
logger.Info("Starting CGGMP21 sign", "walletID", p.sessionID)
148150

149-
logger.Info("starting CGGMP21 sign", "walletID", p.sessionID)
150151
msgHash := msg.Bytes()
151-
152152
var (
153-
result any
154-
err error
153+
sigResult any
154+
err error
155155
)
156156

157-
// Try deterministic presign selection across nodes
157+
// Try presign path if store available
158158
if p.presignInfoStore != nil {
159-
presig, txID := p.selectAndLoadPresign(msgHash)
160-
if presig != nil && txID != "" {
161-
logger.Info("using presign for signing", "walletID", p.sessionID, "txID", txID)
162-
result, err = p.run(ctx, cmp.PresignOnline(p.savedData, presig, msgHash, p.workerPool))
163-
if err != nil {
164-
return nil, fmt.Errorf("presign online failed: %w", err)
165-
}
166-
// Mark used (best-effort)
167-
_ = p.markPresignUsed(txID)
168-
} else {
169-
result, err = p.run(ctx, cmp.Sign(p.savedData, p.peerIDs, msgHash, p.workerPool))
170-
if err != nil {
171-
return nil, fmt.Errorf("full sign failed: %w", err)
172-
}
173-
}
159+
sigResult, err = p.signWithPresign(ctx, msgHash)
174160
} else {
175-
result, err = p.run(ctx, cmp.Sign(p.savedData, p.peerIDs, msgHash, p.workerPool))
176-
if err != nil {
177-
return nil, fmt.Errorf("full sign failed: %w", err)
178-
}
161+
sigResult, err = p.signFull(ctx, msgHash)
162+
}
163+
if err != nil {
164+
return nil, err
179165
}
180166

181-
sig, ok := result.(*ecdsa.Signature)
167+
// Cast and verify
168+
sig, ok := sigResult.(*ecdsa.Signature)
182169
if !ok {
183170
return nil, errors.New("unexpected result type")
184171
}
@@ -274,46 +261,93 @@ func (p *CGGMP21Session) composePresignKey(sid, txID string) string {
274261
return fmt.Sprintf("cggmp21:%s:%s", sid, txID)
275262
}
276263

277-
// selectAndLoadPresign deterministically chooses a presign txID and loads its PreSignature from KV.
278-
// Selection: sort by CreatedAt asc, TxID asc; pick index = hash(msgHash) mod len(list).
279-
func (p *CGGMP21Session) selectAndLoadPresign(msgHash []byte) (*ecdsa.PreSignature, string) {
264+
// signWithPresign tries to select and use an existing presign for signing.
265+
// If no valid presign exists, falls back to full sign.
266+
func (p *CGGMP21Session) signWithPresign(ctx context.Context, msgHash []byte) (any, error) {
267+
presig, txID := p.selectAndLoadPresign()
268+
if presig == nil || txID == "" {
269+
logger.Debug("No presign found, fallback to full sign", "walletID", p.sessionID)
270+
return p.signFull(ctx, msgHash)
271+
}
272+
273+
logger.Info("Using presign for signing", "walletID", p.sessionID, "txID", txID)
274+
result, err := p.run(ctx, cmp.PresignOnline(p.savedData, presig, msgHash, p.workerPool))
275+
if err != nil {
276+
return nil, fmt.Errorf("presign online failed: %w", err)
277+
}
278+
279+
// Mark and cleanup in background (best effort)
280+
go func() {
281+
if err := p.markPresignUsed(txID); err != nil {
282+
logger.Warn("mark presign used failed", "walletID", p.sessionID, "txID", txID, "err", err)
283+
}
284+
if err := p.deletePresign(txID); err != nil {
285+
logger.Warn("delete presign failed", "walletID", p.sessionID, "txID", txID, "err", err)
286+
}
287+
}()
288+
289+
return result, nil
290+
}
291+
292+
// signFull executes a full CGGMP21 signing round.
293+
func (p *CGGMP21Session) signFull(ctx context.Context, msgHash []byte) (any, error) {
294+
logger.Info("Executing full CGGMP21 signing", "walletID", p.sessionID)
295+
result, err := p.run(ctx, cmp.Sign(p.savedData, p.peerIDs, msgHash, p.workerPool))
296+
if err != nil {
297+
return nil, fmt.Errorf("full sign failed: %w", err)
298+
}
299+
return result, nil
300+
}
301+
302+
func (p *CGGMP21Session) selectAndLoadPresign() (*ecdsa.PreSignature, string) {
280303
infos, err := p.presignInfoStore.ListPendingPresigns(p.sessionID)
281304
if err != nil || len(infos) == 0 {
282305
return nil, ""
283306
}
284-
// filter by active + protocol/keytype
285-
filtered := make([]*presigninfo.PresignInfo, 0, len(infos))
307+
308+
// Filter usable presigns
309+
var filtered []*presigninfo.PresignInfo
286310
for _, inf := range infos {
287-
if inf.Status == presigninfo.PresignStatusActive && inf.Protocol == types.ProtocolCGGMP21 && inf.KeyType == types.KeyTypeSecp256k1 {
311+
if inf.Status == presigninfo.PresignStatusActive &&
312+
inf.Protocol == types.ProtocolCGGMP21 &&
313+
inf.KeyType == types.KeyTypeSecp256k1 {
288314
filtered = append(filtered, inf)
289315
}
290316
}
291317
if len(filtered) == 0 {
292318
return nil, ""
293319
}
294-
// sort
320+
321+
// Sort + pick deterministically
295322
sort.Slice(filtered, func(i, j int) bool {
296323
if filtered[i].CreatedAt.Equal(filtered[j].CreatedAt) {
297324
return filtered[i].TxID < filtered[j].TxID
298325
}
299326
return filtered[i].CreatedAt.Before(filtered[j].CreatedAt)
300327
})
301-
// pick index via hash
302-
h := sha256.Sum256(msgHash)
303-
idx := int(h[0]) % len(filtered)
328+
h := sha256.Sum256([]byte(p.sessionID))
329+
idx := int(binary.BigEndian.Uint64(h[:8]) % uint64(len(filtered)))
304330
chosen := filtered[idx]
305-
// load material
306-
bytes, err := p.kvstore.Get(p.composePresignKey(p.sessionID, chosen.TxID))
307-
if err != nil || len(bytes) == 0 {
331+
332+
// Load presign from KV
333+
key := p.composePresignKey(p.sessionID, chosen.TxID)
334+
data, err := p.kvstore.Get(key)
335+
if err != nil || len(data) == 0 {
336+
logger.Warn("presign missing", "walletID", p.sessionID, "txID", chosen.TxID, "err", err)
308337
return nil, ""
309338
}
310-
presig := new(ecdsa.PreSignature)
311-
if err := cbor.Unmarshal(bytes, presig); err != nil {
339+
340+
presig := ecdsa.EmptyPreSignature(curve.Secp256k1{})
341+
if err := cbor.Unmarshal(data, presig); err != nil {
342+
logger.Warn("unmarshal presign failed", "walletID", p.sessionID, "txID", chosen.TxID, "err", err)
312343
return nil, ""
313344
}
314345
if err := presig.Validate(); err != nil {
346+
logger.Warn("presign invalid", "walletID", p.sessionID, "txID", chosen.TxID, "err", err)
315347
return nil, ""
316348
}
349+
350+
logger.Debug("Presign chosen", "walletID", p.sessionID, "txID", chosen.TxID, "idx", idx)
317351
return presig, chosen.TxID
318352
}
319353

@@ -327,3 +361,7 @@ func (p *CGGMP21Session) markPresignUsed(txID string) error {
327361
info.UsedAt = &now
328362
return p.presignInfoStore.Save(p.sessionID, info)
329363
}
364+
365+
func (p *CGGMP21Session) deletePresign(txID string) error {
366+
return p.kvstore.Delete(p.composePresignKey(p.sessionID, txID))
367+
}

0 commit comments

Comments
 (0)