From e0fb91039f34204b2a5c588a95cb3f1789ad2fa7 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 --- Cargo.lock | 35 ++++++++++++++++++++++ Cargo.toml | 2 ++ TODO.md | 4 +-- examples/mining.rs | 35 ++++++++++++++++++++++ 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 ++++++-- 10 files changed, 207 insertions(+), 38 deletions(-) create mode 100644 examples/mining.rs create mode 100644 src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index d4d5926..1116c65 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,6 +44,17 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +[[package]] +name = "blake2" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a5720225ef5daecf08657f23791354e1685a8c91a4c60c7f3d3b2892f978f4" +dependencies = [ + "crypto-mac", + "digest", + "opaque-debug", +] + [[package]] name = "block-buffer" version = "0.9.0" @@ -122,6 +133,16 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" +[[package]] +name = "crypto-mac" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" +dependencies = [ + "generic-array", + "subtle", +] + [[package]] name = "digest" version = "0.9.0" @@ -280,7 +301,9 @@ dependencies = [ name = "gradecoin" version = "0.1.0" dependencies = [ + "blake2", "chrono", + "hex-literal", "lazy_static", "log", "parking_lot", @@ -352,6 +375,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hex-literal" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5af1f635ef1bc545d78392b136bfe1c9809e029023c84a3638a864a10b8819c8" + [[package]] name = "http" version = "0.2.3" @@ -979,6 +1008,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "subtle" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2" + [[package]] name = "syn" version = "1.0.68" diff --git a/Cargo.toml b/Cargo.toml index 2ae2b25..7e12c2d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,8 @@ pretty_env_logger = "0.3.1" parking_lot = "0.10.0" serde_json = "1.0.59" lazy_static = "1.4.0" +blake2 = "0.9.1" +hex-literal = "0.3.1" [dev-dependencies] serde_test = "1.0.117" diff --git a/TODO.md b/TODO.md index 47cf45b..e28373b 100644 --- a/TODO.md +++ b/TODO.md @@ -1,8 +1,6 @@ # TODO ## Proof-of-work -- [ ] pick a block proposal scheme (= pick hash function) [list of hash functions](https://en.bitcoinwiki.org/wiki/List_of_hash_functions) -- [ ] check the nonce for incoming blocks ## Authentication - [X] pick a user authentication scheme = [JWT](https://tools.ietf.org/html/rfc7519) Seems perfect @@ -26,3 +24,5 @@ ## Done & Brag - [x] Switch to RwLock (parking_lot) (done at 2021-04-07 03:43, two possible schemes to represent inner Db (ledger) in code) - [x] We need our own representation of students and their grades, "there is no blockchain" (done at 2021-04-12 00:05) +- [x] pick a block proposal scheme (= pick hash function) [list of hash functions](https://en.bitcoinwiki.org/wiki/List_of_hash_functions) (done at 2021-04-12 05:30) +- [x] check the nonce for incoming blocks (done at 2021-04-12 05:30) diff --git a/examples/mining.rs b/examples/mining.rs new file mode 100644 index 0000000..56e33f3 --- /dev/null +++ b/examples/mining.rs @@ -0,0 +1,35 @@ +use chrono::NaiveDate; +use gradecoin::schema::NakedBlock; +use serde_json; +use std::time::Instant; + +use blake2::{Blake2s, Digest}; + +pub fn main() { + let mut b = NakedBlock { + transaction_list: vec![ + "hash_value".to_owned(), + ], + nonce: 0, + timestamp: NaiveDate::from_ymd(2021, 04, 08).and_hms(12, 30, 30), + }; + + let now = Instant::now(); + + for nonce in 0..u32::MAX { + b.nonce = nonce; + + let j = serde_json::to_vec(&b).unwrap(); + + let result = Blake2s::digest(&j); + + let first_five = result[31] as i32 + result[30] as i32 + (result[29] << 4) as i32; + + if first_five == 0 { + println!("{} - {:x}\n{:?}", nonce, result, b); + break; + } + } + + println!("it took {} seconds", now.elapsed().as_secs()); +} 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