diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/handlers.rs | 386 | ||||
| -rw-r--r-- | src/schema.rs | 58 |
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; | |||
| 4 | use blake2::{Blake2s, Digest}; | 4 | use blake2::{Blake2s, Digest}; |
| 5 | use block_modes::block_padding::Pkcs7; | 5 | use block_modes::block_padding::Pkcs7; |
| 6 | use block_modes::{BlockMode, Cbc}; | 6 | use block_modes::{BlockMode, Cbc}; |
| 7 | use chrono::Utc; | ||
| 7 | use jsonwebtoken::errors::ErrorKind; | 8 | use jsonwebtoken::errors::ErrorKind; |
| 8 | use jsonwebtoken::{decode, Algorithm, DecodingKey, TokenData, Validation}; | 9 | use jsonwebtoken::{decode, Algorithm, DecodingKey, TokenData, Validation}; |
| 9 | use log::{debug, warn}; | 10 | use log::{debug, warn}; |
| @@ -15,12 +16,21 @@ use sha2::Sha256; | |||
| 15 | use std::collections::{HashMap, HashSet}; | 16 | use std::collections::{HashMap, HashSet}; |
| 16 | use std::convert::Infallible; | 17 | use std::convert::Infallible; |
| 17 | use std::fs; | 18 | use std::fs; |
| 19 | use std::hash::Hash; | ||
| 18 | use warp::{http::StatusCode, reply}; | 20 | use warp::{http::StatusCode, reply}; |
| 19 | 21 | ||
| 20 | use crate::PRIVATE_KEY; | 22 | use crate::PRIVATE_KEY; |
| 21 | const BLOCK_TRANSACTION_COUNT: u8 = 1; | 23 | |
| 24 | // Valid blocks should have this many transactions | ||
| 25 | const BLOCK_TRANSACTION_COUNT: u8 = 5; | ||
| 26 | // Inital registration bonus | ||
| 27 | const REGISTER_BONUS: u16 = 40; | ||
| 28 | // Coinbase reward | ||
| 22 | const BLOCK_REWARD: u16 = 3; | 29 | const BLOCK_REWARD: u16 = 3; |
| 30 | // Transaction amount limit | ||
| 23 | const TX_UPPER_LIMIT: u16 = 2; | 31 | const TX_UPPER_LIMIT: u16 = 2; |
| 32 | // Transaction traffic reward | ||
| 33 | const TX_TRAFFIC_REWARD: u16 = 1; | ||
| 24 | 34 | ||
| 25 | // Encryption primitive | 35 | // Encryption primitive |
| 26 | type Aes128Cbc = Cbc<Aes128, Pkcs7>; | 36 | type 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 | ||
| 318 | pub async fn list_transactions(db: Db) -> Result<impl warp::Reply, Infallible> { | 365 | pub 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 | ||
| 852 | fn 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")] |
| 785 | struct UserTemplate<'a> { | 860 | struct UserTemplate<'a> { |
| @@ -789,6 +864,7 @@ struct UserTemplate<'a> { | |||
| 789 | struct DisplayUsers { | 864 | struct DisplayUsers { |
| 790 | fingerprint: String, | 865 | fingerprint: String, |
| 791 | balance: u16, | 866 | balance: u16, |
| 867 | is_bot: bool, | ||
| 792 | } | 868 | } |
| 793 | 869 | ||
| 794 | pub async fn user_list_handler(db: Db) -> Result<impl warp::Reply, warp::Rejection> { | 870 | pub 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 | |||
| 887 | fn has_unique_elements<T>(iter: T) -> bool | ||
| 888 | where | ||
| 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; | |||
| 20 | use std::string::String; | 20 | use std::string::String; |
| 21 | use std::sync::Arc; | 21 | use std::sync::Arc; |
| 22 | use std::vec::Vec; | 22 | use std::vec::Vec; |
| 23 | // use crate::validators; | ||
| 24 | 23 | ||
| 25 | pub type Fingerprint = String; | 24 | pub type Fingerprint = String; |
| 25 | pub type Id = String; | ||
| 26 | 26 | ||
| 27 | fn block_parser(path: String) -> u64 { | 27 | fn 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)] |
| 147 | pub struct Db { | 147 | pub 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)] |
| 184 | pub struct Transaction { | 221 | pub 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() |
