Skip to content

Commit cfe0c29

Browse files
authored
Merge pull request #1087 from opentensor/spiigot/try-all-endpoints
Spiigot/ Try all Drand endpoints
2 parents 974e76b + c406084 commit cfe0c29

File tree

2 files changed

+274
-88
lines changed

2 files changed

+274
-88
lines changed

pallets/drand/src/lib.rs

Lines changed: 107 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
pub use pallet::*;
3838

3939
extern crate alloc;
40-
use crate::alloc::string::ToString;
4140

4241
use alloc::{format, string::String, vec, vec::Vec};
4342
use codec::Encode;
@@ -53,7 +52,6 @@ use scale_info::prelude::cmp;
5352
use sha2::{Digest, Sha256};
5453
use sp_core::blake2_256;
5554
use sp_runtime::{
56-
offchain::{http, Duration},
5755
traits::{Hash, One},
5856
transaction_validity::{InvalidTransaction, TransactionValidity, ValidTransaction},
5957
KeyTypeId, Saturating,
@@ -79,7 +77,14 @@ pub mod weights;
7977
pub use weights::*;
8078

8179
/// the main drand api endpoint
82-
pub const API_ENDPOINT: &str = "https://drand.cloudflare.com";
80+
const ENDPOINTS: [&str; 5] = [
81+
"https://api.drand.sh",
82+
"https://api2.drand.sh",
83+
"https://api3.drand.sh",
84+
"https://drand.cloudflare.com",
85+
"https://api.drand.secureweb3.com:6875",
86+
];
87+
8388
/// the drand quicknet chain hash
8489
/// quicknet uses 'Tiny' BLS381, with small 48-byte sigs in G1 and 96-byte pubkeys in G2
8590
pub const QUICKNET_CHAIN_HASH: &str =
@@ -390,15 +395,8 @@ impl<T: Config> Pallet<T> {
390395
}
391396

392397
let mut last_stored_round = LastStoredRound::<T>::get();
393-
let latest_pulse_body = Self::fetch_drand_latest().map_err(|_| "Failed to query drand")?;
394-
let latest_unbounded_pulse: DrandResponseBody = serde_json::from_str(&latest_pulse_body)
395-
.map_err(|_| {
396-
log::warn!(
397-
"Drand: Response that failed to deserialize: {}",
398-
latest_pulse_body
399-
);
400-
"Drand: Failed to serialize response body to pulse"
401-
})?;
398+
let latest_unbounded_pulse =
399+
Self::fetch_drand_latest().map_err(|_| "Failed to query drand")?;
402400
let latest_pulse = latest_unbounded_pulse
403401
.try_into_pulse()
404402
.map_err(|_| "Drand: Received pulse contains invalid data")?;
@@ -420,17 +418,8 @@ impl<T: Config> Pallet<T> {
420418
for round in (last_stored_round.saturating_add(1))
421419
..=(last_stored_round.saturating_add(rounds_to_fetch))
422420
{
423-
let pulse_body = Self::fetch_drand_by_round(round)
421+
let unbounded_pulse = Self::fetch_drand_by_round(round)
424422
.map_err(|_| "Drand: Failed to query drand for round")?;
425-
let unbounded_pulse: DrandResponseBody = serde_json::from_str(&pulse_body)
426-
.map_err(|_| {
427-
log::warn!(
428-
"Drand: Response that failed to deserialize for round {}: {}",
429-
round,
430-
pulse_body
431-
);
432-
"Drand: Failed to serialize response body to pulse"
433-
})?;
434423
let pulse = unbounded_pulse
435424
.try_into_pulse()
436425
.map_err(|_| "Drand: Received pulse contains invalid data")?;
@@ -470,42 +459,107 @@ impl<T: Config> Pallet<T> {
470459
Ok(())
471460
}
472461

473-
/// Query the endpoint `{api}/{chainHash}/info` to receive information about the drand chain
474-
/// Valid response bodies are deserialized into `BeaconInfoResponse`
475-
fn fetch_drand_by_round(round: RoundNumber) -> Result<String, http::Error> {
476-
let uri: &str = &format!("{}/{}/public/{}", API_ENDPOINT, CHAIN_HASH, round);
477-
Self::fetch(uri)
462+
fn fetch_drand_by_round(round: RoundNumber) -> Result<DrandResponseBody, &'static str> {
463+
let relative_path = format!("/{}/public/{}", CHAIN_HASH, round);
464+
Self::fetch_and_decode_from_any_endpoint(&relative_path)
478465
}
479-
fn fetch_drand_latest() -> Result<String, http::Error> {
480-
let uri: &str = &format!("{}/{}/public/latest", API_ENDPOINT, CHAIN_HASH);
481-
Self::fetch(uri)
466+
467+
fn fetch_drand_latest() -> Result<DrandResponseBody, &'static str> {
468+
let relative_path = format!("/{}/public/latest", CHAIN_HASH);
469+
Self::fetch_and_decode_from_any_endpoint(&relative_path)
482470
}
483471

484-
/// Fetch a remote URL and return the body of the response as a string.
485-
fn fetch(uri: &str) -> Result<String, http::Error> {
486-
let deadline =
487-
sp_io::offchain::timestamp().add(Duration::from_millis(T::HttpFetchTimeout::get()));
488-
let request = http::Request::get(uri);
489-
let pending = request.deadline(deadline).send().map_err(|_| {
490-
log::warn!("Drand: HTTP IO Error");
491-
http::Error::IoError
492-
})?;
493-
let response = pending.try_wait(deadline).map_err(|_| {
494-
log::warn!("Drand: HTTP Deadline Reached");
495-
http::Error::DeadlineReached
496-
})??;
497-
498-
if response.code != 200 {
499-
log::warn!("Drand: Unexpected status code: {}", response.code);
500-
return Err(http::Error::Unknown);
472+
/// Try to fetch from multiple endpoints simultaneously and return the first successfully decoded JSON response.
473+
fn fetch_and_decode_from_any_endpoint(
474+
relative_path: &str,
475+
) -> Result<DrandResponseBody, &'static str> {
476+
let uris: Vec<String> = ENDPOINTS
477+
.iter()
478+
.map(|e| format!("{}{}", e, relative_path))
479+
.collect();
480+
let deadline = sp_io::offchain::timestamp().add(
481+
sp_runtime::offchain::Duration::from_millis(T::HttpFetchTimeout::get()),
482+
);
483+
484+
let mut pending_requests: Vec<(String, sp_runtime::offchain::http::PendingRequest)> =
485+
vec![];
486+
487+
// Try sending requests to all endpoints.
488+
for uri in &uris {
489+
let request = sp_runtime::offchain::http::Request::get(uri);
490+
match request.deadline(deadline).send() {
491+
Ok(pending_req) => {
492+
pending_requests.push((uri.clone(), pending_req));
493+
}
494+
Err(_) => {
495+
log::warn!("Drand: HTTP IO Error on endpoint {}", uri);
496+
}
497+
}
498+
}
499+
500+
if pending_requests.is_empty() {
501+
log::warn!("Drand: No endpoints could be queried");
502+
return Err("Drand: No endpoints could be queried");
503+
}
504+
505+
loop {
506+
let now = sp_io::offchain::timestamp();
507+
if now > deadline {
508+
// We've passed our deadline without getting a valid response.
509+
log::warn!("Drand: HTTP Deadline Reached");
510+
break;
511+
}
512+
513+
let mut still_pending = false;
514+
let mut next_iteration_requests = Vec::new();
515+
516+
for (uri, request) in pending_requests.drain(..) {
517+
match request.try_wait(Some(deadline)) {
518+
Ok(Ok(response)) => {
519+
if response.code != 200 {
520+
log::warn!(
521+
"Drand: Unexpected status code: {} from {}",
522+
response.code,
523+
uri
524+
);
525+
continue;
526+
}
527+
528+
let body = response.body().collect::<Vec<u8>>();
529+
match serde_json::from_slice::<DrandResponseBody>(&body) {
530+
Ok(decoded) => {
531+
return Ok(decoded);
532+
}
533+
Err(e) => {
534+
log::warn!(
535+
"Drand: JSON decode error from {}: {}. Response body: {}",
536+
uri,
537+
e,
538+
String::from_utf8_lossy(&body)
539+
);
540+
}
541+
}
542+
}
543+
Ok(Err(e)) => {
544+
log::warn!("Drand: HTTP error from {}: {:?}", uri, e);
545+
}
546+
Err(pending_req) => {
547+
still_pending = true;
548+
next_iteration_requests.push((uri, pending_req));
549+
}
550+
}
551+
}
552+
553+
pending_requests = next_iteration_requests;
554+
555+
if !still_pending {
556+
break;
557+
}
501558
}
502-
let body = response.body().collect::<Vec<u8>>();
503-
let body_str = alloc::str::from_utf8(&body).map_err(|_| {
504-
log::warn!("Drand: No UTF8 body");
505-
http::Error::Unknown
506-
})?;
507559

508-
Ok(body_str.to_string())
560+
// If we reached here, no valid response was obtained from any endpoint.
561+
log::warn!("Drand: No valid response from any endpoint");
562+
Err("Drand: No valid response from any endpoint")
509563
}
510564

511565
/// get the randomness at a specific block height

0 commit comments

Comments
 (0)