From 5a85152ea089ce9e9ac0e4e5a70739f488c8be93 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Mon, 5 Feb 2024 16:45:38 +0000 Subject: [PATCH] shielded expedition changes --- src/app.rs | 10 +++--- src/config.rs | 21 ++++++++++-- src/dto/faucet.rs | 4 +++ src/error/faucet.rs | 12 +++++++ src/handler/faucet.rs | 74 +++++++++++++++++++++++++++++++++++++++--- src/services/faucet.rs | 29 +++++++++++++---- src/state/faucet.rs | 21 +++++++++--- 7 files changed, 148 insertions(+), 23 deletions(-) diff --git a/src/app.rs b/src/app.rs index a45f330..89d5743 100644 --- a/src/app.rs +++ b/src/app.rs @@ -55,12 +55,13 @@ impl ApplicationServer { assert!(auth_key.len() == 32); - let difficulty = config.difficulty; let rps = config.rps; let chain_id = config.chain_id.clone(); let rpc = config.rpc.clone(); let chain_start = config.chain_start; - let withdraw_limit = config.withdraw_limit.unwrap_or(1000_u64); + let withdraw_limit = config.withdraw_limit.unwrap_or(1_000_000_000_u64); + let webserver_host = config.webserver_host.clone(); + let request_frequency = config.request_frequency; let sk = config.private_key.clone(); let sk = sk_from_str(&sk); @@ -113,15 +114,16 @@ impl ApplicationServer { address, sdk, auth_key, - difficulty, chain_id, chain_start, withdraw_limit, + webserver_host, + request_frequency, ); Router::new() .route("/faucet/setting", get(faucet_handler::faucet_settings)) - .route("/faucet", get(faucet_handler::request_challenge)) + .route("/faucet/challenge/:player_id", get(faucet_handler::request_challenge)) .route("/faucet", post(faucet_handler::request_transfer)) .with_state(faucet_state) .merge(Router::new().route( diff --git a/src/config.rs b/src/config.rs index 4319d65..eba1c9f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -9,30 +9,45 @@ pub struct AppConfig { #[clap(long, env, value_enum)] pub cargo_env: CargoEnv, + /// Port to bind the crawler's HTTP server to #[clap(long, env, default_value = "5000")] pub port: u16, - #[clap(long, env)] - pub difficulty: u64, - + /// Faucet's private key in Namada #[clap(long, env)] pub private_key: String, #[clap(long, env)] pub chain_start: i64, + /// Chain id of Namada #[clap(long, env)] pub chain_id: String, + /// URL of the Namada RPC #[clap(long, env)] pub rpc: String, + /// Withdraw limit given in base units of NAAN #[clap(long, env)] pub withdraw_limit: Option, + /// Authentication key for faucet challenges #[clap(long, env)] pub auth_key: Option, + /// Max number of requests per second #[clap(long, env)] pub rps: Option, + + /// URL of the Shielded Expedition's webserver + #[clap(long, env)] + pub webserver_host: String, + + /// User request frequency given in seconds + /// + /// If more than one request is performed during this + /// interval, the faucet denies the request + #[clap(long, env)] + pub request_frequency: u64, } diff --git a/src/dto/faucet.rs b/src/dto/faucet.rs index 31d1fbd..7f8ba53 100644 --- a/src/dto/faucet.rs +++ b/src/dto/faucet.rs @@ -15,6 +15,10 @@ pub struct FaucetRequestDto { #[validate(length(equal = 64, message = "Invalid proof"))] pub tag: String, pub transfer: Transfer, + #[validate(length(max = 256, message = "Invalid player id"))] + pub player_id: String, + #[validate(length(max = 256, message = "Invalid challenge signature"))] + pub challenge_signature: String, } #[derive(Clone, Serialize, Deserialize, Validate)] diff --git a/src/error/faucet.rs b/src/error/faucet.rs index a2937c9..0db6b6f 100644 --- a/src/error/faucet.rs +++ b/src/error/faucet.rs @@ -15,6 +15,10 @@ pub enum FaucetError { DuplicateChallenge, #[error("Invalid Address")] InvalidAddress, + #[error("Invalid public key")] + InvalidPublicKey, + #[error("Invalid signature")] + InvalidSignature, #[error("Chain didn't start yet")] ChainNotStarted, #[error("Faucet out of balance")] @@ -23,6 +27,10 @@ pub enum FaucetError { SdkError(String), #[error("Withdraw limit must be less then {0}")] InvalidWithdrawLimit(u64), + #[error("Public key {0} does not belong to a shielded expedition player")] + NotPlayer(String), + #[error("Slow down, space cowboy")] + TooManyRequests, } impl IntoResponse for FaucetError { @@ -36,6 +44,10 @@ impl IntoResponse for FaucetError { FaucetError::InvalidWithdrawLimit(_) => StatusCode::BAD_REQUEST, FaucetError::FaucetOutOfBalance => StatusCode::CONFLICT, FaucetError::SdkError(_) => StatusCode::BAD_REQUEST, + FaucetError::NotPlayer(_) => StatusCode::BAD_REQUEST, + FaucetError::TooManyRequests => StatusCode::BAD_REQUEST, + FaucetError::InvalidPublicKey => StatusCode::BAD_REQUEST, + FaucetError::InvalidSignature => StatusCode::BAD_REQUEST, }; ApiErrorResponse::send(status_code.as_u16(), Some(self.to_string())) diff --git a/src/handler/faucet.rs b/src/handler/faucet.rs index 2d47ad8..67b3b63 100644 --- a/src/handler/faucet.rs +++ b/src/handler/faucet.rs @@ -1,7 +1,10 @@ use std::collections::HashMap; +use std::time::Instant; +use axum::extract::Path; use axum::{extract::State, Json}; use axum_macros::debug_handler; +use namada_sdk::types::string_encoding::Format; use namada_sdk::{ args::InputAmount, rpc, @@ -10,6 +13,7 @@ use namada_sdk::{ tx::data::ResultCode, types::{ address::Address, + key::{common, SigScheme}, masp::{TransferSource, TransferTarget}, }, Namada, @@ -45,10 +49,39 @@ pub async fn faucet_settings( pub async fn request_challenge( State(mut state): State, + Path(player_id): Path, ) -> Result, ApiError> { + let is_player = match reqwest::get(format!( + "https://{}/api/v1/player/exists/{}", + state.webserver_host, player_id + )) + .await + .map(|response| response.status().is_success()) + { + Ok(is_success) if is_success => true, + _ => false, + }; + if !is_player { + return Err(FaucetError::NotPlayer(player_id).into()); + } + + let now = Instant::now(); + let too_many_requests = 'result: { + let Some(last_request_instant) = state.last_requests.get(&player_id) else { + break 'result false; + }; + let elapsed_request_time = now.duration_since(*last_request_instant); + elapsed_request_time <= state.request_frequency + }; + + if too_many_requests { + return Err(FaucetError::TooManyRequests.into()); + } + state.last_requests.insert(player_id.clone(), now); + let faucet_request = state .faucet_service - .generate_faucet_request(state.auth_key) + .generate_faucet_request(state.auth_key, player_id) .await?; let response = FaucetResponseDto::from(faucet_request); @@ -66,6 +99,34 @@ pub async fn request_transfer( return Err(FaucetError::InvalidWithdrawLimit(state.withdraw_limit).into()); } + let player_id_pk: common::PublicKey = if let Ok(pk) = payload.player_id.parse() { + pk + } else { + return Err(FaucetError::InvalidPublicKey.into()); + }; + + let challenge_signature = if let Ok(hex_decoded_sig) = hex::decode(payload.challenge_signature) + { + if let Ok(sig) = common::Signature::decode_bytes(&hex_decoded_sig) { + sig + } else { + return Err(FaucetError::InvalidSignature.into()); + } + } else { + return Err(FaucetError::InvalidSignature.into()); + }; + + if common::SigScheme::verify_signature( + &player_id_pk, + // NOTE: signing over the hex encoded challenge data + &payload.challenge.as_bytes(), + &challenge_signature, + ) + .is_err() + { + return Err(FaucetError::InvalidSignature.into()); + } + let token_address = Address::decode(payload.transfer.token.clone()); let token_address = if let Ok(address) = token_address { address @@ -82,10 +143,13 @@ pub async fn request_transfer( if state.faucet_repo.contains(&payload.challenge).await { return Err(FaucetError::DuplicateChallenge.into()); } - let is_valid_proof = - state - .faucet_service - .verify_tag(&auth_key, &payload.challenge, &payload.tag); + + let is_valid_proof = state.faucet_service.verify_tag( + &auth_key, + &payload.challenge, + &payload.player_id, + &payload.tag, + ); if !is_valid_proof { return Err(FaucetError::InvalidProof.into()); } diff --git a/src/services/faucet.rs b/src/services/faucet.rs index 9ddbaca..f0e343d 100644 --- a/src/services/faucet.rs +++ b/src/services/faucet.rs @@ -28,22 +28,34 @@ impl FaucetService { } } - pub async fn generate_faucet_request(&mut self, auth_key: String) -> Result { + pub async fn generate_faucet_request( + &mut self, + auth_key: String, + player_id: String, + ) -> Result { let challenge = self.r.generate(); - let tag = self.compute_tag(&auth_key, &challenge); + let tag = self.compute_tag(&auth_key, &challenge, player_id.as_bytes()); Ok(Faucet::request(challenge, tag)) } - fn compute_tag(&self, auth_key: &String, challenge: &[u8]) -> Vec { + fn compute_tag(&self, auth_key: &String, challenge: &[u8], player_id: &[u8]) -> Vec { let key = auth::SecretKey::from_slice(auth_key.as_bytes()) .expect("Should be able to convert key to bytes"); - let tag = auth::authenticate(&key, challenge).expect("Should be able to compute tag"); + let challenge_and_player_id: Vec<_> = [challenge, player_id].concat(); + let tag = auth::authenticate(&key, &challenge_and_player_id) + .expect("Should be able to compute tag"); tag.unprotected_as_bytes().to_vec() } - pub fn verify_tag(&self, auth_key: &String, challenge: &String, tag: &String) -> bool { + pub fn verify_tag( + &self, + auth_key: &String, + challenge: &String, + player_id: &String, + tag: &String, + ) -> bool { let key = auth::SecretKey::from_slice(auth_key.as_bytes()) .expect("Should be able to convert key to bytes"); @@ -62,9 +74,12 @@ impl FaucetService { let tag = Tag::from_slice(&decoded_tag).expect("Should be able to convert bytes to tag"); - let decoded_challenge = HEXLOWER.decode(challenge.as_bytes()).expect("Test"); + let Ok(decoded_challenge) = HEXLOWER.decode(challenge.as_bytes()) else { + return false; + }; + let challenge_and_player_id = [&decoded_challenge[..], player_id.as_bytes()].concat(); - auth::authenticate_verify(&tag, &key, &decoded_challenge).is_ok() + auth::authenticate_verify(&tag, &key, &challenge_and_player_id).is_ok() } pub fn verify_pow(&self, challenge: &String, solution: &String, difficulty: u64) -> bool { diff --git a/src/state/faucet.rs b/src/state/faucet.rs index 16ba7e9..d3d79d7 100644 --- a/src/state/faucet.rs +++ b/src/state/faucet.rs @@ -1,8 +1,11 @@ +use std::collections::HashMap; +use std::sync::Arc; +use std::time::{Duration, Instant}; + use crate::{ app_state::AppState, repository::faucet::FaucetRepository, repository::faucet::FaucetRepositoryTrait, services::faucet::FaucetService, }; -use std::sync::Arc; use tokio::sync::RwLock; use namada_sdk::{ @@ -11,6 +14,8 @@ use namada_sdk::{ }; use tendermint_rpc::HttpClient; +type PlayerId = String; + #[derive(Clone)] pub struct FaucetState { pub faucet_service: FaucetService, @@ -22,18 +27,23 @@ pub struct FaucetState { pub chain_id: String, pub chain_start: i64, pub withdraw_limit: u64, + pub request_frequency: Duration, + pub last_requests: HashMap, + pub webserver_host: String, } impl FaucetState { + #[allow(clippy::too_many_arguments)] pub fn new( data: &Arc>, faucet_address: Address, sdk: NamadaImpl, auth_key: String, - difficulty: u64, chain_id: String, chain_start: i64, withdraw_limit: u64, + webserver_host: String, + request_frequency: u64, ) -> Self { Self { faucet_service: FaucetService::new(data), @@ -41,10 +51,13 @@ impl FaucetState { faucet_address, sdk: Arc::new(sdk), auth_key, - difficulty, + difficulty: 0, chain_id, chain_start, - withdraw_limit: withdraw_limit * 10_u64.pow(6), + withdraw_limit, + webserver_host, + request_frequency: Duration::from_secs(request_frequency), + last_requests: HashMap::new(), } } }