DEV Community

Cover image for How is a public key really generated? (with Golang)
Diego Cândido
Diego Cândido

Posted on

How is a public key really generated? (with Golang)

When talking about cryptocurrencies and blockchain, it's common to hear that your wallet is just a pair of keys: a private key and a public key.

But how exactly is a public key generated from a private key? And how does that turn into a Bitcoin address?

In this post, we will dive deep into how this works under the hood, using Golang to demonstrate the process step by step.

What is a private key?

A private key is simply a very large number. Seriously. Just a random number between 1 and the maximum allowed by the elliptic curve (secp256k1 in Bitcoin).

The more random, the better. You can generate it from mouse movements, atmospheric noise, or even the lottery draw. But for simplicity, in this example, we'll derive it from a passphrase (which is, of course, not secure in real-world applications).

🚀 Step 1: Generating a Private Key

Let's generate a private key by hashing a simple passphrase using SHA-256.

package main

import (
    "crypto/sha256"
    "fmt"

    "github.com/btcsuite/btcd/btcec/v2"
)

func main() {
    passphrase := "Hello World!"
    privateKeyBytes := sha256.Sum256([]byte(passphrase))

    privateKey, _ := btcec.PrivKeyFromBytes(privateKeyBytes[:])

    fmt.Printf("Private Key: %x\n", privateKey.Serialize())
}

Enter fullscreen mode Exit fullscreen mode

It's important to note that btcec.PrivKeyFromBytes() is a function from the btcsuite/btcd/btcec library that converts bytes into a private key on the secp256k1 elliptic curve, which is used in Bitcoin.

🔓 Step 2: Deriving the Public Key

The public key is generated by applying elliptic curve multiplication: PublicKey = PrivateKey × GeneratorPoint (G) (yeah, math stuff that we don't need to undestand for a while).

This is a one-way function: it's easy to compute the public key from the private key, but practically impossible to compute the private key from the public key.

publicKey := privateKey.PubKey()
publicKeyBytes := publicKey.SerializeCompressed()

fmt.Printf("Public Key: %x\n", publicKeyBytes)
Enter fullscreen mode Exit fullscreen mode

🔗 Step 3: Hashing the Public Key

Before converting it into a Bitcoin address, the public key is hashed twice:

  1. SHA-256
  2. RIPEMD-160

This gives us the public key hash.

import (
    "crypto/sha256"
    "golang.org/x/crypto/ripemd160"
    "log"
)

shaHash := sha256.Sum256(publicKeyBytes)

ripemd := ripemd160.New()
_, err := ripemd.Write(shaHash[:])
if err != nil {
    log.Fatal(err)
}

publicKeyHash := ripemd.Sum(nil)
fmt.Printf("Public Key Hash (RIPEMD160): %x\n", publicKeyHash)
Enter fullscreen mode Exit fullscreen mode

🏷️ Step 4: Adding Version Prefix

Bitcoin addresses start with different characters depending on the network. 0x00 for mainnet (addresses start with "1").

versionedPayload := append([]byte{0x00}, publicKeyHash...)
Enter fullscreen mode Exit fullscreen mode

✅ Step 5: Checksum (Double SHA256)

Compute the checksum:

firstSHA := sha256.Sum256(versionedPayload)
secondSHA := sha256.Sum256(firstSHA[:])
checksum := secondSHA[:4]
Enter fullscreen mode Exit fullscreen mode

📦 Step 6: Create the Final Address

Concatenate the payload and checksum, and encode it using Base58Check (Bitcoin's encoding format).

import "github.com/btcsuite/btcutil/base58"

fullPayload := append(versionedPayload, checksum...)
address := base58.Encode(fullPayload)

fmt.Printf("Bitcoin Address: %s\n", address)
Enter fullscreen mode Exit fullscreen mode

Full Code Example

package main

import (
    "crypto/sha256"
    "fmt"
    "log"

    "github.com/btcsuite/btcd/btcec/v2"
    "github.com/btcsuite/btcutil/base58"
    "golang.org/x/crypto/ripemd160"
)

func main() {
    passphrase := "Hello World!"

    privateKeyBytes := sha256.Sum256([]byte(passphrase))
    privateKey, _ := btcec.PrivKeyFromBytes(privateKeyBytes[:])
    fmt.Printf("Private Key: %x\n", privateKey.Serialize())

    publicKey := privateKey.PubKey()
    publicKeyBytes := publicKey.SerializeCompressed()
    fmt.Printf("Public Key: %x\n", publicKeyBytes)

    shaHash := sha256.Sum256(publicKeyBytes)

    ripemd := ripemd160.New()
    _, err := ripemd.Write(shaHash[:])
    if err != nil {
        log.Fatal(err)
    }
    publicKeyHash := ripemd.Sum(nil)
    fmt.Printf("Public Key Hash (RIPEMD160): %x\n", publicKeyHash)

    versionedPayload := append([]byte{0x00}, publicKeyHash...)

    firstSHA := sha256.Sum256(versionedPayload)
    secondSHA := sha256.Sum256(firstSHA[:])
    checksum := secondSHA[:4]

    fullPayload := append(versionedPayload, checksum...)
    address := base58.Encode(fullPayload)

    fmt.Printf("Bitcoin Address: %s\n", address)
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

This example uses a passphrase as input, which is for educational purposes only. Never generate a wallet this way in the real world. A private key should be generated from a cryptographically secure random number generator to avoid collisions and to maintain security.

This post is based on the topic Generating Keys from the book Mastering Blockchain by Lorne Lantz and Daniel Cawrey.

Top comments (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.