From 5bc476d41b631a0b416df37d9b1153686f8d465b Mon Sep 17 00:00:00 2001 From: Yigit Sever Date: Mon, 25 Apr 2022 17:21:45 +0300 Subject: Implement gas fee --- src/config.rs | 3 ++ src/handlers.rs | 153 ++++++++++++++++++++++++++++++++++++++------------------ 2 files changed, 108 insertions(+), 48 deletions(-) (limited to 'src') diff --git a/src/config.rs b/src/config.rs index 9986970..c0b8584 100644 --- a/src/config.rs +++ b/src/config.rs @@ -49,6 +49,9 @@ pub struct Config { /// Coinbase reward pub block_reward: u16, + /// Transaction gas fee + pub tx_gas_fee: u16, + /// Transaction amount upper limit pub tx_upper_limit: u16, diff --git a/src/handlers.rs b/src/handlers.rs index ae82441..09cd2a5 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -664,30 +664,17 @@ pub async fn propose_block( )) } -/// POST /transaction -/// -/// Handles the new transaction requests -/// Can reject the block if; -/// # Arguments -/// * `new_transaction` - Valid JSON of a [`Transaction`] -/// * `token` - An Authorization header value such as `Bearer aaa.bbb.ccc` -/// * `db` - Global [`Db`] instance -/// -#[allow(clippy::too_many_lines)] // temporary, should be refactored -pub async fn propose_transaction( - new_transaction: Transaction, - token: String, +async fn deduct_gas_fee( + new_transaction: &Transaction, + token: &str, db: Db, -) -> Result { - warn!( - "[{}] New transaction proposal: {:?}", - db.config.name, &new_transaction - ); - - let users_store = db.users.read(); +) -> Option> { + let mut users_store = db.users.write(); // Is this transaction from an authorized source? - let internal_user = if let Some(existing_user) = users_store.get(&new_transaction.source) { + let mut internal_user: &mut User = if let Some(existing_user) = + users_store.get_mut(&new_transaction.source) + { existing_user } else { debug!( @@ -695,7 +682,7 @@ pub async fn propose_transaction( new_transaction.source ); - return Ok(warp::reply::with_status( + return Some(warp::reply::with_status( warp::reply::json(&UserFeedback { res: ResponseType::Error, message: "User with the given public key signature is not authorized".to_owned(), @@ -704,29 +691,28 @@ pub async fn propose_transaction( )); }; + // This check is early on because bots don't have public keys, avoiding undefined behaviour if internal_user.is_bot { - debug!("Someone tried to send as the bot"); + debug!("Someone tried to send as a bot"); - return Ok(warp::reply::with_status( + return Some(warp::reply::with_status( warp::reply::json(&UserFeedback { res: ResponseType::Error, - message: "Don's send transactions on behalf of bots".to_owned(), + message: "Don't send transactions on behalf of bots".to_owned(), }), StatusCode::BAD_REQUEST, )); } - // `internal_user` is an authenticated student and not a bot, can propose - // This public key was already written to the database, we can panic if it's not valid at // *this* point let proposer_public_key = &internal_user.public_key; - let token_payload = match authorize_proposer(&token, proposer_public_key) { + let token_payload = match authorize_proposer(token, proposer_public_key) { Ok(data) => data, Err(below) => { debug!("JWT Error: {:?}", below); - return Ok(warp::reply::with_status( + return Some(warp::reply::with_status( warp::reply::json(&UserFeedback { res: ResponseType::Error, message: below, @@ -736,6 +722,96 @@ pub async fn propose_transaction( } }; + // this transaction was already checked for correctness at custom_filters we can panic here if + // it has been changed since + let serd_tx = serde_json::to_string(&new_transaction).unwrap(); + + debug!("Taking the hash of {}", serd_tx); + + let hashed_transaction = Md5::digest(serd_tx.as_bytes()); + + if token_payload.claims.tha != format!("{:x}", hashed_transaction) { + return Some(warp::reply::with_status( + warp::reply::json(&UserFeedback { + res: ResponseType::Error, + message: "The hash of the transaction did not match the hash given in JWT" + .to_owned(), + }), + StatusCode::BAD_REQUEST, + )); + } + + // At this point we have authorized the user + // Deduct gas fee to process the transaction further + if internal_user.balance < db.config.tx_gas_fee { + debug!( + "User does not have enough balance ({}) to pay for the gas fee", + internal_user.balance + ); + return Some(warp::reply::with_status( + warp::reply::json(&UserFeedback { + res: ResponseType::Error, + message: "You cannot afford the gas fee for this transaction".to_owned(), + }), + StatusCode::BAD_REQUEST, + )); + } + + internal_user.balance -= db.config.tx_gas_fee; + + None +} + +/// POST /transaction +/// +/// Handles the new transaction requests +/// Can reject the block if; +/// # Arguments +/// * `new_transaction` - Valid JSON of a [`Transaction`] +/// * `token` - An Authorization header value such as `Bearer aaa.bbb.ccc` +/// * `db` - Global [`Db`] instance +/// +#[allow(clippy::too_many_lines)] // temporary, should be refactored +pub async fn propose_transaction( + new_transaction: Transaction, + token: String, + db: Db, +) -> Result { + warn!( + "[{}] New transaction proposal: {:?}", + db.config.name, &new_transaction + ); + + if let Some(error) = deduct_gas_fee(&new_transaction, &token, db.clone()).await { + return Ok(error); + } + + // Gas fee exists to discourage dumb bots + // Checks from this point on will be penalized as they already paid the gas fee but can still + // fail + + let users_store = db.users.read(); + + // We _can_ get the internal user from deduct_gas_fee but that one is a mutable reference + // We only need an unmutable reference from here on out, so unless something better comes along + // this is how we get the second internal_user + let internal_user = if let Some(existing_user) = users_store.get(&new_transaction.source) { + existing_user + } else { + debug!( + "User with public key signature {:?} is not found in the database", + new_transaction.source + ); + + return Ok(warp::reply::with_status( + warp::reply::json(&UserFeedback { + res: ResponseType::Error, + message: "User with the given public key signature is not authorized".to_owned(), + }), + StatusCode::BAD_REQUEST, + )); + }; + // is the target of the transaction in the system? if !users_store.contains_key(&new_transaction.target) { debug!( @@ -830,25 +906,6 @@ pub async fn propose_transaction( )); } - // this transaction was already checked for correctness at custom_filters, we can panic here if - // it has been changed since - - let serd_tx = serde_json::to_string(&new_transaction).unwrap(); - - debug!("Taking the hash of {}", serd_tx); - - let hashed_transaction = Md5::digest(serd_tx.as_bytes()); - if token_payload.claims.tha != format!("{:x}", hashed_transaction) { - return Ok(warp::reply::with_status( - warp::reply::json(&UserFeedback { - res: ResponseType::Error, - message: "The hash of the transaction did not match the hash given in JWT" - .to_owned(), - }), - StatusCode::BAD_REQUEST, - )); - } - warn!( "[{}] ACCEPTED TRANSACTION {:?}", db.config.name, new_transaction -- cgit v1.2.3-70-g09d2