From 44d21b676f90a2fc8b255eb9c1393e53f40c9daa Mon Sep 17 00:00:00 2001 From: Yigit Sever Date: Mon, 12 Apr 2021 05:32:53 +0300 Subject: Implement proof-of-work Using blacke2s: https://docs.rs/blake2/0.9.1/blake2/ Using this guy's hash checker https://gist.github.com/gkbrk/2e4835e3a17b3fb6e1e7 blacke2s with 5 bits 0 can mine a block between 20 seconds to 359 during my tests, hope it'll be fun --- src/custom_filters.rs | 8 ++---- src/handlers.rs | 54 +++++++++++++++++++++++----------- src/lib.rs | 9 ++++++ src/main.rs | 6 ++-- src/routes.rs | 80 ++++++++++++++++++++++++++++++++++++++++++++------- src/schema.rs | 12 ++++++-- 6 files changed, 133 insertions(+), 36 deletions(-) create mode 100644 src/lib.rs (limited to 'src') diff --git a/src/custom_filters.rs b/src/custom_filters.rs index 0806c6d..315ba4a 100644 --- a/src/custom_filters.rs +++ b/src/custom_filters.rs @@ -1,10 +1,7 @@ -// Common filters ment to be shared between many endpoints - +use gradecoin::schema::{AuthRequest, Block, Db, Transaction}; use std::convert::Infallible; use warp::{Filter, Rejection}; -use crate::schema::{Block, Db, Transaction, AuthRequest}; - // Database context for routes pub fn with_db(db: Db) -> impl Filter + Clone { warp::any().map(move || db.clone()) @@ -12,7 +9,8 @@ pub fn with_db(db: Db) -> impl Filter + Clo // Accept only json encoded User body and reject big payloads // TODO: find a good limit for this, (=e2482057; 8 char String + rsa pem) <11-04-21, yigit> // -pub fn auth_request_json_body() -> impl Filter + Clone { +pub fn auth_request_json_body() -> impl Filter + Clone +{ warp::body::content_length_limit(1024 * 32).and(warp::body::json()) } diff --git a/src/handlers.rs b/src/handlers.rs index bfd57bc..6edc96f 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -2,12 +2,15 @@ use log::debug; use parking_lot::RwLockUpgradableReadGuard; use serde_json; +use serde_json::json; use std::convert::Infallible; use warp::{http::Response, http::StatusCode, reply}; +use blake2::{Blake2s, Digest}; + use std::fs; -use crate::schema::{AuthRequest, Block, Db, MetuId, Transaction, User}; +use gradecoin::schema::{AuthRequest, Block, Db, MetuId, NakedBlock, Transaction, User}; /// POST /register /// Enables a student to introduce themselves to the system @@ -22,7 +25,6 @@ pub async fn authenticate_user( let userlist = db.users.upgradable_read(); if userlist.contains_key(&given_id) { - let res = Response::builder() .status(StatusCode::BAD_REQUEST) .body("This user is already authenticated"); @@ -124,24 +126,44 @@ pub async fn propose_block(new_block: Block, db: Db) -> Result // - // assume it is for now + let naked_block = NakedBlock { + transaction_list: new_block.transaction_list.clone(), + nonce: new_block.nonce.clone(), + timestamp: new_block.timestamp.clone(), + }; - let mut blockchain = RwLockUpgradableReadGuard::upgrade(blockchain); + let naked_block_flat = serde_json::to_vec(&naked_block).unwrap(); - let block_json = serde_json::to_string(&new_block).unwrap(); + let hashvalue = Blake2s::digest(&naked_block_flat); + let hash_string = format!("{:x}", hashvalue); - // let mut file = File::create(format!("{}.block", new_block.timestamp.timestamp())).unwrap(); - fs::write( - format!("blocks/{}.block", new_block.timestamp.timestamp()), - block_json, - ) - .unwrap(); + // 5 rightmost bits are zero + let should_zero = hashvalue[31] as i32 + hashvalue[30] as i32 + (hashvalue[29] << 4) as i32; - *blockchain = new_block; + if should_zero == 0 { + // one last check to see if block is telling the truth + if hash_string == new_block.hash { + let mut blockchain = RwLockUpgradableReadGuard::upgrade(blockchain); - let mut pending_transactions = RwLockUpgradableReadGuard::upgrade(pending_transactions); - pending_transactions.clear(); + let block_json = serde_json::to_string(&new_block).unwrap(); - Ok(StatusCode::CREATED) + fs::write( + format!("blocks/{}.block", new_block.timestamp.timestamp()), + block_json, + ) + .unwrap(); + + *blockchain = new_block; + + let mut pending_transactions = RwLockUpgradableReadGuard::upgrade(pending_transactions); + pending_transactions.clear(); + + Ok(StatusCode::CREATED) + } else { + Ok(StatusCode::BAD_REQUEST) + } + } else { + // reject + Ok(StatusCode::BAD_REQUEST) + } } diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..aed4591 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,9 @@ +pub mod schema; + +pub use schema::create_database; +pub use schema::AuthRequest; +pub use schema::Block; +pub use schema::Db; +pub use schema::MetuId; +pub use schema::Transaction; +pub use schema::User; diff --git a/src/main.rs b/src/main.rs index 373223c..5683aea 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,10 @@ +use gradecoin::schema::create_database; use std::env; use warp::Filter; -mod handlers; mod custom_filters; +mod handlers; mod routes; -mod schema; // mod validators; #[tokio::main] @@ -15,7 +15,7 @@ async fn main() { } pretty_env_logger::init(); - let db = schema::create_database(); + let db = create_database(); let api = routes::consensus_routes(db); diff --git a/src/routes.rs b/src/routes.rs index 9f0adc5..03a2569 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -2,7 +2,7 @@ use warp::{Filter, Rejection, Reply}; use crate::custom_filters; use crate::handlers; -use crate::schema::Db; +use gradecoin::schema::Db; /// Root, all routes combined pub fn consensus_routes(db: Db) -> impl Filter + Clone { @@ -65,12 +65,11 @@ mod tests { // use std::sync::Arc; use warp::http::StatusCode; - use crate::schema; - use crate::schema::{AuthRequest, Block, Transaction}; + use gradecoin::schema::{create_database, AuthRequest, Block, Transaction}; /// Create a mock database to be used in tests fn mocked_db() -> Db { - let db = schema::create_database(); + let db = create_database(); db.pending_transactions.write().insert( "hash_value".to_owned(), @@ -88,7 +87,7 @@ mod tests { "old_transaction_hash_2".to_owned(), "old_transaction_hash_3".to_owned(), ], - nonce: "not_a_thing_yet".to_owned(), + nonce: 0, timestamp: chrono::NaiveDate::from_ymd(2021, 04, 08).and_hms(12, 30, 30), hash: "not_a_thing_yet".to_owned(), }; @@ -122,6 +121,26 @@ mod tests { } } + /// Create a mock block with a correct mined hash to be used in tests + fn mocked_block() -> Block { + Block { + transaction_list: vec!["hash_value".to_owned()], + nonce: 560108, + timestamp: chrono::NaiveDate::from_ymd(2021, 04, 08).and_hms(12, 30, 30), + hash: "c7d053f3e5b056ba948db3f5c0d30408fb0c29a328a0c3c1cf435fb68d700000".to_owned(), + } + } + + /// Create a mock block with a wrong hash and nonce + fn mocked_wrong_block() -> Block { + Block { + transaction_list: vec!["foobarbaz".to_owned(), "dazsaz".to_owned()], + nonce: 1000, // can you imagine + timestamp: chrono::NaiveDate::from_ymd(2021, 04, 12).and_hms(05, 29, 30), + hash: "tnarstnarsuthnarsthlarjstk".to_owned(), + } + } + /// Test simple GET request to /transaction, resource that exists /// https://tools.ietf.org/html/rfc7231#section-6.3.1 /// We should get the only pending transaction available in the database as json @@ -160,7 +179,7 @@ mod tests { assert_eq!(res.status(), StatusCode::OK); - let expected_json_body = r#"{"transaction_list":["old_transaction_hash_1","old_transaction_hash_2","old_transaction_hash_3"],"nonce":"not_a_thing_yet","timestamp":"2021-04-08T12:30:30","hash":"not_a_thing_yet"}"#; + 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"}"#; assert_eq!(res.body(), expected_json_body); } @@ -201,7 +220,48 @@ mod tests { assert_eq!(db.pending_transactions.read().len(), 2); } - /// TEST a POST request to /transaction, an endpoint that exists + /// Test a POST request to /block, a resource that exists + /// https://tools.ietf.org/html/rfc7231#section-6.3.2 + /// Should accept the json request, create + /// the block + #[tokio::test] + async fn post_block_201() { + let db = mocked_db(); + let filter = consensus_routes(db.clone()); + + let res = warp::test::request() + .method("POST") + .json(&mocked_block()) + .path("/block") + .reply(&filter) + .await; + + assert_eq!(res.status(), StatusCode::CREATED); + assert_eq!( + *db.blockchain.read().hash, + "c7d053f3e5b056ba948db3f5c0d30408fb0c29a328a0c3c1cf435fb68d700000".to_owned() + ); + } + + /// Test a POST request to /block, a resource that exists + /// https://tools.ietf.org/html/rfc7231#section-6.3.2 + /// Should reject the block because of the wrong hash + #[tokio::test] + async fn post_block_wrong_hash() { + let db = mocked_db(); + let filter = consensus_routes(db.clone()); + + let res = warp::test::request() + .method("POST") + .json(&mocked_wrong_block()) + .path("/block") + .reply(&filter) + .await; + + assert_eq!(res.status(), StatusCode::BAD_REQUEST); + } + + /// Test a POST request to /register, an endpoint that exists /// https://tools.ietf.org/html/rfc7231#section-6.3.2 /// Should accept the json request, create a new user and /// add it to the user hashmap in the db @@ -221,9 +281,10 @@ mod tests { assert_eq!(res.status(), StatusCode::CREATED); assert_eq!(db.users.read().len(), 1); } - /// TEST a POST request to /transaction, an endpoint that exists + + /// Test a POST request to /transaction, an endpoint that exists /// https://tools.ietf.org/html/rfc7231#section-6.3.2 - /// Should NOT accept the json request + /// Should NOT accept the json request as the user is unpriviliged #[tokio::test] async fn post_register_unpriviliged_user() { let db = mocked_db(); @@ -261,6 +322,5 @@ mod tests { } } -// TODO: POST block test <09-04-21, yigit> // // TODO: POST block without correct transactions test <09-04-21, yigit> // // 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 909b5cd..98291d7 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -62,17 +62,25 @@ pub struct Block { // somewhere // I want to keep this as a String vector because it makes things easier elsewhere pub transaction_list: Vec, // hashes of the transactions (or just "source" for now) - pub nonce: String, + pub nonce: u32, pub timestamp: NaiveDateTime, pub hash: String, // future proof'd baby } +/// For prototyping and letting serde handle everything json +#[derive(Serialize, Deserialize, Debug)] +pub struct NakedBlock { + pub transaction_list: Vec, + pub nonce: u32, + pub timestamp: NaiveDateTime, +} + impl Block { /// Genesis block pub fn new() -> Block { Block { transaction_list: vec![], - nonce: String::from(""), + nonce: 0, timestamp: NaiveDate::from_ymd(2021, 04, 11).and_hms(20, 45, 00), hash: String::from(""), } -- cgit v1.2.3-70-g09d2