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

Shuntaro
8 min readApr 28, 2023

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

This is partⅡ, partⅠ is here.

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

In partⅠ, we implemented generating the blockchain and the initial block, adding transactions to the transaction pool, and adding transactions to the block.

Let’s start with calculating the hash value.

4. PoW-based hash calculation

Once you’ve added transactions to the block, it’s time to perform PoW. Miners, when adding transactions to the block up to the block size limit, execute what is known as mining. This involves performing PoW (Proof of Work) to find an appropriate Nonce value and calculating the hash value of the block.

Red — Test Code

The contents of the test should flow as follows:

  • Create a blockchain and transactions
  • Add transactions to the block
  • Perform mining (hash value calculation) on the block
  • Confirm that the hash value is stored
  • Confirm that the number of zeros specified by the difficulty is at the beginning of the hash value

blockchain_test.go is here:

func TestMineBlock(t *testing.T) {
transaction := Transaction{
Sender: "Alice",
Recipient: "Bob",
Amount: 10,
}
bc := initTransactionPool(transaction)

txToAdd := bc.TransactionPool[0]
bc.AddTransactionToBlock(txToAdd)

block := bc.Blocks[0]
block.MineBlock(bc.difficulty)

if block.Hash == nil {
t.Errorf("No hash stored in block.")
}
hashStr := string(block.Hash)

if hashStr[:bc.difficulty] != strings.Repeat("0", bc.difficulty) {
t.Errorf("Expected block hash to start with %s, but got %s", strings.Repeat("0", bc.difficulty), hashStr)
}
}

After adding a test function, let’s run the test.

At this stage, the MineBlock function is not implemented at all, so the following error will occur:

block.MineBlock undefined (type Block has no field or method MineBlock)

Green — Implement Function

To find the hash value of the block based on the specified difficulty, change the Nonce (arbitrary number) one by one and repeat the calculation.

Here is the implementation of the MineBlock function in blockchain.go.

func (block *Block) MineBlock(difficulty int) {
target := strings.Repeat("0", difficulty)

for {
hashStr := block.calculateHash()
if hashStr[:difficulty] == target {
block.Hash = []byte(hashStr)
break
}
block.Nonce++
}
}

When you calculate the hash value, if it is a hash value that meets the difficulty conditions, the loop will end with a break. However, the hash value calculation itself is done in a function called calculateHash, which will be implemented later.

Running the test now will result in an error that calculateHash is missing.

block.calculateHash undefined (type *Block has no field or method calculateHash)

OK, let’s implement the calculateHash function.

The hash value of the block is calculated by combining the data representing the entire block. Here, we calculate based on the timestamp, transactions, previous block hash, and Nonce that make up the block structure. Transactions are combined with Sender, Receiver, and token transfer amount.

blockchain.go

func (block *Block) calculateHash() string {
var layout = "2006-01-02 15:04:05"
var transactionStrList []string

for _, transaction := range block.Transactions {
transactionStrList = append(transactionStrList, transaction.Sender+transaction.Recipient+strconv.Itoa(transaction.Amount))
}
transactionsString := strings.Join(transactionStrList, "")
record := block.Timestamp.Format(layout) + transactionsString + string(block.PrevBlockHash) + strconv.Itoa(block.Nonce)
hash := sha256.New()
hash.Write([]byte(record))
return hex.EncodeToString(hash.Sum(nil))
}

Now the test should succeed.

Try adding fmt.Printlin to the MineBlock function and display the hashStr to see that the calculation is repeated many times.

 func (block *Block) MineBlock(difficulty int) {
target := strings.Repeat("0", difficulty)

for {
hashStr := block.calculateHash()
+ fmt.Println(hashStr)
if hashStr[:difficulty] == target {
block.Hash = []byte(hashStr)
break
}
block.Nonce++
}
}

Refactor

For refactoring elements, the calculateHash function is a bit messy, so we’ll take out the part that combines transactions into a single string.

 func (block *Block) calculateHash() string {
var layout = "2006-01-02 15:04:05"
- var transactionStrList []string
-
- for _, transaction := range block.Transactions {
- transactionStrList = append(transactionStrList, transaction.Sender+transaction.Recipient+strconv.Itoa(transaction.Amount))
- }
- transactionsString := strings.Join(transactionStrList, "")
- record := block.Timestamp.Format(layout) + transactionsString + string(block.PrevBlockHash) + strconv.Itoa(block.Nonce)
+ record := block.Timestamp.Format(layout) + block.transactionsToString() + string(block.PrevBlockHash) + strconv.Itoa(block.Nonce)
hash := sha256.New()
hash.Write([]byte(record))
return hex.EncodeToString(hash.Sum(nil))
}

+ func (block *Block) transactionsToString() string {
+ var transactionStrList []string
+
+ for _, transaction := range block.Transactions {
+ transactionStrList = append(transactionStrList, transaction.Sender+transaction.Recipient+strconv.Itoa(transaction.Amount))
+ }
+
+ return strings.Join(transactionStrList, "")
+ }

5. Adding a new block to the blockchain

Develop a function to add a new block to the created blockchain as an AddBlock function.

Red — Test Code

The AddBlock function is used to add a new block to the blockchain. It takes a list of transactions as an argument, retrieves the last block of the blockchain (the previous block), and then calls the CreateBlock function to create a new block and specify the previous block hash. Finally, the new block is added to the blockchain.

The contents of the test should flow as follows:

  • Create a blockchain and transactions
  • Perform mining (hash value calculation) on the block
  • Add a new block
  • Confirm that the previous block’s hash value is in the latest block
  • Confirm that the latest block does not contain transactions
  • Confirm that the latest block’s Nonce is 0
  • Confirm that the latest block’s Hash is empty
func TestAddBlock(t *testing.T) {
transaction := Transaction{
Sender: "Alice",
Recipient: "Bob",
Amount: 10,
}
bc := initTransactionPool(transaction)

block := &bc.Blocks[0]

block.MineBlock(bc.difficulty)

prevBlockCount := len(bc.Blocks)

bc.AddBlock()

// Assert
latestBlock := bc.Blocks[len(bc.Blocks)-1]

// Check the number of blocks has increased
if len(bc.Blocks) != prevBlockCount+1 {
t.Errorf("Expected block count is %d, but got %d", prevBlockCount+1, len(bc.Blocks))
}

// Check that the latest block contains the hash value of the previous block
if !reflect.DeepEqual(latestBlock.PrevBlockHash, block.Hash) {
t.Errorf("Expected prev block hash is %s, but got %s", latestBlock.PrevBlockHash, block.Hash)
}

// Check that the latest block does NOT contain any transactions

if latestBlock.Transactions != nil {
t.Errorf("Expected no transaction is in block, but got %v", latestBlock.Transactions)
}

// Check that the Nonce of the latest block is 0
if latestBlock.Nonce != 0 {
t.Errorf("Nonce must be 0, but got %d", latestBlock.Nonce)
}

// Chech that the Hash of the latest block is empty
if string(latestBlock.Hash) != "" {
t.Errorf("Expected hash is vacant, but got %d", latestBlock.Hash)
}
}

Create the test function above and run the test.

Now we get the following error because we haven’t developed anything:

bc.AddBlock undefined (type Blockchain has no field or method AddBlock)

Green — Implement Function

First, add an empty AddBlock function to blockchain.go.

func (bc *Blockchain) AddBlock() {
}

Then I get the following error:

Expected block count is 2, but got 1
Expected prev block hash is , but got 000e03b4990ae508f4d1eda12f6f49a72f67c84ff556d0ed0bf691649a8790f3
Nonce must be 0, but got 6829
Expected hash is vacant, but got [48 48 48 ...]

Implement the contents of the function in blockchain.go.

func (bc *Blockchain) AddBlock() {
latestBlock := bc.Blocks[len(bc.Blocks)-1]

newBlock := Block{
Timestamp: time.Now(),
PrevBlockHash: latestBlock.Hash,
Hash: []byte{},
Nonce: 0,
}

bc.Blocks = append(bc.Blocks, newBlock)
}

We take the latest block at the time before the addition and create a new block by adding its hash value.

The test should now work correctly. Make sure all assertions complete successfully.

6. Block Verification

Finally, implement a function to verify the validity of the added block and change the transaction status. This process is generally performed on nodes other than the miner’s node. After the block is verified, the transactions in that block become “confirmed” and rewards are paid to the miner (the implementation of reward payments is not covered here).

Red — Test Code

To verify a block, confirm that the hash of the previous block is included in a block.

The contents of the test should flow as follows:

  • Create a blockchain
  • Perform mining (hash calculation) for the first block
  • Add a block
  • Create a transaction and add it to the transaction pool
  • Add the transaction to the added block
  • Perform mining (hash calculation) for the added block
  • Execute validation
  • Confirm that the validation is successful
  • Confirm that the transaction status has changed
func TestValidation(t *testing.T) {
difficulty := 3
currentTime := time.Now()
bc := CreateBlockchain(difficulty, currentTime)

block := &bc.Blocks[0]
block.MineBlock(bc.difficulty)

bc.AddBlock()

transaction1 := Transaction{
Sender: "Alice",
Recipient: "Bob",
Amount: 10,
Status: "Pending",
}
transaction2 := Transaction{
Sender: "Bob",
Recipient: "Alice",
Amount: 20,
Status: "Pending",
}

bc.AddTransaction(transaction1)
bc.AddTransaction(transaction2)

for _, transaction := range bc.TransactionPool {
bc.AddTransactionToBlock(transaction)
}

block2 := &bc.Blocks[1]
block2.MineBlock(bc.difficulty)

err := bc.Validation()

if err != nil {
t.Errorf("Validation Error, %s", err)
}

for _, transaction := range bc.Blocks[1].Transactions {
if transaction.Status != "Success" {
t.Errorf("Expected transaction status is Success, but got %s", transaction.Status)
}
}
}

Once the test function is created, run the test. Since nothing is implemented now, the following error will occur:

unknown field 'Status' in struct literal of type Transaction
bc.Validation undefined (type Blockchain has no field or method Validation)
transaction.Status undefined (type Transaction has no field or method Status)

Green — Implement the function

First, prepare the Validation function and add a status element to the Transaction structure.

The following both files are blockchain.go .

func (bc *Blockchain) Validation() error {
return nil
}
 type Transaction struct {
Sender string
Recipient string
Amount int
+ Status string
}

Running the test will result in an error that the transaction status has not changed.

Expected transaction status is Success, but got Pending

Now let’s implement the Validation function.

For blocks other than the first block, perform the validation. The reason is that there is no previous block for the first block.

For the validation, check that:

  • The hash value stored in the block and the hash value obtained using the calculateHash function match
  • The hash value of the previous block stored in the block and the hash value stored in the previous block match

If either condition is false, it means the legitimacy could not be confirmed, and an error is returned.

If the validation is completed without any errors, change the status of all transactions in the block to Success and return nil.

 func (bc *Blockchain) Validation() error {
+ for i := range bc.Blocks[1:] {
+ previousBlock := bc.Blocks[i]
+ currentBlock := &bc.Blocks[i+1]
+ if string(currentBlock.Hash) != currentBlock.calculateHash() || !reflect.DeepEqual(currentBlock.PrevBlockHash, previousBlock.Hash) {
+ return errors.New("Hash is not valid")
+ }

+ for i := 0; i < len(currentBlock.Transactions); i++ {
+ transaction := &currentBlock.Transactions[i]
+ transaction.Status = "Success"
+ }
+ }
return nil
}

Now the test should work.

It’s done! We have implemented the process from creating a blockchain to verifying the legitimacy of a block.

Conclusion

In this article, I introduced the process of implementing a basic blockchain using the Go, following the Test-Driven Development (TDD) flow. There are many areas where testing is insufficient, but I have omitted them to avoid making the article too long.

By learning topics such as the following, you can further deepen your understanding of blockchain technology:

  • Consensus algorithms (Proof of Work, Proof of Stake, etc.)
  • Construction and operation of distributed networks
  • Cryptography (hash functions, public key cryptography, digital signatures, etc.)
  • Development of smart contracts and decentralized applications (dApps)

We hope this article will be a starting point for readers to become interested in and deepen their understanding of blockchain technology.

In actual blockchains, more advanced features and security measures are required. The blockchain created in this article is an ultra-simplified version and is not suitable for use in actual production environments.

Thank you.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

No responses yet

Write a response