Compare commits

...

6 Commits

Author SHA1 Message Date
57d3807a17
kick contract transaction
one single transaction query to execute all the kick operation
Remove unused fields from deleted_app schema
a basic test for kick and possibilities
2025-05-20 14:07:11 +05:30
929530a4c5
kick contract implemented
app pricing calculation
add node in kick schema and type
improved handling
Clone on all app types
handle expected error on kick contract
validate both app and vm contracts
2025-05-20 14:07:11 +05:30
4c647eef6a
register operator 2025-05-20 14:07:11 +05:30
6a85acda9e
when node reconnects, send deleted VMs
previously, active VMs were sent
2025-05-20 01:45:31 +03:00
32b587c6c5
merged proto branch 2025-05-20 00:19:04 +03:00
0fc9b8003a
adding payments for VM contracts 2025-05-18 16:08:18 +03:00
22 changed files with 743 additions and 151 deletions

2
Cargo.lock generated

@ -1000,7 +1000,7 @@ dependencies = [
[[package]] [[package]]
name = "detee-shared" name = "detee-shared"
version = "0.1.0" version = "0.1.0"
source = "git+ssh://git@gitea.detee.cloud/testnet/proto?branch=surreal_brain_app#da0f3269a31e0ebfb7328e2115e212aabe4d984a" source = "git+ssh://git@gitea.detee.cloud/testnet/proto?branch=surreal_brain#d6ca058d2de78b5257517034bca2b2c7d5929db8"
dependencies = [ dependencies = [
"bincode 2.0.1", "bincode 2.0.1",
"prost", "prost",

@ -13,7 +13,7 @@ serde_yaml = "0.9.34"
surrealdb = "2.2.2" surrealdb = "2.2.2"
tokio = { version = "1.44.2", features = ["macros", "rt-multi-thread"] } tokio = { version = "1.44.2", features = ["macros", "rt-multi-thread"] }
tonic = { version = "0.12", features = ["tls"] } tonic = { version = "0.12", features = ["tls"] }
detee-shared = { git = "ssh://git@gitea.detee.cloud/testnet/proto", branch = "surreal_brain_app" } detee-shared = { git = "ssh://git@gitea.detee.cloud/testnet/proto", branch = "surreal_brain" }
ed25519-dalek = "2.1.1" ed25519-dalek = "2.1.1"
bs58 = "0.5.1" bs58 = "0.5.1"
tokio-stream = "0.1.17" tokio-stream = "0.1.17"

@ -1,91 +0,0 @@
DEFINE TABLE account SCHEMAFULL;
DEFINE FIELD balance ON TABLE account TYPE int;
DEFINE FIELD tmp_locked ON TABLE account TYPE int;
DEFINE FIELD escrow ON TABLE account TYPE int;
DEFINE FIELD email ON TABLE account TYPE string;
DEFINE FIELD hratls_pubkey ON TABLE account TYPE string;
DEFINE FIELD vm_nodes ON TABLE account TYPE array<record>;
DEFINE FIELD app_nodes ON TABLE account TYPE array<record>;
DEFINE TABLE package SCHEMAFULL;
DEFINE FIELD url ON TABLE package TYPE array<string>;
DEFINE TABLE kernel SCHEMAFULL;
DEFINE FIELD url ON TABLE kernel TYPE array<string>;
DEFINE TABLE dtrfs SCHEMAFULL;
DEFINE FIELD url ON TABLE dtrfs TYPE array<string>;
DEFINE FIELD kernel ON TABLE dtrfs TYPE record<kernel>;
DEFINE TABLE vm_node SCHEMAFULL;
DEFINE FIELD country ON TABLE vm_node TYPE string;
DEFINE FIELD region ON TABLE vm_node TYPE string;
DEFINE FIELD city ON TABLE vm_node TYPE string;
DEFINE FIELD ip ON TABLE vm_node TYPE string;
DEFINE FIELD avail_mem_mb ON TABLE vm_node TYPE int;
DEFINE FIELD avail_vcpus ON TABLE vm_node TYPE int;
DEFINE FIELD avail_storage_gbs ON TABLE vm_node TYPE int;
DEFINE FIELD avail_ipv4 ON TABLE vm_node TYPE int;
DEFINE FIELD avail_ipv6 ON TABLE vm_node TYPE int;
DEFINE FIELD avail_ports ON TABLE vm_node TYPE int;
DEFINE FIELD max_ports_per_vm ON TABLE vm_node TYPE int;
DEFINE FIELD price ON TABLE vm_node TYPE int;
DEFINE FIELD offline_minutes ON TABLE vm_node TYPE int;
DEFINE TABLE vm_contract TYPE RELATION FROM account TO vm_node SCHEMAFULL;
DEFINE FIELD state ON TABLE vm_contract TYPE string;
DEFINE FIELD hostname ON TABLE vm_contract TYPE string;
DEFINE FIELD mapped_ports ON TABLE vm_contract TYPE array<[int, int]>;
DEFINE FIELD public_ipv4 ON TABLE vm_contract TYPE string;
DEFINE FIELD public_ipv6 ON TABLE vm_contract TYPE string;
DEFINE FIELD disk_size_gb ON TABLE vm_contract TYPE int;
DEFINE FIELD vcpus ON TABLE vm_contract TYPE int;
DEFINE FIELD memory_mb ON TABLE vm_contract TYPE int;
DEFINE FIELD dtrfs ON TABLE vm_contract TYPE record<dtrfs>;
DEFINE FIELD created_at ON TABLE vm_contract TYPE datetime;
DEFINE FIELD updated_at ON TABLE vm_contract TYPE datetime;
DEFINE FIELD price_per_unit ON TABLE vm_contract TYPE int;
DEFINE FIELD locked_nano ON TABLE vm_contract TYPE int;
DEFINE FIELD collected_at ON TABLE vm_contract TYPE datetime;
DEFINE TABLE app_node SCHEMAFULL;
DEFINE FIELD country ON TABLE app_node TYPE string;
DEFINE FIELD region ON TABLE app_node TYPE string;
DEFINE FIELD city ON TABLE app_node TYPE string;
DEFINE FIELD ip ON TABLE app_node TYPE string;
DEFINE FIELD avail_mem_mb ON TABLE app_node TYPE int;
DEFINE FIELD avail_vcpus ON TABLE app_node TYPE int;
DEFINE FIELD avail_storage_gbs ON TABLE app_node TYPE int;
DEFINE FIELD avail_ports ON TABLE app_node TYPE int;
DEFINE FIELD max_ports_per_app ON TABLE app_node TYPE int;
DEFINE FIELD price ON TABLE app_node TYPE int;
DEFINE FIELD offline_minutes ON TABLE app_node TYPE int;
DEFINE TABLE app_contract TYPE RELATION FROM account TO app_node SCHEMAFULL;
DEFINE FIELD state ON TABLE app_contract TYPE string;
DEFINE FIELD app_name ON TABLE app_contract TYPE string;
DEFINE FIELD mapped_ports ON TABLE app_contract TYPE array<[int, int]>;
DEFINE FIELD host_ipv4 ON TABLE app_contract TYPE string;
DEFINE FIELD vcpus ON TABLE app_contract TYPE int;
DEFINE FIELD memory_mb ON TABLE app_contract TYPE int;
DEFINE FIELD disk_size_gb ON TABLE app_contract TYPE int;
DEFINE FIELD created_at ON TABLE app_contract TYPE datetime;
DEFINE FIELD updated_at ON TABLE app_contract TYPE datetime;
DEFINE FIELD price_per_unit ON TABLE app_contract TYPE int;
DEFINE FIELD locked_nano ON TABLE app_contract TYPE int;
DEFINE FIELD collected_at ON TABLE app_contract TYPE datetime;
DEFINE FIELD mr_enclave ON TABLE app_contract TYPE record<package>;
DEFINE TABLE ban TYPE RELATION FROM account TO account;
DEFINE FIELD created_at ON TABLE ban TYPE datetime;
DEFINE TABLE kick TYPE RELATION FROM account TO account;
DEFINE FIELD created_at ON TABLE kick TYPE datetime;
DEFINE FIELD reason ON TABLE kick TYPE string;
DEFINE FIELD contract ON TABLE kick TYPE record<vm_contract|app_contract>;
DEFINE TABLE report TYPE RELATION FROM account TO vm_node|app_node;
DEFINE FIELD created_at ON TABLE ban TYPE datetime;
DEFINE FIELD reason ON TABLE ban TYPE string;
DEFINE TABLE operator TYPE RELATION FROM account TO vm_node|app_node;

@ -17,5 +17,10 @@ ssh $server systemctl stop detee-brain.service
scp target/release/brain $server:/usr/local/bin/detee-brain scp target/release/brain $server:/usr/local/bin/detee-brain
ssh $server mkdir -p /etc/detee/brain/ ssh $server mkdir -p /etc/detee/brain/
scp scripts/detee-brain.service $server:/etc/systemd/system/detee-brain.service scp scripts/detee-brain.service $server:/etc/systemd/system/detee-brain.service
scp surql/detee-brain-contracts.service $server:/etc/systemd/system/detee-brain-contracts.service
scp surql/detee-brain-contracts.timer $server:/etc/systemd/system/detee-brain-contracts.timer
scp surql/brain-timer.sh $server:/etc/detee/brain/brain-timer.sh
scp surql/timer.sql $server:/etc/detee/brain/timer.surql
ssh $server systemctl daemon-reload ssh $server systemctl daemon-reload
ssh $server systemctl start detee-brain.service ssh $server systemctl start detee-brain.service
ssh $server systemctl enable --now detee-brain-contracts.timer

@ -23,6 +23,8 @@ pub static ADMIN_ACCOUNTS: LazyLock<Vec<String>> = LazyLock::new(|| {
pub const OLD_BRAIN_DATA_PATH: &str = "./saved_data.yaml"; pub const OLD_BRAIN_DATA_PATH: &str = "./saved_data.yaml";
pub const ACCOUNT: &str = "account"; pub const ACCOUNT: &str = "account";
pub const KICK: &str = "kick";
pub const VM_NODE: &str = "vm_node"; pub const VM_NODE: &str = "vm_node";
pub const ACTIVE_VM: &str = "active_vm"; pub const ACTIVE_VM: &str = "active_vm";
pub const VM_UPDATE_EVENT: &str = "vm_update_event"; pub const VM_UPDATE_EVENT: &str = "vm_update_event";
@ -42,3 +44,6 @@ pub const ID_ALPHABET: [char; 62] = [
'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U',
'V', 'W', 'X', 'Y', 'Z', 'V', 'W', 'X', 'Y', 'Z',
]; ];
pub const MIN_ESCROW: u64 = 5000;
pub const TOKEN_DECIMAL: u64 = 1_000_000_000;

@ -12,7 +12,7 @@ use surrealdb::sql::Datetime;
use surrealdb::{Notification, RecordId, Surreal}; use surrealdb::{Notification, RecordId, Surreal};
use tokio_stream::StreamExt; use tokio_stream::StreamExt;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct AppNode { pub struct AppNode {
pub id: RecordId, pub id: RecordId,
pub operator: RecordId, pub operator: RecordId,
@ -32,8 +32,9 @@ pub struct AppNode {
impl AppNode { impl AppNode {
pub async fn register(self, db: &Surreal<Client>) -> Result<AppNode, Error> { pub async fn register(self, db: &Surreal<Client>) -> Result<AppNode, Error> {
db::Account::get_or_create(db, &self.operator.key().to_string()).await?; db::Account::get_or_create(db, &self.operator.key().to_string()).await?;
let app_node: Option<AppNode> = db.upsert(self.id.clone()).content(self).await?; let app_node_id = self.id.clone();
app_node.ok_or(Error::FailedToCreateDBEntry) let app_node: Option<AppNode> = db.upsert(app_node_id.clone()).content(self).await?;
app_node.ok_or(Error::FailedToCreateDBEntry(format!("{APP_NODE}:{app_node_id}")))
} }
} }
@ -54,7 +55,7 @@ impl From<DeletedApp> for AppDaemonMsg {
} }
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct NewAppReq { pub struct NewAppReq {
pub id: RecordId, pub id: RecordId,
#[serde(rename = "in")] #[serde(rename = "in")]
@ -97,6 +98,7 @@ impl NewAppReq {
} }
pub async fn submit(self, db: &Surreal<Client>) -> Result<Vec<Self>, Error> { pub async fn submit(self, db: &Surreal<Client>) -> Result<Vec<Self>, Error> {
// TODO: handle financial transaction
let new_app_req: Vec<Self> = db.insert(NEW_APP_REQ).relation(self).await?; let new_app_req: Vec<Self> = db.insert(NEW_APP_REQ).relation(self).await?;
Ok(new_app_req) Ok(new_app_req)
} }
@ -164,7 +166,7 @@ impl AppNodeWithReports {
} }
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ActiveApp { pub struct ActiveApp {
pub id: RecordId, pub id: RecordId,
#[serde(rename = "in")] #[serde(rename = "in")]
@ -210,6 +212,15 @@ impl From<ActiveApp> for DeletedApp {
} }
impl ActiveApp { impl ActiveApp {
pub fn price_per_minute(&self) -> u64 {
(self.total_units() * self.price_per_unit as f64) as u64
}
fn total_units(&self) -> f64 {
// TODO: Optimize this based on price of hardware.
(self.vcpus as f64 * 5f64) + (self.memory_mb as f64 / 200f64) + (self.disk_size_gb as f64)
}
pub async fn activate(db: &Surreal<Client>, id: &str) -> Result<(), Error> { pub async fn activate(db: &Surreal<Client>, id: &str) -> Result<(), Error> {
let new_app_req = match NewAppReq::get(db, id).await? { let new_app_req = match NewAppReq::get(db, id).await? {
Some(r) => r, Some(r) => r,
@ -293,7 +304,7 @@ impl ActiveApp {
} }
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ActiveAppWithNode { pub struct ActiveAppWithNode {
pub id: RecordId, pub id: RecordId,
#[serde(rename = "in")] #[serde(rename = "in")]
@ -315,6 +326,29 @@ pub struct ActiveAppWithNode {
pub hratls_pubkey: String, pub hratls_pubkey: String,
} }
impl From<ActiveAppWithNode> for ActiveApp {
fn from(val: ActiveAppWithNode) -> Self {
Self {
id: val.id,
admin: val.admin,
app_node: val.app_node.id,
app_name: val.app_name,
mapped_ports: val.mapped_ports,
host_ipv4: val.host_ipv4,
vcpus: val.vcpus,
memory_mb: val.memory_mb,
disk_size_gb: val.disk_size_gb,
created_at: val.created_at,
price_per_unit: val.price_per_unit,
locked_nano: val.locked_nano,
collected_at: val.collected_at,
mr_enclave: val.mr_enclave,
package_url: val.package_url,
hratls_pubkey: val.hratls_pubkey,
}
}
}
impl ActiveAppWithNode { impl ActiveAppWithNode {
pub async fn get_by_uuid(db: &Surreal<Client>, uuid: &str) -> Result<Option<Self>, Error> { pub async fn get_by_uuid(db: &Surreal<Client>, uuid: &str) -> Result<Option<Self>, Error> {
let contract: Option<Self> = let contract: Option<Self> =

@ -1,7 +1,6 @@
use crate::constants::ACCOUNT;
use crate::db::prelude::*;
use super::Error; use super::Error;
use crate::constants::{ACCOUNT, KICK, MIN_ESCROW, TOKEN_DECIMAL};
use crate::db::prelude::*;
use crate::old_brain; use crate::old_brain;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use surrealdb::engine::remote::ws::Client; use surrealdb::engine::remote::ws::Client;
@ -37,7 +36,7 @@ impl Account {
Some(account) => Ok(account), Some(account) => Ok(account),
None => { None => {
let account: Option<Self> = db.create(id).await?; let account: Option<Self> = db.create(id).await?;
account.ok_or(Error::FailedToCreateDBEntry) account.ok_or(Error::FailedToCreateDBEntry(ACCOUNT.to_string()))
} }
} }
} }
@ -49,6 +48,33 @@ impl Account {
.await?; .await?;
Ok(()) Ok(())
} }
pub async fn save(self, db: &Surreal<Client>) -> Result<Option<Self>, Error> {
let account: Option<Self> = db.upsert(self.id.clone()).content(self).await?;
Ok(account)
}
pub async fn operator_reg(
db: &Surreal<Client>,
wallet: &str,
email: &str,
escrow: u64,
) -> Result<(), Error> {
if escrow < MIN_ESCROW {
return Err(Error::MinimalEscrow);
}
let mut op_account = Self::get(db, wallet).await?;
let escrow = escrow.saturating_mul(TOKEN_DECIMAL);
let op_total_balance = op_account.balance.saturating_add(op_account.escrow);
if op_total_balance < escrow {
return Err(Error::InsufficientFunds);
}
op_account.email = email.to_string();
op_account.balance = op_total_balance.saturating_sub(escrow);
op_account.escrow = escrow;
op_account.save(db).await?;
Ok(())
}
} }
impl Account { impl Account {
@ -120,6 +146,24 @@ pub struct Kick {
created_at: Datetime, created_at: Datetime,
reason: String, reason: String,
contract: RecordId, contract: RecordId,
node: RecordId,
}
impl Kick {
pub async fn kicked_in_a_day(db: &Surreal<Client>, account: &str) -> Result<Vec<Self>, Error> {
let mut result = db
.query(format!(
"select * from {KICK} where out = {ACCOUNT}:{account} and created_at > time::now() - 24h;"
))
.await?;
let kicks: Vec<Self> = result.take(0)?;
Ok(kicks)
}
pub async fn submit(self, db: &Surreal<Client>) -> Result<(), Error> {
let _: Vec<Self> = db.insert(KICK).relation(self).await?;
Ok(())
}
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
@ -158,7 +202,7 @@ impl Report {
/// This is the operator obtained from the DB, /// This is the operator obtained from the DB,
/// however the relation is defined using OperatorRelation /// however the relation is defined using OperatorRelation
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Operator { pub struct Operator {
pub account: RecordId, pub account: RecordId,
pub app_nodes: u64, pub app_nodes: u64,
@ -192,8 +236,8 @@ impl Operator {
pub async fn inspect(db: &Surreal<Client>, account: &str) -> Result<Option<Self>, Error> { pub async fn inspect(db: &Surreal<Client>, account: &str) -> Result<Option<Self>, Error> {
let mut result = db let mut result = db
.query(format!( .query(format!(
"$vm_nodes = (select id from vm_node where operator = account:{account}).id; "LET $vm_nodes = (select id from vm_node where operator = account:{account}).id;
$app_nodes = (select id from app_node where operator = account:{account}).id; LET $app_nodes = (select id from app_node where operator = account:{account}).id;
select *, select *,
id as account, id as account,
email, email,
@ -231,3 +275,149 @@ impl Operator {
Ok((operator, vm_nodes, app_nodes)) Ok((operator, vm_nodes, app_nodes))
} }
} }
pub enum WrapperContract {
Vm(ActiveVmWithNode),
App(ActiveAppWithNode),
}
impl WrapperContract {
pub async fn kick_contract(
db: &Surreal<Client>,
operator_wallet: &str,
contract_uuid: &str,
reason: &str,
) -> Result<u64, Error> {
let (
operator_id,
admin_id,
contract_id,
collected_at,
price_per_mint,
deleted_table,
platform_specific_query,
) = if let Some(active_vm) = ActiveVmWithNode::get_by_uuid(db, contract_uuid).await? {
let price_per_minute = active_vm.price_per_minute();
(
active_vm.vm_node.operator,
active_vm.admin,
active_vm.id,
active_vm.collected_at,
price_per_minute,
"deleted_vm",
"
hostname = $contract.hostname,
public_ipv4 = $contract.public_ipv4,
public_ipv6 = $contract.public_ipv6,
dtrfs_sha = $contract.dtrfs_sha,
kernel_sha = $contract.kernel_sha,
",
)
} else if let Some(active_app) = ActiveAppWithNode::get_by_uuid(db, contract_uuid).await? {
let price_per_minute = Into::<ActiveApp>::into(active_app.clone()).price_per_minute();
(
active_app.app_node.operator,
active_app.admin,
active_app.id,
active_app.collected_at,
price_per_minute,
"deleted_app",
"
app_name = $contract.app_name,
host_ipv4 = $contract.host_ipv4,
mr_enclave = $contract.mr_enclave,
package_url= $contract.package_url,
hratls_pubkey = $contract.hratls_pubkey,
",
)
} else {
return Err(Error::ContractNotFound);
};
let operator = operator_id.key().to_string();
let admin = admin_id.key().to_string();
if operator != operator_wallet {
return Err(Error::AccessDenied);
}
let mut minutes_to_refund =
chrono::Utc::now().signed_duration_since(*collected_at).num_minutes().unsigned_abs();
let one_week_minute = 10080;
if minutes_to_refund > one_week_minute {
minutes_to_refund = one_week_minute;
}
let refund_amount = minutes_to_refund * price_per_mint;
log::debug!("Removing {refund_amount} escrow from {} and giving it to {}", operator, admin);
let transaction_query = format!(
"
BEGIN TRANSACTION;
LET $contract = {contract_id};
LET $operator_account = {operator_id};
LET $reason = '{reason}';
LET $refund_amount = {refund_amount};
LET $deleted_contract = {deleted_table}:{contract_uuid};
LET $id = record::id($contract.id);
LET $admin = $contract.in;
LET $node = $contract.out;
-- move contract into deleted state
RELATE $admin->{deleted_table}->$node
SET id = $id,
{platform_specific_query}
mapped_ports = $contract.mapped_ports,
disk_size_gb = $contract.disk_size_gb,
vcpus = $contract.vcpus,
memory_mb = $contract.memory_mb,
created_at = $contract.created_at,
deleted_at = time::now(),
price_per_unit = $contract.price_per_unit
;
DELETE $contract;
-- calculating refund amount
LET $refund = IF SELECT * FROM {KICK} WHERE out = $admin.id AND created_at > time::now() - 24h {{
0
}} ELSE IF $operator_account.escrow <= $refund_amount {{
$operator_account.escrow
}} ELSE {{
$refund_amount;
}};
RELATE $operator_account->{KICK}->$admin
SET id = $id,
reason = $reason,
contract = $deleted_contract,
node = $node,
created_at = time::now()
;
-- update balances
UPDATE $operator_account SET escrow -= $refund;
IF $operator_account.escrow < 0 {{
THROW 'Insufficient funds.'
}};
UPDATE $admin SET balance += $refund;
SELECT * FROM $refund;
COMMIT TRANSACTION;
",
);
log::trace!("kick_contract transaction_query: {}", &transaction_query);
let refunded: Option<u64> = db.query(transaction_query).await?.take(14)?;
let refunded_amount = refunded.ok_or(Error::FailedToCreateDBEntry("Refund".to_string()))?;
Ok(refunded_amount)
}
}

@ -2,7 +2,9 @@ pub mod app;
pub mod general; pub mod general;
pub mod vm; pub mod vm;
use crate::constants::{APP_NODE, DELETED_APP, DELETED_VM, NEW_APP_REQ, NEW_VM_REQ, UPDATE_VM_REQ}; use crate::constants::{
APP_NODE, DELETED_APP, DELETED_VM, MIN_ESCROW, NEW_APP_REQ, NEW_VM_REQ, UPDATE_VM_REQ,
};
use crate::old_brain; use crate::old_brain;
use prelude::*; use prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -22,14 +24,24 @@ pub enum Error {
StdIo(#[from] std::io::Error), StdIo(#[from] std::io::Error),
#[error(transparent)] #[error(transparent)]
TimeOut(#[from] tokio::time::error::Elapsed), TimeOut(#[from] tokio::time::error::Elapsed),
#[error("Failed to create account")] #[error("Failed to create {0}")]
FailedToCreateDBEntry, FailedToCreateDBEntry(String),
#[error("Unknown Table: {0}")] #[error("Unknown Table: {0}")]
UnknownTable(String), UnknownTable(String),
#[error("Daemon channel got closed: {0}")] #[error("Daemon channel got closed: {0}")]
AppDaemonConnection(#[from] tokio::sync::mpsc::error::SendError<AppDaemonMsg>), AppDaemonConnection(#[from] tokio::sync::mpsc::error::SendError<AppDaemonMsg>),
#[error("AppDaemon Error {0}")] #[error("AppDaemon Error {0}")]
NewAppDaemonResp(String), NewAppDaemonResp(String),
#[error("Minimum escrow amount is {MIN_ESCROW}")]
MinimalEscrow,
#[error("Insufficient funds, deposit more tokens")]
InsufficientFunds,
#[error("Contract not found")]
ContractNotFound,
#[error("Access denied")]
AccessDenied,
#[error("Failed to delete contract {0}")]
FailedToDeleteContract(String),
} }
pub mod prelude { pub mod prelude {

@ -30,7 +30,27 @@ pub struct VmNode {
pub avail_ports: u32, pub avail_ports: u32,
pub max_ports_per_vm: u32, pub max_ports_per_vm: u32,
pub price: u64, pub price: u64,
pub offline_minutes: u64, pub connected_at: Datetime,
pub disconnected_at: Datetime,
}
impl VmNode {
pub async fn register(self, db: &Surreal<Client>) -> Result<(), Error> {
Account::get_or_create(db, &self.operator.key().to_string()).await?;
let _: Option<VmNode> = db.upsert(self.id.clone()).content(self).await?;
Ok(())
}
pub async fn set_online(db: &Surreal<Client>, vm_node_id: &str) -> Result<(), Error> {
db.query(format!("UPDATE {VM_NODE}:{vm_node_id} SET connected_at = time::now();")).await?;
Ok(())
}
pub async fn set_offline(db: &Surreal<Client>, vm_node_id: &str) -> Result<(), Error> {
db.query(format!("UPDATE {VM_NODE}:{vm_node_id} SET disconnected_at = time::now();"))
.await?;
Ok(())
}
} }
#[derive(Serialize)] #[derive(Serialize)]
@ -51,14 +71,6 @@ impl VmNodeResources {
} }
} }
impl VmNode {
pub async fn register(self, db: &Surreal<Client>) -> Result<(), Error> {
Account::get_or_create(db, &self.operator.key().to_string()).await?;
let _: Option<VmNode> = db.upsert(self.id.clone()).content(self).await?;
Ok(())
}
}
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct VmNodeWithReports { pub struct VmNodeWithReports {
pub id: RecordId, pub id: RecordId,
@ -75,13 +87,10 @@ pub struct VmNodeWithReports {
pub avail_ports: u32, pub avail_ports: u32,
pub max_ports_per_vm: u32, pub max_ports_per_vm: u32,
pub price: u64, pub price: u64,
pub offline_minutes: u64,
pub reports: Vec<Report>, pub reports: Vec<Report>,
} }
impl VmNodeWithReports { impl VmNodeWithReports {
// TODO: find a more elegant way to do this than importing gRPC in the DB module
// https://en.wikipedia.org/wiki/Dependency_inversion_principle
pub async fn find_by_filters( pub async fn find_by_filters(
db: &Surreal<Client>, db: &Surreal<Client>,
filters: vm_proto::VmNodeFilters, filters: vm_proto::VmNodeFilters,
@ -115,7 +124,7 @@ impl VmNodeWithReports {
if !filters.ip.is_empty() { if !filters.ip.is_empty() {
query += &format!("&& ip = '{}' ", filters.ip); query += &format!("&& ip = '{}' ", filters.ip);
} }
query += ";"; query += " && connected_at > disconnected_at;";
let mut result = db.query(query).await?; let mut result = db.query(query).await?;
let vm_nodes: Vec<Self> = result.take(0)?; let vm_nodes: Vec<Self> = result.take(0)?;
Ok(vm_nodes) Ok(vm_nodes)
@ -191,7 +200,46 @@ impl NewVmReq {
} }
pub async fn submit(self, db: &Surreal<Client>) -> Result<(), Error> { pub async fn submit(self, db: &Surreal<Client>) -> Result<(), Error> {
let _: Vec<Self> = db.insert(NEW_VM_REQ).relation(self).await?; let locked_nano = self.locked_nano;
let account = self.admin.key().to_string();
let vm_id = self.id.key().to_string();
let vm_node = self.vm_node.key().to_string();
// TODO: check for possible injection and maybe use .bind()
let query = format!(
"
BEGIN TRANSACTION;
UPDATE account:{account} SET balance -= {locked_nano};
IF account:{account}.balance < 0 {{
THROW 'Insufficient funds.'
}};
UPDATE account:{account} SET tmp_locked += {locked_nano};
RELATE
account:{account}
->new_vm_req:{vm_id}
->vm_node:{vm_node}
CONTENT {{
created_at: time::now(), hostname: '{}', vcpus: {}, memory_mb: {}, disk_size_gb: {},
extra_ports: {}, public_ipv4: {:?}, public_ipv6: {:?},
dtrfs_url: '{}', dtrfs_sha: '{}', kernel_url: '{}', kernel_sha: '{}',
price_per_unit: {}, locked_nano: {locked_nano}, error: ''
}};
COMMIT TRANSACTION;
",
self.hostname,
self.vcpus,
self.memory_mb,
self.disk_size_gb,
format!("{:?}", self.extra_ports,),
self.public_ipv4,
self.public_ipv6,
self.dtrfs_url,
self.dtrfs_sha,
self.kernel_url,
self.kernel_sha,
self.price_per_unit
);
//let _: Vec<Self> = db.insert(NEW_VM_REQ).relation(self).await?;
db.query(query).await?;
Ok(()) Ok(())
} }
} }
@ -366,9 +414,12 @@ impl ActiveVm {
collected_at: new_vm_req.created_at, collected_at: new_vm_req.created_at,
}; };
let admin_account = active_vm.admin.key().to_string();
let locked_nano = active_vm.locked_nano;
let _: Vec<ActiveVm> = db.insert(()).relation(active_vm).await?; let _: Vec<ActiveVm> = db.insert(()).relation(active_vm).await?;
NewVmReq::delete(db, id).await?; NewVmReq::delete(db, id).await?;
db.query(format!("UPDATE {ACCOUNT}:{admin_account} SET tmp_locked -= {locked_nano};"))
.await?;
Ok(()) Ok(())
} }
@ -665,6 +716,29 @@ pub struct ActiveVmWithNode {
pub collected_at: Datetime, pub collected_at: Datetime,
} }
impl From<ActiveVmWithNode> for ActiveVm {
fn from(val: ActiveVmWithNode) -> Self {
Self {
id: val.id,
admin: val.admin,
vm_node: val.vm_node.id,
hostname: val.hostname,
mapped_ports: val.mapped_ports,
public_ipv4: val.public_ipv4,
public_ipv6: val.public_ipv6,
disk_size_gb: val.disk_size_gb,
vcpus: val.vcpus,
memory_mb: val.memory_mb,
dtrfs_sha: val.dtrfs_sha,
kernel_sha: val.kernel_sha,
created_at: val.created_at,
price_per_unit: val.price_per_unit,
locked_nano: val.locked_nano,
collected_at: val.collected_at,
}
}
}
impl ActiveVmWithNode { impl ActiveVmWithNode {
pub async fn get_by_uuid(db: &Surreal<Client>, uuid: &str) -> Result<Option<Self>, Error> { pub async fn get_by_uuid(db: &Surreal<Client>, uuid: &str) -> Result<Option<Self>, Error> {
let contract: Option<Self> = let contract: Option<Self> =
@ -750,7 +824,8 @@ impl From<&old_brain::BrainData> for Vec<VmNode> {
avail_ports: old_node.avail_ports, avail_ports: old_node.avail_ports,
max_ports_per_vm: old_node.max_ports_per_vm, max_ports_per_vm: old_node.max_ports_per_vm,
price: old_node.price, price: old_node.price,
offline_minutes: old_node.offline_minutes, disconnected_at: Datetime::default(),
connected_at: Datetime::default(),
}); });
} }
nodes nodes

@ -107,24 +107,52 @@ impl BrainGeneralCli for GeneralCliServer {
async fn register_operator( async fn register_operator(
&self, &self,
_req: Request<RegOperatorReq>, req: Request<RegOperatorReq>,
) -> Result<Response<Empty>, Status> { ) -> Result<Response<Empty>, Status> {
todo!(); let req = check_sig_from_req(req)?;
// let req = check_sig_from_req(req)?; log::info!("Regitering new operator: {req:?}");
// info!("Regitering new operator: {req:?}"); match db::Account::operator_reg(&self.db, &req.pubkey, &req.email, req.escrow).await {
// match self.data.register_operator(req) { Ok(()) => Ok(Response::new(Empty {})),
// Ok(()) => Ok(Response::new(Empty {})), Err(e) if matches!(e, db::Error::InsufficientFunds | db::Error::MinimalEscrow) => {
// Err(e) => Err(Status::failed_precondition(e.to_string())), Err(Status::failed_precondition(e.to_string()))
// } }
Err(e) => {
log::info!("Failed to register operator: {e:?}");
Err(Status::unknown(
"Unknown error. Please try again or contact the DeTEE devs team.",
))
}
}
} }
async fn kick_contract(&self, _req: Request<KickReq>) -> Result<Response<KickResp>, Status> { async fn kick_contract(&self, req: Request<KickReq>) -> Result<Response<KickResp>, Status> {
todo!(); let req = check_sig_from_req(req)?;
// let req = check_sig_from_req(req)?; match db::WrapperContract::kick_contract(
// match self.data.kick_contract(&req.operator_wallet, &req.contract_uuid, &req.reason).await { &self.db,
// Ok(nano_lp) => Ok(Response::new(KickResp { nano_lp })), &req.operator_wallet,
// Err(e) => Err(Status::permission_denied(e.to_string())), &req.contract_uuid,
// } &req.reason,
)
.await
{
Ok(nano_lp) => Ok(Response::new(KickResp { nano_lp })),
Err(e)
if matches!(
e,
db::Error::ContractNotFound
| db::Error::AccessDenied
| db::Error::FailedToDeleteContract(_)
) =>
{
Err(Status::failed_precondition(e.to_string()))
}
Err(e) => {
log::info!("Failed to kick contract: {e:?}");
Err(Status::unknown(
"Unknown error. Please try again or contact the DeTEE devs team.",
))
}
}
} }
async fn ban_user(&self, _req: Request<BanUserReq>) -> Result<Response<Empty>, Status> { async fn ban_user(&self, _req: Request<BanUserReq>) -> Result<Response<Empty>, Status> {

@ -30,7 +30,7 @@ impl VmDaemonServer {
#[tonic::async_trait] #[tonic::async_trait]
impl BrainVmDaemon for VmDaemonServer { impl BrainVmDaemon for VmDaemonServer {
type BrainMessagesStream = Pin<Box<dyn Stream<Item = Result<BrainVmMessage, Status>> + Send>>; type BrainMessagesStream = Pin<Box<dyn Stream<Item = Result<BrainVmMessage, Status>> + Send>>;
type RegisterVmNodeStream = Pin<Box<dyn Stream<Item = Result<VmContract, Status>> + Send>>; type RegisterVmNodeStream = Pin<Box<dyn Stream<Item = Result<DeleteVmReq, Status>> + Send>>;
async fn register_vm_node( async fn register_vm_node(
&self, &self,
@ -53,17 +53,18 @@ impl BrainVmDaemon for VmDaemonServer {
avail_ipv6: 0, avail_ipv6: 0,
avail_ports: 0, avail_ports: 0,
max_ports_per_vm: 0, max_ports_per_vm: 0,
offline_minutes: 0, disconnected_at: surrealdb::sql::Datetime::default(),
connected_at: surrealdb::sql::Datetime::default(),
} }
.register(&self.db) .register(&self.db)
.await?; .await?;
info!("Sending existing contracts to {}", req.node_pubkey); info!("Sending deleted contracts to {}", req.node_pubkey);
let contracts = db::ActiveVmWithNode::list_by_node(&self.db, &req.node_pubkey).await?; let deleted_vms = db::DeletedVm::list_by_node(&self.db, &req.node_pubkey).await?;
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 deleted_vm in deleted_vms {
let _ = tx.send(Ok(contract.into())).await; let _ = tx.send(Ok(deleted_vm.into())).await;
} }
}); });
let output_stream = ReceiverStream::new(rx); let output_stream = ReceiverStream::new(rx);
@ -83,6 +84,7 @@ impl BrainVmDaemon for VmDaemonServer {
&auth.signature, &auth.signature,
)?; )?;
info!("Daemon {} connected to receive brain messages", pubkey); info!("Daemon {} connected to receive brain messages", pubkey);
let _ = db::VmNode::set_online(&self.db, &pubkey).await;
let (tx, rx) = mpsc::channel(6); let (tx, rx) = mpsc::channel(6);
{ {
@ -194,7 +196,8 @@ impl BrainVmDaemon for VmDaemonServer {
_ => {} _ => {}
}, },
Err(e) => { Err(e) => {
log::warn!("Daemon disconnected: {e:?}"); log::warn!("Daemon disconnected for {pubkey}: {e:?}");
let _ = db::VmNode::set_offline(&self.db, &pubkey).await;
} }
} }
} }

2
surql/README.md Normal file

@ -0,0 +1,2 @@
This is actually SurrealQL (`.surql`), but the files have the `.sql`
extension to enable syntax coloring.

12
surql/brain-timer.sh Executable file

@ -0,0 +1,12 @@
#!/bin/bash
source /etc/detee/brain/config.ini
import="docker run -i --rm --net=host \
--volume "/etc/detee/brain/timer.surql:/timer.surql" \
surrealdb/surrealdb:latest import \
--endpoint "http://${DB_URL}" \
--username $DB_USER --password "$DB_PASS" \
--namespace $DB_NAMESPACE --database $DB_NAME"
$import timer.surql

@ -0,0 +1,6 @@
[Unit]
Description=Process brain contracts
[Service]
Type=oneshot
ExecStart=/etc/detee/brain/brain-timer.sh

@ -0,0 +1,9 @@
[Unit]
Description=Run detee-brain-contracts.service every minute
[Timer]
OnCalendar=*-*-* *:*:00
Persistent=true
[Install]
WantedBy=timers.target

27
surql/functions.sql Normal file

@ -0,0 +1,27 @@
DEFINE FUNCTION OVERWRITE fn::vm_price_per_minute(
$vm_id: record
) {
LET $vm = (select * from $vm_id)[0];
LET $ip_price = IF $vm.public_ipv4.len() > 0 { 10 } ELSE { 0 };
RETURN (
($vm.vcpus * 10) + (($vm.memory_mb + 256) / 200) + ($vm.disk_size_gb / 10) + $ip_price)
* $vm.price_per_unit;
};
DEFINE FUNCTION OVERWRITE fn::delete_vm(
$vm_id: record
) {
LET $vm = (select * from $vm_id)[0];
LET $account = $vm.in;
LET $deleted_vm = $vm.patch([{
'op': 'replace',
'path': 'id',
'value': type::record("deleted_vm:" + record::id($vm.id))
}]);
IF $vm.locked_nano >= 0 {
UPDATE $account SET balance += $vm.locked_nano;
};
INSERT RELATION INTO deleted_vm ( $deleted_vm );
DELETE $vm.id;
};

@ -18,7 +18,8 @@ DEFINE FIELD avail_ipv6 ON TABLE vm_node TYPE int;
DEFINE FIELD avail_ports ON TABLE vm_node TYPE int; DEFINE FIELD avail_ports ON TABLE vm_node TYPE int;
DEFINE FIELD max_ports_per_vm ON TABLE vm_node TYPE int; DEFINE FIELD max_ports_per_vm ON TABLE vm_node TYPE int;
DEFINE FIELD price ON TABLE vm_node TYPE int; DEFINE FIELD price ON TABLE vm_node TYPE int;
DEFINE FIELD offline_minutes ON TABLE vm_node TYPE int; DEFINE FIELD connected_at ON TABLE vm_node TYPE datetime;
DEFINE FIELD disconnected_at ON TABLE vm_node TYPE datetime;
DEFINE TABLE new_vm_req TYPE RELATION FROM account TO vm_node SCHEMAFULL; DEFINE TABLE new_vm_req TYPE RELATION FROM account TO vm_node SCHEMAFULL;
DEFINE FIELD hostname ON TABLE new_vm_req TYPE string; DEFINE FIELD hostname ON TABLE new_vm_req TYPE string;
@ -74,7 +75,7 @@ DEFINE FIELD memory_mb ON TABLE deleted_vm TYPE int;
DEFINE FIELD dtrfs_sha ON TABLE deleted_vm TYPE string; DEFINE FIELD dtrfs_sha ON TABLE deleted_vm TYPE string;
DEFINE FIELD kernel_sha ON TABLE deleted_vm TYPE string; DEFINE FIELD kernel_sha ON TABLE deleted_vm TYPE string;
DEFINE FIELD created_at ON TABLE deleted_vm TYPE datetime; DEFINE FIELD created_at ON TABLE deleted_vm TYPE datetime;
DEFINE FIELD deleted_at ON TABLE deleted_vm TYPE datetime; DEFINE FIELD deleted_at ON TABLE deleted_vm TYPE datetime DEFAULT time::now();
DEFINE FIELD price_per_unit ON TABLE deleted_vm TYPE int; DEFINE FIELD price_per_unit ON TABLE deleted_vm TYPE int;
DEFINE TABLE app_node SCHEMAFULL; DEFINE TABLE app_node SCHEMAFULL;
@ -130,8 +131,6 @@ DEFINE FIELD disk_size_gb ON TABLE deleted_app TYPE int;
DEFINE FIELD created_at ON TABLE deleted_app TYPE datetime; DEFINE FIELD created_at ON TABLE deleted_app TYPE datetime;
DEFINE FIELD deleted_at ON TABLE deleted_app TYPE datetime; DEFINE FIELD deleted_at ON TABLE deleted_app TYPE datetime;
DEFINE FIELD price_per_unit ON TABLE deleted_app TYPE int; DEFINE FIELD price_per_unit ON TABLE deleted_app TYPE int;
DEFINE FIELD locked_nano ON TABLE deleted_app TYPE int;
DEFINE FIELD collected_at ON TABLE deleted_app TYPE datetime;
DEFINE FIELD mr_enclave ON TABLE deleted_app TYPE string; DEFINE FIELD mr_enclave ON TABLE deleted_app TYPE string;
DEFINE FIELD package_url ON TABLE deleted_app TYPE string; DEFINE FIELD package_url ON TABLE deleted_app TYPE string;
DEFINE FIELD hratls_pubkey ON TABLE deleted_app TYPE string; DEFINE FIELD hratls_pubkey ON TABLE deleted_app TYPE string;
@ -143,6 +142,7 @@ DEFINE TABLE kick TYPE RELATION FROM account TO account;
DEFINE FIELD created_at ON TABLE kick TYPE datetime; DEFINE FIELD created_at ON TABLE kick TYPE datetime;
DEFINE FIELD reason ON TABLE kick TYPE string; DEFINE FIELD reason ON TABLE kick TYPE string;
DEFINE FIELD contract ON TABLE kick TYPE record<deleted_vm|deleted_app>; DEFINE FIELD contract ON TABLE kick TYPE record<deleted_vm|deleted_app>;
DEFINE FIELD node ON TABLE kick TYPE record<vm_node|app_name>;
DEFINE TABLE report TYPE RELATION FROM account TO vm_node|app_node; DEFINE TABLE report TYPE RELATION FROM account TO vm_node|app_node;
DEFINE FIELD created_at ON TABLE report TYPE datetime; DEFINE FIELD created_at ON TABLE report TYPE datetime;

160
surql/testing/data.sql Normal file

@ -0,0 +1,160 @@
INSERT {
id: vm_node:online_node,
avail_ipv4: 0,
avail_ipv6: 0,
avail_mem_mb: 25000,
avail_ports: 19999,
avail_storage_gbs: 700,
avail_vcpus: 27,
city: 'Pula',
connected_at: time::now(),
country: 'HR',
disconnected_at: time::now() - 1m,
ip: '184.107.169.199',
max_ports_per_vm: 5,
operator: account:operator1,
price: 18000,
region: 'Istria'
};
INSERT {
id: vm_node:offline_node,
avail_ipv4: 0,
avail_ipv6: 0,
avail_mem_mb: 25000,
avail_ports: 19999,
avail_storage_gbs: 700,
avail_vcpus: 27,
city: 'Pula',
connected_at: time::now() - 1m,
country: 'HR',
disconnected_at: time::now(),
ip: '184.107.200.100',
max_ports_per_vm: 5,
operator: account:operator2,
price: 18000,
region: 'Istria'
};
INSERT {
id: vm_node:online_node2,
avail_ipv4: 0,
avail_ipv6: 0,
avail_mem_mb: 25000,
avail_ports: 19999,
avail_storage_gbs: 700,
avail_vcpus: 27,
city: 'Pula',
connected_at: time::now() - 1m,
country: 'HR',
disconnected_at: time::now() - 10m,
ip: '184.2.200.100',
max_ports_per_vm: 5,
operator: account:operator3,
price: 18000,
region: 'Istria'
};
INSERT {
balance: 10000000000,
email: '',
escrow: 0,
id: account:user1,
tmp_locked: 0
};
INSERT {
balance: 10000000000,
email: '',
escrow: 0,
id: account:user2,
tmp_locked: 0
};
INSERT {
id: account:operator1,
balance: 10000000000,
email: '',
escrow: 5_000_000_000_000,
tmp_locked: 0
};
INSERT {
id: account:operator2,
balance: 10000000000,
email: '',
escrow: 5000000000000,
tmp_locked: 0
};
INSERT {
id: account:operator3,
balance: 0,
email: '',
escrow: 0,
tmp_locked: 0
};
INSERT RELATION {
id: active_vm:vm1,
in: account:user1,
out: vm_node:online_node,
collected_at: time::now() - 1h,
created_at: time::now() - 1h,
disk_size_gb: 400,
dtrfs_sha: 'd207644ee60d54009b6ecdfb720e2ec251cde31774dd249fcc7435aca0377990',
hostname: 'vm1',
kernel_sha: 'e765e56166ef321b53399b9638584d1279821dbe3d46191c1f66bbaa075e7919',
locked_nano: 1_000_000_000,
mapped_ports: [],
memory_mb: 80000,
price_per_unit: 20000,
public_ipv4: '192.168.10.10',
public_ipv6: '',
vcpus: 40
};
INSERT RELATION {
id: active_vm:vm2,
in: account:user2,
out: vm_node:offline_node,
collected_at: time::now() - 10m,
created_at: time::now() - 1h,
disk_size_gb: 10,
dtrfs_sha: 'd207644ee60d54009b6ecdfb720e2ec251cde31774dd249fcc7435aca0377990',
hostname: 'vm1',
kernel_sha: 'e765e56166ef321b53399b9638584d1279821dbe3d46191c1f66bbaa075e7919',
locked_nano: 80000000,
mapped_ports: [
[
44551,
22
]
],
memory_mb: 5000,
price_per_unit: 20000,
public_ipv4: '',
public_ipv6: '',
vcpus: 4
};
INSERT RELATION {
id: active_vm:vm3,
in: account:user1,
out: vm_node:online_node2,
collected_at: time::now() - 30d,
created_at: time::now() - 60d,
disk_size_gb: 10,
dtrfs_sha: 'd207644ee60d54009b6ecdfb720e2ec251cde31774dd249fcc7435aca0377990',
hostname: 'vm1',
kernel_sha: 'e765e56166ef321b53399b9638584d1279821dbe3d46191c1f66bbaa075e7919',
locked_nano: 25_000_000_000,
mapped_ports: [],
memory_mb: 1000,
price_per_unit: 20000,
public_ipv4: '192.168.10.20',
public_ipv6: '',
vcpus: 1
};

33
surql/testing/run_test1.sh Executable file

@ -0,0 +1,33 @@
#!/bin/bash
cd -- "$( dirname -- "${BASH_SOURCE[0]}" )"
import="docker run -i --rm --net=host \
--volume "$(pwd)/../:/opt/scripts/" \
surrealdb/surrealdb:latest import \
--endpoint http://127.0.0.1:8000 \
--username root --password root \
--namespace testing --database testbrain"
sql="docker run -i --rm --net=host \
surrealdb/surrealdb:latest sql \
--hide-welcome \
--endpoint http://127.0.0.1:8000 \
--username root --password root \
--namespace testing --database testbrain"
echo DELETING EXISTING DATA:
echo "REMOVE DATABASE testbrain;" | $sql
echo CREATING TABLES:
$import /opt/scripts/tables.sql
echo LOADING FUNCTIONS:
$import /opt/scripts/functions.sql
echo LOADING MOCK DATA:
$import /opt/scripts/testing/data.sql
echo RUN TIMER FUNCTION:
$import /opt/scripts/timer.sql
echo CHECK IF DATA GOT MODIFIED CORRECTLY:
$import /opt/scripts/testing/verification.sql

@ -0,0 +1,19 @@
if (select balance from account:operator1)[0].balance != 15_000_000_000 {
throw("Operator 1 did't get paid for serving a VM.")
};
if (select balance from account:user2)[0].balance != 10013400000 {
throw("User 2 did't get compensated for his VM going down.")
};
if (select escrow from account:operator2)[0].escrow != 4999986600000 {
throw("Operator 2 didn't get punished for not serving a VM.")
};
if (select id from deleted_vm:vm1).len() == 0 {
throw("VM1 didn't get deleted.")
};
if (select id from active_vm:vm3).len() =! 1 {
throw("A mini VM costs more than 25LP per month.")
};
if (select balance from account:operator3)[0].balance != 23328000000 {
throw("Operators without escrow still get a bonus.")
};

29
surql/timer.sql Normal file

@ -0,0 +1,29 @@
FOR $contract IN (select * from active_vm fetch out) {
LET $operator = (select * from $contract.out.operator)[0];
LET $node_is_online = $contract.out.connected_at > $contract.out.disconnected_at;
LET $price_per_minute = fn::vm_price_per_minute($contract.id);
LET $amount_due = (time::now() - $contract.collected_at).mins() * $price_per_minute;
LET $amount_paid = IF $amount_due > $contract.locked_nano {
$contract.locked_nano
} ELSE {
$amount_due
};
LET $escrow_multiplier = IF $operator.escrow < 5_000_000_000_000 { 1 } ELSE { 5 };
IF $node_is_online {
UPDATE $operator.id SET balance += $amount_paid * $escrow_multiplier;
UPDATE $contract.id SET
locked_nano -= $amount_paid,
collected_at = time::now();
} ELSE {
LET $compensation = IF $amount_due > $operator.escrow {
$operator.escrow
} ELSE {
$amount_due
};
UPDATE $operator.id SET escrow -= $compensation;
UPDATE $contract.in SET balance += $compensation;
};
IF $amount_paid >= $contract.locked_nano {
fn::delete_vm($contract.id);
};
};

@ -211,3 +211,37 @@ async fn test_inspect_operator() {
assert!(!inspect_response.vm_nodes.is_empty()); assert!(!inspect_response.vm_nodes.is_empty());
assert_eq!(&inspect_response.vm_nodes[0].operator, &operator_key.pubkey); assert_eq!(&inspect_response.vm_nodes[0].operator, &operator_key.pubkey);
} }
#[tokio::test]
async fn test_kick_contract() {
// TODO: implement seed data to test
// possibilities
// 1. vm contract
// 2. app contract
// 3. non existent contract
// 4. other operator's contract
// 5. contract collected more than a week
// 6. refund amount calculation
// 7. refund of multiple contract kick in a day for same user
env_logger::builder()
.filter_level(log::LevelFilter::Trace)
.filter_module("tungstenite", log::LevelFilter::Debug)
.filter_module("tokio_tungstenite", log::LevelFilter::Debug)
.init();
let db_conn = prepare_test_db().await.unwrap();
let operator_wallet = "BFopWmwcZAMF1h2PFECZNdEucdZfnZZ32p6R9ZaBiVsS";
let contract_uuid = "26577f1c98674a1780a86cf0490f1270";
let reason = "test reason";
let kick_response = surreal_brain::db::general::WrapperContract::kick_contract(
&db_conn,
&operator_wallet,
&contract_uuid,
&reason,
)
.await;
dbg!(kick_response.unwrap());
}