Creating a Blockchain: Part 1 — Transport layer

Siddharth Patel
Coinmonks
4 min readNov 17, 2023

--

Transport layer

Project setup

ProjectX is the name of our project

We’ll set up the whole architecture here and will change it as per requirements. So, As of now, we will create a Makefile in the root directory(ProjectX) for running scripts

Makefile

build:
go build -o ./bin/projectx
run: build
./bin/projectx
test:
go test -v ./...

File and directory structure looks like this,

We are focusing on a transport layer, which will handle the node connections in the network

Creating network module

go mod network

Transport layer structure

Creating a Transport Interface with needed properties, which will help to derive further networks
Transport interface contains 4 methods:

  1. Consume: returns RPC channel (for internal use)
  2. Connect: to connect 1 peer to another
  3. SendMessage: message to another peer using RPC channel
  4. Addr: returns address

network/transport.go

package network
type NetAddr stringtype RPC struct {
From NetAddr
Payload []byte
}
type Transport interface {
Consume() <-chan RPC
Connect(Transport) error
SendMessage(NetAddr, []byte) error
Addr() NetAddr
}

Local transport implementation

Implementing local_transport network,

network/local_transport.go

package network

import (
"fmt"
"sync"
)

type LocalTransport struct {
addr NetAddr
consumeCh chan RPC
lock sync.RWMutex
peers map[NetAddr]*LocalTransport
}

func NewLocalTransport(addr NetAddr) Transport {
return &LocalTransport{
addr: addr,
consumeCh: make(chan RPC, 1024),
peers: make(map[NetAddr]*LocalTransport),
}
}

Defining needed methods for LocalTransport to implement Transport interface

func (t *LocalTransport) Consume() <-chan RPC {
return t.consumeCh
}

func (t *LocalTransport) Connect(tr Transport) error {
t.lock.Lock()
defer t.lock.Unlock()
t.peers[tr.Addr()] = tr.(*LocalTransport)
return nil
}

func (t *LocalTransport) SendMessage(to NetAddr, message []byte) error {
t.lock.RLock()
defer t.lock.RUnlock()
peer, ok := t.peers[to]
if !ok {
return fmt.Errorf("%v failed to send message to %v", t.addr, to)
}
peer.consumeCh <- RPC{
From: t.addr,
Payload: message,
}
return nil
}

func (t *LocalTransport) Addr() NetAddr {
return t.addr
}

Test transport connection

To check, whether it’s running or not, we are coding the test file

network/local_transport_test.go

package network

import (
"testing"
"github.com/stretchr/testify/assert"
)

func TestConnect(t *testing.T) {
tra := NewLocalTransport("A").(*LocalTransport)
trb := NewLocalTransport("B").(*LocalTransport)
tra.Connect(trb)
trb.Connect(tra)
assert.Equal(t, tra.peers[trb.addr], trb)
assert.Equal(t, trb.peers[tra.addr], tra)
}

func TestSendMessage(t *testing.T) {
tra := NewLocalTransport("A").(*LocalTransport)
trb := NewLocalTransport("B").(*LocalTransport)
tra.Connect(trb)
trb.Connect(tra)
message := []byte("hello sid!!")
assert.Nil(t, tra.SendMessage(trb.addr, message))
rpc := <-trb.Consume()
assert.Equal(t, rpc.Payload, message)
assert.Equal(t, rpc.From, tra.addr)
}

run it by,

make test

Server and Logs

Creating Server to contain multiple transports and which also handles other server level activities as well.

Server methods:

  1. NewServer: creates server
  2. Start: it starts all server service and initialize all transports by calling initTransport method and logs the all RPC messages
  3. initTransport: It pipelines all transport layer RPC messages to server level channel

network/server.go

package network

import (
"fmt"
"time"
)

type ServerOpts struct {
Transports []Transport
}

type Server struct {
ServerOpts
rpcChan chan RPC
quitChan chan struct{}
}

func NewServer(opts ServerOpts) *Server {
return &Server{
ServerOpts: opts,
rpcChan: make(chan RPC),
quitChan: make(chan struct{}, 1),
}
}

func (s *Server) Start() {
s.initTransports()
ticker := time.NewTicker(5 * time.Second)
free:
for {
select {
case rpc := <-s.rpcChan:
fmt.Println(rpc)
case <-s.quitChan:
break free
case <-ticker.C:
fmt.Println("Every 5 seconds")
}
}
fmt.Println("Server shut down")
}

func (s *Server) initTransports() {
for _, tr := range s.Transports {
go func(tr Transport) {
for rpc := range tr.Consume() {
s.rpcChan <- rpc
}
}(tr)
}
}

Send Message (p2p)

Testing all at once from main package,

main.go

package main

import (
"ProjectX/network"
"fmt"
"time"
)

func main() {
trLocal := network.NewLocalTransport("LOCAL")
trRemote := network.NewLocalTransport("REMOTE")
trLocal.Connect(trRemote)
trRemote.Connect(trLocal)
go func() {
for {
msg := []byte("Hello Local")
trRemote.SendMessage(trLocal.Addr(), msg)
time.Sleep(1 * time.Second)
}
}()
opts := network.ServerOpts{
Transports: []network.Transport{trLocal},
}
s := network.NewServer(opts)
s.Start()
}

Expected output: remote transport send message to local transport every 1 second which will be logged by server, also it will log reminder at every 5 seconds

and here we go,

make run

As expected!!!😉

The following blog post will explore the code related to blocks and transactions.✨

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.

--

--

Siddharth Patel
Coinmonks

I'm Siddharth Patel, Blockchain Engineer and a Full Stack Developer with a proven track record of spearheading innovative SaaS products and web3 development.