Archived
2
0

Initial commit

This commit is contained in:
2023-03-02 15:28:43 +01:00
commit 2d4d7759e0
40 changed files with 4249 additions and 0 deletions

167
utils/client.go Normal file
View File

@@ -0,0 +1,167 @@
// Package utils provides utilities that are needed for the functioning of Kerma
package utils
import (
"bufio"
"errors"
"kerma/helpers"
"kerma/models"
"net"
"time"
"github.com/docker/go/canonical/json"
)
/*
A Client represents a Kerma client with the following properties:
- State - the current state of the application
- Handler - a TCPHandler that is used to handle requests to and from remote peers
- Logger
*/
type Client struct {
State *models.State
Handler TCPHandler
Logger helpers.Logger
}
/*
Construct creates a new client with the specified state and handler objects
*/
func (c *Client) Construct(state *models.State, h TCPHandler) {
c.State = state
c.Handler = h
}
/*
InitHandshake initiates a handshake on the specified connection
*/
func (c *Client) InitHandshake(peerName string) {
var resp models.Hello
resp.Construct()
respJSON, _ := json.MarshalCanonical(resp)
c.Handler.Output(respJSON)
c.Handler.Conn.SetDeadline(time.Now().Add(time.Duration(c.State.Config.ConnTimeout) * time.Second))
in, err := bufio.NewReader(c.Handler.Conn).ReadBytes('\n')
if err != nil {
c.Logger.Error(err.Error())
if errors.Is(err, net.ErrClosed) {
c.State.RemovePeerByName(peerName)
} else {
c.Handler.Fail("Failed reading message")
}
return
}
msg, err := c.Handler.Input(in)
if err != nil {
c.Logger.Error(err.Error())
c.Handler.Fail("Failed parsing message")
return
}
msgJSON, err := json.MarshalCanonical(msg)
if err != nil {
c.Logger.Error("Failed parsing message " + err.Error())
c.Handler.Fail("Failed parsing message")
return
}
if msgType, ok := msg["type"]; ok {
if msgType == "hello" {
var helloReq models.Hello
err := helloReq.UnmarshalJSON(msgJSON)
if err == nil {
c.Logger.Info("Constructing handshake with " + c.Handler.GetRemotePeer())
c.State.CompleteHandshake(peerName)
} else {
c.Handler.Fail("Hello response invalid")
return
}
} else {
c.Handler.Fail("Response type not expected " + msgType.(string))
return
}
} else {
c.Handler.Fail("Response type not specified")
return
}
}
/*
DiscoverPeers requests remote peers from a known peer
*/
func (c *Client) DiscoverPeers(peerName string) {
if c.State.CheckForHandshake(peerName) {
var req models.Generic
req.BuildPeerRequest()
reqJSON, _ := req.MarshalJson()
c.Handler.Output(reqJSON)
}
}
/*
DiscoverChainTip requests chaintip from a known peer
*/
func (c *Client) DiscoverChainTip(peerName string) {
if c.State.CheckForHandshake(peerName) {
var req models.Generic
req.BuildChainTipRequest()
reqJSON, _ := req.MarshalJson()
c.Handler.Output(reqJSON)
}
}
/*
DiscoverMempool requests mempool from a known peer
*/
func (c *Client) DiscoverMempool(peerName string) {
if c.State.CheckForHandshake(peerName) {
var req models.Generic
req.BuildMempoolRequest()
reqJSON, _ := req.MarshalJson()
c.Handler.Output(reqJSON)
}
}
/*
DiscoverObject requests remote objects from a known peer
*/
func (c *Client) DiscoverObject(peerName string, objectID string) {
if c.State.CheckForHandshake(peerName) {
var req models.ObjectWrapper
req.BuildObjectRequest(objectID)
reqJSON, _ := req.MarshalJson()
c.Handler.Output(reqJSON)
}
}
/*
GossipBlocks sends information about local blocks to peer
*/
func (c *Client) GossipBlocks(peerName string) {
if c.State.CheckForHandshake(peerName) {
for _, block := range c.State.Chain {
var req models.ObjectWrapper
bid := block.GetID()
req.BuildGossipObject(bid)
reqJSON, _ := req.MarshalJson()
c.Handler.Output(reqJSON)
}
}
}
/*
GossipTransactions sends information about local transactions to peer
*/
func (c *Client) GossipTransactions(peerName string) {
if c.State.CheckForHandshake(peerName) {
for txid := range c.State.Transactions {
var req models.ObjectWrapper
req.BuildGossipObject(txid)
reqJSON, _ := req.MarshalJson()
c.Handler.Output(reqJSON)
}
}
}

89
utils/handler.go Normal file
View File

@@ -0,0 +1,89 @@
// Package utils provides utilities that are needed for the functioning of Kerma
package utils
import (
"errors"
"kerma/helpers"
"kerma/models"
"net"
"strconv"
"github.com/docker/go/canonical/json"
)
/*
A TCPHandler is a struct used for communicating with other Kerma nodes/clients
It consists of:
- Conn - a connection for sending and receiving requests
- Logger
- Config - the configuration of the application
*/
type TCPHandler struct {
Conn net.Conn
Logger helpers.Logger
Config helpers.Config
}
/*
Construct creates a new handler on the specified connection
*/
func (h *TCPHandler) Construct(conn net.Conn) {
h.Conn = conn
h.Config.Construct()
}
/*
Output handles outgoing data in a canonical JSON format
*/
func (h *TCPHandler) Output(respJSON []byte) {
h.Logger.Info("OUT: " + h.GetRemotePeer() + " " + string(respJSON))
respJSON = append(respJSON, '\n')
h.Conn.Write(respJSON)
}
/*
Error handles outgoing errors in a canonical JSON format
*/
func (h *TCPHandler) Error(errorMsg string) {
var resp models.Error
resp.Construct(errorMsg)
respJSON, _ := resp.MarshalJson()
h.Output(respJSON)
}
/*
Fail handles outgoing errors in a canonical JSON format and terminates the connection
*/
func (h *TCPHandler) Fail(errorMsg string) {
h.Error(errorMsg)
h.Conn.Close()
}
/*
Input handles input data in a canonical JSON format
*/
func (h *TCPHandler) Input(in []byte) (map[string](interface{}), error) {
var msg map[string](interface{})
err := json.Unmarshal(in, &msg)
if err != nil {
h.Logger.Error("Failed decoding message " + err.Error())
return nil, errors.New("failed decoding message")
}
return msg, nil
}
/*
GetRemotePeer returns tha address of the remote peer as a string
*/
func (h *TCPHandler) GetRemotePeer() string {
return h.Conn.RemoteAddr().String()
}
/*
GetLocalPeer returns the address + port combination of the local peer as a string
*/
func (h *TCPHandler) GetLocalPeer() string {
return h.Config.IPAddress + ":" + strconv.Itoa(h.Config.Port)
}

352
utils/server.go Normal file
View File

@@ -0,0 +1,352 @@
// Package utils provides utilities that are needed for the functioning of Kerma
package utils
import (
"bufio"
"errors"
"kerma/helpers"
"kerma/models"
"time"
"github.com/docker/go/canonical/json"
)
/*
A Server represents a Kerma server with the following properties:
- State - the current state of the application
- Handler - a TCPHandler that is used to handle requests to and from remote peers
- Logger
- Client - a new client that uses the same state and handler as this server
*/
type Server struct {
State *models.State
Handler TCPHandler
Logger helpers.Logger
Client Client
}
/*
Construct creates a new server with the specified state and handler objects
*/
func (s *Server) Construct(state *models.State, h TCPHandler) {
s.State = state
s.Handler = h
s.Client.Construct(s.State, s.Handler)
}
/*
Handle is responsible for handling requests from other Kerma nodes/clients
*/
func (s *Server) Handle() {
// Decode message received from reader, fail on error
scanner := bufio.NewScanner(s.Handler.Conn)
for scanner.Scan() {
msg, err := s.Handler.Input(scanner.Bytes())
if err != nil {
s.Handler.Fail(err.Error())
}
// Parse received message, fail on error
msgJSON, err := json.MarshalCanonical(msg)
if err != nil {
s.Logger.Error("Failed parsing message " + err.Error())
s.Handler.Fail("Failed parsing message")
}
s.Logger.Info("IN: " + s.Handler.GetRemotePeer() + " " + string(msgJSON))
remotePeer := s.Handler.GetRemotePeer()
// check message type, proceed accordingly
if msgType, ok := msg["type"]; ok {
switch msgType {
case "error":
break
case "hello":
// check if peer has already handshaked
if !s.State.CheckForHandshake(remotePeer) {
// check if hello request is valid
var helloReq models.Hello
err := helloReq.UnmarshalJSON(msgJSON)
if err == nil {
// finish handshake
s.Logger.Info("Constructing handshake with " + remotePeer)
var resp models.Hello
resp.Construct()
respJSON, _ := resp.MarshalJson()
s.Handler.Output(respJSON)
s.State.CompleteHandshake(remotePeer)
s.Client.DiscoverPeers(remotePeer)
} else {
s.Handler.Fail("Hello request invalid")
}
}
break
case "getpeers":
err := s.terminateIfNoHandshake(remotePeer)
if err != nil {
break
}
// create list of known peers, send to requester
resp := s.State.FetchPeerListResponse()
resp.Peers = append(resp.Peers, s.Handler.GetLocalPeer())
respJSON, _ := resp.MarshalJson()
s.Handler.Output(respJSON)
break
case "peers":
err := s.terminateIfNoHandshake(remotePeer)
if err != nil {
break
}
var peers models.PeerListResponse
json.Unmarshal(msgJSON, &peers)
s.State.ParsePeerListResponse(&peers)
break
case "ihaveobject":
err := s.terminateIfNoHandshake(remotePeer)
if err != nil {
break
}
var obj models.ObjectWrapper
err = obj.UnmarshalJSON(msgJSON)
if err != nil {
s.Handler.Fail("Could not parse request")
break
}
if s.State.Transactions[obj.ObjectID] == nil {
resp := models.ObjectWrapper{
Type: "getobject",
ObjectID: obj.ObjectID,
}
respJSON, _ := resp.MarshalJson()
s.Handler.Output(respJSON)
}
case "getobject":
err = s.terminateIfNoHandshake(remotePeer)
if err != nil {
break
}
var obj models.ObjectWrapper
err := obj.UnmarshalJSON(msgJSON)
if err != nil {
s.Handler.Fail("Could not parse request")
break
}
transaction := s.State.GetTransaction(obj.ObjectID)
if transaction != nil {
s.Logger.Debug("Found transaction: " + obj.ObjectID)
resp := json.RawMessage(`{"type":"object","object":` + transaction.String() + `}`)
respJSON, _ := resp.MarshalJSON()
s.Handler.Output(respJSON)
}
block := s.State.GetBlock(obj.ObjectID)
if block != nil {
s.Logger.Debug("Found block: " + obj.ObjectID)
resp := json.RawMessage(`{"type":"object","object":` + block.String() + `}`)
respJSON, _ := resp.MarshalJSON()
s.Handler.Output(respJSON)
} else {
s.Handler.Error("Could not find object " + obj.ObjectID)
}
break
case "object":
err = s.terminateIfNoHandshake(remotePeer)
if err != nil {
break
}
var object models.Object
err := object.UnmarshalJSON(msgJSON)
if err != nil {
s.Logger.Debug("Could not unmarshal: " + err.Error())
s.Handler.Fail("Object request invalid")
break
}
val, err := object.GetObjectValue()
if err != nil {
s.Logger.Error("Could not parse object: " + err.Error())
s.Handler.Fail("Could not parse object request")
break
}
switch val.GetType() {
case "transaction":
err := s.State.AppendTransaction(val.GetEntity().(*models.Transaction))
if err != nil {
s.Logger.Debug("Failed appending transaction: " + err.Error())
s.Handler.Fail("Error appending transaction")
break
}
break
case "block":
block := val.(*models.Block)
go s.appendBlockToChain(block)
break
default:
s.Logger.Debug("Something went wrong with " + string(msgJSON))
s.Handler.Fail("Could not parse object request")
}
break
case "getchaintip":
err := s.terminateIfNoHandshake(remotePeer)
if err != nil {
break
}
res := s.State.GetChainTip()
var chaintip models.Chaintip
chaintip.Construct(res.GetID())
respJSON, _ := chaintip.MarshalJson()
s.Handler.Output(respJSON)
break
case "chaintip":
err := s.terminateIfNoHandshake(remotePeer)
if err != nil {
break
}
var chaintip models.Chaintip
err = chaintip.UnmarshalJSON(msgJSON)
if err != nil {
s.Logger.Debug("Could not unmarshal: " + err.Error())
s.Handler.Fail("Object request invalid")
break
}
res := s.State.GetChainTip()
if res.GetID() != chaintip.BlockID {
s.Client.DiscoverObject(remotePeer, chaintip.BlockID)
}
break
case "getmempool":
err := s.terminateIfNoHandshake(remotePeer)
if err != nil {
break
}
res := s.State.GetMempoolTransactionIDs()
var mempool models.Mempool
mempool.Construct(res)
respJSON, _ := mempool.MarshalJson()
s.Handler.Output(respJSON)
break
case "mempool":
err := s.terminateIfNoHandshake(remotePeer)
if err != nil {
break
}
var mempool models.Mempool
err = mempool.UnmarshalJSON(msgJSON)
if err != nil {
s.Logger.Debug("Could not unmarshal: " + err.Error())
s.Handler.Fail("Object request invalid")
break
}
for _, txid := range mempool.Txids {
res := s.State.GetTransaction(txid)
if res == nil {
s.Client.DiscoverObject(remotePeer, txid)
}
}
break
default:
s.Handler.Fail("Request type not supported " + msgType.(string))
}
} else {
s.Handler.Fail("Request type not specified")
}
}
}
func (s *Server) terminateIfNoHandshake(remotePeer string) error {
if !s.State.CheckForHandshake(remotePeer) {
s.Handler.Fail("Handshake not completed")
return errors.New("handshake not completed with peer " + remotePeer)
}
return nil
}
func (s *Server) appendBlockToChain(block *models.Block) {
dontAppend := false
missingTx, err := s.State.GetMissingTransactionsInBlock(block)
if err != nil {
s.Logger.Debug("Failed appending block: " + err.Error())
s.Handler.Fail("Error appending block")
return
}
if len(missingTx) != 0 {
for _, txid := range missingTx {
s.Client.DiscoverObject(s.Handler.GetRemotePeer(), txid)
waited := 0
for s.State.GetTransaction(txid) == nil {
s.Logger.Debug("Waiting for transactions " + txid)
time.Sleep(500 * time.Millisecond)
waited += 500
if waited*int(time.Millisecond) >= s.State.Config.ConnTimeout*int(time.Second) {
s.Handler.Fail("Did not receive requested transactions in time, failed appending block")
dontAppend = true
return
}
}
}
}
s.getMissingBlocks(block)
if !dontAppend {
err = s.State.AppendToChain(block)
if err != nil {
s.Logger.Debug("Failed appending block: " + err.Error())
s.Handler.Fail("Error appending block")
}
s.State.DumpBlockStore()
for _, peer := range s.State.PeerList {
if peer.Active {
s.Client.GossipBlocks(peer.Name)
}
}
}
}
func (s *Server) getMissingBlocks(block *models.Block) {
blockId := *block.Previd
if s.State.GetBlock(blockId) != nil {
return
}
s.Logger.Debug("Block missing from chain: " + blockId)
for _, peer := range s.State.PeerList {
s.Client.DiscoverObject(peer.Name, blockId)
}
for s.State.GetBlock(blockId) == nil {
s.Logger.Debug("Waiting for block " + blockId)
time.Sleep(500 * time.Millisecond)
}
}

View File

@@ -0,0 +1,38 @@
package tests
import (
"kerma/helpers"
"kerma/models"
"kerma/utils"
"net"
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestClient(t *testing.T) {
host := "127.0.0.1:1338"
net.Listen("tcp", host)
conn, _ := net.Dial("tcp", host)
defer conn.Close()
var config helpers.Config
config.Construct()
var handler utils.TCPHandler
handler.Construct(conn)
var state models.State
state.Construct()
Convey("Given a new client", t, func() {
var client utils.Client
client.Construct(&state, handler)
Convey("When Construct is called, a new object is returned", func() {
So(client.State, ShouldResemble, &state)
So(client.Handler, ShouldResemble, handler)
})
})
}

View File

@@ -0,0 +1,51 @@
package tests
import (
"bufio"
"kerma/helpers"
"kerma/utils"
"net"
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestHandler(t *testing.T) {
host := "127.0.0.1:1337"
net.Listen("tcp", host)
conn, _ := net.Dial("tcp", host)
scanner := bufio.NewScanner(conn)
var config helpers.Config
config.Construct()
Convey("Given a new handler", t, func() {
var handler utils.TCPHandler
handler.Construct(conn)
Convey("When Construct is called, a new object is returned", func() {
So(handler.Conn, ShouldResemble, conn)
So(handler.Config, ShouldResemble, config)
})
Convey("When Output is called, a JSON string should be returned on the connection", func() {
jsonString := `{"foo": "bar"}`
handler.Output([]byte(jsonString))
resp := string(scanner.Bytes())
So(resp, ShouldEqual, resp)
})
Convey("When Input is called, a JSON string and no error should be obtained from the connection", func() {
jsonString := `{"foo": "bar"}`
resp, err := handler.Input([]byte(jsonString))
So(resp, ShouldEqual, resp)
So(err, ShouldBeNil)
})
Convey("When GetRemotePeer is called, the local connection should be returned", func() {
So(handler.GetRemotePeer(), ShouldEqual, host)
})
})
}