Compare commits

...

3 Commits

Author SHA1 Message Date
810c1b1966
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:04:07 +05:30
16fd64ac13
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-15 19:15:44 +05:30
4295b3ae1d
register operator 2025-05-15 12:52:23 +05:30
8 changed files with 356 additions and 31 deletions

@ -130,8 +130,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 +141,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;

@ -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,
@ -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 {

@ -665,6 +665,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> =

@ -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> {

@ -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());
}