From 20dad7d4290f2c98583168cd4b9afcdec4802944 Mon Sep 17 00:00:00 2001 From: Yigit Sever Date: Wed, 7 Apr 2021 04:33:45 +0300 Subject: Implement Block GET/PUT with new schema - `Arc`+`Mutex` is replaced by `parking_lot::RwLock,` decoupled Read+Write and ability to upgrade read locks into write locks if needed - Schema has changed, `Db` is now a struct that implements `new()` to return a new instance of itself, pros/cons listed in code but tl;dr blockchain and pending transactions are separate now - `custom_filters` now supports extracting Block json and Transaction json in separate functions too - /block GET and PUT implemented, `Blocks` currently have one check (transactions appear in pending transaction) - debug is working after something, dunno how I fixed it --- src/custom_filters.rs | 11 ++++-- src/handlers.rs | 94 ++++++++++++++++++++++++++++++++++++++------------- src/main.rs | 6 ++-- src/routes.rs | 30 +++++++++++++--- src/schema.rs | 65 ++++++++++++++++++++++++----------- 5 files changed, 152 insertions(+), 54 deletions(-) (limited to 'src') diff --git a/src/custom_filters.rs b/src/custom_filters.rs index 86a78d4..7caf71a 100644 --- a/src/custom_filters.rs +++ b/src/custom_filters.rs @@ -3,7 +3,7 @@ use std::convert::Infallible; use warp::{Filter, Rejection}; -use crate::schema::{Db, Transaction}; // `Block` coming later +use crate::schema::{Block, Db, Transaction}; // Database context for routes pub fn with_db(db: Db) -> impl Filter + Clone { @@ -15,7 +15,12 @@ pub fn with_db(db: Db) -> impl Filter + Clo // warp::query::() // } -// Accept only JSON body and reject big payloads -pub fn json_body() -> impl Filter + Clone { +// Accept only json encoded Transaction body and reject big payloads +pub fn transaction_json_body() -> impl Filter + Clone { + warp::body::content_length_limit(1024 * 32).and(warp::body::json()) +} + +// Accept only json encoded Transaction body and reject big payloads +pub fn block_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 51c7b63..ecf5a92 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -1,45 +1,93 @@ -// API handlers, the ends of each filter chain - -use log::debug; +/// API handlers, the ends of each filter chain +use log::debug; // this is more useful than debug! learn how to use this +use parking_lot::RwLockUpgradableReadGuard; use std::convert::Infallible; use warp::{http::StatusCode, reply}; -use crate::schema::{Db, Transaction}; // `Block` coming later +use crate::schema::{Block, Db, Transaction}; + +/// GET /transaction +/// Returns JSON array of transactions +/// Cannot fail +pub async fn list_transactions(db: Db) -> Result { + debug!("list all transactions"); + let mut result = Vec::new(); + + let transactions = db.pending_transactions.read(); + // let transactions = transactions.clone().into_iter().collect(); + + for (_, value) in transactions.iter() { + result.push(value) + } + + Ok(reply::with_status(reply::json(&result), StatusCode::OK)) +} + +/// GET /block +/// Returns JSON array of blocks +/// Cannot fail +/// Mostly around for debug purposes +pub async fn list_blocks(db: Db) -> Result { + debug!("list all blocks"); + + let mut result = Vec::new(); + let blocks = db.blockchain.read(); + + for block in blocks.iter() { + result.push(block); + } -// PROPOSE Transaction -// POST /transaction + Ok(reply::with_status(reply::json(&result), StatusCode::OK)) +} + +/// POST /transaction +/// Pushes a new transaction for pending transaction pool +/// Can reject the transaction proposal +/// TODO: when is a new transaction rejected <07-04-21, yigit> // pub async fn propose_transaction( new_transaction: Transaction, db: Db, ) -> Result { debug!("new transaction request {:?}", new_transaction); - let mut transactions = db.lock().await; + // let mut transactions = db.lock().await; + let mut transactions = db.pending_transactions.write(); - transactions.push(new_transaction); + transactions.insert(new_transaction.source.to_owned(), new_transaction); Ok(StatusCode::CREATED) } -// GET Transaction List -// GET /transaction -// Returns JSON array of transactions -// Cannot fail? -pub async fn list_transactions(db: Db) -> Result { - debug!("list all transactions"); +/// POST /block +/// Proposes a new block for the next round +/// Can reject the block +pub async fn propose_block(new_block: Block, db: Db) -> Result { + debug!("new block request {:?}", new_block); - let transactions = db.lock().await; + // https://blog.logrocket.com/create-an-async-crud-web-service-in-rust-with-warp/ (this has + // error.rs, error struct, looks very clean) - let transactions: Vec = transactions.clone().into_iter().collect(); + let pending_transactions = db.pending_transactions.upgradable_read(); + let blockchain = db.blockchain.upgradable_read(); - Ok(reply::with_status( - reply::json(&transactions), - StatusCode::OK, - )) -} + // TODO: check 1, new_block.transaction_list from pending_transactions pool? <07-04-21, yigit> // + for transaction_hash in new_block.transaction_list.iter() { + if !pending_transactions.contains_key(transaction_hash) { + return Ok(StatusCode::BAD_REQUEST); + } + } -// PROPOSE Block -// POST /block + // TODO: check 2, block hash (\w nonce) asserts $hash_condition? <07-04-21, yigit> // + // assume it is for now + + let mut blockchain = RwLockUpgradableReadGuard::upgrade(blockchain); + blockchain.push(new_block); + + let mut pending_transactions = RwLockUpgradableReadGuard::upgrade(pending_transactions); + pending_transactions.clear(); + + Ok(StatusCode::CREATED) +} // `GET /games` // Returns JSON array of todos diff --git a/src/main.rs b/src/main.rs index bcd4173..7ef2597 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,15 +11,15 @@ mod schema; async fn main() { // Show debug logs by default by setting `RUST_LOG=restful_rust=debug` if env::var_os("RUST_LOG").is_none() { - env::set_var("RUST_LOG", "restful_rust=debug"); + env::set_var("RUST_LOG", "gradecoin=debug"); } pretty_env_logger::init(); - let db = schema::ledger(); // 1. we need this to return a _simple_ db + let db = schema::create_database(); let api = routes::consensus_routes(db); - let routes = api.with(warp::log("restful_rust")); + let routes = api.with(warp::log("gradecoin")); // Start the server warp::serve(routes).run(([127, 0, 0, 1], 8080)).await; diff --git a/src/routes.rs b/src/routes.rs index fc4426a..9054fb6 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -4,12 +4,15 @@ use crate::custom_filters; use crate::handlers; use crate::schema::Db; -// Root, all routes combined +/// Root, all routes combined pub fn consensus_routes(db: Db) -> impl Filter + Clone { - transaction_list(db.clone()).or(transaction_propose(db.clone())) + transaction_list(db.clone()) + .or(transaction_propose(db.clone())) + .or(block_propose(db.clone())) + .or(block_list(db.clone())) } -// GET /transaction +/// GET /transaction pub fn transaction_list(db: Db) -> impl Filter + Clone { warp::path!("transaction") .and(warp::get()) @@ -17,15 +20,32 @@ pub fn transaction_list(db: Db) -> impl Filter impl Filter + Clone { + warp::path!("block") + .and(warp::get()) + .and(custom_filters::with_db(db)) + .and_then(handlers::list_blocks) +} + +/// POST /transaction pub fn transaction_propose(db: Db) -> impl Filter + Clone { warp::path!("transaction") .and(warp::post()) - .and(custom_filters::json_body()) + .and(custom_filters::transaction_json_body()) .and(custom_filters::with_db(db)) .and_then(handlers::propose_transaction) } +/// POST /block +pub fn block_propose(db: Db) -> impl Filter + Clone { + warp::path!("block") + .and(warp::post()) + .and(custom_filters::block_json_body()) + .and(custom_filters::with_db(db)) + .and_then(handlers::propose_block) +} + /////////////////////////// // below are not mine. // /////////////////////////// diff --git a/src/schema.rs b/src/schema.rs index ea36a70..57210a3 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -1,31 +1,46 @@ -// Common types used across API - -use chrono::{NaiveDate, NaiveDateTime}; +use chrono::NaiveDateTime; +use parking_lot::RwLock; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; use std::sync::Arc; -use tokio::sync::Mutex; // use crate::validators; -pub fn ledger() -> Db { - // TODO: there was something simpler in one of the other tutorials? <07-04-21, yigit> // - - Arc::new(Mutex::new(vec![ - Transaction { - source: String::from("Myself"), - target: String::from("Nobody"), - amount: 4, - timestamp: NaiveDate::from_ymd(2021, 4, 7).and_hms(00, 17, 00), - }, - ])) +// In memory data structure + +// Two approaches here +// 1. Db is a type +// pub type Db = Arc>>; +// Ledger is a struct, we wrap the ledger with arc + mutex in ledger() +// to access transactions we need to unwrap blocks as well, vice versa +// +// 2. Db is a struct, attributes are wrapped +// we can offload ::new() to it's member method +// blocks and transactions are accessible separately, which is the biggest pro + +/// Creates a new database +pub fn create_database() -> Db { + Db::new() } +#[derive(Debug, Clone)] +pub struct Db { + // heh. also https://doc.rust-lang.org/std/collections/struct.LinkedList.html says Vec is generally faster + pub blockchain: Arc>>, + // every proposer can have _one_ pending transaction, a way to enforce this, String is proposer identifier + pub pending_transactions: Arc>>, +} -// For presentation purposes keep mocked data in in-memory structure -// In real life scenario connection with regular database would be established - -pub type Db = Arc>>; +impl Db { + fn new() -> Self { + Db { + blockchain: Arc::new(RwLock::new(Vec::new())), + pending_transactions: Arc::new(RwLock::new(HashMap::new())), + } + } +} +/// A transaction between `source` and `target` that moves `amount` #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct Transaction { pub source: String, @@ -34,14 +49,22 @@ pub struct Transaction { pub timestamp: NaiveDateTime, } +/// A block that was proposed with `transaction_list` and `nonce` that made `hash` valid #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct Block { - pub transaction_list: Vec, // [Transaction; N] + pub transaction_list: Vec, // hashes of the transactions (or just "source" for now) pub nonce: i32, pub timestamp: NaiveDateTime, pub hash: String, // future proof'd baby } +// pub struct Ledger { +// // heh. also https://doc.rust-lang.org/std/collections/struct.LinkedList.html says Vec is generally faster +// blockchain: Vec, +// // every proposer can have _one_ pending transaction, a way to enforce this, String is proposer identifier +// pending_transactions: HashMap, +// } + // #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] // #[serde(rename_all = "camelCase")] // pub struct Game { @@ -99,6 +122,8 @@ pub struct Block { // )) // } +// TODO: these tests are amazing, we should write some when schema is decided upon <07-04-21, yigit> // + // #[cfg(test)] // mod tests { // use super::*; -- cgit v1.2.3-70-g09d2