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 keyx- The x-coordinate of the public key (the Starknet public key/account identifier)y- The y-coordinate of the public keyerr- Error if key generation fails
- 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.
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)
}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)privKey- The private key
x- The x-coordinate of the public key pointy- The y-coordinate of the public key point
- 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.
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!")
}
}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)
}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.FeltstarkX- The x-coordinate of the point
*felt.Felt- The y-coordinate of the point
- 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)
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())
}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()
}
}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
- Never log or print private keys:
// BAD
fmt.Printf("Private key: %s\n", privKey.String())
// GOOD
fmt.Printf("Public key: %s\n", pubKeyX.String())- 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)
}- 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
- Always use GetRandomKeys() for production:
// GOOD - Cryptographically secure
privKey, pubKeyX, pubKeyY, err := curve.GetRandomKeys()
// BAD - Never use for production
privKey := big.NewInt(12345) // Predictable!- 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
- Cryptographic Functions - Using keys for signing and verification
- Hashing Functions - Computing message hashes for signing
- Account Package - Higher-level account management
- Starknet Key Management

