Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementation of the request-response protocol #29

Merged
merged 37 commits into from
May 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
ad49b52
chore: complete request_reponse impementation
thewoodfish May 3, 2024
cd9449f
chore: complete request_response impl
thewoodfish May 5, 2024
c9342b5
fix: impl of the request-response protocol
thewoodfish May 6, 2024
6179ed6
fix: added appropriate network stream interface to communicate with t…
thewoodfish May 7, 2024
29a35f4
add: added more functionality to age_of_empire
thewoodfish May 7, 2024
fc15383
fix: added 'await' to all the futures of the application event handle…
thewoodfish May 7, 2024
018445d
fix: added base58 decoding of a peer id string
thewoodfish May 7, 2024
a37cf46
chore: bebug request-response
thewoodfish May 8, 2024
6cf0e95
chore: clean up request-response
thewoodfish May 9, 2024
6fbaf78
add: implementation of request-response
thewoodfish May 9, 2024
3c16563
add: testing for network_id
sacha-l May 2, 2024
bc7f527
fix: made with_network_id panic if an invalid protocol_id passed in
thewoodfish May 2, 2024
c9f9c7a
add: tests for valid network_id checks
sacha-l May 2, 2024
e8e589d
fix: add panic macro for tests
sacha-l May 2, 2024
836b941
add tests: core builder setup
sacha-l May 6, 2024
7fdc895
fix: async-std as dependency
sacha-l May 6, 2024
25ad59e
fix: add default values to prelude
sacha-l May 6, 2024
4b7fd2c
test: invalid keypair
sacha-l May 6, 2024
6b677b0
add: keypair protobuf tests
sacha-l May 6, 2024
ab2a93e
fix: generate rsa on the fly for tests
sacha-l May 6, 2024
aa076ff
chore: run fmt on all codebase
sacha-l May 6, 2024
b23d8a6
fix merge conflicts: add tests to new core mod
sacha-l May 10, 2024
cf5888e
fix: make tests compile
sacha-l May 10, 2024
29770cf
fix: re-add panic for network_id validity
sacha-l May 10, 2024
60a04cb
fix: update save_keypair_offline to handle non-existant file
sacha-l May 10, 2024
656dfc9
fix: tests for tokio
sacha-l May 10, 2024
41d83de
fix Cargo issues
sacha-l May 10, 2024
d40b452
add: testing for network_id
sacha-l May 2, 2024
4a6c28b
fix: made with_network_id panic if an invalid protocol_id passed in
thewoodfish May 2, 2024
cc927cc
add: tests for valid network_id checks
sacha-l May 2, 2024
d9b6e9c
fix: add panic macro for tests
sacha-l May 2, 2024
7842996
add tests: core builder setup
sacha-l May 6, 2024
9d47c40
fix: async-std as dependency
sacha-l May 6, 2024
75f6aaa
fix: add default values to prelude
sacha-l May 6, 2024
5b64e34
chore: run fmt on all codebase
sacha-l May 6, 2024
46fac47
Merge branch 'main' into dev
sacha-l May 10, 2024
12172ec
merge main->dev issued fixed
sacha-l May 10, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ edition = "2021"
rust-ini = "0.20.0"
swarm_nl = { path = "../swarm_nl", features = ["tokio-runtime"] }
tokio = { version = "1.37.0", features = ["full"] }
base58 = "0.2.0"
12 changes: 12 additions & 0 deletions client/bootstrap_config.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
; Copyright (c) 2024 Algorealm
; A typical template showing the necessary config to bootstrap a node

[ports]
; TCP/IP port to listen on
tcp=49200
; UDP port to listen on
udp=49201

[bootstrap]
; The boostrap nodes to connect to immediately after start up
boot_nodes=[12D3KooWMD3kvZ7hSngeu1p7HAoCCYusSXqPPYDPvzxsa9T4vz3a:/ip4/127.0.0.1/tcp/49152]
340 changes: 158 additions & 182 deletions client/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,207 +1,183 @@
/// Copyright (c) 2024 Algorealm

/// This crate is simply for quick-testing the swarmNL library APIs and assisting in
/// developement. It is not the default or de-facto test crate/module, as it is only used
/// in-dev and will be removed subsequently.
use std::collections::HashMap;
/// Age of Empires
/// Objective: Form alliances and conquer as much empires as possible!
/// It is a multi-player game
/// Enjoy!
use std::{borrow::Cow, num::NonZeroU32, time::Duration};
use swarm_nl::{
core::{DefaultHandler, EventHandler},
ListenerId, Multiaddr, MultiaddrString, PeerIdString, StreamData, StreamExt,
async_trait,
core::{EventHandler, AppResponse},
core::{AppData, Core, CoreBuilder},
setup::BootstrapConfig,
util::string_to_peer_id,
ConnectedPoint, ConnectionId, PeerId,
};

pub static CONFIG_FILE_PATH: &str = "test_config.ini";

/// Complex Event Handler
struct ComplexHandler;

impl EventHandler for ComplexHandler {
fn new_listen_addr(&mut self, _listener_id: ListenerId, addr: Multiaddr) {
// Log the address we begin listening on
println!("We're now listening on: {}", addr);
}
}

#[tokio::main]
async fn main() {
// handler for events happening in the network layer (majorly for technical use)
// use default handler
let handler = DefaultHandler;
let complex_handler = ComplexHandler;

// set up node
let mut bootnodes: HashMap<PeerIdString, MultiaddrString> = HashMap::new();
bootnodes.insert(
"12D3KooWBmwXN3rsVfnLsZKbXeBrSLfczHxZHwVjPrbKwpLfYm3t".to_string(),
"/ip4/127.0.0.1/tcp/63307".to_string(),
);

// configure default data
let config = swarm_nl::setup::BootstrapConfig::new().with_bootnodes(bootnodes);

// set up network core
let mut network = swarm_nl::core::CoreBuilder::with_config(config, complex_handler)
.build()
.await
.unwrap();

// read first (ready) message
if let Some(StreamData::Ready) = network.application_receiver.next().await {
println!("Database is online");

// begin listening
loop {
if let Some(data) = network.application_receiver.next().await {
println!("{:?}", data);
}
}
}
// Start our game! Age of Empires!
play_game().await
}

mod age_of_empires {
use std::{num::NonZeroU32, time::Duration};
use swarm_nl::{core::EventHandler, PeerId, ConnectionId, ConnectedPoint, Multiaddr, StreamData, Sender, AppData};

// Rename Sender during re-export to something more custom for its function
// pub use Sender as StreamComm

/// The state of the game
struct Empire {
soldiers: u32,
farmers: u32,
black_smith: u32,
land_mass: u32,
gold_reserve: u32
}
#[derive(Clone)]
pub struct Empire {
name: String,
soldiers: u8,
farmers: u8,
blacksmith: u8,
land_mass: u8,
gold_reserve: u8,
}

/// implement `EventHander` for `Empire` to reponse to network events and make state changes
impl EventHandler for Empire {
fn connection_established(
&mut self,
peer_id: PeerId,
_connection_id: ConnectionId,
_endpoint: &ConnectedPoint,
_num_established: NonZeroU32,
_concurrent_dial_errors: Option<Vec<(Multiaddr, TransportError<Error>)>>,
_established_in: Duration,
mut application_sender: Sender<StreamData>
) {
// We want to only merge with empired that are richer than us.
// We we connect, we ask for their reserve and make sure they are richer,
// Then we collect a fee of 200 gold coins

// construct keys to send
let keys = vec!["get_coins".to_string()];

// Ask the network to get the gold reserve of the empire seeking partnership
let total_coins_from_peer = application_sender.try_send(StreamData::Application(AppData::FetchData { keys , peer: peer_id }));


// if total_coins_from_peer > self.gold_reserve {
// // ask for merging fee ReqRes::AskForMergingFee
// let m_fee = 100;

// // add to our gold reserve
// self.gold_reserve += m_fee;

// // deal complete
// } else {
// // we dont merge with broke empires
// // disconnect (This is a network operation)
// }
}
}

/// Function to run game
fn start_game() {}

/// Setup network
async fn setup_network() {
// set up a default bootrap config
let config = swarm_nl::setup::BootstrapConfig::new();

let spartan_empire = Empire {
soldiers: 1000,
farmers: 1000,
black_smith: 1000,
land_mass: 1000,
gold_reserve: 1000,
};

// set up network core
let mut network = swarm_nl::core::CoreBuilder::with_config(config, spartan_empire)
.build()
.await
.unwrap();

network.application_sender.try_send(StreamData::ReqRes("gettotalcoins".as_bytes().to_vec())).await;
impl Empire {
/// Create a new empire and assign the assets to begin with
pub fn new(name: String) -> Self {
Empire {
name,
soldiers: 100,
farmers: 100,
blacksmith: 100,
land_mass: 100,
gold_reserve: 100,
}
}
}

#[cfg(test)]
mod tests {
use ini::Ini;
use std::borrow::Cow;

use crate::CONFIG_FILE_PATH;

/// try to read/write a byte vector to config file
#[test]
fn write_to_ini_file() {
let test_vec = vec![12, 234, 45, 34, 54, 34, 43, 34, 43, 23, 43, 43, 34, 67, 98];

// try vec to `.ini` file
assert!(write_config(
"auth",
"serialized_keypair",
&format!("{:?}", test_vec)
));
assert_eq!(
read_config("auth", "serialized_keypair"),
format!("{:?}", test_vec)
#[async_trait]
impl EventHandler for Empire {
async fn new_listen_addr(
&mut self,
local_peer_id: PeerId,
_listener_id: swarm_nl::ListenerId,
addr: swarm_nl::Multiaddr,
) {
// announce interfaces we're listening on
println!("Peer id: {}", local_peer_id);
println!("We're listening on the {}", addr);
println!(
"There are {} soldiers guarding the {} Empire gate",
self.soldiers, self.name
);

// test that we can read something after it
assert_eq!(read_config("bio", "name"), "@thewoodfish");
}

#[test]
fn test_conversion_fn() {
let test_vec = vec![12, 234, 45, 34, 54, 34, 43, 34, 43, 23, 43, 43, 34, 67, 98];

let vec_string = "[12, 234, 45, 34, 54, 34, 43, 34, 43, 23, 43, 43, 34, 67, 98,]";
assert_eq!(string_to_vec(vec_string), test_vec);
async fn connection_established(
&mut self,
peer_id: PeerId,
_connection_id: ConnectionId,
_endpoint: &ConnectedPoint,
_num_established: NonZeroU32,
_established_in: Duration,
) {
println!("Connection established with peer: {}", peer_id);
}

/// read value from config file
fn read_config(section: &str, key: &str) -> Cow<'static, str> {
if let Ok(conf) = Ini::load_from_file(CONFIG_FILE_PATH) {
if let Some(section) = conf.section(Some(section)) {
if let Some(value) = section.get(key) {
return Cow::Owned(value.to_owned());
}
}
/// Handle any incoming RPC from any neighbouring empire
fn handle_incoming_message(&mut self, data: Vec<Vec<u8>>) -> Vec<Vec<u8>> {
// The semantics is left to the application to handle
match String::from_utf8_lossy(&data[0]) {
// Handle the request to get military status
Cow::Borrowed("military_status") => {
// Get empire name
let empire_name = self.name.as_bytes().to_vec();

// Get military capacity
let military_capacity = self.soldiers;

// marshall into accepted format andd then return it
vec![empire_name, vec![military_capacity]]
},
_ => Default::default(),
}

"".into()
}
}

fn string_to_vec(input: &str) -> Vec<u8> {
input
.trim_matches(|c| c == '[' || c == ']')
.split(',')
.filter_map(|s| s.trim().parse().ok())
.collect()
}
/// Setup game (This is for the persian Empire)
/// This requires no bootnodes connection
// #[cfg(not(feature = "macedonian"))]
// pub async fn setup_game() -> Core<Empire> {
// // First, we want to configure our node
// let config = BootstrapConfig::default();

// // State kept by this node
// let empire = Empire::new(String::from("Spartan"));

// // Set up network
// CoreBuilder::with_config(config, empire)
// .build()
// .await
// .unwrap()
// }

/// The Macedonian Empire setup.
/// These require bootnodes of empires to form alliance.
/// We will be providing the location (peer id and multiaddress) of the Spartan Empire as boot
/// parameters
// #[cfg(feature = "macedonian")]
pub async fn setup_game() -> Core<Empire> {
// First, we want to configure our node with the bootstrap config file on disk
let config = BootstrapConfig::from_file("bootstrap_config.ini");

// State kept by this node
let empire = Empire::new(String::from("Macedonian"));

// Set up network
CoreBuilder::with_config(config, empire)
.build()
.await
.unwrap()
}

/// write value into config file
fn write_config(section: &str, key: &str, new_value: &str) -> bool {
if let Ok(mut conf) = Ini::load_from_file(CONFIG_FILE_PATH) {
// Set a value:
conf.set_to(Some(section), key.into(), new_value.into());
if let Ok(_) = conf.write_to_file(CONFIG_FILE_PATH) {
return true;
}
/// Play game
pub async fn play_game() {
// Setup network
let mut core = setup_game().await;

// TODO: DELAY FOR A WHILE

// Print game state
println!("Empire Information:");
println!("Name: {}", core.state.soldiers);
println!("Farmers: {}", core.state.farmers);
println!("Black smiths: {}", core.state.blacksmith);
println!("Land mass: {}", core.state.land_mass);
println!("Gold reserve: {}", core.state.gold_reserve);

// TODO! FUNCTION TO CHECK NODES I'M CONNECTED WITH

// TODO: Wait a little to help the network boot

// Let them connect first
tokio::time::sleep(Duration::from_secs(6)).await;

let request = vec!["military_status".as_bytes().to_vec()];

// Spartan Empire
let remote_peer_id = "12D3KooWMD3kvZ7hSngeu1p7HAoCCYusSXqPPYDPvzxsa9T4vz3a";

// Prepare request
let status_request = AppData::FetchData {
keys: request,
peer: string_to_peer_id(remote_peer_id).unwrap(),
};

// Send request
let stream_id = core.send_to_network(status_request).await.unwrap();

// Get response
// AppData::Fetch returns a Vec<Vec<u8>>, hence we can parse the response from it
if let Ok(status_response) = core.recv_from_network(stream_id).await {
if let AppResponse::FetchData(status) = status_response {
let empire_name = String::from_utf8_lossy(&status[0]);
let military_status = status[1][0];

// Print the military status of the empire we just contacted
println!("Empire Contacted:");
println!("Name: {} Empire", empire_name);
println!("Military Capacity: {} Soldiers", military_status);
}
false
}

// Keep looping so we can record network events
loop {}
}
10 changes: 0 additions & 10 deletions client/test_config.ini

This file was deleted.

Loading
Loading