Skip to main content

bal_source/
lib.rs

1#![doc = include_str!("../README.md")]
2//!
3//! Source abstraction: the archive never talks to a node directly, it talks
4//! to a [`BalSource`] (blocks + BALs) and a [`StateSource`] (Merkle proofs for
5//! bootstrap). Implementations: JSON-RPC (here), Engine API and in-process
6//! (later crates). Which of these actually work is the day-0 question; the
7//! archive does not care.
8
9mod fallback;
10mod jsonrpc;
11pub mod proof;
12
13pub use fallback::Fallback;
14pub use jsonrpc::{BalProbe, JsonRpcSource, ProbeReport, BAL_HASH_FIELD, BAL_METHOD};
15pub use proof::{check_requested, verify_account_proof, ProofError};
16
17use alloy_primitives::{Address, Bytes, B256, U256};
18use async_trait::async_trait;
19use bal_codec::BlockAccessList;
20
21/// What can go wrong between the archive and a node.
22#[derive(Debug, thiserror::Error)]
23pub enum SourceError {
24    /// HTTP / connection failure.
25    #[error("transport: {0}")]
26    Transport(String),
27    /// The node answered with a JSON-RPC error object.
28    #[error("rpc error {code}: {message}")]
29    Rpc {
30        /// JSON-RPC error code.
31        code: i64,
32        /// JSON-RPC error message.
33        message: String,
34    },
35    /// `eth_getBlockByNumber` returned null.
36    #[error("block {0} not found")]
37    BlockNotFound(u64),
38    /// The block exists but the node has no BAL for it (pruned or pre-fork).
39    #[error("eth_getBlockAccessList returned null for block {0}")]
40    NoBal(u64),
41    /// The header carries no `blockAccessListHash`.
42    #[error("header of block {0} has no block_access_list_hash")]
43    NoBalHash(u64),
44    /// A response did not have the expected shape.
45    #[error("malformed response: {0}")]
46    Malformed(String),
47    /// The BAL failed to decode or validate.
48    #[error("codec: {0}")]
49    Codec(#[from] bal_codec::CodecError),
50}
51
52/// Result of source operations.
53pub type Result<T> = std::result::Result<T, SourceError>;
54
55/// The parts of a block header the archive needs. Kept minimal on purpose:
56/// the full Glamsterdam header layout is not frozen, and only these fields
57/// participate in verification.
58#[derive(Clone, Debug, PartialEq, Eq)]
59pub struct Header {
60    /// Block height.
61    pub number: u64,
62    /// Block hash as reported by the node.
63    pub hash: B256,
64    /// Hash of the parent block; used to detect reorgs between passes.
65    pub parent_hash: B256,
66    /// Post-state root; Merkle proofs are verified against it.
67    pub state_root: B256,
68    /// Block timestamp (seconds).
69    pub timestamp: u64,
70    /// `None` on pre-Glamsterdam blocks or clients that do not expose it.
71    pub block_access_list_hash: Option<B256>,
72}
73
74/// A block as the archive consumes it: header plus decoded, *unverified* BAL.
75/// Verification against `header.block_access_list_hash` is the archive's job
76/// so that no source implementation can skip it.
77#[derive(Clone, Debug)]
78pub struct SourcedBlock {
79    /// Header fields relevant to verification.
80    pub header: Header,
81    /// Decoded BAL, not yet checked against the header.
82    pub bal: BlockAccessList,
83}
84
85/// Where blocks and their BALs come from.
86#[async_trait]
87pub trait BalSource: Send + Sync {
88    /// Latest block number the source knows.
89    async fn head(&self) -> Result<u64>;
90    /// Latest finalized block number (reorg horizon).
91    async fn finalized(&self) -> Result<u64>;
92    /// Header + BAL for one block.
93    async fn block(&self, number: u64) -> Result<SourcedBlock>;
94    /// Header only. Used for reorg checks, where fetching the BAL would be
95    /// wasted work; the default goes through [`BalSource::block`].
96    async fn header(&self, number: u64) -> Result<Header> {
97        Ok(self.block(number).await?.header)
98    }
99    /// BAL body only, unverified. Lets a backup supply the body while the
100    /// header — the canonical-chain fact — still comes from the primary.
101    async fn bal(&self, number: u64) -> Result<BlockAccessList> {
102        Ok(self.block(number).await?.bal)
103    }
104}
105
106/// One slot's proof against an account's storage root.
107#[derive(Clone, Debug)]
108pub struct StorageProof {
109    /// Slot key (32 bytes).
110    pub key: B256,
111    /// Value at that slot; zero means absent, proven by exclusion.
112    pub value: U256,
113    /// Trie nodes from the storage root down to the leaf (or to the point of exclusion).
114    pub proof: Vec<Bytes>,
115}
116
117/// `eth_getProof` response: account leaf plus any number of storage proofs.
118#[derive(Clone, Debug)]
119pub struct AccountProof {
120    /// Account address.
121    pub address: Address,
122    /// Account balance.
123    pub balance: U256,
124    /// Account nonce.
125    pub nonce: u64,
126    /// keccak of the account's code.
127    pub code_hash: B256,
128    /// Root of the account's storage trie.
129    pub storage_hash: B256,
130    /// Trie nodes from the state root down to the account leaf.
131    pub account_proof: Vec<Bytes>,
132    /// Proofs for the requested slots, in request order.
133    pub storage_proofs: Vec<StorageProof>,
134}
135
136/// Where Merkle proofs come from.
137#[async_trait]
138pub trait StateSource: Send + Sync {
139    /// `eth_getProof(addr, slots, block)`. One call per (address, block)
140    /// carries any number of slots — batching happens here, not in JSON-RPC.
141    async fn proof(&self, addr: Address, slots: &[B256], block: u64) -> Result<AccountProof>;
142}