Skip to main content

forest/rpc/methods/eth/
bloom.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4//! Ethereum block logs bloom support: the [`Bloom`] type and the derivation and storage of
5//! per-tipset block logs blooms.
6//!
7//! Blooms are stored when a tipset is executed, alongside its receipts and events, and
8//! served from the store when building Ethereum blocks. For tipsets never executed by
9//! this node (nor covered by index backfill), the block reports [`FULL_BLOOM`].
10
11use super::*;
12use crate::db::EthBlockBloomStore;
13
14/// Ethereum Bloom filter size in bits.
15/// Bloom filter is used in Ethereum to minimize the number of block queries.
16const BLOOM_SIZE: usize = 2048;
17
18/// Ethereum Bloom filter size in bytes.
19const BLOOM_SIZE_IN_BYTES: usize = BLOOM_SIZE / 8;
20
21/// Ethereum Bloom filter with all bits set to 1.
22pub(super) const FULL_BLOOM: [u8; BLOOM_SIZE_IN_BYTES] = [0xff; BLOOM_SIZE_IN_BYTES];
23
24/// Ethereum Bloom filter with all bits set to 0.
25pub(super) const EMPTY_BLOOM: [u8; BLOOM_SIZE_IN_BYTES] = [0x0; BLOOM_SIZE_IN_BYTES];
26
27/// Environment variable that enables computing (and storing) a block's logs bloom on a read
28/// miss, instead of reporting [`FULL_BLOOM`].
29pub(crate) const COMPUTE_BLOOM_ON_MISS_ENV: &str = "FOREST_ETH_RPC_COMPUTE_BLOOM_ON_MISS";
30
31#[derive(PartialEq, Debug, Deserialize, Serialize, Default, Clone, JsonSchema, GetSize)]
32pub struct Bloom(
33    #[schemars(with = "String")]
34    #[serde(with = "crate::lotus_json::hexify_bytes")]
35    #[get_size(ignore)]
36    pub ethereum_types::Bloom,
37);
38lotus_json_with_self!(Bloom);
39
40impl Bloom {
41    /// Accrues the raw input bytes into the bloom filter.
42    pub fn accrue(&mut self, input: &[u8]) {
43        self.0.accrue(ethereum_types::BloomInput::Raw(input));
44    }
45}
46
47/// Accrues an Ethereum log (its emitter address and topics) into the given bloom.
48pub(super) fn accrue_eth_log(bloom: &mut Bloom, address: &EthAddress, topics: &[EthHash]) {
49    for topic in topics {
50        bloom.accrue(topic.0.as_bytes());
51    }
52    bloom.accrue(address.0.as_bytes());
53}
54
55/// Computes the block logs bloom of a tipset directly from its executed messages, resolving
56/// event emitters against the post-execution state root.
57fn compute_block_logs_bloom(
58    state_manager: &StateManager,
59    state_root: &Cid,
60    executed_messages: &[ExecutedMessage],
61) -> anyhow::Result<Bloom> {
62    let state_tree = state_manager.get_state_tree(state_root)?;
63    let mut resolved_eth_addrs = HashMap::default();
64    let mut bloom = Bloom::default();
65    for executed_message in executed_messages {
66        let Some(events) = &executed_message.events else {
67            continue;
68        };
69        for event in events {
70            let emitter = event.emitter();
71            let address = resolved_eth_addrs.entry(emitter).or_insert_with(|| {
72                state_tree
73                    .resolve_to_deterministic_address(
74                        state_manager.chain_store().db(),
75                        FilecoinAddress::new_id(emitter),
76                    )
77                    .ok()
78                    .and_then(|addr| EthAddress::from_filecoin_address(&addr).ok())
79            });
80            let Some(address) = address else {
81                continue;
82            };
83            let entries: Vec<EventEntry> = event
84                .entries()
85                .into_iter()
86                .map(|entry| {
87                    let (flags, key, codec, value) = entry.into_parts();
88                    EventEntry {
89                        flags,
90                        key,
91                        codec,
92                        value: value.into(),
93                    }
94                })
95                .collect();
96            let Some((_data, topics)) = eth_log_from_event(&entries) else {
97                continue;
98            };
99            accrue_eth_log(&mut bloom, address, &topics);
100        }
101    }
102    Ok(bloom)
103}
104
105/// Computes and stores the block logs bloom of an executed tipset so that serving it later
106/// is a plain read. Called when a tipset is executed and from index backfill.
107pub(crate) fn store_block_logs_bloom(
108    state_manager: &StateManager,
109    tipset: &Tipset,
110    state_root: &Cid,
111    executed_messages: &[ExecutedMessage],
112) -> anyhow::Result<()> {
113    let key = tipset.key().cid()?;
114    if state_manager.db().read_bloom(&key)?.is_some() {
115        return Ok(());
116    }
117    let bloom = compute_block_logs_bloom(state_manager, state_root, executed_messages)?;
118    state_manager
119        .db()
120        .write_bloom(&key, tipset.epoch(), &bloom.0.0)
121}
122
123/// Returns the block's logs bloom: the stored bloom when available, otherwise a full
124/// (all-ones) bloom.
125/// Setting [`COMPUTE_BLOOM_ON_MISS_ENV`] computes and stores the bloom on a miss instead.
126pub(super) fn block_logs_bloom(
127    state_manager: &StateManager,
128    tipset: &Tipset,
129    state_root: &Cid,
130    executed_messages: &[ExecutedMessage],
131) -> anyhow::Result<Bloom> {
132    crate::def_is_env_truthy!(compute_bloom_on_miss, COMPUTE_BLOOM_ON_MISS_ENV);
133
134    let key = tipset.key().cid()?;
135    if let Some(bloom) = state_manager.db().read_bloom(&key)? {
136        return Ok(Bloom(ethereum_types::Bloom(bloom)));
137    }
138
139    if compute_bloom_on_miss() {
140        let bloom = compute_block_logs_bloom(state_manager, state_root, executed_messages)?;
141        state_manager
142            .db()
143            .write_bloom(&key, tipset.epoch(), &bloom.0.0)?;
144        return Ok(bloom);
145    }
146    Ok(Bloom(ethereum_types::Bloom(FULL_BLOOM)))
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    #[test]
154    fn test_accrue_eth_log_and_block_bloom_decomposition() {
155        let empty = Bloom::default();
156        let full = Bloom(ethereum_types::Bloom(FULL_BLOOM));
157
158        // No logs yields the all-zeros bloom — the "definitely no events here" case
159        // indexers rely on.
160        assert_eq!(empty.0.0, EMPTY_BLOOM);
161
162        let addr_a = EthAddress(ethereum_types::H160::from_slice(&[0x11; ADDRESS_LENGTH]));
163        let topic_a = EthHash(ethereum_types::H256::from_slice(&[0x22; EVM_WORD_LENGTH]));
164        let addr_b = EthAddress(ethereum_types::H160::from_slice(&[0x33; ADDRESS_LENGTH]));
165        let topic_b = EthHash(ethereum_types::H256::from_slice(&[0x44; EVM_WORD_LENGTH]));
166
167        // A real log sets some bits, but not all of them.
168        let mut bloom_a = empty.clone();
169        accrue_eth_log(&mut bloom_a, &addr_a, std::slice::from_ref(&topic_a));
170        assert_ne!(bloom_a, empty);
171        assert_ne!(bloom_a, full);
172
173        let mut bloom_b = empty;
174        accrue_eth_log(&mut bloom_b, &addr_b, std::slice::from_ref(&topic_b));
175
176        // The block bloom (both logs) equals the bitwise OR of the two individual
177        // (receipt) blooms.
178        let mut combined = bloom_a.clone();
179        accrue_eth_log(&mut combined, &addr_b, std::slice::from_ref(&topic_b));
180
181        let mut expected = bloom_a.0.0;
182        for (out, b) in expected.iter_mut().zip(bloom_b.0.0.iter()) {
183            *out |= *b;
184        }
185        assert_eq!(combined.0.0, expected);
186
187        // Accruing the same log twice equals accruing it once.
188        let mut twice = bloom_a.clone();
189        accrue_eth_log(&mut twice, &addr_a, std::slice::from_ref(&topic_a));
190        assert_eq!(twice, bloom_a);
191    }
192}