Skip to content

Commit 6c2dec5

Browse files
committed
feat(sdk-go): add parallel workers support for Celestia keyring
- Implement NewMnemonic() to create worker keys remotely in POPSigner - Add thread-safe keys map to track master + dynamically created workers - Store namespace_id from master key for creating workers in same namespace - Update List(), Key(), KeyByAddress(), Sign() to work with multiple keys - Add metadata to worker keys: created_by, worker_type, master_key This enables Celestia clientTX parallel workers (--tx.worker.accounts=N) to automatically create and use worker keys via POPSigner without any changes to celestia-node. Workers are named 'parallel-worker-1', etc. by clientTX, and feegrants are handled automatically on-chain. BREAKING: CelestiaKeyring struct has new fields (namespaceID, keys map)
1 parent 21f5a08 commit 6c2dec5

File tree

1 file changed

+186
-35
lines changed

1 file changed

+186
-35
lines changed

sdk-go/celestia.go

Lines changed: 186 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"encoding/hex"
3232
"errors"
3333
"fmt"
34+
"sync"
3435

3536
"github.com/cosmos/cosmos-sdk/crypto/hd"
3637
"github.com/cosmos/cosmos-sdk/crypto/keyring"
@@ -48,13 +49,29 @@ var _ keyring.Keyring = (*CelestiaKeyring)(nil)
4849
// It uses POPSigner as the backend for secure remote signing.
4950
//
5051
// This keyring can be passed directly to the Celestia client.New() function.
52+
// It supports parallel workers by implementing NewMnemonic() to dynamically create
53+
// worker keys when requested by clientTX.
5154
type CelestiaKeyring struct {
52-
client *Client
53-
keyID uuid.UUID
54-
keyName string
55+
client *Client
56+
keyID uuid.UUID
57+
keyName string
58+
pubKey *secp256k1.PubKey
59+
address sdk.AccAddress
60+
celestia string // bech32 celestia1... address
61+
namespaceID uuid.UUID // namespace for creating new keys
62+
63+
// mu protects the keys map for concurrent access
64+
mu sync.RWMutex
65+
keys map[string]*celestiaKeyRecord // name -> key record (for dynamically created keys)
66+
}
67+
68+
// celestiaKeyRecord stores info about a key in the keyring.
69+
type celestiaKeyRecord struct {
70+
id uuid.UUID
71+
name string
5572
pubKey *secp256k1.PubKey
5673
address sdk.AccAddress
57-
celestia string // bech32 celestia1... address
74+
celestia string
5875
}
5976

6077
// CelestiaKeyringOption configures the CelestiaKeyring.
@@ -173,13 +190,28 @@ func NewCelestiaKeyring(apiKey, keyNameOrID string, opts ...CelestiaKeyringOptio
173190
// Derive Celestia bech32 address from the same address bytes
174191
celestiaAddr := deriveCelestiaAddressFromBytes(address)
175192

176-
return &CelestiaKeyring{
177-
client: client,
178-
keyID: keyUUID,
179-
keyName: key.Name,
193+
// Create the master key record
194+
masterRecord := &celestiaKeyRecord{
195+
id: keyUUID,
196+
name: key.Name,
180197
pubKey: pubKey,
181198
address: address,
182199
celestia: celestiaAddr,
200+
}
201+
202+
// Initialize keys map with the master key
203+
keys := make(map[string]*celestiaKeyRecord)
204+
keys[key.Name] = masterRecord
205+
206+
return &CelestiaKeyring{
207+
client: client,
208+
keyID: keyUUID,
209+
keyName: key.Name,
210+
pubKey: pubKey,
211+
address: address,
212+
celestia: celestiaAddr,
213+
namespaceID: key.NamespaceID, // Store namespace for creating workers
214+
keys: keys,
183215
}, nil
184216
}
185217

@@ -218,13 +250,20 @@ func (k *CelestiaKeyring) Backend() string {
218250
}
219251

220252
// List returns all keys in the keyring.
221-
// For POPSigner, this returns only the configured key.
253+
// This includes the master key and any dynamically created worker keys.
222254
func (k *CelestiaKeyring) List() ([]*keyring.Record, error) {
223-
record, err := k.Key(k.keyName)
224-
if err != nil {
225-
return nil, err
255+
k.mu.RLock()
256+
defer k.mu.RUnlock()
257+
258+
records := make([]*keyring.Record, 0, len(k.keys))
259+
for _, keyRec := range k.keys {
260+
record, err := keyring.NewOfflineRecord(keyRec.name, keyRec.pubKey)
261+
if err != nil {
262+
return nil, err
263+
}
264+
records = append(records, record)
226265
}
227-
return []*keyring.Record{record}, nil
266+
return records, nil
228267
}
229268

230269
// SupportedAlgorithms returns the supported signing algorithms.
@@ -235,19 +274,40 @@ func (k *CelestiaKeyring) SupportedAlgorithms() (keyring.SigningAlgoList, keyrin
235274
// Key returns the key by name or ID.
236275
// Accepts either the key name (e.g., "blobcell-example") or the KEY_ID (UUID).
237276
func (k *CelestiaKeyring) Key(uid string) (*keyring.Record, error) {
238-
// Accept either key name or key ID (UUID)
239-
if uid != k.keyName && uid != k.keyID.String() {
240-
return nil, fmt.Errorf("key %s not found (available: name=%q, id=%q)", uid, k.keyName, k.keyID.String())
277+
k.mu.RLock()
278+
defer k.mu.RUnlock()
279+
280+
// First check by name
281+
if keyRec, ok := k.keys[uid]; ok {
282+
return keyring.NewOfflineRecord(keyRec.name, keyRec.pubKey)
283+
}
284+
285+
// Then check by ID (UUID)
286+
for _, keyRec := range k.keys {
287+
if keyRec.id.String() == uid {
288+
return keyring.NewOfflineRecord(keyRec.name, keyRec.pubKey)
289+
}
241290
}
242-
return keyring.NewOfflineRecord(k.keyName, k.pubKey)
291+
292+
// Build list of available keys for helpful error message
293+
available := make([]string, 0, len(k.keys))
294+
for name := range k.keys {
295+
available = append(available, name)
296+
}
297+
return nil, fmt.Errorf("key %q not found (available: %v)", uid, available)
243298
}
244299

245300
// KeyByAddress returns a key by its address.
246301
func (k *CelestiaKeyring) KeyByAddress(address sdk.Address) (*keyring.Record, error) {
247-
if !address.Equals(k.address) {
248-
return nil, fmt.Errorf("key with address %s not found", address.String())
302+
k.mu.RLock()
303+
defer k.mu.RUnlock()
304+
305+
for _, keyRec := range k.keys {
306+
if address.Equals(keyRec.address) {
307+
return keyring.NewOfflineRecord(keyRec.name, keyRec.pubKey)
308+
}
249309
}
250-
return keyring.NewOfflineRecord(k.keyName, k.pubKey)
310+
return nil, fmt.Errorf("key with address %s not found", address.String())
251311
}
252312

253313
// Delete removes a key from the keyring.
@@ -268,10 +328,73 @@ func (k *CelestiaKeyring) Rename(from, to string) error {
268328
return errors.New("popsigner keyring is read-only: use POPSigner API to rename keys")
269329
}
270330

271-
// NewMnemonic generates a new mnemonic and key.
272-
// Not supported by POPSigner keyring.
331+
// NewMnemonic creates a new key in POPSigner when called by clientTX for parallel workers.
332+
// The mnemonic is NOT returned as the key is stored securely in POPSigner's backend.
333+
//
334+
// This method is called by Celestia's clientTX when creating parallel worker accounts
335+
// (e.g., "parallel-worker-1", "parallel-worker-2", etc.). POPSigner creates the key
336+
// remotely and returns the public key info.
273337
func (k *CelestiaKeyring) NewMnemonic(uid string, language keyring.Language, hdPath, bip39Passphrase string, algo keyring.SignatureAlgo) (*keyring.Record, string, error) {
274-
return nil, "", errors.New("popsigner keyring does not support mnemonic generation: use POPSigner API to create keys")
338+
// Check if key already exists
339+
k.mu.RLock()
340+
if existing, ok := k.keys[uid]; ok {
341+
k.mu.RUnlock()
342+
record, err := keyring.NewOfflineRecord(uid, existing.pubKey)
343+
if err != nil {
344+
return nil, "", err
345+
}
346+
return record, "", nil // Already exists
347+
}
348+
k.mu.RUnlock()
349+
350+
// Create new key in POPSigner
351+
newKey, err := k.client.Keys.Create(context.Background(), CreateKeyRequest{
352+
Name: uid,
353+
NamespaceID: k.namespaceID,
354+
Algorithm: "secp256k1",
355+
Exportable: false, // Workers should not be exportable
356+
Metadata: map[string]string{
357+
"created_by": "celestia_keyring",
358+
"worker_type": "parallel",
359+
"master_key": k.keyName,
360+
},
361+
})
362+
if err != nil {
363+
return nil, "", fmt.Errorf("failed to create key %q in POPSigner: %w", uid, err)
364+
}
365+
366+
// Decode public key
367+
pubKeyBytes, err := hex.DecodeString(newKey.PublicKey)
368+
if err != nil {
369+
return nil, "", fmt.Errorf("failed to decode public key for new key %q: %w", uid, err)
370+
}
371+
372+
if len(pubKeyBytes) != 33 {
373+
return nil, "", fmt.Errorf("invalid public key length for new key %q: expected 33, got %d", uid, len(pubKeyBytes))
374+
}
375+
376+
pubKey := &secp256k1.PubKey{Key: pubKeyBytes}
377+
address := sdk.AccAddress(pubKey.Address())
378+
celestiaAddr := deriveCelestiaAddressFromBytes(address)
379+
380+
// Store the new key
381+
k.mu.Lock()
382+
k.keys[uid] = &celestiaKeyRecord{
383+
id: newKey.ID,
384+
name: uid,
385+
pubKey: pubKey,
386+
address: address,
387+
celestia: celestiaAddr,
388+
}
389+
k.mu.Unlock()
390+
391+
// Return the record (mnemonic is empty - key is stored remotely in POPSigner)
392+
record, err := keyring.NewOfflineRecord(uid, pubKey)
393+
if err != nil {
394+
return nil, "", err
395+
}
396+
397+
return record, "", nil
275398
}
276399

277400
// NewAccount creates a new account from a mnemonic.
@@ -300,24 +423,38 @@ func (k *CelestiaKeyring) SaveMultisig(uid string, pubkey cryptotypes.PubKey) (*
300423

301424
// Sign signs a message using POPSigner.
302425
func (k *CelestiaKeyring) Sign(uid string, msg []byte, signMode signing.SignMode) ([]byte, cryptotypes.PubKey, error) {
303-
if uid != k.keyName {
304-
return nil, nil, fmt.Errorf("key %s not found", uid)
426+
k.mu.RLock()
427+
keyRec, ok := k.keys[uid]
428+
k.mu.RUnlock()
429+
430+
if !ok {
431+
return nil, nil, fmt.Errorf("key %q not found", uid)
305432
}
306433

307-
resp, err := k.client.Sign.Sign(context.Background(), k.keyID, msg, false)
434+
resp, err := k.client.Sign.Sign(context.Background(), keyRec.id, msg, false)
308435
if err != nil {
309-
return nil, nil, fmt.Errorf("signing failed: %w", err)
436+
return nil, nil, fmt.Errorf("signing failed for key %q: %w", uid, err)
310437
}
311438

312-
return resp.Signature, k.pubKey, nil
439+
return resp.Signature, keyRec.pubKey, nil
313440
}
314441

315442
// SignByAddress signs a message using the key associated with the given address.
316443
func (k *CelestiaKeyring) SignByAddress(address sdk.Address, msg []byte, signMode signing.SignMode) ([]byte, cryptotypes.PubKey, error) {
317-
if !address.Equals(k.address) {
444+
k.mu.RLock()
445+
var keyRec *celestiaKeyRecord
446+
for _, rec := range k.keys {
447+
if address.Equals(rec.address) {
448+
keyRec = rec
449+
break
450+
}
451+
}
452+
k.mu.RUnlock()
453+
454+
if keyRec == nil {
318455
return nil, nil, fmt.Errorf("key with address %s not found", address.String())
319456
}
320-
return k.Sign(k.keyName, msg, signMode)
457+
return k.Sign(keyRec.name, msg, signMode)
321458
}
322459

323460
// ImportPrivKey imports an ASCII armored private key.
@@ -340,19 +477,33 @@ func (k *CelestiaKeyring) ImportPubKey(uid, armor string) error {
340477

341478
// ExportPubKeyArmor exports the public key as ASCII armor.
342479
func (k *CelestiaKeyring) ExportPubKeyArmor(uid string) (string, error) {
343-
if uid != k.keyName {
344-
return "", fmt.Errorf("key %s not found", uid)
480+
k.mu.RLock()
481+
keyRec, ok := k.keys[uid]
482+
k.mu.RUnlock()
483+
484+
if !ok {
485+
return "", fmt.Errorf("key %q not found", uid)
345486
}
346487
// Return hex-encoded public key for simplicity
347-
return hex.EncodeToString(k.pubKey.Bytes()), nil
488+
return hex.EncodeToString(keyRec.pubKey.Bytes()), nil
348489
}
349490

350491
// ExportPubKeyArmorByAddress exports the public key by address.
351492
func (k *CelestiaKeyring) ExportPubKeyArmorByAddress(address sdk.Address) (string, error) {
352-
if !address.Equals(k.address) {
493+
k.mu.RLock()
494+
var keyRec *celestiaKeyRecord
495+
for _, rec := range k.keys {
496+
if address.Equals(rec.address) {
497+
keyRec = rec
498+
break
499+
}
500+
}
501+
k.mu.RUnlock()
502+
503+
if keyRec == nil {
353504
return "", fmt.Errorf("key with address %s not found", address.String())
354505
}
355-
return k.ExportPubKeyArmor(k.keyName)
506+
return k.ExportPubKeyArmor(keyRec.name)
356507
}
357508

358509
// ExportPrivKeyArmor exports the private key.

0 commit comments

Comments
 (0)