2.2.12 Cycle 12 - Sending messages across the network
Design
Objectives
Since the project is getting near the end of the time period allowed for development and the validation/transaction part of the blockchain doesn't exist outside of the theoretical space yet, but the majority communication layer is nearly finished I think it would be a good idea to get general communication working such that by the end of the allowed period of development the project exists in a form that is somewhat usable.
This form would effectively end up being a decentralised messaging service, which although is not quite the full blockchain that I was aiming for in the first place, still utilises a lot of the same networking and logic - due to effectively being a demo of the decentralised communication layer.
To do this, I will need to introduce a few new features that will build upon what I have created in previous cycles:
With these features the messages being sent will be very similar to the transactions that were initially hypothesised in the analysis of this project, however instead of being sent in bulk during block creation, will instead be sent individually whenever nodes feel like it.
This will then allow for the adaption of these messages into blocks in a future cycle if there is enough time to continue on this path, meaning that in any form this is not a detour of the theorised final project, just a way to create a demo that uses everything that has been created so far.
Usability Features
Receiving messages - Only store valid messages so as to prevent the waste of storage on a user's computer that comes with storing invalid messages.
Logging - Continuous logging should be made while the program operates so that the user can check what is going on with their node by checking the terminal output/logs.
Key Variables
Broadcast_Message_Contents
This object holds a message received or created for broadcasting, it holds all necessary data such as the sender, timestamp, contents, etc. It's useful for ensuring that messages broadcasted across the message are all consistently typed.
Broadcast_Message
This object wraps the Broadcast_Message_Contents
object and contains a signature that confirms that the sender did in fact send and approve a message.
Pseudocode
The API endpoint
This is the endpoint at which other nodes will send their own messages to be broadcasted onwards, although it also includes some logic to ensure that the messages received are indeed valid before the node will send them onwards.
// internal imports
IMPORT database
IMPORT cryptography
IMPORT configuration
// external imports
IMPORT json
IMPORT vweb
IMPORT time
IMPORT net.http
//objects
OBJECT Broadcast_Message_Contents {
sender
receiver
data
time
}
OBJECT Broadcast_Message {
message
signature
}
EXTENT OBJECT Web_Server:
// setup the api route as using the POST http method.
FUNCTION broadcast_route(this) METHOD POST:
db := this.db
body := this.req.data
decoded := {}
TRY:
decoded = json.decode(Broadcast_Message, body)
CATCH:
OUTPUT "[Broadcaster] Message received that cannot be decoded: $body"
RETURN app.server_error(403)
END TRY
valid_message = cryptography.verify(decoded.message.sender, json.encode(decoded.message).bytes(), decoded.signature)
IF (valid_message):
OUTPUT "[Broadcaster] Message received from $decoded.message.sender is valid, checking if seen before..."
// check if message has been recieved before
parsed_signature = decoded.signature.str()
parsed_sender = decoded.message.sender.str()
parsed_receiver = decoded.message.receiver.str()
// check if message has been recieved before
message_seen_before = db.get_message(parsed_signature, parsed_sender, parsed_receiver, decoded.message.time, decoded.message.data).length > 0
IF (!message_seen_before):
OUTPUT "[Broadcaster] Have not seen message before.\n[Broadcaster] Saving message to database."
message_db = {
timestamp: decoded.message.time
contents: decoded.message.data
sender: parsed_sender
receiver: parsed_receiver
signature: parsed_signature
}
// save to database/file system/etc
SAVE message_db
OUTPUT "[Database] Saved message to database."
OUTPUT "\n[Broadcaster] Received message:\n[Broadcaster] Sender: $decoded.message.sender\n[Broadcaster] Sent at: $decoded.message.time\n[Broadcaster] Message: $decoded.message.data\n"
forward_to_all(db, decoded)
RETURN this.ok("Message received and forwarded.")
END IF
OUTPUT "[Broadcaster] Have seen message before."
RETURN this.ok("Message received but not forwarded.")
ELSE:
OUTPUT "[Broadcaster] Received an invalid message"
RETURN this.server_error(403)
END IF
OUTPUT "[Broadcaster] Shouldn't have reached this part of the code - please report as a bug."
RETURN app.server_error(403)
END FUNCTION
END OBJECT EXTEND
Functionality for internal use
These are the functions that will allow the node to handle sending messages internally and these functions will be called by other sections - such as the dashboard page also being developed in this cycle.
FUNCTION forward_to_all(db, msg):
OUTPUT "[Broadcaster] Sending message to all known nodes."
// get all known nodes - file/db/etc
refs = GET ALL node_references
FOR (ref IN refs):
// start the send function on a new thread
GO send(ref.domain, ref.ws, msg)
END FOR
OUTPUT "[Broadcaster] Sent message to all known nodes."
END FUNCTION
FUNCTION send(ref string, ws bool, msg Broadcast_Message) bool {
println("[Broadcaster] Attempting to send message to $ref")
IF (ws) {
OUTPUT "[Broadcaster] Websockets are not implemented yet, cannot send message."
RETURN false
END IF
raw_response = {}
TRY:
raw_response = http.post("$ref/broadcast", json.encode(msg))
CATCH:
OUTPUT "[Broadcaster] Failed to send a message to $ref, Node is probably offline. Error: $err"
RETURN false
END TRY
IF (raw_response.status_code != 200):
OUTPUT "[Broadcaster] $ref responded to message with an error. Code: $raw_response.status_code"
RETURN false
END IF
OUTPUT "[Broadcaster] Successfully Sent message to $ref"
return true
END FUNCTION
FUNCTION broadcast_message(db database.DatabaseConnection, data string){
send_message(db, data, "".bytes())
END FUNCTION
FUNCTION send_message(db, data, receiver):
OUTPUT "[Broadcaster] Assembling message with data: $data"
config = configuration.get_config()
keys = cryptography.get_keys(config.key_path)
contents = Broadcast_Message_Contents{
sender: config.self.key
receiver: receiver
time: time.now().str()
data: data
}
message = Broadcast_Message{
signature: keys.sign(json.encode(contents).bytes())
message: contents
}
OUTPUT "[Database] Saving message to database."
db_msg = {
timestamp: contents.time
sender: contents.sender.str()
receiver: contents.receiver.str()
contents: contents.data
signature: message.signature.str()
}
// save to a file/database/etc
SAVE db_msg
OUTPUT "[Database] Message saved."
OUTPUT "[Broadcaster] Message assembled, broadcasting to refs..."
forward_to_all(db, message)
END FUNCTION
Development
An addition that I ended up creating for this cycle that wasn't originally planned but was very useful to test and use the functionality created was a basic html page that was hosted on the http server to allow a user to send and view messages in an easy to use, simple web page.
Paired with all the new broadcasting functionality that allows messages to be shared across the network and the development for this cycle ends up being a lot more substantial than the pseudocode would make it appear, hence the outcome section simply includes two relevant files in their entirety as they were both created during this cycle and both contain relevant code.
The first being the broadcast file that shows all the code written to send messages across the network and forward those that have been received and checked to be valid.
Then the second being the user sided dashboard that helps to provide a nice way to view and contribute to these messages.
Outcome
The broadcast file
This file is split up into three key sections: the setup; the api endpoints; and the internal functions; this is because although it would be briefer to just show the api endpoints and the internal functions, the setup helps to give more context to those two sections so I included it anyway.
File setup
This shows the setup of the file where various internal and external modules are imported and then then two structs used for message generation are setup for use in the sections below.
module server
// internal imports
import database
import cryptography
import configuration
// external imports
import json
import vweb
import time
import net.http
//structs
struct Broadcast_Message_Contents {
sender []u8
receiver []u8
data string
time string
}
struct Broadcast_Message {
message Broadcast_Message_Contents
signature []u8
}
The dashboard file
This file houses all the logic and code for the http dashboard that is hosted the same way that the api endpoints are, with a basic token system to allow a node's 'owner' to send messages from anywhere that can access the node's api server
File setup
This just shows the setup for modules and constants needed by other parts of the code and contains no actual logic.
module server
// external modules
import vweb
import crypto.rand
import json
import net.http
// internal modules
import configuration
import utils
// constants
const token_path = "$configuration.base_path/tokens.json"
// structs
struct MessageInfo {
pub:
sender string
timestamp string
contents string
}
Challenges
One of the key challenges this cycle was to figure out how to give access to the dashboard to the node's owner and not to other users without having to make them send any private data over a network as it is fairly probable that the software will be running without https as it isn't required for any other part of the software.
The method I created to handle this is very simple but should be secure enough for now and simply relies upon the user trying to login having access to the terminal output of the software at login time.
This is because the security method is based upon a basic cookie and token system where the user clicks "get a token" to generate a new token that is then outputted in the software's terminal output, they can then copy this token into the input field to login where it is submitted as a form of password. Assuming this token is correct, the user's browser is then sent a cookie holding this token so that any other request sent by the user contains this token and verifies that they have control of this node.
Testing
Tests
1
Navigate to 'https://localhost:8000/dashboard'
The dashboard page should redirect to the login page with an option to input a token or generate one.
As Expected
2
Click the 'generate token' button
A token to be output in the node's terminal output.
As Expected
3
Enter an invalid token into the input field and click 'submit'
The dashboard should not give the user access.
As Expected
4
Enter an valid token into the input field and click 'submit'
The dashboard should give the user acess to itself.
As Expected
5
Type a message and click send.
The node should receive the message from the dashboard and attempt to send it across the network.
As Expected
6
Connect to and then send a message to another node using the dashboard.
The node should connect to a pre-existing node and then send a message to it after a message is sent from the dashboard.
As Expected
7
Send a message to the node from another node.
The node on the receipient end of test 6 should receive the same message as was sent from the other node.
As Expected
Last updated