diff --git a/src/db/general.rs b/src/db/general.rs index 952cfab..b6d8061 100644 --- a/src/db/general.rs +++ b/src/db/general.rs @@ -75,6 +75,38 @@ impl Account { op_account.save(db).await?; Ok(()) } + + pub async fn slash_account( + db: &Surreal, + account: &str, + slash_amount: u64, + ) -> Result<(), Error> { + let tx_query = " + BEGIN TRANSACTION; + LET $account = $account_input; + + UPDATE $account SET escrow -= $slash_amount; + IF $account.escrow < 0 {{ + THROW 'Insufficient escrow.' + }}; + + COMMIT TRANSACTION;"; + + let mut query_resp = db + .query(tx_query) + .bind(("account_input", RecordId::from((ACCOUNT, account)))) + .bind(("slash_amount", slash_amount.saturating_mul(TOKEN_DECIMAL))) + .await?; + + log::trace!("query_resp: {query_resp:?}"); + + let query_error = query_resp.take_errors(); + if !query_error.is_empty() { + log::error!("slash_account query error: {query_error:?}"); + return Err(Error::FailedToSlashOperator(account.to_string())); + } + Ok(()) + } } impl Account { @@ -105,6 +137,9 @@ impl Account { let vm_node_ban: Option = query_response.take(0).unwrap(); let app_node_ban: Option = query_response.take(1).unwrap(); + log::trace!("vm_node_ban: {vm_node_ban:?}"); + log::trace!("app_node_ban: {app_node_ban:?}"); + Ok(vm_node_ban.is_some() || app_node_ban.is_some()) } } @@ -162,6 +197,7 @@ impl Ban { user_wallet: &str, ) -> Result<(), Error> { if Self::get_record_by_wallets(db, op_wallet, user_wallet).await?.is_some() { + log::error!("User {user_wallet} is already banned by {op_wallet}"); return Err(Error::AlreadyBanned(op_wallet.to_string())); } let _: Vec = db diff --git a/src/db/mod.rs b/src/db/mod.rs index 0f66625..c84689a 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -47,6 +47,8 @@ pub enum Error { FailedKickContract(String), #[error("Already banned {0}")] AlreadyBanned(String), + #[error("Failed to slash operator {0}")] + FailedToSlashOperator(String), } pub mod prelude { diff --git a/src/grpc/general.rs b/src/grpc/general.rs index fa97751..8c048ed 100644 --- a/src/grpc/general.rs +++ b/src/grpc/general.rs @@ -154,6 +154,7 @@ impl BrainGeneralCli for GeneralCliServer { async fn ban_user(&self, req: Request) -> Result, Status> { let req = check_sig_from_req(req)?; + log::info!("Banning user: {}, by: {}", req.user_wallet, req.operator_wallet); db::Ban::ban_a_user(&self.db, &req.operator_wallet, &req.user_wallet).await?; Ok(Response::new(Empty {})) } @@ -163,16 +164,16 @@ impl BrainGeneralCli for GeneralCliServer { async fn airdrop(&self, req: Request) -> Result, Status> { check_admin_key(&req)?; let req = check_sig_from_req(req)?; + log::info!("Airdropping {} tokens to {}", req.tokens, req.pubkey); db::Account::airdrop(&self.db, &req.pubkey, req.tokens).await?; Ok(Response::new(Empty {})) } - async fn slash(&self, _req: Request) -> Result, Status> { - todo!(); - // check_admin_key(&req)?; - // let req = check_sig_from_req(req)?; - // self.data.slash_account(&req.pubkey, req.tokens); - // Ok(Response::new(Empty {})) + async fn slash(&self, req: Request) -> Result, Status> { + check_admin_key(&req)?; + let req = check_sig_from_req(req)?; + db::Account::slash_account(&self.db, &req.pubkey, req.tokens).await?; + Ok(Response::new(Empty {})) } async fn list_accounts( diff --git a/tests/common/vm_cli_utils.rs b/tests/common/vm_cli_utils.rs index 3a1bb40..e106462 100644 --- a/tests/common/vm_cli_utils.rs +++ b/tests/common/vm_cli_utils.rs @@ -2,7 +2,7 @@ use super::test_utils::{admin_keys, Key}; use anyhow::{anyhow, Result}; use detee_shared::common_proto::Empty; use detee_shared::general_proto::brain_general_cli_client::BrainGeneralCliClient; -use detee_shared::general_proto::{AirdropReq, ReportNodeReq}; +use detee_shared::general_proto::{AirdropReq, RegOperatorReq, ReportNodeReq}; use detee_shared::vm_proto; use detee_shared::vm_proto::brain_vm_cli_client::BrainVmCliClient; use surreal_brain::constants::{ACTIVE_VM, NEW_VM_REQ}; @@ -77,3 +77,12 @@ pub async fn report_node( Ok(client_gen_cli.report_node(key.sign_request(report_req)?).await?) } + +pub async fn register_operator(brain_channel: &Channel, key: &Key, escrow: u64) -> Result<()> { + let mut cli_client = BrainGeneralCliClient::new(brain_channel.clone()); + let reg_req = + RegOperatorReq { pubkey: key.pubkey.clone(), escrow, email: "foo@bar.com".to_string() }; + + cli_client.register_operator(key.sign_request(reg_req.clone()).unwrap()).await?; + Ok(()) +} diff --git a/tests/grpc_general_test.rs b/tests/grpc_general_test.rs index b0a1092..aecc728 100644 --- a/tests/grpc_general_test.rs +++ b/tests/grpc_general_test.rs @@ -2,14 +2,14 @@ use common::prepare_test_env::{ prepare_test_db, run_service_for_stream, run_service_in_background, }; use common::test_utils::{admin_keys, Key}; -use common::vm_cli_utils::{airdrop, create_new_vm, report_node}; +use common::vm_cli_utils::{airdrop, create_new_vm, register_operator, report_node}; use common::vm_daemon_utils::{mock_vm_daemon, register_vm_node}; use detee_shared::common_proto::{Empty, Pubkey}; use detee_shared::general_proto::brain_general_cli_client::BrainGeneralCliClient; -use detee_shared::general_proto::{AirdropReq, BanUserReq}; +use detee_shared::general_proto::{AirdropReq, BanUserReq, SlashReq}; use detee_shared::vm_proto::brain_vm_daemon_client::BrainVmDaemonClient; use futures::StreamExt; -use surreal_brain::constants::{BAN, TOKEN_DECIMAL, VM_NODE}; +use surreal_brain::constants::{ACCOUNT, BAN, TOKEN_DECIMAL, VM_NODE}; use surreal_brain::db::prelude as db; use surreal_brain::db::vm::VmNodeWithReports; @@ -210,6 +210,37 @@ async fn test_inspect_operator() { assert_eq!(&inspect_response.vm_nodes[0].operator, &operator_key.pubkey); } +#[tokio::test] +async fn test_register_operator() { + let db_conn = prepare_test_db().await.unwrap(); + + let brain_channel = run_service_for_stream().await.unwrap(); + let key = Key::new(); + + let min_escrew_error = register_operator(&brain_channel, &key, 10).await.err().unwrap(); + assert!(min_escrew_error.to_string().contains("Minimum escrow amount is 5000")); + + let no_balance = register_operator(&brain_channel, &key, 5000).await.err().unwrap(); + assert!(no_balance.to_string().contains("Insufficient funds, deposit more tokens")); + + airdrop(&brain_channel, &key.pubkey, 1000).await.unwrap(); + + let no_balance = register_operator(&brain_channel, &key, 5000).await.err().unwrap(); + assert!(no_balance.to_string().contains("Insufficient funds, deposit more tokens")); + + airdrop(&brain_channel, &key.pubkey, 7000).await.unwrap(); + + register_operator(&brain_channel, &key, 6000).await.unwrap(); + + let operator_account: Option = + db_conn.select((ACCOUNT, key.pubkey)).await.unwrap(); + assert!(operator_account.is_some()); + let account = operator_account.unwrap(); + assert_eq!(account.escrow, 6000 * TOKEN_DECIMAL); + assert_eq!(account.balance, 2000 * TOKEN_DECIMAL); + assert_eq!(account.tmp_locked, 0); +} + #[tokio::test] async fn test_kick_contract() { // TODO: implement seed data to test @@ -308,3 +339,40 @@ async fn test_ban_user() { assert!(operator_banned_you.to_string().contains("This operator banned you")); } +#[tokio::test] +async fn test_slash_operator() { + let db_conn = prepare_test_db().await.unwrap(); + + let brain_channel = run_service_for_stream().await.unwrap(); + let mut cli_client = BrainGeneralCliClient::new(brain_channel.clone()); + let op_key = Key::new(); + + let admin = admin_keys()[0].clone(); + let escrew = 5500; + let slash_amt = 2500; + + airdrop(&brain_channel, &op_key.pubkey, 10000).await.unwrap(); + register_operator(&brain_channel, &op_key, escrew).await.unwrap(); + + let raw_slash_req = SlashReq { pubkey: op_key.pubkey.clone(), tokens: 2500 }; + + let other_key = Key::new(); + let admin_error = cli_client + .slash(other_key.sign_request(raw_slash_req.clone()).unwrap()) + .await + .err() + .unwrap(); + assert!(admin_error.message().contains("This operation is reserved to admin accounts")); + + let admin_error = + cli_client.slash(op_key.sign_request(raw_slash_req.clone()).unwrap()).await.err().unwrap(); + assert!(admin_error.message().contains("This operation is reserved to admin accounts")); + + cli_client.slash(admin.sign_request(raw_slash_req.clone()).unwrap()).await.unwrap(); + + let operator_account: Option = + db_conn.select((ACCOUNT, op_key.pubkey)).await.unwrap(); + assert!(operator_account.is_some()); + let account = operator_account.unwrap(); + assert_eq!(account.escrow, (escrew - slash_amt) * TOKEN_DECIMAL); +}