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::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.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/// `Option<S>` is a source that is simply absent: every call fails with
88/// [`crate::SourceError::Transport`]. Lets `Fallback<P, Option<B>>` express
89/// "backup configured or not" without a second type.
90#[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    /// A chain source that serves headers but has pruned every BAL.
207    struct HeadersOnly;
208    /// A chain source that serves BALs but whose headers must never be used.
209    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        // Chain facts: primary only, even though the backup is "ahead".
284        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        // Body from the backup, header from the primary.
288        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}