mcaptcha/api/v1/pow/
verify_pow.rs

1// Copyright (C) 2022  Aravinth Manivannan <realaravinth@batsense.net>
2// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
3//
4// SPDX-License-Identifier: AGPL-3.0-or-later
5
6//! PoW Verification module
7
8use actix_web::HttpRequest;
9use actix_web::{web, HttpResponse, Responder};
10use libmcaptcha::pow::Work;
11use serde::{Deserialize, Serialize};
12
13use crate::errors::*;
14use crate::AppData;
15use crate::V1_API_ROUTES;
16
17#[derive(Clone, Debug, Deserialize, Serialize)]
18/// validation token that clients receive as proof for submiting
19/// valid PoW
20pub struct ValidationToken {
21    pub token: String,
22}
23
24#[derive(Clone, Debug, Deserialize, Serialize)]
25pub struct ApiWork {
26    pub string: String,
27    pub result: String,
28    pub nonce: u64,
29    pub key: String,
30    pub time: Option<u32>,
31    pub worker_type: Option<String>,
32}
33
34impl From<ApiWork> for Work {
35    fn from(value: ApiWork) -> Self {
36        Self {
37            string: value.string,
38            nonce: value.nonce,
39            result: value.result,
40            key: value.key,
41        }
42    }
43}
44
45// API keys are mcaptcha actor names
46
47/// route handler that verifies PoW and issues a solution token
48/// if verification is successful
49#[my_codegen::post(path = "V1_API_ROUTES.pow.verify_pow()")]
50pub async fn verify_pow(
51    req: HttpRequest,
52    payload: web::Json<ApiWork>,
53    data: AppData,
54) -> ServiceResult<impl Responder> {
55    #[cfg(not(test))]
56    let ip = req.connection_info().peer_addr().unwrap().to_string();
57    // From actix-web docs:
58    //  Will only return None when called in unit tests unless TestRequest::peer_addr is used.
59    //
60    // ref: https://docs.rs/actix-web/latest/actix_web/struct.HttpRequest.html#method.peer_addr
61    #[cfg(test)]
62    let ip = "127.0.1.1".into();
63
64    let key = payload.key.clone();
65    let payload = payload.into_inner();
66    let worker_type = payload.worker_type.clone();
67    let time = payload.time;
68    let nonce = payload.nonce;
69    let (res, difficulty_factor) = data.captcha.verify_pow(payload.into(), ip).await?;
70    data.stats.record_solve(&data, &key).await?;
71    if let (Some(time), Some(worker_type)) = (time, worker_type) {
72        let analytics = db_core::CreatePerformanceAnalytics {
73            difficulty_factor,
74            time,
75            worker_type,
76        };
77        data.db.analysis_save(&key, &analytics).await?;
78    }
79    data.db
80        .update_max_nonce_for_level(&key, difficulty_factor, nonce as u32)
81        .await?;
82    let payload = ValidationToken { token: res };
83    Ok(HttpResponse::Ok().json(payload))
84}
85
86#[cfg(test)]
87pub mod tests {
88    use actix_web::http::StatusCode;
89    use actix_web::test;
90    use libmcaptcha::pow::PoWConfig;
91
92    use super::*;
93    use crate::api::v1::pow::get_config::GetConfigPayload;
94    use crate::tests::*;
95    use crate::*;
96
97    #[actix_rt::test]
98    async fn verify_pow_works_pg() {
99        let data = crate::tests::pg::get_data().await;
100        verify_pow_works(data).await;
101    }
102
103    #[actix_rt::test]
104    async fn verify_pow_works_maria() {
105        let data = crate::tests::maria::get_data().await;
106        verify_pow_works(data).await;
107    }
108
109    #[actix_rt::test]
110    async fn verify_analytics_pow_works_pg() {
111        let data = crate::tests::pg::get_data().await;
112        verify_analytics_pow_works(data).await;
113    }
114
115    #[actix_rt::test]
116    async fn verify_analytics_pow_works_maria() {
117        let data = crate::tests::maria::get_data().await;
118        verify_analytics_pow_works(data).await;
119    }
120
121    pub async fn verify_analytics_pow_works(data: ArcData) {
122        const NAME: &str = "powanalyticsuser";
123        const PASSWORD: &str = "testingpas";
124        const EMAIL: &str = "powanalyticsuser@a.com";
125        let data = &data;
126
127        delete_user(data, NAME).await;
128
129        register_and_signin(data, NAME, EMAIL, PASSWORD).await;
130        let (_, _signin_resp, token_key) = add_levels_util(data, NAME, PASSWORD).await;
131        let app = get_app!(data).await;
132
133        let get_config_payload = GetConfigPayload {
134            key: token_key.key.clone(),
135        };
136
137        // update and check changes
138
139        let get_config_resp = test::call_service(
140            &app,
141            post_request!(&get_config_payload, V1_API_ROUTES.pow.get_config)
142                .to_request(),
143        )
144        .await;
145        assert_eq!(get_config_resp.status(), StatusCode::OK);
146        let config: PoWConfig = test::read_body_json(get_config_resp).await;
147
148        let pow = mcaptcha_pow_sha256::ConfigBuilder::default()
149            .salt(config.salt)
150            .build()
151            .unwrap();
152        let work = pow
153            .prove_work(&config.string.clone(), config.difficulty_factor)
154            .unwrap();
155
156        let work = ApiWork {
157            string: config.string.clone(),
158            result: work.result,
159            nonce: work.nonce,
160            key: token_key.key.clone(),
161            time: Some(100),
162            worker_type: Some("wasm".into()),
163        };
164
165        let pow_verify_resp = test::call_service(
166            &app,
167            post_request!(&work, V1_API_ROUTES.pow.verify_pow).to_request(),
168        )
169        .await;
170        assert_eq!(pow_verify_resp.status(), StatusCode::OK);
171        let limit = 50;
172        let offset = 0;
173        let mut analytics = data
174            .db
175            .analytics_fetch(&token_key.key, limit, offset)
176            .await
177            .unwrap();
178        assert_eq!(analytics.len(), 1);
179        let a = analytics.pop().unwrap();
180        assert_eq!(a.time, work.time.unwrap());
181        assert_eq!(a.worker_type, work.worker_type.unwrap());
182    }
183
184    pub async fn verify_pow_works(data: ArcData) {
185        const NAME: &str = "powverifyusr";
186        const PASSWORD: &str = "testingpas";
187        const EMAIL: &str = "verifyuser@a.com";
188        let data = &data;
189
190        delete_user(data, NAME).await;
191
192        register_and_signin(data, NAME, EMAIL, PASSWORD).await;
193        let (_, _signin_resp, token_key) = add_levels_util(data, NAME, PASSWORD).await;
194        let app = get_app!(data).await;
195
196        let get_config_payload = GetConfigPayload {
197            key: token_key.key.clone(),
198        };
199
200        // update and check changes
201
202        let get_config_resp = test::call_service(
203            &app,
204            post_request!(&get_config_payload, V1_API_ROUTES.pow.get_config)
205                .to_request(),
206        )
207        .await;
208        assert_eq!(get_config_resp.status(), StatusCode::OK);
209        let config: PoWConfig = test::read_body_json(get_config_resp).await;
210
211        let pow = mcaptcha_pow_sha256::ConfigBuilder::default()
212            .salt(config.salt)
213            .build()
214            .unwrap();
215        let work = pow
216            .prove_work(&config.string.clone(), config.difficulty_factor)
217            .unwrap();
218
219        let work = Work {
220            string: config.string.clone(),
221            result: work.result,
222            nonce: work.nonce,
223            key: token_key.key.clone(),
224        };
225
226        let pow_verify_resp = test::call_service(
227            &app,
228            post_request!(&work, V1_API_ROUTES.pow.verify_pow).to_request(),
229        )
230        .await;
231        assert_eq!(pow_verify_resp.status(), StatusCode::OK);
232        assert!(data
233            .db
234            .analytics_fetch(&token_key.key, 50, 0)
235            .await
236            .unwrap()
237            .is_empty());
238
239        let string_not_found = test::call_service(
240            &app,
241            post_request!(&work, V1_API_ROUTES.pow.verify_pow).to_request(),
242        )
243        .await;
244        assert_eq!(string_not_found.status(), StatusCode::BAD_REQUEST);
245        let err: ErrorToResponse = test::read_body_json(string_not_found).await;
246        assert_eq!(err.error, "Challenge: not found");
247
248        // let pow_config_resp = test::call_service(
249        //     &app,
250        //     post_request!(&get_config_payload, V1_API_ROUTES.pow.get_config).to_request(),
251        // )
252        // .await;
253        // assert_eq!(pow_config_resp.status(), StatusCode::OK);
254        // I'm not checking for errors because changing work.result triggered
255        // InssuficientDifficulty, which is possible because libmcaptcha calculates
256        // difficulty with the submitted result. Besides, this endpoint is merely
257        // propagating errors from libmcaptcha and libmcaptcha has tests covering the
258        // pow aspects ¯\_(ツ)_/¯
259    }
260}