Creating a Blockchain: Part 4 - Block and TX signing and verification

Creating a Blockchain: Part 4 - Block and TX signing and verification

Improvements

First of all, we are improving our code a little bit. As we are making this blockchain modular, we have to improve our code to gain modularity.
Modular blockchains are composed of interconnected modules or components, each responsible for specific functions.

core/block.go

  1. Remove EncodeBinary and DecodeBinary methods for Block and Header. Also, Remove Hash function.

    Reason: we will change the logic and implement it soon.

  2. Add DataHash var to Header struct as shown below,

    core/block.go

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

    Reason: We were only hashing Header that doesn't contain Transactions which means, In future, anyone can change the transactions and block hash still won't change which compromises the system.

    So, We are adding DataHash that will have a hash of Transactions.

  3. Change Header with *Header, It'll help in future when there is any need for all Headers at once.

    core/block.go

     type Block struct {
         *Header
         Transactions []Transaction
    
         hash types.Hash
     }
    
  4. Remove EncodeBinary and DecodeBinary for Transaction from core/transaction.go

Modifications

As we have implemented the Public-Private key, we are using it in Transaction and Block structs.

  • Add Validator and Signature in Block.

  • Function NewBlock to create new block

core/block.go

import (
    "ProjectX/crypto"
    "ProjectX/types"
)

type Block struct {
    Header
    Transactions []Transaction
    Validator crypto.PublicKey
    Signature *crypto.Signature

    hash types.Hash
}

func NewBlock(h *Header, txx []Transaction) *Block {
    return &Block{
        Header : h, 
        Transactions: txx,
    }
}
  • Add Validator and Signature in Transaction.

    core/transaction.go

      import "ProjectX/crypto"
    
      type Transaction struct {
          Data []byte
    
          PublicKey crypto.PublicKey
          Signature *crypto.Signature
      }
    

Hashing

We have removed Hash function in the improvement step, so now, We are implementing it again with other appropriate logic.

This code practise will make our block hashing more modular.

core/hasher.go

package core

import (
    "ProjectX/types"
)

type Hasher[T any] interface{
    Hash(T) types.Hash
}

Hasher interface is generic, which we are going to use for Block, Transaction and others.


core/block.go

func (b *Block) Hash(hasher Hasher[*Block]) types.Hash {
    if b.hash.IsZero() {
        b.hash = hasher.Hash(b)
    }
    return b.hash
}

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

For Block, Hash function will accept hasher type of Hasher[*Block], which has Hash function capable of encode Block.

In simpler terms, the HeaderData method in the Block struct is creating a byte representation of the header information of the block. It does this by encoding the Header field of the block into a sequence of bytes using the gob encoding format. The resulting byte slice represents the serialized form of the block's header data


This BlockHasher struct has implemented hasher interface and can be used as hasher parameter in Hash method of Block.

core/hasher.go

package core

import (
    "ProjectX/types"
    "crypto/sha256"
    "encoding/json"
)

type Hasher[T any] interface{
    Hash(T) types.Hash
}

type BlockHasher struct{}

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

Encoding and Decoding

We have removed EncodeBinary and DecodeBinary methods of Block at the start. so, we are implementing it with different logic.

This code practice will make our block hashing more modular.

core/encoding.go

package core

import "io"

type Encoder[T any] interface{
    Encode(io.Writer,T) error
}

type Decoder[T any] interface{
    Decode(io.Reader,T) error
}

The way we have declared and used an interface for Hashing, is the same way we are defining and using an interface for encoding and decoding.

In simpler terms, the HeaderData method in the Block struct is creating a byte representation of the header information of the block. It does this by encoding the Header field of the block into a sequence of bytes using the gob encoding format. The resulting byte slice represents the serialized form of the block's header data


core/block.go

func (b *Block) Encode(w io.Writer,enc Encoder[*Block]) error{
    return enc.Encode(w, b)
}
func (b *Block) Decode(r io.Reader,dec Decoder[*Block]) error{
    return dec.Decode(r, b)
}

We are defining Encode and Decode method for Block using interface Encoder and Decoder that we've defined in encoding.go

We are not putting any logic of encoding and decoding here. just making sure the skeleton is ready to use.

Testing

As we have changed the core logic of hashing and encoding, we have to clean block_test.go file and have to write tests again.

core/block_test.go

package core

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

func RandomBlock(height uint32) *Block {
    header := &Header{
        Version: 1,
        PrevBlockHash: types.RandomHash(),
        Timestamp: time.Now().UnixNano(),
        Height: height,
    }
    tx := Transaction{
        Data : []byte("Hello tx"),
    }

    return NewBlock(header,[]Transaction{tx})
}

func TestHashBlock(t *testing.T) {
    b := RandomBlock(0)
    fmt.Println("b.Hash", b.Hash(BlockHasher{}))
}

Good to go!

make test

Sign and Verify Transaction

We are implementing Sign and Verify method for Transaction with the functionality we have as of now and It will surely improve by the time when we add more functionality to our code.

core/transaction.go

func (t *Transaction) Sign(privKey crypto.PrivateKey) error {
    sig, err := privKey.Sign(t.Data)
    if err != nil {
        return err
    }
    t.Signature = sig
    t.PublicKey = privKey.PublicKey()
    return nil
}


func (t *Transaction) Verify() error {
    if t.Signature == nil {
        return fmt.Errorf("transaction has no signature")
    }
    if !t.Signature.Verify(t.PublicKey, t.Data){
        return fmt.Errorf("transaction has Invalid signature")
    }
    return nil
}
  1. Sign Method:

    • The Sign method is used to sign a transaction. Given a private key (privKey), it generates a cryptographic signature for the transaction data (t.Data).

    • After signing, it sets the Signature field of the transaction to the generated signature and also sets the PublicKey field of the transaction to the public key corresponding to the provided private key.

  2. Verify Method:

    • The Verify method checks the validity of a signed transaction. It ensures that the transaction has a signature.

    • If the signature is present, it uses the public key (t.PublicKey) to verify that the signature matches the original data (t.Data). If the verification fails, it indicates that the transaction has an invalid signature.

    • If the verification succeeds, it means that the transaction is authentic and has not been tampered with.


Sign and Verify Block

The same Sign and Verify methods goes for Block as well,

core/block.go

func (b *Block) Sign(privKey crypto.PrivateKey) error {
    sig, err := privKey.Sign(b.HeaderData())
    if err != nil {
        return err
    }
    b.Signature = sig
    b.Validator = privKey.PublicKey()
    return nil
}


func (b *Block) Verify() error {
    if b.Signature == nil {
        return fmt.Errorf("Block has no signature")
    }
    if !b.Signature.Verify(b.Validator, b.HeaderData()) {
        return fmt.Errorf("Block has Invalid signature")
    }    
    return nil
}
  1. Sign Method:

    • The Sign method is used to sign a block. Given a private key (privKey), it generates a cryptographic signature for the header data of the block (b.HeaderData()).

    • After signing, it sets the Signature field of the block to the generated signature and also sets the Validator field of the block to the public key corresponding to the provided private key.

  2. Verify Method:

    • The Verify method checks the validity of a signed block. It ensures that the block has a signature.

    • If the signature is present, it uses the public key (b.Validator) to verify that the signature matches the original header data of the block (b.HeaderData()).

    • If the verification fails, it indicates that the block has an invalid signature.

    • If the verification succeeds, it means that the block is authentic and has not been tampered with.


Testing Sign and Verify Tx and Block

These test functions ensure that:

  1. For transactions:

    • TestSignTransaction: Signing a transaction with a private key generates a valid signature.

    • TestVerifyTransaction: Verifying a transaction checks if the signature matches the public key and detects tampering.

  2. For blocks:

    • TestSignBlock: Signing a block with a private key generates a valid signature.

    • TestVerifyBlock: Verifying a block checks if the signature matches the validator's public key and detects tampering.

core/transaction_test.go

package core

import (
    "ProjectX/crypto"
    "testing"

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

func TestSignTransaction(t *testing.T) {
    privKey := crypto.GeneratePrivateKey()
    tx := &Transaction{
        Data: []byte("tx data 1"),
    }

    assert.Nil(t, tx.Sign(privKey))

    assert.NotNil(t, tx.Signature)
    assert.Equal(t, tx.PublicKey, privKey.PublicKey())
}

func TestVerifyTransaction(t *testing.T) {
    privKey := crypto.GeneratePrivateKey()
    anotherPrivKey := crypto.GeneratePrivateKey()

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

    assert.Nil(t, tx.Sign(privKey))
    tx.Signature = nil
    assert.NotNil(t, tx.Verify())

    assert.Nil(t, tx.Sign(privKey))
    tx.PublicKey = anotherPrivKey.PublicKey()
    assert.NotNil(t, tx.Verify())
}

core/block_test.go

func TestSignBlock(t *testing.T) {
    privKey := crypto.GeneratePrivateKey()

    b := RandomBlock(1)
    assert.Nil(t, b.Sign(privKey))
    assert.NotNil(t, b.Signature)
    assert.Equal(t, b.Validator, privKey.PublicKey())
}

func TestVerifyBlock(t *testing.T) {
    privKey := crypto.GeneratePrivateKey()
    anotherPrivKey := crypto.GeneratePrivateKey()
    b := RandomBlock(1)

    assert.Nil(t, b.Sign(privKey))
    b.Signature = nil
    assert.NotNil(t, b.Verify())

    assert.Nil(t, b.Sign(privKey))
    b.Validator = anotherPrivKey.PublicKey()
    assert.NotNil(t, b.Verify())
}

Let's test it!!

make test

The following blog post will explore the code related to Creating the blockchain data structure✨


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!