1use crate::{AccountProof, BalSource, Header, Result, SourcedBlock, StateSource};
14use alloy_primitives::{Address, B256};
15use async_trait::async_trait;
16use bal_codec::BlockAccessList;
17use tracing::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.bal(number).await?
58 }
59 };
60 Ok(SourcedBlock { header, bal })
61 }
62
63 async fn bal(&self, number: u64) -> Result<BlockAccessList> {
64 match self.primary.bal(number).await {
65 Ok(b) => Ok(b),
66 Err(e) => {
67 info!(block = number, %e, "primary has no BAL body; asking backup");
68 self.backup.bal(number).await
69 }
70 }
71 }
72}
73
74#[async_trait]
75impl<P: StateSource, B: StateSource> StateSource for Fallback<P, B> {
76 async fn proof(&self, addr: Address, slots: &[B256], block: u64) -> Result<AccountProof> {
77 match self.primary.proof(addr, slots, block).await {
78 Ok(p) => Ok(p),
79 Err(e) => {
80 info!(block, %addr, slots = slots.len(), %e, "primary cannot prove; asking backup");
81 self.backup.proof(addr, slots, block).await
82 }
83 }
84 }
85}
86
87#[async_trait]
91impl<S: BalSource> BalSource for Option<S> {
92 async fn head(&self) -> Result<u64> {
93 match self {
94 Some(s) => s.head().await,
95 None => Err(absent()),
96 }
97 }
98 async fn finalized(&self) -> Result<u64> {
99 match self {
100 Some(s) => s.finalized().await,
101 None => Err(absent()),
102 }
103 }
104 async fn block(&self, number: u64) -> Result<SourcedBlock> {
105 match self {
106 Some(s) => s.block(number).await,
107 None => Err(absent()),
108 }
109 }
110 async fn header(&self, number: u64) -> Result<Header> {
111 match self {
112 Some(s) => s.header(number).await,
113 None => Err(absent()),
114 }
115 }
116 async fn bal(&self, number: u64) -> Result<BlockAccessList> {
117 match self {
118 Some(s) => s.bal(number).await,
119 None => Err(absent()),
120 }
121 }
122}
123
124#[async_trait]
125impl<S: StateSource> StateSource for Option<S> {
126 async fn proof(&self, addr: Address, slots: &[B256], block: u64) -> Result<AccountProof> {
127 match self {
128 Some(s) => s.proof(addr, slots, block).await,
129 None => Err(absent()),
130 }
131 }
132}
133
134#[async_trait]
135impl<S: BalSource + ?Sized> BalSource for &S {
136 async fn head(&self) -> Result<u64> {
137 (**self).head().await
138 }
139 async fn finalized(&self) -> Result<u64> {
140 (**self).finalized().await
141 }
142 async fn block(&self, number: u64) -> Result<SourcedBlock> {
143 (**self).block(number).await
144 }
145 async fn header(&self, number: u64) -> Result<Header> {
146 (**self).header(number).await
147 }
148 async fn bal(&self, number: u64) -> Result<BlockAccessList> {
149 (**self).bal(number).await
150 }
151}
152
153#[async_trait]
154impl<S: StateSource + ?Sized> StateSource for &S {
155 async fn proof(&self, addr: Address, slots: &[B256], block: u64) -> Result<AccountProof> {
156 (**self).proof(addr, slots, block).await
157 }
158}
159
160fn absent() -> crate::SourceError {
161 crate::SourceError::Transport("no backup source configured".into())
162}
163
164#[cfg(test)]
165mod tests {
166 #![allow(clippy::unwrap_used)]
167 use super::*;
168 use crate::SourceError;
169 use alloy_primitives::U256;
170
171 struct Fails;
172 struct Answers(u64);
173
174 #[async_trait]
175 impl StateSource for Fails {
176 async fn proof(&self, _: Address, _: &[B256], b: u64) -> Result<AccountProof> {
177 Err(SourceError::Rpc {
178 code: -32602,
179 message: format!("distance to target block {b} exceeds maximum proof window"),
180 })
181 }
182 }
183
184 #[async_trait]
185 impl StateSource for Answers {
186 async fn proof(&self, addr: Address, slots: &[B256], _: u64) -> Result<AccountProof> {
187 Ok(AccountProof {
188 address: addr,
189 balance: U256::from(self.0),
190 nonce: 0,
191 code_hash: B256::ZERO,
192 storage_hash: B256::ZERO,
193 account_proof: vec![],
194 storage_proofs: slots
195 .iter()
196 .map(|k| crate::StorageProof {
197 key: *k,
198 value: U256::ZERO,
199 proof: vec![],
200 })
201 .collect(),
202 })
203 }
204 }
205
206 struct HeadersOnly;
208 struct BodiesOnly;
210
211 fn header(n: u64, tag: u8) -> Header {
212 Header {
213 number: n,
214 hash: B256::repeat_byte(tag),
215 parent_hash: B256::ZERO,
216 state_root: B256::ZERO,
217 timestamp: 0,
218 block_access_list_hash: Some(bal_codec::EMPTY_BAL_HASH),
219 }
220 }
221
222 #[async_trait]
223 impl BalSource for HeadersOnly {
224 async fn head(&self) -> Result<u64> {
225 Ok(100)
226 }
227 async fn finalized(&self) -> Result<u64> {
228 Ok(90)
229 }
230 async fn block(&self, n: u64) -> Result<SourcedBlock> {
231 Err(SourceError::NoBal(n))
232 }
233 async fn header(&self, n: u64) -> Result<Header> {
234 Ok(header(n, 0xAA))
235 }
236 async fn bal(&self, n: u64) -> Result<BlockAccessList> {
237 Err(SourceError::NoBal(n))
238 }
239 }
240
241 #[async_trait]
242 impl BalSource for BodiesOnly {
243 async fn head(&self) -> Result<u64> {
244 Ok(999)
245 }
246 async fn finalized(&self) -> Result<u64> {
247 Ok(998)
248 }
249 async fn block(&self, n: u64) -> Result<SourcedBlock> {
250 Ok(SourcedBlock {
251 header: header(n, 0xBB),
252 bal: BlockAccessList::default(),
253 })
254 }
255 async fn bal(&self, _: u64) -> Result<BlockAccessList> {
256 Ok(BlockAccessList::default())
257 }
258 }
259
260 #[tokio::test]
261 async fn backup_is_used_when_primary_fails() {
262 let f = Fallback::new(Fails, Answers(7));
263 let p = f.proof(Address::ZERO, &[B256::ZERO], 1).await.unwrap();
264 assert_eq!(p.balance, U256::from(7));
265 }
266
267 #[tokio::test]
268 async fn primary_wins_when_it_answers() {
269 let f = Fallback::new(Answers(1), Answers(2));
270 let p = f.proof(Address::ZERO, &[], 1).await.unwrap();
271 assert_eq!(p.balance, U256::from(1));
272 }
273
274 #[tokio::test]
275 async fn both_failing_returns_backup_error() {
276 let f = Fallback::new(Fails, Fails);
277 assert!(f.proof(Address::ZERO, &[], 1).await.is_err());
278 }
279
280 #[tokio::test]
281 async fn backup_supplies_body_but_never_the_chain() {
282 let f = Fallback::new(HeadersOnly, BodiesOnly);
283 assert_eq!(f.head().await.unwrap(), 100);
285 assert_eq!(f.finalized().await.unwrap(), 90);
286 assert_eq!(f.header(5).await.unwrap().hash, B256::repeat_byte(0xAA));
287 let b = f.block(5).await.unwrap();
289 assert_eq!(b.header.hash, B256::repeat_byte(0xAA));
290 assert!(b.bal.is_empty());
291 }
292
293 #[tokio::test]
294 async fn primary_down_means_no_chain_facts() {
295 struct Down;
296 #[async_trait]
297 impl BalSource for Down {
298 async fn head(&self) -> Result<u64> {
299 Err(SourceError::Transport("down".into()))
300 }
301 async fn finalized(&self) -> Result<u64> {
302 Err(SourceError::Transport("down".into()))
303 }
304 async fn block(&self, n: u64) -> Result<SourcedBlock> {
305 Err(SourceError::BlockNotFound(n))
306 }
307 }
308 let f = Fallback::new(Down, BodiesOnly);
309 assert!(f.head().await.is_err(), "backup must not decide the head");
310 assert!(
311 f.block(1).await.is_err(),
312 "no header from primary, no block"
313 );
314 }
315}