1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
//! # drand-client-rs
//!
//! `drand_client_rs` is a small rust library for retrieving random numbers from the [drand network](https://drand.love).
//!

extern crate core;

pub mod chain_info;
pub mod http;
pub mod verify;

use crate::chain_info::ChainInfo;
use crate::http::{new_http_transport, HttpTransport};
use crate::verify::{verify_beacon, Beacon};
use crate::DrandClientError::{InvalidChainInfo, InvalidRound};
use thiserror::Error;

/// a struct encapsulating all the necessary state for retrieving and validating drand beacons.
pub struct DrandClient<'a, T: Transport> {
    transport: T,
    base_url: &'a str,
    chain_info: ChainInfo,
}

/// create a new instance of the client with an HTTP transport for a given `base_url`.
/// Supported `base_url`s include: "<https://api.drand.sh>", "<https://drand.cloudflare.com>" and "<https://api.drand.secureweb3.com:6875>".
/// A full list can be found at <https://drand.love/developer/>
pub fn new_http_client(base_url: &str) -> Result<DrandClient<HttpTransport>, DrandClientError> {
    let http_transport = new_http_transport();
    let chain_info = fetch_chain_info(&http_transport, base_url)?;
    Ok(DrandClient {
        base_url,
        transport: http_transport,
        chain_info,
    })
}

/// represents a transport on which to connect to the drand network. This crate provides an
/// HTTP transport out of the box, which can be created by calling `new_http_transport()`
pub trait Transport {
    fn fetch(&self, url: &str) -> Result<String, TransportError>;
}

/// fetch the chain info for a given URL. The chain info contains the public key (used to
/// verify beacons) and the genesis time (used to calculate the time for given rounds).
pub fn fetch_chain_info(
    transport: &HttpTransport,
    base_url: &str,
) -> Result<ChainInfo, DrandClientError> {
    let url = format!("{base_url}/info");
    match transport.fetch(&url) {
        Err(_) => Err(DrandClientError::NotResponding),
        Ok(body) => serde_json::from_str(&body).map_err(|e| {
            println!("{}", e);
            InvalidChainInfo
        }),
    }
}

/// an implementation of the logic for retrieving randomness
impl<'a, T: Transport> DrandClient<'a, T> {
    /// fetch the latest available randomness beacon
    pub fn latest_randomness(&self) -> Result<Beacon, DrandClientError> {
        self.fetch_beacon_tag("latest")
    }

    /// fetch a randomness beacon for a specific round
    pub fn randomness(&self, round_number: u64) -> Result<Beacon, DrandClientError> {
        if round_number == 0 {
            Err(InvalidRound)
        } else {
            self.fetch_beacon_tag(&format!("{round_number}"))
        }
    }

    fn fetch_beacon_tag(&self, tag: &str) -> Result<Beacon, DrandClientError> {
        let url = format!("{}/public/{}", self.base_url, tag);

        match self.transport.fetch(&url) {
            Err(_) => Err(DrandClientError::NotResponding),

            Ok(body) => match serde_json::from_str::<Beacon>(&body) {
                Ok(beacon) => {
                    verify_beacon(
                        &self.chain_info.scheme_id,
                        &self.chain_info.public_key,
                        &beacon,
                    )
                    .map_err(|_| DrandClientError::FailedVerification)?;
                    Ok(beacon)
                }
                Err(_) => Err(DrandClientError::InvalidBeacon),
            },
        }
    }
}

#[derive(Error, Debug, PartialEq)]
pub enum DrandClientError {
    #[error("invalid round")]
    InvalidRound,
    #[error("invalid beacon")]
    InvalidBeacon,
    #[error("beacon failed verification")]
    FailedVerification,
    #[error("invalid chain info")]
    InvalidChainInfo,
    #[error("not responding")]
    NotResponding,
}

#[derive(Error, Debug)]
pub enum TransportError {
    #[error("not found")]
    NotFound,
    #[error("unexpected")]
    Unexpected,
}

#[cfg(test)]
mod test {
    use crate::DrandClientError::InvalidRound;
    use crate::{new_http_client, DrandClientError};

    #[test]
    fn request_chained_randomness_success() -> Result<(), DrandClientError> {
        let chained_url = "https://api.drand.sh";
        let client = new_http_client(chained_url)?;
        let randomness = client.latest_randomness()?;
        assert!(randomness.round_number > 0);
        Ok(())
    }

    #[test]
    fn request_unchained_randomness_success() -> Result<(), DrandClientError> {
        let unchained_url = "https://pl-eu.testnet.drand.sh/7672797f548f3f4748ac4bf3352fc6c6b6468c9ad40ad456a397545c6e2df5bf";
        let client = new_http_client(unchained_url)?;
        let randomness = client.latest_randomness()?;
        assert!(randomness.round_number > 0);
        Ok(())
    }

    #[test]
    fn request_genesis_returns_error() -> Result<(), DrandClientError> {
        let chained_url = "https://api.drand.sh";
        let client = new_http_client(chained_url)?;
        let result = client.randomness(0);
        assert!(result.is_err());
        assert_eq!(result.unwrap_err(), InvalidRound);
        Ok(())
    }

    #[test]
    fn request_g1g2swapped_beacon_succeeds() -> Result<(), DrandClientError> {
        let unchained_url =
            "https://api.drand.sh/dbd506d6ef76e5f386f41c651dcb808c5bcbd75471cc4eafa3f4df7ad4e4c493";
        let client = new_http_client(unchained_url)?;
        client.randomness(1)?;
        Ok(())
    }

    #[test]
    fn request_g1g2swapped_rfc_beacon_succeeds() -> Result<(), DrandClientError> {
        let unchained_url =
            "https://api.drand.sh/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971";
        let client = new_http_client(unchained_url)?;
        client.randomness(1)?;
        Ok(())
    }

    #[test]
    fn request_g1g2swapped_rfc_latest_succeeds() -> Result<(), DrandClientError> {
        let unchained_url =
            "https://api.drand.sh/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971";
        let client = new_http_client(unchained_url)?;
        client.latest_randomness()?;
        Ok(())
    }
}