Creating a Blockchain: Part 2 - Blocks and hashing

Creating a Blockchain: Part 2 - Blocks and hashing

Blocks and hashing

Block structure

We are creating a directory named core and implementing a core package with functionalities of Blocks and Transactions.

We'll add Transaction, Hash and Encoding and Decoding of Blocks and Transaction with Hash.

mkdir core

core/block.go

package core

import (
    "ProjectX/types"
    "bytes"
    "crypto/sha256"
    "encoding/binary"
    "io"
)

type Header struct {
    Version uint32 
    PrevBlock types.Hash
    Timestamp int64
    Height uint32
    Nonce uint64
}

type Block struct {
    Header
    Transactions []Transaction

    hash types.Hash
}

Types and Hash

We will be doing every hash related work in package types, so creating a directory named types and implementing basic hash type and methods

mkdir types

Hash type is array of 32 bytes uint8 as it contains 32 hexadecimal

Functions:

  1. HashFromBytes: generates a hash from a byte slice while enforcing a restriction that the input slice must be exactly 32 bytes long.

  2. RandomBytes: generates a slice of random bytes with a length equal to the input size provided.

  3. RandomHash: generates hash with length of 32 by utilizing RandomBytes function

types/hash.go

package types

import (
    "crypto/rand"
    "encoding/hex"
    "fmt"
)

type Hash [32]uint8

func HashFromBytes(b []byte) Hash{
    if len(b)!=32 {
        msg := fmt.Sprintf("given bytes with length %d is should be 32",len(b))
        panic(msg)
    }

    var value [32]uint8
    for i:=0 ; i<len(b) ; i++ {
        value[i] = b[i]
    }
    return value
}

func RandomBytes(size uint) []byte {
    token := make([]byte, size)
    rand.Read(token)
    return token
}

func RandomHash() Hash{
    return HashFromBytes(RandomBytes(32))
}

Encoding and decoding block header

EncodeBinary: method encodes the Header struct fields into binary data and writes them into the provided io.Writer

DecodeBinary: method reads and decodes binary data from the provided io.Reader into the Header struct fields.

core/block.go

type Header struct {
    Version uint32 
    PrevBlock types.Hash
    Timestamp int64
    Height uint32
    Nonce uint64
}
func (h *Header)EncodeBinary(w io.Writer) error {
    if err := binary.Write(w, binary.LittleEndian, &h.Version); err != nil {
        return err
    }
    if err := binary.Write(w, binary.LittleEndian, &h.PrevBlock); err != nil {
        return err
    }
    if err := binary.Write(w, binary.LittleEndian, &h.Timestamp); err != nil {
        return err
    }
    if err := binary.Write(w, binary.LittleEndian, &h.Height); err != nil {
        return err
    }
    return binary.Write(w, binary.LittleEndian, &h.Nonce);
}

func (h *Header)DecodeBinary(r io.Reader) error {
    if err := binary.Read(r, binary.LittleEndian, &h.Version); err != nil {
        return err
    }
    if err := binary.Read(r, binary.LittleEndian, &h.PrevBlock); err != nil {
        return err
    }
    if err := binary.Read(r, binary.LittleEndian, &h.Timestamp); err != nil {
        return err
    }
    if err := binary.Read(r, binary.LittleEndian, &h.Height); err != nil {
        return err
    }
    return binary.Read(r, binary.LittleEndian, &h.Nonce);
}

Testing of Encoding and Decoding

The TestHeader_Encode_Decode test ensures that the encoding and subsequent decoding operations of a Header struct using binary I/O functions are successful and result in an accurate reproduction of the original Header.

core/block_test.go

package core

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

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

func TestHeader_Encode_Decode(t *testing.T){
    h := &Header{
        Version:1,
        PrevBlock: types.RandomHash(),
        Timestamp: time.Now().UnixNano(),
        Height:32,
        Nonce:12450,
    }

    buf := &bytes.Buffer{}
    assert.Nil(t, h.EncodeBinary(buf))

    hDecode := &Header{}
    assert.Nil(t, hDecode.DecodeBinary(buf))
    assert.Equal(t, hDecode, h)
}
make test


Transactions

We are only implementing Transaction, so we can use it in our block and do further encoding and decoding. It is not going to be implemented full-fledged.

We will work on Transaction later on and will update it soon.

core/transaction.go

package core

import "io"

type Transaction struct {
}

func (t *Transaction) EncodeBinary(w io.Writer) error {
    return nil
}

func (t *Transaction) DecodeBinary(r io.Reader) error {
    return nil
}

Encoding and Decoding of block

EncodeBinary method**:** encodes a Block struct and its transactions into binary data by utilizing the EncodeBinary method from the Header and Transaction structs. It writes this encoded data into the provided io.Writer. If any error occurs during encoding, it returns the error immediately.

DecodeBinary method**:** decodes binary data from the provided io.Reader into the Block struct and its associated transactions. It uses the DecodeBinary method from the Header and Transaction structs to decode the data. If an error occurs during decoding, it returns the error.

core/block.go

func (b *Block)EncodeBinary(w io.Writer) error {
    if err := b.Header.EncodeBinary(w); err != nil {
        return err
    }

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

    return nil
}

func (b *Block)DecodeBinary(r io.Reader) error {
    if err := b.Header.DecodeBinary(r); err != nil {
        return err
    }

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

    return nil
}

Test it!!

This test verifies whether the encoding and decoding methods for the Block works correctly.

func TestBlock_Encode_Decode(t *testing.T){
    b := &Block{
        Header: Header{
        Version:1,
        PrevBlock: types.RandomHash(),
        Timestamp: time.Now().UnixNano(),
        Height:32,
        Nonce:12450,
        },
        Transactions: nil,
    }

    buf := &bytes.Buffer{}
    assert.Nil(t, b.EncodeBinary(buf))

    bDecode := &Block{}
    assert.Nil(t, bDecode.DecodeBinary(buf))
    assert.Equal(t, bDecode, b)
}
make test

It is working indeed!!


Creating block hash

We are implementing necessary functions which will help later.

  1. ToSlice(): Converts a Hash array into a byte slice by iterating over its elements and copying them into the slice.

  2. String(): Converts the Hash array to a hexadecimal string by first converting it to a byte slice and then encoding it into a hexadecimal representation using hex.EncodeToString.

  3. IsZero(): Checks if the Hash array is zero-valued by iterating over its elements and returning true if all elements are zero; otherwise, it returns false.

types/hash.go

type Hash [32]uint8

func (h Hash) ToSlice() []byte {
    b := make([]byte, 32)
    for i:=0;i<32;i++ {
        b[i] = h[i]
    }
    return b
}

func (h Hash) String() string {
    return hex.EncodeToString(h.ToSlice())
}

func (h Hash) IsZero() bool {
    for i := 0; i < len(h); i++{
        if h[i] != 0 {
            return false
        }
    }
    return true
}

Hash(): Generates and returns the hash value of the block by encoding the block's header into binary data, computing its SHA-256 hash if the block's existing hash value is zero, and finally returning the computed hash.

core/block.go

func (b *Block) Hash() types.Hash {
    buf := &bytes.Buffer{}
    b.Header.EncodeBinary(buf)

    if b.hash.IsZero() {
        b.hash = types.Hash(sha256.Sum256(buf.Bytes()))
    }
    return b.hash
}

Testing it then,

TestBlockHash(): A test function that creates a sample Block, generates its hash using the Hash() method, asserts that the generated hash is not zero, and then prints the details of the hash for verification purposes.

core/block_test.go

func TestBlockHash(t *testing.T){
    b := &Block{
        Header: Header{
        Version:1,
        PrevBlock: types.RandomHash(),
        Timestamp: time.Now().UnixNano(),
        Height:32,
        Nonce:12450,
        },
        Transactions: []Transaction{},
    }

    h := b.Hash()
    assert.False(t, h.IsZero())

    fmt.Printf("%+v",h)
}
make test

It's generating block hash!!✨

The following blog post will explore the code related to Crypto keypairs and signatures.✨


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!