37
37
pub use pallet:: * ;
38
38
39
39
extern crate alloc;
40
- use crate :: alloc:: string:: ToString ;
41
40
42
41
use alloc:: { format, string:: String , vec, vec:: Vec } ;
43
42
use codec:: Encode ;
@@ -53,7 +52,6 @@ use scale_info::prelude::cmp;
53
52
use sha2:: { Digest , Sha256 } ;
54
53
use sp_core:: blake2_256;
55
54
use sp_runtime:: {
56
- offchain:: { http, Duration } ,
57
55
traits:: { Hash , One } ,
58
56
transaction_validity:: { InvalidTransaction , TransactionValidity , ValidTransaction } ,
59
57
KeyTypeId , Saturating ,
@@ -79,7 +77,14 @@ pub mod weights;
79
77
pub use weights:: * ;
80
78
81
79
/// 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
+
83
88
/// the drand quicknet chain hash
84
89
/// quicknet uses 'Tiny' BLS381, with small 48-byte sigs in G1 and 96-byte pubkeys in G2
85
90
pub const QUICKNET_CHAIN_HASH : & str =
@@ -390,15 +395,8 @@ impl<T: Config> Pallet<T> {
390
395
}
391
396
392
397
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" ) ?;
402
400
let latest_pulse = latest_unbounded_pulse
403
401
. try_into_pulse ( )
404
402
. map_err ( |_| "Drand: Received pulse contains invalid data" ) ?;
@@ -420,17 +418,8 @@ impl<T: Config> Pallet<T> {
420
418
for round in ( last_stored_round. saturating_add ( 1 ) )
421
419
..=( last_stored_round. saturating_add ( rounds_to_fetch) )
422
420
{
423
- let pulse_body = Self :: fetch_drand_by_round ( round)
421
+ let unbounded_pulse = Self :: fetch_drand_by_round ( round)
424
422
. 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
- } ) ?;
434
423
let pulse = unbounded_pulse
435
424
. try_into_pulse ( )
436
425
. map_err ( |_| "Drand: Received pulse contains invalid data" ) ?;
@@ -470,42 +459,107 @@ impl<T: Config> Pallet<T> {
470
459
Ok ( ( ) )
471
460
}
472
461
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)
478
465
}
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)
482
470
}
483
471
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
+ }
501
558
}
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
- } ) ?;
507
559
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" )
509
563
}
510
564
511
565
/// get the randomness at a specific block height
0 commit comments