Creating a Blockchain: Part 5 - Blockchain data structure

Blockchain data structure and validation

Creating a Blockchain: Part 5 - Blockchain data structure

Modifications:

We are modifying Hash function of Blockhasher. so we can use it with Header, not block because of all required data to create Hash is also present in Header, so not using whole block will be efficient

core/block.go

Added functionality to header to convert into bytes which later helps to create Hash

func (h *Header) Bytes() []byte {
    buf := &bytes.Buffer{}
    enc := gob.NewEncoder(buf)
    enc.Encode(h)
    return buf.Bytes()
}

core/hasher.go

func (BlockHasher) Hash(h *Header) types.Hash { 
    b:= sha256.Sum256(h.Bytes())
    return types.Hash(b)
}

As blocks and transaction has been added, time to implement blockchain data structure,

Blockchain data structure:

  • Defines a Blockchain struct, which appears to be the main data structure for the blockchain.

  • Contains fields for storing the blockchain data: store (a storage interface), headers (a slice of headers), and validator (a validator interface).

core/blockchain.go

package core

type Blockchain struct {
    store Storage
    headers []*Header
    validator Validator
}

core/storage.go

package core

type Storage interface {
    Put(* Block) error
}

type MemoryStorage struct {}

func NewMemoryStore() *MemoryStorage {
    return &MemoryStorage{}
}

func (ms *MemoryStorage) Put(b *Block) error {
    return nil
}
  • Declares a Storage interface with a Put method, which seems to be for storing a block in the blockchain.

  • Implements a MemoryStorage type that satisfies the Storage interface.

  • Provides a function NewMemoryStore for creating a new instance of MemoryStorage.


core/validator.go

package core

import "fmt"

type Validator interface {
    ValidateBlock(b *Block) error
}

type BlockValidator struct {
    bc *Blockchain
}

func NewBlockValidator(bc *Blockchain) *BlockValidator {
    return &BlockValidator{
        bc: bc,
    }
}

func (v *BlockValidator) ValidateBlock(b *Block) error {
    return nil
}
  • Introduces a Validator interface with a ValidateBlock method, likely used for validating the integrity of a block in the blockchain.

  • Defines a BlockValidator struct that implements the Validator interface.

  • The BlockValidator has a reference to the blockchain (bc), and its ValidateBlock method currently returns no error.

  • Later in this blog, we'll implement ValidateBlock after completing other parts.


We've implemented further methods for blockchain to use blockchain struct very efficiently.

  1. type Blockchain struct {...}: Defines a struct named Blockchain with fields for a storage system (store), an array of headers (headers), and a validator (validator).

  2. func NewBlockchain(genesis *Block) (*Blockchain, error) {...}: Creates a new instance of Blockchain with a given genesis block. It initializes the storage, an empty array of headers, and sets up a block validator.

  3. func (bc *Blockchain) AddBlock(b *Block) error {...}: Adds a new block to the blockchain. It first validates the block using the blockchain's validator and then adds the block to the blockchain without revalidating.

  4. func (bc *Blockchain) GetHeader(height uint32) (*Header, error) {...}: Retrieves the header of a block at a specified height in the blockchain. It returns an error if the given height is too high.

  5. func (bc *Blockchain) addBlockWithoutValidation(b *Block) error {...}: Adds a new block to the blockchain without revalidating it. It appends the block's header to the array of headers, logs information about the block, and stores the block in the blockchain's storage.

  6. func (bc *Blockchain) SetValidator(v Validator) {...}: Sets a new validator for the blockchain.

  7. func (bc *Blockchain) HasBlock(height uint32) bool {...}: Checks if the blockchain has a block at a specified height.

  8. func (bc *Blockchain) Height() uint32 {...}: Returns the height of the blockchain, which is the index of the last block in the headers array.

core/blockchain.go

package core

import (
    "fmt"

    "github.com/sirupsen/logrus"
)

type Blockchain struct {
    store Storage
    headers []*Header
    validator Validator
}

func NewBlockchain(genesis *Block) (*Blockchain, error) {
    bc := &Blockchain{
        store : NewMemoryStore(),
        headers : []*Header{},
    }
    bc.validator = NewBlockValidator(bc)
    bc.addBlockWithoutValidation(genesis)

    return bc, nil
}

func (bc *Blockchain) AddBlock(b *Block) error {
    if err:= bc.validator.ValidateBlock(b); err != nil {
        return err
    }
    return bc.addBlockWithoutValidation(b)
}

func (bc *Blockchain) GetHeader(height uint32) (*Header, error){

    if height > bc.Height(){
        return nil, fmt.Errorf("given height %d is too high", height)
    }
    return bc.headers[height], nil
}

func (bc *Blockchain) addBlockWithoutValidation(b *Block) error {
    bc.headers = append(bc.headers,b.Header)

    logrus.WithFields(logrus.Fields{
        "height": b.Height, 
        "hash": b.Hash(BlockHasher{}),
    }).Info("adding new block")

    return bc.store.Put(b)
}

func (bc *Blockchain) SetValidator(v Validator) {
    bc.validator = v
}

func (bc *Blockchain) HasBlock(height uint32) bool {
    return bc.Height() >= height
}

func (bc *Blockchain) Height() uint32 {
    return uint32(len(bc.headers) - 1)
}

Random Block and Transaction

In testing, we have to generate random block and random transaction to check blockchain parts fits together and working properly or not.

So to generate, random transaction with signature we have created randomTxWithSignature

core/transaction_test.go

  • Generates a random private key using the crypto.GeneratePrivateKey function.

  • Creates a new transaction (Transaction) with some arbitrary data (in this case, "tx data").

  • Signs the transaction using the generated private key.

func randomTxWithSignature() *Transaction { 
    privKey := crypto.GeneratePrivateKey()

    tx := &Transaction{
        Data: []byte("tx data"),
    }

    tx.Sign((privKey));
    return tx;
}

And to generate random Blocks, we have created randomBlock and randomBlockWithSignature,


core/block_test.go

func randomBlock(height uint32, prevBlockHash types.Hash) *Block {
    header := &Header{
        Version: 1,
        PrevBlockHash: prevBlockHash,
        Timestamp: time.Now().UnixNano(),
        Height: height,
    }

    b := NewBlock(header,[]Transaction{});
    tx := randomTxWithSignature();
    b.AddTransaction(tx)

    return b
}

func randomBlockWithSignature(height uint32, prevBlockHash types.Hash) *Block {
    privKey := crypto.GeneratePrivateKey()

    b := randomBlock(height, prevBlockHash)
    b.Sign(privKey)
    return b
}
  1. randomBlock:

    • Takes two parameters, height and prevBlockHash, to create a new block with a corresponding header.

    • The header includes information such as the version, previous block hash, timestamp, and block height.

    • Creates a new block (Block) with the generated header and an empty list of transactions.

    • Adds a randomly generated transaction with a signature to the block using the randomTxWithSignature function.

  2. randomBlockWithSignature:

    • Similar to randomBlock but additionally signs the block with a randomly generated private key.

    • Generates a new private key using crypto.GeneratePrivateKey.

    • Calls the randomBlock function to create a block.

    • Signs the block using the generated private key.


Testing

core/blockchain_test.go

package core

import (
    "ProjectX/types"
    "fmt"
    "testing"

    "github.com/stretchr/testify/assert"
)

func newBlockchainWithGenesis(t *testing.T) *Blockchain {
    bc, err := NewBlockchain(randomBlock(0, types.Hash{}))

    assert.Nil(t,err)
    return bc
}

func TestNewBlockchain(t *testing.T) {
    bc := newBlockchainWithGenesis(t)

    assert.NotNil(t,bc)
}

func getPreviousBlockHash(t *testing.T, bc *Blockchain, height uint32) types.Hash {
    prevHeader,err := bc.GetHeader(height - 1);

    assert.Nil(t, err);
    prevBlockHash := BlockHasher{}.Hash(prevHeader);
    // assert.NotNil(t, prevBlockHash)
    return prevBlockHash
}

func TestHasBlock(t *testing.T){
    bc := newBlockchainWithGenesis(t)
    assert.True(t, bc.HasBlock(0))
    assert.False(t, bc.HasBlock(1))
}
  • newBlockchainWithGenesis:

    • Creates a new instance of the Blockchain with a genesis block (a block with height 0 and an empty hash).

    • Uses the NewBlockchain constructor and a helper function randomBlock to generate the genesis block.

    • Asserts that the returned blockchain (bc) and the error (err) are both nil.

  • TestNewBlockchain:

    • Calls the newBlockchainWithGenesis function to create a blockchain instance.

    • Asserts that the created blockchain (bc) is not nil, indicating a successful creation.

  • getPreviousBlockHash:

    • Retrieves the hash of the block at a specified height (height - 1) from the blockchain (bc).

    • Uses the BlockHasher to calculate the hash of the previous block's header.

    • Asserts that the returned hash (prevBlockHash) and the error (err) are both nil.

  • TestHasBlock:

    • Creates a new blockchain instance with a genesis block.

      • Tests if the blockchain has a block at height 0 using assert.True.

      • Tests if the blockchain does not have a block at height 1 using assert.False.

Output:

Succeed!!


Verification of transaction, block and blockchain

To ValidateBlock by BlockValidate we have to verify that is that block fits in blockchain well or not, is all block data is verified or not and is every transaction in that block is verified or what!?

For transaction verification:

core/transaction.go

The Verify method of the Transaction type checks whether the transaction has a valid signature. It returns an error if the transaction has no signature or if the signature is found to be invalid for the specified sender (From) and transaction data (Data).

func (t *Transaction) Verify() error {

    if t.Signature == nil {
        return fmt.Errorf("transaction has no signature")
    }

    if !t.Signature.Verify(t.From, t.Data){
        return fmt.Errorf("transaction has Invalid signature")
    }

    return nil
}

For blocks verification:

core/block.go:

  • Checks if the block has a valid signature by verifying the signature against the validator's public key and the serialized bytes of the block header.

  • Iterates through the transactions in the block and verifies each transaction using its own Verify method.

  • Returns an error if the block has no signature, has an invalid signature, or if any of its transactions fail the verification process.

func (b *Block) Verify() error {

    if b.Signature == nil {
        return fmt.Errorf("Block has no signature")
    }

    if !b.Signature.Verify(b.Validator, b.Header.Bytes()) {
        return fmt.Errorf("Block has Invalid signature")
    }

    for _, tx := range b.Transactions {
        if err := tx.Verify(); err != nil {
            return err
        }
    }

    return nil
}

To Validate block in aspect of blockchain's latest state:

core/validator.go

The ValidateBlock method of the BlockValidator type performs the following tasks:

  • Checks if the blockchain already contains a block with the same height as the incoming block, indicating a potential duplicate.

  • Verifies that the incoming block has the correct height, which should be one more than the current height of the blockchain.

  • Retrieves the header of the previous block and compares its hash with the PrevBlockHash field of the incoming block to ensure the blockchain continuity.

  • Calls the Verify method of the incoming block to check the validity of its signature and the signatures of its transactions.

  • Returns an error if any of the validation checks fail, providing specific error messages indicating the nature of the failure.


func (v *BlockValidator) ValidateBlock(b *Block) error {

    if v.bc.HasBlock(b.Height){
        return fmt.Errorf("chain already contains block (%d) with hash (%s)", b.Height, b.Hash(BlockHasher{}))
    }

    if(b.Height != v.bc.Height()+1 ){
        return fmt.Errorf("block is too high with height (%d) with hash (%s)", b.Height, b.Hash(BlockHasher{}))
    }

    prevHeader, err := v.bc.GetHeader(b.Height - 1);
    if err != nil { 
        return err
    }

    hash := BlockHasher{}.Hash(prevHeader);

    if b.PrevBlockHash != hash {
        return fmt.Errorf("the hash of prev block is invalid")
    }

    if err := b.Verify(); err != nil {
        return err
    }

    return nil
}

Testing of Add block

The TestAddBlock function is a testing scenario for adding multiple blocks to a blockchain. Here's a short explanation:

  • Initializes a new blockchain with a genesis block using the newBlockchainWithGenesis function.

  • Iterates through a loop to add 1000 blocks to the blockchain, each with a random signature and height.

  • For each iteration, it gets the previous block's hash, creates a new block with a signature, and adds it to the blockchain using bc.AddBlock.

  • After adding each block, it retrieves the header of the latest block in the blockchain and asserts that the header is not nil and there is no error.

  • The overall goal is to test the functionality of adding multiple blocks to the blockchain and ensuring the headers are correctly updated.

core/blockchain_test.go

func TestAddBlock(t *testing.T) {
    bc := newBlockchainWithGenesis(t);
    header, err := bc.GetHeader(bc.Height());
    fmt.Println("header",header)
    assert.Nil(t, err);
    assert.NotNil(t, header);

    lenBlocks := 1000;
    for i := 0; i < lenBlocks; i++ {
        prevHash := getPreviousBlockHash(t, bc, uint32(i+1));

        b := randomBlockWithSignature(uint32(i+1),prevHash)
        err := bc.AddBlock(b);
        assert.Nil(t, err);

        header, err := bc.GetHeader(bc.Height());
        assert.Nil(t, err);
        assert.NotNil(t, header);
    }
}

Good to go!

make test

The following blog post will explore the code related to Transaction Mempool


In this blog series, I'll be sharing code snippets related to blockchain architecture. While the code will be available on my GitHub, I want to highlight that the entire architecture isn't solely my own. I'm learning as I go, drawing inspiration and knowledge from various sources, including a helpful YouTube playlist that has contributed to my learning process.

Did you find this article valuable?

Support Siddharth Patel by becoming a sponsor. Any amount is appreciated!