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
Remove
EncodeBinary
andDecodeBinary
methods forBlock
andHeader
. Also, Remove Hash function.Reason: we will change the logic and implement it soon.
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 containTransactions
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.
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 }
Remove
EncodeBinary
andDecodeBinary
forTransaction
from core/transaction.go
Modifications
As we have implemented the Public-Private key, we are using it in Transaction
and Block
structs.
Add
Validator
andSignature
inBlock
.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
andSignature
inTransaction
.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
}
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 thePublicKey
field of the transaction to the public key corresponding to the provided private key.
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
}
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 theValidator
field of the block to the public key corresponding to the provided private key.
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:
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.
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.