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    /// A response body exceeded the size cap. Not retried: the node would
48    /// send the same body again.
49    #[error("{method}: response of {bytes} bytes exceeds the {limit}-byte limit")]
50    TooLarge {
51        /// JSON-RPC method.
52        method: String,
53        /// Announced or accumulated size.
54        bytes: u64,
55        /// The cap.
56        limit: u64,
57    },
58    /// The BAL failed to decode or validate.
59    #[error("codec: {0}")]
60    Codec(#[from] bal_codec::CodecError),
61}
62
63/// Result of source operations.
64pub type Result<T> = std::result::Result<T, SourceError>;
65
66/// The parts of a block header the archive needs. Kept minimal on purpose:
67/// the full Glamsterdam header layout is not frozen, and only these fields
68/// participate in verification.
69#[derive(Clone, Debug, PartialEq, Eq)]
70pub struct Header {
71    /// Block height.
72    pub number: u64,
73    /// Block hash as reported by the node.
74    pub hash: B256,
75    /// Hash of the parent block; used to detect reorgs between passes.
76    pub parent_hash: B256,
77    /// Post-state root; Merkle proofs are verified against it.
78    pub state_root: B256,
79    /// Block timestamp (seconds).
80    pub timestamp: u64,
81    /// `None` on pre-Glamsterdam blocks or clients that do not expose it.
82    pub block_access_list_hash: Option<B256>,
83}
84
85/// A block as the archive consumes it: header plus decoded, *unverified* BAL.
86/// Verification against `header.block_access_list_hash` is the archive's job
87/// so that no source implementation can skip it.
88#[derive(Clone, Debug)]
89pub struct SourcedBlock {
90    /// Header fields relevant to verification.
91    pub header: Header,
92    /// Decoded BAL, not yet checked against the header.
93    pub bal: BlockAccessList,
94}
95
96/// Where blocks and their BALs come from.
97#[async_trait]
98pub trait BalSource: Send + Sync {
99    /// Latest block number the source knows.
100    async fn head(&self) -> Result<u64>;
101    /// Latest finalized block number (reorg horizon).
102    async fn finalized(&self) -> Result<u64>;
103    /// Header + BAL for one block.
104    async fn block(&self, number: u64) -> Result<SourcedBlock>;
105    /// Header only. Used for reorg checks, where fetching the BAL would be
106    /// wasted work; the default goes through [`BalSource::block`].
107    async fn header(&self, number: u64) -> Result<Header> {
108        Ok(self.block(number).await?.header)
109    }
110    /// BAL body only, unverified. Lets a backup supply the body while the
111    /// header — the canonical-chain fact — still comes from the primary.
112    async fn bal(&self, number: u64) -> Result<BlockAccessList> {
113        Ok(self.block(number).await?.bal)
114    }
115}
116
117/// One slot's proof against an account's storage root.
118#[derive(Clone, Debug)]
119pub struct StorageProof {
120    /// Slot key (32 bytes).
121    pub key: B256,
122    /// Value at that slot; zero means absent, proven by exclusion.
123    pub value: U256,
124    /// Trie nodes from the storage root down to the leaf (or to the point of exclusion).
125    pub proof: Vec<Bytes>,
126}
127
128/// `eth_getProof` response: account leaf plus any number of storage proofs.
129#[derive(Clone, Debug)]
130pub struct AccountProof {
131    /// Account address.
132    pub address: Address,
133    /// Account balance.
134    pub balance: U256,
135    /// Account nonce.
136    pub nonce: u64,
137    /// keccak of the account's code.
138    pub code_hash: B256,
139    /// Root of the account's storage trie.
140    pub storage_hash: B256,
141    /// Trie nodes from the state root down to the account leaf.
142    pub account_proof: Vec<Bytes>,
143    /// Proofs for the requested slots, in request order.
144    pub storage_proofs: Vec<StorageProof>,
145}
146
147/// Where Merkle proofs come from.
148#[async_trait]
149pub trait StateSource: Send + Sync {
150    /// `eth_getProof(addr, slots, block)`. One call per (address, block)
151    /// carries any number of slots — batching happens here, not in JSON-RPC.
152    async fn proof(&self, addr: Address, slots: &[B256], block: u64) -> Result<AccountProof>;
153}