pink_chain_extension/
lib.rs

1use std::borrow::Cow;
2use std::io::Write;
3use std::{
4    fmt::Display,
5    str::FromStr,
6    time::{Duration, SystemTime},
7};
8
9use pink::{
10    chain_extension::{
11        self as ext, HttpRequest, HttpRequestError, HttpResponse, JsCode, JsValue, PinkExtBackend,
12        SigType, StorageQuotaExceeded,
13    },
14    types::sgx::SgxQuote,
15    Balance, EcdhPublicKey, EcdsaPublicKey, EcdsaSignature, Hash,
16};
17use reqwest::{
18    header::{HeaderMap, HeaderName, HeaderValue},
19    Method,
20};
21use reqwest_env_proxy::EnvProxyBuilder;
22use sp_core::{ByteArray as _, Pair};
23
24pub mod local_cache;
25pub mod mock_ext;
26
27pub trait PinkRuntimeEnv {
28    type AccountId: AsRef<[u8]> + Display;
29
30    fn address(&self) -> &Self::AccountId;
31}
32
33pub struct DefaultPinkExtension<'a, T, Error> {
34    pub env: &'a T,
35    _e: std::marker::PhantomData<Error>,
36}
37
38impl<'a, T, E> DefaultPinkExtension<'a, T, E> {
39    pub fn new(env: &'a T) -> Self {
40        Self {
41            env,
42            _e: std::marker::PhantomData,
43        }
44    }
45}
46
47fn block_on<F: core::future::Future>(f: F) -> F::Output {
48    match tokio::runtime::Handle::try_current() {
49        Ok(handle) => handle.block_on(f),
50        Err(_) => tokio::runtime::Runtime::new()
51            .expect("Failed to create tokio runtime")
52            .block_on(f),
53    }
54}
55
56pub fn batch_http_request(requests: Vec<HttpRequest>, timeout_ms: u64) -> ext::BatchHttpResult {
57    const MAX_CONCURRENT_REQUESTS: usize = 5;
58    if requests.len() > MAX_CONCURRENT_REQUESTS {
59        return Err(ext::HttpRequestError::TooManyRequests);
60    }
61    block_on(async move {
62        let futs = requests
63            .into_iter()
64            .map(|request| async_http_request(request, timeout_ms));
65        tokio::time::timeout(
66            Duration::from_millis(timeout_ms + 200),
67            futures::future::join_all(futs),
68        )
69        .await
70    })
71    .or(Err(ext::HttpRequestError::Timeout))
72}
73
74pub fn http_request(
75    request: HttpRequest,
76    timeout_ms: u64,
77) -> Result<HttpResponse, HttpRequestError> {
78    use HttpRequestError::*;
79    match block_on(async_http_request(request, timeout_ms)) {
80        Ok(resp) => Ok(resp),
81        Err(err) => match err {
82            // runtime v1.0 supported errors
83            InvalidUrl | InvalidMethod | InvalidHeaderName | InvalidHeaderValue
84            | FailedToCreateClient | Timeout => Err(err),
85            _ => {
86                // To be compatible with runtime v1.0, we need to convert the v1.1 extended errors
87                // to an HTTP response with status code 524.
88                log::error!("chain_ext: http request failed: {}", err.display());
89                Ok(HttpResponse {
90                    status_code: 524,
91                    reason_phrase: "IO Error".into(),
92                    body: format!("{err:?}").into_bytes(),
93                    headers: vec![],
94                })
95            }
96        },
97    }
98}
99
100async fn async_http_request(
101    request: HttpRequest,
102    timeout_ms: u64,
103) -> Result<HttpResponse, HttpRequestError> {
104    if timeout_ms == 0 {
105        return Err(HttpRequestError::Timeout);
106    }
107    let timeout = Duration::from_millis(timeout_ms);
108    let url: reqwest::Url = request.url.parse().or(Err(HttpRequestError::InvalidUrl))?;
109    let client = reqwest::Client::builder()
110        .trust_dns(true)
111        .timeout(timeout)
112        .env_proxy(url.host_str().unwrap_or_default())
113        .build()
114        .or(Err(HttpRequestError::FailedToCreateClient))?;
115
116    let method: Method =
117        FromStr::from_str(request.method.as_str()).or(Err(HttpRequestError::InvalidMethod))?;
118    let mut headers = HeaderMap::new();
119    for (key, value) in &request.headers {
120        let key =
121            HeaderName::from_str(key.as_str()).or(Err(HttpRequestError::InvalidHeaderName))?;
122        let value = HeaderValue::from_str(value).or(Err(HttpRequestError::InvalidHeaderValue))?;
123        headers.insert(key, value);
124    }
125
126    let result = client
127        .request(method, url)
128        .headers(headers)
129        .body(request.body)
130        .send()
131        .await;
132
133    let mut response = match result {
134        Ok(response) => response,
135        Err(err) => {
136            // If there is somthing wrong with the network, we can not inspect the reason too
137            // much here. Let it return a non-standard 523 here.
138            return Ok(HttpResponse {
139                status_code: 523,
140                reason_phrase: "Unreachable".into(),
141                body: format!("{err:?}").into_bytes(),
142                headers: vec![],
143            });
144        }
145    };
146
147    let headers: Vec<_> = response
148        .headers()
149        .iter()
150        .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or_default().into()))
151        .collect();
152
153    const MAX_BODY_SIZE: usize = 1024 * 1024 * 2; // 2MB
154
155    let mut body = Vec::new();
156    let mut writer = LimitedWriter::new(&mut body, MAX_BODY_SIZE);
157
158    while let Some(chunk) = response
159        .chunk()
160        .await
161        .or(Err(HttpRequestError::NetworkError))?
162    {
163        writer
164            .write_all(&chunk)
165            .or(Err(HttpRequestError::ResponseTooLarge))?;
166    }
167
168    let response = HttpResponse {
169        status_code: response.status().as_u16(),
170        reason_phrase: response
171            .status()
172            .canonical_reason()
173            .unwrap_or_default()
174            .into(),
175        body,
176        headers,
177    };
178    Ok(response)
179}
180
181impl<T: PinkRuntimeEnv, E: From<&'static str>> PinkExtBackend for DefaultPinkExtension<'_, T, E> {
182    type Error = E;
183    fn http_request(&self, request: HttpRequest) -> Result<HttpResponse, Self::Error> {
184        http_request(request, 10 * 1000).map_err(|err| err.display().into())
185    }
186
187    fn batch_http_request(
188        &self,
189        requests: Vec<HttpRequest>,
190        timeout_ms: u64,
191    ) -> Result<ext::BatchHttpResult, Self::Error> {
192        Ok(batch_http_request(requests, timeout_ms))
193    }
194
195    fn sign(
196        &self,
197        sigtype: SigType,
198        key: Cow<[u8]>,
199        message: Cow<[u8]>,
200    ) -> Result<Vec<u8>, Self::Error> {
201        macro_rules! sign_with {
202            ($sigtype:ident) => {{
203                let pair = sp_core::$sigtype::Pair::from_seed_slice(&key).or(Err("Invalid key"))?;
204                let signature = pair.sign(&message);
205                let signature: &[u8] = signature.as_ref();
206                signature.to_vec()
207            }};
208        }
209
210        Ok(match sigtype {
211            SigType::Sr25519 => sign_with!(sr25519),
212            SigType::Ed25519 => sign_with!(ed25519),
213            SigType::Ecdsa => sign_with!(ecdsa),
214        })
215    }
216
217    fn verify(
218        &self,
219        sigtype: SigType,
220        pubkey: Cow<[u8]>,
221        message: Cow<[u8]>,
222        signature: Cow<[u8]>,
223    ) -> Result<bool, Self::Error> {
224        macro_rules! verify_with {
225            ($sigtype:ident) => {{
226                let pubkey = sp_core::$sigtype::Public::from_slice(&pubkey)
227                    .map_err(|_| "Invalid public key")?;
228                let signature = sp_core::$sigtype::Signature::from_slice(&signature)
229                    .ok_or("Invalid signature")?;
230                Ok(sp_core::$sigtype::Pair::verify(
231                    &signature, message, &pubkey,
232                ))
233            }};
234        }
235        match sigtype {
236            SigType::Sr25519 => verify_with!(sr25519),
237            SigType::Ed25519 => verify_with!(ed25519),
238            SigType::Ecdsa => verify_with!(ecdsa),
239        }
240    }
241
242    fn derive_sr25519_key(&self, salt: Cow<[u8]>) -> Result<Vec<u8>, Self::Error> {
243        // This default implementation is for unit tests. The host should override this.
244        let mut seed: <sp_core::sr25519::Pair as Pair>::Seed = Default::default();
245        let len = seed.len().min(salt.len());
246        seed[..len].copy_from_slice(&salt[..len]);
247        let key = sp_core::sr25519::Pair::from_seed(&seed);
248
249        Ok(key.as_ref().secret.to_bytes().to_vec())
250    }
251
252    fn get_public_key(&self, sigtype: SigType, key: Cow<[u8]>) -> Result<Vec<u8>, Self::Error> {
253        macro_rules! public_key_with {
254            ($sigtype:ident) => {{
255                sp_core::$sigtype::Pair::from_seed_slice(&key)
256                    .or(Err("Invalid key"))?
257                    .public()
258                    .to_raw_vec()
259            }};
260        }
261        let pubkey = match sigtype {
262            SigType::Ed25519 => public_key_with!(ed25519),
263            SigType::Sr25519 => public_key_with!(sr25519),
264            SigType::Ecdsa => public_key_with!(ecdsa),
265        };
266        Ok(pubkey)
267    }
268
269    fn cache_set(
270        &self,
271        _key: Cow<[u8]>,
272        _value: Cow<[u8]>,
273    ) -> Result<Result<(), StorageQuotaExceeded>, Self::Error> {
274        Ok(Ok(()))
275    }
276
277    fn cache_set_expiration(&self, _key: Cow<[u8]>, _expire: u64) -> Result<(), Self::Error> {
278        Ok(())
279    }
280
281    fn cache_get(&self, _key: Cow<'_, [u8]>) -> Result<Option<Vec<u8>>, Self::Error> {
282        Ok(None)
283    }
284
285    fn cache_remove(&self, _key: Cow<'_, [u8]>) -> Result<Option<Vec<u8>>, Self::Error> {
286        Ok(None)
287    }
288
289    fn log(&self, level: u8, message: Cow<str>) -> Result<(), Self::Error> {
290        let address = self.env.address();
291        let level = match level {
292            1 => log::Level::Error,
293            2 => log::Level::Warn,
294            3 => log::Level::Info,
295            4 => log::Level::Debug,
296            5 => log::Level::Trace,
297            _ => log::Level::Error,
298        };
299        log::log!(target: "pink", level, "[{}] {}", address, message);
300        Ok(())
301    }
302
303    fn getrandom(&self, length: u8) -> Result<Vec<u8>, Self::Error> {
304        let mut buf = vec![0u8; length as _];
305        getrandom::getrandom(&mut buf[..]).or(Err("Failed to get random bytes"))?;
306        Ok(buf)
307    }
308
309    fn is_in_transaction(&self) -> Result<bool, Self::Error> {
310        Ok(false)
311    }
312
313    fn ecdsa_sign_prehashed(
314        &self,
315        key: Cow<[u8]>,
316        message_hash: Hash,
317    ) -> Result<EcdsaSignature, Self::Error> {
318        let pair = sp_core::ecdsa::Pair::from_seed_slice(&key).or(Err("Invalid key"))?;
319        let signature = pair.sign_prehashed(&message_hash);
320        Ok(signature.0)
321    }
322
323    fn ecdsa_verify_prehashed(
324        &self,
325        signature: EcdsaSignature,
326        message_hash: Hash,
327        pubkey: EcdsaPublicKey,
328    ) -> Result<bool, Self::Error> {
329        let public = sp_core::ecdsa::Public(pubkey);
330        let sig = sp_core::ecdsa::Signature(signature);
331        Ok(sp_core::ecdsa::Pair::verify_prehashed(
332            &sig,
333            &message_hash,
334            &public,
335        ))
336    }
337
338    fn system_contract_id(&self) -> Result<ext::AccountId, Self::Error> {
339        Err("No default system contract id".into())
340    }
341
342    fn balance_of(&self, _account: ext::AccountId) -> Result<(Balance, Balance), Self::Error> {
343        Ok((0, 0))
344    }
345
346    fn untrusted_millis_since_unix_epoch(&self) -> Result<u64, Self::Error> {
347        let duration = SystemTime::now()
348            .duration_since(SystemTime::UNIX_EPOCH)
349            .or(Err("The system time is earlier than UNIX_EPOCH"))?;
350        Ok(duration.as_millis() as u64)
351    }
352
353    fn worker_pubkey(&self) -> Result<EcdhPublicKey, Self::Error> {
354        Ok(Default::default())
355    }
356
357    fn code_exists(&self, _code_hash: Hash, _sidevm: bool) -> Result<bool, Self::Error> {
358        Ok(false)
359    }
360
361    fn import_latest_system_code(
362        &self,
363        _payer: ext::AccountId,
364    ) -> Result<Option<Hash>, Self::Error> {
365        Ok(None)
366    }
367
368    fn runtime_version(&self) -> Result<(u32, u32), Self::Error> {
369        Ok((1, 0))
370    }
371
372    fn current_event_chain_head(&self) -> Result<(u64, Hash), Self::Error> {
373        Ok((0, Default::default()))
374    }
375
376    fn js_eval(&self, _codes: Vec<JsCode>, _args: Vec<String>) -> Result<JsValue, Self::Error> {
377        Ok(JsValue::Exception("No Js Runtime".into()))
378    }
379
380    fn worker_sgx_quote(&self) -> Result<Option<SgxQuote>, Self::Error> {
381        Ok(None)
382    }
383}
384
385struct LimitedWriter<W> {
386    writer: W,
387    written: usize,
388    limit: usize,
389}
390
391impl<W> LimitedWriter<W> {
392    fn new(writer: W, limit: usize) -> Self {
393        Self {
394            writer,
395            written: 0,
396            limit,
397        }
398    }
399}
400
401impl<W: std::io::Write> std::io::Write for LimitedWriter<W> {
402    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
403        if self.written + buf.len() > self.limit {
404            return Err(std::io::Error::new(
405                std::io::ErrorKind::Other,
406                "Buffer limit exceeded",
407            ));
408        }
409        let wlen = self.writer.write(buf)?;
410        self.written += wlen;
411        Ok(wlen)
412    }
413
414    fn flush(&mut self) -> std::io::Result<()> {
415        self.writer.flush()
416    }
417}
418
419#[cfg(test)]
420mod tests {
421    use super::*;
422
423    use pink::chain_extension::HttpRequest;
424
425    #[test]
426    fn default_impl_works() {
427        use mock_ext::MockExtension;
428        use pink::chain_extension::PinkExtBackend;
429
430        let mock = MockExtension;
431        let ext = DefaultPinkExtension::<_, String>::new(&mock);
432
433        let key = b"key";
434        let value = b"value";
435        assert!(ext
436            .cache_set(Cow::Borrowed(key), Cow::Borrowed(value))
437            .is_ok());
438        assert!(ext.cache_set_expiration(Cow::Borrowed(key), 100).is_ok());
439        assert!(ext.cache_get(Cow::Borrowed(key)).is_ok());
440        assert!(ext.cache_remove(Cow::Borrowed(key)).is_ok());
441
442        ext.log(1, "error".into()).unwrap();
443        ext.log(2, "warn".into()).unwrap();
444        ext.log(3, "info".into()).unwrap();
445        ext.log(4, "debug".into()).unwrap();
446        ext.log(5, "trace".into()).unwrap();
447        ext.log(6, "unknown".into()).unwrap();
448
449        assert!(ext.is_in_transaction().is_ok());
450        assert!(ext.system_contract_id().is_err());
451        assert!(ext.balance_of([0u8; 32].into()).is_ok());
452        assert!(ext.worker_pubkey().is_ok());
453        assert!(ext.code_exists(Default::default(), false).is_ok());
454        assert!(ext.import_latest_system_code([0u8; 32].into()).is_ok());
455        assert!(ext.runtime_version().is_ok());
456        assert!(ext.current_event_chain_head().is_ok());
457    }
458
459    #[test]
460    fn test_too_large_batch_http_req() {
461        let requests = (0..10)
462            .map(|_| HttpRequest {
463                url: "https://www.google.com".into(),
464                method: "GET".into(),
465                headers: vec![],
466                body: vec![],
467            })
468            .collect::<Vec<_>>();
469        let responses = batch_http_request(requests, 10 * 1000);
470        assert!(responses.is_err());
471    }
472
473    #[cfg(coverage)]
474    #[tokio::test]
475    async fn test_http_req() {
476        let response = tokio::task::spawn_blocking(|| {
477            http_request(
478                HttpRequest {
479                    url: "https://httpbin.org/get".into(),
480                    method: "GET".into(),
481                    headers: vec![("X-Foo".to_string(), "bar".to_string())],
482                    body: vec![],
483                },
484                1000 * 10,
485            )
486        })
487        .await;
488        assert!(response.is_ok());
489    }
490
491    #[tokio::test]
492    async fn test_http_req_net_error() {
493        let response = tokio::task::spawn_blocking(|| {
494            http_request(
495                HttpRequest {
496                    url: "http://127.0.0.1:54321/get".into(),
497                    method: "GET".into(),
498                    headers: vec![],
499                    body: vec![],
500                },
501                1000 * 10,
502            )
503        })
504        .await;
505        assert_eq!(response.unwrap().unwrap().status_code, 523);
506    }
507
508    #[tokio::test]
509    async fn test_http_req_zero_timeout() {
510        let response = tokio::task::spawn_blocking(|| {
511            http_request(
512                HttpRequest {
513                    url: "https://httpbin.org/get".into(),
514                    method: "GET".into(),
515                    headers: vec![],
516                    body: vec![],
517                },
518                0,
519            )
520        })
521        .await;
522        assert!(response.unwrap().is_err());
523    }
524}