aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/handlers.rs386
-rw-r--r--src/schema.rs58
2 files changed, 286 insertions, 158 deletions
diff --git a/src/handlers.rs b/src/handlers.rs
index 7e022c3..e37cb40 100644
--- a/src/handlers.rs
+++ b/src/handlers.rs
@@ -4,6 +4,7 @@ use askama::Template;
4use blake2::{Blake2s, Digest}; 4use blake2::{Blake2s, Digest};
5use block_modes::block_padding::Pkcs7; 5use block_modes::block_padding::Pkcs7;
6use block_modes::{BlockMode, Cbc}; 6use block_modes::{BlockMode, Cbc};
7use chrono::Utc;
7use jsonwebtoken::errors::ErrorKind; 8use jsonwebtoken::errors::ErrorKind;
8use jsonwebtoken::{decode, Algorithm, DecodingKey, TokenData, Validation}; 9use jsonwebtoken::{decode, Algorithm, DecodingKey, TokenData, Validation};
9use log::{debug, warn}; 10use log::{debug, warn};
@@ -15,12 +16,21 @@ use sha2::Sha256;
15use std::collections::{HashMap, HashSet}; 16use std::collections::{HashMap, HashSet};
16use std::convert::Infallible; 17use std::convert::Infallible;
17use std::fs; 18use std::fs;
19use std::hash::Hash;
18use warp::{http::StatusCode, reply}; 20use warp::{http::StatusCode, reply};
19 21
20use crate::PRIVATE_KEY; 22use crate::PRIVATE_KEY;
21const BLOCK_TRANSACTION_COUNT: u8 = 1; 23
24// Valid blocks should have this many transactions
25const BLOCK_TRANSACTION_COUNT: u8 = 5;
26// Inital registration bonus
27const REGISTER_BONUS: u16 = 40;
28// Coinbase reward
22const BLOCK_REWARD: u16 = 3; 29const BLOCK_REWARD: u16 = 3;
30// Transaction amount limit
23const TX_UPPER_LIMIT: u16 = 2; 31const TX_UPPER_LIMIT: u16 = 2;
32// Transaction traffic reward
33const TX_TRAFFIC_REWARD: u16 = 1;
24 34
25// Encryption primitive 35// Encryption primitive
26type Aes128Cbc = Cbc<Aes128, Pkcs7>; 36type Aes128Cbc = Cbc<Aes128, Pkcs7>;
@@ -106,19 +116,20 @@ pub async fn authenticate_user(
106 116
107 let padding = PaddingScheme::new_oaep::<sha2::Sha256>(); 117 let padding = PaddingScheme::new_oaep::<sha2::Sha256>();
108 118
119 // Peel away the base64 layer from "key" field
109 let key_ciphertext = match base64::decode(&request.key) { 120 let key_ciphertext = match base64::decode(&request.key) {
110 Ok(c) => c, 121 Ok(c) => c,
111 Err(err) => { 122 Err(err) => {
112 debug!( 123 debug!(
113 "The ciphertext of the key was not base64 encoded {}, {}", 124 "\"key\" field of initial auth request was not base64 encoded: {}, {}",
114 &request.key, err 125 &request.key, err
115 ); 126 );
116 127
117 let res_json = warp::reply::json(&GradeCoinResponse { 128 let res_json = warp::reply::json(&GradeCoinResponse {
118 res: ResponseType::Error, 129 res: ResponseType::Error,
119 message: format!( 130 message: format!(
120 "The ciphertext of the key was not base64 encoded: {}", 131 "\"key\" field of initial auth request was not base64 encoded: {}, {}",
121 request.key 132 &request.key, err
122 ), 133 ),
123 }); 134 });
124 135
@@ -126,19 +137,39 @@ pub async fn authenticate_user(
126 } 137 }
127 }; 138 };
128 139
140 // Decrypt the "key" field using Gradecoin's private key
129 let temp_key = match gradecoin_private_key.decrypt(padding, &key_ciphertext) { 141 let temp_key = match gradecoin_private_key.decrypt(padding, &key_ciphertext) {
130 Ok(k) => k, 142 Ok(k) => k,
131 Err(err) => { 143 Err(err) => {
132 debug!( 144 debug!(
133 "Failed to decrypt ciphertext {:?}, {}", 145 "Failed to decrypt ciphertext of the key with Gradecoin's public key: {}. Key was {:?}",
134 &key_ciphertext, err 146 err, &key_ciphertext
147 );
148
149 let res_json = warp::reply::json(&GradeCoinResponse {
150 res: ResponseType::Error,
151 message: "Failed to decrypt the 'key_ciphertext' field of the auth request"
152 .to_owned(),
153 });
154
155 return Ok(warp::reply::with_status(res_json, StatusCode::BAD_REQUEST));
156 }
157 };
158
159 // Peel away the base64 from the iv field as well
160 let byte_iv = match base64::decode(&request.iv) {
161 Ok(iv) => iv,
162 Err(err) => {
163 debug!(
164 "\"iv\" field of initial auth request was not base64 encoded: {}, {}",
165 &request.iv, err
135 ); 166 );
136 167
137 let res_json = warp::reply::json(&GradeCoinResponse { 168 let res_json = warp::reply::json(&GradeCoinResponse {
138 res: ResponseType::Error, 169 res: ResponseType::Error,
139 message: format!( 170 message: format!(
140 "Failed to decrypt the ciphertext of the temporary key: {:?}", 171 "\"iv\" field of initial auth request was not base64 encoded: {}, {}",
141 &key_ciphertext 172 &request.iv, err
142 ), 173 ),
143 }); 174 });
144 175
@@ -146,8 +177,7 @@ pub async fn authenticate_user(
146 } 177 }
147 }; 178 };
148 179
149 let byte_iv = base64::decode(&request.iv).unwrap(); 180 // we have key and iv, time to decrypt the "c" field, first prepare the decryptor
150
151 let cipher = match Aes128Cbc::new_var(&temp_key, &byte_iv) { 181 let cipher = match Aes128Cbc::new_var(&temp_key, &byte_iv) {
152 Ok(c) => c, 182 Ok(c) => c,
153 Err(err) => { 183 Err(err) => {
@@ -165,42 +195,49 @@ pub async fn authenticate_user(
165 } 195 }
166 }; 196 };
167 197
198 // peel away the base64 from the auth packet
168 let auth_packet = match base64::decode(&request.c) { 199 let auth_packet = match base64::decode(&request.c) {
169 Ok(a) => a, 200 Ok(a) => a,
170
171 Err(err) => { 201 Err(err) => {
172 debug!( 202 debug!(
173 "The auth_packet (c field) did not base64 decode {} {}", 203 "\"c\" field of initial auth request was not base64 encoded: {}, {}",
174 &request.c, err 204 &request.c, err
175 ); 205 );
176 206
177 let res_json = warp::reply::json(&GradeCoinResponse { 207 let res_json = warp::reply::json(&GradeCoinResponse {
178 res: ResponseType::Error, 208 res: ResponseType::Error,
179 message: "The c field was not correctly base64 encoded".to_owned(), 209 message: format!(
210 "\"c\" field of initial auth request was not base64 encoded: {}, {}",
211 &request.c, err
212 ),
180 }); 213 });
181 214
182 return Ok(warp::reply::with_status(res_json, StatusCode::BAD_REQUEST)); 215 return Ok(warp::reply::with_status(res_json, StatusCode::BAD_REQUEST));
183 } 216 }
184 }; 217 };
185 218
219 // c field was properly base64 encoded, now available in auth_packet
220 // decryptor was setup properly, with the correct lenght key
186 let mut buf = auth_packet.to_vec(); 221 let mut buf = auth_packet.to_vec();
187 let auth_plaintext = match cipher.decrypt(&mut buf) { 222 let auth_plaintext = match cipher.decrypt(&mut buf) {
188 Ok(p) => p, 223 Ok(p) => p,
189 Err(err) => { 224 Err(err) => {
190 debug!( 225 println!(
191 "Base64 decoded auth request did not decrypt correctly {:?} {}", 226 "auth request (c) did not decrypt correctly {:?} {}",
192 &auth_packet, err 227 &buf, err
193 ); 228 );
194 229
195 let res_json = warp::reply::json(&GradeCoinResponse { 230 let res_json = warp::reply::json(&GradeCoinResponse {
196 res: ResponseType::Error, 231 res: ResponseType::Error,
197 message: "The base64 decoded auth request did not decrypt correctly".to_owned(), 232 message: "Failed to decrypt the 'c' field of the auth request, 'iv' and 'k_temp' were valid so far though"
233 .to_owned(),
198 }); 234 });
199 235
200 return Ok(warp::reply::with_status(res_json, StatusCode::BAD_REQUEST)); 236 return Ok(warp::reply::with_status(res_json, StatusCode::BAD_REQUEST));
201 } 237 }
202 }; 238 };
203 239
240 // we have a decrypted c field, create a string from the bytes mess
204 let utf8_auth_plaintext = match String::from_utf8(auth_plaintext.to_vec()) { 241 let utf8_auth_plaintext = match String::from_utf8(auth_plaintext.to_vec()) {
205 Ok(text) => text, 242 Ok(text) => text,
206 Err(err) => { 243 Err(err) => {
@@ -211,13 +248,15 @@ pub async fn authenticate_user(
211 248
212 let res_json = warp::reply::json(&GradeCoinResponse { 249 let res_json = warp::reply::json(&GradeCoinResponse {
213 res: ResponseType::Error, 250 res: ResponseType::Error,
214 message: "Auth plaintext could not get converted to UTF-8".to_owned(), 251 message: "P_AR couldn't get converted to UTF-8, please check your encoding"
252 .to_owned(),
215 }); 253 });
216 254
217 return Ok(warp::reply::with_status(res_json, StatusCode::BAD_REQUEST)); 255 return Ok(warp::reply::with_status(res_json, StatusCode::BAD_REQUEST));
218 } 256 }
219 }; 257 };
220 258
259 // finally create an AuthRequest object from the plaintext
221 let request: AuthRequest = match serde_json::from_str(&utf8_auth_plaintext) { 260 let request: AuthRequest = match serde_json::from_str(&utf8_auth_plaintext) {
222 Ok(req) => req, 261 Ok(req) => req,
223 Err(err) => { 262 Err(err) => {
@@ -228,24 +267,32 @@ pub async fn authenticate_user(
228 267
229 let res_json = warp::reply::json(&GradeCoinResponse { 268 let res_json = warp::reply::json(&GradeCoinResponse {
230 res: ResponseType::Error, 269 res: ResponseType::Error,
231 message: "The auth request JSON did not serialize correctly".to_owned(), 270 message: "The P_AR JSON did not serialize correctly, did it include all 3 fields 'student_id', 'passwd' and 'public_key'?".to_owned(),
232 }); 271 });
233 272
234 return Ok(warp::reply::with_status(res_json, StatusCode::BAD_REQUEST)); 273 return Ok(warp::reply::with_status(res_json, StatusCode::BAD_REQUEST));
235 } 274 }
236 }; 275 };
237 276
238 let privileged_student_id = match MetuId::new(request.student_id, request.passwd) { 277 // is the student in AuthRequest privileged?
239 Some(id) => id, 278 let privileged_student_id =
240 None => { 279 match MetuId::new(request.student_id.clone(), request.passwd.clone()) {
241 let res_json = warp::reply::json(&GradeCoinResponse { 280 Some(id) => id,
281 None => {
282 debug!(
283 "Someone tried to auth with invalid credentials: {} {}",
284 &request.student_id, &request.passwd
285 );
286 let res_json = warp::reply::json(&GradeCoinResponse {
242 res: ResponseType::Error, 287 res: ResponseType::Error,
243 message: "This user cannot have a gradecoin account".to_owned(), 288 message:
289 "The credentials given ('student_id', 'passwd') cannot hold a Gradecoin account"
290 .to_owned(),
244 }); 291 });
245 292
246 return Ok(warp::reply::with_status(res_json, StatusCode::BAD_REQUEST)); 293 return Ok(warp::reply::with_status(res_json, StatusCode::BAD_REQUEST));
247 } 294 }
248 }; 295 };
249 296
250 // Students should be able to authenticate once 297 // Students should be able to authenticate once
251 { 298 {
@@ -264,12 +311,11 @@ pub async fn authenticate_user(
264 } 311 }
265 } 312 }
266 313
267 // We're using this as the validator 314 // We're using this as the validator instead of anything reasonable
268 // I hate myself
269 if DecodingKey::from_rsa_pem(request.public_key.as_bytes()).is_err() { 315 if DecodingKey::from_rsa_pem(request.public_key.as_bytes()).is_err() {
270 let res_json = warp::reply::json(&GradeCoinResponse { 316 let res_json = warp::reply::json(&GradeCoinResponse {
271 res: ResponseType::Error, 317 res: ResponseType::Error,
272 message: "The supplied RSA public key is not in valid PEM format".to_owned(), 318 message: "The RSA 'public_key' in 'P_AR' is not in valid PEM format".to_owned(),
273 }); 319 });
274 320
275 return Ok(warp::reply::with_status(res_json, StatusCode::BAD_REQUEST)); 321 return Ok(warp::reply::with_status(res_json, StatusCode::BAD_REQUEST));
@@ -280,25 +326,27 @@ pub async fn authenticate_user(
280 let new_user = User { 326 let new_user = User {
281 user_id: privileged_student_id, 327 user_id: privileged_student_id,
282 public_key: request.public_key, 328 public_key: request.public_key,
283 balance: 0, 329 balance: REGISTER_BONUS,
330 is_bot: false,
284 }; 331 };
285 332
286 debug!("New user authenticated themselves! {:?}", &new_user); 333 debug!("NEW USER: {:?}", &new_user);
287 334
335 // save the user to disk
288 let user_at_rest_json = serde_json::to_string(&UserAtRest { 336 let user_at_rest_json = serde_json::to_string(&UserAtRest {
337 fingerprint: fingerprint.clone(),
289 user: User { 338 user: User {
290 user_id: new_user.user_id.clone(), 339 user_id: new_user.user_id.clone(),
291 public_key: new_user.public_key.clone(), 340 public_key: new_user.public_key.clone(),
292 balance: 0, 341 balance: new_user.balance,
342 is_bot: false,
293 }, 343 },
294 fingerprint: fingerprint.clone(),
295 }) 344 })
296 .unwrap(); 345 .unwrap();
297 346
298 fs::write(format!("users/{}.guy", new_user.user_id), user_at_rest_json).unwrap(); 347 fs::write(format!("users/{}.guy", new_user.user_id), user_at_rest_json).unwrap();
299 348
300 let mut userlist = db.users.write(); 349 let mut userlist = db.users.write();
301
302 userlist.insert(fingerprint.clone(), new_user); 350 userlist.insert(fingerprint.clone(), new_user);
303 351
304 let res_json = warp::reply::json(&GradeCoinResponse { 352 let res_json = warp::reply::json(&GradeCoinResponse {
@@ -314,9 +362,7 @@ pub async fn authenticate_user(
314 362
315/// GET /transaction 363/// GET /transaction
316/// Returns JSON array of transactions 364/// Returns JSON array of transactions
317/// Cannot fail
318pub async fn list_transactions(db: Db) -> Result<impl warp::Reply, Infallible> { 365pub async fn list_transactions(db: Db) -> Result<impl warp::Reply, Infallible> {
319 debug!("GET /transaction, list_transactions() is handling");
320 let mut result = HashMap::new(); 366 let mut result = HashMap::new();
321 367
322 let transactions = db.pending_transactions.read(); 368 let transactions = db.pending_transactions.read();
@@ -342,12 +388,9 @@ pub async fn propose_block(
342 token: String, 388 token: String,
343 db: Db, 389 db: Db,
344) -> Result<impl warp::Reply, warp::Rejection> { 390) -> Result<impl warp::Reply, warp::Rejection> {
345 debug!("POST /block, propose_block() is handling");
346
347 let users_store = db.users.upgradable_read();
348
349 warn!("New block proposal: {:?}", &new_block); 391 warn!("New block proposal: {:?}", &new_block);
350 392
393 // Check if there are enough transactions in the block
351 if new_block.transaction_list.len() < BLOCK_TRANSACTION_COUNT as usize { 394 if new_block.transaction_list.len() < BLOCK_TRANSACTION_COUNT as usize {
352 debug!( 395 debug!(
353 "{} transactions offered, needed {}", 396 "{} transactions offered, needed {}",
@@ -366,7 +409,29 @@ pub async fn propose_block(
366 } 409 }
367 410
368 // proposer (first transaction fingerprint) checks 411 // proposer (first transaction fingerprint) checks
369 let internal_user = match users_store.get(&new_block.transaction_list[0]) { 412 let pending_transactions = db.pending_transactions.upgradable_read();
413
414 let internal_user_fingerprint = match pending_transactions.get(&new_block.transaction_list[0]) {
415 Some(coinbase) => &coinbase.source,
416 None => {
417 debug!(
418 "Block proposer with public key signature {:?} is not found in the database",
419 new_block.transaction_list[0]
420 );
421
422 let res_json = warp::reply::json(&GradeCoinResponse {
423 res: ResponseType::Error,
424 message: "Proposer of the first transaction is not found in the system".to_owned(),
425 });
426
427 return Ok(warp::reply::with_status(res_json, StatusCode::BAD_REQUEST));
428 }
429 };
430
431 let users_store = db.users.upgradable_read();
432
433 // this probably cannot fail, if the transaction is valid then it must've been checked already
434 let internal_user = match users_store.get(internal_user_fingerprint) {
370 Some(existing_user) => existing_user, 435 Some(existing_user) => existing_user,
371 None => { 436 None => {
372 debug!( 437 debug!(
@@ -390,7 +455,7 @@ pub async fn propose_block(
390 let token_payload = match authorize_proposer(token, &proposer_public_key) { 455 let token_payload = match authorize_proposer(token, &proposer_public_key) {
391 Ok(data) => data, 456 Ok(data) => data,
392 Err(below) => { 457 Err(below) => {
393 debug!("Something went wrong below {:?}", below); 458 debug!("Something went wrong with the JWT {:?}", below);
394 459
395 let res_json = warp::reply::json(&GradeCoinResponse { 460 let res_json = warp::reply::json(&GradeCoinResponse {
396 res: ResponseType::Error, 461 res: ResponseType::Error,
@@ -409,49 +474,36 @@ pub async fn propose_block(
409 ); 474 );
410 let res_json = warp::reply::json(&GradeCoinResponse { 475 let res_json = warp::reply::json(&GradeCoinResponse {
411 res: ResponseType::Error, 476 res: ResponseType::Error,
412 message: "The hash of the block did not match the hash given in JWT".to_owned(), 477 message: "The hash of the block did not match the hash given in JWT tha field"
478 .to_owned(),
413 }); 479 });
414 480
415 return Ok(warp::reply::with_status(res_json, StatusCode::BAD_REQUEST)); 481 return Ok(warp::reply::with_status(res_json, StatusCode::BAD_REQUEST));
416 } 482 }
417 483
418 // scope the HashSet 484 if !has_unique_elements(&new_block.transaction_list) {
419 { 485 debug!("Block contains duplicate transactions!");
420 let mut proposed_transactions = HashSet::new(); 486 let res_json = warp::reply::json(&GradeCoinResponse {
421 for tx in new_block.transaction_list.iter() { 487 res: ResponseType::Error,
422 proposed_transactions.insert(tx); 488 message: "Block cannot contain duplicate transactions".to_owned(),
423 } 489 });
424 490
425 if proposed_transactions.len() < BLOCK_TRANSACTION_COUNT as usize { 491 return Ok(warp::reply::with_status(res_json, StatusCode::BAD_REQUEST));
492 }
493
494 // Are transactions in the block valid?
495 for transaction_hash in new_block.transaction_list.iter() {
496 if !pending_transactions.contains_key(transaction_hash) {
426 let res_json = warp::reply::json(&GradeCoinResponse { 497 let res_json = warp::reply::json(&GradeCoinResponse {
427 res: ResponseType::Error, 498 res: ResponseType::Error,
428 message: format!( 499 message: "Block contains an unknown transaction".to_owned(),
429 "Block cannot contain less than {} unique transaction(s).",
430 BLOCK_TRANSACTION_COUNT
431 ),
432 }); 500 });
433 501
434 return Ok(warp::reply::with_status(res_json, StatusCode::BAD_REQUEST)); 502 return Ok(warp::reply::with_status(res_json, StatusCode::BAD_REQUEST));
435 } 503 }
436 } 504 }
437 505
438 // Scope the RwLocks, there are hashing stuff below 506 // hash the block ourselves to double check
439 {
440 let pending_transactions = db.pending_transactions.read();
441
442 // Are transactions in the block valid?
443 for transaction_hash in new_block.transaction_list.iter() {
444 if !pending_transactions.contains_key(transaction_hash) {
445 let res_json = warp::reply::json(&GradeCoinResponse {
446 res: ResponseType::Error,
447 message: "Block contains unknown transaction".to_owned(),
448 });
449
450 return Ok(warp::reply::with_status(res_json, StatusCode::BAD_REQUEST));
451 }
452 }
453 }
454
455 let naked_block = NakedBlock { 507 let naked_block = NakedBlock {
456 transaction_list: new_block.transaction_list.clone(), 508 transaction_list: new_block.transaction_list.clone(),
457 nonce: new_block.nonce, 509 nonce: new_block.nonce,
@@ -490,15 +542,14 @@ pub async fn propose_block(
490 // All clear, block accepted! 542 // All clear, block accepted!
491 warn!("ACCEPTED BLOCK {:?}", new_block); 543 warn!("ACCEPTED BLOCK {:?}", new_block);
492 544
493 // Scope the pending_transactions 545 // Scope the read guards
494 { 546 {
495 let pending_transactions = db.pending_transactions.read(); 547 let mut pending_transactions = RwLockUpgradableReadGuard::upgrade(pending_transactions);
496 let mut users_store = RwLockUpgradableReadGuard::upgrade(users_store); 548 let mut users_store = RwLockUpgradableReadGuard::upgrade(users_store);
497 549
498 let coinbase_fingerprint = new_block.transaction_list.get(0).unwrap(); 550 // Play out the transactions
499
500 for fingerprint in new_block.transaction_list.iter() { 551 for fingerprint in new_block.transaction_list.iter() {
501 if let Some(transaction) = pending_transactions.get(fingerprint) { 552 if let Some(transaction) = pending_transactions.remove(fingerprint) {
502 let source = &transaction.source; 553 let source = &transaction.source;
503 let target = &transaction.target; 554 let target = &transaction.target;
504 555
@@ -507,21 +558,34 @@ pub async fn propose_block(
507 } 558 }
508 559
509 if let Some(to) = users_store.get_mut(target) { 560 if let Some(to) = users_store.get_mut(target) {
510 to.balance += transaction.amount; 561 to.balance += transaction.amount + TX_TRAFFIC_REWARD;
562 }
563
564 // if the receiver is a bot, they will reciprocate
565 if users_store.get(target).unwrap().is_bot {
566 let transaction_id =
567 calculate_transaction_id(&transaction.target, &transaction.source);
568 pending_transactions.insert(
569 transaction_id,
570 Transaction {
571 source: target.to_owned(),
572 target: source.to_owned(),
573 amount: transaction.amount,
574 timestamp: Utc::now().naive_local(),
575 },
576 );
511 } 577 }
512 } 578 }
513 } 579 }
514 580
581 // Reward the block proposer
582 let coinbase_fingerprint = new_block.transaction_list.get(0).unwrap();
583
515 if let Some(coinbase_user) = users_store.get_mut(coinbase_fingerprint) { 584 if let Some(coinbase_user) = users_store.get_mut(coinbase_fingerprint) {
516 coinbase_user.balance += BLOCK_REWARD; 585 coinbase_user.balance += BLOCK_REWARD;
517 } 586 }
518 } 587 }
519 588
520 {
521 let mut pending_transactions = db.pending_transactions.write();
522 pending_transactions.clear();
523 }
524
525 let block_json = serde_json::to_string(&new_block).unwrap(); 589 let block_json = serde_json::to_string(&new_block).unwrap();
526 590
527 fs::write( 591 fs::write(
@@ -538,7 +602,7 @@ pub async fn propose_block(
538 Ok(warp::reply::with_status( 602 Ok(warp::reply::with_status(
539 warp::reply::json(&GradeCoinResponse { 603 warp::reply::json(&GradeCoinResponse {
540 res: ResponseType::Success, 604 res: ResponseType::Success,
541 message: "Block accepted coinbase reward awarded".to_owned(), 605 message: "Block accepted, coinbase reward awarded".to_owned(),
542 }), 606 }),
543 StatusCode::CREATED, 607 StatusCode::CREATED,
544 )) 608 ))
@@ -558,19 +622,17 @@ pub async fn propose_transaction(
558 token: String, 622 token: String,
559 db: Db, 623 db: Db,
560) -> Result<impl warp::Reply, warp::Rejection> { 624) -> Result<impl warp::Reply, warp::Rejection> {
561 debug!("POST /transaction, propose_transaction() is handling");
562
563 warn!("New transaction proposal: {:?}", &new_transaction); 625 warn!("New transaction proposal: {:?}", &new_transaction);
564 626
565 let users_store = db.users.read(); 627 let users_store = db.users.read();
566 628
567 // Is this transaction from an authorized source? 629 // Is this transaction from an authorized source?
568 let internal_user = match users_store.get(&new_transaction.by) { 630 let internal_user = match users_store.get(&new_transaction.source) {
569 Some(existing_user) => existing_user, 631 Some(existing_user) => existing_user,
570 None => { 632 None => {
571 debug!( 633 debug!(
572 "User with public key signature {:?} is not found in the database", 634 "User with public key signature {:?} is not found in the database",
573 new_transaction.by 635 new_transaction.source
574 ); 636 );
575 637
576 return Ok(warp::reply::with_status( 638 return Ok(warp::reply::with_status(
@@ -586,105 +648,112 @@ pub async fn propose_transaction(
586 648
587 // `internal_user` is an authenticated student, can propose 649 // `internal_user` is an authenticated student, can propose
588 650
589 // Does this user have a pending transaction? 651 // This public key was already written to the database, we can panic if it's not valid at
590 { 652 // *this* point
591 let transactions = db.pending_transactions.read(); 653 let proposer_public_key = &internal_user.public_key;
592 if transactions.contains_key(&*new_transaction.by.to_owned()) { 654
593 debug!("{:?} already has a pending transaction", new_transaction.by); 655 let token_payload = match authorize_proposer(token, &proposer_public_key) {
656 Ok(data) => data,
657 Err(below) => {
658 debug!("JWT Error: {:?}", below);
594 return Ok(warp::reply::with_status( 659 return Ok(warp::reply::with_status(
595 warp::reply::json(&GradeCoinResponse { 660 warp::reply::json(&GradeCoinResponse {
596 res: ResponseType::Error, 661 res: ResponseType::Error,
597 message: "This user already has another pending transaction".to_owned(), 662 message: below,
598 }), 663 }),
599 StatusCode::BAD_REQUEST, 664 StatusCode::BAD_REQUEST,
600 )); 665 ));
601 } 666 }
602 } 667 };
603 668
604 // Is transaction amount within bounds 669 // is the target of the transaction in the system?
605 if new_transaction.amount > TX_UPPER_LIMIT { 670 if !users_store.contains_key(&new_transaction.target) {
606 debug!( 671 debug!(
607 "Transaction amount cannot exceed {}, was {}", 672 "Target of the transaction is not in the system {}",
608 TX_UPPER_LIMIT, new_transaction.amount 673 new_transaction.target
609 ); 674 );
675
610 return Ok(warp::reply::with_status( 676 return Ok(warp::reply::with_status(
611 warp::reply::json(&GradeCoinResponse { 677 warp::reply::json(&GradeCoinResponse {
612 res: ResponseType::Error, 678 res: ResponseType::Error,
613 message: format!("Transaction amount cannot exceed {}", TX_UPPER_LIMIT), 679 message: format!(
680 "Target of the transaction {} is not found in the system",
681 new_transaction.target
682 ),
614 }), 683 }),
615 StatusCode::BAD_REQUEST, 684 StatusCode::BAD_REQUEST,
616 )); 685 ));
617 } 686 }
618 687
619 if new_transaction.by == new_transaction.source { 688 let transaction_id = calculate_transaction_id(&new_transaction.source, &new_transaction.target);
620 // check if user can afford the transaction
621 if internal_user.balance < new_transaction.amount {
622 debug!(
623 "User does not have enough balance ({}) for this TX {}",
624 internal_user.balance, new_transaction.amount
625 );
626 return Ok(warp::reply::with_status(
627 warp::reply::json(&GradeCoinResponse {
628 res: ResponseType::Error,
629 message:
630 "User does not have enough balance in their account for this transaction"
631 .to_owned(),
632 }),
633 StatusCode::BAD_REQUEST,
634 ));
635 }
636 } else if new_transaction.by == new_transaction.target {
637 // Only transactions FROM bank could appear here
638 689
639 if new_transaction.source 690 // OLD: Does this user have a pending transaction?
640 != "31415926535897932384626433832795028841971693993751058209749445923" 691 // NEW: Is this source:target pair unqiue?
641 { 692 {
693 let transactions = db.pending_transactions.read();
694 debug!(
695 "This is a transaction from {} to {}",
696 new_transaction.source, new_transaction.target,
697 );
698
699 if transactions.contains_key(&transaction_id) {
642 debug!( 700 debug!(
643 "Extortion attempt - between {} and {}", 701 "this source/target combination {} already has a pending transaction",
644 new_transaction.source, new_transaction.target 702 transaction_id
645 ); 703 );
704
646 return Ok(warp::reply::with_status( 705 return Ok(warp::reply::with_status(
647 warp::reply::json(&GradeCoinResponse { 706 warp::reply::json(&GradeCoinResponse {
648 res: ResponseType::Error, 707 res: ResponseType::Error,
649 message: "Transactions cannot extort Gradecoin from unsuspecting users" 708 message: "This user already has another pending transaction".to_owned(),
650 .to_owned(),
651 }), 709 }),
652 StatusCode::BAD_REQUEST, 710 StatusCode::BAD_REQUEST,
653 )); 711 ));
654 } 712 }
655 } else { 713 }
714
715 if new_transaction.source == new_transaction.target {
716 debug!("transaction source and target are the same",);
717
718 return Ok(warp::reply::with_status(
719 warp::reply::json(&GradeCoinResponse {
720 res: ResponseType::Error,
721 message: "transaction to yourself, you had to try didn't you? :)".to_owned(),
722 }),
723 StatusCode::BAD_REQUEST,
724 ));
725 }
726
727 // Is transaction amount within bounds
728 if new_transaction.amount > TX_UPPER_LIMIT {
656 debug!( 729 debug!(
657 "Attempt to transact between two unrelated parties - {} and {}", 730 "Transaction amount cannot exceed {}, was {}",
658 new_transaction.source, new_transaction.target 731 TX_UPPER_LIMIT, new_transaction.amount
659 ); 732 );
660 return Ok(warp::reply::with_status( 733 return Ok(warp::reply::with_status(
661 warp::reply::json(&GradeCoinResponse { 734 warp::reply::json(&GradeCoinResponse {
662 res: ResponseType::Error, 735 res: ResponseType::Error,
663 message: "Transactions cannot be proposed on behalf of someone else".to_owned(), 736 message: format!("Transaction amount cannot exceed {}", TX_UPPER_LIMIT),
664 }), 737 }),
665 StatusCode::BAD_REQUEST, 738 StatusCode::BAD_REQUEST,
666 )); 739 ));
667 } 740 }
668 741
669 // This public key was already written to the database, we can panic if it's not valid at 742 // check if user can afford the transaction
670 // *this* point 743 if internal_user.balance < new_transaction.amount {
671 let proposer_public_key = &internal_user.public_key; 744 debug!(
672 745 "User does not have enough balance ({}) for this TX {}",
673 let token_payload = match authorize_proposer(token, &proposer_public_key) { 746 internal_user.balance, new_transaction.amount
674 Ok(data) => data, 747 );
675 Err(below) => { 748 return Ok(warp::reply::with_status(
676 debug!("Something went wrong at JWT {:?}", below); 749 warp::reply::json(&GradeCoinResponse {
677 return Ok(warp::reply::with_status( 750 res: ResponseType::Error,
678 warp::reply::json(&GradeCoinResponse { 751 message: "User does not have enough balance in their account for this transaction"
679 res: ResponseType::Error, 752 .to_owned(),
680 message: below, 753 }),
681 }), 754 StatusCode::BAD_REQUEST,
682 StatusCode::BAD_REQUEST, 755 ));
683 )); 756 }
684 }
685 };
686
687 // authorized for transaction proposal
688 757
689 // this transaction was already checked for correctness at custom_filters, we can panic here if 758 // this transaction was already checked for correctness at custom_filters, we can panic here if
690 // it has been changed since 759 // it has been changed since
@@ -709,7 +778,7 @@ pub async fn propose_transaction(
709 778
710 let mut transactions = db.pending_transactions.write(); 779 let mut transactions = db.pending_transactions.write();
711 780
712 transactions.insert(new_transaction.by.to_owned(), new_transaction); 781 transactions.insert(transaction_id, new_transaction);
713 782
714 Ok(warp::reply::with_status( 783 Ok(warp::reply::with_status(
715 warp::reply::json(&GradeCoinResponse { 784 warp::reply::json(&GradeCoinResponse {
@@ -780,6 +849,12 @@ fn authorize_proposer(jwt_token: String, user_pem: &str) -> Result<TokenData<Cla
780 Ok(token_payload) 849 Ok(token_payload)
781} 850}
782 851
852fn calculate_transaction_id(source: &str, target: &str) -> String {
853 let long_fingerprint = format!("{}{}", source, target);
854 let id = format!("{:x}", Sha256::digest(long_fingerprint.as_bytes()));
855 id
856}
857
783#[derive(Template)] 858#[derive(Template)]
784#[template(path = "list.html")] 859#[template(path = "list.html")]
785struct UserTemplate<'a> { 860struct UserTemplate<'a> {
@@ -789,6 +864,7 @@ struct UserTemplate<'a> {
789struct DisplayUsers { 864struct DisplayUsers {
790 fingerprint: String, 865 fingerprint: String,
791 balance: u16, 866 balance: u16,
867 is_bot: bool,
792} 868}
793 869
794pub async fn user_list_handler(db: Db) -> Result<impl warp::Reply, warp::Rejection> { 870pub async fn user_list_handler(db: Db) -> Result<impl warp::Reply, warp::Rejection> {
@@ -799,6 +875,7 @@ pub async fn user_list_handler(db: Db) -> Result<impl warp::Reply, warp::Rejecti
799 sane_users.push(DisplayUsers { 875 sane_users.push(DisplayUsers {
800 fingerprint: fingerprint.to_owned(), 876 fingerprint: fingerprint.to_owned(),
801 balance: user.balance, 877 balance: user.balance,
878 is_bot: user.is_bot,
802 }); 879 });
803 } 880 }
804 881
@@ -806,3 +883,12 @@ pub async fn user_list_handler(db: Db) -> Result<impl warp::Reply, warp::Rejecti
806 let res = template.render().unwrap(); 883 let res = template.render().unwrap();
807 Ok(warp::reply::html(res)) 884 Ok(warp::reply::html(res))
808} 885}
886
887fn has_unique_elements<T>(iter: T) -> bool
888where
889 T: IntoIterator,
890 T::Item: Eq + Hash,
891{
892 let mut uniq = HashSet::new();
893 iter.into_iter().all(move |x| uniq.insert(x))
894}
diff --git a/src/schema.rs b/src/schema.rs
index 19b7fd8..537e0a5 100644
--- a/src/schema.rs
+++ b/src/schema.rs
@@ -20,9 +20,9 @@ use std::path::PathBuf;
20use std::string::String; 20use std::string::String;
21use std::sync::Arc; 21use std::sync::Arc;
22use std::vec::Vec; 22use std::vec::Vec;
23// use crate::validators;
24 23
25pub type Fingerprint = String; 24pub type Fingerprint = String;
25pub type Id = String;
26 26
27fn block_parser(path: String) -> u64 { 27fn block_parser(path: String) -> u64 {
28 let end_pos = path.find(".block").unwrap(); 28 let end_pos = path.find(".block").unwrap();
@@ -146,7 +146,7 @@ pub struct Claims {
146#[derive(Debug, Clone)] 146#[derive(Debug, Clone)]
147pub struct Db { 147pub struct Db {
148 pub blockchain: Arc<RwLock<Block>>, 148 pub blockchain: Arc<RwLock<Block>>,
149 pub pending_transactions: Arc<RwLock<HashMap<Fingerprint, Transaction>>>, 149 pub pending_transactions: Arc<RwLock<HashMap<Id, Transaction>>>,
150 pub users: Arc<RwLock<HashMap<Fingerprint, User>>>, 150 pub users: Arc<RwLock<HashMap<Fingerprint, User>>>,
151} 151}
152 152
@@ -154,14 +154,51 @@ impl Db {
154 pub fn new() -> Self { 154 pub fn new() -> Self {
155 let mut users: HashMap<Fingerprint, User> = HashMap::new(); 155 let mut users: HashMap<Fingerprint, User> = HashMap::new();
156 156
157 let bank_acc = MetuId::new("bank".to_owned(), "P7oxDm30g1jeIId".to_owned()).unwrap(); 157 let friendly_1 = MetuId::new("friend_1".to_owned(), "not_used".to_owned()).unwrap();
158 158
159 users.insert( 159 users.insert(
160 "31415926535897932384626433832795028841971693993751058209749445923".to_owned(), 160 "cde48537ca2c28084ff560826d0e6388b7c57a51497a6cb56f397289e52ff41b".to_owned(),
161 User { 161 User {
162 user_id: bank_acc, 162 user_id: friendly_1,
163 public_key: "null".to_owned(), 163 public_key: "not_used".to_owned(),
164 balance: 27 * 80, 164 balance: 0,
165 is_bot: true,
166 },
167 );
168
169 let friendly_2 = MetuId::new("friend_2".to_owned(), "not_used".to_owned()).unwrap();
170
171 users.insert(
172 "a1a38b5bae5866d7d998a9834229ec2f9db7a4fc8fb6f58b1115a96a446875ff".to_owned(),
173 User {
174 user_id: friendly_2,
175 public_key: "not_used".to_owned(),
176 balance: 0,
177 is_bot: true,
178 },
179 );
180
181 let friendly_3 = MetuId::new("friend_4".to_owned(), "not_used".to_owned()).unwrap();
182
183 users.insert(
184 "4e048fd2a62f1307866086e803e9be43f78a702d5df10831fbf434e7663ae0e7".to_owned(),
185 User {
186 user_id: friendly_3,
187 public_key: "not_used".to_owned(),
188 balance: 0,
189 is_bot: true,
190 },
191 );
192
193 let friendly_4 = MetuId::new("friend_4".to_owned(), "not_used".to_owned()).unwrap();
194
195 users.insert(
196 "60e77101e76950a9b1830fa107fd2f8fc545255b3e0f14b6a7797cf9ee005f07".to_owned(),
197 User {
198 user_id: friendly_4,
199 public_key: "not_used".to_owned(),
200 balance: 0,
201 is_bot: true,
165 }, 202 },
166 ); 203 );
167 204
@@ -182,7 +219,6 @@ impl Default for Db {
182/// A transaction between `source` and `target` that moves `amount` 219/// A transaction between `source` and `target` that moves `amount`
183#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] 220#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
184pub struct Transaction { 221pub struct Transaction {
185 pub by: Fingerprint,
186 pub source: Fingerprint, 222 pub source: Fingerprint,
187 pub target: Fingerprint, 223 pub target: Fingerprint,
188 pub amount: u16, 224 pub amount: u16,
@@ -244,6 +280,8 @@ pub struct User {
244 pub user_id: MetuId, 280 pub user_id: MetuId,
245 pub public_key: String, 281 pub public_key: String,
246 pub balance: u16, 282 pub balance: u16,
283 #[serde(skip, default = "bool::default")]
284 pub is_bot: bool,
247} 285}
248 286
249/// The values are hard coded in [`OUR_STUDENTS`] so MetuId::new() can accept/reject values based on that 287/// The values are hard coded in [`OUR_STUDENTS`] so MetuId::new() can accept/reject values based on that
@@ -308,6 +346,10 @@ lazy_static! {
308 ("e223715", "1H5QuOYI1b2r9ET"), 346 ("e223715", "1H5QuOYI1b2r9ET"),
309 ("e181932", "THANKYOUHAVEFUN"), 347 ("e181932", "THANKYOUHAVEFUN"),
310 ("bank", "P7oxDm30g1jeIId"), 348 ("bank", "P7oxDm30g1jeIId"),
349 ("friend_1", "not_used"),
350 ("friend_2", "not_used"),
351 ("friend_3", "not_used"),
352 ("friend_4", "not_used"),
311 ] 353 ]
312 .iter() 354 .iter()
313 .cloned() 355 .cloned()