Skip to content

Commit

Permalink
Merge pull request #13 from heliaxdev/se-changes
Browse files Browse the repository at this point in the history
shielded expedition changes
  • Loading branch information
Fraccaman authored Feb 6, 2024
2 parents 53067ea + 5a85152 commit b09b96c
Show file tree
Hide file tree
Showing 7 changed files with 148 additions and 23 deletions.
10 changes: 6 additions & 4 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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(
Expand Down
21 changes: 18 additions & 3 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u64>,

/// Authentication key for faucet challenges
#[clap(long, env)]
pub auth_key: Option<String>,

/// Max number of requests per second
#[clap(long, env)]
pub rps: Option<u64>,

/// 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,
}
4 changes: 4 additions & 0 deletions src/dto/faucet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
12 changes: 12 additions & 0 deletions src/error/faucet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand All @@ -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 {
Expand All @@ -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()))
Expand Down
74 changes: 69 additions & 5 deletions src/handler/faucet.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -10,6 +13,7 @@ use namada_sdk::{
tx::data::ResultCode,
types::{
address::Address,
key::{common, SigScheme},
masp::{TransferSource, TransferTarget},
},
Namada,
Expand Down Expand Up @@ -45,10 +49,39 @@ pub async fn faucet_settings(

pub async fn request_challenge(
State(mut state): State<FaucetState>,
Path(player_id): Path<String>,
) -> Result<Json<FaucetResponseDto>, 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);

Expand All @@ -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
Expand All @@ -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());
}
Expand Down
29 changes: 22 additions & 7 deletions src/services/faucet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,34 @@ impl FaucetService {
}
}

pub async fn generate_faucet_request(&mut self, auth_key: String) -> Result<Faucet, ApiError> {
pub async fn generate_faucet_request(
&mut self,
auth_key: String,
player_id: String,
) -> Result<Faucet, ApiError> {
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<u8> {
fn compute_tag(&self, auth_key: &String, challenge: &[u8], player_id: &[u8]) -> Vec<u8> {
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");

Expand All @@ -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 {
Expand Down
21 changes: 17 additions & 4 deletions src/state/faucet.rs
Original file line number Diff line number Diff line change
@@ -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::{
Expand All @@ -11,6 +14,8 @@ use namada_sdk::{
};
use tendermint_rpc::HttpClient;

type PlayerId = String;

#[derive(Clone)]
pub struct FaucetState {
pub faucet_service: FaucetService,
Expand All @@ -22,29 +27,37 @@ pub struct FaucetState {
pub chain_id: String,
pub chain_start: i64,
pub withdraw_limit: u64,
pub request_frequency: Duration,
pub last_requests: HashMap<PlayerId, Instant>,
pub webserver_host: String,
}

impl FaucetState {
#[allow(clippy::too_many_arguments)]
pub fn new(
data: &Arc<RwLock<AppState>>,
faucet_address: Address,
sdk: NamadaImpl<HttpClient, FsWalletUtils, FsShieldedUtils, NullIo>,
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),
faucet_repo: FaucetRepository::new(data),
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(),
}
}
}

0 comments on commit b09b96c

Please sign in to comment.