Kick Contract #2
@ -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";
|
||||||
|
@ -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")]
|
||||||
@ -164,7 +165,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 +211,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 +303,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 +325,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, MIN_ESCROW, TOKEN_DECIMAL};
|
|
||||||
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()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -147,6 +146,25 @@ 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 yesterday = chrono::Utc::now() - chrono::Duration::days(1);
|
||||||
|
let mut result = db
|
||||||
|
.query(format!(
|
||||||
|
"select * from {KICK} where in = {ACCOUNT}:{account} and created_at > {yesterday};"
|
||||||
|
))
|
||||||
|
.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)]
|
||||||
@ -185,7 +203,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,
|
||||||
@ -258,3 +276,107 @@ 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 (node_operator, admin, contract_id, node_id, collected_at, price_per_mint, is_vm) =
|
||||||
|
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.to_string(),
|
||||||
|
active_vm.admin.key().to_string(),
|
||||||
|
active_vm.id,
|
||||||
|
active_vm.vm_node.id,
|
||||||
|
active_vm.collected_at,
|
||||||
|
price_per_minute,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
} 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.to_string(),
|
||||||
|
active_app.admin.key().to_string(),
|
||||||
|
active_app.id,
|
||||||
|
active_app.app_node.id,
|
||||||
|
active_app.collected_at,
|
||||||
|
price_per_minute,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return Err(Error::ContractNotFound);
|
||||||
|
};
|
||||||
|
|
||||||
|
if node_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 mut refund_amount = minutes_to_refund * price_per_mint;
|
||||||
|
|
||||||
|
if !Kick::kicked_in_a_day(db, &admin).await?.is_empty() {
|
||||||
|
refund_amount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut operator_account = Account::get(db, &node_operator).await?;
|
||||||
|
let mut admin_account = Account::get(db, &admin).await?;
|
||||||
|
|
||||||
|
if operator_account.escrow < refund_amount {
|
||||||
|
refund_amount = operator_account.escrow;
|
||||||
|
}
|
||||||
|
|
||||||
|
log::debug!(
|
||||||
|
"Removing {refund_amount} escrow from {} and giving it to {}",
|
||||||
|
node_operator,
|
||||||
|
admin
|
||||||
|
);
|
||||||
|
|
||||||
|
admin_account.balance = admin_account.balance.saturating_add(refund_amount);
|
||||||
|
operator_account.escrow = operator_account.escrow.saturating_sub(refund_amount);
|
||||||
|
|
||||||
|
let kick = Kick {
|
||||||
|
id: RecordId::from((KICK, contract_uuid)),
|
||||||
|
from_account: operator_account.id.clone(),
|
||||||
|
to_account: admin_account.id.clone(),
|
||||||
|
created_at: Datetime::default(),
|
||||||
|
reason: reason.to_string(),
|
||||||
|
contract: contract_id.clone(),
|
||||||
|
node: node_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
kick.submit(db).await?;
|
||||||
|
operator_account.save(db).await?;
|
||||||
|
admin_account.save(db).await?;
|
||||||
|
|
||||||
|
let contract_id = contract_id.to_string();
|
||||||
|
|
||||||
|
if is_vm && !ActiveVm::delete(db, &contract_id).await? {
|
||||||
|
return Err(Error::FailedToDeleteContract(format!("vm:{contract_id}")));
|
||||||
|
} else if !is_vm && !ActiveApp::delete(db, &contract_id).await? {
|
||||||
|
return Err(Error::FailedToDeleteContract(format!("app:{contract_id}")));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(refund_amount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -24,8 +24,8 @@ 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}")]
|
||||||
@ -36,6 +36,12 @@ pub enum Error {
|
|||||||
MinimalEscrow,
|
MinimalEscrow,
|
||||||
#[error("Insufficient funds, deposit more tokens")]
|
#[error("Insufficient funds, deposit more tokens")]
|
||||||
InsufficientFunds,
|
InsufficientFunds,
|
||||||
|
#[error("Contract not found")]
|
||||||
|
ContractNotFound,
|
||||||
|
#[error("Access denied")]
|
||||||
|
AccessDenied,
|
||||||
|
#[error("Failed to delete contract {0}")]
|
||||||
|
FailedToDeleteContract(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod prelude {
|
pub mod prelude {
|
||||||
|
23
src/db/vm.rs
23
src/db/vm.rs
@ -728,6 +728,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> =
|
||||||
|
@ -125,13 +125,34 @@ impl BrainGeneralCli for GeneralCliServer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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> {
|
||||||
|
@ -144,6 +144,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;
|
||||||
|
@ -37,7 +37,7 @@ pub async fn register_vm_node(
|
|||||||
client: &mut BrainVmDaemonClient<Channel>,
|
client: &mut BrainVmDaemonClient<Channel>,
|
||||||
key: &Key,
|
key: &Key,
|
||||||
operator_wallet: &str,
|
operator_wallet: &str,
|
||||||
) -> Result<Vec<vm_proto::VmContract>> {
|
) -> Result<Vec<vm_proto::DeleteVmReq>> {
|
||||||
log::info!("Registering vm_node: {}", key.pubkey);
|
log::info!("Registering vm_node: {}", key.pubkey);
|
||||||
let node_pubkey = key.pubkey.clone();
|
let node_pubkey = key.pubkey.clone();
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user