Skip to content

Commit b09b96c

Browse files
authored
Merge pull request #13 from heliaxdev/se-changes
shielded expedition changes
2 parents 53067ea + 5a85152 commit b09b96c

File tree

7 files changed

+148
-23
lines changed

7 files changed

+148
-23
lines changed

src/app.rs

+6-4
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,13 @@ impl ApplicationServer {
5555

5656
assert!(auth_key.len() == 32);
5757

58-
let difficulty = config.difficulty;
5958
let rps = config.rps;
6059
let chain_id = config.chain_id.clone();
6160
let rpc = config.rpc.clone();
6261
let chain_start = config.chain_start;
63-
let withdraw_limit = config.withdraw_limit.unwrap_or(1000_u64);
62+
let withdraw_limit = config.withdraw_limit.unwrap_or(1_000_000_000_u64);
63+
let webserver_host = config.webserver_host.clone();
64+
let request_frequency = config.request_frequency;
6465

6566
let sk = config.private_key.clone();
6667
let sk = sk_from_str(&sk);
@@ -113,15 +114,16 @@ impl ApplicationServer {
113114
address,
114115
sdk,
115116
auth_key,
116-
difficulty,
117117
chain_id,
118118
chain_start,
119119
withdraw_limit,
120+
webserver_host,
121+
request_frequency,
120122
);
121123

122124
Router::new()
123125
.route("/faucet/setting", get(faucet_handler::faucet_settings))
124-
.route("/faucet", get(faucet_handler::request_challenge))
126+
.route("/faucet/challenge/:player_id", get(faucet_handler::request_challenge))
125127
.route("/faucet", post(faucet_handler::request_transfer))
126128
.with_state(faucet_state)
127129
.merge(Router::new().route(

src/config.rs

+18-3
Original file line numberDiff line numberDiff line change
@@ -9,30 +9,45 @@ pub struct AppConfig {
99
#[clap(long, env, value_enum)]
1010
pub cargo_env: CargoEnv,
1111

12+
/// Port to bind the crawler's HTTP server to
1213
#[clap(long, env, default_value = "5000")]
1314
pub port: u16,
1415

15-
#[clap(long, env)]
16-
pub difficulty: u64,
17-
16+
/// Faucet's private key in Namada
1817
#[clap(long, env)]
1918
pub private_key: String,
2019

2120
#[clap(long, env)]
2221
pub chain_start: i64,
2322

23+
/// Chain id of Namada
2424
#[clap(long, env)]
2525
pub chain_id: String,
2626

27+
/// URL of the Namada RPC
2728
#[clap(long, env)]
2829
pub rpc: String,
2930

31+
/// Withdraw limit given in base units of NAAN
3032
#[clap(long, env)]
3133
pub withdraw_limit: Option<u64>,
3234

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

39+
/// Max number of requests per second
3640
#[clap(long, env)]
3741
pub rps: Option<u64>,
42+
43+
/// URL of the Shielded Expedition's webserver
44+
#[clap(long, env)]
45+
pub webserver_host: String,
46+
47+
/// User request frequency given in seconds
48+
///
49+
/// If more than one request is performed during this
50+
/// interval, the faucet denies the request
51+
#[clap(long, env)]
52+
pub request_frequency: u64,
3853
}

src/dto/faucet.rs

+4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ pub struct FaucetRequestDto {
1515
#[validate(length(equal = 64, message = "Invalid proof"))]
1616
pub tag: String,
1717
pub transfer: Transfer,
18+
#[validate(length(max = 256, message = "Invalid player id"))]
19+
pub player_id: String,
20+
#[validate(length(max = 256, message = "Invalid challenge signature"))]
21+
pub challenge_signature: String,
1822
}
1923

2024
#[derive(Clone, Serialize, Deserialize, Validate)]

src/error/faucet.rs

+12
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ pub enum FaucetError {
1515
DuplicateChallenge,
1616
#[error("Invalid Address")]
1717
InvalidAddress,
18+
#[error("Invalid public key")]
19+
InvalidPublicKey,
20+
#[error("Invalid signature")]
21+
InvalidSignature,
1822
#[error("Chain didn't start yet")]
1923
ChainNotStarted,
2024
#[error("Faucet out of balance")]
@@ -23,6 +27,10 @@ pub enum FaucetError {
2327
SdkError(String),
2428
#[error("Withdraw limit must be less then {0}")]
2529
InvalidWithdrawLimit(u64),
30+
#[error("Public key {0} does not belong to a shielded expedition player")]
31+
NotPlayer(String),
32+
#[error("Slow down, space cowboy")]
33+
TooManyRequests,
2634
}
2735

2836
impl IntoResponse for FaucetError {
@@ -36,6 +44,10 @@ impl IntoResponse for FaucetError {
3644
FaucetError::InvalidWithdrawLimit(_) => StatusCode::BAD_REQUEST,
3745
FaucetError::FaucetOutOfBalance => StatusCode::CONFLICT,
3846
FaucetError::SdkError(_) => StatusCode::BAD_REQUEST,
47+
FaucetError::NotPlayer(_) => StatusCode::BAD_REQUEST,
48+
FaucetError::TooManyRequests => StatusCode::BAD_REQUEST,
49+
FaucetError::InvalidPublicKey => StatusCode::BAD_REQUEST,
50+
FaucetError::InvalidSignature => StatusCode::BAD_REQUEST,
3951
};
4052

4153
ApiErrorResponse::send(status_code.as_u16(), Some(self.to_string()))

src/handler/faucet.rs

+69-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
use std::collections::HashMap;
2+
use std::time::Instant;
23

4+
use axum::extract::Path;
35
use axum::{extract::State, Json};
46
use axum_macros::debug_handler;
7+
use namada_sdk::types::string_encoding::Format;
58
use namada_sdk::{
69
args::InputAmount,
710
rpc,
@@ -10,6 +13,7 @@ use namada_sdk::{
1013
tx::data::ResultCode,
1114
types::{
1215
address::Address,
16+
key::{common, SigScheme},
1317
masp::{TransferSource, TransferTarget},
1418
},
1519
Namada,
@@ -45,10 +49,39 @@ pub async fn faucet_settings(
4549

4650
pub async fn request_challenge(
4751
State(mut state): State<FaucetState>,
52+
Path(player_id): Path<String>,
4853
) -> Result<Json<FaucetResponseDto>, ApiError> {
54+
let is_player = match reqwest::get(format!(
55+
"https://{}/api/v1/player/exists/{}",
56+
state.webserver_host, player_id
57+
))
58+
.await
59+
.map(|response| response.status().is_success())
60+
{
61+
Ok(is_success) if is_success => true,
62+
_ => false,
63+
};
64+
if !is_player {
65+
return Err(FaucetError::NotPlayer(player_id).into());
66+
}
67+
68+
let now = Instant::now();
69+
let too_many_requests = 'result: {
70+
let Some(last_request_instant) = state.last_requests.get(&player_id) else {
71+
break 'result false;
72+
};
73+
let elapsed_request_time = now.duration_since(*last_request_instant);
74+
elapsed_request_time <= state.request_frequency
75+
};
76+
77+
if too_many_requests {
78+
return Err(FaucetError::TooManyRequests.into());
79+
}
80+
state.last_requests.insert(player_id.clone(), now);
81+
4982
let faucet_request = state
5083
.faucet_service
51-
.generate_faucet_request(state.auth_key)
84+
.generate_faucet_request(state.auth_key, player_id)
5285
.await?;
5386
let response = FaucetResponseDto::from(faucet_request);
5487

@@ -66,6 +99,34 @@ pub async fn request_transfer(
6699
return Err(FaucetError::InvalidWithdrawLimit(state.withdraw_limit).into());
67100
}
68101

102+
let player_id_pk: common::PublicKey = if let Ok(pk) = payload.player_id.parse() {
103+
pk
104+
} else {
105+
return Err(FaucetError::InvalidPublicKey.into());
106+
};
107+
108+
let challenge_signature = if let Ok(hex_decoded_sig) = hex::decode(payload.challenge_signature)
109+
{
110+
if let Ok(sig) = common::Signature::decode_bytes(&hex_decoded_sig) {
111+
sig
112+
} else {
113+
return Err(FaucetError::InvalidSignature.into());
114+
}
115+
} else {
116+
return Err(FaucetError::InvalidSignature.into());
117+
};
118+
119+
if common::SigScheme::verify_signature(
120+
&player_id_pk,
121+
// NOTE: signing over the hex encoded challenge data
122+
&payload.challenge.as_bytes(),
123+
&challenge_signature,
124+
)
125+
.is_err()
126+
{
127+
return Err(FaucetError::InvalidSignature.into());
128+
}
129+
69130
let token_address = Address::decode(payload.transfer.token.clone());
70131
let token_address = if let Ok(address) = token_address {
71132
address
@@ -82,10 +143,13 @@ pub async fn request_transfer(
82143
if state.faucet_repo.contains(&payload.challenge).await {
83144
return Err(FaucetError::DuplicateChallenge.into());
84145
}
85-
let is_valid_proof =
86-
state
87-
.faucet_service
88-
.verify_tag(&auth_key, &payload.challenge, &payload.tag);
146+
147+
let is_valid_proof = state.faucet_service.verify_tag(
148+
&auth_key,
149+
&payload.challenge,
150+
&payload.player_id,
151+
&payload.tag,
152+
);
89153
if !is_valid_proof {
90154
return Err(FaucetError::InvalidProof.into());
91155
}

src/services/faucet.rs

+22-7
Original file line numberDiff line numberDiff line change
@@ -28,22 +28,34 @@ impl FaucetService {
2828
}
2929
}
3030

31-
pub async fn generate_faucet_request(&mut self, auth_key: String) -> Result<Faucet, ApiError> {
31+
pub async fn generate_faucet_request(
32+
&mut self,
33+
auth_key: String,
34+
player_id: String,
35+
) -> Result<Faucet, ApiError> {
3236
let challenge = self.r.generate();
33-
let tag = self.compute_tag(&auth_key, &challenge);
37+
let tag = self.compute_tag(&auth_key, &challenge, player_id.as_bytes());
3438

3539
Ok(Faucet::request(challenge, tag))
3640
}
3741

38-
fn compute_tag(&self, auth_key: &String, challenge: &[u8]) -> Vec<u8> {
42+
fn compute_tag(&self, auth_key: &String, challenge: &[u8], player_id: &[u8]) -> Vec<u8> {
3943
let key = auth::SecretKey::from_slice(auth_key.as_bytes())
4044
.expect("Should be able to convert key to bytes");
41-
let tag = auth::authenticate(&key, challenge).expect("Should be able to compute tag");
45+
let challenge_and_player_id: Vec<_> = [challenge, player_id].concat();
46+
let tag = auth::authenticate(&key, &challenge_and_player_id)
47+
.expect("Should be able to compute tag");
4248

4349
tag.unprotected_as_bytes().to_vec()
4450
}
4551

46-
pub fn verify_tag(&self, auth_key: &String, challenge: &String, tag: &String) -> bool {
52+
pub fn verify_tag(
53+
&self,
54+
auth_key: &String,
55+
challenge: &String,
56+
player_id: &String,
57+
tag: &String,
58+
) -> bool {
4759
let key = auth::SecretKey::from_slice(auth_key.as_bytes())
4860
.expect("Should be able to convert key to bytes");
4961

@@ -62,9 +74,12 @@ impl FaucetService {
6274

6375
let tag = Tag::from_slice(&decoded_tag).expect("Should be able to convert bytes to tag");
6476

65-
let decoded_challenge = HEXLOWER.decode(challenge.as_bytes()).expect("Test");
77+
let Ok(decoded_challenge) = HEXLOWER.decode(challenge.as_bytes()) else {
78+
return false;
79+
};
80+
let challenge_and_player_id = [&decoded_challenge[..], player_id.as_bytes()].concat();
6681

67-
auth::authenticate_verify(&tag, &key, &decoded_challenge).is_ok()
82+
auth::authenticate_verify(&tag, &key, &challenge_and_player_id).is_ok()
6883
}
6984

7085
pub fn verify_pow(&self, challenge: &String, solution: &String, difficulty: u64) -> bool {

src/state/faucet.rs

+17-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
use std::collections::HashMap;
2+
use std::sync::Arc;
3+
use std::time::{Duration, Instant};
4+
15
use crate::{
26
app_state::AppState, repository::faucet::FaucetRepository,
37
repository::faucet::FaucetRepositoryTrait, services::faucet::FaucetService,
48
};
5-
use std::sync::Arc;
69
use tokio::sync::RwLock;
710

811
use namada_sdk::{
@@ -11,6 +14,8 @@ use namada_sdk::{
1114
};
1215
use tendermint_rpc::HttpClient;
1316

17+
type PlayerId = String;
18+
1419
#[derive(Clone)]
1520
pub struct FaucetState {
1621
pub faucet_service: FaucetService,
@@ -22,29 +27,37 @@ pub struct FaucetState {
2227
pub chain_id: String,
2328
pub chain_start: i64,
2429
pub withdraw_limit: u64,
30+
pub request_frequency: Duration,
31+
pub last_requests: HashMap<PlayerId, Instant>,
32+
pub webserver_host: String,
2533
}
2634

2735
impl FaucetState {
36+
#[allow(clippy::too_many_arguments)]
2837
pub fn new(
2938
data: &Arc<RwLock<AppState>>,
3039
faucet_address: Address,
3140
sdk: NamadaImpl<HttpClient, FsWalletUtils, FsShieldedUtils, NullIo>,
3241
auth_key: String,
33-
difficulty: u64,
3442
chain_id: String,
3543
chain_start: i64,
3644
withdraw_limit: u64,
45+
webserver_host: String,
46+
request_frequency: u64,
3747
) -> Self {
3848
Self {
3949
faucet_service: FaucetService::new(data),
4050
faucet_repo: FaucetRepository::new(data),
4151
faucet_address,
4252
sdk: Arc::new(sdk),
4353
auth_key,
44-
difficulty,
54+
difficulty: 0,
4555
chain_id,
4656
chain_start,
47-
withdraw_limit: withdraw_limit * 10_u64.pow(6),
57+
withdraw_limit,
58+
webserver_host,
59+
request_frequency: Duration::from_secs(request_frequency),
60+
last_requests: HashMap::new(),
4861
}
4962
}
5063
}

0 commit comments

Comments
 (0)