diff options
| author | Yigit Sever | 2021-04-07 04:33:45 +0300 |
|---|---|---|
| committer | Yigit Sever | 2021-04-07 04:35:44 +0300 |
| commit | 84d9c14a17e864058527981e3388cef148827c11 (patch) | |
| tree | 19c7b4b4d67d2ba2d3d87ba7eca009ff997e0cd0 /src | |
| parent | 95ff6371303ac28d05b25fd9f6e436c5d0a58d4c (diff) | |
| download | gradecoin-84d9c14a17e864058527981e3388cef148827c11.tar.gz gradecoin-84d9c14a17e864058527981e3388cef148827c11.tar.bz2 gradecoin-84d9c14a17e864058527981e3388cef148827c11.zip | |
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
Diffstat (limited to 'src')
| -rw-r--r-- | src/custom_filters.rs | 11 | ||||
| -rw-r--r-- | src/handlers.rs | 94 | ||||
| -rw-r--r-- | src/main.rs | 6 | ||||
| -rw-r--r-- | src/routes.rs | 30 | ||||
| -rw-r--r-- | src/schema.rs | 65 |
5 files changed, 152 insertions, 54 deletions
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 @@ | |||
| 3 | use std::convert::Infallible; | 3 | use std::convert::Infallible; |
| 4 | use warp::{Filter, Rejection}; | 4 | use warp::{Filter, Rejection}; |
| 5 | 5 | ||
| 6 | use crate::schema::{Db, Transaction}; // `Block` coming later | 6 | use crate::schema::{Block, Db, Transaction}; |
| 7 | 7 | ||
| 8 | // Database context for routes | 8 | // Database context for routes |
| 9 | pub fn with_db(db: Db) -> impl Filter<Extract = (Db,), Error = Infallible> + Clone { | 9 | pub fn with_db(db: Db) -> impl Filter<Extract = (Db,), Error = Infallible> + Clone { |
| @@ -15,7 +15,12 @@ pub fn with_db(db: Db) -> impl Filter<Extract = (Db,), Error = Infallible> + Clo | |||
| 15 | // warp::query::<ListOptions>() | 15 | // warp::query::<ListOptions>() |
| 16 | // } | 16 | // } |
| 17 | 17 | ||
| 18 | // Accept only JSON body and reject big payloads | 18 | // Accept only json encoded Transaction body and reject big payloads |
| 19 | pub fn json_body() -> impl Filter<Extract = (Transaction,), Error = Rejection> + Clone { | 19 | pub fn transaction_json_body() -> impl Filter<Extract = (Transaction,), Error = Rejection> + Clone { |
| 20 | warp::body::content_length_limit(1024 * 32).and(warp::body::json()) | ||
| 21 | } | ||
| 22 | |||
| 23 | // Accept only json encoded Transaction body and reject big payloads | ||
| 24 | pub fn block_json_body() -> impl Filter<Extract = (Block,), Error = Rejection> + Clone { | ||
| 20 | warp::body::content_length_limit(1024 * 32).and(warp::body::json()) | 25 | warp::body::content_length_limit(1024 * 32).and(warp::body::json()) |
| 21 | } | 26 | } |
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 @@ | |||
| 1 | // API handlers, the ends of each filter chain | 1 | /// API handlers, the ends of each filter chain |
| 2 | 2 | use log::debug; // this is more useful than debug! learn how to use this | |
| 3 | use log::debug; | 3 | use parking_lot::RwLockUpgradableReadGuard; |
| 4 | use std::convert::Infallible; | 4 | use std::convert::Infallible; |
| 5 | use warp::{http::StatusCode, reply}; | 5 | use warp::{http::StatusCode, reply}; |
| 6 | 6 | ||
| 7 | use crate::schema::{Db, Transaction}; // `Block` coming later | 7 | use crate::schema::{Block, Db, Transaction}; |
| 8 | |||
| 9 | /// GET /transaction | ||
| 10 | /// Returns JSON array of transactions | ||
| 11 | /// Cannot fail | ||
| 12 | pub async fn list_transactions(db: Db) -> Result<impl warp::Reply, Infallible> { | ||
| 13 | debug!("list all transactions"); | ||
| 14 | let mut result = Vec::new(); | ||
| 15 | |||
| 16 | let transactions = db.pending_transactions.read(); | ||
| 17 | // let transactions = transactions.clone().into_iter().collect(); | ||
| 18 | |||
| 19 | for (_, value) in transactions.iter() { | ||
| 20 | result.push(value) | ||
| 21 | } | ||
| 22 | |||
| 23 | Ok(reply::with_status(reply::json(&result), StatusCode::OK)) | ||
| 24 | } | ||
| 25 | |||
| 26 | /// GET /block | ||
| 27 | /// Returns JSON array of blocks | ||
| 28 | /// Cannot fail | ||
| 29 | /// Mostly around for debug purposes | ||
| 30 | pub async fn list_blocks(db: Db) -> Result<impl warp::Reply, Infallible> { | ||
| 31 | debug!("list all blocks"); | ||
| 32 | |||
| 33 | let mut result = Vec::new(); | ||
| 34 | let blocks = db.blockchain.read(); | ||
| 35 | |||
| 36 | for block in blocks.iter() { | ||
| 37 | result.push(block); | ||
| 38 | } | ||
| 8 | 39 | ||
| 9 | // PROPOSE Transaction | 40 | Ok(reply::with_status(reply::json(&result), StatusCode::OK)) |
| 10 | // POST /transaction | 41 | } |
| 42 | |||
| 43 | /// POST /transaction | ||
| 44 | /// Pushes a new transaction for pending transaction pool | ||
| 45 | /// Can reject the transaction proposal | ||
| 46 | /// TODO: when is a new transaction rejected <07-04-21, yigit> // | ||
| 11 | pub async fn propose_transaction( | 47 | pub async fn propose_transaction( |
| 12 | new_transaction: Transaction, | 48 | new_transaction: Transaction, |
| 13 | db: Db, | 49 | db: Db, |
| 14 | ) -> Result<impl warp::Reply, warp::Rejection> { | 50 | ) -> Result<impl warp::Reply, warp::Rejection> { |
| 15 | debug!("new transaction request {:?}", new_transaction); | 51 | debug!("new transaction request {:?}", new_transaction); |
| 16 | 52 | ||
| 17 | let mut transactions = db.lock().await; | 53 | // let mut transactions = db.lock().await; |
| 54 | let mut transactions = db.pending_transactions.write(); | ||
| 18 | 55 | ||
| 19 | transactions.push(new_transaction); | 56 | transactions.insert(new_transaction.source.to_owned(), new_transaction); |
| 20 | 57 | ||
| 21 | Ok(StatusCode::CREATED) | 58 | Ok(StatusCode::CREATED) |
| 22 | } | 59 | } |
| 23 | 60 | ||
| 24 | // GET Transaction List | 61 | /// POST /block |
| 25 | // GET /transaction | 62 | /// Proposes a new block for the next round |
| 26 | // Returns JSON array of transactions | 63 | /// Can reject the block |
| 27 | // Cannot fail? | 64 | pub async fn propose_block(new_block: Block, db: Db) -> Result<impl warp::Reply, warp::Rejection> { |
| 28 | pub async fn list_transactions(db: Db) -> Result<impl warp::Reply, Infallible> { | 65 | debug!("new block request {:?}", new_block); |
| 29 | debug!("list all transactions"); | ||
| 30 | 66 | ||
| 31 | let transactions = db.lock().await; | 67 | // https://blog.logrocket.com/create-an-async-crud-web-service-in-rust-with-warp/ (this has |
| 68 | // error.rs, error struct, looks very clean) | ||
| 32 | 69 | ||
| 33 | let transactions: Vec<Transaction> = transactions.clone().into_iter().collect(); | 70 | let pending_transactions = db.pending_transactions.upgradable_read(); |
| 71 | let blockchain = db.blockchain.upgradable_read(); | ||
| 34 | 72 | ||
| 35 | Ok(reply::with_status( | 73 | // TODO: check 1, new_block.transaction_list from pending_transactions pool? <07-04-21, yigit> // |
| 36 | reply::json(&transactions), | 74 | for transaction_hash in new_block.transaction_list.iter() { |
| 37 | StatusCode::OK, | 75 | if !pending_transactions.contains_key(transaction_hash) { |
| 38 | )) | 76 | return Ok(StatusCode::BAD_REQUEST); |
| 39 | } | 77 | } |
| 78 | } | ||
| 40 | 79 | ||
| 41 | // PROPOSE Block | 80 | // TODO: check 2, block hash (\w nonce) asserts $hash_condition? <07-04-21, yigit> // |
| 42 | // POST /block | 81 | // assume it is for now |
| 82 | |||
| 83 | let mut blockchain = RwLockUpgradableReadGuard::upgrade(blockchain); | ||
| 84 | blockchain.push(new_block); | ||
| 85 | |||
| 86 | let mut pending_transactions = RwLockUpgradableReadGuard::upgrade(pending_transactions); | ||
| 87 | pending_transactions.clear(); | ||
| 88 | |||
| 89 | Ok(StatusCode::CREATED) | ||
| 90 | } | ||
| 43 | 91 | ||
| 44 | // `GET /games` | 92 | // `GET /games` |
| 45 | // Returns JSON array of todos | 93 | // 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; | |||
| 11 | async fn main() { | 11 | async fn main() { |
| 12 | // Show debug logs by default by setting `RUST_LOG=restful_rust=debug` | 12 | // Show debug logs by default by setting `RUST_LOG=restful_rust=debug` |
| 13 | if env::var_os("RUST_LOG").is_none() { | 13 | if env::var_os("RUST_LOG").is_none() { |
| 14 | env::set_var("RUST_LOG", "restful_rust=debug"); | 14 | env::set_var("RUST_LOG", "gradecoin=debug"); |
| 15 | } | 15 | } |
| 16 | pretty_env_logger::init(); | 16 | pretty_env_logger::init(); |
| 17 | 17 | ||
| 18 | let db = schema::ledger(); // 1. we need this to return a _simple_ db | 18 | let db = schema::create_database(); |
| 19 | 19 | ||
| 20 | let api = routes::consensus_routes(db); | 20 | let api = routes::consensus_routes(db); |
| 21 | 21 | ||
| 22 | let routes = api.with(warp::log("restful_rust")); | 22 | let routes = api.with(warp::log("gradecoin")); |
| 23 | 23 | ||
| 24 | // Start the server | 24 | // Start the server |
| 25 | warp::serve(routes).run(([127, 0, 0, 1], 8080)).await; | 25 | 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; | |||
| 4 | use crate::handlers; | 4 | use crate::handlers; |
| 5 | use crate::schema::Db; | 5 | use crate::schema::Db; |
| 6 | 6 | ||
| 7 | // Root, all routes combined | 7 | /// Root, all routes combined |
| 8 | pub fn consensus_routes(db: Db) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone { | 8 | pub fn consensus_routes(db: Db) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone { |
| 9 | transaction_list(db.clone()).or(transaction_propose(db.clone())) | 9 | transaction_list(db.clone()) |
| 10 | .or(transaction_propose(db.clone())) | ||
| 11 | .or(block_propose(db.clone())) | ||
| 12 | .or(block_list(db.clone())) | ||
| 10 | } | 13 | } |
| 11 | 14 | ||
| 12 | // GET /transaction | 15 | /// GET /transaction |
| 13 | pub fn transaction_list(db: Db) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone { | 16 | pub fn transaction_list(db: Db) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone { |
| 14 | warp::path!("transaction") | 17 | warp::path!("transaction") |
| 15 | .and(warp::get()) | 18 | .and(warp::get()) |
| @@ -17,15 +20,32 @@ pub fn transaction_list(db: Db) -> impl Filter<Extract = impl Reply, Error = Rej | |||
| 17 | .and_then(handlers::list_transactions) | 20 | .and_then(handlers::list_transactions) |
| 18 | } | 21 | } |
| 19 | 22 | ||
| 20 | // POST /transaction | 23 | /// GET /block |
| 24 | pub fn block_list(db: Db) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone { | ||
| 25 | warp::path!("block") | ||
| 26 | .and(warp::get()) | ||
| 27 | .and(custom_filters::with_db(db)) | ||
| 28 | .and_then(handlers::list_blocks) | ||
| 29 | } | ||
| 30 | |||
| 31 | /// POST /transaction | ||
| 21 | pub fn transaction_propose(db: Db) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone { | 32 | pub fn transaction_propose(db: Db) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone { |
| 22 | warp::path!("transaction") | 33 | warp::path!("transaction") |
| 23 | .and(warp::post()) | 34 | .and(warp::post()) |
| 24 | .and(custom_filters::json_body()) | 35 | .and(custom_filters::transaction_json_body()) |
| 25 | .and(custom_filters::with_db(db)) | 36 | .and(custom_filters::with_db(db)) |
| 26 | .and_then(handlers::propose_transaction) | 37 | .and_then(handlers::propose_transaction) |
| 27 | } | 38 | } |
| 28 | 39 | ||
| 40 | /// POST /block | ||
| 41 | pub fn block_propose(db: Db) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone { | ||
| 42 | warp::path!("block") | ||
| 43 | .and(warp::post()) | ||
| 44 | .and(custom_filters::block_json_body()) | ||
| 45 | .and(custom_filters::with_db(db)) | ||
| 46 | .and_then(handlers::propose_block) | ||
| 47 | } | ||
| 48 | |||
| 29 | /////////////////////////// | 49 | /////////////////////////// |
| 30 | // below are not mine. // | 50 | // below are not mine. // |
| 31 | /////////////////////////// | 51 | /////////////////////////// |
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 @@ | |||
| 1 | // Common types used across API | 1 | use chrono::NaiveDateTime; |
| 2 | 2 | use parking_lot::RwLock; | |
| 3 | use chrono::{NaiveDate, NaiveDateTime}; | ||
| 4 | use serde::{Deserialize, Serialize}; | 3 | use serde::{Deserialize, Serialize}; |
| 4 | use std::collections::HashMap; | ||
| 5 | use std::sync::Arc; | 5 | use std::sync::Arc; |
| 6 | use tokio::sync::Mutex; | ||
| 7 | 6 | ||
| 8 | // use crate::validators; | 7 | // use crate::validators; |
| 9 | 8 | ||
| 10 | pub fn ledger() -> Db { | 9 | // In memory data structure |
| 11 | // TODO: there was something simpler in one of the other tutorials? <07-04-21, yigit> // | 10 | |
| 12 | 11 | // Two approaches here | |
| 13 | Arc::new(Mutex::new(vec![ | 12 | // 1. Db is a type |
| 14 | Transaction { | 13 | // pub type Db = Arc<RwLock<Vec<Ledger>>>; |
| 15 | source: String::from("Myself"), | 14 | // Ledger is a struct, we wrap the ledger with arc + mutex in ledger() |
| 16 | target: String::from("Nobody"), | 15 | // to access transactions we need to unwrap blocks as well, vice versa |
| 17 | amount: 4, | 16 | // |
| 18 | timestamp: NaiveDate::from_ymd(2021, 4, 7).and_hms(00, 17, 00), | 17 | // 2. Db is a struct, attributes are wrapped |
| 19 | }, | 18 | // we can offload ::new() to it's member method |
| 20 | ])) | 19 | // blocks and transactions are accessible separately, which is the biggest pro |
| 20 | |||
| 21 | /// Creates a new database | ||
| 22 | pub fn create_database() -> Db { | ||
| 23 | Db::new() | ||
| 21 | } | 24 | } |
| 22 | 25 | ||
| 26 | #[derive(Debug, Clone)] | ||
| 27 | pub struct Db { | ||
| 28 | // heh. also https://doc.rust-lang.org/std/collections/struct.LinkedList.html says Vec is generally faster | ||
| 29 | pub blockchain: Arc<RwLock<Vec<Block>>>, | ||
| 30 | // every proposer can have _one_ pending transaction, a way to enforce this, String is proposer identifier | ||
| 31 | pub pending_transactions: Arc<RwLock<HashMap<String, Transaction>>>, | ||
| 32 | } | ||
| 23 | 33 | ||
| 24 | // For presentation purposes keep mocked data in in-memory structure | 34 | impl Db { |
| 25 | // In real life scenario connection with regular database would be established | 35 | fn new() -> Self { |
| 26 | 36 | Db { | |
| 27 | pub type Db = Arc<Mutex<Vec<Transaction>>>; | 37 | blockchain: Arc::new(RwLock::new(Vec::new())), |
| 38 | pending_transactions: Arc::new(RwLock::new(HashMap::new())), | ||
| 39 | } | ||
| 40 | } | ||
| 41 | } | ||
| 28 | 42 | ||
| 43 | /// A transaction between `source` and `target` that moves `amount` | ||
| 29 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] | 44 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] |
| 30 | pub struct Transaction { | 45 | pub struct Transaction { |
| 31 | pub source: String, | 46 | pub source: String, |
| @@ -34,14 +49,22 @@ pub struct Transaction { | |||
| 34 | pub timestamp: NaiveDateTime, | 49 | pub timestamp: NaiveDateTime, |
| 35 | } | 50 | } |
| 36 | 51 | ||
| 52 | /// A block that was proposed with `transaction_list` and `nonce` that made `hash` valid | ||
| 37 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] | 53 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] |
| 38 | pub struct Block { | 54 | pub struct Block { |
| 39 | pub transaction_list: Vec<Transaction>, // [Transaction; N] | 55 | pub transaction_list: Vec<String>, // hashes of the transactions (or just "source" for now) |
| 40 | pub nonce: i32, | 56 | pub nonce: i32, |
| 41 | pub timestamp: NaiveDateTime, | 57 | pub timestamp: NaiveDateTime, |
| 42 | pub hash: String, // future proof'd baby | 58 | pub hash: String, // future proof'd baby |
| 43 | } | 59 | } |
| 44 | 60 | ||
| 61 | // pub struct Ledger { | ||
| 62 | // // heh. also https://doc.rust-lang.org/std/collections/struct.LinkedList.html says Vec is generally faster | ||
| 63 | // blockchain: Vec<Block>, | ||
| 64 | // // every proposer can have _one_ pending transaction, a way to enforce this, String is proposer identifier | ||
| 65 | // pending_transactions: HashMap<String, Transaction>, | ||
| 66 | // } | ||
| 67 | |||
| 45 | // #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] | 68 | // #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] |
| 46 | // #[serde(rename_all = "camelCase")] | 69 | // #[serde(rename_all = "camelCase")] |
| 47 | // pub struct Game { | 70 | // pub struct Game { |
| @@ -99,6 +122,8 @@ pub struct Block { | |||
| 99 | // )) | 122 | // )) |
| 100 | // } | 123 | // } |
| 101 | 124 | ||
| 125 | // TODO: these tests are amazing, we should write some when schema is decided upon <07-04-21, yigit> // | ||
| 126 | |||
| 102 | // #[cfg(test)] | 127 | // #[cfg(test)] |
| 103 | // mod tests { | 128 | // mod tests { |
| 104 | // use super::*; | 129 | // use super::*; |
