Skip to main content

bal_source/
fallback.rs

1//! Primary + backup source.
2//!
3//! The primary is the node that decides what the chain *is*: head,
4//! finalized, headers. That is the one thing balq takes on trust, and it is
5//! never delegated — if the primary is down, sync waits.
6//!
7//! The backup supplies only facts that are verified afterwards: the BAL
8//! body of a block the primary has pruned (checked against the primary's
9//! header hash), and proofs at blocks outside the primary's state window
10//! (checked against a `state_root` the archive already holds). It can be
11//! any third-party archive endpoint: it adds reach, not authority.
12
13use 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
19/// Primary for the chain, backup for old data.
20pub struct Fallback<P, B> {
21    /// Decides head/finalized/headers; asked first for everything.
22    pub primary: P,
23    /// Asked for a BAL body or a proof when the primary cannot serve it.
24    pub backup: B,
25}
26
27impl<P, B> Fallback<P, B> {
28    /// Wrap `primary` with `backup`.
29    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    /// Header always from the primary; body from the primary, or from the
49    /// backup if the primary has none. The archive verifies the body against
50    /// this header, so a backup body is held to the primary's chain.
51    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/// `Option<S>` is a source that is simply absent: every call fails with
97/// [`crate::SourceError::Transport`]. Lets `Fallback<P, Option<B>>` express
98/// "backup configured or not" without a second type.
99#[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
169/// The error to report when both failed: the primary's, if there is no
170/// backup at all (its absence is not the news), else both.
171fn 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    /// A chain source that serves headers but has pruned every BAL.
227    struct HeadersOnly;
228    /// A chain source that serves BALs but whose headers must never be used.
229    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        // Chain facts: primary only, even though the backup is "ahead".
304        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        // Body from the backup, header from the primary.
308        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    /// Without a backup the primary's own error is what the caller sees —
372    /// "no backup configured" is not the news.
373    #[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    /// With a backup that also fails, both errors are reported.
383    #[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}