Compare commits

...

11 Commits
v0.1.0 ... main

Author SHA1 Message Date
cc027d2cc1
pass message from daemon to CLI 2025-03-06 17:22:32 +02:00
64b65d7ecd
don't delete VMs if node is offline for < 1day 2025-02-28 00:37:34 +02:00
02be48fd96
add support for operators 2025-02-17 17:51:34 +02:00
5c213f2eb4
small refactoring on var names and impls 2025-02-12 02:12:56 +02:00
df805ea291
add admin key 2025-02-11 21:03:05 +05:30
c98db7f8c3
rename structs so that they say "VM" 2025-02-09 00:14:32 +02:00
64f892c174
added admin functionality 2025-02-06 15:16:00 +02:00
5359ba039b
added auth 2025-02-04 03:09:53 +02:00
7dfdf4844e
switch language from tokens to LP 2025-01-28 18:31:30 +02:00
928c68f550
reduce the price of memory 2025-01-28 15:32:33 +02:00
9fa62a1978
inform daemon about VMs deleted by cron 2025-01-27 17:45:28 +02:00
7 changed files with 1349 additions and 317 deletions

253
Cargo.lock generated

@ -209,25 +209,42 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64ct"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.6.0" version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]] [[package]]
name = "brain-mock" name = "brain-mock"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"bs58",
"chrono", "chrono",
"dashmap", "dashmap",
"ed25519-dalek",
"env_logger", "env_logger",
"log", "log",
"prost", "prost",
"prost-types", "prost-types",
"reqwest", "reqwest",
"serde", "serde",
"serde_json", "serde_yaml",
"thiserror", "thiserror",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
@ -236,6 +253,15 @@ dependencies = [
"uuid", "uuid",
] ]
[[package]]
name = "bs58"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4"
dependencies = [
"tinyvec",
]
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.16.0" version = "3.16.0"
@ -279,6 +305,7 @@ dependencies = [
"iana-time-zone", "iana-time-zone",
"js-sys", "js-sys",
"num-traits", "num-traits",
"serde",
"wasm-bindgen", "wasm-bindgen",
"windows-targets", "windows-targets",
] ]
@ -289,6 +316,12 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]]
name = "const-oid"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.9.4" version = "0.9.4"
@ -305,12 +338,58 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "crossbeam-utils" name = "crossbeam-utils"
version = "0.8.21" version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "curve25519-dalek"
version = "4.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
dependencies = [
"cfg-if",
"cpufeatures",
"curve25519-dalek-derive",
"digest",
"fiat-crypto",
"rustc_version",
"subtle",
"zeroize",
]
[[package]]
name = "curve25519-dalek-derive"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "dashmap" name = "dashmap"
version = "6.1.0" version = "6.1.0"
@ -323,6 +402,27 @@ dependencies = [
"lock_api", "lock_api",
"once_cell", "once_cell",
"parking_lot_core", "parking_lot_core",
"serde",
]
[[package]]
name = "der"
version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0"
dependencies = [
"const-oid",
"zeroize",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
] ]
[[package]] [[package]]
@ -336,6 +436,30 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "ed25519"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
dependencies = [
"pkcs8",
"signature",
]
[[package]]
name = "ed25519-dalek"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871"
dependencies = [
"curve25519-dalek",
"ed25519",
"serde",
"sha2",
"subtle",
"zeroize",
]
[[package]] [[package]]
name = "either" name = "either"
version = "1.13.0" version = "1.13.0"
@ -396,6 +520,12 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "fiat-crypto"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
[[package]] [[package]]
name = "fixedbitset" name = "fixedbitset"
version = "0.4.2" version = "0.4.2"
@ -471,6 +601,16 @@ dependencies = [
"pin-utils", "pin-utils",
] ]
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.2.15" version = "0.2.15"
@ -1112,6 +1252,16 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkcs8"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
dependencies = [
"der",
"spki",
]
[[package]] [[package]]
name = "pkg-config" name = "pkg-config"
version = "0.3.31" version = "0.3.31"
@ -1340,6 +1490,15 @@ version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
"semver",
]
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.38.42" version = "0.38.42"
@ -1443,19 +1602,25 @@ dependencies = [
] ]
[[package]] [[package]]
name = "serde" name = "semver"
version = "1.0.216" version = "1.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03"
[[package]]
name = "serde"
version = "1.0.217"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.216" version = "1.0.217"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1486,12 +1651,45 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serde_yaml"
version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
"indexmap 2.7.0",
"itoa",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]]
name = "sha2"
version = "0.10.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]] [[package]]
name = "shlex" name = "shlex"
version = "1.3.0" version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signature"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
dependencies = [
"rand_core",
]
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.9" version = "0.4.9"
@ -1523,6 +1721,16 @@ version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
[[package]]
name = "spki"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
dependencies = [
"base64ct",
"der",
]
[[package]] [[package]]
name = "stable_deref_trait" name = "stable_deref_trait"
version = "1.2.0" version = "1.2.0"
@ -1630,6 +1838,21 @@ dependencies = [
"zerovec", "zerovec",
] ]
[[package]]
name = "tinyvec"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.42.0" version = "1.42.0"
@ -1829,12 +2052,24 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "typenum"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.14" version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
[[package]]
name = "unsafe-libyaml"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]] [[package]]
name = "untrusted" name = "untrusted"
version = "0.9.0" version = "0.9.0"
@ -1885,6 +2120,12 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]] [[package]]
name = "want" name = "want"
version = "0.3.1" version = "0.3.1"

@ -4,15 +4,17 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
chrono = "0.4.39" bs58 = "0.5.1"
dashmap = "6.1.0" chrono = { version = "0.4.39", features = ["serde"] }
dashmap = { version = "6.1.0", features = ["serde"] }
ed25519-dalek = "2.1.1"
env_logger = "0.11.6" env_logger = "0.11.6"
log = "0.4.22" log = "0.4.22"
prost = "0.13.4" prost = "0.13.4"
prost-types = "0.13.4" prost-types = "0.13.4"
reqwest = "0.12.10" reqwest = "0.12.10"
serde = { version = "1.0.216", features = ["derive"] } serde = { version = "1.0.217", features = ["derive"] }
serde_json = "1.0.134" serde_yaml = "0.9.34"
thiserror = "2.0.11" thiserror = "2.0.11"
tokio = { version = "1.42.0", features = ["macros", "rt-multi-thread"] } tokio = { version = "1.42.0", features = ["macros", "rt-multi-thread"] }
tokio-stream = "0.1.17" tokio-stream = "0.1.17"

@ -1,6 +1,6 @@
fn main() { fn main() {
tonic_build::configure() tonic_build::configure()
.build_server(true) .build_server(true)
.compile_protos(&["snp.proto"], &["proto"]) .compile_protos(&["vm.proto"], &["proto"])
.unwrap_or_else(|e| panic!("Failed to compile protos {:?}", e)); .unwrap_or_else(|e| panic!("Failed to compile protos {:?}", e));
} }

@ -1,33 +1,57 @@
#![allow(dead_code)]
use crate::grpc::snp_proto::{self as grpc}; use crate::grpc::snp_proto::{self as grpc};
use chrono::Utc; use chrono::Utc;
use dashmap::DashMap; use dashmap::DashMap;
use log::{debug, info, warn}; use log::{debug, info, warn};
use serde::{Deserialize, Serialize};
use std::str::FromStr; use std::str::FromStr;
use std::sync::RwLock; use std::sync::RwLock;
use std::{
collections::{HashMap, HashSet},
fs::File,
io::Write,
};
use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::Sender;
use tokio::sync::oneshot::Sender as OneshotSender; use tokio::sync::oneshot::Sender as OneshotSender;
const DATA_PATH: &str = "/etc/detee/brain-mock/saved_data.yaml";
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
pub enum Error { pub enum Error {
#[error("We do not allow locking of more than 100000 tokens.")] #[error("We do not allow locking of more than 100000 LP.")]
TxTooBig, TxTooBig,
#[error("Escrow must be at least 5000 LP.")]
MinimalEscrow,
#[error("Account has insufficient funds for this operation")] #[error("Account has insufficient funds for this operation")]
InsufficientFunds, InsufficientFunds,
#[error("I have no idea how this happened. Please report this bug.")]
ImpossibleError,
#[error("Could not find contract {0}")] #[error("Could not find contract {0}")]
ContractNotFound(String), VmContractNotFound(String),
#[error("This error should never happen.")]
ImpossibleError,
#[error("You don't have the required permissions for this operation.")]
AccessDenied,
} }
#[derive(Clone)] #[derive(Clone, Default, Serialize, Deserialize)]
pub struct AccountNanoTokens { pub struct AccountData {
pub balance: u64, pub balance: u64,
pub tmp_locked: u64, pub tmp_locked: u64,
// holds reasons why VMs of this account got kicked
pub kicked_for: Vec<String>,
pub last_kick: chrono::DateTime<Utc>,
// holds accounts that banned this account
pub banned_by: HashSet<String>,
} }
impl From<AccountNanoTokens> for grpc::AccountBalance { #[derive(Clone, Default, Serialize, Deserialize)]
fn from(value: AccountNanoTokens) -> Self { pub struct OperatorData {
pub escrow: u64,
pub email: String,
pub banned_users: HashSet<String>,
pub vm_nodes: HashSet<String>,
}
impl From<AccountData> for grpc::AccountBalance {
fn from(value: AccountData) -> Self {
grpc::AccountBalance { grpc::AccountBalance {
balance: value.balance, balance: value.balance,
tmp_locked: value.tmp_locked, tmp_locked: value.tmp_locked,
@ -35,10 +59,10 @@ impl From<AccountNanoTokens> for grpc::AccountBalance {
} }
} }
#[derive(Eq, Hash, PartialEq, Clone, Debug, Default)] #[derive(Eq, PartialEq, Clone, Debug, Default, Serialize, Deserialize)]
pub struct Node { pub struct VmNode {
pub public_key: String, pub public_key: String,
pub owner_key: String, pub operator_wallet: String,
pub country: String, pub country: String,
pub region: String, pub region: String,
pub city: String, pub city: String,
@ -50,27 +74,30 @@ pub struct Node {
pub avail_ipv6: u32, pub avail_ipv6: u32,
pub avail_ports: u32, pub avail_ports: u32,
pub max_ports_per_vm: u32, pub max_ports_per_vm: u32,
// nanotokens per unit per minute // nanoLP per unit per minute
pub price: u64, pub price: u64,
// 1st String is user wallet and 2nd String is report message
pub reports: HashMap<String, String>,
pub offline_minutes: u64,
} }
impl Into<grpc::NodeListResp> for Node { impl Into<grpc::VmNodeListResp> for VmNode {
fn into(self) -> grpc::NodeListResp { fn into(self) -> grpc::VmNodeListResp {
grpc::NodeListResp { grpc::VmNodeListResp {
operator: self.operator_wallet,
node_pubkey: self.public_key, node_pubkey: self.public_key,
country: self.country, country: self.country,
region: self.region, region: self.region,
city: self.city, city: self.city,
ip: self.ip, ip: self.ip,
server_rating: 0,
provider_rating: 0,
price: self.price, price: self.price,
reports: self.reports.into_values().collect(),
} }
} }
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Contract { pub struct VmContract {
pub uuid: String, pub uuid: String,
pub hostname: String, pub hostname: String,
pub admin_pubkey: String, pub admin_pubkey: String,
@ -85,34 +112,35 @@ pub struct Contract {
pub dtrfs_sha: String, pub dtrfs_sha: String,
pub created_at: chrono::DateTime<Utc>, pub created_at: chrono::DateTime<Utc>,
pub updated_at: chrono::DateTime<Utc>, pub updated_at: chrono::DateTime<Utc>,
// price per unit per minute
// recommended value is 20000 // recommended value is 20000
/// price per unit per minute
pub price_per_unit: u64, pub price_per_unit: u64,
pub locked_nano: u64, pub locked_nano: u64,
pub collected_at: chrono::DateTime<Utc>, pub collected_at: chrono::DateTime<Utc>,
} }
impl Contract { impl VmContract {
/// total hardware units of this VM
fn total_units(&self) -> u64 { fn total_units(&self) -> u64 {
// TODO: Optimize this based on price of hardware. // TODO: Optimize this based on price of hardware.
// I tried, but this can be done better. // I tried, but this can be done better.
// Storage cost should also be based on tier // Storage cost should also be based on tier
(self.vcpus as u64 * 10) (self.vcpus as u64 * 10)
+ ((self.memory_mb + 256) as u64 * 4 / 100) + ((self.memory_mb + 256) as u64 / 200)
+ (self.disk_size_gb as u64 / 10) + (self.disk_size_gb as u64 / 10)
+ (!self.public_ipv4.is_empty() as u64 * 10) + (!self.public_ipv4.is_empty() as u64 * 10)
} }
// Returns price per minute in nanotokens /// Returns price per minute in nanoLP
fn price_per_minute(&self) -> u64 { fn price_per_minute(&self) -> u64 {
self.total_units() * self.price_per_unit self.total_units() * self.price_per_unit
} }
} }
impl Into<grpc::Contract> for Contract { impl Into<grpc::VmContract> for VmContract {
fn into(self) -> grpc::Contract { fn into(self) -> grpc::VmContract {
let nano_per_minute = self.price_per_minute(); let nano_per_minute = self.price_per_minute();
grpc::Contract { grpc::VmContract {
uuid: self.uuid, uuid: self.uuid,
hostname: self.hostname, hostname: self.hostname,
admin_pubkey: self.admin_pubkey, admin_pubkey: self.admin_pubkey,
@ -134,96 +162,205 @@ impl Into<grpc::Contract> for Contract {
} }
} }
#[derive(Default)] #[derive(Default, Serialize, Deserialize)]
pub struct BrainData { pub struct BrainData {
// amount of nanotokens in each account // amount of nanoLP in each account
accounts: DashMap<String, AccountNanoTokens>, accounts: DashMap<String, AccountData>,
nodes: RwLock<Vec<Node>>, operators: DashMap<String, OperatorData>,
contracts: RwLock<Vec<Contract>>, vm_nodes: RwLock<Vec<VmNode>>,
vm_contracts: RwLock<Vec<VmContract>>,
#[serde(skip_serializing, skip_deserializing)]
tmp_newvm_reqs: DashMap<String, (grpc::NewVmReq, OneshotSender<grpc::NewVmResp>)>, tmp_newvm_reqs: DashMap<String, (grpc::NewVmReq, OneshotSender<grpc::NewVmResp>)>,
#[serde(skip_serializing, skip_deserializing)]
tmp_updatevm_reqs: DashMap<String, (grpc::UpdateVmReq, OneshotSender<grpc::UpdateVmResp>)>, tmp_updatevm_reqs: DashMap<String, (grpc::UpdateVmReq, OneshotSender<grpc::UpdateVmResp>)>,
daemon_tx: DashMap<String, Sender<grpc::BrainMessage>>, #[serde(skip_serializing, skip_deserializing)]
} daemon_tx: DashMap<String, Sender<grpc::BrainVmMessage>>,
#[derive(Debug)]
enum TxType {
CliContract,
DaemonDeleteVm,
DaemonNewVm,
} }
impl BrainData { impl BrainData {
pub fn save_to_disk(&self) -> Result<(), Box<dyn std::error::Error>> {
let mut file = File::create(DATA_PATH)?;
file.write_all(serde_yaml::to_string(self)?.as_bytes())?;
Ok(())
}
fn load_from_disk() -> Result<Self, Box<dyn std::error::Error>> {
let content = std::fs::read_to_string(DATA_PATH)?;
let data: Self = serde_yaml::from_str(&content)?;
Ok(data)
}
pub fn new() -> Self { pub fn new() -> Self {
match Self::load_from_disk() {
Ok(data) => data,
Err(e) => {
warn!("Could not data {DATA_PATH} due to error: {e:?}");
info!("Creating new instance of brain.");
Self { Self {
accounts: DashMap::new(), accounts: DashMap::new(),
nodes: RwLock::new(Vec::new()), operators: DashMap::new(),
contracts: RwLock::new(Vec::new()), vm_nodes: RwLock::new(Vec::new()),
vm_contracts: RwLock::new(Vec::new()),
tmp_newvm_reqs: DashMap::new(), tmp_newvm_reqs: DashMap::new(),
tmp_updatevm_reqs: DashMap::new(), tmp_updatevm_reqs: DashMap::new(),
daemon_tx: DashMap::new(), daemon_tx: DashMap::new(),
} }
} }
}
}
pub fn get_balance(&self, account: &str) -> AccountNanoTokens { pub fn get_balance(&self, account: &str) -> AccountData {
if let Some(account) = self.accounts.get(account) { if let Some(account) = self.accounts.get(account) {
return account.value().clone(); return account.value().clone();
} else { } else {
let balance = AccountNanoTokens { let balance = AccountData {
balance: 0, balance: 0,
tmp_locked: 0, tmp_locked: 0,
kicked_for: Vec::new(),
banned_by: HashSet::new(),
last_kick: chrono::Utc::now(),
}; };
return balance; return balance;
} }
} }
pub fn get_airdrop(&self, account: &str) { pub fn give_airdrop(&self, account: &str, tokens: u64) {
self.add_nano_to_wallet(account, 1000_000000000); warn!("Airdropping {tokens} to {account}.");
self.add_nano_to_wallet(account, tokens.saturating_mul(1_000_000_000));
} }
fn add_nano_to_wallet(&self, account: &str, nanotokens: u64) { pub fn slash_account(&self, account: &str, tokens: u64) {
log::debug!("Adding {nanotokens} nanotokens to {account}"); warn!("Slashing {tokens} from {account}.");
self.rm_nano_from_wallet(account, tokens.saturating_mul(1_000_000_000));
}
fn add_nano_to_wallet(&self, account: &str, nano_lp: u64) {
log::debug!("Adding {nano_lp} nanoLP to {account}");
self.accounts self.accounts
.entry(account.to_string()) .entry(account.to_string())
.and_modify(|tokens| tokens.balance += nanotokens) .and_modify(|d| d.balance += nano_lp)
.or_insert(AccountNanoTokens { .or_insert(AccountData {
balance: nanotokens, balance: nano_lp,
tmp_locked: 0, ..Default::default()
}); });
} }
pub fn contracts_cron(&self) { fn rm_nano_from_wallet(&self, account: &str, nano_lp: u64) {
log::debug!("Slashing {nano_lp} nanoLP to {account}");
self.accounts.entry(account.to_string()).and_modify(|d| {
d.balance = d.balance.saturating_sub(nano_lp);
});
}
/// This is written to run every minute
pub async fn vm_nodes_cron(&self) {
log::debug!("Running vm nodes cron...");
let mut nodes = self.vm_nodes.write().unwrap();
let mut vm_contracts = self.vm_contracts.write().unwrap();
for node in nodes.iter_mut() {
if self.daemon_tx.contains_key(&node.public_key) {
node.offline_minutes = 0;
continue;
}
let mut operator = match self
.operators
.iter_mut()
.find(|o| o.vm_nodes.contains(&node.public_key))
{
Some(op) => op,
None => continue,
};
node.offline_minutes += 1;
// compensate contract admin if the node is offline more then 5 minutes
if node.offline_minutes > 5 {
for c in vm_contracts
.iter()
.filter(|c| c.node_pubkey == node.public_key)
{
let compensation = c.price_per_minute() * 10;
if compensation < operator.escrow {
operator.escrow -= compensation;
self.add_nano_to_wallet(&c.admin_pubkey, compensation);
}
}
}
}
// delete nodes that are offline more than 3 hours, and clean contracts
nodes.retain(|n| {
if n.offline_minutes > 1600 {
vm_contracts.retain_mut(|c| {
if c.node_pubkey == n.public_key {
self.add_nano_to_wallet(&c.admin_pubkey, c.locked_nano);
}
c.node_pubkey != n.public_key
});
for mut op in self.operators.iter_mut() {
op.vm_nodes.remove(&n.public_key);
}
}
n.offline_minutes <= 180
});
}
pub async fn vm_contracts_cron(&self) {
let mut deleted_contracts: Vec<(String, String)> = Vec::new();
log::debug!("Running contracts cron..."); log::debug!("Running contracts cron...");
let mut contracts = self.contracts.write().unwrap(); {
let mut contracts = self.vm_contracts.write().unwrap();
contracts.retain_mut(|c| { contracts.retain_mut(|c| {
let owner_key = self let node = self.find_node_by_pubkey(&c.node_pubkey).unwrap();
.find_nodes_by_pubkey(&c.node_pubkey) if node.offline_minutes == 0 {
.unwrap() let operator_wallet = node.operator_wallet.clone();
.owner_key
.clone();
let minutes_to_collect = (Utc::now() - c.collected_at).num_minutes() as u64; let minutes_to_collect = (Utc::now() - c.collected_at).num_minutes() as u64;
c.collected_at = Utc::now(); c.collected_at = Utc::now();
log::debug!("{minutes_to_collect}"); let mut nanolp_to_collect =
let mut nanotokens_to_collect = c.price_per_minute().saturating_mul(minutes_to_collect); c.price_per_minute().saturating_mul(minutes_to_collect);
if nanotokens_to_collect > c.locked_nano { if nanolp_to_collect > c.locked_nano {
nanotokens_to_collect = c.locked_nano; nanolp_to_collect = c.locked_nano;
} }
log::debug!( log::debug!("Removing {nanolp_to_collect} nanoLP from {}", c.uuid);
"Removing {nanotokens_to_collect} nanotokens from {}", c.locked_nano -= nanolp_to_collect;
c.uuid let escrow_multiplier = match self.operators.get(&operator_wallet) {
Some(op) if op.escrow > 5000 => match self.operators.get(&c.admin_pubkey) {
Some(user_is_op) if user_is_op.escrow > 5000 => 1,
_ => 5,
},
_ => 1,
};
self.add_nano_to_wallet(
&operator_wallet,
nanolp_to_collect * escrow_multiplier,
); );
c.locked_nano -= nanotokens_to_collect; if c.locked_nano == 0 {
self.add_nano_to_wallet(&owner_key, nanotokens_to_collect); deleted_contracts.push((c.uuid.clone(), c.node_pubkey.clone()));
}
}
c.locked_nano > 0 c.locked_nano > 0
}); });
} }
// inform daemons of the deletion of the contracts
for (uuid, node_pubkey) in deleted_contracts.iter() {
if let Some(daemon_tx) = self.daemon_tx.get(&node_pubkey.clone()) {
let msg = grpc::BrainVmMessage {
msg: Some(grpc::brain_vm_message::Msg::DeleteVm(grpc::DeleteVmReq {
uuid: uuid.to_string(),
admin_pubkey: String::new(),
})),
};
let daemon_tx = daemon_tx.clone();
let _ = daemon_tx.send(msg).await;
}
}
}
pub fn insert_node(&self, node: Node) { pub fn register_node(&self, node: VmNode) {
info!("Registering node {node:?}"); info!("Registering node {node:?}");
let mut nodes = self.nodes.write().unwrap(); self.add_vmnode_to_operator(&node.operator_wallet, &node.public_key);
let mut nodes = self.vm_nodes.write().unwrap();
for n in nodes.iter_mut() { for n in nodes.iter_mut() {
if n.public_key == node.public_key { if n.public_key == node.public_key {
// TODO: figure what to do in this case. // TODO: figure what to do in this case.
warn!("Node {} already exists. Updating data.", n.public_key); warn!("VM Node {} already exists. Updating data.", n.public_key);
*n = node; *n = node;
return; return;
} }
@ -231,69 +368,154 @@ impl BrainData {
nodes.push(node); nodes.push(node);
} }
pub fn lock_nanotockens(&self, account: &str, nanotokens: u64) -> Result<(), Error> { // todo: this should also support Apps
if nanotokens > 100_000_000_000_000 { /// Receives: operator, contract uuid, reason of kick
pub async fn kick_contract(
&self,
operator: &str,
uuid: &str,
reason: &str,
) -> Result<u64, Error> {
log::debug!("Operator {operator} requested a kick of {uuid} for reason: {reason}");
let contract = self.find_contract_by_uuid(uuid)?;
let mut operator_data = self
.operators
.get_mut(operator)
.ok_or(Error::AccessDenied)?;
if !operator_data.vm_nodes.contains(&contract.node_pubkey) {
return Err(Error::AccessDenied);
}
let mut minutes_to_refund = chrono::Utc::now()
.signed_duration_since(contract.updated_at)
.num_minutes()
.abs() as u64;
// cap refund at 1 week
if minutes_to_refund > 10080 {
minutes_to_refund = 10080;
}
let mut refund_amount = minutes_to_refund * contract.price_per_minute();
let mut admin_account = self
.accounts
.get_mut(&contract.admin_pubkey)
.ok_or(Error::ImpossibleError)?;
// check if he got kicked within the last day
if !chrono::Utc::now()
.signed_duration_since(admin_account.last_kick)
.gt(&chrono::Duration::days(1))
{
refund_amount = 0;
}
if operator_data.escrow < refund_amount {
refund_amount = operator_data.escrow;
}
log::debug!(
"Removing {refund_amount} escrow from {} and giving it to {}",
operator_data.key(),
admin_account.key()
);
admin_account.balance += refund_amount;
admin_account.kicked_for.push(reason.to_string());
operator_data.escrow -= refund_amount;
let admin_pubkey = contract.admin_pubkey.clone();
drop(admin_account);
drop(contract);
self.delete_vm(grpc::DeleteVmReq {
uuid: uuid.to_string(),
admin_pubkey,
})
.await?;
Ok(refund_amount)
}
pub fn ban_user(&self, operator: &str, user: &str) {
self.accounts
.entry(user.to_string())
.and_modify(|a| {
a.banned_by.insert(operator.to_string());
})
.or_insert(AccountData {
banned_by: HashSet::from([operator.to_string()]),
..Default::default()
});
self.operators
.entry(operator.to_string())
.and_modify(|o| {
o.banned_users.insert(user.to_string());
})
.or_insert(OperatorData {
banned_users: HashSet::from([user.to_string()]),
..Default::default()
});
}
pub fn report_node(&self, admin_pubkey: String, node: &str, report: String) {
let mut nodes = self.vm_nodes.write().unwrap();
if let Some(node) = nodes.iter_mut().find(|n| n.public_key == node) {
node.reports.insert(admin_pubkey, report);
}
}
pub fn lock_nanotockens(&self, account: &str, nano_lp: u64) -> Result<(), Error> {
if nano_lp > 100_000_000_000_000 {
return Err(Error::TxTooBig); return Err(Error::TxTooBig);
} }
if let Some(mut account) = self.accounts.get_mut(account) { if let Some(mut account) = self.accounts.get_mut(account) {
if nanotokens > account.balance { if nano_lp > account.balance {
return Err(Error::InsufficientFunds); return Err(Error::InsufficientFunds);
} }
account.balance = account.balance.saturating_sub(nanotokens); account.balance = account.balance.saturating_sub(nano_lp);
account.tmp_locked = account.tmp_locked.saturating_add(nanotokens); account.tmp_locked = account.tmp_locked.saturating_add(nano_lp);
Ok(()) Ok(())
} else { } else {
Err(Error::InsufficientFunds) Err(Error::InsufficientFunds)
} }
} }
pub fn unlock_nanotockens(&self, account: &str, nanotokens: u64) -> Result<(), Error> { pub fn extend_vm_contract_time(
if let Some(mut account) = self.accounts.get_mut(account) {
if nanotokens > account.tmp_locked {
return Err(Error::ImpossibleError);
}
account.balance = account.balance.saturating_add(nanotokens);
account.tmp_locked = account.tmp_locked.saturating_sub(nanotokens);
Ok(())
} else {
Err(Error::ImpossibleError)
}
}
pub fn extend_contract_time(
&self, &self,
uuid: &str, uuid: &str,
account: &str, wallet: &str,
nanotokens: u64, nano_lp: u64,
) -> Result<(), Error> { ) -> Result<(), Error> {
if nanotokens > 100_000_000_000_000 { if nano_lp > 100_000_000_000_000 {
return Err(Error::TxTooBig); return Err(Error::TxTooBig);
} }
let mut account = match self.accounts.get_mut(account) { let mut account = match self.accounts.get_mut(wallet) {
Some(account) => account, Some(account) => account,
None => return Err(Error::InsufficientFunds), None => return Err(Error::InsufficientFunds),
}; };
match self match self
.contracts .vm_contracts
.write() .write()
.unwrap() .unwrap()
.iter_mut() .iter_mut()
.find(|c| c.uuid == uuid) .find(|c| c.uuid == uuid)
{ {
Some(contract) => { Some(contract) => {
if account.balance + contract.locked_nano < nanotokens { if contract.admin_pubkey != wallet {
return Err(Error::VmContractNotFound(uuid.to_string()));
}
if account.balance + contract.locked_nano < nano_lp {
return Err(Error::InsufficientFunds); return Err(Error::InsufficientFunds);
} }
account.balance = account.balance + contract.locked_nano - nanotokens; account.balance = account.balance + contract.locked_nano - nano_lp;
contract.locked_nano = nanotokens; contract.locked_nano = nano_lp;
Ok(()) Ok(())
} }
None => Err(Error::ContractNotFound(uuid.to_string())), None => Err(Error::VmContractNotFound(uuid.to_string())),
} }
} }
pub fn submit_node_resources(&self, res: grpc::NodeResources) { pub fn submit_node_resources(&self, res: grpc::VmNodeResources) {
let mut nodes = self.nodes.write().unwrap(); let mut nodes = self.vm_nodes.write().unwrap();
for n in nodes.iter_mut() { for n in nodes.iter_mut() {
if n.public_key == res.node_pubkey { if n.public_key == res.node_pubkey {
debug!( debug!(
@ -311,13 +533,13 @@ impl BrainData {
} }
} }
debug!( debug!(
"Node {} not found when trying to update resources.", "VM Node {} not found when trying to update resources.",
res.node_pubkey res.node_pubkey
); );
debug!("Node list:\n{:?}", nodes); debug!("VM Node list:\n{:?}", nodes);
} }
pub fn add_daemon_tx(&self, node_pubkey: &str, tx: Sender<grpc::BrainMessage>) { pub fn add_daemon_tx(&self, node_pubkey: &str, tx: Sender<grpc::BrainVmMessage>) {
self.daemon_tx.insert(node_pubkey.to_string(), tx); self.daemon_tx.insert(node_pubkey.to_string(), tx);
} }
@ -325,16 +547,20 @@ impl BrainData {
self.daemon_tx.remove(node_pubkey); self.daemon_tx.remove(node_pubkey);
} }
pub async fn delete_vm(&self, delete_vm: grpc::DeleteVmReq) { pub async fn delete_vm(&self, delete_vm: grpc::DeleteVmReq) -> Result<(), Error> {
if let Some(contract) = self.find_contract_by_uuid(&delete_vm.uuid) { log::debug!("Starting deletion of VM {}", delete_vm.uuid);
let contract = self.find_contract_by_uuid(&delete_vm.uuid)?;
if contract.admin_pubkey != delete_vm.admin_pubkey {
return Err(Error::AccessDenied);
}
info!("Found vm {}. Deleting...", delete_vm.uuid); info!("Found vm {}. Deleting...", delete_vm.uuid);
if let Some(daemon_tx) = self.daemon_tx.get(&contract.node_pubkey) { if let Some(daemon_tx) = self.daemon_tx.get(&contract.node_pubkey) {
debug!( debug!(
"TX for daemon {} found. Informing daemon about deletion of {}.", "TX for daemon {} found. Informing daemon about deletion of {}.",
contract.node_pubkey, delete_vm.uuid contract.node_pubkey, delete_vm.uuid
); );
let msg = grpc::BrainMessage { let msg = grpc::BrainVmMessage {
msg: Some(grpc::brain_message::Msg::DeleteVm(delete_vm.clone())), msg: Some(grpc::brain_vm_message::Msg::DeleteVm(delete_vm.clone())),
}; };
if let Err(e) = daemon_tx.send(msg).await { if let Err(e) = daemon_tx.send(msg).await {
warn!( warn!(
@ -347,11 +573,12 @@ impl BrainData {
} }
self.add_nano_to_wallet(&contract.admin_pubkey, contract.locked_nano); self.add_nano_to_wallet(&contract.admin_pubkey, contract.locked_nano);
let mut contracts = self.contracts.write().unwrap(); let mut contracts = self.vm_contracts.write().unwrap();
contracts.retain(|c| c.uuid != delete_vm.uuid); contracts.retain(|c| c.uuid != delete_vm.uuid);
} Ok(())
} }
// also unlocks nanotokens in case VM creation failed
pub async fn submit_newvm_resp(&self, new_vm_resp: grpc::NewVmResp) { pub async fn submit_newvm_resp(&self, new_vm_resp: grpc::NewVmResp) {
let new_vm_req = match self.tmp_newvm_reqs.remove(&new_vm_resp.uuid) { let new_vm_req = match self.tmp_newvm_reqs.remove(&new_vm_resp.uuid) {
Some((_, r)) => r, Some((_, r)) => r,
@ -398,7 +625,7 @@ impl BrainData {
admin_wallet.tmp_locked -= new_vm_req.0.locked_nano; admin_wallet.tmp_locked -= new_vm_req.0.locked_nano;
} }
let contract = Contract { let contract = VmContract {
uuid: new_vm_resp.uuid, uuid: new_vm_resp.uuid,
exposed_ports: args.exposed_ports.clone(), exposed_ports: args.exposed_ports.clone(),
public_ipv4, public_ipv4,
@ -418,7 +645,7 @@ impl BrainData {
collected_at: Utc::now(), collected_at: Utc::now(),
}; };
info!("Created new contract: {contract:?}"); info!("Created new contract: {contract:?}");
self.contracts.write().unwrap().push(contract); self.vm_contracts.write().unwrap().push(contract);
} }
pub async fn submit_updatevm_resp(&self, mut update_vm_resp: grpc::UpdateVmResp) { pub async fn submit_updatevm_resp(&self, mut update_vm_resp: grpc::UpdateVmResp) {
@ -434,10 +661,15 @@ impl BrainData {
return; return;
} }
}; };
if let Err(e) = update_vm_req.1.send(update_vm_resp.clone()) {
log::warn!(
"CLI RX dropped before receiving UpdateVMResp {update_vm_resp:?}. Error: {e:?}"
);
}
if update_vm_resp.error != "" { if update_vm_resp.error != "" {
return; return;
} }
let mut contracts = self.contracts.write().unwrap(); let mut contracts = self.vm_contracts.write().unwrap();
match contracts.iter_mut().find(|c| c.uuid == update_vm_resp.uuid) { match contracts.iter_mut().find(|c| c.uuid == update_vm_resp.uuid) {
Some(contract) => { Some(contract) => {
if update_vm_req.0.vcpus != 0 { if update_vm_req.0.vcpus != 0 {
@ -466,15 +698,10 @@ impl BrainData {
contract.updated_at = Utc::now(); contract.updated_at = Utc::now();
} }
None => { None => {
log::error!("Contract not found for {}.", update_vm_req.0.uuid); log::error!("VM Contract not found for {}.", update_vm_req.0.uuid);
update_vm_resp.error = "Contract not found.".to_string(); update_vm_resp.error = "VM Contract not found.".to_string();
} }
} }
if let Err(e) = update_vm_req.1.send(update_vm_resp.clone()) {
log::warn!(
"CLI RX dropped before receiving UpdateVMResp {update_vm_resp:?}. Error: {e:?}"
);
}
} }
pub async fn submit_newvm_req( pub async fn submit_newvm_req(
@ -499,8 +726,8 @@ impl BrainData {
"Found daemon TX for {}. Sending newVMReq {}", "Found daemon TX for {}. Sending newVMReq {}",
req.node_pubkey, req.uuid req.node_pubkey, req.uuid
); );
let msg = grpc::BrainMessage { let msg = grpc::BrainVmMessage {
msg: Some(grpc::brain_message::Msg::NewVmReq(req.clone())), msg: Some(grpc::brain_vm_message::Msg::NewVmReq(req.clone())),
}; };
if let Err(e) = daemon_tx.send(msg).await { if let Err(e) = daemon_tx.send(msg).await {
warn!( warn!(
@ -516,6 +743,13 @@ impl BrainData {
}) })
.await; .await;
} }
} else {
self.submit_newvm_resp(grpc::NewVmResp {
error: "Daemon is offline.".to_string(),
uuid: req.uuid,
args: None,
})
.await;
} }
} }
@ -527,15 +761,25 @@ impl BrainData {
let uuid = req.uuid.clone(); let uuid = req.uuid.clone();
info!("Inserting new vm update request in memory: {req:?}"); info!("Inserting new vm update request in memory: {req:?}");
let node_pubkey = match self.find_contract_by_uuid(&req.uuid) { let node_pubkey = match self.find_contract_by_uuid(&req.uuid) {
Some(contract) => contract.node_pubkey, Ok(contract) => {
None => { if contract.admin_pubkey != req.admin_pubkey {
let _ = tx.send(grpc::UpdateVmResp {
uuid,
error: "VM Contract does not exist.".to_string(),
args: None,
});
return;
}
contract.node_pubkey
}
Err(_) => {
log::warn!( log::warn!(
"Received UpdateVMReq for a contract that does not exist: {}", "Received UpdateVMReq for a contract that does not exist: {}",
req.uuid req.uuid
); );
let _ = tx.send(grpc::UpdateVmResp { let _ = tx.send(grpc::UpdateVmResp {
uuid, uuid,
error: "Contract does not exist.".to_string(), error: "VM Contract does not exist.".to_string(),
args: None, args: None,
}); });
return; return;
@ -548,8 +792,8 @@ impl BrainData {
"Found daemon TX for {}. Sending updateVMReq {}", "Found daemon TX for {}. Sending updateVMReq {}",
node_pubkey, req.uuid node_pubkey, req.uuid
); );
let msg = grpc::BrainMessage { let msg = grpc::BrainVmMessage {
msg: Some(grpc::brain_message::Msg::UpdateVmReq(req.clone())), msg: Some(grpc::brain_vm_message::Msg::UpdateVmReq(req.clone())),
}; };
match server_tx.send(msg).await { match server_tx.send(msg).await {
Ok(_) => { Ok(_) => {
@ -579,26 +823,85 @@ impl BrainData {
} }
} }
pub fn insert_contract(&self, contract: Contract) { pub fn find_node_by_pubkey(&self, public_key: &str) -> Option<VmNode> {
let mut contracts = self.contracts.write().unwrap(); let nodes = self.vm_nodes.read().unwrap();
contracts.push(contract);
}
pub fn find_nodes_by_pubkey(&self, public_key: &str) -> Option<Node> {
let nodes = self.nodes.read().unwrap();
nodes.iter().cloned().find(|n| n.public_key == public_key) nodes.iter().cloned().find(|n| n.public_key == public_key)
} }
pub fn find_ns_by_owner_key(&self, owner_key: &str) -> Option<Node> { pub fn is_user_banned_by_node(&self, user_wallet: &str, node_pubkey: &str) -> bool {
let nodes = self.nodes.read().unwrap(); if let Some(node) = self.find_node_by_pubkey(&node_pubkey) {
nodes.iter().cloned().find(|n| n.owner_key == owner_key) if let Some(account) = self.accounts.get(user_wallet) {
if account.banned_by.contains(&node.operator_wallet) {
return true;
}
}
}
false
} }
pub fn find_nodes_by_filters( pub fn add_vmnode_to_operator(&self, operator_wallet: &str, node_pubkey: &str) {
self.operators
.entry(operator_wallet.to_string())
.and_modify(|op| {
op.vm_nodes.insert(node_pubkey.to_string());
})
.or_insert(OperatorData {
escrow: 0,
email: String::new(),
banned_users: HashSet::new(),
vm_nodes: HashSet::from([node_pubkey.to_string()]),
});
}
pub fn register_operator(&self, req: grpc::RegOperatorReq) -> Result<(), Error> {
let mut operator = match self.operators.get(&req.pubkey) {
Some(o) => (*(o.value())).clone(),
None => OperatorData {
..Default::default()
},
};
if req.escrow < 5000 {
return Err(Error::MinimalEscrow);
}
let escrow = req.escrow * 1_000_000_000;
if let Some(mut account) = self.accounts.get_mut(&req.pubkey) {
if (account.balance + operator.escrow) < escrow {
return Err(Error::InsufficientFunds);
}
account.balance = account.balance + operator.escrow - escrow;
operator.escrow = escrow;
} else {
return Err(Error::InsufficientFunds);
}
operator.email = req.email;
self.operators.insert(req.pubkey, operator);
Ok(())
}
pub fn find_vm_nodes_by_operator(&self, operator_wallet: &str) -> Vec<VmNode> {
let nodes = self.vm_nodes.read().unwrap();
nodes
.iter()
.filter(|node| node.operator_wallet == operator_wallet)
.cloned()
.collect()
}
pub fn total_operator_reports(&self, operator_wallet: &str) -> usize {
let nodes = self.vm_nodes.read().unwrap();
nodes
.iter()
.cloned()
.filter(|n| n.operator_wallet == operator_wallet)
.map(|node| node.reports.len())
.sum()
}
pub fn find_vm_nodes_by_filters(
&self, &self,
filters: &crate::grpc::snp_proto::NodeFilters, filters: &crate::grpc::snp_proto::VmNodeFilters,
) -> Vec<Node> { ) -> Vec<VmNode> {
let nodes = self.nodes.read().unwrap(); let nodes = self.vm_nodes.read().unwrap();
nodes nodes
.iter() .iter()
.filter(|n| { .filter(|n| {
@ -620,9 +923,9 @@ impl BrainData {
// TODO: sort by rating // TODO: sort by rating
pub fn get_one_node_by_filters( pub fn get_one_node_by_filters(
&self, &self,
filters: &crate::grpc::snp_proto::NodeFilters, filters: &crate::grpc::snp_proto::VmNodeFilters,
) -> Option<Node> { ) -> Option<VmNode> {
let nodes = self.nodes.read().unwrap(); let nodes = self.vm_nodes.read().unwrap();
nodes nodes
.iter() .iter()
.find(|n| { .find(|n| {
@ -641,27 +944,98 @@ impl BrainData {
.cloned() .cloned()
} }
pub fn find_contract_by_uuid(&self, uuid: &str) -> Option<Contract> { pub fn find_contract_by_uuid(&self, uuid: &str) -> Result<VmContract, Error> {
let contracts = self.contracts.read().unwrap(); let contracts = self.vm_contracts.read().unwrap();
contracts.iter().cloned().find(|c| c.uuid == uuid) contracts
.iter()
.cloned()
.find(|c| c.uuid == uuid)
.ok_or(Error::VmContractNotFound(uuid.to_string()))
} }
pub fn find_contracts_by_admin_pubkey(&self, admin_pubkey: &str) -> Vec<Contract> { pub fn list_all_contracts(&self) -> Vec<VmContract> {
debug!("Searching contracts for admin pubkey {admin_pubkey}"); let contracts = self.vm_contracts.read().unwrap();
let contracts: Vec<Contract> = self contracts.iter().cloned().collect()
.contracts }
pub fn list_accounts(&self) -> Vec<grpc::Account> {
self.accounts
.iter()
.map(|a| grpc::Account {
pubkey: a.key().to_string(),
balance: a.balance,
tmp_locked: a.tmp_locked,
})
.collect()
}
pub fn list_operators(&self) -> Vec<grpc::ListOperatorsResp> {
self.operators
.iter()
.map(|op| grpc::ListOperatorsResp {
pubkey: op.key().to_string(),
escrow: op.escrow / 1_000_000_000,
email: op.email.clone(),
app_nodes: 0,
vm_nodes: op.vm_nodes.len() as u64,
reports: self.total_operator_reports(op.key()) as u64,
})
.collect()
}
pub fn inspect_operator(&self, wallet: &str) -> Option<grpc::InspectOperatorResp> {
self.operators.get(wallet).map(|op| {
let nodes = self
.find_vm_nodes_by_operator(wallet)
.into_iter()
.map(|n| n.into())
.collect();
grpc::InspectOperatorResp {
operator: Some(grpc::ListOperatorsResp {
pubkey: op.key().to_string(),
escrow: op.escrow,
email: op.email.clone(),
app_nodes: 0,
vm_nodes: op.vm_nodes.len() as u64,
reports: self.total_operator_reports(op.key()) as u64,
}),
nodes,
}
})
}
pub fn find_vm_contracts_by_operator(&self, wallet: &str) -> Vec<VmContract> {
debug!("Searching contracts for operator {wallet}");
let nodes = match self.operators.get(wallet) {
Some(op) => op.vm_nodes.clone(),
None => return Vec::new(),
};
let contracts: Vec<VmContract> = self
.vm_contracts
.read() .read()
.unwrap() .unwrap()
.iter() .iter()
.filter(|c| c.admin_pubkey == admin_pubkey) .filter(|c| nodes.contains(&c.node_pubkey))
.cloned() .cloned()
.collect(); .collect();
debug!("Found {} contracts or {admin_pubkey}.", contracts.len());
contracts contracts
} }
pub fn find_contracts_by_node_pubkey(&self, node_pubkey: &str) -> Vec<Contract> { pub fn find_vm_contracts_by_admin(&self, admin_wallet: &str) -> Vec<VmContract> {
let contracts = self.contracts.read().unwrap(); debug!("Searching contracts for admin pubkey {admin_wallet}");
let contracts: Vec<VmContract> = self
.vm_contracts
.read()
.unwrap()
.iter()
.filter(|c| c.admin_pubkey == admin_wallet)
.cloned()
.collect();
contracts
}
pub fn find_vm_contracts_by_node(&self, node_pubkey: &str) -> Vec<VmContract> {
let contracts = self.vm_contracts.read().unwrap();
contracts contracts
.iter() .iter()
.filter(|c| c.node_pubkey == node_pubkey) .filter(|c| c.node_pubkey == node_pubkey)

@ -1,13 +1,12 @@
#![allow(dead_code)]
pub mod snp_proto { pub mod snp_proto {
tonic::include_proto!("snp_proto"); tonic::include_proto!("vm_proto");
} }
use crate::data::BrainData; use crate::data::BrainData;
use crate::grpc::vm_daemon_message;
use log::info; use log::info;
use snp_proto::brain_cli_server::BrainCli; use snp_proto::brain_cli_server::BrainCli;
use snp_proto::brain_daemon_server::BrainDaemon; use snp_proto::brain_vm_daemon_server::BrainVmDaemon;
use snp_proto::*; use snp_proto::*;
use std::pin::Pin; use std::pin::Pin;
use std::sync::Arc; use std::sync::Arc;
@ -15,6 +14,12 @@ use tokio::sync::mpsc;
use tokio_stream::{wrappers::ReceiverStream, Stream, StreamExt}; use tokio_stream::{wrappers::ReceiverStream, Stream, StreamExt};
use tonic::{Request, Response, Status, Streaming}; use tonic::{Request, Response, Status, Streaming};
const ADMIN_ACCOUNTS: &[&str] = &[
"x52w7jARC5erhWWK65VZmjdGXzBK6ZDgfv1A283d8XK",
"FHuecMbeC1PfjkW2JKyoicJAuiU7khgQT16QUB3Q1XdL",
"H21Shi4iE7vgfjWEQNvzmpmBMJSaiZ17PYUcdNoAoKNc",
];
pub struct BrainDaemonMock { pub struct BrainDaemonMock {
data: Arc<BrainData>, data: Arc<BrainData>,
} }
@ -36,17 +41,17 @@ impl BrainCliMock {
} }
#[tonic::async_trait] #[tonic::async_trait]
impl BrainDaemon for BrainDaemonMock { impl BrainVmDaemon for BrainDaemonMock {
type RegisterNodeStream = Pin<Box<dyn Stream<Item = Result<Contract, Status>> + Send>>; type RegisterVmNodeStream = Pin<Box<dyn Stream<Item = Result<VmContract, Status>> + Send>>;
async fn register_node( async fn register_vm_node(
&self, &self,
req: Request<RegisterNodeReq>, req: Request<RegisterVmNodeReq>,
) -> Result<Response<Self::RegisterNodeStream>, Status> { ) -> Result<Response<Self::RegisterVmNodeStream>, Status> {
let req = req.into_inner(); let req = check_sig_from_req(req)?;
info!("Starting registration process for {:?}", req); info!("Starting registration process for {:?}", req);
let node = crate::data::Node { let node = crate::data::VmNode {
public_key: req.node_pubkey.clone(), public_key: req.node_pubkey.clone(),
owner_key: req.owner_pubkey, operator_wallet: req.operator_wallet,
country: req.country, country: req.country,
region: req.region, region: req.region,
city: req.city, city: req.city,
@ -54,10 +59,10 @@ impl BrainDaemon for BrainDaemonMock {
price: req.price, price: req.price,
..Default::default() ..Default::default()
}; };
self.data.insert_node(node); self.data.register_node(node);
info!("Sending existing contracts to {}", req.node_pubkey); info!("Sending existing contracts to {}", req.node_pubkey);
let contracts = self.data.find_contracts_by_node_pubkey(&req.node_pubkey); let contracts = self.data.find_vm_contracts_by_node(&req.node_pubkey);
let (tx, rx) = mpsc::channel(6); let (tx, rx) = mpsc::channel(6);
tokio::spawn(async move { tokio::spawn(async move {
for contract in contracts { for contract in contracts {
@ -66,19 +71,26 @@ impl BrainDaemon for BrainDaemonMock {
}); });
let output_stream = ReceiverStream::new(rx); let output_stream = ReceiverStream::new(rx);
Ok(Response::new( Ok(Response::new(
Box::pin(output_stream) as Self::RegisterNodeStream Box::pin(output_stream) as Self::RegisterVmNodeStream
)) ))
} }
type BrainMessagesStream = Pin<Box<dyn Stream<Item = Result<BrainMessage, Status>> + Send>>; type BrainMessagesStream = Pin<Box<dyn Stream<Item = Result<BrainVmMessage, Status>> + Send>>;
async fn brain_messages( async fn brain_messages(
&self, &self,
req: Request<Pubkey>, req: Request<DaemonStreamAuth>,
) -> Result<Response<Self::BrainMessagesStream>, Status> { ) -> Result<Response<Self::BrainMessagesStream>, Status> {
let req = req.into_inner(); let auth = req.into_inner();
info!("Daemon {} connected to receive brain messages", req.pubkey); let pubkey = auth.pubkey.clone();
check_sig_from_parts(
&pubkey,
&auth.timestamp,
&format!("{:?}", auth.contracts),
&auth.signature,
)?;
info!("Daemon {} connected to receive brain messages", pubkey);
let (tx, rx) = mpsc::channel(6); let (tx, rx) = mpsc::channel(6);
self.data.add_daemon_tx(&req.pubkey, tx); self.data.add_daemon_tx(&pubkey, tx);
let output_stream = ReceiverStream::new(rx).map(|msg| Ok(msg)); let output_stream = ReceiverStream::new(rx).map(|msg| Ok(msg));
Ok(Response::new( Ok(Response::new(
Box::pin(output_stream) as Self::BrainMessagesStream Box::pin(output_stream) as Self::BrainMessagesStream
@ -87,27 +99,46 @@ impl BrainDaemon for BrainDaemonMock {
async fn daemon_messages( async fn daemon_messages(
&self, &self,
req: Request<Streaming<DaemonMessage>>, req: Request<Streaming<VmDaemonMessage>>,
) -> Result<Response<Empty>, Status> { ) -> Result<Response<Empty>, Status> {
let mut req_stream = req.into_inner(); let mut req_stream = req.into_inner();
let mut pubkey = String::new(); let pubkey: String;
if let Some(Ok(msg)) = req_stream.next().await {
log::debug!(
"demon_messages received the following auth message: {:?}",
msg.msg
);
if let Some(vm_daemon_message::Msg::Auth(auth)) = msg.msg {
pubkey = auth.pubkey.clone();
check_sig_from_parts(
&pubkey,
&auth.timestamp,
&format!("{:?}", auth.contracts),
&auth.signature,
)?;
} else {
return Err(Status::unauthenticated(
"Could not authenticate the daemon: could not extract auth signature",
));
}
} else {
return Err(Status::unauthenticated("Could not authenticate the daemon"));
}
// info!("Received a message from daemon {pubkey}: {daemon_message:?}");
while let Some(daemon_message) = req_stream.next().await { while let Some(daemon_message) = req_stream.next().await {
info!("Received a message from daemon {pubkey}: {daemon_message:?}");
match daemon_message { match daemon_message {
Ok(msg) => match msg.msg { Ok(msg) => match msg.msg {
Some(daemon_message::Msg::Pubkey(p)) => { Some(vm_daemon_message::Msg::NewVmResp(new_vm_resp)) => {
pubkey = p.pubkey;
}
Some(daemon_message::Msg::NewVmResp(new_vm_resp)) => {
self.data.submit_newvm_resp(new_vm_resp).await; self.data.submit_newvm_resp(new_vm_resp).await;
} }
Some(daemon_message::Msg::UpdateVmResp(update_vm_resp)) => { Some(vm_daemon_message::Msg::UpdateVmResp(update_vm_resp)) => {
self.data.submit_updatevm_resp(update_vm_resp).await; self.data.submit_updatevm_resp(update_vm_resp).await;
} }
Some(daemon_message::Msg::NodeResources(node_resources)) => { Some(vm_daemon_message::Msg::VmNodeResources(node_resources)) => {
self.data.submit_node_resources(node_resources); self.data.submit_node_resources(node_resources);
} }
None => {} _ => {}
}, },
Err(e) => { Err(e) => {
log::warn!("Daemon disconnected: {e:?}"); log::warn!("Daemon disconnected: {e:?}");
@ -122,19 +153,21 @@ impl BrainDaemon for BrainDaemonMock {
#[tonic::async_trait] #[tonic::async_trait]
impl BrainCli for BrainCliMock { impl BrainCli for BrainCliMock {
async fn get_balance(&self, req: Request<Pubkey>) -> Result<Response<AccountBalance>, Status> { async fn get_balance(&self, req: Request<Pubkey>) -> Result<Response<AccountBalance>, Status> {
Ok(Response::new( let req = check_sig_from_req(req)?;
self.data.get_balance(&req.into_inner().pubkey).into(), Ok(Response::new(self.data.get_balance(&req.pubkey).into()))
))
}
async fn get_airdrop(&self, req: Request<Pubkey>) -> Result<Response<Empty>, Status> {
self.data.get_airdrop(&req.into_inner().pubkey);
Ok(Response::new(Empty {}))
} }
async fn new_vm(&self, req: Request<NewVmReq>) -> Result<Response<NewVmResp>, Status> { async fn new_vm(&self, req: Request<NewVmReq>) -> Result<Response<NewVmResp>, Status> {
let req = req.into_inner(); let req = check_sig_from_req(req)?;
info!("New VM requested via CLI: {req:?}"); info!("New VM requested via CLI: {req:?}");
if self
.data
.is_user_banned_by_node(&req.admin_pubkey, &req.node_pubkey)
{
return Err(Status::permission_denied(
"This operator banned you. What did you do?",
));
}
let admin_pubkey = req.admin_pubkey.clone(); let admin_pubkey = req.admin_pubkey.clone();
let (oneshot_tx, oneshot_rx) = tokio::sync::oneshot::channel(); let (oneshot_tx, oneshot_rx) = tokio::sync::oneshot::channel();
self.data.submit_newvm_req(req, oneshot_tx).await; self.data.submit_newvm_req(req, oneshot_tx).await;
@ -153,7 +186,7 @@ impl BrainCli for BrainCliMock {
} }
async fn update_vm(&self, req: Request<UpdateVmReq>) -> Result<Response<UpdateVmResp>, Status> { async fn update_vm(&self, req: Request<UpdateVmReq>) -> Result<Response<UpdateVmResp>, Status> {
let req = req.into_inner(); let req = check_sig_from_req(req)?;
info!("Update VM requested via CLI: {req:?}"); info!("Update VM requested via CLI: {req:?}");
let (oneshot_tx, oneshot_rx) = tokio::sync::oneshot::channel(); let (oneshot_tx, oneshot_rx) = tokio::sync::oneshot::channel();
self.data.submit_updatevm_req(req, oneshot_tx).await; self.data.submit_updatevm_req(req, oneshot_tx).await;
@ -162,39 +195,72 @@ impl BrainCli for BrainCliMock {
info!("Sending UpdateVMResp: {response:?}"); info!("Sending UpdateVMResp: {response:?}");
Ok(Response::new(response)) Ok(Response::new(response))
} }
Err(e) => { Err(e) => Err(Status::unknown(format!(
Err(Status::unknown( "Update VM request failed due to error: {e}"
"Update VM request failed due to error: {e}", ))),
))
}
} }
} }
async fn extend_vm(&self, req: Request<ExtendVmReq>) -> Result<Response<Empty>, Status> { async fn extend_vm(&self, req: Request<ExtendVmReq>) -> Result<Response<Empty>, Status> {
let req = req.into_inner(); let req = check_sig_from_req(req)?;
match self match self
.data .data
.extend_contract_time(&req.uuid, &req.admin_pubkey, req.locked_nano) .extend_vm_contract_time(&req.uuid, &req.admin_pubkey, req.locked_nano)
{ {
Ok(()) => Ok(Response::new(Empty {})), Ok(()) => Ok(Response::new(Empty {})),
Err(e) => Err(Status::unknown(format!("Could not extend contract: {e}"))), Err(e) => Err(Status::unknown(format!("Could not extend contract: {e}"))),
} }
} }
type ListContractsStream = Pin<Box<dyn Stream<Item = Result<Contract, Status>> + Send>>; async fn delete_vm(&self, req: Request<DeleteVmReq>) -> Result<Response<Empty>, Status> {
async fn list_contracts( let req = check_sig_from_req(req)?;
&self, match self.data.delete_vm(req).await {
req: Request<ListContractsReq>, Ok(()) => Ok(Response::new(Empty {})),
) -> Result<Response<Self::ListContractsStream>, Status> { Err(e) => Err(Status::not_found(e.to_string())),
let req = req.into_inner(); }
info!("CLI {} requested ListVMContractsStream", req.admin_pubkey); }
let contracts = match req.uuid.is_empty() {
false => match self.data.find_contract_by_uuid(&req.uuid) { async fn report_node(&self, req: Request<ReportNodeReq>) -> Result<Response<Empty>, Status> {
Some(contract) => vec![contract], let req = check_sig_from_req(req)?;
None => Vec::new(), match self.data.find_contract_by_uuid(&req.contract) {
}, Ok(contract)
true => self.data.find_contracts_by_admin_pubkey(&req.admin_pubkey), if contract.admin_pubkey == req.admin_pubkey
&& contract.node_pubkey == req.node_pubkey =>
{
()
}
_ => return Err(Status::unauthenticated("No contract found by this ID.")),
}; };
self.data
.report_node(req.admin_pubkey, &req.node_pubkey, req.reason);
Ok(Response::new(Empty {}))
}
type ListVmContractsStream = Pin<Box<dyn Stream<Item = Result<VmContract, Status>> + Send>>;
async fn list_vm_contracts(
&self,
req: Request<ListVmContractsReq>,
) -> Result<Response<Self::ListVmContractsStream>, Status> {
let req = check_sig_from_req(req)?;
info!(
"CLI {} requested ListVMVmContractsStream. As operator: {}",
req.wallet, req.as_operator
);
let mut contracts = Vec::new();
if !req.uuid.is_empty() {
if let Ok(specific_contract) = self.data.find_contract_by_uuid(&req.uuid) {
if specific_contract.admin_pubkey == req.wallet {
contracts.push(specific_contract);
}
// TODO: allow operator to inspect contracts
}
} else {
if req.as_operator {
contracts.append(&mut self.data.find_vm_contracts_by_operator(&req.wallet));
} else {
contracts.append(&mut self.data.find_vm_contracts_by_admin(&req.wallet));
}
}
let (tx, rx) = mpsc::channel(6); let (tx, rx) = mpsc::channel(6);
tokio::spawn(async move { tokio::spawn(async move {
for contract in contracts { for contract in contracts {
@ -203,18 +269,18 @@ impl BrainCli for BrainCliMock {
}); });
let output_stream = ReceiverStream::new(rx); let output_stream = ReceiverStream::new(rx);
Ok(Response::new( Ok(Response::new(
Box::pin(output_stream) as Self::ListContractsStream Box::pin(output_stream) as Self::ListVmContractsStream
)) ))
} }
type ListNodesStream = Pin<Box<dyn Stream<Item = Result<NodeListResp, Status>> + Send>>; type ListVmNodesStream = Pin<Box<dyn Stream<Item = Result<VmNodeListResp, Status>> + Send>>;
async fn list_nodes( async fn list_vm_nodes(
&self, &self,
req: Request<NodeFilters>, req: Request<VmNodeFilters>,
) -> Result<Response<Self::ListNodesStream>, tonic::Status> { ) -> Result<Response<Self::ListVmNodesStream>, tonic::Status> {
let req = req.into_inner(); let req = check_sig_from_req(req)?;
info!("Unknown CLI requested ListNodesStream: {req:?}"); info!("CLI requested ListVmNodesStream: {req:?}");
let nodes = self.data.find_nodes_by_filters(&req); let nodes = self.data.find_vm_nodes_by_filters(&req);
let (tx, rx) = mpsc::channel(6); let (tx, rx) = mpsc::channel(6);
tokio::spawn(async move { tokio::spawn(async move {
for node in nodes { for node in nodes {
@ -223,16 +289,16 @@ impl BrainCli for BrainCliMock {
}); });
let output_stream = ReceiverStream::new(rx); let output_stream = ReceiverStream::new(rx);
Ok(Response::new( Ok(Response::new(
Box::pin(output_stream) as Self::ListNodesStream Box::pin(output_stream) as Self::ListVmNodesStream
)) ))
} }
async fn get_one_node( async fn get_one_vm_node(
&self, &self,
req: Request<NodeFilters>, req: Request<VmNodeFilters>,
) -> Result<Response<NodeListResp>, Status> { ) -> Result<Response<VmNodeListResp>, Status> {
let req = req.into_inner(); let req = check_sig_from_req(req)?;
info!("Unknown CLI requested ListNodesStream: {req:?}"); info!("Unknown CLI requested ListVmNodesStream: {req:?}");
match self.data.get_one_node_by_filters(&req) { match self.data.get_one_node_by_filters(&req) {
Some(node) => Ok(Response::new(node.into())), Some(node) => Ok(Response::new(node.into())),
None => Err(Status::not_found( None => Err(Status::not_found(
@ -241,10 +307,278 @@ impl BrainCli for BrainCliMock {
} }
} }
async fn delete_vm(&self, req: Request<DeleteVmReq>) -> Result<Response<Empty>, Status> { async fn register_operator(
let req = req.into_inner(); &self,
info!("Unknown CLI requested to delete vm {}", req.uuid); req: Request<RegOperatorReq>,
self.data.delete_vm(req).await; ) -> Result<Response<Empty>, Status> {
let req = check_sig_from_req(req)?;
info!("Regitering new operator: {req:?}");
match self.data.register_operator(req) {
Ok(()) => Ok(Response::new(Empty {})),
Err(e) => Err(Status::failed_precondition(e.to_string())),
}
}
async fn kick_contract(&self, req: Request<KickReq>) -> Result<Response<KickResp>, Status> {
let req = check_sig_from_req(req)?;
match self
.data
.kick_contract(&req.operator_wallet, &req.contract_uuid, &req.reason)
.await
{
Ok(nano_lp) => Ok(Response::new(KickResp { nano_lp })),
Err(e) => Err(Status::permission_denied(e.to_string())),
}
}
async fn ban_user(&self, req: Request<BanUserReq>) -> Result<Response<Empty>, Status> {
let req = check_sig_from_req(req)?;
self.data.ban_user(&req.operator_wallet, &req.user_wallet);
Ok(Response::new(Empty {})) Ok(Response::new(Empty {}))
} }
type ListOperatorsStream =
Pin<Box<dyn Stream<Item = Result<ListOperatorsResp, Status>> + Send>>;
async fn list_operators(
&self,
req: Request<Empty>,
) -> Result<Response<Self::ListOperatorsStream>, Status> {
let _ = check_sig_from_req(req)?;
let operators = self.data.list_operators();
let (tx, rx) = mpsc::channel(6);
tokio::spawn(async move {
for op in operators {
let _ = tx.send(Ok(op.into())).await;
}
});
let output_stream = ReceiverStream::new(rx);
Ok(Response::new(
Box::pin(output_stream) as Self::ListOperatorsStream
))
}
async fn inspect_operator(
&self,
req: Request<Pubkey>,
) -> Result<Response<InspectOperatorResp>, Status> {
match self.data.inspect_operator(&req.into_inner().pubkey) {
Some(op) => Ok(Response::new(op.into())),
None => Err(Status::not_found(
"The wallet you specified is not an operator",
)),
}
}
async fn airdrop(&self, req: Request<AirdropReq>) -> Result<Response<Empty>, Status> {
check_admin_key(&req)?;
let req = check_sig_from_req(req)?;
self.data.give_airdrop(&req.pubkey, req.tokens);
Ok(Response::new(Empty {}))
}
async fn slash(&self, req: Request<SlashReq>) -> Result<Response<Empty>, Status> {
check_admin_key(&req)?;
let req = check_sig_from_req(req)?;
self.data.slash_account(&req.pubkey, req.tokens);
Ok(Response::new(Empty {}))
}
type ListAllVmContractsStream = Pin<Box<dyn Stream<Item = Result<VmContract, Status>> + Send>>;
async fn list_all_vm_contracts(
&self,
req: Request<Empty>,
) -> Result<Response<Self::ListVmContractsStream>, Status> {
check_admin_key(&req)?;
let _ = check_sig_from_req(req)?;
let contracts = self.data.list_all_contracts();
let (tx, rx) = mpsc::channel(6);
tokio::spawn(async move {
for contract in contracts {
let _ = tx.send(Ok(contract.into())).await;
}
});
let output_stream = ReceiverStream::new(rx);
Ok(Response::new(
Box::pin(output_stream) as Self::ListVmContractsStream
))
}
type ListAccountsStream = Pin<Box<dyn Stream<Item = Result<Account, Status>> + Send>>;
async fn list_accounts(
&self,
req: Request<Empty>,
) -> Result<Response<Self::ListAccountsStream>, Status> {
check_admin_key(&req)?;
let _ = check_sig_from_req(req)?;
let accounts = self.data.list_accounts();
let (tx, rx) = mpsc::channel(6);
tokio::spawn(async move {
for account in accounts {
let _ = tx.send(Ok(account.into())).await;
}
});
let output_stream = ReceiverStream::new(rx);
Ok(Response::new(
Box::pin(output_stream) as Self::ListAccountsStream
))
}
}
trait PubkeyGetter {
fn get_pubkey(&self) -> Option<String>;
}
macro_rules! impl_pubkey_getter {
($t:ty, $field:ident) => {
impl PubkeyGetter for $t {
fn get_pubkey(&self) -> Option<String> {
Some(self.$field.clone())
}
}
};
($t:ty) => {
impl PubkeyGetter for $t {
fn get_pubkey(&self) -> Option<String> {
None
}
}
};
}
impl_pubkey_getter!(Pubkey, pubkey);
impl_pubkey_getter!(NewVmReq, admin_pubkey);
impl_pubkey_getter!(DeleteVmReq, admin_pubkey);
impl_pubkey_getter!(UpdateVmReq, admin_pubkey);
impl_pubkey_getter!(ExtendVmReq, admin_pubkey);
impl_pubkey_getter!(ReportNodeReq, admin_pubkey);
impl_pubkey_getter!(ListVmContractsReq, wallet);
impl_pubkey_getter!(RegisterVmNodeReq, node_pubkey);
impl_pubkey_getter!(RegOperatorReq, pubkey);
impl_pubkey_getter!(KickReq, operator_wallet);
impl_pubkey_getter!(BanUserReq, operator_wallet);
impl_pubkey_getter!(VmNodeFilters);
impl_pubkey_getter!(Empty);
impl_pubkey_getter!(AirdropReq);
impl_pubkey_getter!(SlashReq);
fn check_sig_from_req<T: std::fmt::Debug + PubkeyGetter>(req: Request<T>) -> Result<T, Status> {
let time = match req.metadata().get("timestamp") {
Some(t) => t.clone(),
None => return Err(Status::unauthenticated("Timestamp not found in metadata.")),
};
let time = time
.to_str()
.map_err(|_| Status::unauthenticated("Timestamp in metadata is not a string"))?;
let now = chrono::Utc::now();
let parsed_time = chrono::DateTime::parse_from_rfc3339(time)
.map_err(|_| Status::unauthenticated("Coult not parse timestamp"))?;
let seconds_elapsed = now.signed_duration_since(parsed_time).num_seconds();
if seconds_elapsed > 4 || seconds_elapsed < -4 {
return Err(Status::unauthenticated(format!(
"Date is not within 4 sec of the time of the server: CLI {} vs Server {}",
parsed_time, now
)));
}
let signature = match req.metadata().get("request-signature") {
Some(t) => t,
None => return Err(Status::unauthenticated("signature not found in metadata.")),
};
let signature = bs58::decode(signature)
.into_vec()
.map_err(|_| Status::unauthenticated("signature is not a bs58 string"))?;
let signature = ed25519_dalek::Signature::from_bytes(
signature
.as_slice()
.try_into()
.map_err(|_| Status::unauthenticated("could not parse ed25519 signature"))?,
);
let pubkey_value = match req.metadata().get("pubkey") {
Some(p) => p.clone(),
None => return Err(Status::unauthenticated("pubkey not found in metadata.")),
};
let pubkey = ed25519_dalek::VerifyingKey::from_bytes(
&bs58::decode(&pubkey_value)
.into_vec()
.map_err(|_| Status::unauthenticated("pubkey is not a bs58 string"))?
.try_into()
.map_err(|_| Status::unauthenticated("pubkey does not have the correct size."))?,
)
.map_err(|_| Status::unauthenticated("could not parse ed25519 pubkey"))?;
let req = req.into_inner();
let message = format!("{time}{req:?}");
use ed25519_dalek::Verifier;
pubkey
.verify(message.as_bytes(), &signature)
.map_err(|_| Status::unauthenticated("the signature is not valid"))?;
if let Some(req_pubkey) = req.get_pubkey() {
if pubkey_value.to_str().unwrap().to_string() != req_pubkey {
return Err(Status::unauthenticated(
"pubkey of signature does not match pubkey of request",
));
}
}
Ok(req)
}
fn check_sig_from_parts(pubkey: &str, time: &str, msg: &str, sig: &str) -> Result<(), Status> {
let now = chrono::Utc::now();
let parsed_time = chrono::DateTime::parse_from_rfc3339(time)
.map_err(|_| Status::unauthenticated("Coult not parse timestamp"))?;
let seconds_elapsed = now.signed_duration_since(parsed_time).num_seconds();
if seconds_elapsed > 4 || seconds_elapsed < -4 {
return Err(Status::unauthenticated(format!(
"Date is not within 4 sec of the time of the server: CLI {} vs Server {}",
parsed_time, now
)));
}
let signature = bs58::decode(sig)
.into_vec()
.map_err(|_| Status::unauthenticated("signature is not a bs58 string"))?;
let signature = ed25519_dalek::Signature::from_bytes(
signature
.as_slice()
.try_into()
.map_err(|_| Status::unauthenticated("could not parse ed25519 signature"))?,
);
let pubkey = ed25519_dalek::VerifyingKey::from_bytes(
&bs58::decode(&pubkey)
.into_vec()
.map_err(|_| Status::unauthenticated("pubkey is not a bs58 string"))?
.try_into()
.map_err(|_| Status::unauthenticated("pubkey does not have the correct size."))?,
)
.map_err(|_| Status::unauthenticated("could not parse ed25519 pubkey"))?;
let msg = time.to_string() + msg;
use ed25519_dalek::Verifier;
pubkey
.verify(msg.as_bytes(), &signature)
.map_err(|_| Status::unauthenticated("the signature is not valid"))?;
Ok(())
}
fn check_admin_key<T>(req: &Request<T>) -> Result<(), Status> {
let pubkey = match req.metadata().get("pubkey") {
Some(p) => p.clone(),
None => return Err(Status::unauthenticated("pubkey not found in metadata.")),
};
let pubkey = pubkey
.to_str()
.map_err(|_| Status::unauthenticated("could not parse pubkey metadata to str"))?;
if !ADMIN_ACCOUNTS.contains(&pubkey) {
return Err(Status::unauthenticated(
"This operation is reserved to admin accounts",
));
}
Ok(())
} }

@ -3,7 +3,7 @@ mod grpc;
use data::BrainData; use data::BrainData;
use grpc::snp_proto::brain_cli_server::BrainCliServer; use grpc::snp_proto::brain_cli_server::BrainCliServer;
use grpc::snp_proto::brain_daemon_server::BrainDaemonServer; use grpc::snp_proto::brain_vm_daemon_server::BrainVmDaemonServer;
use grpc::BrainCliMock; use grpc::BrainCliMock;
use grpc::BrainDaemonMock; use grpc::BrainDaemonMock;
use std::sync::Arc; use std::sync::Arc;
@ -19,12 +19,16 @@ async fn main() {
tokio::spawn(async move { tokio::spawn(async move {
loop { loop {
tokio::time::sleep(tokio::time::Duration::from_secs(60)).await; tokio::time::sleep(tokio::time::Duration::from_secs(60)).await;
data_clone.contracts_cron(); data_clone.vm_nodes_cron().await;
data_clone.vm_contracts_cron().await;
if let Err(e) = data_clone.save_to_disk() {
log::error!("Could not save data to disk due to error: {e}")
}
} }
}); });
let addr = "0.0.0.0:31337".parse().unwrap(); let addr = "0.0.0.0:31337".parse().unwrap();
let daemon_server = BrainDaemonServer::new(BrainDaemonMock::new(data.clone())); let daemon_server = BrainVmDaemonServer::new(BrainDaemonMock::new(data.clone()));
let cli_server = BrainCliServer::new(BrainCliMock::new(data.clone())); let cli_server = BrainCliServer::new(BrainCliMock::new(data.clone()));
Server::builder() Server::builder()

@ -1,5 +1,5 @@
syntax = "proto3"; syntax = "proto3";
package snp_proto; package vm_proto;
message Empty { message Empty {
} }
@ -13,7 +13,7 @@ message AccountBalance {
uint64 tmp_locked = 2; uint64 tmp_locked = 2;
} }
message Contract { message VmContract {
string uuid = 1; string uuid = 1;
string hostname = 2; string hostname = 2;
string admin_pubkey = 3; string admin_pubkey = 3;
@ -28,7 +28,7 @@ message Contract {
string dtrfs_sha = 12; string dtrfs_sha = 12;
string created_at = 13; string created_at = 13;
string updated_at = 14; string updated_at = 14;
// total nanotoken cost per minute (for all units) // total nanoLP cost per minute (for all units)
uint64 nano_per_minute = 15; uint64 nano_per_minute = 15;
uint64 locked_nano = 16; uint64 locked_nano = 16;
string collected_at = 17; string collected_at = 17;
@ -52,18 +52,19 @@ message MeasurementIP {
string gateway = 4; string gateway = 4;
} }
message RegisterNodeReq { // This should also include a block hash or similar, for auth
message RegisterVmNodeReq {
string node_pubkey = 1; string node_pubkey = 1;
string owner_pubkey = 2; string operator_wallet = 2;
string main_ip = 3; string main_ip = 3;
string country = 4; string country = 4;
string region = 5; string region = 5;
string city = 6; string city = 6;
// nanotokens per unit per minute // nanoLP per unit per minute
uint64 price = 7; uint64 price = 7;
} }
message NodeResources { message VmNodeResources {
string node_pubkey = 1; string node_pubkey = 1;
uint32 avail_ports = 2; uint32 avail_ports = 2;
uint32 avail_ipv4 = 3; uint32 avail_ipv4 = 3;
@ -101,13 +102,14 @@ message NewVmResp {
message UpdateVmReq { message UpdateVmReq {
string uuid = 1; string uuid = 1;
uint32 disk_size_gb = 2; string admin_pubkey = 2;
uint32 vcpus = 3; uint32 disk_size_gb = 3;
uint32 memory_mb = 4; uint32 vcpus = 4;
string kernel_url = 5; uint32 memory_mb = 5;
string kernel_sha = 6; string kernel_url = 6;
string dtrfs_url = 7; string kernel_sha = 7;
string dtrfs_sha = 8; string dtrfs_url = 8;
string dtrfs_sha = 9;
} }
message UpdateVmResp { message UpdateVmResp {
@ -118,9 +120,10 @@ message UpdateVmResp {
message DeleteVmReq { message DeleteVmReq {
string uuid = 1; string uuid = 1;
string admin_pubkey = 2;
} }
message BrainMessage { message BrainVmMessage {
oneof Msg { oneof Msg {
NewVmReq new_vm_req = 1; NewVmReq new_vm_req = 1;
UpdateVmReq update_vm_req = 2; UpdateVmReq update_vm_req = 2;
@ -128,28 +131,35 @@ message BrainMessage {
} }
} }
message DaemonMessage { message DaemonStreamAuth {
string timestamp = 1;
string pubkey = 2;
repeated string contracts = 3;
string signature = 4;
}
message VmDaemonMessage {
oneof Msg { oneof Msg {
Pubkey pubkey = 1; DaemonStreamAuth auth = 1;
NewVmResp new_vm_resp = 2; NewVmResp new_vm_resp = 2;
UpdateVmResp update_vm_resp = 3; UpdateVmResp update_vm_resp = 3;
NodeResources node_resources = 4; VmNodeResources vm_node_resources = 4;
} }
} }
service BrainDaemon { service BrainVmDaemon {
rpc RegisterNode (RegisterNodeReq) returns (stream Contract); rpc RegisterVmNode (RegisterVmNodeReq) returns (stream VmContract);
rpc BrainMessages (Pubkey) returns (stream BrainMessage); rpc BrainMessages (DaemonStreamAuth) returns (stream BrainVmMessage);
rpc DaemonMessages (stream DaemonMessage) returns (Empty); rpc DaemonMessages (stream VmDaemonMessage) returns (Empty);
} }
message ListContractsReq { message ListVmContractsReq {
string admin_pubkey = 1; string wallet = 1;
string node_pubkey = 2; bool as_operator = 2;
string uuid = 3; string uuid = 3;
} }
message NodeFilters { message VmNodeFilters {
uint32 free_ports = 1; uint32 free_ports = 1;
bool offers_ipv4 = 2; bool offers_ipv4 = 2;
bool offers_ipv6 = 3; bool offers_ipv6 = 3;
@ -163,16 +173,15 @@ message NodeFilters {
string node_pubkey = 11; string node_pubkey = 11;
} }
message NodeListResp { message VmNodeListResp {
string node_pubkey = 1; string operator = 1;
string country = 2; string node_pubkey = 2;
string region = 3; string country = 3;
string city = 4; string region = 4;
string ip = 5; // required for latency test string city = 5;
uint32 server_rating = 6; string ip = 6; // required for latency test
uint32 provider_rating = 7; repeated string reports = 7; // TODO: this will become an enum
// nanotokens per unit per minute uint64 price = 8; // nanoLP per unit per minute
uint64 price = 8;
} }
message ExtendVmReq { message ExtendVmReq {
@ -181,14 +190,82 @@ message ExtendVmReq {
uint64 locked_nano = 3; uint64 locked_nano = 3;
} }
message AirdropReq {
string pubkey = 1;
uint64 tokens = 2;
}
message SlashReq {
string pubkey = 1;
uint64 tokens = 2;
}
message Account {
string pubkey = 1;
uint64 balance = 2;
uint64 tmp_locked = 3;
}
message RegOperatorReq {
string pubkey = 1;
uint64 escrow = 2;
string email = 3;
}
message ListOperatorsResp {
string pubkey = 1;
uint64 escrow = 2;
string email = 3;
uint64 app_nodes = 4;
uint64 vm_nodes = 5;
uint64 reports = 6;
}
message InspectOperatorResp {
ListOperatorsResp operator = 1;
repeated VmNodeListResp nodes = 2;
}
message ReportNodeReq {
string admin_pubkey = 1;
string node_pubkey = 2;
string contract = 3;
string reason = 4;
}
message KickReq {
string operator_wallet = 1;
string contract_uuid = 2;
string reason = 3;
}
message BanUserReq {
string operator_wallet = 1;
string user_wallet = 2;
}
message KickResp {
uint64 nano_lp = 1;
}
service BrainCli { service BrainCli {
rpc GetAirdrop (Pubkey) returns (Empty);
rpc GetBalance (Pubkey) returns (AccountBalance); rpc GetBalance (Pubkey) returns (AccountBalance);
rpc NewVm (NewVmReq) returns (NewVmResp); rpc NewVm (NewVmReq) returns (NewVmResp);
rpc ListContracts (ListContractsReq) returns (stream Contract); rpc ListVmContracts (ListVmContractsReq) returns (stream VmContract);
rpc ListNodes (NodeFilters) returns (stream NodeListResp); rpc ListVmNodes (VmNodeFilters) returns (stream VmNodeListResp);
rpc GetOneNode (NodeFilters) returns (NodeListResp); rpc GetOneVmNode (VmNodeFilters) returns (VmNodeListResp);
rpc DeleteVm (DeleteVmReq) returns (Empty); rpc DeleteVm (DeleteVmReq) returns (Empty);
rpc UpdateVm (UpdateVmReq) returns (UpdateVmResp); rpc UpdateVm (UpdateVmReq) returns (UpdateVmResp);
rpc ExtendVm (ExtendVmReq) returns (Empty); rpc ExtendVm (ExtendVmReq) returns (Empty);
rpc ReportNode (ReportNodeReq) returns (Empty);
rpc ListOperators (Empty) returns (stream ListOperatorsResp);
rpc InspectOperator (Pubkey) returns (InspectOperatorResp);
rpc RegisterOperator (RegOperatorReq) returns (Empty);
rpc KickContract (KickReq) returns (KickResp);
rpc BanUser (BanUserReq) returns (Empty);
// admin commands
rpc Airdrop (AirdropReq) returns (Empty);
rpc Slash (SlashReq) returns (Empty);
rpc ListAllVmContracts (Empty) returns (stream VmContract);
rpc ListAccounts (Empty) returns (stream Account);
} }