Creating a Blockchain: Part 6 - Transaction Mempool and Tx encoding
Synchronization of data
We'd implemented blockchain data structure but we have to look after the situation that whenever data is going to be fetched ow written from blockchain, it should be on latest blochchain state.
To take care of that, we implemented mutex,
core/blockchain.go
package core
import (
"fmt"
"sync"
"github.com/sirupsen/logrus"
)
type Blockchain struct {
// other properties
lock sync.RWMutex
}
func (bc *Blockchain) GetHeader(height uint32) (*Header, error){
if height > bc.Height(){
return nil, fmt.Errorf("given height %d is too high", height)
}
bc.lock.Lock()
defer bc.lock.Unlock()
return bc.headers[height], nil
}
func (bc *Blockchain) addBlockWithoutValidation(b *Block) error {
bc.lock.Lock()
defer bc.lock.Unlock()
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) Height() uint32 {
bc.lock.RLock()
defer bc.lock.RUnlock()
return uint32(len(bc.headers) - 1)
}
// other methods of blockchain
we updated only necessary functions.
Modifications
Before, we go ahead, we did some changes in previous code to decrease overhead of small changes while implementing important and useful code.
Generally we found this changes while coding future parts, but to make sequence of code in blog more understandable, we modified the code first.
Make Key in Public Key accessible to other code
crypto/keypair.go
type PublicKey struct {
Key *ecdsa.PublicKey
}
type Signature struct {
R,S *big.Int
}
//also, updated the code which has used Public key and Signature
//with Capital letters starting Key in Public Key and R , S in Signature.
RandomTransaction and RandomBlock
core/transaction_test.go
added assert.Nil
to make sure tx.sign
runs successfully.
func randomTxWithSignature(t *testing.T) *Transaction {
privKey := crypto.GeneratePrivateKey()
tx := &Transaction{
Data: []byte("tx data"),
}
assert.Nil(t, tx.Sign(privKey));
return tx;
}
due to changes in randomTxWithSignature
, few other code should be update which use randomTxWithSignature
function in their code,
core/block_test.go
func randomBlock(height uint32, prevBlockHash types.Hash) *Block {
header := &Header{
Version: 1,
PrevBlockHash: prevBlockHash,
Timestamp: time.Now().UnixNano(),
Height: height,
}
return NewBlock(header, []Transaction{})
}
func randomBlockWithSignature(t *testing.T, height uint32, prevBlockHash types.Hash) *Block {
privKey := crypto.GeneratePrivateKey()
b := randomBlock(height, prevBlockHash)
tx := randomTxWithSignature(t)
b.AddTransaction(tx)
assert.Nil(t, b.Sign(privKey))
return b
}
Encoding and Decoding of Transactions
In previous code, we left encoding and decoding part but now it's time to make it work so it can be useful in transaction pool implementation.
Interface Definitions:
Encoder[T]
andDecoder[T]
are interfaces that define methods for encoding and decoding data of typeT
.Encode(T) error
is a method that takes an object of typeT
and encodes it, returning an error if any issue occurs.Decode(T) error
is a method that takes an empty object of typeT
, decodes data into it, and returns an error if any issue occurs.
GobTxEncoder:
GobTxEncoder
is a struct that implements theEncoder
interface for transactions using the Gob encoding format.It has a writer (
w
) where the encoded data will be written.NewGobTxEncoder(w io.Writer)
is a constructor that initializes a newGobTxEncoder
with a specified writer.Encode(tx *Transaction) error
encodes a transaction using Gob and writes the encoded data to the specified writer.
GobTxDecoder:
GobTxDecoder
is a struct that implements theDecoder
interface for transactions using the Gob encoding format.It has a reader (
r
) from which the encoded data will be read.NewGobTxDecoder(r io.Reader)
is a constructor that initializes a newGobTxDecoder
with a specified reader.Decode(tx *Transaction) error
decodes data from the specified reader using Gob and populates the provided transaction object.
core/encoding.go
package core
import (
"crypto/elliptic"
"encoding/gob"
"io"
)
type Encoder[T any] interface{
Encode(T) error
}
type Decoder[T any] interface{
Decode(T) error
}
type GobTxEncoder struct {
w io.Writer
}
func NewGobTxEncoder(w io.Writer) *GobTxEncoder {
gob.Register(elliptic.P256())
return &GobTxEncoder{
w : w,
}
}
func (e *GobTxEncoder) Encode(tx *Transaction) error {
return gob.NewEncoder(e.w).Encode(tx)
}
type GobTxDecoder struct {
r io.Reader
}
func NewGobTxDecoder(r io.Reader) *GobTxDecoder {
gob.Register(elliptic.P256())
return &GobTxDecoder{
r : r,
}
}
func (d *GobTxDecoder) Decode(tx *Transaction) error {
return gob.NewDecoder(d.r).Decode(tx)
}
As we've changed Encoder and Decoder interface, we've to also update affected portion of code as well as use code.
core/block.go
func (b *Block) Encode(enc Encoder[*Block]) error{
return enc.Encode(b)
}
func (b *Block) Decode(dec Decoder[*Block]) error{
return dec.Decode(b)
}
core/transactions
func (t *Transaction) Encode(enc Encoder[*Transaction]) error{
return enc.Encode(t)
}
func (t *Transaction) Decode(dec Decoder[*Transaction]) error{
return dec.Decode(t)
}
Testing of Transaction encoding and decoding,
core/transaction_test.go
func TestEncodeDecodeTransact(t *testing.T){
tx := randomTxWithSignature(t)
buf := &bytes.Buffer{}
assert.Nil(t, tx.Encode(NewGobTxEncoder(buf)))
txDecoded := new(Transaction)
assert.Nil(t, txDecoded.Decode(NewGobTxDecoder(buf)))
assert.Equal(t, tx, txDecoded)
}
make test
It worked!!
Transaction Hashing and First Seen
Hasher
: For hashing the transaction, it accepts hasher of interface Hasher[*Transaction]
.
firstSeen
: firstSeen for transaction mempool purposes, it denotes the value of appearance of particular transaction in local node's tx pool. It'll be required in transaction pool code.
firstSeen
and SetFirstSeen
are getter and setter function of that transaction
core/transaction.go
type Transaction struct {
// other
firstSeen int64
}
func (t *Transaction) Hash(hasher Hasher[*Transaction]) types.Hash {
if t.hash.IsZero() {
t.hash = hasher.Hash(t)
}
return t.hash
}
func NewTransaction(data []byte) *Transaction {
return &Transaction{
Data: data,
}
}
func (t *Transaction) SetFirstSeen(val int64) {
t.firstSeen = val;
}
func (t *Transaction) FirstSeen() int64 {
return t.firstSeen;
}
//other remaining functions
core/hasher.go
type TxHasher struct{}
func (TxHasher) Hash(tx *Transaction) types.Hash {
return types.Hash(sha256.Sum256(tx.Data))
}
Transaction Mempool
Transaction mempool need to be implement as server or node level,
Here's a brief explanation of each function:
NewTxPool:
- Constructor function that creates a new transaction pool with an empty map to store transactions.
NewTxMapSorter:
Constructor function for a sorter of transactions based on their first seen timestamp.
Takes a map of transactions, converts it to a slice, sorts it based on the first seen timestamp, and returns a
TxMapSorter
instance.
Less (TxMapSorter):
- Implements the
sort.Interface
methodLess
for sorting transactions by their first seen timestamp.
- Implements the
Len (TxMapSorter):
- Implements the
sort.Interface
methodLen
to get the length of the transactions slice.
- Implements the
Swap (TxMapSorter):
- Implements the
sort.Interface
methodSwap
for swapping two transactions in the slice.
- Implements the
Transactions (TxPool):
- Retrieves all transactions from the transaction pool, sorted by their first seen timestamp.
Add (TxPool):
Adds a transaction to the pool.
Computes the hash of the transaction using a transaction hasher and uses it as the key in the map.
Has (TxPool):
- Checks if a transaction with a given hash exists in the pool.
Len (TxPool):
- Gets the number of transactions currently in the pool.
Flush (TxPool):
- Clears all transactions from the pool by creating a new empty map.
network/txpool.go
package network
import (
"ProjectX/core"
"ProjectX/types"
"sort"
)
type TxPool struct {
transactions map[types.Hash]*core.Transaction
}
func NewTxPool() *TxPool{
return &TxPool{
transactions: make(map[types.Hash]*core.Transaction),
}
}
type TxMapSorter struct{
transactions []*core.Transaction
}
func NewTxMapSorter(txMap map[types.Hash]*core.Transaction) *TxMapSorter{
txx := make([]*core.Transaction, len(txMap))
i := 0
for _, tx := range txMap{
txx[i] = tx
i++
}
s := &TxMapSorter{
transactions : txx,
}
sort.Sort(s);
return s
}
func (s *TxMapSorter) Less(i,j int) bool {
return s.transactions[i].FirstSeen() < s.transactions[j].FirstSeen();
}
func (s *TxMapSorter) Len() int {
return len(s.transactions)
}
func (s *TxMapSorter) Swap(i, j int) {
s.transactions[i], s.transactions[j] = s.transactions[j], s.transactions[i]
}
func (tp *TxPool) Transactions() []*core.Transaction {
s := NewTxMapSorter(tp.transactions)
return s.transactions
}
//Add adds transaction to the pool, caller is responsible to check transaction
// whether that transaction already exists or not.
func (tp *TxPool) Add(tx *core.Transaction) error {
hash := tx.Hash(core.TxHasher{})
tp.transactions[hash] = tx
return nil
}
func (tp *TxPool) Has(hash types.Hash) bool{
_, ok := tp.transactions[hash]
return ok
}
func (tp *TxPool) Len() int{
return len(tp.transactions)
}
func (tp *TxPool) Flush() {
tp.transactions = make(map[types.Hash]*core.Transaction)
}
network/txpool_test.go
Here's a brief explanation for each test function:
TestTxPool:
Creates a new transaction pool (
tp
) usingNewTxPool
.Asserts that the initial length of the pool is 0.
TestTxPoolAddTx:
Creates a new transaction pool (
tp
) usingNewTxPool
.Adds two new transactions to the pool using
Add
and checks if the length increases accordingly.Flushes the pool using
Flush
and verifies that the length becomes 0 again.
TestTxSort:
Creates a new transaction pool (
tp
) usingNewTxPool
.Adds 1000 new transactions to the pool, each with a unique timestamp set through
SetFirstSeen
.Verifies that the length of the pool matches the expected count.
Retrieves the transactions sorted by their first seen timestamp using
Transactions
.Checks if the transactions are indeed sorted by comparing consecutive timestamps.
package network
import (
"ProjectX/core"
"math/rand"
"strconv"
"testing"
"github.com/stretchr/testify/assert"
)
func TestTxPool(t *testing.T){
tp := NewTxPool()
assert.Equal(t, tp.Len(), 0)
}
func TestTxPoolAddTx(t *testing.T){
tp := NewTxPool()
assert.Equal(t, tp.Len(), 0)
tx := core.NewTransaction([]byte("hello new tx"))
err := tp.Add(tx)
assert.Nil(t,err)
assert.Equal(t,tp.Len(),1)
tx2 := core.NewTransaction([]byte("hello new txxx"))
err2 := tp.Add(tx2)
assert.Nil(t,err2)
assert.Equal(t,tp.Len(),2)
tp.Flush()
assert.Equal(t,tp.Len(),0)
}
func TestTxSort(t *testing.T){
tp := NewTxPool()
end := 1000
for i := 0; i < end; i++ {
newTx := core.NewTransaction([]byte(strconv.FormatInt(int64(i), 10)))
newTx.SetFirstSeen(int64(i * rand.Intn(10000)))
assert.Nil(t, tp.Add(newTx))
}
assert.Equal(t,tp.Len(),end)
sortedTx := tp.Transactions()
assert.Equal(t, len(sortedTx), end)
for i := 0; i < len(sortedTx) - 1; i++ {
assert.True(t, sortedTx[i].FirstSeen() < sortedTx[i + 1].FirstSeen())
}
}
make test
Done!
The following blog post will explore the code related to RPC protocol
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.
Subscribe to my newsletter
Read articles from Siddharth Patel directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Siddharth Patel
Siddharth Patel
I'm Siddharth Patel, a Full Stack Developer and Blockchain Engineer with a proven track record of spearheading innovative SaaS products and web3 development. My extensive portfolio spans across diverse sectors, from blockchain-based tokenized investment platforms to PoS software solutions for restaurants, and from decentralized finance (DeFi) initiatives to comprehensive analytics tools that harness big data for global stock trends. Let's connect and explore how we can innovate together.