aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/bin/main.rs (renamed from src/main.rs)9
-rw-r--r--src/custom_filters.rs18
-rw-r--r--src/handlers.rs128
-rw-r--r--src/lib.rs34
-rw-r--r--src/routes.rs380
-rw-r--r--src/schema.rs68
6 files changed, 157 insertions, 480 deletions
diff --git a/src/main.rs b/src/bin/main.rs
index 5683aea..8d88286 100644
--- a/src/main.rs
+++ b/src/bin/main.rs
@@ -1,10 +1,9 @@
1use gradecoin::schema::create_database;
2use std::env; 1use std::env;
3use warp::Filter; 2use warp::Filter;
4 3
5mod custom_filters; 4use gradecoin::routes::consensus_routes;
6mod handlers; 5use gradecoin::schema::create_database;
7mod routes; 6
8// mod validators; 7// mod validators;
9 8
10#[tokio::main] 9#[tokio::main]
@@ -17,7 +16,7 @@ async fn main() {
17 16
18 let db = create_database(); 17 let db = create_database();
19 18
20 let api = routes::consensus_routes(db); 19 let api = consensus_routes(db);
21 20
22 let routes = api.with(warp::log("gradecoin")); 21 let routes = api.with(warp::log("gradecoin"));
23 22
diff --git a/src/custom_filters.rs b/src/custom_filters.rs
index f93f572..dfdae04 100644
--- a/src/custom_filters.rs
+++ b/src/custom_filters.rs
@@ -1,30 +1,38 @@
1use gradecoin::schema::{AuthRequest, Block, Db, Transaction}; 1/// Functions that extracts Structs to be used in warp routines
2use crate::schema::{AuthRequest, Block, Db, Transaction};
2use std::convert::Infallible; 3use std::convert::Infallible;
3use warp::{Filter, Rejection}; 4use warp::{Filter, Rejection};
4 5
5// Database context for routes 6/// Wraps the database to be used in warp routes
6pub fn with_db(db: Db) -> impl Filter<Extract = (Db,), Error = Infallible> + Clone { 7pub fn with_db(db: Db) -> impl Filter<Extract = (Db,), Error = Infallible> + Clone {
7 warp::any().map(move || db.clone()) 8 warp::any().map(move || db.clone())
8} 9}
9 10
10// Accept only json encoded User body and reject big payloads 11/// Extracts an `AuthRequest` JSON body from the request
12/// Accepts only JSON encoded `AuthRequest` body and rejects big payloads
13///
11// TODO: find a good limit for this, (=e2482057; 8 char String + rsa pem) <11-04-21, yigit> // 14// TODO: find a good limit for this, (=e2482057; 8 char String + rsa pem) <11-04-21, yigit> //
12pub fn auth_request_json_body() -> impl Filter<Extract = (AuthRequest,), Error = Rejection> + Clone 15pub fn auth_request_json_body() -> impl Filter<Extract = (AuthRequest,), Error = Rejection> + Clone
13{ 16{
14 warp::body::content_length_limit(1024 * 32).and(warp::body::json()) 17 warp::body::content_length_limit(1024 * 32).and(warp::body::json())
15} 18}
16 19
17// Accept only json encoded Transaction body and reject big payloads 20/// Extracts an `Transaction` JSON body from the request
21/// Accepts only JSON encoded `Transaction` body and rejects big payloads
18// TODO: find a good limit for this <11-04-21, yigit> // 22// TODO: find a good limit for this <11-04-21, yigit> //
19pub fn transaction_json_body() -> impl Filter<Extract = (Transaction,), Error = Rejection> + Clone { 23pub fn transaction_json_body() -> impl Filter<Extract = (Transaction,), Error = Rejection> + Clone {
20 warp::body::content_length_limit(1024 * 32).and(warp::body::json()) 24 warp::body::content_length_limit(1024 * 32).and(warp::body::json())
21} 25}
22 26
27/// Extracts the value of the `Authorization` header field, hopefully a valid JWT
28/// Used in Authorization for `Block` and `Transaction` proposals
29/// Rejects the request if the Authorization header does not exist
23pub fn auth_header() -> impl Filter<Extract = (String,), Error = Rejection> + Clone { 30pub fn auth_header() -> impl Filter<Extract = (String,), Error = Rejection> + Clone {
24 warp::header::header::<String>("Authorization") 31 warp::header::header::<String>("Authorization")
25} 32}
26 33
27// Accept only json encoded Block body and reject big payloads 34/// Extracts an `Block` JSON body from the request
35/// Accepts only JSON encoded `Block` body and rejects big payloads
28// TODO: find a good limit for this <11-04-21, yigit> // 36// TODO: find a good limit for this <11-04-21, yigit> //
29pub fn block_json_body() -> impl Filter<Extract = (Block,), Error = Rejection> + Clone { 37pub fn block_json_body() -> impl Filter<Extract = (Block,), Error = Rejection> + Clone {
30 warp::body::content_length_limit(1024 * 32).and(warp::body::json()) 38 warp::body::content_length_limit(1024 * 32).and(warp::body::json())
diff --git a/src/handlers.rs b/src/handlers.rs
index 07986f5..80ed1f7 100644
--- a/src/handlers.rs
+++ b/src/handlers.rs
@@ -1,37 +1,28 @@
1/// API handlers, the ends of each filter chain
1use blake2::{Blake2s, Digest}; 2use blake2::{Blake2s, Digest};
2use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; 3use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
3/// API handlers, the ends of each filter chain
4use log::debug; 4use log::debug;
5use md5::Md5; 5use md5::Md5;
6use parking_lot::RwLockUpgradableReadGuard; 6use parking_lot::RwLockUpgradableReadGuard;
7use serde::{Deserialize, Serialize};
8use serde_json; 7use serde_json;
9use serde_json::json;
10use std::convert::Infallible; 8use std::convert::Infallible;
11use std::fs; 9use std::fs;
12use warp::{http::Response, http::StatusCode, reject, reply}; 10use warp::{http::Response, http::StatusCode, reply};
13 11
14use gradecoin::schema::{ 12use crate::schema::{AuthRequest, Block, Claims, Db, MetuId, NakedBlock, Transaction, User};
15 AuthRequest, Block, Db, MetuId, NakedBlock, PublicKeySignature, Transaction, User,
16};
17 13
18const BEARER: &str = "Bearer "; 14const BEARER: &str = "Bearer ";
19 15
20/// tha: Transaction Hash, String 16/// POST request to /register endpoint
21/// iat: Issued At, Unix Time, epoch 17///
22#[derive(Debug, Serialize, Deserialize)] 18/// Lets a [`User`] (=student) to authenticate themselves to the system
23pub struct Claims { 19/// This `request` can be rejected if the payload is malformed (= not authenticated properly) or if
24 pub tha: String, 20/// the [`AuthRequest.user_id`] of the `request` is not in the list of users that can hold a Gradecoin account
25 pub iat: usize,
26}
27
28/// POST /register
29/// Enables a student to introduce themselves to the system
30/// Can fail
31pub async fn authenticate_user( 21pub async fn authenticate_user(
32 request: AuthRequest, 22 request: AuthRequest,
33 db: Db, 23 db: Db,
34) -> Result<impl warp::Reply, warp::Rejection> { 24) -> Result<impl warp::Reply, warp::Rejection> {
25 debug!("POST request to /register, authenticate_user");
35 let given_id = request.student_id.clone(); 26 let given_id = request.student_id.clone();
36 27
37 if let Some(priv_student_id) = MetuId::new(request.student_id) { 28 if let Some(priv_student_id) = MetuId::new(request.student_id) {
@@ -77,7 +68,7 @@ pub async fn authenticate_user(
77/// Returns JSON array of transactions 68/// Returns JSON array of transactions
78/// Cannot fail 69/// Cannot fail
79pub async fn list_transactions(db: Db) -> Result<impl warp::Reply, Infallible> { 70pub async fn list_transactions(db: Db) -> Result<impl warp::Reply, Infallible> {
80 debug!("list all transactions"); 71 debug!("GET request to /transaction, list_transactions");
81 let mut result = Vec::new(); 72 let mut result = Vec::new();
82 73
83 let transactions = db.pending_transactions.read(); 74 let transactions = db.pending_transactions.read();
@@ -95,31 +86,13 @@ pub async fn list_transactions(db: Db) -> Result<impl warp::Reply, Infallible> {
95/// Cannot fail 86/// Cannot fail
96/// Mostly around for debug purposes 87/// Mostly around for debug purposes
97pub async fn list_blocks(db: Db) -> Result<impl warp::Reply, Infallible> { 88pub async fn list_blocks(db: Db) -> Result<impl warp::Reply, Infallible> {
98 debug!("list all block"); 89 debug!("GET request to /block, list_blocks");
99 90
100 let block = db.blockchain.read(); 91 let block = db.blockchain.read();
101 92
102 Ok(reply::with_status(reply::json(&*block), StatusCode::OK)) 93 Ok(reply::with_status(reply::json(&*block), StatusCode::OK))
103} 94}
104 95
105/// POST /transaction
106/// Pushes a new transaction for pending transaction pool
107/// Can reject the transaction proposal
108/// TODO: when is a new transaction rejected <07-04-21, yigit> //
109pub async fn propose_transaction(
110 new_transaction: Transaction,
111 db: Db,
112) -> Result<impl warp::Reply, warp::Rejection> {
113 debug!("new transaction request {:?}", new_transaction);
114
115 // let mut transactions = db.lock().await;
116 let mut transactions = db.pending_transactions.write();
117
118 transactions.insert(new_transaction.source.to_owned(), new_transaction);
119
120 Ok(StatusCode::CREATED)
121}
122
123/// POST /block 96/// POST /block
124/// Proposes a new block for the next round 97/// Proposes a new block for the next round
125/// Can reject the block 98/// Can reject the block
@@ -181,41 +154,64 @@ pub async fn propose_block(new_block: Block, db: Db) -> Result<impl warp::Reply,
181 } 154 }
182} 155}
183 156
157/// POST /transaction
158///
159/// Handles the new transaction requests
160/// Can reject the block if;
161/// # Arguments
162/// * `new_transaction` - Valid JSON of a [`Transaction`]
163/// * `token` - An Authorization header value such as `Bearer aaa.bbb.ccc`
164/// * `db` - Global [`Db`] instance
165///
166/// TODO This method should check if the user has enough balance for the transaction
184pub async fn auth_propose_transaction( 167pub async fn auth_propose_transaction(
185 new_transaction: Transaction, 168 new_transaction: Transaction,
186 token: String, 169 token: String,
187 db: Db, 170 db: Db,
188) -> Result<impl warp::Reply, warp::Rejection> { 171) -> Result<impl warp::Reply, warp::Rejection> {
189 debug!("new transaction request {:?}", new_transaction); 172 debug!("POST request to /transaction, propose_transaction");
190 let raw_jwt = token.trim_start_matches(BEARER).to_owned(); 173 debug!("The transaction request: {:?}", new_transaction);
191 174
192 let decoded = jsonwebtoken::decode::<Claims>( 175 let raw_jwt = token.trim_start_matches(BEARER).to_owned();
193 &token, 176 debug!("raw_jwt: {:?}", raw_jwt);
194 &DecodingKey::from_rsa_pem( 177
195 db.users 178 if let Some(user) = db.users.read().get(&new_transaction.by) {
196 .read() 179 // This public key was already written to the database, we can panic if it's not valid at
197 .get(&new_transaction.by) 180 // *this* point
198 .unwrap() 181 let by_public_key = &user.public_key;
199 .public_key 182
200 .as_bytes(), 183 if let Ok(decoded) = decode::<Claims>(
201 ) 184 &raw_jwt,
202 .unwrap(), 185 &DecodingKey::from_rsa_pem(by_public_key.as_bytes()).unwrap(),
203 // todo@keles: If user is not found return user not found error 186 &Validation::new(Algorithm::RS256),
204 &Validation::new(Algorithm::PS256), 187 ) {
205 ) 188 // this transaction was already checked for correctness at custom_filters, we can panic
206 .unwrap(); 189 // here if it has been changed since
207 // todo: If user is found but header is not validated, return header not valid 190
208 191 let hashed_transaction = Md5::digest(&serde_json::to_vec(&new_transaction).unwrap());
209 let hashed_transaction = Md5::digest(&serde_json::to_vec(&new_transaction).unwrap()); 192
210 193 if decoded.claims.tha == format!("{:x}", hashed_transaction) {
211 // let mut transactions = db.lock().await; 194 let mut transactions = db.pending_transactions.write();
212 if decoded.claims.tha == format!("{:x}", hashed_transaction) { 195
213 let mut transactions = db.pending_transactions.write(); 196 transactions.insert(new_transaction.source.to_owned(), new_transaction);
214 197
215 transactions.insert(new_transaction.source.to_owned(), new_transaction); 198 Ok(StatusCode::CREATED)
216 199 } else {
217 Ok(StatusCode::CREATED) 200 debug!(
201 "the hash of the request {:x} did not match with the hash given in jwt {:?}",
202 hashed_transaction, decoded.claims.tha
203 );
204 Ok(StatusCode::BAD_REQUEST)
205 }
206 } else {
207 debug!("raw_jwt was malformed {:?}", raw_jwt);
208 Ok(StatusCode::BAD_REQUEST)
209 }
218 } else { 210 } else {
211 debug!(
212 "A user with public key signature {:?} is not found in the database",
213 new_transaction.by
214 );
219 Ok(StatusCode::BAD_REQUEST) 215 Ok(StatusCode::BAD_REQUEST)
220 } 216 }
221} 217}
diff --git a/src/lib.rs b/src/lib.rs
index aed4591..6e51899 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,9 +1,27 @@
1pub mod schema; 1//! # Gradecoin
2//!
3//! ## Services
4//! ### /register
5//! - Student creates their own 2048 bit RSA `keypair`
6//! - Downloads `Gradecoin`'s Public Key from Moodle
7//! - Encrypts their JSON wrapped `Public Key` and `Student ID` using Gradecoin's Public Key
8//! - Their public key is now in our Db under [`schema::User::public_key`] and can be used to sign their JWT's during requests
9//!
10//! ### /transaction
11//! - offer a [`schema::Transaction`] - POST request
12//! - The request should have `Authorization`
13//! - The request header should be signed by the Public Key of the `by` field in the transaction
14//! - fetch the list of `Transaction`s - GET request
15//!
16//! ### /block
17//! - offer a [`schema::Block`] - POST request
18//! - The request should have `Authorization`
19//! - The [`schema::Block::transaction_list`] of the block should be a subset of [`schema::Db::pending_transactions`]
20//! - fetch the last accepted [`schema::Block`] - GET request
21//!
22//! `Authorization`: The request header should have Bearer JWT.Token signed with Student Public Key
2 23
3pub use schema::create_database; 24pub mod custom_filters;
4pub use schema::AuthRequest; 25pub mod handlers;
5pub use schema::Block; 26pub mod routes;
6pub use schema::Db; 27pub mod schema;
7pub use schema::MetuId;
8pub use schema::Transaction;
9pub use schema::User;
diff --git a/src/routes.rs b/src/routes.rs
index ed2acad..e4bdee4 100644
--- a/src/routes.rs
+++ b/src/routes.rs
@@ -1,10 +1,11 @@
1use gradecoin::schema::Db; 1/// Endpoints and their construction
2use warp::{Filter, Rejection, Reply}; 2use warp::{Filter, Rejection, Reply};
3 3
4use crate::custom_filters; 4use crate::custom_filters;
5use crate::handlers; 5use crate::handlers;
6use crate::schema::Db;
6 7
7/// Root, all routes combined 8/// Every route combined
8pub fn consensus_routes(db: Db) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone { 9pub fn consensus_routes(db: Db) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
9 transaction_list(db.clone()) 10 transaction_list(db.clone())
10 .or(register_user(db.clone())) 11 .or(register_user(db.clone()))
@@ -39,15 +40,6 @@ pub fn block_list(db: Db) -> impl Filter<Extract = impl Reply, Error = Rejection
39} 40}
40 41
41/// POST /transaction warp route 42/// POST /transaction warp route
42pub fn transaction_propose(db: Db) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
43 warp::path!("transaction")
44 .and(warp::post())
45 .and(custom_filters::transaction_json_body())
46 .and(custom_filters::with_db(db))
47 .and_then(handlers::propose_transaction)
48}
49
50/// POST /transaction warp route
51pub fn auth_transaction_propose( 43pub fn auth_transaction_propose(
52 db: Db, 44 db: Db,
53) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone { 45) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
@@ -68,369 +60,3 @@ pub fn block_propose(db: Db) -> impl Filter<Extract = impl Reply, Error = Reject
68 .and_then(handlers::propose_block) 60 .and_then(handlers::propose_block)
69} 61}
70 62
71#[cfg(test)]
72mod tests {
73 use gradecoin::schema::{create_database, AuthRequest, Block, MetuId, Transaction, User};
74 use handlers::Claims;
75 // use chrono::prelude::*;
76 // use parking_lot::RwLock;
77 // use std::sync::Arc;
78 use warp::http::StatusCode;
79
80 use super::*;
81
82 use jsonwebtoken::{Header, encode, EncodingKey, Algorithm};
83 const private_key_pem: &str = "-----BEGIN RSA PRIVATE KEY-----
84MIIEpAIBAAKCAQEA4nU0G4WjkmcQUx0hq6LQuV5Q+ACmUFL/OjoYMDwC/O/6pCd1
85UZgCfgHN2xEffDPznzcTn8OiFRxr4oWyBinyrUpnY4mhy0SQUwoeCw7YkcHAyhCj
86NT74aR/ohX0MCj0qRRdbt5ZQXM/GC3HJuXE1ptSuhFgQxziItamn8maoJ6JUSVEX
87VO1NOrrjoM3r7Q+BK2B+sX4/bLZ+VG5g1q2nEbFdTHS6pHqtZNHQndTmEKwRfh0R
88YtzEzOXuO6e1gQY42Tujkof40dhGCIU7TeIGGHwdFxy1niLkXwtHNjV7lnIOkTbx
896+sSPamRfQAlZqUWM2Lf5o+7h3qWP3ENB138sQIDAQABAoIBAD23nYTmrganag6M
90wPFrBSGP79c3Lhx0EjUHQjJbGKFgsdltG48qM3ut+DF9ACy0Z+/7bbC7+39vaIOq
911jLR2d6aiYTaLKseO4s2FawD1sgamvU3BZPsXn0gAhnnU5Gyy8Nas1dccvhoc9wI
92neaZUPrvucQ90AzLfo6r9yacDbYHB1lOyomApUvpJxOgHISGEtc9qGPDrdH19aF0
938fCv2bbQRh+TChgN3IB0o5w0wXaI7YAyAouAv/AzHCoEMpt7OGjFTkjh/ujlPL9O
94+FLuJNsQRHDN0gJo2pcvwGwDCsioMixQ9bZ7ZrUu2BNpEQygyeSbj9ZI1iRvhosO
95JU3rwEECgYEA9MppTYA6A9WQbCCwPH1QMpUAmPNVSWVhUVag4lGOEhdCDRcz9ook
96DohQMKctiEB1luKuvDokxo0uMOfMO9/YwjsRB7qjQip7Th1zMJIjD+A+juLzHK4r
97/RiRtWYGAnF8mptDvE+93JsPb3C/lQLvIhio5GQYWBqPJu6SpeosIskCgYEA7NPi
98Gbffzr2UQhW8BNKmctEEh8yFRVojFo3wwwWxSNUVXGSmSm31CL+Q8h817R+2OkPV
991ZMUOBU4UJiqFt28kIvTDFqbAJlJQGCpY2mY7OLQiD2A+TVLcFrHmoCaPfCAK1Qd
100hQ0PmFK7Mf8qClpA3E5chop/WfKQfiu46sZv1qkCgYAhGdXPcw1lQ1W6KVlrdI6J
101qHhiNlVMDXdxZkNvFxQdAiQeXQrbxaZGiMw/J/wSNpUwCAsUzM/4QVMDrfSCDCzl
102ZtNQtj4pTlFKKNVQthIjrXEIJUw2jp7IJLBfVSJu5iWxSlmId0f3MsiNizN81N69
103P5Rm/doE3+KHoy8VXGsHcQKBgQCkNh62enqjHWypjex6450qS6f6iWN3PRLLVsw0
104TcQpniZblCaBwVCAKmRUnjOEIdL2/4ZLutnwMTaFG/YEOOfAylMiY8jKV38lNmD9
105X4D78CFr9klxgvS2CRwSE03f2NzmLkLxuKaxldvaxPTfjMkgeO1LFMlNExYBhkuH
1067uQpUQKBgQCKX6qMNh2gSdgG7qyxfTFZ4y5EGOBoKe/dE+IcVF3Vnh6DZVbCAbBL
1075EdFWZSrCnDjA4xiKW55mwp95Ud9EZsZAb13L8V9t82eK+UDBoWlb7VRNYpda/x1
1085/i4qQJ28x2UNJDStpYFpnp4Ba1lvXjKngIbDPkjU+hbBJ+BNGAIeg==
109-----END RSA PRIVATE KEY-----";
110 /// Create a mock database to be used in tests
111 fn mocked_db() -> Db {
112 let db = create_database();
113
114 db.users.write().insert(
115 "mock_transaction_source".to_owned(),
116 User {
117 user_id: MetuId::new("e254275".to_owned()).unwrap(),
118 public_key:
119"-----BEGIN PUBLIC KEY-----
120MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4nU0G4WjkmcQUx0hq6LQ
121uV5Q+ACmUFL/OjoYMDwC/O/6pCd1UZgCfgHN2xEffDPznzcTn8OiFRxr4oWyBiny
122rUpnY4mhy0SQUwoeCw7YkcHAyhCjNT74aR/ohX0MCj0qRRdbt5ZQXM/GC3HJuXE1
123ptSuhFgQxziItamn8maoJ6JUSVEXVO1NOrrjoM3r7Q+BK2B+sX4/bLZ+VG5g1q2n
124EbFdTHS6pHqtZNHQndTmEKwRfh0RYtzEzOXuO6e1gQY42Tujkof40dhGCIU7TeIG
125GHwdFxy1niLkXwtHNjV7lnIOkTbx6+sSPamRfQAlZqUWM2Lf5o+7h3qWP3ENB138
126sQIDAQAB
127-----END PUBLIC KEY-----"
128 .to_owned(),
129 balance: 0,
130 },
131 );
132 db.pending_transactions.write().insert(
133 "hash_value".to_owned(),
134 Transaction {
135 by: "source_account".to_owned(),
136 source: "source_account".to_owned(),
137 target: "target_account".to_owned(),
138 amount: 20,
139 timestamp: chrono::NaiveDate::from_ymd(2021, 04, 09).and_hms(1, 30, 30),
140 },
141 );
142
143 *db.blockchain.write() = Block {
144 transaction_list: vec![
145 "old_transaction_hash_1".to_owned(),
146 "old_transaction_hash_2".to_owned(),
147 "old_transaction_hash_3".to_owned(),
148 ],
149 nonce: 0,
150 timestamp: chrono::NaiveDate::from_ymd(2021, 04, 08).and_hms(12, 30, 30),
151 hash: "not_a_thing_yet".to_owned(),
152 };
153
154 db
155 }
156
157 fn mocked_jwt() -> String {
158
159 let claims = Claims {
160 tha: "6692e774eba7fb92dc0fe6cf7347591e".to_owned(),
161 iat: 1516239022,
162 };
163 let header = Header::new(Algorithm::RS256);
164 encode(&header, &claims, &EncodingKey::from_rsa_pem(private_key_pem.as_bytes()).unwrap()).unwrap()
165 }
166 /// Create a mock user that is allowed to be in gradecoin to be used in tests
167 fn priviliged_mocked_user() -> AuthRequest {
168 AuthRequest {
169 student_id: String::from("e254275"),
170 public_key: "NOT IMPLEMENTED".to_owned(),
171 }
172 }
173
174 /// Create a mock user that is NOT allowed to be in gradecoin to be used in tests
175 fn unpriviliged_mocked_user() -> AuthRequest {
176 AuthRequest {
177 student_id: String::from("foobarbaz"),
178 public_key: "NOT IMPLEMENTED".to_owned(),
179 }
180 }
181
182 /// Create a mock transaction to be used in tests
183 fn mocked_transaction() -> Transaction {
184 Transaction {
185 by: "mock_transaction_source".to_owned(),
186 source: "mock_transaction_source".to_owned(),
187 target: "mock_transaction_target".to_owned(),
188 amount: 25,
189 timestamp: chrono::NaiveDate::from_ymd(2021, 04, 09).and_hms(14, 30, 00),
190 }
191 }
192
193 /// Create a mock block with a correct mined hash to be used in tests
194 fn mocked_block() -> Block {
195 Block {
196 transaction_list: vec!["hash_value".to_owned()],
197 nonce: 3831993,
198 timestamp: chrono::NaiveDate::from_ymd(2021, 04, 08).and_hms(12, 30, 30),
199 hash: "2b648ffab5d9af1d5d5fc052fc9e51b882fc4fb0c998608c99232f9282000000".to_owned(),
200 }
201 }
202
203 /// Create a mock block with a wrong hash and nonce
204 fn mocked_wrong_block() -> Block {
205 Block {
206 transaction_list: vec!["foobarbaz".to_owned(), "dazsaz".to_owned()],
207 nonce: 1000, // can you imagine
208 timestamp: chrono::NaiveDate::from_ymd(2021, 04, 12).and_hms(05, 29, 30),
209 hash: "tnarstnarsuthnarsthlarjstk".to_owned(),
210 }
211 }
212
213 /// Test simple GET request to /transaction, resource that exists
214 /// https://tools.ietf.org/html/rfc7231#section-6.3.1
215 /// We should get the only pending transaction available in the database as json
216 #[tokio::test]
217 async fn get_pending_transactions() {
218 let db = mocked_db();
219
220 let reply = consensus_routes(db);
221
222 let res = warp::test::request()
223 .method("GET")
224 .path("/transaction")
225 .reply(&reply)
226 .await;
227
228 assert_eq!(res.status(), StatusCode::OK);
229
230 let expected_json_body = r#"[{"by":"source_account","source":"source_account","target":"target_account","amount":20,"timestamp":"2021-04-09T01:30:30"}]"#;
231
232 assert_eq!(res.body(), expected_json_body);
233 }
234
235 /// Test simple GET request to /block, resource that exists
236 /// https://tools.ietf.org/html/rfc7231#section-6.3.1
237 /// Should return the single block available in the database as json
238 #[tokio::test]
239 async fn get_blockchain() {
240 let db = mocked_db();
241 let filter = consensus_routes(db);
242
243 let res = warp::test::request()
244 .method("GET")
245 .path("/block")
246 .reply(&filter)
247 .await;
248
249 assert_eq!(res.status(), StatusCode::OK);
250
251 let expected_json_body = r#"{"transaction_list":["old_transaction_hash_1","old_transaction_hash_2","old_transaction_hash_3"],"nonce":0,"timestamp":"2021-04-08T12:30:30","hash":"not_a_thing_yet"}"#;
252 assert_eq!(res.body(), expected_json_body);
253 }
254
255 /// Test a simple GET request to a nonexisting path
256 /// https://tools.ietf.org/html/rfc7231#section-6.5.4
257 /// Should respond with 404 and stop
258 #[tokio::test]
259 async fn get_nonexisting_path_404() {
260 let db = mocked_db();
261 let filter = consensus_routes(db);
262
263 let res = warp::test::request()
264 .method("GET")
265 .path("/this_path_does_not_exist")
266 .reply(&filter)
267 .await;
268
269 assert_eq!(res.status(), StatusCode::NOT_FOUND);
270 }
271
272 /// Test a POST request to /transaction, a resource that exists
273 /// https://tools.ietf.org/html/rfc7231#section-6.3.2
274 /// Should accept the json request, create
275 /// the transaction and add it to pending transactions in the db
276 #[tokio::test]
277 async fn post_json_201() {
278 let db = mocked_db();
279 let filter = consensus_routes(db.clone());
280 let res = warp::test::request()
281 .method("POST")
282 .json(&mocked_transaction())
283 .path("/transaction")
284 .reply(&filter)
285 .await;
286
287 assert_eq!(res.status(), StatusCode::CREATED);
288 assert_eq!(db.pending_transactions.read().len(), 2);
289 }
290
291 /// Test a POST request to /transaction, a resource that exists
292 /// https://tools.ietf.org/html/rfc7231#section-6.3.2
293 /// Should accept the json request, create
294 /// the transaction and add it to pending transactions in the db
295 #[tokio::test]
296 async fn post_auth_json_201() {
297 let db = mocked_db();
298 let filter = consensus_routes(db.clone());
299
300 let res = warp::test::request()
301 .method("POST")
302 .json(&mocked_transaction())
303 .header("Authorization", format!("Bearer {}", &mocked_jwt()))
304 .path("/transaction")
305 .reply(&filter)
306 .await;
307
308 assert_eq!(res.status(), StatusCode::CREATED);
309 assert_eq!(db.pending_transactions.read().len(), 2);
310 }
311
312 /// Test a POST request to /transaction, a resource that exists
313 /// https://tools.ietf.org/html/rfc7231#section-6.3.2
314 /// Should accept the json request, create
315 /// the transaction and add it to pending transactions in the db
316 #[tokio::test]
317 async fn post_auth_json_400() {
318 let db = mocked_db();
319 let filter = consensus_routes(db.clone());
320
321 let res = warp::test::request()
322 .method("POST")
323 .json(&mocked_transaction())
324 .header("Authorization", "Bearer aaaaaaaasdlkjaldkasljdaskjlaaaaaaaaaaaaaa")
325 .path("/transaction")
326 .reply(&filter)
327 .await;
328
329 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
330 assert_eq!(db.pending_transactions.read().len(), 1);
331 }
332
333 /// Test a POST request to /block, a resource that exists
334 /// https://tools.ietf.org/html/rfc7231#section-6.3.2
335 /// Should accept the json request, create
336 /// the block
337 #[tokio::test]
338 async fn post_block_201() {
339 let db = mocked_db();
340 let filter = consensus_routes(db.clone());
341
342 let res = warp::test::request()
343 .method("POST")
344 .json(&mocked_block())
345 .path("/block")
346 .reply(&filter)
347 .await;
348
349 assert_eq!(res.status(), StatusCode::CREATED);
350 assert_eq!(
351 *db.blockchain.read().hash,
352 "2b648ffab5d9af1d5d5fc052fc9e51b882fc4fb0c998608c99232f9282000000".to_owned()
353 );
354 }
355
356 /// Test a POST request to /block, a resource that exists
357 /// https://tools.ietf.org/html/rfc7231#section-6.3.2
358 /// Should reject the block because of the wrong hash
359 #[tokio::test]
360 async fn post_block_wrong_hash() {
361 let db = mocked_db();
362 let filter = consensus_routes(db.clone());
363
364 let res = warp::test::request()
365 .method("POST")
366 .json(&mocked_wrong_block())
367 .path("/block")
368 .reply(&filter)
369 .await;
370
371 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
372 }
373
374 /// Test a POST request to /register, an endpoint that exists
375 /// https://tools.ietf.org/html/rfc7231#section-6.3.2
376 /// Should accept the json request, create a new user and
377 /// add it to the user hashmap in the db
378 #[tokio::test]
379 async fn post_register_priviliged_user() {
380 let db = mocked_db();
381 let filter = consensus_routes(db.clone());
382
383 let res = warp::test::request()
384 .method("POST")
385 .json(&priviliged_mocked_user())
386 .path("/register")
387 .reply(&filter)
388 .await;
389
390 println!("{:?}", res.body());
391 assert_eq!(res.status(), StatusCode::CREATED);
392 assert_eq!(db.users.read().len(), 2);
393 }
394
395 /// Test a POST request to /transaction, an endpoint that exists
396 /// https://tools.ietf.org/html/rfc7231#section-6.3.2
397 /// Should NOT accept the json request as the user is unpriviliged
398 #[tokio::test]
399 async fn post_register_unpriviliged_user() {
400 let db = mocked_db();
401 let filter = consensus_routes(db.clone());
402
403 let res = warp::test::request()
404 .method("POST")
405 .json(&unpriviliged_mocked_user())
406 .path("/register")
407 .reply(&filter)
408 .await;
409
410 println!("{:?}", res.body());
411 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
412 assert_eq!(db.users.read().len(), 1);
413 }
414
415 /// Test a POST request to /transaction, a resource that exists with a longer than expected
416 /// payload
417 /// https://tools.ietf.org/html/rfc7231#section-6.5.11
418 /// Should return 413 to user
419 #[tokio::test]
420 async fn post_too_long_content_413() {
421 let db = mocked_db();
422 let filter = consensus_routes(db);
423
424 let res = warp::test::request()
425 .method("POST")
426 .header("content-length", 1024 * 36)
427 .path("/transaction")
428 .reply(&filter)
429 .await;
430
431 assert_eq!(res.status(), StatusCode::PAYLOAD_TOO_LARGE);
432 }
433}
434
435// TODO: POST block without correct transactions test <09-04-21, yigit> //
436// TODO: POST transaction while that source has pending transaction test <09-04-21, yigit> //
diff --git a/src/schema.rs b/src/schema.rs
index 39921b8..4eb2f0a 100644
--- a/src/schema.rs
+++ b/src/schema.rs
@@ -1,3 +1,12 @@
1//! # Data Representations
2//!
3//! We need persistence for [`Block`]s and [`User`]s, not so much for [`Transaction`]s
4//!
5//! There are around 30 students, a full fledged database would be an overkill (for next year?)
6//!
7//! Pending transactions are held in memory, these are cleared with every new block
8//! Only the last block is held in memory, every block is written to a file
9//! Users are held in memory and they're also backed up to text files
1use chrono::{NaiveDate, NaiveDateTime}; 10use chrono::{NaiveDate, NaiveDateTime};
2use lazy_static::lazy_static; 11use lazy_static::lazy_static;
3use parking_lot::RwLock; 12use parking_lot::RwLock;
@@ -9,24 +18,45 @@ use std::sync::Arc;
9 18
10// use crate::validators; 19// use crate::validators;
11 20
12/// We need persistence for blocks and users, not so much for transactions 21pub type PublicKeySignature = String;
13/// There are around 30 students, a full fledged database would be an overkill (for next year?)
14/// Pending transactions are held in memory, these are cleared with every new block
15/// Only the last block is held in memory, every block is written to a file
16/// Users are held in memory and they're also backed up to text files
17 22
18/// Creates a new database connection 23/// Creates a new database
19pub fn create_database() -> Db { 24pub fn create_database() -> Db {
20 fs::create_dir_all("blocks").unwrap(); 25 fs::create_dir_all("blocks").unwrap();
21 fs::create_dir_all("users").unwrap(); 26 fs::create_dir_all("users").unwrap();
22 Db::new() 27 Db::new()
23} 28}
24 29
30/// A JWT Payload/Claims representation
31///
32/// https://tools.ietf.org/html/rfc7519#section-4.1
33///
34/// - `tha`: Transaction Hash, String (custom field)
35/// - `iat`: Issued At, Unix Time, epoch
36/// - `exp`: Expiration Time, epoch
37#[derive(Debug, Serialize, Deserialize)]
38pub struct Claims {
39 pub tha: String,
40 pub iat: usize,
41 pub exp: usize,
42}
43
44/// Global Database representation
45///
46/// [`blockchain`] is just the last block that was mined. All the blocks are written to disk as text
47/// files whenever they are accepted.
48///
49/// [`pending_transactions`] is the in memory representation of the waiting transactions. Every
50/// user can have only one outstanding transaction at any given time.
51///
52/// [`users`] is the in memory representation of the users, with their public keys, metu_ids and
53/// gradecoin balances.
54///
55/// TODO: Replace the pending_transactions HashMap<String, Transaction> with
56/// HashMap<PublicKeySignature, Transaction>
25#[derive(Debug, Clone)] 57#[derive(Debug, Clone)]
26pub struct Db { 58pub struct Db {
27 // heh. also https://doc.rust-lang.org/std/collections/struct.LinkedList.html says Vec is generally faster
28 pub blockchain: Arc<RwLock<Block>>, 59 pub blockchain: Arc<RwLock<Block>>,
29 // every proposer can have _one_ pending transaction, a way to enforce this, String is proposer identifier
30 pub pending_transactions: Arc<RwLock<HashMap<String, Transaction>>>, 60 pub pending_transactions: Arc<RwLock<HashMap<String, Transaction>>>,
31 pub users: Arc<RwLock<HashMap<String, User>>>, 61 pub users: Arc<RwLock<HashMap<String, User>>>,
32} 62}
@@ -41,8 +71,6 @@ impl Db {
41 } 71 }
42} 72}
43 73
44pub type PublicKeySignature = String;
45
46/// A transaction between `source` and `target` that moves `amount` 74/// A transaction between `source` and `target` that moves `amount`
47#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] 75#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
48pub struct Transaction { 76pub struct Transaction {
@@ -53,20 +81,21 @@ pub struct Transaction {
53 pub timestamp: NaiveDateTime, 81 pub timestamp: NaiveDateTime,
54} 82}
55 83
56/// A block that was proposed with `transaction_list` and `nonce` that made `hash` valid 84/// A block that was proposed with `transaction_list` and `nonce` that made `hash` valid, 6 zeroes
85/// at the right hand side of the hash (24 bytes)
86///
87/// We are mining using blake2s algorithm, which produces 256 bit hashes. Hash/second is roughly
88/// 20x10^3.
89///
57/// https://serde.rs/container-attrs.html might be valuable to normalize the serialize/deserialize 90/// https://serde.rs/container-attrs.html might be valuable to normalize the serialize/deserialize
58/// conventions as these will be hashed 91/// conventions as these will be hashed
92///
59#[derive(Serialize, Deserialize, Debug)] 93#[derive(Serialize, Deserialize, Debug)]
60pub struct Block { 94pub struct Block {
61 // TODO: transaction list should hold transaction hash values <09-04-21, yigit> // 95 pub transaction_list: Vec<PublicKeySignature>,
62 // but do we link them somehow? (like a log of old transactions?)
63 // we can leave this as is and whenever we have a new block we _could_ just log it to file
64 // somewhere
65 // I want to keep this as a String vector because it makes things easier elsewhere
66 pub transaction_list: Vec<PublicKeySignature>, // hashes of the transactions (or just "source" for now)
67 pub nonce: u32, 96 pub nonce: u32,
68 pub timestamp: NaiveDateTime, 97 pub timestamp: NaiveDateTime,
69 pub hash: String, // future proof'd baby 98 pub hash: String,
70} 99}
71 100
72/// For prototyping and letting serde handle everything json 101/// For prototyping and letting serde handle everything json
@@ -89,7 +118,7 @@ impl Block {
89 } 118 }
90} 119}
91 120
92/// Or simply a Student 121/// Simply a Student
93#[derive(Serialize, Deserialize, Debug)] 122#[derive(Serialize, Deserialize, Debug)]
94pub struct User { 123pub struct User {
95 pub user_id: MetuId, 124 pub user_id: MetuId,
@@ -103,6 +132,7 @@ pub struct MetuId {
103 id: String, 132 id: String,
104} 133}
105 134
135// TODO: this will arrive encrypted <13-04-21, yigit> //
106#[derive(Serialize, Deserialize, Debug)] 136#[derive(Serialize, Deserialize, Debug)]
107pub struct AuthRequest { 137pub struct AuthRequest {
108 pub student_id: String, 138 pub student_id: String,