Skip to content

Keystore

The Keystore interface and MemKeystore implementation provide flexible key management for Starknet accounts.

Keystore Interface

The Keystore interface defines the contract for key storage and signing implementations.

Interface Definition

type Keystore interface {
	Sign(ctx context.Context, id string, msgHash *big.Int) (x, y *big.Int, err error)
}

Method

Sign

Signs a message hash using a stored private key.

Signature:
Sign(ctx context.Context, id string, msgHash *big.Int) (x, y *big.Int, err error)
Parameters:
  • ctx - Context for cancellation and timeout
  • id - Identifier for the key (typically the public key)
  • msgHash - Message hash to sign
Returns:
  • x - X coordinate (r) of the signature
  • y - Y coordinate (s) of the signature
  • err - Error if signing fails

Implementing Custom Keystores

You can implement the Keystore interface to create custom key storage solutions:

package main
 
import (
	"context"
	"math/big"
 
	"github.com/NethermindEth/starknet.go/curve"
)
 
// SecureKeystore implements the Keystore interface with encryption
type SecureKeystore struct {
	encryptedKeys map[string][]byte
	// Add your encryption/decryption logic
}
 
func (sk *SecureKeystore) Sign(
	ctx context.Context,
	id string,
	msgHash *big.Int,
) (x, y *big.Int, err error) {
	// 1. Decrypt the private key
	encryptedKey, exists := sk.encryptedKeys[id]
	if !exists {
		return nil, nil, fmt.Errorf("key not found: %s", id)
	}
 
	privateKey := sk.decrypt(encryptedKey) // Your decryption logic
 
	// 2. Check context cancellation
	select {
	case <-ctx.Done():
		return nil, nil, ctx.Err()
	default:
	}
 
	// 3. Sign the message
	x, y, err = curve.Sign(msgHash, privateKey)
	return x, y, err
}

Use Cases

Custom Keystore implementations can:

  • Encrypt keys at rest
  • Store keys in hardware security modules (HSM)
  • Use cloud key management services (AWS KMS, Google Cloud KMS)
  • Integrate with existing key management infrastructure
  • Implement multi-signature schemes

MemKeystore

In-memory keystore implementation for development and testing.

Type Definition

type MemKeystore struct {
	mu   sync.Mutex
	keys map[string]*big.Int
}

Description

MemKeystore is a thread-safe, in-memory implementation of the Keystore interface. It stores private keys in a map without encryption or persistence.

Features:
  • Thread-safe with mutex protection
  • Simple key-value storage
  • No persistence (keys lost on exit)
  • No encryption
Intended for:
  • Testing and development
  • Examples and tutorials
  • Temporary accounts
Not suitable for:
  • Production use
  • Long-lived accounts
  • Accounts with real value

Methods

Put

Stores a private key in the keystore.

Signature:
func (ks *MemKeystore) Put(senderAddress string, k *big.Int)
Parameters:
  • senderAddress - Identifier for the key (typically public key)
  • k - Private key to store
Usage Example:
package main
 
import (
	"fmt"
	"math/big"
 
	"github.com/NethermindEth/starknet.go/account"
)
 
func main() {
	ks := account.NewMemKeystore()
 
	// Store a key
	publicKey := "0x1234..."
	privateKey := new(big.Int).SetUint64(12345)
	ks.Put(publicKey, privateKey)
 
	fmt.Println("Key stored successfully")
}

Get

Retrieves a private key from the keystore.

Signature:
func (ks *MemKeystore) Get(senderAddress string) (*big.Int, error)
Parameters:
  • senderAddress - Identifier for the key
Returns:
  • *big.Int - Private key
  • error - Error if key doesn't exist
Usage Example:
package main
 
import (
	"fmt"
	"log"
	"math/big"
 
	"github.com/NethermindEth/starknet.go/account"
)
 
func main() {
	ks := account.NewMemKeystore()
 
	// Store a key
	publicKey := "0x1234..."
	privateKey := new(big.Int).SetUint64(12345)
	ks.Put(publicKey, privateKey)
 
	// Retrieve the key
	retrievedKey, err := ks.Get(publicKey)
	if err != nil {
		log.Fatal(err)
	}
 
	fmt.Printf("Retrieved key: %s\n", retrievedKey.String())
}
Error Handling:
key, err := ks.Get(publicKey)
if err != nil {
	if errors.Is(err, account.ErrSenderNoExist) {
		// Handle missing key
		fmt.Println("Key not found")
	}
	return err
}

Sign

Signs a message hash using a stored private key.

Signature:
func (ks *MemKeystore) Sign(
	ctx context.Context,
	id string,
	msgHash *big.Int,
) (r, s *big.Int, err error)
Parameters:
  • ctx - Context for cancellation and timeout
  • id - Identifier for the key (typically public key)
  • msgHash - Message hash to sign
Returns:
  • r - R component of the signature
  • s - S component of the signature
  • err - Error if signing fails
Usage Example:
package main
 
import (
	"context"
	"fmt"
	"log"
	"math/big"
 
	"github.com/NethermindEth/starknet.go/account"
)
 
func main() {
	// Create keystore with keys
	ks, pubKey, _ := account.GetRandomKeys()
 
	// Message to sign
	msgHash := new(big.Int).SetUint64(42)
 
	// Sign the message
	r, s, err := ks.Sign(context.Background(), pubKey.String(), msgHash)
	if err != nil {
		log.Fatal(err)
	}
 
	fmt.Printf("Signature r: %s\n", r.String())
	fmt.Printf("Signature s: %s\n", s.String())
}
With Context Cancellation:
// Create context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
 
// Sign with timeout
r, s, err := ks.Sign(ctx, publicKey, msgHash)
if err != nil {
	if err == context.DeadlineExceeded {
		fmt.Println("Signing timed out")
	}
	return err
}

Get

Retrieves a private key from the keystore by sender address.

Method Signature:
func (ks *MemKeystore) Get(senderAddress string) (*big.Int, error)
Parameters:
  • senderAddress - Address of the sender as string
Returns:
  • *big.Int - Private key associated with the address
  • error - ErrSenderNoExist if address not found
Usage Example:
package main
 
import (
	"fmt"
	"log"
	"math/big"
 
	"github.com/NethermindEth/starknet.go/account"
)
 
func main() {
	// Create keystore and add a key
	ks := account.NewMemKeystore()
	address := "0x1234567890abcdef"
	privateKey := new(big.Int).SetUint64(42)
 
	// Store the key
	ks.Put(address, privateKey)
 
	// Retrieve the key
	retrievedKey, err := ks.Get(address)
	if err != nil {
		log.Fatal("Key not found:", err)
	}
 
	fmt.Printf("Retrieved private key: %s\n", retrievedKey.String())
 
	// Try to get non-existent key
	_, err = ks.Get("0xnonexistent")
	if err != nil {
		fmt.Println("Expected error:", err) // Will print: sender does not exist
	}
}

Put

Stores a private key in the keystore for a given sender address.

Method Signature:
func (ks *MemKeystore) Put(senderAddress string, k *big.Int)
Parameters:
  • senderAddress - Address of the sender as string
  • k - Private key to store as *big.Int
Usage Example:
package main
 
import (
	"fmt"
	"math/big"
 
	"github.com/NethermindEth/starknet.go/account"
)
 
func main() {
	// Create a new keystore
	ks := account.NewMemKeystore()
 
	// Multiple addresses and keys
	addresses := []string{
		"0x1234567890abcdef",
		"0xfedcba0987654321",
		"0xaabbccddeeff0011",
	}
 
	// Store multiple keys
	for i, addr := range addresses {
		privateKey := new(big.Int).SetUint64(uint64(i + 1) * 100)
		ks.Put(addr, privateKey)
		fmt.Printf("Stored key for %s\n", addr)
	}
 
	// Verify all keys are stored
	for _, addr := range addresses {
		key, err := ks.Get(addr)
		if err != nil {
			fmt.Printf("Error getting key for %s: %v\n", addr, err)
		} else {
			fmt.Printf("Key for %s: %s\n", addr, key.String())
		}
	}
}

Complete Usage Example

This example demonstrates the full lifecycle of using MemKeystore with an Account.

package main
 
import (
	"context"
	"fmt"
	"log"
	"math/big"
 
	"github.com/NethermindEth/juno/core/felt"
	"github.com/NethermindEth/starknet.go/account"
	"github.com/NethermindEth/starknet.go/rpc"
	"github.com/NethermindEth/starknet.go/utils"
)
 
func main() {
	// 1. Create keystore and generate keys
	ks, pubKey, privKey := account.GetRandomKeys()
	fmt.Printf("Generated keys:\n")
	fmt.Printf("  Public: %s\n", pubKey.String())
	fmt.Printf("  Private: %s\n", privKey.String())
 
	// 2. Or use existing keys
	existingPubKey := "0x1234..."
	existingPrivKey, _ := new(big.Int).SetString("0xabcd...", 16)
	ks2 := account.SetNewMemKeystore(existingPubKey, existingPrivKey)
 
	// 3. Create RPC provider
	provider, err := rpc.NewProvider("https://starknet-sepolia.public.blastapi.io/rpc/v0_8")
	if err != nil {
		log.Fatal(err)
	}
 
	// 4. Create account with keystore
	accountAddress, _ := utils.HexToFelt("0x1234...")
	acc, err := account.NewAccount(
		provider,
		accountAddress,
		pubKey.String(),
		ks,
		account.CairoV2,
	)
	if err != nil {
		log.Fatal(err)
	}
 
	// 5. Use account to sign messages
	message := new(felt.Felt).SetUint64(42)
	signature, err := acc.Sign(context.Background(), message)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("Signature: [%s, %s]\n", signature[0].String(), signature[1].String())
 
	// 6. Verify signature
	valid, err := acc.Verify(message, signature)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("Signature valid: %t\n", valid)
 
	// 7. Sign and send transactions
	functionCalls := []rpc.InvokeFunctionCall{
		{
			ContractAddress:    contractAddress,
			EntryPointSelector: entryPointSelector,
			Calldata:           calldata,
		},
	}
 
	// The keystore is automatically used by BuildAndSendInvokeTxn
	response, err := acc.BuildAndSendInvokeTxn(
		context.Background(),
		functionCalls,
		nil, // Use default options
	)
	if err != nil {
		log.Fatal(err)
	}
 
	fmt.Printf("Transaction hash: %s\n", response.TransactionHash)
}

Security Considerations

MemKeystore Security

MemKeystore has several limitations:

  1. No Encryption: Keys stored in plain text in memory
  2. No Persistence: Keys lost when program exits
  3. Memory Exposure: Keys may be visible in memory dumps
  4. No Access Control: Any code with keystore reference can access keys

Production Recommendations

For production use, implement a secure Keystore:

// Example: Encrypted file-based keystore
type FileKeystore struct {
	filepath   string
	passphrase string
}
 
func (fk *FileKeystore) Sign(
	ctx context.Context,
	id string,
	msgHash *big.Int,
) (x, y *big.Int, err error) {
	// 1. Load encrypted key file
	// 2. Decrypt using passphrase
	// 3. Sign message
	// 4. Clear decrypted key from memory
	// 5. Return signature
}

Best Practices

  1. Never hardcode private keys in source code
  2. Use environment variables or secure configuration for keys
  3. Implement key rotation for long-lived accounts
  4. Use hardware security modules for high-value accounts
  5. Encrypt keys at rest in any persistent storage
  6. Limit key access to minimum necessary code
  7. Log security events (key access, failed signatures)
  8. Implement rate limiting to prevent brute force attacks

Related Resources