DEV Community

loading...
Cover image for Building a Blockchain in Go PT: IV - Transactions

Building a Blockchain in Go PT: IV - Transactions

nheindev profile image Noah Hein ・15 min read

You can find the code here

Hello everyone!

Last I left off I was adding the ability to print out the chain that we have stored in the database. I would like to start approaching something that people would find a bit more recognizable as a blockchain. That would be crypto! Many people that aren't in the thick of things would tell you that bitcoin and blockchain are the same thing. So to take a step into the mainstream I would like to add wallets.

Wallets have many moving parts to them, so we are looking at one piece of the puzzle specifically. That will be transactions.

Building Transactions

A wallet module is a bit complicated. I thought transactions would be a good place to start. In order to have transactions, we need to have a transaction struct. We also need a way to generate some "coins". This allows us to actually insert something into the transaction. As I talked about earlier in the series, bitcoin "miners" get fees for doing the expensive work of blockchain mining. We can start there.

The Transaction struct

So to start things out, as in our previous chapters, we will make a new file in our Blockchain folder called transaction.go

//transaction.go
type Transaction struct {
    ID      []byte
    Inputs  []TxInput
    Outputs []TxOutput
}
Enter fullscreen mode Exit fullscreen mode

Looks pretty straightforward right? A transaction should have a unique ID to differentiate itself from the many other transactions. It goes TO someone, and it comes FROM someone. That means we need Inputs and Outputs fields.

Don't get too ahead of yourself though, you'll notice those aren't primitive data types. We are gonna have to make those inputs and outputs ourselves. Let's see what that looks like.

type TxOutput struct {
    Value int
    //Value would be representative of the amount of coins in a transaction
    PubKey string
    //The Pubkey is needed to "unlock" any coins within an Output. This indicated that YOU are the one that sent it.
    //You are indentifiable by your PubKey
    //PubKey in this iteration will be very straightforward, however in an actual application this is a more complex algorithm
}
//TxInput is representative of a reference to a previous TxOutput
type TxInput struct {
    ID []byte
    //ID will find the Transaction that a specific output is inside of
    Out int
    //Out will be the index of the specific output we found within a transaction.
    //For example if a transaction has 4 outputs, we can use this "Out" field to specify which output we are looking for
    Sig string
    //This would be a script that adds data to an outputs' PubKey
    //however for this tutorial the Sig will be indentical to the PubKey.
}
Enter fullscreen mode Exit fullscreen mode

With that piece finished we can get onto initializing our first transaction.

Coinbase Transaction

Not THAT coinbase. The first transaction in a block is called a Coinbase. Our first transaction will be when a new block is "mined" as it were. So upon our Genesis call, we will want to add some coins inside of the block to be rewarded to whoever is fortunate enough to crack the code. Similar to our Blocks, we don't have any previous transactions to point back to. This means we will need to set an empty TxInput for our first transaction.

The following function should incorporate everything I just mentioned above.

//transaction.go

func CoinbaseTx(toAddress, data string) *Transaction {
    if data == "" {
        data = fmt.Sprintf("Coins to %s", toAddress)
    }
    //Since this is the "first" transaction of the block, it has no previous output to reference.
    //This means that we initialize it with no ID, and it's OutputIndex is -1
    txIn := TxInput{[]byte{}, -1, data}

    txOut := TxOutput{reward, toAddress}

    tx := Transaction{nil, []TxInput{txIn}, []TxOutput{txOut}}

    return &tx

}
Enter fullscreen mode Exit fullscreen mode

You may get a warning because you are referencing reward which is undefined currently. Go to the top of your file, and initialize a constant global variable named reward with a value of 100.

const reward = 100

Great! We have the goody egg on standby.

Utility functions.

I won't go over these too much as I believe they explain themselves quite nicely.

You are reading the code right?

SetID will encode and hash our Transactions ID field. We then use the CanUnlock and CanBeUnlocked methods to check that the Sig and the PubKey are correct. The last one is checking a transaction to see if it was a coinbase transaction.

//transaction.go

func (tx *Transaction) SetID() {
    var encoded bytes.Buffer
    var hash [32]byte

    encoder := gob.NewEncoder(&encoded)
    err := encoder.Encode(tx)
    Handle(err)

    hash = sha256.Sum256(encoded.Bytes())
    tx.ID = hash[:]

}

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

func (tx *Transaction) IsCoinbase() bool {
    //This checks a transaction and will only return true if it is a newly minted "coin"
    return len(tx.Inputs) == 1 && len(tx.Inputs[0].ID) == 0 && tx.Inputs[0].Out == -1
}
Enter fullscreen mode Exit fullscreen mode

I think that is everything we need for transaction for now.

Let's go add it to our Blocks!

Implementing Transactions

We are done with our transaction.go file for now. Let us move over to our block.go file.
Since we now have transactions, this will replace our Data field in our Block struct.

//block.go
type Block struct {
    Hash     []byte
    Transactions []*Transaction
    PrevHash []byte
    Nonce    int
}
Enter fullscreen mode Exit fullscreen mode

With that done you will notice your entire project becomes inundated with errors.

Do not worry, we will be working through our refactoring, and hopefully when we're done everything will compile!

Updating Methods

In our CreateBlock method, we will want to change the Data string in our parameters. Instead we will put in txs []*Transaction.

//block.go
func CreateBlock(txs []*Transaction, prevHash []byte) *Block {
    block := &Block{[]byte{}, txs, prevHash, 0}
    pow := NewProofOfWork(block)
    nonce, hash := pow.Run()

    block.Hash = hash[:]
    block.Nonce = nonce

    return block
}
Enter fullscreen mode Exit fullscreen mode

With that out of the way, we meander our way to the Genesis function. We need to add an empty Transaction to our Genesis block.

//block.go
func Genesis(coinbase *Transaction) *Block {
    return CreateBlock([]*Transaction{coinbase}, []byte{})
}
Enter fullscreen mode Exit fullscreen mode

Data Hashing

The last thing we have to do is build a method that takes all of the transactions existing in a block and hashes them.

//block.go
func (b *Block) HashTransactions() []byte {
    var txHashes [][]byte
    var txHash [32]byte

    for _, tx := range b.Transactions {
        txHashes = append(txHashes, tx.ID)
    }
    txHash = sha256.Sum256(bytes.Join(txHashes, []byte))

    return txHash[:]
}
Enter fullscreen mode Exit fullscreen mode

That should be all of the changes we need to make in our block.go file for the time being.

Time to hop over to proof.go!

Proof of Work Changes

This one should be super quick, we just need to change the InitData method. Since it calls to Block.Data which no longer exists. However, this is where our previous function comes in handy. Since this method is expecting everything to be in bytes, we can call our new function. Our refactored InitData method should look like this.

//proof.go
func (pow *ProofOfWork) InitData(nonce int) []byte {
    data := bytes.Join(
        [][]byte{
            pow.Block.PrevHash,
            pow.Block.HashTransactions(), // THIS IS THE LINE WE CHANGED
            toHex(int64(nonce)),
            toHex(int64(Difference)),
        },
        []byte{},
    )
    return data
}
Enter fullscreen mode Exit fullscreen mode

Great job! We're all done here.

We can now move onto our last file in the blockchain folder, which is blockchain.go.

Blockchain Changes

Alrighty, back to the fun stuff we were doing with the database before!

Moving forward, we need to add a few more global constants at the top of our blockchain.go file. We want to add a dbFile, and genesisData. so right under our imports we want to initialize our constants like so:

//blockchain.go
const (
    dbPath = "./tmp/blocks"

    // This can be used to verify that the blockchain exists
    dbFile = "./tmp/blocks/MANIFEST" 

    // This is arbitrary data for our genesis block
    genesisData = "First Transaction from Genesis" 
)
Enter fullscreen mode Exit fullscreen mode

You may have noticed when you finished part III, that whenever you ran the code, in the /tmp/blocks folder that it creates a Manifest file. This is what we are setting the dbFIle to.

It allows us to check if a blockchain already exists or not.

The genesisData is simply some data to provide our genesis block so it won't be empty.

We will need to Breakup our InitBlockChain function so we can more easily reference our transactions using the cli later. Currently there a large if/else block, that essentially checks if we've already got a database, and responds conditionally.

We will break this up into an InitBlockChain and ContinueBlockChain function. We can break this function cleanly at the if/else junction. The logic that is inside each portion will be placed into its own function. The only caveat is we will need to duplicate the extra code that is required to make database transactions.

Before I show you what each function looks like, I would like to add a quick utility that we can use to check if the blockchain exists already.

//blockchain.go

//DBexists checks to see if we've initialized a database
func DBexists(db) bool {
    if _, err := os.Stat(db); os.IsNotExist(err) {
        return false
    }
    return true
}
Enter fullscreen mode Exit fullscreen mode

With this we can pass the dbFile constant to the function, and it will give us a Boolean value in return.

Alrighty, back to the splitting of our initBlockChain function. As stated before, we will split the if/else logic into its own function. One will handle if the database exists, and one will handle if it doesn't exist.

The main difference being, if the database/blockchain does not exist, we need to add the CoinbaseTx.

We want to get some transaction action goin' on!

Enough explanation, SHOW ME THE CODE!

//blockchain.go
func InitBlockChain(address string) *BlockChain {
    var lastHash []byte

    if DBexists(dbFile) {
        fmt.Println("blockchain already exists")
        runtime.Goexit()
    }

    opts := badger.DefaultOptions(dbPath)
    db, err := badger.Open(opts)
    Handle(err)

    err = db.Update(func(txn *badger.Txn) error {

        cbtx := CoinbaseTx(address, genesisData)
        genesis := Genesis(cbtx)
        fmt.Println("Genesis Created")
        err = txn.Set(genesis.Hash, genesis.Serialize())
        Handle(err)
        err = txn.Set([]byte("lh"), genesis.Hash)

        lastHash = genesis.Hash

        return err

    })
Enter fullscreen mode Exit fullscreen mode

So that's the first one.

Here's the ContinueBlockChain function.

//blockchain.go

//I Know we don't reference address anywhere in here. Keep it anyway.
func ContinueBlockChain(address string) *BlockChain {
    if DBexists(dbFile) == false {
        fmt.Println("No blockchain found, please create one first")
        runtime.Goexit()
    }

    var lastHash []byte

    opts := badger.DefaultOptions(dbPath)
    db, err := badger.Open(opts)
    Handle(err)

    err = db.Update(func(txn *badger.Txn) error {
        item, err := txn.Get([]byte("lh"))
        Handle(err)
        err = item.Value(func (val []byte) error {
            lastHash = val
            return nil
        })
        handle(err)
        return err
    })
    Handle(err)

    chain := BlockChain{lastHash, db}
    return &chain
}
Enter fullscreen mode Exit fullscreen mode

The main difference, is now the InitBlockChain has a parameter that we can pass an argument to. This argument will be the address of the person that mined the block.
Now that we have done that, we will be able to get to the good stuff.

Adding Transactions

So with those two functions split up we will have a way to implement the CLI to check the balance of an account as well as being able to send coins from one account to another.

You may have noticed that while an TxOutput is representative of some action between two addresses, TxInput is merely a reference to a previous TxOutput.

This may not be immeaditely apparent, but because of this we are able to figure out the balance of an account. This is because we can check for all of the outputs that an account has linked to it, and then check all of the inputs. Whichever outputs do not have an input pointing to it will be spendable.

Let me say that again. Transactions that have outputs, but no inputs pointing to them are spendable. We will call these Unspent Transactions.

Let's build that out.

Finding Unspent Transactions

Remember when I said I wouldn't be teaching you go?
This may be one of those moments where you have to pause and read some other material for a bit if you aren't quite familiar with everything yet.

However, I will send you along with some relevant resources.

I use this site all the time. Many people think you go through it once and that's it. That couldn't be farther from the truth. I encourage you do run through this one multiple times and refer back to it often.

Here it is https://gobyexample.com/

This is a good place to start for maps.

Here is a resource for labesl as well.

//blockchain.go
func (chain *BlockChain) FindUnspentTransactions(address string) []Transaction {
    var unspentTxs []Transaction

    spentTXNs := make(map[string][]int)

    iter := chain.Iterator()

    for {
        block := iter.Next()

        for _, tx := range block.Transactions {
            txID := hex.EncodeToString(tx.ID)

        Outputs:
            for outIdx, out := range tx.Outputs {
                if spentTXNs[txID] != nil {
                    for _, spentOut := range spentTXNs[txID] {
                        if spentOut == outIdx {
                            continue Outputs
                        }
                    }
                }
                if out.CanBeUnlocked(address){
                    unspentTxs = append(unspentTxs, *tx)
                }
            }
            if tx.IsCoinbase() == false {
                for _, in := range tx.Inputs {
                    if in.CanUnlock(address) {
                        inTxID := hex.EncodeToString(in.ID)
                        spentTXNs[inTxID] = append(spentTXNs[inTxID], in.Out)
                    }
            }
        }
        if len(block.PrevHash) == 0 {
            break
        }
    }
    return unspentTxs

}

Enter fullscreen mode Exit fullscreen mode

This is a lot to unpack!

We can break this function down into a few parts.

We need to loop through our entire chain, and we can do that like we did before using the chain.Iterator().Next() method.

  • We want to loop through each blocks Transactions.

  • As we loop through the Transactions, we also want to take a look at all of the Outputs that each one has.

  • If at anypoint during this we find a txID we know this Output has been spent.

  • At this point we hit the continue Outputs line and continue to our next conditional.

  • We now loop through the outputs that are available for spending.

    We know they are spendable because they have no inputs associated with them

Now we can check if these Outputs can be unlocked.

Using our CanBeUnlocked method, we just need to pass in the address.

If that returns true we can add it to our unspentTxs variable that we declared at the top of the method.

With all of that is done; we can move on to the next portion of the method.

Keep in mind that we are still looping through each blocks transactions at this point.

Now we check if the Transaction is a coinbase transaction.

If it isn't, we can look through all of the Inputs.

If an input has a matching address, we can add the Input's ID to the spentTXNs map that we were checking our outputs against above.

At the end, return all of the unspent transactions that we gathered for the particular address.

Congradulations! We're all done here.

Finding Unspent Transaction Outputs

Luckily that above function is the most complicated thing we will be dealing with. Now that we have a way to find all of the unspent transactions, we can narrow it down a bit and look for the unspent outputs.

//blockchain.go
func (chain *BlockChain) FindUTXO(address string) []TxOutput {
    var UTXOs []TxOutput
    unspentTransactions := chain.FindUnspentTransactions(address)
    for _, tx := range unspentTransactions {
        for _, out := range tx.Outputs {
            if CanBeUnlocked(address) {
                UTXOs = append(UTXOs, out
            }
        }
    }

    return UTXOs
}
Enter fullscreen mode Exit fullscreen mode

Loop through all of the unspent transactions and see if we can unlock the outputs.

Add them all to an array and return that array of TxOutputs

Finding Unspent Transaction Outputs That Are Spendable

Now to the fun bit! we have narrowed down our search into the thing we actually care about. Spending money!

Currently the only way to make a transaction is to be a coinbase transaction which defeats the purpose of the whole "blockchain" thing we have goin' on. What point is there in having money if you can only make more by mining it?

We should be able to spread the wealth. In order to do that we need to find all of the spendable outputs. Just because there is an output that hasn't been spent, doesn't mean that ANYONE can spend it. Only the person with the key has those honors.

The following will be a function that takes an account's address, and the amount that we would like to spend. It returns a tuple that contains the amount we can spend, and a map of the aggregated outputs that can make that happen.

//blockchain.go
func (chain *BlockChain) FindSpendableOutputs(address string, amount int) (int, map[string][]int) {
    unspentOuts := make(map[string][]int)
    unspentTxs := chain.FindUnspentTransactions(address)
    accumulated := 0

Work:
    for _, tx := range unspentTxs {
        txID := hex.EncodeToString(tx.ID)
        for outIdx, out := range tx.Outputs {
            if out.CanBeUnlocked(address) && accumulated < amount {
                accumulated += out.Value
                unspentOuts[txID] = append(unspentOuts[txID], outIdx)

                if accumulated >= amount {
                    break Work
                }
            }
        }
    }
    return accumulated, unspentOuts
}
Enter fullscreen mode Exit fullscreen mode

Alrighty, with that done we can make the last change in our blockchain.go file.

Our AddBlock method is still using data as a paramter, and passes it down to createBlock as an argument. We need to change both of those to be transactions. The new function will be shown below. Try not to just copy/paste everything. Look for what changed and why! Understanding the flow of data in your program is critical.

//blockchain.go
//DATA STRING ---> TRANSACTIONS []*TRANSACTION
func (chain *BlockChain) AddBlock(transactions []*Transaction) {
    var lastHash []byte

    err := chain.Database.View(func(txn *badger.Txn) error {
        item, err := txn.Get([]byte("lh"))
        Handle(err)
        err = item.Value(func(val []byte) error {
            lastHash = val
            return nil
        })
        Handle(err)
        return err
    })
    Handle(err)

    newBlock := CreateBlock(transactions, lastHash) //THIS LINE CHANGED
    err = chain.Database.Update(func(transaction *badger.Txn) error {
        err := transaction.Set(newBlock.Hash, newBlock.Serialize())
        Handle(err)
        err = transaction.Set([]byte("lh"), newBlock.Hash)

        chain.LastHash = newBlock.Hash
        return err
    })
    Handle(err)
}
Enter fullscreen mode Exit fullscreen mode

We can now save our blockchain file with no more errors and move onto the rest of our program.

Spending Coins!

With all of our data running smoothly through our blockchain, we can go back to our transaction.go file and add our last function before moving onto our CLI.

We need this function to do a few things:

  1. Find Spendable Outputs
  2. Check if we have enough money to send the amount that we are asking
  3. If we do, make inputs that point to the outputs we are spending
  4. If there is any leftover money, make new outputs from the difference.
  5. Initialize a new transaction with all the new inputs and outputs we made
  6. Set a new ID, and return it.

Let's get typing!

//transaction.go
func NewTransaction(from, to string, amount int, chain *BlockChain) *Transaction {
    var inputs []TxInput
    var outputs []TxOutput

    //STEP 1
    acc, validOutputs := chain.FindSpendableOutputs(from, amount)

    //STEP 2
    if acc < amount {
        log.Panic("Error: Not enough funds!")
    }

    //STEP 3
    for txid, outs := range validOutputs {
        txID, err := hex.DecodeString(txid)
        Handle(err)

        for _, out := range outs {
            input := TxInput{txID, out, from}
            inputs = append(inputs, input)
        }
    }

    outputs = append(outputs, TxOutput{amount, to})

    //STEP 4
    if acc > amount {
        outputs = append(outputs, TxOutput{acc - amount, from})
    }

    //STEP 5
    tx := Transaction{nil, inputs, outputs}
    //STEP 6
    tx.SetID()

    return &tx
}
Enter fullscreen mode Exit fullscreen mode

Awesome! That's all we need to complete the transaction.go file.

Updating The CLI

Okay, so we're all done with the heavy lifting, now we just have some updating of functions.

Moving to main.go

Let's start by simplifying our lives a bit.

We're going to remove the blockchain field within our CommandLine struct.

It should now look like this:

type CommandLine struct {}
Enter fullscreen mode Exit fullscreen mode

Now to update our printUsage method.

We will no longer support the addBlock method. We can delete that whole function.

Afterwards we will want to add 3 more functions.

  1. getbalance
  2. createblockchain
  3. send

That means we need to tell our users how this cli operates. The new printUsage should look like:

 //main.go
 func (cli \*CommandLine) printUsage() {

 fmt.Println("Usage: ")

 fmt.Println("getbalance -address ADDRESS - get balance for ADDRESS")

 fmt.Println("createblockchain -address ADDRESS creates a blockchain and rewards the mining fee")

 fmt.Println("printchain - Prints the blocks in the chain")

 fmt.Println("send -from FROM -to TO -amount AMOUNT - Send amount of coins from one address to another")

}
Enter fullscreen mode Exit fullscreen mode

After this go to printChain and remove the line that was displaying the data field of the block, as data no longer exists.

Now to create the new methods that we outlined in our printUsage method.

first createBlockChain:

 //main.go

 func (cli *CommandLine) createBlockChain(address string) {
    newChain := blockchain.InitBlockChain(address)
    newChain.Database.Close()
    fmt.Println("Finished creating chain")
}

Enter fullscreen mode Exit fullscreen mode

WIth that out of the way we can move to getBalance

//main.go
func (cli *CommandLine) getBalance(address string) {
    chain := blockchain.ContinueBlockChain(address)
    defer chain.Database.Close()

    balance := 0
    UTXOs := chain.FindUTXO(address)

    for _, out := range UTXOs {
        balance += out.Value
    }

    fmt.Printf("Balance of %s: %d\n", address, balance)
}
Enter fullscreen mode Exit fullscreen mode

Fantastic, moving through at a blazing pace! Time to send money!

//main.go

func (cli *CommandLine) send(from, to string, amount int) {
    chain := blockchain.ContinueBlockChain(from)
    defer chain.Database.Close()

    tx := blockchain.NewTransaction(from, to, amount, chain)

    chain.AddBlock([]*blockchain.Transaction{tx})
    fmt.Println("Success!")
}
Enter fullscreen mode Exit fullscreen mode

Now we have to update all of our CLI flags and strings inside the run method. Feel free to copy paste this bit honestly. It was very painful to write out.

//main.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)

    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)
        }
    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)
    }
}
Enter fullscreen mode Exit fullscreen mode

The very last thing to do is remove the database portion of the main command!

The new one should look like this:

func main() {
    defer os.Exit(0)
    cli := CommandLine{}
    cli.run()
Enter fullscreen mode Exit fullscreen mode

Now we're all done!

I won't hold your hand through how to use this as I'm sure with the wonderful usage flags we made you'll be able to figure it out. The only thing to be aware of is if you have an existing chain in your tmp/blocks folder you will need to delete that before using this new fancy stuff as we can only have one chain at a time currently.

Please leave any questions you may have in the comments and I'll try my best to help you out!

Until next time!

Discussion (0)

pic
Editor guide