DEV Community

loading...
Cover image for Building a Blockchain in Go Pt V - Wallets

Building a Blockchain in Go Pt V - Wallets

nheindev profile image Noah Hein 惻Updated on 惻13 min read

Hello Readers!

You may have guessed that we will be building a wallet module. You would be correct.

code can be found here

What is a Wallet?

Excellent question!

In the real world a wallet may hold your money, along with maybe your credit/debit cards or ID. However in the blockchain world your wallet does not hold any money at all. It holds all of your private keys. You went over transactions, in the previous post and used a pseudo-key to see if you were eligible to spend the money in the block that the key unlocked. This is the functionality of what keys do in the blockchain.

If you were to come up with a diagram, it would look a little something like this.

Wallet makes private key -> key unlocks block -> block releases money.

So a wallet holds keys.

That's great, but....

What is a Key?

Excellent question!

Wow, you sure do have a lot of those. Good thing I anticiapted you being such a smart cookie.

As you remember from the last section, a key will unlock the data that is representative of your money.

The thing about this "key", is there is actually two of them. They are tied together, there is a public key, and a private key.

Hydra heads multiplying

You have a private key, and this is the one that you wallet holds. You don't want to share this with anyone, as anyone that has your private key can access all of the money that belongs to you.

You also have a public key which is tied to your private key. This key may be used by anyone, and when someone sends money to you, they use your public key to lock the funds up.

If I were to use an analogy, it would be slapping your own personal branded padlock on something. Everyone knows its your padlock, and everyone knows that only your private key will unlock it. Anyone can take your padlock and lock funds up that they want to send to you.

Lock it down, playing piano

So in recap, you have the two keys. Your wallet generates both of them, and they are tied together. Your wallet keeps your private key a secret from everyone, while simultaneously sharing your public key with anyone who wants it.

Public keys let people lock funds up under your name.

Private keys let you unlock said funds.

Keep in mind that this is not a perfect analogy, and depending on the form of encryption you are using, these roles may be reversed. Just remember that either can lock, and whichever one locked it, the other one can unlock.

It is this mechanic where the phrase, "not your keys, not your coins" comes from.

If you are on an exchange that abstracts this wallet idea from you as a user, they are holding your keys. While this is not typically an issue, it is certainly a potential one. Many people who join blockchain technology for the decentralization and security aspect will not stomach such vulnerability.

Hopefully you now know a bit more about wallets and keys and why they are important. However, I am talking encryption here and I would be doing you a disservice to not talk about the underlying algorithms that make your keys safe from ne'er-do-wells.

A wallet has an address that is unique to itself, and is the address that people will see and use when sending you money.

This mental model is pretty simple to visualize.
Wallet Mental Model

However, how we generate these keys and derive the address from them is a bit more complicated. This will become apparent as we start to build out the wallet.

BUT FIRST!

House Cleaning

As per usual, there is a bit of refactoring to do before we go further.

I think it's time you boot up ye-olde code editor to follow along!

Cli Cleanup

I like my main.go files to be a bit cleaner than what it looks like currently. I'm going to abstract all of this CLI business to its own folder and have it be its own package.

You will want to create a cli folder in your root directory, and inside of it make a cli.go file.

Folder Structure

We are going to move everything that is not inside our main function over to cli.go. The only thing to change here is you need to make the run() command start with a capital R to indicate to the go complier that it is a public method and can be called as such.

//cli.go
func (cli *CommandLine) Run(){
// Big fat function here
}
Enter fullscreen mode Exit fullscreen mode

That should leave your main.go file looking like this:

//main.go
package main

import (
    "os"

    "github.com/nheingit/learnGo/cli"
)

func main() {
    defer os.Exit(0)

    cmd := cli.CommandLine{}

    cmd.Run()
}

Enter fullscreen mode Exit fullscreen mode

Everything else should be in the cli.go file, with a package cli up top to declare a new package for importing. With that done we can now move on to our transactions, since our wallet will be dealing with those.

Transaction Cleanup

Most of your transaction implementation and logic will be left alone. I would like to pull out the Input and Output, along with their methods. You can place these in a new file tx.go

//tx.go

package blockchain

type TxOutput struct {
    Value int

    PubKey string
}

//Important to note that each output is Indivisible.
//You cannot "make change" with any output.
//If the Value is 10, in order to give someone 5, we need to make two five coin outputs.

type TxInput struct {
    ID []byte
    Out int
    Sig string

}

func (in *TxInput) CanUnlock(data string) bool {
    return in.Sig == data
}

func (out *TxOutput) CanBeUnlocked(data string) bool {
    return out.PubKey == data
}

Enter fullscreen mode Exit fullscreen mode

That should be everything for cleanup! Good job.
SpongeBob Dusting Hands off

Wallet Building

First off, you will want to navigate over to your Blockchain folder and add a wallet folder. Inisde wallet You should make util.go and wallet.go files.

Wallet.go

Important to note that this will be a new package

As with any go file, we need to start with our package, and import statement. I will also go ahead and include the global variable we will be using. You won't really understand it at this point. That's okay, don't worry!

//wallet.go
package wallet

import (
    "crypto/ecdsa"
    "crypto/elliptic"
    "crypto/rand"
    "crypto/sha256"
    "log"

    "golang.org/x/crypto/ripemd160"
)

const (
    checksumLength = 4
    //hexadecimal representation of 0
    version = byte(0x00)
)

Enter fullscreen mode Exit fullscreen mode

After our boilerplate, we will be creating.......You guessed it! A struct.

//wallet.go
type Wallet struct {
    //ecdsa = eliptical curve digital signiture algorithm
    PrivateKey ecdsa.PrivateKey
    PublicKey  []byte
}
Enter fullscreen mode Exit fullscreen mode

Now here you can see a data type that most of you will not recognize. ecdsa.Privatekey is a data type that has quite a bit going on behind the scenes. So much going on that I didn't want to just refer you to some other content.

Here's a 5 minute video explaining public/private keys, and the underlying logic.

Something not mentioned in the video is what the heck ECDSA stands for.

Elipitic Curve Digial Signature Algorithm.

With that overhead out of the way, let us continue onto generating the keys.

//wallet.go
func NewKeyPair() (ecdsa.PrivateKey, []byte) {
    curve := elliptic.P256()

    private, err := ecdsa.GenerateKey(curve, rand.Reader)
    if err != nil {
        log.Panic(err)
    }

    pub := append(private.PublicKey.X.Bytes(), private.PublicKey.Y.Bytes()...)

    return *private, pub
}
Enter fullscreen mode Exit fullscreen mode

Fun Fact: This algorithm can generate potentially 10^77 number of keys. For a reference of how big that number is; it is currently estimated that there are approximately 10^80 number of atoms in the observable universe

Mind Blown

We have a wallet, and some keys. This is where the magic happens. We now need to create a wallet address that uses our public key (created from our private key), and does a bunch of stuff to it, and then gives us an address where people can send us money!

Before you get into that, move over to your util.go file for a moment to add some utility functions.

Utility Functions

All you need to do here is add the encode and decode function for base58 algorithms.

//util.go
package wallet

import (
    "log"

    "github.com/mr-tron/base58"
)

func base58Encode(input []byte) []byte {
    encode := base58.Encode(input)

    return []byte(encode)
}

func base58Decode(input []byte) []byte {
    decode, err := base58.Decode(string(input[:]))
    if err != nil {
        log.Panic(err)
    }
    return decode
}
Enter fullscreen mode Exit fullscreen mode

The only thing funny in here is we do a bit of type casting to ensure that we recieve bytes, and return bytes. The base.58Decode() method expects us to give it a string.

This portion is necessary because the base58 encode will give us a wallet address that is more readable.

It ensures that we get a shorter output, and one that doesn't have the trouble of characters like: 0, O, l, I

(zero, capital O, capital i, lowercase L)

While we will typically be copy/pasting any wallet addresses, it is nice to be able to read them and type in if necessary.

To generate this wallet address, our key is going to go through a number of processes. Here is a diagram of the pipeline our publickey will run through

Blockchain key diagram

Now that may be a lot to take in, so I'll break it down step-by-step for you here:

  1. Take our public key (in bytes)
  2. Run a SHA256 hash on it, then run a RipeMD160 hash on that hash. This is called our PublicHash
  3. take our Publish Hash and append our Version (the global variable from earlier) to it. This is called the Versioned Hash
  4. Run SHA256 on our Versioned Hash twice. Then take the first 4 bytes of that output. This is called the Checksum
  5. Then we will add our Checksum to the end of our original Versioned Hash. We can call this FinalHash
  6. Lastly, we will base58Encode our FinalHash. This is our wallet address!

Now that was quite a lot. You have to make sure your keys stay secure! Doing that while also giving people a public wallet address is a bit tricky, but this manages to do that.

Let me walk you through all of those steps, and make sure we have the functionality to do all of them.

Step 1

Take our public key (in bytes)

You have your public key available already with the NewKeyPair() function.

Step 2

Run a SHA256 hash on it, then run a RipeMD160 hash on that hash. This is called our PublicHash

We will make a function to do this.

//wallet.go
func PublicKeyHash(publicKey []byte) []byte {
    hashedPublicKey := sha256.Sum256(publicKey)

    hasher := ripemd160.New()
    _, err := hasher.Write(hashedPublicKey[:])
    if err != nil {
        log.Panic(err)
    }
    publicRipeMd := hasher.Sum(nil)

    return publicRipeMd
}
Enter fullscreen mode Exit fullscreen mode

Step 3

take our Publish Hash and append our Version (the global variable from earlier) to it. This is called the Versioned Hash

This is achievable through simply calling an append() method on our hash, so this is doable already.

Step 4

Run SHA256 on our Versioned Hash twice. Then take the first 4 bytes of that output. This is called the Checksum

We can make a Checksum function:

//wallet.go
func Checksum(ripeMdHash []byte) []byte {
    firstHash := sha256.Sum256(ripeMdHash)
    secondHash := sha256.Sum256(firstHash[:])

    return secondHash[:checksumLength]
}
Enter fullscreen mode Exit fullscreen mode

Step 5

Then we will add our Checksum to the end of our original Versioned Hash. We can call this FinalHash

Same as step 3, we will be able to append this to our hash, no function necessary here.

Step 6

Lastly, we will base58Encode our FinalHash. This is our wallet address!

We just built out our utility function for this. It looks like we're ready to go.

All that's left is to put it all together!

//wallet.go
func (w *Wallet) Address() []byte {
    // Step 1/2
    pubHash := PublicKeyHash(w.PublicKey)
    //Step 3
    versionedHash := append([]byte{version}, pubHash...)
    //Step 4
    checksum := Checksum(versionedHash)
    //Step 5
    finalHash := append(versionedHash, checksum...)
    //Step 6
    address := base58Encode(finalHash)
    return address
}

Enter fullscreen mode Exit fullscreen mode

The next thing will be a way to initialize a wallet. We can call this MakeWallet()

//wallet.go
func MakeWallet() *Wallet {
    privateKey, publicKey := NewKeyPair()
    wallet := Wallet{privateKey, publicKey}
    return &wallet
}
Enter fullscreen mode Exit fullscreen mode

That's everything for the wallet.go file!

Wallet(s)

Now that we have everything in place for a singular wallet, it would be a good idea to pluralize the functionality.

Start by making a wallets.go file

The purpose of our wallets.go file is to add persistence to our wallets, and to incorporate the CLI functions that we will be calling.

We won't be using badger for the persistence of this. We want to keep the blockchain logic and the wallet logic seperate.

Here's how the begining of your file should look:

//wallets.go
package wallet

import(
    "bytes"
    "crypto/elliptic"
    "encoding/gob"
    "fmt"
    "io/ioutil"
    "log"
    "os")

const walletFile = "./tmp/wallets.data"
Enter fullscreen mode Exit fullscreen mode

Then we can add the Wallets struct.

//wallets.go
type Wallets struct {
  Wallets map[string]*Wallet
}
Enter fullscreen mode Exit fullscreen mode

Now we will need a way to save and load our wallets, using the global file we made at the top.

This will need to use the encoder and ioutil libaries, in addition to the elliptic libary. This is because we need to encode our wallets on the same elliptic curve that we used originally.

Here's the save file method:

//wallets.go
func (ws *Wallets) SaveFile() {
  var content bytes.Buffer

  gob.Register(elliptic.P256())

  encoder := gob.NewEncoder(&content)
  err := encoder.Encode(ws)
  if err != nil {
    log.Panic(err)
  }

  err = ioutil.WriteFile(walletFile, content.Bytes(), 0644)
  if err != nil {
    log.Panic(err)
  }
}
Enter fullscreen mode Exit fullscreen mode

Then the load will be pretty much the same thing, but opposite, in that we're using the decoder instead of the encoder

//wallets.go
func (ws *Wallets) LoadFile() error {
  if _, err := os.Stat(walletFile); os.IsNotExist(err) {
    return err
  }

  var wallets Wallets

  fileContent, err := ioutil.ReadFile(walletFile)

  gob.Register(elliptic.P256())
  decoder := gob.NewDecoder(bytes.NewReader(fileContent))
  err = decoder.Decode(&wallets)
  if err != nil {
    return err
  }

  ws.Wallets = wallets.Wallets

  return nil
}
Enter fullscreen mode Exit fullscreen mode

Now we need a way to create a wallet. This will create a wallet using the MakeWallet() method that we created earlier, and then adding it to the list of wallets.

//wallets.go
func CreateWallets() (*Wallets, error) {
  wallets := Wallets{}
  wallets.Wallets = make(map[string]*Wallet)
  err := wallets.LoadFile()

  return &wallets, err
}
Enter fullscreen mode Exit fullscreen mode

Now we don't want to create a new Wallets{} struct every time, so we will add a way to append to our Wallets{} struct.

//wallets.go
func (ws *Wallets) AddWallet() string {
  wallet := MakeWallet()
  address := fmt.Sprintf("%s", wallet.Address())

  ws.Wallets[address] = wallet

  return address
}
Enter fullscreen mode Exit fullscreen mode

The last things we need is a way to look up a wallet by its address, and a way to list ALL of the wallets' addresses.

Here's the former:

//wallets.go
func (ws Wallets) GetWallet(address string) Wallet {
  return *ws.Wallets[address]
}
Enter fullscreen mode Exit fullscreen mode

and the latter:

//wallets.go
func (ws *Wallets) GetAllAddresses() []string {
  var addresses []string

  for address := range ws.Wallets {
    addresses = append(addresses, address)
  }
  return addresses
}
Enter fullscreen mode Exit fullscreen mode

That's everything we need for our wallets.go file. Now we can add the functionality to our cli.

CLI Implementation

First you will want to add your wallet package into your imports:
You will need to adjust yours based off of whatever you initialized in your go.mod file. This is mine.

//cli.go
import (
    "flag"
    "fmt"
    "log"
    "os"
    "runtime"
    "strconv"

    "github.com/nheingit/learnGo/blockchain"
    "github.com/nheingit/learnGo/blockchain/wallet" // This is the new one

)

Enter fullscreen mode Exit fullscreen mode

Then we can update the printUsage method to add our new commands

//cli.go
func (cli *CommandLine) printUsage() {
    // ...
    // ...
    // Rest of commands above
    fmt.Println("createwallet - Creates a new wallet")
    fmt.Println("listaddresses - Lists the addresses in the wallet file")
}
Enter fullscreen mode Exit fullscreen mode

Then to add the two methods that our CLI will call.

First up the listAddresses() method

//cli.go
func(cli *CommandLine) listAddresses() {
    wallets, _ := wallet.CreateWallets()
    addresses := wallets.GetAllAddresses()

    for _, address := range addresses {
        fmt.Println(address)
    }

}
Enter fullscreen mode Exit fullscreen mode

Second, the createWallet() method.

//cli.go
func(cli *CommandLine) createWallet() {
    wallets, _ := wallet.CreateWallets()
    address := wallets.AddWallet()
    wallets.SaveFile()

    fmt.Printf("New address is: %s\n", address)

}

Enter fullscreen mode Exit fullscreen mode

Next roadblock is our Run() method. We will need to add the flags, the switch statements, and then the if statements.

I'm going to dump the entire method out right here, and comment where you need to look, as I think doing these one at a time would be much too long for not much reason.

//cli.go
func (cli *CommandLine) Run() {
    cli.validateArgs()

    getBalanceCmd := flag.NewFlagSet("getbalance", flag.ExitOnError)
    createBlockchainCmd := flag.NewFlagSet("createblockchain", flag.ExitOnError)
    sendCmd := flag.NewFlagSet("send", flag.ExitOnError)
    printChainCmd := flag.NewFlagSet("printchain", flag.ExitOnError)
    createWalletCmd := flag.NewFlagSet("createwallet", flag.ExitOnError) // this Cmd is new
    listAddressesCmd := flag.NewFlagSet("listaddresses", flag.ExitOnError) // this Cmd is new

    getBalanceAddress := getBalanceCmd.String("address", "", "The address to get balance for")
    createBlockchainAddress := createBlockchainCmd.String("address", "", "The address to send genesis block reward to")
    sendFrom := sendCmd.String("from", "", "Source wallet address")
    sendTo := sendCmd.String("to", "", "Destination wallet address")
    sendAmount := sendCmd.Int("amount", 0, "Amount to send")

    switch os.Args[1] {
    case "getbalance":
        err := getBalanceCmd.Parse(os.Args[2:])
        if err != nil {
            log.Panic(err)
        }
    case "createblockchain":
        err := createBlockchainCmd.Parse(os.Args[2:])
        if err != nil {
            log.Panic(err)
        }
    case "printchain":
        err := printChainCmd.Parse(os.Args[2:])
        if err != nil {
            log.Panic(err)
        }
    case "send":
        err := sendCmd.Parse(os.Args[2:])
        if err != nil {
            log.Panic(err)
        }
    case "listaddresses": // this case statement is new
        err := listAddressesCmd.Parse(os.Args[2:])
        if err != nil {
            log.Panic(err)
        }
    case "createwallet": // this case statement is new
        err := createWalletCmd.Parse(os.Args[2:])
        if err != nil {
            log.Panic(err)
        }
    default:
        cli.printUsage()
        runtime.Goexit()
    }

    if getBalanceCmd.Parsed() {
        if *getBalanceAddress == "" {
            getBalanceCmd.Usage()
            runtime.Goexit()
        }
        cli.getBalance(*getBalanceAddress)
    }

    if createBlockchainCmd.Parsed() {
        if *createBlockchainAddress == "" {
            createBlockchainCmd.Usage()
            runtime.Goexit()
        }
        cli.createBlockChain(*createBlockchainAddress)
    }

    if printChainCmd.Parsed() {
        cli.printChain()
    }

    if sendCmd.Parsed() {
        if *sendFrom == "" || *sendTo == "" || *sendAmount <= 0 {
            sendCmd.Usage()
            runtime.Goexit()
        }

        cli.send(*sendFrom, *sendTo, *sendAmount)
    }
    if listAddressesCmd.Parsed() { // this if statement is new
        cli.listAddresses()
    }
    if createWalletCmd.Parsed(){ // this if statement is new
        cli.createWallet()
    }
}
Enter fullscreen mode Exit fullscreen mode

With this change, you should now be able to boot up the command line with go run main.go
and be greeted with this:

Usage: 
getbalance -address ADDRESS - get balance for ADDRESS

createblockchain -address ADDRESS creates a blockchain and rewards the mining fee

printchain - Prints the blocks in the chain

send -from FROM -to TO -amount AMOUNT - Send amount of coins from one address to another

createwallet - Creates a new wallet

listaddresses - Lists the addresses in the wallet file
Enter fullscreen mode Exit fullscreen mode

You can use go run main.go createwallet and get an output that looks something like this:

New address is: 1HMHzfY8RsNaH2JG5cNLLrZk6UBGnFEmnV
Enter fullscreen mode Exit fullscreen mode

then finally go run main.go listaddresses after running createwallet a few times, will look something like this:

1HSJhCX9y6YhyPffG8vMXphfYZABJ3aaK9
15kR566wYiNDf8dCsJG91JfrVgCiQqTsFC
1M76k9VZdhvmxMong4yZQ9SqZBVZiQLnfS
1NTBAtCZjiWCXorNP45S19gyzVtdwYzV6T
Enter fullscreen mode Exit fullscreen mode

That's everything for this portion!

You are done building!

You may have noticed that the interaction between our wallets and our transactions aren't integrated. Currently the wallet address is just an arbitrary string. In the next module we will incorporate the two, and add in the signature functionality of the wallets.

As always I hope you learned a thing or two, and I hope to see you in the next module!

Discussion (4)

Collapse
maxbridgland profile image
Max Bridgland

Great tutorial! I've been following along and I came across an issue, it looks like you might have left out the MakeWallet() function to create wallets initially. AddWallet() does not work because MakeWallet is undefined.

Collapse
nheindev profile image
Noah Hein Author

AddWallet is in the wallets.go file, and MakeWallet is in the wallet.go file

Make sure you have "package wallet" declared as the first line in each of your files. You should be able to make any calls that share a package regardless of which file you are currently located in.

If you are using VSCode it can sometimes be a bit wonky. when having such problems I pull up the command pallet and use ">Go: Restart Language Server".

Thanks for commenting. I hope this helped!

Collapse
maxbridgland profile image
Max Bridgland

I can't seem to find anywhere in your code where you put a MakeWallet function, I see CreateWallets but no MakeWallet. I assumed that it would be something like this:

func MakeWallet() *Wallet {
private, pub := NewKeyPair()
wallet := Wallet{private, pub}
return &wallet
}

Looking forward to your coming tutorials! Thanks for all the hard work

Thread Thread
nheindev profile image
Noah Hein Author

You are entirely correct. I had only checked the repl and not the actual blog post.

My mistake, thanks for catching it!

You are also correct about how the method is implemented. I just updated the post for people that go through it in the future.

Thanks so much for the feedback max!

Forem Open with the Forem app