Building a Blockchain in Go PT: IV - Transactions
Noah Hein
Posted on April 15, 2021
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
}
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.
}
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
}
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
}
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
}
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
}
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{})
}
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[:]
}
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
}
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"
)
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
}
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
})
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
}
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
}
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 theOutputs
that each one has.If at anypoint during this we find a
txID
we know thisOutput
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
}
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 TxOutput
s
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
}
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)
}
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:
- Find Spendable Outputs
- Check if we have enough money to send the amount that we are asking
- If we do, make inputs that point to the outputs we are spending
- If there is any leftover money, make new outputs from the difference.
- Initialize a new transaction with all the new inputs and outputs we made
- 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
}
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 {}
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.
- getbalance
- createblockchain
- 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")
}
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")
}
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)
}
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!")
}
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)
}
}
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()
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!
Posted on April 15, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.