Part Ⅰ — Implementing Simple Blockchain using Go and Test-Driven Development (TDD)

This article introduces how to implement a basic blockchain using the Go language, specifically for blockchain beginners. The content is explained following the flow of Test-Driven Development (TDD).
I implemented the basic functionalities from scratch to deepen my own understanding of blockchain. My understanding is still a work in progress, but I hope this can be a reference for others in similar situations.
The code can be found at the following URL. Feel free to check it out if you want to review just the code: https://github.com/ShuntaroOkuma/blockchain-tdd-golang-for-learning
Basic Concepts of Blockchain
A blockchain is a data structure that holds transaction records and functions as a distributed database.
The blockchain consists of a list structure with multiple blocks connected together. In the diagram below, each green box represents a block, and they are connected through hash values, hence the name “blockchain.”

Each block contains the previous block’s hash value, a nonce value, a timestamp, and transactions.
For more details, please refer to the following article:
The Process of Adding a Block
The process of adding a block is illustrated in the diagram below.

I will explain following the numbers in the diagram. In the descriptions, “user” refers to a user who wants to write data as a transaction to the blockchain, and “miner” refers to a user who calculates the block’s hash value through Proof of Work (PoW) and adds transactions to the blockchain.
- Generation of the blockchain and the initial block:
First, generate the blockchain itself and the initial block to include in it.
2. Adding transactions to the transaction pool:
Add transactions executed by users to the transaction pool (temporary storage for unconfirmed transactions).
3. Adding transactions to the block
Miners select transactions from the transaction pool and add them to a block.
4. Hash value calculation using PoW
Miners perform Proof of Work (PoW) to find the appropriate nonce value and calculate the block’s hash value. More details can be found in the same article mentioned above.
5. Adding a new block to the blockchain
Add a new block to the blockchain. In the process, save the previous block’s hash value as part of the block information.
6. Block verification
Verify that the added block is valid. This is executed on a node other than the miner’s node. After verification, the transactions in the block become “confirmed,” and the miner is rewarded.
From here on, we will proceed with the implementation following this flow. Implementing while referring to the diagram can help deepen your understanding.
Preparation
Before starting the implementation, make sure your Go environment is set up. For the installation and configuration of Go, please refer to the official documentation.
First, run the following commands to initialize the environment.
mkdir blockchain-tdd-golang-for-learning
cd blockchain-tdd-golang-for-learning
go mod init blockchain-tdd-golang-for-learning
mkdir blockchain
touch blockchain/blockchain.go
touch blockchain/blockchain_test.go
- Write test code in
blockchain_test.go
- Write the actual blockchain code in
blockchain.go
Implementation
1. Generation of the blockchain and the initial block
From here, we will implement following the flow of TDD. First, we will implement the process of generating the blockchain and the initial block.
Red — Test Code
First, implement the test. The information needed for the first block is the difficulty, the hash value, and the current time. The difficulty can be set freely for each blockchain. For the hash value, set it to 0 for the first block, but represent it as a byte type. Store the current time atcurrentTime
once.
There is one point here that should be noted. If you use time.Now()
directly when defining got
and want
, the current time may be slightly off, causing the test to fail. To avoid this, first, get the current time and then pass it to got
and want
.
blockchain_test.go
is here:
package blockchain
import (
"reflect"
"testing"
"time"
)
func TestCreateBlockchain(t *testing.T) {
difficulty := 3
currentTime := time.Now()
want := Blockchain{
[]Block{
Block{
Hash: []byte("0"),
Timestamp: currentTime,
}
},
difficulty,
}
got := CreateBlockchain(difficulty, currentTime)
if !reflect.DeepEqual(want, got) {
t.Errorf("wanted %v got %v", want, got)
}
}
Green — Function Implementation
Now let’s implement the main part. First, run the test. You can use the features of VSCODE or execute the test by running go test
. When you run the test, the following error should appear:
undefined: Blockchain
undefined: Block
undefined: CreateBlockchain
Proceed with the implementation to fix the error. Let’s add code to blockchain.go
.
type Transaction struct {
Sender string
Recipient string
Amount int
}
type Block struct {
Timestamp time.Time
Transactions []Transaction
PrevBlockHash []byte
Hash []byte
Nonce int
}
type Blockchain struct {
Blocks []Block
difficulty int
}
func CreateBlockchain(difficulty int, currentTime time.Time) Blockchain {
}
Define multiple structs and set the required information as variables in each.
When you run the test now, the previous error should no longer appear, but you will get a missing return
error. As the error description suggests, implement the return part in the CreateBlockchain
function. Since we want to define a function to create a Blockchain, we hold data in the Blockchain struct and return it.
func CreateBlockchain(difficulty int, currentTime time.Time) Blockchain {
+ return Blockchain{
+ []Block{Block{
+ Hash: []byte("0"),
+ Timestamp: currentTime,
+ }},
+ difficulty,
+ }
}
We have implemented a function that takes difficulty
and currentTime
as arguments and returns a Blockchain struct. If you run the test now, it will succeed without error.
Refactor
Since genesisBlock
can be externalized in both test code and main code, we will perform refactoring as a matter of course.
blockchain_test.go
:
func TestCreateBlockchain(t *testing.T) {
difficulty := 3
currentTime := time.Now()
- want := Blockchain{
- []Block{
- Block{
- Hash: []byte("0"),
- Timestamp: currentTime,
- }
- },
- difficulty,
- }
+ genesisBlock := Block{
+ Hash: []byte("0"),
+ Timestamp: currentTime,
+ }
+
+ want := Blockchain{
+ []Block{genesisBlock},
+ difficulty,
+ }
got := CreateBlockchain(difficulty, currentTime)
if !reflect.DeepEqual(want, got) {
t.Errorf("wanted %v got %v", want, got)
}
}
blockchain.go
:
func CreateBlockchain(difficulty int, currentTime time.Time) Blockchain {
+ genesisBlock := Block{
+ Hash: []byte("0"),
+ Timestamp: currentTime,
+ }
return Blockchain{
- []Block{Block{
- Hash: []byte("0"),
- Timestamp: currentTime,
- }},
+ []Block{genesisBlock},
difficulty,
}
}
Now the implementation for generating the blockchain is complete.
After refactoring, make sure to run the tests to confirm that everything works correctly.
2. Adding transactions to the transaction pool
Next is the implementation of the function to add transactions executed by users to the transaction pool (where unconfirmed transactions are temporarily stored).
Red — Test Code
The contents of the test should flow as follows:
- Generate a blockchain and a transaction
- Add the transaction to the blockchain
- Assert the contents of the transaction pool
blockchain_test.go
is here:
func TestAddTransaction(t *testing.T) {
difficulty := 3
currentTime := time.Now()
bc := CreateBlockchain(difficulty, currentTime)
transaction := Transaction{
Sender: "Alice",
Recipient: "Bob",
Amount: 10,
}
bc.AddTransaction(transaction)
got := bc.TransactionPool
want := append([]Transaction{}, transaction)
if !reflect.DeepEqual(want, got) {
t.Errorf("wanted %v got %v", want, got)
}
}
In an actual blockchain implementation, various validations are performed before adding a transaction, but I omit them here.
When you run the test, the following error occurs.
bc.AddTransaction undefined (type Blockchain has no field or method AddTransaction)
bc.TransactionPool undefined (type Blockchain has no field or method TransactionPool)
Green — Function Implementation
Now, as the error message suggests, define AddTransaction
and TransactionPool
in blockchain.go
.
type Blockchain struct {
Blocks []Block
+ TransactionPool []Transaction
difficulty int
}
func CreateBlockchain(difficulty int, currentTime time.Time) Blockchain {
...
}
+func (bc *Blockchain) AddTransaction(transaction Transaction) {
+
+}
When you run the test now, an error occurs in the CreateBlockchain
function because we added variables to the Blockchain struct.
cannot use difficulty (variable of type int) as type []Transaction in struct literal
too few values in struct literal
cannot use difficulty (variable of type int) as type []Transaction in struct literal
too few values in struct literal
Therefore, we will put the AddTransaction
function aside for a moment and fix the CreateBlockchain
function.
Add TransactionPool
to the main and test code.
blockchain.go
:
func CreateBlockchain(difficulty int, currentTime time.Time) Blockchain {
genesisBlock := Block{
Hash: []byte("0"),
Timestamp: currentTime,
}
return Blockchain{
[]Block{genesisBlock},
+ []Transaction{},
difficulty,
}
}
blockchain_test.go
:
func TestCreateBlockchain(t *testing.T) {
difficulty := 3
currentTime := time.Now() // gotにもwantにも同じ時刻を適用したいため(time.Nowを直接指定すると、gotとwantでタイミングがずれてテストに失敗してしまう)
genesisBlock := Block{
Hash: []byte("0"),
Timestamp: currentTime,
}
want := Blockchain{
[]Block{genesisBlock},
+ []Transaction{},
difficulty,
}
got := CreateBlockchain(difficulty, currentTime)
if !reflect.DeepEqual(want, got) {
t.Errorf("wanted %v got %v", want, got)
}
}
Run the TestCreateBlockchain
test and make sure no errors occur.
Now, let’s return to the implementation of the AddTransaction
function. Run the TestAddTransaction
test again.
Errors still occur, but as you can see below, we can finally see that the assert part has been executed.
wanted [{Alice Bob 10}] got []
Now let’s implement the contents of the AddTransaction
function in blockchain.go
.
func (bc *Blockchain) AddTransaction(transaction Transaction) {
+ bc.TransactionPool = []Transaction{transaction}
}
Simply add the passed transaction to the transaction pool as a slice.
When you run the test now, it succeeds without any errors.
Red — Test Code
Now that we can successfully add transactions, the above test only verifies adding the first transaction to an empty transaction pool. We haven’t tested adding another transaction to a transaction pool that already contains a transaction.
Let’s implement that test in blockchain_test.go
.
func TestAddTransaction(t *testing.T) {
+ t.Run("Add transactions to an empty transaction pool", func(t *testing.T) {
difficulty := 3
currentTime := time.Now()
bc := CreateBlockchain(difficulty, currentTime)
transaction := Transaction{
Sender: "Alice",
Recipient: "Bob",
Amount: 10,
}
bc.AddTransaction(transaction)
got := bc.TransactionPool
want := append([]Transaction{}, transaction)
if !reflect.DeepEqual(want, got) {
t.Errorf("wanted %v got %v", want, got)
}
+ })
+
+ t.Run("Adding a transaction to an already existing transaction pool", func(t *testing.T) {
+ difficulty := 3
+ currentTime := time.Now()
+ bc := CreateBlockchain(difficulty, currentTime)
+
+ transaction1 := Transaction{
+ Sender: "Alice",
+ Recipient: "Bob",
+ Amount: 10,
+ }
+
+ bc.AddTransaction(transaction1)
+
+ transaction2 := Transaction{
+ Sender: "Bob",
+ Recipient: "Alice",
+ Amount: 20,
+ }
+
+ bc.AddTransaction(transaction2)
+
+ got := bc.TransactionPool
+ want := append([]Transaction{}, transaction1, transaction2)
+
+ if !reflect.DeepEqual(want, got) {
+ t.Errorf("wanted %v got %v", want, got)
+ }
+
+ })
}
When we run this test, the following error occurs:
wanted [{Alice Bob 10} {Bob Alice 20}] got [{Bob Alice 20}]
We would like two transactions to be stored in the slice, but in reality, only the second transaction is stored.
Let’s modify the function implementation.
We rewrite `blockchain.go` as follows:
Green — Implement Function
func (bc *Blockchain) AddTransaction(transaction Transaction) {
- bc.TransactionPool = []Transaction{transaction}
+ bc.TransactionPool = append(bc.TransactionPool, transaction)
}
We changed the implementation to append elements to the slice.
It was a bit roundabout, but now the test works without any problems.
Refactor
At this point, the test function is redundant, so we extract the common parts.
func TestAddTransaction(t *testing.T) {
+ transaction := Transaction{
+ Sender: "Alice",
+ Recipient: "Bob",
+ Amount: 10,
+ }
t.Run("Adding a transaction to an already existing transaction pool", func(t *testing.T) {
- difficulty := 3
- currentTime := time.Now()
- bc := CreateBlockchain(difficulty, currentTime)
-
- transaction := Transaction{
- Sender: "Alice",
- Recipient: "Bob",
- Amount: 10,
- }
-
- bc.AddTransaction(transaction)
+ bc := initTransactionPool(transaction)
got := bc.TransactionPool
want := append([]Transaction{}, transaction)
if !reflect.DeepEqual(want, got) {
t.Errorf("wanted %v got %v", want, got)
}
})
t.Run("Adding a transaction to an already existing transaction pool", func(t *testing.T) {
- difficulty := 3
- currentTime := time.Now()
- bc := CreateBlockchain(difficulty, currentTime)
-
- transaction1 := Transaction{
- Sender: "Alice",
- Recipient: "Bob",
- Amount: 10,
- }
-
- bc.AddTransaction(transaction1)
+ bc := initTransactionPool(transaction)
transaction2 := Transaction{
Sender: "Bob",
Recipient: "Alice",
Amount: 20,
}
bc.AddTransaction(transaction2)
got := bc.TransactionPool
- want := append([]Transaction{}, transaction1, transaction2)
+ want := append([]Transaction{}, transaction, transaction2)
if !reflect.DeepEqual(want, got) {
t.Errorf("wanted %v got %v", want, got)
}
})
}
+ func initTransactionPool(transaction Transaction) Blockchain {
+ difficulty := 3
+ currentTime := time.Now()
+ bc := CreateBlockchain(difficulty, currentTime)
+
+ bc.AddTransaction(transaction)
+
+ return bc
+}
3. Adding Transactions to a Block
Next, we implement a function to add transactions from the transaction pool to a block. Normally, this task is performed by miners.
Miners receive new coins (coinbase transactions) and transaction fees as rewards. The more transactions included in a block, the higher the transaction fee rewards. Therefore, miners usually prioritize transactions with higher fees and add transactions until the block size limit is reached.
In other words, the function implementation should allow miners to choose their preferred transactions and add them to a block.
Red — Test Code
By following the steps below, we can confirm that the requirements are met:
- Generate a blockchain and two transactions
- Add one transaction to a block
- Assert the transaction pool and the number and contents of the block’s transactions
blockchain_test.go
is here:
func TestAddBlock(t *testing.T) {
transaction := Transaction{
Sender: "Alice",
Recipient: "Bob",
Amount: 10,
}
bc := initTransactionPool(transaction)
transaction2 := Transaction{
Sender: "Bob",
Recipient: "Alice",
Amount: 20,
}
bc.AddTransaction(transaction2)
txToAdd := bc.TransactionPool[0]
bc.TransactionPool = bc.TransactionPool[1:]
bc.AddTransactionToBlock(txToAdd)
latestBlock := bc.Blocks[len(bc.Blocks)-1]
// assert1: the number of transaction in latest block
if len(latestBlock.Transactions) != 1 {
t.Errorf("Transaction in Block count must be 1, got %d", len(latestBlock.Transactions))
}
// assert2: the number of transations in the pool
if len(bc.TransactionPool) != 1 {
t.Errorf("TransactionPool count must be 1, got %d", len(bc.TransactionPool))
}
// assert3: transaction contents
if latestBlock.Transactions[0] != transaction {
t.Errorf("Transaction in block does not match the expected transaction")
}
}
After creating the test function, run the test.
At this stage, the AddTransactionToBlock
function hasn’t been implemented yet, so the following error occurs.
bc.AddTransactionToBlock undefined (type Blockchain has no field or method AddTransactionToBlock)
Green — Implement Function
Now let’s add the AddTransactionToBlock
function to blockchain.go
.
func (bc *Blockchain) AddTransactionToBlock(transaction Transaction) {
}
Running the test now, the error content changes, and we can confirm that AddTransactionToBlock
is recognized.
Transaction in Block count must be 1, got 0
Let’s proceed with the implementation in blockchain.go
.
func (bc *Blockchain) AddTransactionToBlock(transaction Transaction) {
latestBlock := &Block{}
if len(bc.Blocks) > 1 {
latestBlock = &bc.Blocks[len(bc.Blocks)-1]
} else if len(bc.Blocks) == 1 {
latestBlock = &bc.Blocks[0]
} else {
panic("Blockchain must contain at least one Block.")
}
latestBlock.Transactions = append(latestBlock.Transactions, transaction)
}
We retrieve the latest block and store the transaction in it.
It is important to note that if you do not get a reference to the block as a pointer with latestBlock
, the transaction will not be added to the block.
If you do not use a pointer and implement it, the block information will be copied to latestBlock
, and you will only store the transaction there. In that case, nothing changes inside the instantiated blockchain.
Now run the test. It should complete without any problems.
—
The rest of this article is divided into the following articles: TBD