Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A simple mockup of L2 logic without proofs or signatures. #6

Merged
merged 7 commits into from
Jan 29, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,667 changes: 1,667 additions & 0 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ resolver = "2"

members = [
"kairos-cli",
"kairos-server",
]

[workspace.package]
Expand Down
25 changes: 25 additions & 0 deletions kairos-server/Cargo.toml
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The types that flow in/out of server to/from an sdk could be factored out to a dedicated crate ... I began this process in my paradox implementation.

https://github.com/cspr-rad/paradox-4/tree/main/node/types/src/external

Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[package]
name = "kairos-server"
version.workspace = true
edition.workspace = true

[lib]

[[bin]]
name = "kairos-server"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
axum = { version = "0.7", features = ["tracing"]}
thiserror = "1.0"
anyhow = "1.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1.35", features = ["full", "tracing", "macros"] }
tracing = "0.1"
tracing-subscriber = "0.3"

[dev-dependencies]
proptest = "1.4"
axum-test-helper = "0.3"
68 changes: 68 additions & 0 deletions kairos-server/src/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use std::{
fmt,
ops::{Deref, DerefMut},
};

use axum::{
http::StatusCode,
response::{IntoResponse, Response},
};

#[derive(Debug)]
pub struct AppErr {
error: anyhow::Error,
status: Option<StatusCode>,
}

impl AppErr {
pub fn set_status(err: impl Into<Self>, status: StatusCode) -> Self {
let mut err = err.into();
err.status = Some(status);
err
}
}

impl IntoResponse for AppErr {
fn into_response(self) -> Response {
(
self.status.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
format!("{}", self.error),
)
.into_response()
}
}

impl Deref for AppErr {
type Target = anyhow::Error;
fn deref(&self) -> &Self::Target {
&self.error
}
}

impl DerefMut for AppErr {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.error
}
}

impl fmt::Display for AppErr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}: {}",
self.status.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
self.error
)
}
}

impl std::error::Error for AppErr {}

impl From<anyhow::Error> for AppErr {
fn from(error: anyhow::Error) -> Self {
Self {
error,
status: None,
}
}
}
19 changes: 19 additions & 0 deletions kairos-server/src/lib.rs
Avi-D-coder marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
pub mod errors;
pub mod routes;
pub mod state;

use axum::{routing::post, Router};
use routes::*;
use state::LockedBatchState;

pub use errors::AppErr;

type PublicKey = String;

pub fn app_router(state: LockedBatchState) -> Router {
Router::new()
.route("/api/v1/mock/deposit", post(deposit))
.route("/api/v1/mock/withdraw", post(withdraw))
.route("/api/v1/transfer", post(transfer))
.with_state(state)
}
22 changes: 22 additions & 0 deletions kairos-server/src/main.rs
Avi-D-coder marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use std::net::SocketAddr;

use kairos_server::state::BatchState;

#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let axum_port: u16 = std::env::var("SERVER_PORT").map_or(8000, |x| {
Avi-D-coder marked this conversation as resolved.
Show resolved Hide resolved
x.parse().unwrap_or_else(|e| {
format!("Failed to parse SERVER_PORT: {}", e)
.parse()
.unwrap()
})
});

let app = kairos_server::app_router(BatchState::new());

let axum_addr = SocketAddr::from(([127, 0, 0, 1], axum_port));
tracing::info!("starting http server");
let listener = tokio::net::TcpListener::bind(axum_addr).await.unwrap();
axum::serve(listener, app).await.unwrap();
}
41 changes: 41 additions & 0 deletions kairos-server/src/routes/deposit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use std::ops::Deref;

use anyhow::anyhow;
use axum::{extract::State, http::StatusCode, Json};
use serde::{Deserialize, Serialize};

use crate::{state::LockedBatchState, AppErr, PublicKey};

#[derive(Serialize, Deserialize)]
pub struct Deposit {
pub public_key: PublicKey,
pub amount: u64,
}

pub async fn deposit(
state: State<LockedBatchState>,
Json(Deposit { public_key, amount }): Json<Deposit>,
) -> Result<(), AppErr> {
tracing::info!("TODO: verifying deposit");

tracing::info!("TODO: adding deposit to batch");

let mut state = state.deref().write().await;
let account = state.balances.entry(public_key.clone());

let prior_balance = account.or_insert(0);
let updated_balance = prior_balance.checked_add(amount).ok_or_else(|| {
AppErr::set_status(
anyhow!("deposit would overflow account"),
StatusCode::CONFLICT,
)
})?;

tracing::info!(
"Updated account public_key={} balance={}",
public_key,
updated_balance
);

Ok(())
}
7 changes: 7 additions & 0 deletions kairos-server/src/routes/mod.rs
Avi-D-coder marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
pub mod deposit;
pub mod transfer;
pub mod withdraw;

pub use deposit::deposit;
pub use transfer::transfer;
pub use withdraw::withdraw;
108 changes: 108 additions & 0 deletions kairos-server/src/routes/transfer.rs
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

General comment in respect of the handlers. Typically they should be a thin layer, the business logic should be handled by an inner layer that can be tested outside of the network context. But it's early days.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another general comment in respect of the handlers. The input/output request/response types could be wrapped in generic ServerRequest & ServerResponse types. These types would provide a contextual envelope.

For an example of what I mean, check out the core external types exposed in my paradox implementation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another general comment in respect of the handlers. The input/output request/response types could be wrapped in generic ServerRequest & ServerResponse types. These types would provide a contextual envelope.

I'd suggest using a middle layer there.

https://docs.rs/axum/latest/axum/middleware/index.html

Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
use anyhow::anyhow;
use axum::{extract::State, http::StatusCode, Json};
use serde::{Deserialize, Serialize};

use crate::{state::LockedBatchState, AppErr, PublicKey};

#[derive(Serialize, Deserialize)]
pub struct Transfer {
pub from: PublicKey,
pub to: PublicKey,
pub amount: u64,
}

#[derive(Serialize, Deserialize)]
pub struct TransferRequest {
transfer: Transfer,
signature: String,
}

pub async fn transfer(
State(state): State<LockedBatchState>,
Json(TransferRequest {
transfer,
signature: _,
}): Json<TransferRequest>,
) -> Result<(), AppErr> {
if transfer.amount == 0 {
return Err(AppErr::set_status(
anyhow!("transfer amount must be greater than 0"),
StatusCode::BAD_REQUEST,
));
}

tracing::info!("TODO: verifying transfer signature");

// We pre-check this read-only to error early without acquiring the write lock.
// This prevents a DoS attack exploiting the write lock.
tracing::info!("verifying transfer sender has sufficient funds");
check_sender_funds(&state, &transfer).await?;

let mut state = state.write().await;
let from_balance = state.balances.get_mut(&transfer.from).ok_or_else(|| {
AppErr::set_status(
anyhow!(
"Sender no longer has an account.
The sender just removed all their funds."
),
StatusCode::CONFLICT,
)
})?;

*from_balance = from_balance.checked_sub(transfer.amount).ok_or_else(|| {
AppErr::set_status(
anyhow!(
"Sender no longer has sufficient funds, balance={}, transfer_amount={}.
The sender just moved their funds in a concurrent request",
from_balance,
transfer.amount
),
StatusCode::CONFLICT,
)
})?;

let to_balance = state
.balances
.entry(transfer.to.clone())
.or_insert_with(|| {
tracing::info!("creating new account for receiver");
0
});

*to_balance = to_balance.checked_add(transfer.amount).ok_or_else(|| {
AppErr::set_status(anyhow!("Receiver balance overflow"), StatusCode::CONFLICT)
})?;

Ok(())
}

async fn check_sender_funds(state: &LockedBatchState, transfer: &Transfer) -> Result<(), AppErr> {
let state = state.read().await;
let from_balance = state.balances.get(&transfer.from).ok_or_else(|| {
AppErr::set_status(
anyhow!("Sender does not have an account"),
StatusCode::BAD_REQUEST,
)
})?;

from_balance.checked_sub(transfer.amount).ok_or_else(|| {
AppErr::set_status(
anyhow!(
"Sender does not have sufficient funds, balance={}, transfer_amount={}",
from_balance,
transfer.amount
),
StatusCode::FORBIDDEN,
)
})?;

let to_balance = state.balances.get(&transfer.to).unwrap_or(&0);
if to_balance.checked_add(transfer.amount).is_none() {
return Err(AppErr::set_status(
anyhow!("Receiver balance overflow"),
StatusCode::CONFLICT,
));
}

Ok(())
}
87 changes: 87 additions & 0 deletions kairos-server/src/routes/withdraw.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
use anyhow::anyhow;
use axum::{extract::State, http::StatusCode, Json};
use serde::{Deserialize, Serialize};

use crate::{state::LockedBatchState, AppErr, PublicKey};

#[derive(Serialize, Deserialize)]
pub struct Withdrawal {
pub public_key: PublicKey,
pub signature: String,
pub amount: u64,
}

pub async fn withdraw(
State(state): State<LockedBatchState>,
Json(withdrawal): Json<Withdrawal>,
) -> Result<(), AppErr> {
tracing::info!("TODO: verifying withdrawal signature");

tracing::info!("verifying withdrawal sender has sufficient funds");
check_sender_funds(&state, &withdrawal).await?;

tracing::info!("TODO: adding withdrawal to batch");

let mut state = state.write().await;
let from_balance = state
.balances
.get_mut(&withdrawal.public_key)
.ok_or_else(|| {
AppErr::set_status(
anyhow!(
"Sender no longer has an account.
The sender just removed all their funds."
),
StatusCode::CONFLICT,
)
})?;

let updated_balance = from_balance.checked_sub(withdrawal.amount).ok_or_else(|| {
AppErr::set_status(
anyhow!(
"Sender no longer has sufficient funds, balance={}, withdrawal_amount={}.
The sender just moved their funds in a concurrent request",
from_balance,
withdrawal.amount
),
StatusCode::CONFLICT,
)
})?;

*from_balance = updated_balance;

if updated_balance == 0 {
state.balances.remove(&withdrawal.public_key);
}

tracing::info!(
"Updated account public_key={} balance={}",
withdrawal.public_key,
updated_balance
);

Ok(())
}

async fn check_sender_funds(
state: &LockedBatchState,
withdrawal: &Withdrawal,
) -> Result<(), AppErr> {
let state = state.read().await;
let from_balance = state.balances.get(&withdrawal.public_key).ok_or_else(|| {
AppErr::set_status(anyhow!("Withdrawer has no account."), StatusCode::CONFLICT)
})?;

if *from_balance < withdrawal.amount {
return Err(AppErr::set_status(
anyhow!(
"Withdrawer has insufficient funds, balance={}, withdrawal_amount={}.",
from_balance,
withdrawal.amount
),
StatusCode::FORBIDDEN,
));
}

Ok(())
}
Loading
Loading