diff --git a/.gitignore b/.gitignore index 0e2a851081d..dd3546d3290 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ target .env /.vscode/settings.json +validator/.vscode sample-configs/validator-config.toml \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 0b7a7789dea..3c9610bb6ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -244,6 +244,19 @@ dependencies = [ "byte-tools", ] +[[package]] +name = "bodyparser" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f023abfa58aad6f6bc4ae0630799e24d5ee0ab8bb2e49f651d9b1f9aa4f52f30" +dependencies = [ + "iron", + "persistent", + "plugin", + "serde", + "serde_json", +] + [[package]] name = "bs58" version = "0.3.0" @@ -327,9 +340,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31850b4a4d6bae316f7a09e691c944c28299298837edc0a03f755618c23cbc01" +checksum = "80094f509cf8b5ae86a4966a39b3ff66cd7e2a3e594accec3743ff3fabeab5b2" dependencies = [ "num-integer", "num-traits", @@ -541,6 +554,28 @@ dependencies = [ "subtle 2.2.2", ] +[[package]] +name = "diesel" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d7cc03b910de9935007861dce440881f69102aaaedfd4bc5a6f40340ca5840c" +dependencies = [ + "byteorder", + "diesel_derives", + "libsqlite3-sys", +] + +[[package]] +name = "diesel_derives" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45f5098f628d02a7a0f68ddba586fb61e80edec3bdc1be3b921f4ceec60858d3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "difference" version = "2.0.0" @@ -1226,6 +1261,16 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "libsqlite3-sys" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5b95e89c330291768dc840238db7f9e204fd208511ab6319b56193a7f2ae25" +dependencies = [ + "pkg-config", + "vcpkg", +] + [[package]] name = "libz-sys" version = "1.0.25" @@ -1620,11 +1665,13 @@ name = "nym-validator" version = "0.7.0-dev" dependencies = [ "abci", + "bodyparser", "built", "byteorder", "clap", "config", "crypto", + "diesel", "directory-client", "dirs", "dotenv", @@ -1748,6 +1795,16 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +[[package]] +name = "persistent" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e8fa0009c4f3d350281309909c618abddf10bb7e3145f28410782f6a5ec74c5" +dependencies = [ + "iron", + "plugin", +] + [[package]] name = "pest" version = "2.1.3" diff --git a/validator/Cargo.toml b/validator/Cargo.toml index df1c38244be..a7a43d1d3a3 100644 --- a/validator/Cargo.toml +++ b/validator/Cargo.toml @@ -9,10 +9,12 @@ edition = "2018" [dependencies] abci = "0.6.4" +bodyparser = "0.8.0" byteorder = "1.3.2" clap = "2.33.0" +# chrono = { version = "0.4.11", features = ["serde"] } TODO: remove this if I don't start using it again soon +diesel = { version = "1.4.3", features = ["sqlite"] } dirs = "2.0.2" -# Read notes https://crates.io/crates/dotenv - tl;dr: don't use in production, set environmental variables properly. dotenv = "0.15.0" futures = "0.3.1" iron = "0.6.1" diff --git a/validator/README.md b/validator/README.md index c8bbad50420..47b90bec59d 100644 --- a/validator/README.md +++ b/validator/README.md @@ -4,10 +4,10 @@ Nym Validator The Nym Validator has several jobs: * use Tendermint (v0.33.0) to maintain a total global ordering of incoming transactions -* track quality of service for mixnet nodes (mixmining) +* rewards + stake slashing based quality of service measurements for mixnet nodes (aka "mixmining") * generate Coconut credentials and ensure they're not double spent * maintain a decentralized directory of all Nym nodes that have staked into the system - + Some of these functions may be moved away to their own node types in the future, for example to increase scalability or performance. At the moment, we'd like to keep deployments simple, so they're all in the validator node. Running the validator on your local machine diff --git a/validator/diesel.toml b/validator/diesel.toml new file mode 100644 index 00000000000..92267c829f2 --- /dev/null +++ b/validator/diesel.toml @@ -0,0 +1,5 @@ +# For documentation on how to configure this file, +# see diesel.rs/guides/configuring-diesel-cli + +[print_schema] +file = "src/schema.rs" diff --git a/validator/migrations/.gitkeep b/validator/migrations/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/validator/src/network/rest/capacity/mod.rs b/validator/src/network/rest/capacity/mod.rs new file mode 100644 index 00000000000..c9b77fe3d9d --- /dev/null +++ b/validator/src/network/rest/capacity/mod.rs @@ -0,0 +1,62 @@ +use serde::{Deserialize, Serialize}; + +use super::*; +use bodyparser::Struct; +use iron::mime::Mime; +use iron::status; +use iron::Handler; + +/// Holds data for a capacity update (json) +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Capacity { + value: usize, +} + +pub struct Update { + service: Arc>, +} + +impl Update { + pub fn new(service: Arc>) -> Update { + Update { service } + } +} + +impl Handler for Update { + fn handle(&self, req: &mut Request) -> IronResult { + let json_parse = req.get::>(); + + if json_parse.is_ok() { + let capacity = json_parse + .unwrap() + .expect("Unexpected JSON parsing problem") + .value; + self.service.lock().unwrap().set_capacity(capacity); + Ok(Response::with(status::Created)) + } else { + let error = json_parse.unwrap_err(); + Ok(Response::with((status::BadRequest, error.detail))) + } + } +} + +pub struct Get { + service: Arc>, +} + +impl Get { + pub fn new(service: Arc>) -> Get { + Get { service } + } +} + +impl Handler for Get { + fn handle(&self, _: &mut Request) -> IronResult { + let content_type = "application/json".parse::().unwrap(); + let value = self.service.lock().unwrap().capacity(); + let c = Capacity { value }; + let json = serde_json::to_string(&c).unwrap(); + Ok(Response::with((content_type, status::Ok, json))) + } +} diff --git a/validator/src/network/rest/mod.rs b/validator/src/network/rest/mod.rs index 4978f80b76b..4b991caf00b 100644 --- a/validator/src/network/rest/mod.rs +++ b/validator/src/network/rest/mod.rs @@ -11,33 +11,62 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. - +use crate::services::mixmining; use iron::prelude::*; +use presence::mixnode; +use presence::topology; use router::Router; +use std::sync::{Arc, Mutex}; -mod models; -mod routes; +mod capacity; +mod presence; +mod staking; -pub struct Api {} +pub struct Api { + mixmining_service: Arc>, +} impl Api { - pub fn new() -> Api { - Api {} + pub fn new(mixmining_service: mixmining::Service) -> Api { + let service = Arc::new(Mutex::new(mixmining_service)); + Api { + mixmining_service: service, + } } + /// Run the REST API. pub async fn run(self) { - let port = 3000; - println!("* starting REST API on localhost:{}", port); + let port = 3000; // TODO: make this configurable + let address = format!("localhost:{}", port); + println!("* starting REST API on http://{}", address); + + let router = self.setup_router(); - let router = self.setup_routes(); - Iron::new(router) - .http(format!("localhost:{}", port)) - .unwrap(); + Iron::new(router).http(address).unwrap(); } - pub fn setup_routes(&self) -> Router { + /// Tie together URL route paths with handler functions. + fn setup_router(self) -> Router { + // define a Router to hold our routes let mut router = Router::new(); - router.get("/topology", routes::topology::get, "topology_get"); + + // set up handlers + let capacity_update = capacity::Update::new(Arc::clone(&self.mixmining_service)); + let capacity_get = capacity::Get::new(Arc::clone(&self.mixmining_service)); + let presence_mixnode_create = + mixnode::CreatePresence::new(Arc::clone(&self.mixmining_service)); + let topology_get = topology::GetTopology::new(Arc::clone(&self.mixmining_service)); + + // tie routes to handlers + router.get("/capacity", capacity_get, "capacity_get"); + router.post("/capacity", capacity_update, "capacity_update"); + router.get("/topology", topology_get, "topology_get"); + router.post( + "/presence/mixnodes", + presence_mixnode_create, + "presence_mixnodes_post", + ); + router } } diff --git a/validator/src/network/rest/presence/conversions.rs b/validator/src/network/rest/presence/conversions.rs new file mode 100644 index 00000000000..703d97000b4 --- /dev/null +++ b/validator/src/network/rest/presence/conversions.rs @@ -0,0 +1,123 @@ +use crate::network::rest::presence::models::Mixnode as RestMixnode; +use crate::network::rest::presence::models::Topology as RestTopology; +use crate::services::mixmining::models::Mixnode as ServiceMixnode; +use crate::services::mixmining::models::Topology as ServiceTopology; +use std::convert::From; +use std::time::{SystemTime, UNIX_EPOCH}; + +impl From for ServiceMixnode { + fn from(value: RestMixnode) -> ServiceMixnode { + let now = SystemTime::now(); + let timestamp = now.duration_since(UNIX_EPOCH).unwrap().as_millis() as u64; + ServiceMixnode { + host: value.host, + last_seen: timestamp, + location: value.location, + public_key: value.public_key, + stake: 0, + version: value.version, + } + } +} + +impl From for RestMixnode { + fn from(value: ServiceMixnode) -> RestMixnode { + RestMixnode { + host: value.host, + location: value.location, + public_key: value.public_key, + version: value.version, + } + } +} + +impl From for ServiceTopology { + fn from(value: RestTopology) -> ServiceTopology { + let mut converted_mixnodes: Vec = Vec::new(); + for mixnode in value.mixnodes { + converted_mixnodes.push(mixnode.into()); + } + ServiceTopology { + mixnodes: converted_mixnodes.to_vec(), + service_providers: vec![], // add these when conversions exist + validators: vec![], // add these when conversions exist + } + } +} + +impl From for RestTopology { + fn from(value: ServiceTopology) -> RestTopology { + let mut converted_mixnodes: Vec = Vec::new(); + for mixnode in value.mixnodes { + converted_mixnodes.push(mixnode.into()); + } + RestTopology { + mixnodes: converted_mixnodes.to_vec(), + service_providers: vec![], // add these when conversions exist + validators: vec![], // add these when conversions exist + } + } +} + +#[cfg(test)] +mod test_presence_conversions_for_mixmining_service { + fn rest_mixnode_fixture() -> RestMixnode { + RestMixnode { + host: "foo.org".to_owned(), + public_key: "abc".to_owned(), + location: "London".to_owned(), + version: "1.0.0".to_owned(), + } + } + + fn service_mixnode_fixture() -> ServiceMixnode { + ServiceMixnode { + host: "foo.org".to_owned(), + public_key: "abc".to_owned(), + last_seen: 1234, + location: "London".to_owned(), + stake: 0, + version: "1.0.0".to_owned(), + } + } + + use super::*; + + #[test] + fn test_building_service_mixnode_from_rest_mixnode() { + let rest_mixnode = rest_mixnode_fixture(); + let service_mixnode = ServiceMixnode::from(rest_mixnode.clone()); + assert_eq!(service_mixnode.host, rest_mixnode.host); + assert_eq!(service_mixnode.public_key, rest_mixnode.public_key); + assert_eq!(service_mixnode.location, rest_mixnode.location); + assert_eq!(service_mixnode.stake, 0); + assert_eq!(service_mixnode.version, rest_mixnode.version); + // I'm not going to test the last_seen timestamp as I can't be bothered + // setting up a fake clock right now. + // The behaviour is: it should set time to SystemTime::now(). + } + + #[test] + fn test_building_rest_mixnode_from_service_mixnode() { + let service_mixnode = service_mixnode_fixture(); + let rest_mixnode = RestMixnode::from(service_mixnode.clone()); + assert_eq!(rest_mixnode.host, service_mixnode.host); + assert_eq!(rest_mixnode.public_key, service_mixnode.public_key); + assert_eq!(rest_mixnode.location, service_mixnode.location); + assert_eq!(rest_mixnode.version, service_mixnode.version); + } + + #[test] + fn test_building_service_topology_from_rest_topology() { + let rest_mixnode = rest_mixnode_fixture(); + let rest_topology = RestTopology { + mixnodes: vec![rest_mixnode.clone()], + service_providers: vec![], + validators: vec![], + }; + + let service_topology = ServiceTopology::from(rest_topology); + let service_mixnode = ServiceMixnode::from(rest_mixnode); + assert_eq!(service_mixnode, service_topology.mixnodes[0]); + } +} diff --git a/validator/src/network/rest/presence/mixnode.rs b/validator/src/network/rest/presence/mixnode.rs new file mode 100644 index 00000000000..ed4f46d229d --- /dev/null +++ b/validator/src/network/rest/presence/mixnode.rs @@ -0,0 +1,32 @@ +use super::*; +use crate::network::rest::presence::models::Mixnode as PresenceMixnode; +use bodyparser::Struct; +use iron::status; +use iron::Handler; + +pub struct CreatePresence { + service: Arc>, +} + +impl CreatePresence { + pub fn new(service: Arc>) -> CreatePresence { + CreatePresence { service } + } +} + +impl Handler for CreatePresence { + fn handle(&self, req: &mut Request) -> IronResult { + let json_parse = req.get::>(); + + if json_parse.is_ok() { + let mixnode = json_parse + .unwrap() + .expect("Unexpected JSON parsing problem"); + self.service.lock().unwrap().add(mixnode.into()); + Ok(Response::with(status::Created)) + } else { + let error = json_parse.unwrap_err(); + Ok(Response::with((status::BadRequest, error.detail))) + } + } +} diff --git a/validator/src/network/rest/presence/mod.rs b/validator/src/network/rest/presence/mod.rs new file mode 100644 index 00000000000..108d75277ea --- /dev/null +++ b/validator/src/network/rest/presence/mod.rs @@ -0,0 +1,6 @@ +use super::*; + +mod conversions; +pub mod mixnode; +mod models; +pub mod topology; diff --git a/validator/src/network/rest/presence/models.rs b/validator/src/network/rest/presence/models.rs new file mode 100644 index 00000000000..60293d930d0 --- /dev/null +++ b/validator/src/network/rest/presence/models.rs @@ -0,0 +1,39 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Mixnode { + pub host: String, + pub public_key: String, + pub version: String, + pub location: String, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct ServiceProvider { + host: String, + public_key: String, + version: String, + last_seen: u64, + location: String, +} + +/// Topology shows us the current state of the overall Nym network +#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct Topology { + pub mixnodes: Vec, + pub service_providers: Vec, + pub validators: Vec, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct Validator { + host: String, + public_key: String, + version: String, + last_seen: u64, + location: String, +} diff --git a/validator/src/network/rest/presence/topology.rs b/validator/src/network/rest/presence/topology.rs new file mode 100644 index 00000000000..46517c62f70 --- /dev/null +++ b/validator/src/network/rest/presence/topology.rs @@ -0,0 +1,23 @@ +use super::*; +use iron::status; +use iron::Handler; + +pub struct GetTopology { + service: Arc>, +} + +impl GetTopology { + pub fn new(service: Arc>) -> GetTopology { + GetTopology { service } + } +} + +impl Handler for GetTopology { + fn handle(&self, _req: &mut Request) -> IronResult { + println!("Getting topology!..."); + let service_topology = self.service.lock().unwrap().topology(); + let topology = models::Topology::from(service_topology); + let response = serde_json::to_string_pretty(&topology).unwrap(); + Ok(Response::with((status::Ok, response))) + } +} diff --git a/validator/src/network/rest/staking/mod.rs b/validator/src/network/rest/staking/mod.rs new file mode 100644 index 00000000000..b4780ebde8d --- /dev/null +++ b/validator/src/network/rest/staking/mod.rs @@ -0,0 +1 @@ +// pub struct StakeUpdate {} diff --git a/validator/src/network/tendermint/mod.rs b/validator/src/network/tendermint/mod.rs index 66b5c60ad97..140a8153be6 100644 --- a/validator/src/network/tendermint/mod.rs +++ b/validator/src/network/tendermint/mod.rs @@ -46,18 +46,18 @@ impl abci::Application for Abci { fn check_tx(&mut self, req: &RequestCheckTx) -> ResponseCheckTx { // Get the Tx [u8] and convert to u64 let c = convert_tx(req.get_tx()); - let mut resp = ResponseCheckTx::new(); + let mut response = ResponseCheckTx::new(); // Validation logic if c != self.count + 1 { - resp.set_code(1); - resp.set_log(String::from("Count must be incremental!")); - return resp; + response.set_code(1); + response.set_log(String::from("Count must be incremental!")); + return response; } // Update state to keep state correct for next check_tx call self.count = c; - resp + response } fn deliver_tx(&mut self, req: &RequestDeliverTx) -> ResponseDeliverTx { @@ -71,12 +71,12 @@ impl abci::Application for Abci { fn commit(&mut self, _req: &RequestCommit) -> ResponseCommit { // Create the response - let mut resp = ResponseCommit::new(); + let mut response = ResponseCommit::new(); // Convert count to bits let mut buf = [0; 8]; BigEndian::write_u64(&mut buf, self.count); // Set data so last state is included in the block - resp.set_data(buf.to_vec()); - resp + response.set_data(buf.to_vec()); + response } } diff --git a/validator/src/schema.rs b/validator/src/schema.rs new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/validator/src/schema.rs @@ -0,0 +1 @@ + diff --git a/validator/src/services/mixmining/db.rs b/validator/src/services/mixmining/db.rs new file mode 100644 index 00000000000..c53a2e19f0b --- /dev/null +++ b/validator/src/services/mixmining/db.rs @@ -0,0 +1,105 @@ +use super::Mixnode; + +/// A (currently RAM-based) data store to keep tabs on which nodes have what +/// stake assigned to them. +#[derive(Clone, Debug, PartialEq)] +pub struct MixminingDb { + mixnodes: Vec, + capacity: usize, +} + +impl MixminingDb { + pub fn new() -> MixminingDb { + let mixnodes = Vec::::new(); + MixminingDb { + capacity: 6, + mixnodes, + } + } + + pub fn add(&mut self, mixnode: Mixnode) { + self.mixnodes.push(mixnode); + } + + pub fn get_mixnodes(&self) -> &Vec { + &self.mixnodes + } + + pub fn set_capacity(&mut self, capacity: usize) { + self.capacity = capacity; + } + + pub fn capacity(&self) -> usize { + self.capacity + } +} + +#[cfg(test)] +mod capacity { + use super::*; + + #[test] + fn starts_at_6() { + let db = MixminingDb::new(); + assert_eq!(6, db.capacity()); + } + + #[test] + fn setting_and_getting_work() { + let mut db = MixminingDb::new(); + db.set_capacity(1); + assert_eq!(1, db.capacity()); + } +} + +#[cfg(test)] +mod adding_and_retrieving_mixnodes { + use super::*; + + #[test] + fn add_and_retrieve_one_works() { + let node = fake_mixnode("London, UK"); + let mut db = MixminingDb::new(); + + db.add(node.clone()); + + assert_eq!(&node, db.get_mixnodes().first().unwrap()); + } + + #[test] + fn add_and_retrieve_two_works() { + let node1 = fake_mixnode("London, UK"); + let node2 = fake_mixnode("Neuchatel"); + let mut db = MixminingDb::new(); + + db.add(node1.clone()); + db.add(node2.clone()); + + assert_eq!(node1, db.get_mixnodes()[0]); + assert_eq!(node2, db.get_mixnodes()[1]); + } + + #[test] + fn starts_empty() { + let db = MixminingDb::new(); + assert_eq!(0, db.mixnodes.len()); + } + + #[test] + fn calling_list_when_empty_returns_empty_vec() { + let db = MixminingDb::new(); + let empty: Vec = vec![]; + assert_eq!(&empty, db.get_mixnodes()); + } + + fn fake_mixnode(location: &str) -> Mixnode { + Mixnode { + host: String::from("foo.com"), + last_seen: 123, + location: String::from(location), + public_key: String::from("abc123"), + stake: 8, + version: String::from("1.0"), + } + } +} diff --git a/validator/src/services/mixmining/mod.rs b/validator/src/services/mixmining/mod.rs index 6ef63657859..c93cfa1d28c 100644 --- a/validator/src/services/mixmining/mod.rs +++ b/validator/src/services/mixmining/mod.rs @@ -12,4 +12,166 @@ // See the License for the specific language governing permissions and // limitations under the License. +use db::MixminingDb; +use models::*; + +pub mod db; pub mod health_check_runner; +pub mod models; +mod tests; + +pub struct Service { + db: MixminingDb, +} + +/// The mixmining::Service provides logic for updating and slashing mixnode +/// stake, retrieving lists of mixnodes based on stake, and adding/removing +/// mixnodes from the active set. It monitors mixnodes and rewards or slashes +/// based on the observed quality of service provided by a given mixnode. +/// +/// Mixing and staking interact. Mixnodes first need to announce +/// their presence to the validators. +/// +/// The mixnode then goes into the stack of available mixnodes. +/// +/// However, it's not necessarily going to start actively mixing traffic. +/// That depends on how much stake is riding on it, and how much capacity the +/// network requires right now. We depend on the wisdom of stakers to put their +/// money on trustworthy mixnodes. +/// +/// The active set of mixnodes will be able to expand or contract based on capacity. +/// For now, we simply take the top nodes available, ordered by +/// . +/// +/// A lot is going to need to change here. Commented code is here mainly to +/// quickly sketch out the guts of the mixmining and staking service. This is not the basis +/// of our real staking system quite yet - it's a way to start getting the system +/// to function with all the different node types to start talking to each other, +/// and will be dramatically reworked over the next few months. +impl Service { + pub fn new(db: MixminingDb) -> Service { + Service { db } + } + + // Add a mixnode so that it becomes part of the possible mixnode set. + pub fn add(&mut self, mixnode: Mixnode) { + self.db.add(mixnode); + } + + pub fn topology(&self) -> Topology { + let mixnodes = self.db.get_mixnodes(); + let service_providers: Vec = vec![]; + let validators: Vec = vec![]; + Topology::new(mixnodes.to_vec(), service_providers, validators) + } + + pub fn set_capacity(&mut self, capacity: usize) { + self.db.set_capacity(capacity); + } + + /// A fake capacity, so we can take the top n mixnodes based on stake + pub fn capacity(&self) -> usize { + self.db.capacity() + } + + /* + + /// Update (or create) a given mixnode stake, identified by the mixnode's public key + fn update(&self, public_key: &str, amount: u64) { + // retrieve the given Mixnode from the database and update its stake + } + + /// For now, we have no notion of measuring capacity. For now just use capacity(). + fn active_mixnodes(&self) -> Vec { + Vec::::new() + // hit the database + } + + + /// Remove a mixnode from the active set in a way that does not impact its stake. + /// In a more built-out system, this method would mean: + /// "mixnode x has done its job well and requested to leave, so it can be removed + /// at the end of an epoch." + fn remove(&self, public_key: &str) { + // free locked up stake back to originating stakeholder + // remove the mixnode from the database + } + + /// Add the given amount of stake to the given Mixnode. Presumably it has done + /// its job well. + fn reward(&self, public_key: &str, amount: u64) {} + + /// Slash a mixnode's stake based on bad performance or detected malign intent. + fn slash(&self, public_key: &str, amount: u64) { + // transfer slashed stake amount to reserve fund + // retrieve the mixnode from the database, and decrement its stake amount + // by the amount given. + } + + /// Slash a mixnode's stake and immediately remove it from the mixnode set. + fn slash_remove(&self, public_key: String, amount: u64) { + // call slash (the method, not the guitarist) + // remove the mixnode from the database + } + */ +} + +#[cfg(test)] +mod mixnodes { + use super::*; + + #[test] + fn adding_and_retrieving_works() { + let mock_db = MixminingDb::new(); + let mut service = Service::new(mock_db); + let node1 = tests::fake_mixnode("London, UK"); + + service.add(node1.clone()); + let nodes = service.topology().mixnodes; + assert_eq!(1, nodes.len()); + assert_eq!(node1.clone(), nodes[0]); + let node2 = tests::fake_mixnode("Neuchatel"); + + service.add(node2.clone()); + let nodes = service.topology().mixnodes; + assert_eq!(2, nodes.len()); + assert_eq!(node1.clone(), nodes[0]); + assert_eq!(node2.clone(), nodes[1]); + } +} + +#[cfg(test)] +mod constructor { + use super::*; + + #[test] + fn sets_database() { + let db = db::MixminingDb::new(); + let service = Service::new(db.clone()); + + assert_eq!(db, service.db); + } +} + +#[cfg(test)] +mod capacity { + use super::*; + + #[test] + fn setting_capacity_sends_correct_value_to_datastore() { + let mock_db = db::MixminingDb::new(); + let mut service = Service::new(mock_db); + + service.set_capacity(3); + + assert_eq!(3, service.capacity()); + } + + #[test] + fn getting_capacity_works() { + let mut mock_db = db::MixminingDb::new(); + mock_db.set_capacity(3); + let service = Service::new(mock_db); + assert_eq!(3, service.capacity()); + } +} diff --git a/validator/src/services/mixmining/models.rs b/validator/src/services/mixmining/models.rs new file mode 100644 index 00000000000..bff8702ed78 --- /dev/null +++ b/validator/src/services/mixmining/models.rs @@ -0,0 +1,51 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Mixnode { + pub host: String, + pub public_key: String, + pub last_seen: u64, + pub location: String, + pub stake: u64, + pub version: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct ServiceProvider { + host: String, + public_key: String, + version: String, + last_seen: u64, + location: String, +} + +/// Topology shows us the current state of the overall Nym network +#[derive(Serialize, Deserialize, Debug)] +pub struct Topology { + pub mixnodes: Vec, + pub service_providers: Vec, + pub validators: Vec, +} + +impl Topology { + pub fn new( + mixnodes: Vec, + service_providers: Vec, + validators: Vec, + ) -> Topology { + Topology { + mixnodes, + service_providers, + validators, + } + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Validator { + host: String, + public_key: String, + version: String, + last_seen: u64, + location: String, +} diff --git a/validator/src/services/mixmining/tests/mod.rs b/validator/src/services/mixmining/tests/mod.rs new file mode 100644 index 00000000000..fac930f9022 --- /dev/null +++ b/validator/src/services/mixmining/tests/mod.rs @@ -0,0 +1,11 @@ +#[cfg(test)] +pub fn fake_mixnode(location: &str) -> super::Mixnode { + super::Mixnode { + host: String::from("foo.com"), + last_seen: 123, + location: String::from(location), + public_key: String::from("abc123"), + stake: 8, + version: String::from("1.0"), + } +} diff --git a/validator/src/validator.rs b/validator/src/validator.rs index ded11a39463..24c61841cbd 100644 --- a/validator/src/validator.rs +++ b/validator/src/validator.rs @@ -15,6 +15,7 @@ use crate::config::Config; use crate::network::rest; use crate::network::tendermint; +use crate::services::mixmining; use crate::services::mixmining::health_check_runner; use crypto::identity::MixIdentityKeyPair; use healthcheck::HealthChecker; @@ -45,7 +46,10 @@ impl Validator { hc, ); - let rest_api = rest::Api::new(); + let mixmining_db = mixmining::db::MixminingDb::new(); + let mixmining_service = mixmining::Service::new(mixmining_db); + + let rest_api = rest::Api::new(mixmining_service); Validator { health_check_runner, @@ -64,7 +68,7 @@ impl Validator { rt.spawn(self.rest_api.run()); rt.spawn(self.tendermint_abci.run()); - // TODO: this message is going to come out of order (if at all), as spawns are async + // TODO: this message is going to come out of order (if at all), as spawns are async, see issue above println!("Validator startup complete."); rt.block_on(blocker()); }