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).
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 := ¤tBlock.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.