1use crate::{AccountProof, BalSource, Header, Result, SourcedBlock, StateSource};
14use alloy_primitives::{Address, B256};
15use async_trait::async_trait;
16use bal_codec::BlockAccessList;
17use tracing::{debug, info};
18
19pub struct Fallback<P, B> {
21 pub primary: P,
23 pub backup: B,
25}
26
27impl<P, B> Fallback<P, B> {
28 pub fn new(primary: P, backup: B) -> Self {
30 Self { primary, backup }
31 }
32}
33
34#[async_trait]
35impl<P: BalSource, B: BalSource> BalSource for Fallback<P, B> {
36 async fn head(&self) -> Result<u64> {
37 self.primary.head().await
38 }
39
40 async fn finalized(&self) -> Result<u64> {
41 self.primary.finalized().await
42 }
43
44 async fn header(&self, number: u64) -> Result<Header> {
45 self.primary.header(number).await
46 }
47
48 async fn block(&self, number: u64) -> Result<SourcedBlock> {
52 let header = self.primary.header(number).await?;
53 let bal = match self.primary.bal(number).await {
54 Ok(b) => b,
55 Err(e) => {
56 info!(block = number, %e, "primary has no BAL body; asking backup");
57 self.backup
58 .bal(number)
59 .await
60 .map_err(|b| prefer_primary(e, b))?
61 }
62 };
63 Ok(SourcedBlock { header, bal })
64 }
65
66 async fn bal(&self, number: u64) -> Result<BlockAccessList> {
67 match self.primary.bal(number).await {
68 Ok(b) => Ok(b),
69 Err(e) => {
70 info!(block = number, %e, "primary has no BAL body; asking backup");
71 self.backup
72 .bal(number)
73 .await
74 .map_err(|b| prefer_primary(e, b))
75 }
76 }
77 }
78}
79
80#[async_trait]
81impl<P: StateSource, B: StateSource> StateSource for Fallback<P, B> {
82 async fn proof(&self, addr: Address, slots: &[B256], block: u64) -> Result<AccountProof> {
83 match self.primary.proof(addr, slots, block).await {
84 Ok(p) => Ok(p),
85 Err(e) => {
86 debug!(block, %addr, slots = slots.len(), %e, "primary cannot prove; asking backup");
87 self.backup
88 .proof(addr, slots, block)
89 .await
90 .map_err(|b| prefer_primary(e, b))
91 }
92 }
93 }
94}
95
96#[async_trait]
100impl<S: BalSource> BalSource for Option<S> {
101 async fn head(&self) -> Result<u64> {
102 match self {
103 Some(s) => s.head().await,
104 None => Err(absent()),
105 }
106 }
107 async fn finalized(&self) -> Result<u64> {
108 match self {
109 Some(s) => s.finalized().await,
110 None => Err(absent()),
111 }
112 }
113 async fn block(&self, number: u64) -> Result<SourcedBlock> {
114 match self {
115 Some(s) => s.block(number).await,
116 None => Err(absent()),
117 }
118 }
119 async fn header(&self, number: u64) -> Result<Header> {
120 match self {
121 Some(s) => s.header(number).await,
122 None => Err(absent()),
123 }
124 }
125 async fn bal(&self, number: u64) -> Result<BlockAccessList> {
126 match self {
127 Some(s) => s.bal(number).await,
128 None => Err(absent()),
129 }
130 }
131}
132
133#[async_trait]
134impl<S: StateSource> StateSource for Option<S> {
135 async fn proof(&self, addr: Address, slots: &[B256], block: u64) -> Result<AccountProof> {
136 match self {
137 Some(s) => s.proof(addr, slots, block).await,
138 None => Err(absent()),
139 }
140 }
141}
142
143#[async_trait]
144impl<S: BalSource + ?Sized> BalSource for &S {
145 async fn head(&self) -> Result<u64> {
146 (**self).head().await
147 }
148 async fn finalized(&self) -> Result<u64> {
149 (**self).finalized().await
150 }
151 async fn block(&self, number: u64) -> Result<SourcedBlock> {
152 (**self).block(number).await
153 }
154 async fn header(&self, number: u64) -> Result<Header> {
155 (**self).header(number).await
156 }
157 async fn bal(&self, number: u64) -> Result<BlockAccessList> {
158 (**self).bal(number).await
159 }
160}
161
162#[async_trait]
163impl<S: StateSource + ?Sized> StateSource for &S {
164 async fn proof(&self, addr: Address, slots: &[B256], block: u64) -> Result<AccountProof> {
165 (**self).proof(addr, slots, block).await
166 }
167}
168
169fn prefer_primary(primary: crate::SourceError, backup: crate::SourceError) -> crate::SourceError {
172 match &backup {
173 crate::SourceError::Transport(m) if m == ABSENT => primary,
174 _ => crate::SourceError::Transport(format!("primary: {primary}; backup: {backup}")),
175 }
176}
177
178const ABSENT: &str = "no backup source configured";
179
180fn absent() -> crate::SourceError {
181 crate::SourceError::Transport(ABSENT.into())
182}
183
184#[cfg(test)]
185mod tests {
186 #![allow(clippy::unwrap_used)]
187 use super::*;
188 use crate::SourceError;
189 use alloy_primitives::U256;
190
191 struct Fails;
192 struct Answers(u64);
193
194 #[async_trait]
195 impl StateSource for Fails {
196 async fn proof(&self, _: Address, _: &[B256], b: u64) -> Result<AccountProof> {
197 Err(SourceError::Rpc {
198 code: -32602,
199 message: format!("distance to target block {b} exceeds maximum proof window"),
200 })
201 }
202 }
203
204 #[async_trait]
205 impl StateSource for Answers {
206 async fn proof(&self, addr: Address, slots: &[B256], _: u64) -> Result<AccountProof> {
207 Ok(AccountProof {
208 address: addr,
209 balance: U256::from(self.0),
210 nonce: 0,
211 code_hash: B256::ZERO,
212 storage_hash: B256::ZERO,
213 account_proof: vec![],
214 storage_proofs: slots
215 .iter()
216 .map(|k| crate::StorageProof {
217 key: *k,
218 value: U256::ZERO,
219 proof: vec![],
220 })
221 .collect(),
222 })
223 }
224 }
225
226 struct HeadersOnly;
228 struct BodiesOnly;
230
231 fn header(n: u64, tag: u8) -> Header {
232 Header {
233 number: n,
234 hash: B256::repeat_byte(tag),
235 parent_hash: B256::ZERO,
236 state_root: B256::ZERO,
237 timestamp: 0,
238 block_access_list_hash: Some(bal_codec::EMPTY_BAL_HASH),
239 }
240 }
241
242 #[async_trait]
243 impl BalSource for HeadersOnly {
244 async fn head(&self) -> Result<u64> {
245 Ok(100)
246 }
247 async fn finalized(&self) -> Result<u64> {
248 Ok(90)
249 }
250 async fn block(&self, n: u64) -> Result<SourcedBlock> {
251 Err(SourceError::NoBal(n))
252 }
253 async fn header(&self, n: u64) -> Result<Header> {
254 Ok(header(n, 0xAA))
255 }
256 async fn bal(&self, n: u64) -> Result<BlockAccessList> {
257 Err(SourceError::NoBal(n))
258 }
259 }
260
261 #[async_trait]
262 impl BalSource for BodiesOnly {
263 async fn head(&self) -> Result<u64> {
264 Ok(999)
265 }
266 async fn finalized(&self) -> Result<u64> {
267 Ok(998)
268 }
269 async fn block(&self, n: u64) -> Result<SourcedBlock> {
270 Ok(SourcedBlock {
271 header: header(n, 0xBB),
272 bal: BlockAccessList::default(),
273 })
274 }
275 async fn bal(&self, _: u64) -> Result<BlockAccessList> {
276 Ok(BlockAccessList::default())
277 }
278 }
279
280 #[tokio::test]
281 async fn backup_is_used_when_primary_fails() {
282 let f = Fallback::new(Fails, Answers(7));
283 let p = f.proof(Address::ZERO, &[B256::ZERO], 1).await.unwrap();
284 assert_eq!(p.balance, U256::from(7));
285 }
286
287 #[tokio::test]
288 async fn primary_wins_when_it_answers() {
289 let f = Fallback::new(Answers(1), Answers(2));
290 let p = f.proof(Address::ZERO, &[], 1).await.unwrap();
291 assert_eq!(p.balance, U256::from(1));
292 }
293
294 #[tokio::test]
295 async fn both_failing_returns_backup_error() {
296 let f = Fallback::new(Fails, Fails);
297 assert!(f.proof(Address::ZERO, &[], 1).await.is_err());
298 }
299
300 #[tokio::test]
301 async fn backup_supplies_body_but_never_the_chain() {
302 let f = Fallback::new(HeadersOnly, BodiesOnly);
303 assert_eq!(f.head().await.unwrap(), 100);
305 assert_eq!(f.finalized().await.unwrap(), 90);
306 assert_eq!(f.header(5).await.unwrap().hash, B256::repeat_byte(0xAA));
307 let b = f.block(5).await.unwrap();
309 assert_eq!(b.header.hash, B256::repeat_byte(0xAA));
310 assert!(b.bal.is_empty());
311 }
312
313 #[tokio::test]
314 async fn primary_down_means_no_chain_facts() {
315 struct Down;
316 #[async_trait]
317 impl BalSource for Down {
318 async fn head(&self) -> Result<u64> {
319 Err(SourceError::Transport("down".into()))
320 }
321 async fn finalized(&self) -> Result<u64> {
322 Err(SourceError::Transport("down".into()))
323 }
324 async fn block(&self, n: u64) -> Result<SourcedBlock> {
325 Err(SourceError::BlockNotFound(n))
326 }
327 }
328 let f = Fallback::new(Down, BodiesOnly);
329 assert!(f.head().await.is_err(), "backup must not decide the head");
330 assert!(
331 f.block(1).await.is_err(),
332 "no header from primary, no block"
333 );
334 }
335}
336
337#[cfg(test)]
338mod error_tests {
339 #![allow(clippy::unwrap_used)]
340 use super::*;
341 use crate::SourceError;
342
343 struct Down;
344
345 #[async_trait]
346 impl BalSource for Down {
347 async fn head(&self) -> Result<u64> {
348 Err(SourceError::Transport("primary down".into()))
349 }
350 async fn finalized(&self) -> Result<u64> {
351 Err(SourceError::Transport("primary down".into()))
352 }
353 async fn block(&self, n: u64) -> Result<SourcedBlock> {
354 Err(SourceError::NoBal(n))
355 }
356 async fn header(&self, n: u64) -> Result<Header> {
357 Ok(Header {
358 number: n,
359 hash: B256::ZERO,
360 parent_hash: B256::ZERO,
361 state_root: B256::ZERO,
362 timestamp: 0,
363 block_access_list_hash: None,
364 })
365 }
366 async fn bal(&self, n: u64) -> Result<BlockAccessList> {
367 Err(SourceError::NoBal(n))
368 }
369 }
370
371 #[tokio::test]
374 async fn no_backup_reports_the_primary_error() {
375 let src: Fallback<Down, Option<Down>> = Fallback::new(Down, None);
376 let e = src.bal(7).await.unwrap_err();
377 assert!(matches!(e, SourceError::NoBal(7)), "{e}");
378 let e = src.block(7).await.unwrap_err();
379 assert!(matches!(e, SourceError::NoBal(7)), "{e}");
380 }
381
382 #[tokio::test]
384 async fn both_failing_reports_both() {
385 let src = Fallback::new(Down, Down);
386 let e = src.bal(7).await.unwrap_err().to_string();
387 assert!(e.contains("primary:") && e.contains("backup:"), "{e}");
388 }
389}