Skip to content

Commit a22b202

Browse files
authored
[fortuna] Add tests (#1133)
* fix stuff * ok that was annoying * ok * stuff * better tests * cleanup * pr comments
1 parent cc7054b commit a22b202

File tree

14 files changed

+457
-46
lines changed

14 files changed

+457
-46
lines changed

fortuna/Cargo.lock

Lines changed: 55 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

fortuna/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,8 @@ tracing = { version = "0.1.37", features = ["log"] }
2929
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
3030
utoipa = { version = "3.4.0", features = ["axum_extras"] }
3131
utoipa-swagger-ui = { version = "3.1.4", features = ["axum"] }
32+
once_cell = "1.18.0"
33+
lazy_static = "1.4.0"
34+
35+
[dev-dependencies]
36+
axum-test = "13.1.1"

fortuna/src/api.rs

Lines changed: 241 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
use {
22
crate::{
3-
ethereum::PythContract,
3+
chain::reader::EntropyReader,
44
state::HashChainState,
55
},
66
axum::{
7+
body::Body,
78
http::StatusCode,
89
response::{
910
IntoResponse,
1011
Response,
1112
},
13+
routing::get,
14+
Router,
1215
},
1316
ethers::core::types::Address,
1417
prometheus_client::{
@@ -51,13 +54,24 @@ pub struct ApiState {
5154
pub metrics: Arc<Metrics>,
5255
}
5356

57+
impl ApiState {
58+
pub fn new(chains: &[(ChainId, BlockchainState)]) -> ApiState {
59+
let map: HashMap<ChainId, BlockchainState> = chains.into_iter().cloned().collect();
60+
ApiState {
61+
chains: Arc::new(map),
62+
metrics: Arc::new(Metrics::new()),
63+
}
64+
}
65+
}
66+
5467
/// The state of the randomness service for a single blockchain.
68+
#[derive(Clone)]
5569
pub struct BlockchainState {
5670
/// The hash chain(s) required to serve random numbers for this blockchain
5771
pub state: Arc<HashChainState>,
58-
/// The EVM contract where the protocol is running.
59-
pub contract: Arc<PythContract>,
60-
/// The EVM address of the provider that this server is operating for.
72+
/// The contract that the server is fulfilling requests for.
73+
pub contract: Arc<dyn EntropyReader>,
74+
/// The address of the provider that this server is operating for.
6175
pub provider_address: Address,
6276
}
6377

@@ -137,3 +151,226 @@ impl IntoResponse for RestError {
137151
}
138152
}
139153
}
154+
155+
pub fn routes(state: ApiState) -> Router<(), Body> {
156+
Router::new()
157+
.route("/", get(index))
158+
.route("/live", get(live))
159+
.route("/metrics", get(metrics))
160+
.route("/ready", get(ready))
161+
.route("/v1/chains", get(chain_ids))
162+
.route(
163+
"/v1/chains/:chain_id/revelations/:sequence",
164+
get(revelation),
165+
)
166+
.with_state(state)
167+
}
168+
169+
#[cfg(test)]
170+
mod test {
171+
use {
172+
crate::{
173+
api::{
174+
self,
175+
ApiState,
176+
BinaryEncoding,
177+
Blob,
178+
BlockchainState,
179+
GetRandomValueResponse,
180+
},
181+
chain::reader::mock::MockEntropyReader,
182+
state::{
183+
HashChainState,
184+
PebbleHashChain,
185+
},
186+
},
187+
axum::http::StatusCode,
188+
axum_test::{
189+
TestResponse,
190+
TestServer,
191+
},
192+
ethers::prelude::Address,
193+
lazy_static::lazy_static,
194+
std::sync::Arc,
195+
};
196+
197+
const PROVIDER: Address = Address::zero();
198+
lazy_static! {
199+
static ref OTHER_PROVIDER: Address = Address::from_low_u64_be(1);
200+
// Note: these chains are immutable. They are wrapped in Arc because we need Arcs to
201+
// initialize the BlockchainStates below, but they aren't cloneable (nor do they need to be cloned).
202+
static ref ETH_CHAIN: Arc<HashChainState> = Arc::new(HashChainState::from_chain_at_offset(
203+
0,
204+
PebbleHashChain::new([0u8; 32], 1000),
205+
));
206+
static ref AVAX_CHAIN: Arc<HashChainState> = Arc::new(HashChainState::from_chain_at_offset(
207+
100,
208+
PebbleHashChain::new([1u8; 32], 1000),
209+
));
210+
}
211+
212+
fn test_server() -> (TestServer, Arc<MockEntropyReader>, Arc<MockEntropyReader>) {
213+
let eth_read = Arc::new(MockEntropyReader::with_requests(&[]));
214+
215+
let eth_state = BlockchainState {
216+
state: ETH_CHAIN.clone(),
217+
contract: eth_read.clone(),
218+
provider_address: PROVIDER,
219+
};
220+
221+
let avax_read = Arc::new(MockEntropyReader::with_requests(&[]));
222+
223+
let avax_state = BlockchainState {
224+
state: AVAX_CHAIN.clone(),
225+
contract: avax_read.clone(),
226+
provider_address: PROVIDER,
227+
};
228+
229+
let api_state = ApiState::new(&[
230+
("ethereum".into(), eth_state),
231+
("avalanche".into(), avax_state),
232+
]);
233+
234+
let app = api::routes(api_state);
235+
(TestServer::new(app).unwrap(), eth_read, avax_read)
236+
}
237+
238+
async fn get_and_assert_status(
239+
server: &TestServer,
240+
path: &str,
241+
status: StatusCode,
242+
) -> TestResponse {
243+
let response = server.get(path).await;
244+
response.assert_status(status);
245+
response
246+
}
247+
248+
#[tokio::test]
249+
async fn test_revelation() {
250+
let (server, eth_contract, avax_contract) = test_server();
251+
252+
// Can't access a revelation if it hasn't been requested
253+
get_and_assert_status(
254+
&server,
255+
"/v1/chains/ethereum/revelations/0",
256+
StatusCode::FORBIDDEN,
257+
)
258+
.await;
259+
260+
// Once someone requests the number, then it is accessible
261+
eth_contract.insert(PROVIDER, 0);
262+
let response =
263+
get_and_assert_status(&server, "/v1/chains/ethereum/revelations/0", StatusCode::OK)
264+
.await;
265+
response.assert_json(&GetRandomValueResponse {
266+
value: Blob::new(BinaryEncoding::Hex, ETH_CHAIN.reveal(0).unwrap()),
267+
});
268+
269+
// Each chain and provider has its own set of requests
270+
eth_contract.insert(PROVIDER, 100);
271+
eth_contract.insert(*OTHER_PROVIDER, 101);
272+
eth_contract.insert(PROVIDER, 102);
273+
avax_contract.insert(PROVIDER, 102);
274+
avax_contract.insert(PROVIDER, 103);
275+
avax_contract.insert(*OTHER_PROVIDER, 104);
276+
277+
let response = get_and_assert_status(
278+
&server,
279+
"/v1/chains/ethereum/revelations/100",
280+
StatusCode::OK,
281+
)
282+
.await;
283+
response.assert_json(&GetRandomValueResponse {
284+
value: Blob::new(BinaryEncoding::Hex, ETH_CHAIN.reveal(100).unwrap()),
285+
});
286+
287+
get_and_assert_status(
288+
&server,
289+
"/v1/chains/ethereum/revelations/101",
290+
StatusCode::FORBIDDEN,
291+
)
292+
.await;
293+
let response = get_and_assert_status(
294+
&server,
295+
"/v1/chains/ethereum/revelations/102",
296+
StatusCode::OK,
297+
)
298+
.await;
299+
response.assert_json(&GetRandomValueResponse {
300+
value: Blob::new(BinaryEncoding::Hex, ETH_CHAIN.reveal(102).unwrap()),
301+
});
302+
get_and_assert_status(
303+
&server,
304+
"/v1/chains/ethereum/revelations/103",
305+
StatusCode::FORBIDDEN,
306+
)
307+
.await;
308+
get_and_assert_status(
309+
&server,
310+
"/v1/chains/ethereum/revelations/104",
311+
StatusCode::FORBIDDEN,
312+
)
313+
.await;
314+
315+
get_and_assert_status(
316+
&server,
317+
"/v1/chains/avalanche/revelations/100",
318+
StatusCode::FORBIDDEN,
319+
)
320+
.await;
321+
get_and_assert_status(
322+
&server,
323+
"/v1/chains/avalanche/revelations/101",
324+
StatusCode::FORBIDDEN,
325+
)
326+
.await;
327+
let response = get_and_assert_status(
328+
&server,
329+
"/v1/chains/avalanche/revelations/102",
330+
StatusCode::OK,
331+
)
332+
.await;
333+
response.assert_json(&GetRandomValueResponse {
334+
value: Blob::new(BinaryEncoding::Hex, AVAX_CHAIN.reveal(102).unwrap()),
335+
});
336+
let response = get_and_assert_status(
337+
&server,
338+
"/v1/chains/avalanche/revelations/103",
339+
StatusCode::OK,
340+
)
341+
.await;
342+
response.assert_json(&GetRandomValueResponse {
343+
value: Blob::new(BinaryEncoding::Hex, AVAX_CHAIN.reveal(103).unwrap()),
344+
});
345+
get_and_assert_status(
346+
&server,
347+
"/v1/chains/avalanche/revelations/104",
348+
StatusCode::FORBIDDEN,
349+
)
350+
.await;
351+
352+
// Bad chain ids fail
353+
get_and_assert_status(
354+
&server,
355+
"/v1/chains/not_a_chain/revelations/0",
356+
StatusCode::BAD_REQUEST,
357+
)
358+
.await;
359+
360+
// Requesting a number that has a request, but isn't in the HashChainState also fails.
361+
// (Note that this shouldn't happen in normal operation)
362+
get_and_assert_status(
363+
&server,
364+
"/v1/chains/avalanche/revelations/99",
365+
StatusCode::FORBIDDEN,
366+
)
367+
.await;
368+
avax_contract.insert(PROVIDER, 99);
369+
get_and_assert_status(
370+
&server,
371+
"/v1/chains/avalanche/revelations/99",
372+
StatusCode::INTERNAL_SERVER_ERROR,
373+
)
374+
.await;
375+
}
376+
}

0 commit comments

Comments
 (0)