Skip to content

Key Operations

The curve package provides functions for generating and managing cryptographic keys on the StarkCurve. These operations are fundamental for creating accounts and managing identities on Starknet.

Overview

Starknet uses elliptic curve cryptography on the StarkCurve (a variant of secp256k1). Key operations include:

  • Random key generation - Creating new key pairs securely
  • Point derivation - Computing public keys from private keys
  • Coordinate calculation - Computing y-coordinates from x-coordinates

Key Structure

A Starknet key pair consists of:

  • Private Key: A secret number used for signing
  • Public Key X: The x-coordinate of the public key point (used as the account identifier)
  • Public Key Y: The y-coordinate of the public key point

Key Generation

GetRandomKeys

Generates a cryptographically secure random private key and its corresponding public key.

Signature:
func GetRandomKeys() (privKey, x, y *big.Int, err error)

Parameters: None

Returns:
  • privKey - The generated private key
  • x - The x-coordinate of the public key (the Starknet public key/account identifier)
  • y - The y-coordinate of the public key
  • err - Error if key generation fails
When to Use:
  • Creating new Starknet accounts
  • Generating test keys for development
  • Initializing wallets or key management systems

Security: This function uses crypto/rand.Reader for cryptographically secure random number generation. The generated keys are suitable for production use.

Usage Example:
package main
 
import (
	"fmt"
	"log"
 
	"github.com/NethermindEth/starknet.go/curve"
)
 
func main() {
	// Generate a new key pair
	privKey, pubKeyX, pubKeyY, err := curve.GetRandomKeys()
	if err != nil {
		log.Fatal(err)
	}
 
	fmt.Println("=== New Starknet Account Keys ===")
	fmt.Printf("Private Key: %s\n", privKey.String())
	fmt.Printf("Public Key X: %s\n", pubKeyX.String())
	fmt.Printf("Public Key Y: %s\n", pubKeyY.String())
 
	// The pubKeyX is typically used as the Starknet account public key
	fmt.Printf("\nAccount Public Key (use this): %s\n", pubKeyX.String())
 
	// Format for display (hex with 0x prefix)
	fmt.Printf("Public Key (hex): 0x%x\n", pubKeyX)
}
Creating Multiple Accounts:
package main
 
import (
	"fmt"
	"log"
 
	"github.com/NethermindEth/starknet.go/curve"
)
 
func main() {
	numAccounts := 5
 
	fmt.Printf("Generating %d Starknet accounts...\n\n", numAccounts)
 
	for i := 0; i < numAccounts; i++ {
		privKey, pubKeyX, _, err := curve.GetRandomKeys()
		if err != nil {
			log.Fatal(err)
		}
 
		fmt.Printf("Account %d:\n", i+1)
		fmt.Printf("  Private Key: %s\n", privKey.String())
		fmt.Printf("  Public Key:  0x%x\n\n", pubKeyX)
	}
}

Point Operations

PrivateKeyToPoint

Derives the public key point from a private key by performing scalar multiplication on the StarkCurve base point.

Signature:
func PrivateKeyToPoint(privKey *big.Int) (x, y *big.Int)
Parameters:
  • privKey - The private key
Returns:
  • x - The x-coordinate of the public key point
  • y - The y-coordinate of the public key point
When to Use:
  • Recovering public keys from stored private keys
  • Verifying key pair consistency
  • Deriving public keys for imported accounts

Mathematical Background: The function computes PublicKey = PrivateKey * G, where G is the generator point of the StarkCurve.

Usage Example:
package main
 
import (
	"fmt"
	"log"
	"math/big"
 
	"github.com/NethermindEth/starknet.go/curve"
)
 
func main() {
	// Generate a key pair
	privKey, expectedX, expectedY, err := curve.GetRandomKeys()
	if err != nil {
		log.Fatal(err)
	}
 
	fmt.Println("=== Key Derivation Test ===")
	fmt.Printf("Private Key: %s\n", privKey.String())
 
	// Derive public key from private key
	derivedX, derivedY := curve.PrivateKeyToPoint(privKey)
 
	fmt.Printf("\nExpected Public Key:\n")
	fmt.Printf("  X: %s\n", expectedX.String())
	fmt.Printf("  Y: %s\n", expectedY.String())
 
	fmt.Printf("\nDerived Public Key:\n")
	fmt.Printf("  X: %s\n", derivedX.String())
	fmt.Printf("  Y: %s\n", derivedY.String())
 
	// Verify they match
	if derivedX.Cmp(expectedX) == 0 && derivedY.Cmp(expectedY) == 0 {
		fmt.Println("\nDerivation successful - keys match!")
	} else {
		fmt.Println("\nERROR: Derived keys don't match!")
	}
}
Recovering Keys from Hex String:
package main
 
import (
	"fmt"
	"log"
	"math/big"
 
	"github.com/NethermindEth/starknet.go/curve"
)
 
func main() {
	// Import a private key from hex string (e.g., from a wallet backup)
	privateKeyHex := "0x0000000000000000000000000000000085b0ed141c12d4297a9f6fa3032b9757"
 
	privKey := new(big.Int)
	privKey.SetString(privateKeyHex[2:], 16) // Skip "0x" prefix
 
	// Derive the public key
	pubKeyX, pubKeyY := curve.PrivateKeyToPoint(privKey)
 
	fmt.Println("=== Imported Account ===")
	fmt.Printf("Private Key: %s\n", privateKeyHex)
	fmt.Printf("Public Key X: 0x%x\n", pubKeyX)
	fmt.Printf("Public Key Y: 0x%x\n", pubKeyY)
 
	// This is the Starknet account address (public key)
	fmt.Printf("\nStarknet Account: 0x%x\n", pubKeyX)
}
Verifying Key Consistency:
package main
 
import (
	"fmt"
	"log"
	"math/big"
 
	"github.com/NethermindEth/starknet.go/curve"
)
 
func verifyKeyPair(privKey, pubKeyX *big.Int) bool {
	// Derive public key from private key
	derivedX, _ := curve.PrivateKeyToPoint(privKey)
 
	// Check if it matches the provided public key
	return derivedX.Cmp(pubKeyX) == 0
}
 
func main() {
	// Generate keys
	privKey, pubKeyX, _, err := curve.GetRandomKeys()
	if err != nil {
		log.Fatal(err)
	}
 
	// Verify the key pair is valid
	valid := verifyKeyPair(privKey, pubKeyX)
	fmt.Printf("Key pair valid: %t\n", valid)
 
	// Test with wrong public key
	wrongPubKey := new(big.Int).Add(pubKeyX, big.NewInt(1))
	invalid := verifyKeyPair(privKey, wrongPubKey)
	fmt.Printf("Wrong key pair valid: %t (expected false)\n", invalid)
}

Coordinate Calculations

GetYCoordinate

Computes the y-coordinate of a point on the StarkCurve given its x-coordinate.

Signature:
func GetYCoordinate(starkX *felt.Felt) *felt.Felt
Parameters:
  • starkX - The x-coordinate of the point
Returns:
  • *felt.Felt - The y-coordinate of the point
When to Use:
  • Reconstructing full public keys from x-coordinates
  • Working with compressed public keys
  • Validating points on the curve

Mathematical Background: The StarkCurve equation is: yยฒ = xยณ + x + b

This function computes: y = โˆš(xยณ + x + b)

Usage Example:
package main
 
import (
	"fmt"
 
	"github.com/NethermindEth/juno/core/felt"
	"github.com/NethermindEth/starknet.go/curve"
)
 
func main() {
	// Start with just an x-coordinate
	pubKeyX := new(felt.Felt)
	pubKeyX.SetString("0x43135f5e8e5e73d9750659bb5cccc803bc63318584933d584f9b5372ee8ffa6")
 
	// Calculate the corresponding y-coordinate
	pubKeyY := curve.GetYCoordinate(pubKeyX)
 
	fmt.Println("=== Point Reconstruction ===")
	fmt.Printf("X-coordinate: %s\n", pubKeyX.String())
	fmt.Printf("Y-coordinate: %s\n", pubKeyY.String())
 
	// Now we have the complete public key point
	fmt.Printf("\nComplete Public Key: (%s, %s)\n", pubKeyX.String(), pubKeyY.String())
}
Decompressing Multiple Public Keys:
package main
 
import (
	"fmt"
 
	"github.com/NethermindEth/juno/core/felt"
	"github.com/NethermindEth/starknet.go/curve"
)
 
func main() {
	// List of compressed public keys (x-coordinates only)
	compressedKeys := []string{
		"0x43135f5e8e5e73d9750659bb5cccc803bc63318584933d584f9b5372ee8ffa6",
		"0x6591082275c7da568b1542044eb08c2fcf3e0c75121a5275dce4960367f2bb8",
		"0x6a78b5ad5abdb109d4d362c14895efbd45a111d5f80157f669fd127ad0c0fd",
	}
 
	fmt.Println("=== Decompressing Public Keys ===\n")
 
	for i, xHex := range compressedKeys {
		x := new(felt.Felt)
		x.SetString(xHex)
 
		y := curve.GetYCoordinate(x)
 
		fmt.Printf("Key %d:\n", i+1)
		fmt.Printf("  X: %s\n", x.String())
		fmt.Printf("  Y: %s\n", y.String())
		fmt.Println()
	}
}
Verifying Point on Curve:
package main
 
import (
	"fmt"
	"math/big"
 
	"github.com/NethermindEth/juno/core/felt"
	"github.com/NethermindEth/starknet.go/curve"
	starkcurve "github.com/consensys/gnark-crypto/ecc/stark-curve"
)
 
func isPointOnCurve(x, y *felt.Felt) bool {
	// Get curve parameters
	_, b := starkcurve.CurveCoefficients()
 
	// Compute yยฒ
	yBig := y.BigInt(new(big.Int))
	ySquared := new(big.Int).Mul(yBig, yBig)
 
	// Compute xยณ + x + b
	xBig := x.BigInt(new(big.Int))
	xCubed := new(big.Int).Exp(xBig, big.NewInt(3), nil)
	rightSide := new(big.Int).Add(xCubed, xBig)
	rightSide.Add(rightSide, b.BigInt(new(big.Int)))
 
	// Check if curve equation is satisfied
	return ySquared.Cmp(rightSide) == 0
}
 
func main() {
	// Generate a key pair
	_, pubKeyX, pubKeyY, _ := curve.GetRandomKeys()
 
	// Convert to felt.Felt
	x := new(felt.Felt).SetBigInt(pubKeyX)
	y := new(felt.Felt).SetBigInt(pubKeyY)
 
	// Verify point is on curve
	onCurve := isPointOnCurve(x, y)
	fmt.Printf("Point is on curve: %t\n", onCurve)
 
	// Calculate Y from X and verify
	calculatedY := curve.GetYCoordinate(x)
	fmt.Printf("Calculated Y matches: %t\n", calculatedY.Equal(y))
}

Complete Account Setup Example

Here's a comprehensive example showing the full account creation workflow:

package main
 
import (
	"fmt"
	"log"
	"math/big"
 
	"github.com/NethermindEth/juno/core/felt"
	"github.com/NethermindEth/starknet.go/curve"
)
 
type StarknetAccount struct {
	PrivateKey *big.Int
	PublicKeyX *big.Int
	PublicKeyY *big.Int
	Address    string
}
 
func createAccount() (*StarknetAccount, error) {
	// Step 1: Generate random keys
	privKey, pubKeyX, pubKeyY, err := curve.GetRandomKeys()
	if err != nil {
		return nil, fmt.Errorf("key generation failed: %w", err)
	}
 
	// Step 2: Format address (using public key X)
	address := fmt.Sprintf("0x%064x", pubKeyX)
 
	return &StarknetAccount{
		PrivateKey: privKey,
		PublicKeyX: pubKeyX,
		PublicKeyY: pubKeyY,
		Address:    address,
	}, nil
}
 
func (acc *StarknetAccount) Sign(msgHash *big.Int) (r, s *big.Int, err error) {
	return curve.Sign(msgHash, acc.PrivateKey)
}
 
func (acc *StarknetAccount) Verify(msgHash, r, s *big.Int) (bool, error) {
	return curve.Verify(msgHash, r, s, acc.PublicKeyX)
}
 
func (acc *StarknetAccount) Export() map[string]string {
	return map[string]string{
		"address":     acc.Address,
		"private_key": fmt.Sprintf("0x%064x", acc.PrivateKey),
		"public_key":  fmt.Sprintf("0x%064x", acc.PublicKeyX),
	}
}
 
func importAccount(privateKeyHex string) (*StarknetAccount, error) {
	// Parse private key
	privKey := new(big.Int)
	_, ok := privKey.SetString(privateKeyHex[2:], 16)
	if !ok {
		return nil, fmt.Errorf("invalid private key format")
	}
 
	// Derive public key
	pubKeyX, pubKeyY := curve.PrivateKeyToPoint(privKey)
 
	// Format address
	address := fmt.Sprintf("0x%064x", pubKeyX)
 
	return &StarknetAccount{
		PrivateKey: privKey,
		PublicKeyX: pubKeyX,
		PublicKeyY: pubKeyY,
		Address:    address,
	}, nil
}
 
func main() {
	fmt.Println("=== Create New Account ===")
 
	// Create a new account
	account, err := createAccount()
	if err != nil {
		log.Fatal(err)
	}
 
	fmt.Printf("Address: %s\n", account.Address)
	fmt.Printf("Public Key: 0x%x\n", account.PublicKeyX)
	fmt.Println()
 
	// Test signing
	fmt.Println("=== Test Signing ===")
	msgHash := big.NewInt(12345)
	r, s, err := account.Sign(msgHash)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("Message signed successfully\n")
	fmt.Printf("Signature: (r=%s, s=%s)\n\n", r.String(), s.String())
 
	// Test verification
	fmt.Println("=== Test Verification ===")
	valid, err := account.Verify(msgHash, r, s)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("Signature valid: %t\n\n", valid)
 
	// Export account
	fmt.Println("=== Export Account ===")
	exported := account.Export()
	for key, value := range exported {
		fmt.Printf("%s: %s\n", key, value)
	}
	fmt.Println()
 
	// Import account
	fmt.Println("=== Import Account ===")
	imported, err := importAccount(exported["private_key"])
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("Imported address: %s\n", imported.Address)
	fmt.Printf("Keys match: %t\n", imported.Address == account.Address)
}

Key Derivation Paths

For hierarchical deterministic (HD) wallets, you might want to derive multiple keys from a master seed:

package main
 
import (
	"crypto/sha256"
	"fmt"
	"math/big"
 
	"github.com/NethermindEth/starknet.go/curve"
)
 
// Simple key derivation (for demonstration only - use proper HD wallet libs in production)
func deriveKey(masterSeed []byte, index uint32) (*big.Int, *big.Int, *big.Int) {
	// Create child seed by hashing master seed with index
	h := sha256.New()
	h.Write(masterSeed)
	h.Write([]byte(fmt.Sprintf("%d", index)))
	childSeed := h.Sum(nil)
 
	// Use child seed as private key (modulo curve order)
	privKey := new(big.Int).SetBytes(childSeed)
 
	// Derive public key
	pubKeyX, pubKeyY := curve.PrivateKeyToPoint(privKey)
 
	return privKey, pubKeyX, pubKeyY
}
 
func main() {
	// Master seed (in practice, this would be from a BIP39 mnemonic)
	masterSeed := []byte("my_secure_master_seed_phrase")
 
	fmt.Println("=== HD Wallet - Key Derivation ===\n")
 
	// Derive multiple accounts from master seed
	for i := uint32(0); i < 5; i++ {
		_, pubKeyX, _ := deriveKey(masterSeed, i)
 
		fmt.Printf("Account %d: 0x%064x\n", i, pubKeyX)
	}
 
	fmt.Println("\nNote: Use proper HD wallet libraries like BIP32/BIP44 in production!")
}

Security Best Practices

Private Key Protection

  1. Never log or print private keys:
// BAD
fmt.Printf("Private key: %s\n", privKey.String())
 
// GOOD
fmt.Printf("Public key: %s\n", pubKeyX.String())
  1. Clear sensitive data from memory:
func secureKeyGeneration() {
	privKey, pubKeyX, pubKeyY, err := curve.GetRandomKeys()
	if err != nil {
		log.Fatal(err)
	}
 
	// Use the keys...
	doSomethingWithKeys(privKey, pubKeyX, pubKeyY)
 
	// Clear private key from memory when done
	privKey.SetInt64(0)
}
  1. Use encrypted storage:
import "crypto/aes"
 
func storePrivateKey(privKey *big.Int, password string) error {
	// Encrypt private key before storing
	encryptedKey := encryptWithPassword(privKey.Bytes(), password)
 
	// Store encrypted key
	return saveToSecureStorage(encryptedKey)
}

Key Generation

  1. Always use GetRandomKeys() for production:
// GOOD - Cryptographically secure
privKey, pubKeyX, pubKeyY, err := curve.GetRandomKeys()
 
// BAD - Never use for production
privKey := big.NewInt(12345) // Predictable!
  1. Validate imported keys:
func validatePrivateKey(privKey *big.Int) error {
	// Check if private key is in valid range
	if privKey.Sign() <= 0 {
		return errors.New("private key must be positive")
	}
 
	// Check against curve order
	// (Simplified - use proper curve order check in production)
	maxKey := new(big.Int).Exp(big.NewInt(2), big.NewInt(251), nil)
	if privKey.Cmp(maxKey) >= 0 {
		return errors.New("private key too large")
	}
 
	return nil
}

Key Backup and Recovery

type KeyBackup struct {
	EncryptedPrivateKey []byte
	PublicKeyX          string
	CreatedAt           time.Time
	Version             string
}
 
func backupKey(privKey *big.Int, pubKeyX *big.Int, password string) (*KeyBackup, error) {
	// Encrypt private key
	encrypted, err := encryptKey(privKey, password)
	if err != nil {
		return nil, err
	}
 
	return &KeyBackup{
		EncryptedPrivateKey: encrypted,
		PublicKeyX:          fmt.Sprintf("0x%064x", pubKeyX),
		CreatedAt:           time.Now(),
		Version:             "1.0",
	}, nil
}

Testing Key Operations

package main
 
import (
	"fmt"
	"math/big"
	"testing"
 
	"github.com/NethermindEth/starknet.go/curve"
)
 
func TestKeyGeneration(t *testing.T) {
	privKey, pubKeyX, pubKeyY, err := curve.GetRandomKeys()
	if err != nil {
		t.Fatalf("Key generation failed: %v", err)
	}
 
	// Keys should not be nil
	if privKey == nil || pubKeyX == nil || pubKeyY == nil {
		t.Fatal("Generated keys are nil")
	}
 
	// Private key should be positive
	if privKey.Sign() <= 0 {
		t.Fatal("Private key is not positive")
	}
 
	// Public keys should be positive
	if pubKeyX.Sign() <= 0 || pubKeyY.Sign() <= 0 {
		t.Fatal("Public keys are not positive")
	}
}
 
func TestKeyDerivation(t *testing.T) {
	// Generate key pair
	privKey, expectedX, expectedY, err := curve.GetRandomKeys()
	if err != nil {
		t.Fatalf("Key generation failed: %v", err)
	}
 
	// Derive public key from private key
	derivedX, derivedY := curve.PrivateKeyToPoint(privKey)
 
	// Verify they match
	if derivedX.Cmp(expectedX) != 0 {
		t.Errorf("X coordinates don't match: expected %s, got %s",
			expectedX.String(), derivedX.String())
	}
 
	if derivedY.Cmp(expectedY) != 0 {
		t.Errorf("Y coordinates don't match: expected %s, got %s",
			expectedY.String(), derivedY.String())
	}
}
 
func TestSignVerifyRoundTrip(t *testing.T) {
	// Generate keys
	privKey, pubKeyX, _, err := curve.GetRandomKeys()
	if err != nil {
		t.Fatalf("Key generation failed: %v", err)
	}
 
	// Create message
	msgHash := big.NewInt(999999)
 
	// Sign
	r, s, err := curve.Sign(msgHash, privKey)
	if err != nil {
		t.Fatalf("Signing failed: %v", err)
	}
 
	// Verify
	valid, err := curve.Verify(msgHash, r, s, pubKeyX)
	if err != nil {
		t.Fatalf("Verification failed: %v", err)
	}
 
	if !valid {
		t.Fatal("Signature verification failed")
	}
}

Related Resources