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

shielded expedition changes #13

Merged
merged 1 commit into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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(),
}
}
}
Loading