Building a "Decentralized" Application in Rust

in Programming & Dev3 years ago

Over the past six months, I have been primarily focused on developing software in my free time outside my full time development job as a software engineer. The focus of this programming has been thinking about and designing actually decentralized infrastructure and technology.


Boxes.png

For the past year, I have felt that too much development and hype has gone into building traditional models of applications on top of blockchain infrastructure due to the monetary incentives and mechanics that incentivize developers to do so. But still yet we struggle to find those killer applications that breach into the broader public sphere. Given my programming background and my experience with networking in my day job, I decided that I could waste a little time exploring a different viewpoint of decentralized app.

My view of decentralized application being the following:

  • An application that can function with any random n-2 users being removed from the network
  • An application that minimizes data transfer over a network and makes all data localized until requested

All current iterations of "dapps" fail this test. The first point requires all users to run nodes. Clearly we are not even close to this in the blockchain space. Blockchains tend to be massive repositories of global state that all participating nodes need some knowledge of in order to properly validate and process that data. The second point is also problematic in that "dapps" that do use blockchains as part of their back ends end up storing data in one of two places: a centralized server or a global blockchain. Neither localizes and protects user data. While blockchains can make more data transparent, it is arguable that most data suffers from a lack of privacy rather than a lack of transparency. Although a small subset of censored data can find value in that transparency.

The point here and why I chose to wonder down a non-blockchain path was that there are huge concepts and spaces yet to truly be discovered that can potentially do more and do helpful things as part of the decentralization movement outside of blockchains.

So, let's get to the decentralized application. First off, I wrote this in Rust. Rust is a cool programming language created in 2010 by some folks at Mozilla. Rust does a great job of being a fast and efficient language like C or C++, but uses some novel concepts to achieve memory safety without needing a garbage collector. Rust also has modern tooling built around it and a compiler that is just the right amount of mean to get developers writing clean and trustworthy code. Of course, if you are first getting into programming and really want to understand how things work underneath the hood, C is still the standard. Even though C still lets you shoot yourself in the foot if you don't know what you are doing.

Although I wanted to build a decentralized application, those kind of already exist. Peer-to-peer networks have been around for awhile. So, I wanted to build a framework that I could build decentralized applications on top of. That way I could write more applications quicker and write less of the code between applications. With that I was able to write a decentralized command line chatroom (codename "Akron" for the city in Ohio) in around 100 lines of well-formatted, plenty of room to breath in code. The source code for full chatroom application is provided below:

use open_pond_api::{new_interface, RequestEndpoint, ResponseEndpoint, ServiceEndpoint};
use open_pond_protocol::parse_config;

use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::{env, io, thread, time};

const NEW: u8 = 1;
const OLD: u8 = 0;

fn main() {
    // Attempt parse of Open Pond configuration file
    let config_file = env::args()
        .nth(1)
        .unwrap_or_else(|| "config/example.toml".to_string());
    let config = parse_config(config_file).unwrap();
    let name = config.local.name;

    // Create application interface objects for making requests and servicing requests
    let (request_endpoint, service_endpoint, response_endpoint) =
        new_interface(config.settings, 0).unwrap();

    // Create tuple to track last message from standard input
    let last_message: Arc<Mutex<(u8, String)>> = Arc::new(Mutex::new((0, "".to_string())));
    let last_message_ref = last_message.clone();
    let name_ref = name.clone();

    // Spawn threads to send requests, service requests, and read responses
    thread::spawn(|| request_updates(request_endpoint, name_ref));
    thread::spawn(|| service(service_endpoint, last_message_ref));
    thread::spawn(|| receive_updates(response_endpoint));

    // Store messages from standard input
    loop {
        let mut input = String::new();
        if io::stdin().read_line(&mut input).is_ok() {
            let message = format!("{}: {}", name, input);
            println!("{}", message);
            if let Ok(mut lock) = last_message.lock() {
                lock.0 += 1;
                lock.1 = message;
            }
        }
    }
}

// This thread is responsible for sending requests to other users
fn request_updates(endpoint: RequestEndpoint, requester_name: String) {
    loop {
        thread::sleep(time::Duration::new(1, 0));
        endpoint
            .write_request(requester_name.as_bytes().to_vec())
            .unwrap();
    }
}

// This thread is responsible for parsing requests and responding back to them
fn service(endpoint: ServiceEndpoint, last_message: Arc<Mutex<(u8, String)>>) {
    let mut peer_requests: HashMap<String, u8> = HashMap::new();
    loop {
        let (request, return_address) = endpoint.read_request().unwrap();
        let peer_name = String::from_utf8(request).unwrap();

        if let Ok(lock) = last_message.lock() {
            let mut response = vec![OLD; 1];

            // Add message to response if requester has not seen it before
            if let Some(request_id) = peer_requests.get_mut(&peer_name) {
                if *request_id != lock.0 {
                    response[0] = NEW;
                    response.append(&mut lock.1.as_bytes().to_vec());
                    *request_id = lock.0;
                }

            // Add message to response if the requester is new
            } else {
                response[0] = NEW;
                response.append(&mut lock.1.as_bytes().to_vec());
                peer_requests.insert(peer_name, lock.0);
            }
            endpoint.write_response(return_address, response).unwrap();
        }
    }
}

// This thread is responsible for checking responses
fn receive_updates(endpoint: ResponseEndpoint) {
    loop {
        let response = endpoint.read_response().unwrap();
        if response[0] != OLD {
            println!(
                "{}",
                String::from_utf8(response[1..response.len()].to_vec()).unwrap()
            );
        }
    }
}

Project Akron Source Code

All I really need to use was a relatively simple API I designed to interact with the meat in the project. Even though this application took six months to create (in "free time" whatever that means), future applications of this simplicity shouldn't take more than an hour of planning and another hour with wrestling with some code.

The hard work was done on the backend. Getting a node to send it requests to other nodes in the network and get responses back isn't too hard, but of course, I didn't want to retread prior ground and do something somewhat novel. So I build a communication pipeline that scales to multiple applications running on the same pipeline at the same time. The hope is that this choice will make things more efficient and require applications to manage less, but we'll see how that turns out. Long story short after three failed designs and many hours of debugging, I finally got something working consistently and something I think I can build upon.

So here's the final product below in video form (via tutorial):

Pretty simple and nothing special. I probably could spin up something similar in about a day, but the key thing is putting the infrastructure in place and having something to build upon to be able to build and design cooler applications and programs on top of and to integrate security and encryption features into. Security is definitely something that is lacking in this code and something I'll be focused on in the coming months.

We'll see what the next six months brings. With crypto being in a bull market that's always fun to watch. Last bull market I dreamt of reaching escape velocity. Maybe this time I can reach it, so I can spend more time working on cool projects like this.

Anyways, check out Rust if you are curious. There is plenty of interest in Rust in the blockchain community. I had a recruiter reach out to me the other day to talk about programming in Rust for the Polkadot blockchain, so there is hope for those of you who aren't C++ nerds. Lastly, my project source is here, open as always because even if one day my code or ideas are good enough to steal, that just means you are doing my work for me.

Feel free to ask me questions about programming, decentralization, whatever below. I value comments over rewards more than ever given being stuck inside for nearly a year now.