Compare commits

..

1 Commits

Author SHA1 Message Date
af3c3103e2
add support for operators 2025-02-13 17:55:46 +02:00
3 changed files with 99 additions and 24 deletions

@ -18,6 +18,10 @@ pub enum Error {
InsufficientFunds, InsufficientFunds,
#[error("Could not find contract {0}")] #[error("Could not find contract {0}")]
VmContractNotFound(String), VmContractNotFound(String),
#[error("This error should never happen.")]
ImpossibleError,
#[error("You don't have the required permissions for this operation.")]
AccessDenied,
} }
#[derive(Clone, Default)] #[derive(Clone, Default)]
@ -26,6 +30,7 @@ pub struct AccountData {
pub tmp_locked: u64, pub tmp_locked: u64,
// holds reasons why VMs of this account got kicked // holds reasons why VMs of this account got kicked
pub kicked_for: Vec<String>, pub kicked_for: Vec<String>,
pub last_kick: chrono::DateTime<Utc>,
// holds accounts that banned this account // holds accounts that banned this account
pub banned_by: HashSet<String>, pub banned_by: HashSet<String>,
} }
@ -99,14 +104,15 @@ pub struct VmContract {
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 VmContract { 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.
@ -117,7 +123,7 @@ impl VmContract {
+ (!self.public_ipv4.is_empty() as u64 * 10) + (!self.public_ipv4.is_empty() as u64 * 10)
} }
// Returns price per minute in nanoLP /// 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
} }
@ -182,6 +188,7 @@ impl BrainData {
tmp_locked: 0, tmp_locked: 0,
kicked_for: Vec::new(), kicked_for: Vec::new(),
banned_by: HashSet::new(), banned_by: HashSet::new(),
last_kick: chrono::Utc::now(),
}; };
return balance; return balance;
} }
@ -271,6 +278,63 @@ impl BrainData {
nodes.push(node); nodes.push(node);
} }
// todo: this should also support Apps
/// Receives: operator, contract uuid, reason of kick
pub async fn kick_contract(
&self,
operator: &str,
uuid: &str,
reason: &str,
) -> Result<(), Error> {
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_ammount = minutes_to_refund * contract.price_per_minute();
let mut user = 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(user.last_kick)
.gt(&chrono::Duration::days(1))
{
refund_ammount = 0;
}
if operator_data.escrow < refund_ammount {
refund_ammount = operator_data.escrow;
}
user.balance += refund_ammount;
user.kicked_for.push(reason.to_string());
operator_data.escrow -= refund_ammount;
self.delete_vm(grpc::DeleteVmReq {
uuid: contract.uuid,
admin_pubkey: contract.admin_pubkey,
})
.await?;
Ok(())
}
pub fn report_node(&self, admin_pubkey: String, node: &str, report: String) { pub fn report_node(&self, admin_pubkey: String, node: &str, report: String) {
let mut nodes = self.vm_nodes.write().unwrap(); let mut nodes = self.vm_nodes.write().unwrap();
if let Some(node) = nodes.iter_mut().find(|n| n.public_key == node) { if let Some(node) = nodes.iter_mut().find(|n| n.public_key == node) {
@ -363,17 +427,10 @@ impl BrainData {
} }
pub async fn delete_vm(&self, delete_vm: grpc::DeleteVmReq) -> Result<(), Error> { pub async fn delete_vm(&self, delete_vm: grpc::DeleteVmReq) -> Result<(), Error> {
let contract = match self.find_contract_by_uuid(&delete_vm.uuid) { let contract = self.find_contract_by_uuid(&delete_vm.uuid)?;
Some(contract) => { if contract.admin_pubkey != delete_vm.admin_pubkey {
if contract.admin_pubkey != delete_vm.admin_pubkey { return Err(Error::AccessDenied);
return Err(Error::VmContractNotFound(delete_vm.uuid)); }
}
contract
}
None => {
return Err(Error::VmContractNotFound(delete_vm.uuid));
}
};
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!(
@ -582,7 +639,7 @@ 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) => { Ok(contract) => {
if contract.admin_pubkey != req.admin_pubkey { if contract.admin_pubkey != req.admin_pubkey {
let _ = tx.send(grpc::UpdateVmResp { let _ = tx.send(grpc::UpdateVmResp {
uuid, uuid,
@ -593,7 +650,7 @@ impl BrainData {
} }
contract.node_pubkey contract.node_pubkey
} }
None => { 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
@ -753,9 +810,13 @@ impl BrainData {
.cloned() .cloned()
} }
pub fn find_contract_by_uuid(&self, uuid: &str) -> Option<VmContract> { pub fn find_contract_by_uuid(&self, uuid: &str) -> Result<VmContract, Error> {
let contracts = self.vm_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 list_all_contracts(&self) -> Vec<VmContract> { pub fn list_all_contracts(&self) -> Vec<VmContract> {

@ -216,7 +216,7 @@ impl BrainCli for BrainCliMock {
async fn report_node(&self, req: Request<ReportNodeReq>) -> Result<Response<Empty>, Status> { async fn report_node(&self, req: Request<ReportNodeReq>) -> Result<Response<Empty>, Status> {
let req = check_sig_from_req(req)?; let req = check_sig_from_req(req)?;
match self.data.find_contract_by_uuid(&req.contract) { match self.data.find_contract_by_uuid(&req.contract) {
Some(contract) Ok(contract)
if contract.admin_pubkey == req.admin_pubkey if contract.admin_pubkey == req.admin_pubkey
&& contract.node_pubkey == req.node_pubkey => && contract.node_pubkey == req.node_pubkey =>
{ {
@ -238,8 +238,8 @@ impl BrainCli for BrainCliMock {
info!("CLI {} requested ListVMVmContractsStream", req.admin_pubkey); info!("CLI {} requested ListVMVmContractsStream", req.admin_pubkey);
let contracts = match req.uuid.is_empty() { let contracts = match req.uuid.is_empty() {
false => match self.data.find_contract_by_uuid(&req.uuid) { false => match self.data.find_contract_by_uuid(&req.uuid) {
Some(contract) => vec![contract], Ok(contract) => vec![contract],
None => Vec::new(), _ => Vec::new(),
}, },
true => self.data.find_vm_contracts_by_admin(&req.admin_pubkey), true => self.data.find_vm_contracts_by_admin(&req.admin_pubkey),
}; };
@ -291,8 +291,8 @@ impl BrainCli for BrainCliMock {
async fn register_operator( async fn register_operator(
&self, &self,
req: tonic::Request<RegOperatorReq>, req: Request<RegOperatorReq>,
) -> std::result::Result<tonic::Response<Empty>, tonic::Status> { ) -> Result<Response<Empty>, Status> {
let req = check_sig_from_req(req)?; let req = check_sig_from_req(req)?;
info!("Regitering new operator: {req:?}"); info!("Regitering new operator: {req:?}");
match self.data.register_operator(req) { match self.data.register_operator(req) {
@ -301,6 +301,18 @@ impl BrainCli for BrainCliMock {
} }
} }
async fn kick_contract(&self, req: Request<KickReq>) -> Result<Response<Empty>, Status> {
let req = check_sig_from_req(req)?;
match self
.data
.kick_contract(&req.operator_wallet, &req.contract_uuid, &req.reason)
.await
{
Ok(()) => Ok(Response::new(Empty {})),
Err(e) => Err(Status::permission_denied(e.to_string())),
}
}
type ListOperatorsStream = type ListOperatorsStream =
Pin<Box<dyn Stream<Item = Result<ListOperatorsResp, Status>> + Send>>; Pin<Box<dyn Stream<Item = Result<ListOperatorsResp, Status>> + Send>>;
async fn list_operators( async fn list_operators(
@ -419,6 +431,7 @@ impl_pubkey_getter!(ReportNodeReq, admin_pubkey);
impl_pubkey_getter!(ListVmContractsReq, admin_pubkey); impl_pubkey_getter!(ListVmContractsReq, admin_pubkey);
impl_pubkey_getter!(RegisterVmNodeReq, node_pubkey); impl_pubkey_getter!(RegisterVmNodeReq, node_pubkey);
impl_pubkey_getter!(RegOperatorReq, pubkey); impl_pubkey_getter!(RegOperatorReq, pubkey);
impl_pubkey_getter!(KickReq, operator_wallet);
impl_pubkey_getter!(VmNodeFilters); impl_pubkey_getter!(VmNodeFilters);
impl_pubkey_getter!(Empty); impl_pubkey_getter!(Empty);

@ -234,8 +234,9 @@ message ReportNodeReq {
} }
message KickReq { message KickReq {
string uuid = 1; string operator_wallet = 1;
string reason = 2; string contract_uuid = 2;
string reason = 3;
} }
service BrainCli { service BrainCli {