2.2.6 Cycle 6 - Setting up Inter-Nodal Communication

Design

Objectives

Now that a lot of the structure of the project has been built up and I have a bit more of an idea where I want to head with the design of the software it's time to start actually putting together the actual networking side of the blockchain. This is arguably the most important part as it was what allows the blockchain to function properly and securely.

Therefore for this cycle the objectives are to setup some kind of basic communication between nodes, which I will be doing through the use of a ping/pong API which will allow nodes to request other nodes to sign some example data (using the cryptography functionality from the last cycle) to check that they are online and owned by the wallet they claimed to be owned by.

Usability Features

  • When a pong request is received, the node should log it in the console.

  • When incorrect data is fed to the route it should not crash the program so as to ensure the program keeps running for as long as possible without user intervention.

  • All other errors within this route should result in soft-errors that are logged to console but do not crash the program.

Key Variables

Variable Name
Use

PingRequest

Holds a public key and message and represents the data received in the request.

PongRequest

Holds the data to be sent in response to a ping request, includes: the key supplied in the ping request; the receiver's public key; the message supplied originally; the signature of the message by the receiver.

Pseudocode

Since all of the objectives in this cycle revolve around the code I will simplify this part of the cycle by introducing the pseudocode all in two sections, the api route which will be requested by other nodes and the request function used by those nodes.

The Api Route

Due to there being so many different ways that http routes can be created based upon the language being used, this pseudocode is a little bit adjusted up to fit my requirements but should hopefully be clear enough that it doesn't matter,

// Psuedocode

// import functions from modules made in previous cycles
IMPORT configuration
IMPORT cryptography


// This is meant to indicate a url of form "http://~~~/pong/[req]" 
// where req is what is fed into this "function"
HTTP_ROUTE pong (req string):

	TRY:
		req_parsed = json.decode(req)
	CATCH ERROR:
		OUTPUT "Incorrect data supplied to /pong/:req"
		RETURN HTTP.code(403) 	
		// a code 403 means "forbidden" in http terms
	END TRY

	// collect the configuration object from that module
	self = configuration.get_config()

	OUTPUT "Received pong request, data supplied:" + req_parsed

	// create an object that represents the response
	response = {
		pong_key: self.pub_key
		ping_key: req_parsed.ping_key
		message: req_parsed.message
		signature: cryptography.sign(self.priv_key, message)
	}
	
	// encode the response to be http safe
	data = json.encode(response)
	// return it to the requester
	RETURN HTTP.text(data)
END HTTP_ROUTE

The Request Function

// Psuedocode

FUNCTION ping(ref, this):
	// ref should be an ip or a domain
	message = "gfhajbsfhka" // should be random or datetime or something
	req = {
		ping_key: this.self.key,
		message: msg
	}

	OUTPUT "Sending ping request to $ref"
	
	// fetch domain, domain should respond with their wallet pub key/address, "pong" and a signed hash of the message
	req_encoded = json.encode(req)
	
	TRY
		raw = http.get("$ref/pong/$req_encoded") or {
			
		}
	CATCH
		OUTPUT "Failed to ping $ref, Node is probably offline."
		RETURN false
	END TRY
	
	TRY
		data = json.decode(PongResponse, raw.body)
	CATCH
		OUTPUT "Failed to decode response, responder is probably using an old node version.\nTheir Response: $raw"
		RETURN false
	
	END TRY

	OUTPUT ref + " responded to ping request with pong" + data

	// signed hash can then be verified using the wallet pub key supplied
	IF (data.message == msg && data.ping_key == this.self.key):
		OUTPUT "Should also verify signature but I haven't implemented DSA yet"
		RETURN true
	END IF

	// if valid, return true, if not return false
	RETURN false
END FUNCTION

Development

Theoretically this cycle shouldn't have been that complicated but there were some weird issues with how V's default web api (V web) works and how data that will be needed on every request should be handled.

This is relevant as for the 'pong' route and a decent quantity of other routes, the keys will be needed to sign/validate different data and as each route request creates a new process to handle it, the keys need to find some way to make their way into these processes without too much inefficiency or overhead.

Outcome

The Api Route

// V - /src/modules/server/main.v

struct App {
	vweb.Context
}

['/pong/:req']
pub fn (mut app App) pong(req string) vweb.Result {
	req_parsed := json.decode(PingRequest, req) or {
		eprintln("Incorrect data supplied to /pong/:req")
		return app.server_error(403)
	}

	this := configuration.get_config()

	println("Received pong request.\n data supplied: $req_parsed \n Raw data supplied $req")

	res := PongResponse{
		pong_key: this.self.key
		ping_key: req_parsed.ping_key
		message: req_parsed.message
		signature: cryptography.sign(this.priv_key, req_parsed.message.bytes())
	}

	data := json.encode(res)
	return app.text(data)
}

The Request Function

// V - /src/modules/server/handshake.v

module server
import configuration
import json
import net.http

// This is to establish a handshake between two nodes and should be done everytime two nodes connect
pub struct PongResponse {
	pong_key []u8
	ping_key []u8
	message string
	signature []u8
}

pub struct PingRequest {
	ping_key []u8
	message string
}

pub fn ping(ref string, this configuration.UserConfig) bool {
	// ref should be an ip or a domain
	msg := "gfhajbsfhka" // should be random or datetime or something
	req := PingRequest{ping_key: this.self.key, message: msg}

	println("Sending ping request to $ref")
	// fetch domain, domain should respond with their wallet pub key/address, "pong" and a signed hash of the message
	req_encoded := json.encode(req)
	raw := http.get("$ref/pong/$req_encoded") or {
		eprintln("Failed to ping $ref, Node is probably offline. Error: $err")
		return false
	}

	data := json.decode(PongResponse, raw.body) or {
		eprintln("Failed to decode response, responder is probably using an old node version.\nTheir Response: $raw")
		return false
	}

	println("$ref responded to ping request with pong $data")

	// signed hash can then be verified using the wallet pub key supplied
	if data.message == msg && data.ping_key == this.self.key {
		println("Should also verify signature but I haven't implemented DSA yet")
		return true
	}

	// if valid, return true, if not return false
	return false
}

Challenges

The main challenge in this cycle, as mentioned in the introductory paragraph was how to get the keys for signing and validating data into the request process.

This was more complicated than I anticipated because although it may appear as if you can feed custom inputs to the app object which is the object that the route exists on (can be seen in the outcome code), all these values are wiped on every new request so the result is just an empty value.

So after trialing that method multiple times and realising the only way to fix that issue would be to clone the V web package, edit the five lines of code that cause this to happen and then recompile it - which wouldn't be too hard but would massively inflate the compile times and amount of code needed to be compiled - I moved onto trialing exporting the configuration as a constant generated on code compilation, but that would give all parts of the program access to the private key which is not great for security and therefore also not an option.

Eventually this detour led to just calling the configuration.get_config() function on each new request, although this does lead to loading from the save file a lot and since most of the configuration object isn't actually needed in most requests, just the keys, this will very likely be changed again in a future cycle but it'll work for now.

[Update: The refactor mentioned in this section is completed as of Cycle 8 - Node Refactor]

Testing

Tests

Test
Instructions
What I expect
What actually happens
Pass/Fail

1

Send a ping request to "https://nano.monochain.network"

The requester to successfully complete the ping request.

As expected

Pass

2

Receive a ping request as "https://nano.monochain.network"

The receiver to successfully receive and process the request.

As expected

Pass

Evidence

Sadly I forgot to collect evidence of the test in action during this stage of development, however the code of which was written in this cycle is still mostly in tact during the current stage of development and just contains more modules that haven't been written at this point.

Therefore the below evidence was generated during Cycle 11 - Remembering Nodes and contains excess outputs but does show the process working.

Evidence generated using newer version of code

Last updated