@@ -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.
5154type 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 .
222254func (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).
237276func (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.
246301func (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.
273337func (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.
302425func (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.
316443func (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.
342479func (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.
351492func (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