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}