The Business & Technology Network
Helping Business Interpret and Use Technology
«  
  »
S M T W T F S
 
1
 
2
 
3
 
4
 
5
 
6
 
7
 
8
 
9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Revolutionizing Digital Marketing

DATE POSTED:February 7, 2024
Creating a Blockchain: Part 8 — Creating BlockModifications

We modified some of the code so later, it will not be hurdle to work on this while working on important functionality and also modifications are made in a way that it’ll be memory efficient aswell.

Transaction memory efficiency

If you notice, we’ve returned Transaction in many Transaction related functions instead of returning *Transaction. We made it *Transaction in most function with following code modifications.

core/transaction_test.go

func randomTxWithSignature(t *testing.T) *Transaction {
privKey := crypto.GeneratePrivateKey()
tx := Transaction{
Data: []byte("tx data"),
}
assert.Nil(t, tx.Sign(privKey));
return &tx;
}

core/block.go

func NewBlock(h *Header, txx []*Transaction) (*Block, error) {
return &Block{
Header : h,
Transactions: txx,
},nil
}Logger Replacement

We were using logrus so far, but instead we want to use following logger

install it in go project,

go get github.com/go-kit/log

use it as follows,

network/server.go

package network
import (
"github.com/go-kit/log"
)
type ServerOpts struct {
// other
Logger log.Logger
ID string
}
type Server struct {
//prop
}
func NewServer(opts ServerOpts) (*Server, error) {
if opts.Logger == nil {
opts.Logger = log.NewLogfmtLogger(os.Stderr)
opts.Logger = log.With(opts.Logger, "ID", opts.ID)
}
//other code
//return s, nil
}
func (s *Server) processTransaction(tx *core.Transaction) error {
// use logger like this..
s.Logger.Log("msg","adding new tx to the mempool","hash",hash,"mempool length", s.memPool.Len())
//other code
}

core/blockchain.go

Use logger in blockchain aswell.

type Blockchain struct {
logger log.Logger
//other
}
func NewBlockchain(logger log.Logger,genesis *Block) (*Blockchain, error) {
bc := &Blockchain{
store : NewMemoryStore(),
headers : []*Header{},
logger : logger,
}
bc.validator = NewBlockValidator(bc)
bc.addBlockWithoutValidation(genesis)
return bc, nil
}
func (bc *Blockchain) addBlockWithoutValidation(b *Block) error {
// use logger like this,
bc.logger.Log("msg", "adding new block","height", b.Height, "hash", b.Hash(BlockHasher{}))
// other code
}Validator Loop

As of now, server is adding block to blockchain every 5 seconds (default ticker). but we are doing it in same function which other rpc channels are using. which shown here,

free:
for {
select {
case rpc := <-s.rpcChan:
decodedMessage, err := s.RPCDecodeFunc(rpc)
if err != nil {
logrus.Error(err)
}
if err := s.RPCProcessor.ProcessMessage(decodedMessage); err!=nil {
logrus.Error(err)
}
case <-s.quitChan:
break free
case <-ticker.C:
if s.isValidator {
s.createNewBlock()
}
}
}

In ideal case, if server is validator then as per decided blockTime createNewBlock function triggers and create the block in blockchain but here’s the issue comes, what if any rpc channel send any message, in that case instead of creating block, server is utilizing it’s time in processing that message which can cause issue in creating blocks with not any sync with blockTime.

So, we are hearing ticker channel and creating block outside of the other rpc channels processor function.

network/server.go

func NewServer(opts ServerOpts) (*Server, error) {
// ... other code
if s.isValidator {
go s.validatorLoop()
}
return s,nil
}
func (s *Server) validatorLoop() {
ticker := time.NewTicker(s.blockTime)
s.Logger.Log("msg", "Starting validator loop", "blocktime", s.blockTime)
for {
<- ticker.C
s.createNewBlock()
}
}Create new block

After this, server will start creating and adding block to blockchain if it is validator(private key provided to server) in blocktime interval.

Calculate Datahash

calulating datahash is important as we are creating blocks and in block, header contains Datahash which is indeed hash of all transaction.

The CalculateDataHash function takes an array of transactions (txx), encodes each transaction using a gob encoder, concatenates the encoded transaction data, and computes the SHA-256 hash of the concatenated data. The resulting hash and any encountered errors during the encoding process are returned.

core/block.go

func CalculateDataHash(txx []*Transaction) (hash types.Hash, err error) {
buf := &bytes.Buffer{}
for _, tx := range txx {
if err = tx.Encode(NewGobTxEncoder(buf)); err != nil{
return
}
}
hash = sha256.Sum256(buf.Bytes())
return
}New block from previous header

We need function which creates new block from previous header, which will be utilised by server at the time of creating block

The NewBlockFromPrevHeader function creates a new block based on the provided previous block header (prevHeader) and an array of transactions (txx). It calculates the hash of the concatenated transaction data using CalculateDataHash, and then constructs a new block header with the same version, the calculated data hash, the hash of the previous block, a timestamp, and an incremented height. Finally, it calls the NewBlock constructor with the newly created header and the array of transactions to generate a complete block, which is returned along with any encountered errors during the process.

core/block.go

func NewBlockFromPrevHeader(prevHeader *Header, txx []*Transaction) (*Block, error) {
dataHash,err := CalculateDataHash(txx)
if err != nil {
return nil, err
}
header := &Header{
Version: prevHeader.Version,
DataHash: dataHash,
PrevBlockHash: BlockHasher{}.Hash(prevHeader),
Timestamp: time.Now().UnixNano(),
Height: prevHeader.Height + 1,
}
return NewBlock(header, txx)
}Add blockchain to server

To connect server with blockchain, we need to add it,

No need to explain in this as code is self explanatory and not complex at all.

network/server.go

type Server struct {
// other properties
chain *core.Blockchain
}
func NewServer(opts ServerOpts) (*Server, error) {
// other code
chain, err := core.NewBlockchain( opts.Logger,genesisBlock())
if err != nil {
return nil, err
}
//other code
}
func genesisBlock() *core.Block {
h := &core.Header{
Version:1,
DataHash: types.Hash{},
Timestamp: 00000,
PrevBlockHash:types.Hash{},
Height:0,
}
b,_ := core.NewBlock(h, nil)
return b
}Create and Add block to blockchain

So far, our server is listening transactions and adding it to it’s mempool. Now, We are implementing functionality of taking transactions from the mempool and create block out of it and add it to blockchain when you are validator and your turn comes up.

Basically, mempool would have been full with transaction as it listens transaction from it’s many peers and also someone could submit transaction directly to server as well. So server selects some of the transaction and create block out of it and removes that selected transactions from mempool but we are not having that much traffic right now and for sake of simplicity we will create block of all transactions in available in mempool and will flush mempool when block is getting created.

network/server.go

func (s *Server) createNewBlock() error {
header,err := s.chain.GetHeader(s.chain.Height())
if err != nil{
return err
}
txx := s.memPool.Transactions()
block, err := core.NewBlockFromPrevHeader(header,txx)
if err != nil{
return err
}
if err := block.Sign(*s.PrivateKey); err != nil{
return err
}
if err := s.chain.AddBlock(block); err != nil{
return err
}
s.memPool.Flush()
return nil
}

The createNewBlock method is responsible for generating and adding a new block to the blockchain. Here's a brief explanation:

  • Retrieves the header of the latest block in the blockchain using s.chain.GetHeader(s.chain.Height()).
  • Retrieves the transactions from the server’s transaction pool (s.memPool.Transactions()).
  • Calls core.NewBlockFromPrevHeader to create a new block with the retrieved header and transactions.
  • Signs the newly created block using the server’s private key (s.PrivateKey).
  • Adds the signed block to the blockchain using s.chain.AddBlock(block).
  • Flushes the server’s transaction pool (s.memPool.Flush()).
Affected Changes

As there’s many changes in block creation process including change in randomTxWithSignature function, change in NewBlock function also DataHash method, there is slightly modification in following functions

core/block_test.go

func randomBlock(t *testing.T, height uint32, prevBlockHash types.Hash) *Block {
privKey := crypto.GeneratePrivateKey()
tx := randomTxWithSignature(t)
header := &Header{
Version: 1,
PrevBlockHash: prevBlockHash,
Timestamp: time.Now().UnixNano(),
Height: height,
}
b, err := NewBlock(header, []*Transaction{tx})
assert.Nil(t, err)
dataHash,err := CalculateDataHash([]*Transaction{tx})
assert.Nil(t, err)
header.DataHash = dataHash
assert.Nil(t, b.Sign(privKey))
return b
}

core/validator.go

added validation of Datahash

func (v *BlockValidator) ValidateBlock(b *Block) error {
// other code
dataHash,err := CalculateDataHash(b.Transactions)
if err != nil {
return err
}
if dataHash != b.DataHash {
return fmt.Errorf("block %s has invalid datahash", b.Hash(BlockHasher{}))
}
if err := b.Verify(); err != nil {
return err
}
return nil
}Multiple server testing

We are testing our block creation functionality with multiple servers,
in brief:

initRemoteServersfunction:

  • Initializes remote servers using the provided array of transports.
  • For each transport in the array, creates a server (makeServer) with a unique ID and the respective transport.
  • Runs each remote server in a separate goroutine using go s.Start().

makeServerfunction:

  • Creates a new network.Server instance with the specified ID, transport, and private key.
  • Handles any errors that occur during the server creation.
  • Returns the created server.

mainfunction:

  • Sets up local and remote transports (trLocal, trRemoteA, trRemoteB, trRemoteC).
  • Establishes connections between the transports to simulate a network topology.
  • Initializes remote servers (trRemoteA, trRemoteB, trRemoteC) using initRemoteServers.
  • Runs a goroutine to periodically send transactions from trRemoteA to trLocal.
  • Generates a private key, creates and starts a local server (localServer) using makeServer, and starts the server.

main.go

func main() {
trLocal := network.NewLocalTransport("LOCAL")
trRemoteA := network.NewLocalTransport("REMOTE_A")
trRemoteB := network.NewLocalTransport("REMOTE_B")
trRemoteC := network.NewLocalTransport("REMOTE_C")
trLocal.Connect(trRemoteA)
trRemoteA.Connect(trRemoteB)
trRemoteB.Connect(trRemoteC)
trRemoteA.Connect(trLocal)

initRemoteServers([]network.Transport{trRemoteA, trRemoteB, trRemoteC})
go func() {
for {
if err := sendTransaction(trRemoteA, trLocal.Addr()); err != nil {
logrus.Error(err)
}
time.Sleep(1 * time.Second)
}
}()
privKey := crypto.GeneratePrivateKey()
localServer := makeServer("LOCAL", trLocal, &privKey)
localServer.Start()
}
func initRemoteServers(transports []network.Transport){
for i, tr := range transports {
id := fmt.Sprintf("REMOTE_%d",i)
s := makeServer(id,tr, nil)
go s.Start()
}
}
func makeServer(id string, tr network.Transport, privKey *crypto.PrivateKey) *network.Server {
opts := network.ServerOpts{
Transports: []network.Transport{tr},
PrivateKey : privKey,
ID : id,
}
s, err := network.NewServer(opts)
if err != nil {
logrus.Error(err)
}
return s
}

In summary, the main function sets up a simulated network with local and remote transports, initializes remote servers, and runs a local server. The initRemoteServers function initializes remote servers with unique IDs and associated transports. The makeServer function creates a new server instance with the given parameters. Together, these functions set up a basic blockchain network simulation with local and remote nodes.

Let’s test it,

make run

Here, we go,

Working fine as per our code but still there are many things to work on like, transactions are not flushing from mempool of servers other than validator and also block broadcasting

The following blogpost will explore the code related to Broadcast of Block

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.

Creating a Blockchain: Part 8 — Creating Block was originally published in Coinmonks on Medium, where people are continuing the conversation by highlighting and responding to this story.