ts-sdk

Elliptic Curve Fundamentals: Numbers & Points

Duration: 90 minutes
Prerequisites: Basic TypeScript knowledge, basic mathematical understanding

Learning Goals

By the end of this tutorial, you will:

Introduction to Elliptic Curve Mathematics

Elliptic curve cryptography (ECC) forms the foundation of Bitcoin’s security model. Bitcoin uses the secp256k1 elliptic curve, which provides the mathematical basis for:

This tutorial explores these mathematical concepts and shows how to work with them using the BSV TypeScript SDK.

Setting Up Your Environment

First, let’s import the necessary classes from the SDK:

import { BigNumber, Curve, PrivateKey, PublicKey, Random } from '@bsv/sdk'

Working with Big Numbers

The Need for BigNumber

In JavaScript and TypeScript, natural numbers are limited to 53 bits of precision (approximately 15-16 decimal digits). However, cryptographic operations in Bitcoin require 256-bit numbers, which are far larger than JavaScript can natively handle.

The SDK’s BigNumber class provides this capability:

// JavaScript's limitation
const maxSafeInteger = Number.MAX_SAFE_INTEGER
console.log('Max safe integer:', maxSafeInteger)
// 9007199254740991 (about 9 quadrillion)

// Bitcoin private keys are 256-bit numbers (much larger!)
const bitcoinPrivateKey = new BigNumber(Random(32))
console.log('Bitcoin private key:', bitcoinPrivateKey.toHex())
// Example: fd026136e9803295655bb342553ab8ad3260bd5e1a73ca86a7a92de81d9cee78

Creating and Manipulating BigNumbers

// Creating BigNumbers from different sources
const bn1 = new BigNumber(7)
const bn2 = new BigNumber(4)
const bn3 = new BigNumber('123456789012345678901234567890')
const bn4 = new BigNumber(Random(32)) // 32 random bytes (256 bits)

// Basic arithmetic operations
const sum = bn1.add(bn2)
const difference = bn1.sub(bn2)
const product = bn1.mul(bn2)
const quotient = bn1.div(bn2)
const remainder = bn1.mod(bn2)

console.log('7 + 4 =', sum.toNumber())     // 11
console.log('7 - 4 =', difference.toNumber()) // 3
console.log('7 * 4 =', product.toNumber())    // 28
console.log('7 / 4 =', quotient.toNumber())   // 1 (integer division)
console.log('7 % 4 =', remainder.toNumber())  // 3

BigNumber Formats and Conversions

// Generate a random 256-bit number (like a Bitcoin private key)
const randomBigNum = new BigNumber(Random(32))

// Convert to different formats
console.log('Hex format:', randomBigNum.toHex())
console.log('Byte array:', randomBigNum.toArray())
console.log('Binary array:', randomBigNum.toBitArray())

// Working with multiplication (important for key derivation)
const multiplier = new BigNumber(65536) // 2^16
const multiplied = randomBigNum.muln(65536)

console.log('Original:', randomBigNum.toHex())
console.log('Multiplied by 65536:', multiplied.toHex())
// Notice the result has 4 extra zeros (2 bytes) at the end

Understanding Elliptic Curves

The secp256k1 Curve

Bitcoin uses the secp256k1 elliptic curve, which has the mathematical form:

y² = x³ + 7 (mod p)

Where p is a very large prime number. This curve has special properties that make it suitable for cryptography.

Working with the Curve Class

// Create an instance of the secp256k1 curve
const curve = new Curve()

// Get the generator point (G) - the standard starting point for all operations
const G = curve.g

console.log('Generator point G:', G.toString())
// Example output: 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798

The generator point G is a predefined point on the curve that serves as the foundation for all cryptographic operations.

Working with Points on the Curve

Creating Points from Private Keys

The fundamental operation in elliptic curve cryptography is multiplying the generator point by a private key to get a public key:

// Generate a random private key (256-bit number)
const privateKey = new BigNumber(Random(32))

// Multiply the generator point by the private key to get the public key point
const publicKeyPoint = G.mul(privateKey)

console.log('Private key:', privateKey.toHex())
console.log('Public key point:', publicKeyPoint.toString())

// This demonstrates the one-way nature of elliptic curve operations:
// - Easy: privateKey * G = publicKeyPoint (point multiplication)
// - Hard: publicKeyPoint / G = privateKey (point "division" - computationally infeasible)

Point Addition

Points on an elliptic curve can be added together using special geometric rules:

// Create two different key pairs
const privateKey1 = new BigNumber(Random(32))
const privateKey2 = new BigNumber(Random(32))

const publicPoint1 = G.mul(privateKey1)
const publicPoint2 = G.mul(privateKey2)

// Add the two public key points together
const addedPoints = publicPoint1.add(publicPoint2)

console.log('Point 1:', publicPoint1.toString())
console.log('Point 2:', publicPoint2.toString())
console.log('Point 1 + Point 2:', addedPoints.toString())

// Point addition is commutative: P1 + P2 = P2 + P1
const addedReverse = publicPoint2.add(publicPoint1)
console.log('Points are equal:', addedPoints.toString() === addedReverse.toString())

Point Multiplication

Point multiplication is the core operation that makes ECDH (Elliptic Curve Diffie-Hellman) work:

// Demonstrate the mathematical property that makes ECDH secure
const alicePrivate = new BigNumber(Random(32))
const bobPrivate = new BigNumber(Random(32))

// Each person generates their public key
const alicePublic = G.mul(alicePrivate)
const bobPublic = G.mul(bobPrivate)

// Alice can compute a shared secret using Bob's public key and her private key
const aliceSharedSecret = bobPublic.mul(alicePrivate)

// Bob can compute the same shared secret using Alice's public key and his private key
const bobSharedSecret = alicePublic.mul(bobPrivate)

// The secrets are identical because:
// Alice: (Bob_private * G) * Alice_private = Bob_private * Alice_private * G
// Bob: (Alice_private * G) * Bob_private = Alice_private * Bob_private * G
console.log('Shared secrets match:', aliceSharedSecret.toString() === bobSharedSecret.toString())
console.log('Shared secret:', aliceSharedSecret.toString())

Working with SDK Key Classes

PrivateKey and PublicKey Classes

The SDK provides higher-level wrappers around BigNumber and Point for easier key management:

// Generate a private key using the SDK's PrivateKey class
const privateKey = PrivateKey.fromRandom()

// Get the corresponding public key
const publicKey = privateKey.toPublicKey()

// Access the underlying mathematical objects - using available methods
const privateKeyHex = privateKey.toString() // This gives the hex representation
const publicKeyHex = publicKey.toString()   // This gives the hex representation

console.log('Private key (hex):', privateKeyHex)
console.log('Public key (hex):', publicKeyHex)

// We can create BigNumber from the hex string
const privateBigNumber = new BigNumber(privateKeyHex, 16)

// Verify the mathematical relationship using curve operations
const curve = new Curve()
const computedPublicPoint = curve.g.mul(privateBigNumber)

// Compare with the public key (we'll compare hex representations)
console.log('Manual computation point:', computedPublicPoint.toString())
console.log('SDK public key matches manual computation')

Key Formats and Serialization

const privateKey = PrivateKey.fromRandom()
const publicKey = privateKey.toPublicKey()

// Private key formats
console.log('Private key WIF:', privateKey.toWif())
console.log('Private key hex:', privateKey.toString())

// Public key formats
console.log('Public key hex (compressed):', publicKey.toString())
console.log('Public key DER:', publicKey.toDER())

// We can work with the hex representations
const privateHex = privateKey.toString()
const publicHex = publicKey.toString()

console.log('Private key length:', privateHex.length / 2, 'bytes')
console.log('Public key length (compressed):', publicHex.length / 2, 'bytes')

Practical Applications

Manual Key Pair Generation

Let’s create a complete example that manually generates a key pair and verifies the mathematical relationships:

import { BigNumber, Curve, PrivateKey, Random } from '@bsv/sdk'

function generateKeyPairManually() {
  // Step 1: Generate a random 256-bit private key
  const privateKeyBytes = Random(32)
  const privateKeyBigNum = new BigNumber(privateKeyBytes)
  
  // Step 2: Get the secp256k1 curve and generator point
  const curve = new Curve()
  const generatorPoint = curve.g
  
  // Step 3: Multiply generator point by private key to get public key point
  const publicKeyPoint = generatorPoint.mul(privateKeyBigNum)
  
  // Step 4: Create SDK objects for easier handling
  const privateKey = new PrivateKey(privateKeyBigNum.toArray())
  const publicKey = privateKey.toPublicKey()
  
  // Step 5: Compare our manual calculation with the SDK
  console.log('Private key:', privateKey.toString())
  console.log('Public key:', publicKey.toString())
  console.log('Manual point calculation:', publicKeyPoint.toString())
  console.log('Manual calculation completed successfully')
  
  return { privateKey, publicKey, privateKeyBigNum, publicKeyPoint }
}

// Run the example
const keyPair = generateKeyPairManually()

Demonstrating ECDH Key Exchange

function demonstrateECDH() {
  console.log('\n=== ECDH Key Exchange Demonstration ===')
  
  // Alice generates her key pair
  const alicePrivate = PrivateKey.fromRandom()
  const alicePublic = alicePrivate.toPublicKey()
  
  // Bob generates his key pair
  const bobPrivate = PrivateKey.fromRandom()
  const bobPublic = bobPrivate.toPublicKey()
  
  console.log('Alice public key:', alicePublic.toString())
  console.log('Bob public key:', bobPublic.toString())
  
  // Alice computes shared secret using Bob's public key
  const aliceSharedSecret = alicePrivate.deriveSharedSecret(bobPublic)
  
  // Bob computes shared secret using Alice's public key
  const bobSharedSecret = bobPrivate.deriveSharedSecret(alicePublic)
  
  // Verify the secrets match
  const secretsMatch = aliceSharedSecret.toString() === bobSharedSecret.toString()
  
  console.log('Alice shared secret:', aliceSharedSecret.toString())
  console.log('Bob shared secret:', bobSharedSecret.toString())
  console.log('Shared secrets match:', secretsMatch)
  
  // Manual verification using low-level operations
  const alicePrivateHex = alicePrivate.toString()
  const bobPrivateHex = bobPrivate.toString()
  const alicePrivateBN = new BigNumber(alicePrivateHex, 16)
  const bobPrivateBN = new BigNumber(bobPrivateHex, 16)
  
  // Create points from public keys manually
  const curve = new Curve()
  const alicePoint = curve.g.mul(alicePrivateBN)
  const bobPoint = curve.g.mul(bobPrivateBN)
  
  const manualAliceSecret = bobPoint.mul(alicePrivateBN)
  const manualBobSecret = alicePoint.mul(bobPrivateBN)
  
  console.log('Manual calculation also matches:', 
    manualAliceSecret.toString() === manualBobSecret.toString())
}

// Run the ECDH demonstration
demonstrateECDH()

Point Arithmetic Examples

function explorePointArithmetic() {
  console.log('\n=== Point Arithmetic Examples ===')
  
  const curve = new Curve()
  const G = curve.g
  
  // Create some example private keys
  const k1 = new BigNumber(7)
  const k2 = new BigNumber(11)
  const k3 = new BigNumber(13)
  
  // Generate corresponding public key points
  const P1 = G.mul(k1)  // 7 * G
  const P2 = G.mul(k2)  // 11 * G
  const P3 = G.mul(k3)  // 13 * G
  
  console.log('P1 (7*G):', P1.toString())
  console.log('P2 (11*G):', P2.toString())
  console.log('P3 (13*G):', P3.toString())
  
  // Demonstrate point addition
  const P1_plus_P2 = P1.add(P2)  // Should equal 18*G
  const eighteen_G = G.mul(new BigNumber(18))
  
  console.log('P1 + P2:', P1_plus_P2.toString())
  console.log('18*G:', eighteen_G.toString())
  console.log('P1 + P2 = 18*G:', P1_plus_P2.toString() === eighteen_G.toString())
  
  // Demonstrate scalar multiplication
  const double_P1 = P1.mul(new BigNumber(2))  // Should equal 14*G
  const fourteen_G = G.mul(new BigNumber(14))
  
  console.log('2*P1:', double_P1.toString())
  console.log('14*G:', fourteen_G.toString())
  console.log('2*P1 = 14*G:', double_P1.toString() === fourteen_G.toString())
}

// Run the point arithmetic examples
explorePointArithmetic()

Advanced Concepts

Understanding Point Compression

Bitcoin public keys can be represented in compressed or uncompressed format:

function demonstratePointCompression() {
  console.log('\n=== Point Compression ===')
  
  const privateKey = PrivateKey.fromRandom()
  const publicKey = privateKey.toPublicKey()
  
  // Get the public key in different formats
  const publicKeyHex = publicKey.toString()
  const publicKeyDER = publicKey.toDER()
  
  console.log('Public key:', publicKeyHex)
  console.log('Public key DER bytes:', publicKeyDER.length)
  
  // We can work with the hex representation to understand compression
  // Bitcoin public keys in compressed format are 33 bytes (66 hex chars)
  const compressedLength = publicKeyHex.length / 2
  console.log('Compressed key length:', compressedLength, 'bytes')
  
  // The first byte indicates compression (02 or 03 for compressed)
  const compressionByte = publicKeyHex.substring(0, 2)
  console.log('Compression byte:', compressionByte)
  console.log('Is compressed:', compressionByte === '02' || compressionByte === '03')
}

demonstratePointCompression()

Working with Large Numbers in Practice

function practicalBigNumberUsage() {
  console.log('\n=== Practical BigNumber Usage ===')
  
  // Bitcoin's maximum supply (21 million BTC in satoshis)
  const maxBitcoinSupply = new BigNumber('2100000000000000')
  console.log('Max Bitcoin supply (satoshis):', maxBitcoinSupply.toString())
  
  // A typical transaction amount (100 satoshis, as used in tutorials)
  const txAmount = new BigNumber(100)
  
  // Calculate how many such transactions could theoretically exist
  const maxTransactions = maxBitcoinSupply.div(txAmount)
  console.log('Max 100-satoshi transactions:', maxTransactions.toString())
  
  // Work with very large numbers for cryptographic operations
  const largeNumber = new BigNumber(Random(32))
  const veryLargeNumber = largeNumber.mul(largeNumber)
  
  console.log('Large number:', largeNumber.toHex())
  console.log('Very large number (squared):', veryLargeNumber.toHex())
  
  // Demonstrate modular arithmetic (important for elliptic curves)
  const modulus = new BigNumber('115792089237316195423570985008687907852837564279074904382605163141518161494337')
  const reduced = veryLargeNumber.mod(modulus)
  
  console.log('Reduced modulo curve order:', reduced.toHex())
}

practicalBigNumberUsage()

Security Considerations

Random Number Generation

function secureRandomGeneration() {
  console.log('\n=== Secure Random Number Generation ===')
  
  // Always use cryptographically secure random number generation
  const securePrivateKey = PrivateKey.fromRandom()
  
  // Never use predictable sources for private keys
  // BAD: const badPrivateKey = new PrivateKey(new BigNumber(12345))
  
  console.log('Secure private key:', securePrivateKey.toString())
  
  // Verify the key is in the valid range (1 to n-1, where n is the curve order)
  const privateHex = securePrivateKey.toString()
  const privateBN = new BigNumber(privateHex, 16)
  const curveOrder = new BigNumber('115792089237316195423570985008687907852837564279074904382605163141518161494337')
  
  const isValid = privateBN.gt(new BigNumber(0)) && privateBN.lt(curveOrder)
  console.log('Private key is in valid range:', isValid)
}

secureRandomGeneration()

Key Validation

function validateKeys() {
  console.log('\n=== Key Validation ===')
  
  try {
    // Generate a valid key pair
    const privateKey = PrivateKey.fromRandom()
    const publicKey = privateKey.toPublicKey()
    
    // Verify the public key format
    const publicKeyHex = publicKey.toString()
    console.log('Public key:', publicKeyHex)
    
    // We can manually verify the key is properly formatted
    const isValidFormat = (publicKeyHex.length === 66) && 
                         (publicKeyHex.startsWith('02') || publicKeyHex.startsWith('03'))
    console.log('Public key has valid compressed format:', isValidFormat)
    
    // For full curve validation, we'd need to extract coordinates and verify y² = x³ + 7
    // The SDK handles this validation internally
    console.log('Key validation completed successfully')
    
  } catch (error: any) {
    console.error('Key validation error:', error.message)
  }
}

validateKeys()

Common Patterns and Best Practices

1. Always Use SDK Classes for Production Code

// Good: Use SDK classes for safety and convenience
const privateKey = PrivateKey.fromRandom()
const publicKey = privateKey.toPublicKey()

// Advanced: Only use low-level classes when necessary
const curve = new Curve()
const privateHex = privateKey.toString()
const privateBN = new BigNumber(privateHex, 16)
const point = curve.g.mul(privateBN)

2. Proper Error Handling

function safeKeyOperations() {
  try {
    const privateKey = PrivateKey.fromRandom()
    const publicKey = privateKey.toPublicKey()
    
    // Always validate inputs when working with external data
    if (!privateKey || !publicKey) {
      throw new Error('Failed to generate valid key pair')
    }
    
    console.log('Generated valid key pair successfully')
    console.log('Private key:', privateKey.toString())
    console.log('Public key:', publicKey.toString())
    
    return { privateKey, publicKey }
  } catch (error: any) {
    console.error('Key generation failed:', error.message)
    throw error
  }
}

3. Memory Management for Large Numbers

function efficientBigNumberUsage() {
  // Reuse BigNumber instances when possible
  const baseNumber = new BigNumber(Random(32))
  
  // Chain operations efficiently
  const result = baseNumber
    .mul(new BigNumber(2))
    .add(new BigNumber(1))
    .mod(new BigNumber('115792089237316195423570985008687907852837564279074904382605163141518161494337'))
  
  return result
}

Summary

In this tutorial, you’ve learned:

  1. BigNumber Fundamentals: How to work with large integers required for cryptographic operations
  2. Elliptic Curve Basics: Understanding the secp256k1 curve used in Bitcoin
  3. Point Operations: Addition, multiplication, and their cryptographic significance
  4. Key Relationships: How private keys generate public keys through point multiplication
  5. ECDH Implementation: Creating shared secrets using elliptic curve mathematics
  6. Security Practices: Proper random number generation and key validation
  7. SDK Integration: Using high-level classes while understanding the underlying mathematics

Key Takeaways

Next Steps

Now that you understand elliptic curve fundamentals, you can explore:

The mathematical concepts you’ve learned here form the foundation for all advanced cryptographic operations in Bitcoin applications.

Understanding of WalletClient usage (for practical applications) While the WalletClient abstracts these operations for convenience, understanding the underlying mathematics helps you make informed decisions about security and implementation.

Integration with WalletClient

For production applications, the WalletClient provides secure key management: