Skip to main content

bal_source/
lib.rs

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