diff options
author | Yigit Sever | 2021-04-22 20:15:40 +0300 |
---|---|---|
committer | Yigit Sever | 2021-04-25 23:04:39 +0300 |
commit | 32b49380880aab00057b8a663b5327d6f58def3a (patch) | |
tree | 059dc128813a93679233f5489cf1a42b2ea0374d /src | |
parent | 97b2f6c796fd2af1c338725884189685a82adc01 (diff) | |
download | gradecoin-32b49380880aab00057b8a663b5327d6f58def3a.tar.gz gradecoin-32b49380880aab00057b8a663b5327d6f58def3a.tar.bz2 gradecoin-32b49380880aab00057b8a663b5327d6f58def3a.zip |
Implement nicenet
- There are bot accounts that return what you sent them
- Sending a transaction generates some coin out of thin air
- No more one tx per person per block limit
- Unused transactions do not disappear anymore
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() |