From 221f098c07b23fc8a9af7b1904484011e2beb147 Mon Sep 17 00:00:00 2001 From: Aryan Tikarya Date: Wed, 1 Jul 2026 16:00:47 +0530 Subject: [PATCH 1/9] feat: add EthBlockBloom store and column --- CHANGELOG.md | 2 + scripts/tests/api_compare/filter-list | 3 - scripts/tests/api_compare/filter-list-gateway | 3 - src/db/car/many.rs | 18 +++- src/db/db_impl.rs | 1 + src/db/memory.rs | 40 ++++++++- src/db/mod.rs | 32 +++++++ src/db/parity_db.rs | 45 +++++++++- src/db/parity_db/gc.rs | 14 +++ src/db/tests/mem_test.rs | 6 ++ src/db/tests/parity_test.rs | 6 ++ src/db/tests/subtests/mod.rs | 27 +++++- src/rpc/methods/eth.rs | 86 ++++++++++++++++++- .../api_cmd/generate_test_snapshot.rs | 18 +++- .../subcommands/api_cmd/test_snapshots.txt | 6 +- 15 files changed, 287 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e05b32089e4d..f25bcf50d179 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,8 @@ - [#7096](https://github.com/ChainSafe/forest/issues/7096): `eth_subscribe` `logs` now re-emits the logs of reorg-reverted tipsets with `removed: true`, ahead of the logs of the replacing tipsets. +- [#7156](https://github.com/ChainSafe/forest/pull/7156): Fixed the block-level bloom is now computed from the block's logs. + ## Forest v0.33.7 "Shimmergloom" ### Added diff --git a/scripts/tests/api_compare/filter-list b/scripts/tests/api_compare/filter-list index c2c32dc64f46..0656b736d920 100644 --- a/scripts/tests/api_compare/filter-list +++ b/scripts/tests/api_compare/filter-list @@ -1,5 +1,2 @@ # This list contains potentially broken methods (or tests) that are ignored. # They should be considered bugged, and not used until the root cause is resolved. - -!Filecoin.EthGetBlockByHash -!Filecoin.EthGetBlockByNumber diff --git a/scripts/tests/api_compare/filter-list-gateway b/scripts/tests/api_compare/filter-list-gateway index b5cd3ac514d5..c0f054a15968 100644 --- a/scripts/tests/api_compare/filter-list-gateway +++ b/scripts/tests/api_compare/filter-list-gateway @@ -66,6 +66,3 @@ !Filecoin.StateSearchMsg # https://github.com/filecoin-project/lotus/pull/13562 !Filecoin.EthGetTransactionReceiptLimited -# https://github.com/filecoin-project/lotus/pull/13618 -!Filecoin.EthGetBlockByHash -!Filecoin.EthGetBlockByNumber diff --git a/src/db/car/many.rs b/src/db/car/many.rs index adbeaf3e5fe1..81df82b4f9b9 100644 --- a/src/db/car/many.rs +++ b/src/db/car/many.rs @@ -12,8 +12,8 @@ use super::{AnyCar, ZstdFrameCache}; use crate::blocks::TipsetKey; use crate::db::parity_db::GarbageCollectableDb; use crate::db::{ - BlockstoreWriteOpsSubscribable, EthMappingsStore, MemoryDB, PersistentStore, SettingsStore, - SettingsStoreExt, + BlockstoreWriteOpsSubscribable, EthBlockBloomStore, EthMappingsStore, MemoryDB, + PersistentStore, SettingsStore, SettingsStoreExt, }; use crate::libp2p_bitswap::BitswapStoreReadWrite; use crate::prelude::*; @@ -315,6 +315,20 @@ impl EthMappingsStore for ManyCar { } } +impl EthBlockBloomStore for ManyCar { + fn read_bloom(&self, key: &Cid) -> anyhow::Result>> { + EthBlockBloomStore::read_bloom(self.writer(), key) + } + + fn write_bloom(&self, key: &Cid, height: i64, bloom: &[u8]) -> anyhow::Result<()> { + EthBlockBloomStore::write_bloom(self.writer(), key, height, bloom) + } + + fn delete_blooms_before_height(&self, height: i64) -> anyhow::Result<()> { + EthBlockBloomStore::delete_blooms_before_height(self.writer(), height) + } +} + impl super::super::HeaviestTipsetKeyProvider for ManyCar { fn heaviest_tipset_key(&self) -> anyhow::Result> { match SettingsStoreExt::read_obj::(self, crate::db::setting_keys::HEAD_KEY)? { diff --git a/src/db/db_impl.rs b/src/db/db_impl.rs index 2936e195c7f6..a2ad86c8eec6 100644 --- a/src/db/db_impl.rs +++ b/src/db/db_impl.rs @@ -17,6 +17,7 @@ use std::path::PathBuf; #[derive(Delegate)] #[delegate(SettingsStore)] #[delegate(EthMappingsStore)] +#[delegate(EthBlockBloomStore)] #[delegate(HeaviestTipsetKeyProvider)] #[delegate(BitswapStoreRead)] #[delegate(BitswapStoreReadWrite)] diff --git a/src/db/memory.rs b/src/db/memory.rs index c72baee39b27..da3025bb1d0c 100644 --- a/src/db/memory.rs +++ b/src/db/memory.rs @@ -1,7 +1,10 @@ // Copyright 2019-2026 ChainSafe Systems // SPDX-License-Identifier: Apache-2.0, MIT -use super::{EthMappingsStore, SettingsStore, SettingsStoreExt}; +use super::{ + EthBlockBloomStore, EthMappingsStore, SettingsStore, SettingsStoreExt, decode_block_bloom, + encode_block_bloom, +}; use crate::blocks::{Tipset, TipsetKey}; use crate::db::PersistentStore; use crate::libp2p_bitswap::{BitswapStoreRead, BitswapStoreReadWrite}; @@ -21,6 +24,7 @@ pub struct MemoryDB { settings_db: RwLock>>, pub eth_mappings_db: RwLock>>, ts_lookup_db: RwLock>, + eth_block_bloom_db: RwLock>>, } impl MemoryDB { @@ -143,6 +147,30 @@ impl EthMappingsStore for MemoryDB { } } +impl EthBlockBloomStore for MemoryDB { + fn read_bloom(&self, key: &Cid) -> anyhow::Result>> { + self.eth_block_bloom_db + .read() + .get(key) + .map(|entry| anyhow::Ok(decode_block_bloom(entry)?.1.to_vec())) + .transpose() + } + + fn write_bloom(&self, key: &Cid, height: i64, bloom: &[u8]) -> anyhow::Result<()> { + self.eth_block_bloom_db + .write() + .insert(*key, encode_block_bloom(height, bloom)); + Ok(()) + } + + fn delete_blooms_before_height(&self, height: i64) -> anyhow::Result<()> { + self.eth_block_bloom_db + .write() + .retain(|_, entry| decode_block_bloom(entry).is_ok_and(|(h, _)| h >= height)); + Ok(()) + } +} + impl Blockstore for MemoryDB { fn get(&self, k: &Cid) -> anyhow::Result>> { Ok(self.blockchain_db.read().get(k).cloned().or(self @@ -234,4 +262,14 @@ mod tests { assert!(car.has(&key1).unwrap()); assert!(car.has(&key2).unwrap()); } + + #[test] + fn block_bloom_encode_decode() { + let bloom = vec![0xab; 256]; + let entry = encode_block_bloom(42, &bloom); + let (height, decoded) = decode_block_bloom(&entry).unwrap(); + assert_eq!(height, 42); + assert_eq!(decoded, bloom.as_slice()); + assert!(decode_block_bloom(&[0, 1, 2]).is_err()); + } } diff --git a/src/db/mod.rs b/src/db/mod.rs index 78518517cdc6..5ed4571ea396 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -138,6 +138,38 @@ impl EthMappingsStoreExt for T { } } +/// Interface used to store and retrieve per-tipset Ethereum block logs blooms. +#[auto_impl::auto_impl(&, Arc)] +#[delegatable_trait] +pub trait EthBlockBloomStore { + /// Reads the logs bloom stored for the given tipset key CID. + fn read_bloom(&self, key: &Cid) -> anyhow::Result>>; + + /// Stores the logs bloom for the given tipset key CID, tagged with its height. + fn write_bloom(&self, key: &Cid, height: i64, bloom: &[u8]) -> anyhow::Result<()>; + + /// Deletes every stored bloom whose height is below `height`. + fn delete_blooms_before_height(&self, height: i64) -> anyhow::Result<()>; +} + +/// Encodes a block bloom entry as its little-endian height followed by the raw bloom bytes. +pub(crate) fn encode_block_bloom(height: i64, bloom: &[u8]) -> Vec { + let mut entry = Vec::with_capacity(size_of::() + bloom.len()); + entry.extend_from_slice(&height.to_le_bytes()); + entry.extend_from_slice(bloom); + entry +} + +/// Splits a block bloom entry into its height and raw bloom bytes. +pub(crate) fn decode_block_bloom(entry: &[u8]) -> anyhow::Result<(i64, &[u8])> { + anyhow::ensure!( + entry.len() >= size_of::(), + "block bloom entry is too short" + ); + let (height, bloom) = entry.split_at(size_of::()); + Ok((i64::from_le_bytes(height.try_into()?), bloom)) +} + /// Traits for collecting DB stats pub trait DBStatistics { fn get_statistics(&self) -> Option { diff --git a/src/db/parity_db.rs b/src/db/parity_db.rs index 12d913e35063..e3b19d2f4b50 100644 --- a/src/db/parity_db.rs +++ b/src/db/parity_db.rs @@ -4,7 +4,10 @@ mod gc; pub use gc::*; -use super::{EthMappingsStore, PersistentStore, SettingsStore}; +use super::{ + EthBlockBloomStore, EthMappingsStore, PersistentStore, SettingsStore, decode_block_bloom, + encode_block_bloom, +}; use crate::blocks::{Tipset, TipsetKey}; use crate::db::{DBStatistics, parity_db_config::ParityDbConfig}; use crate::libp2p_bitswap::{BitswapStoreRead, BitswapStoreReadWrite}; @@ -40,6 +43,8 @@ pub enum DbColumn { /// Anything stored in this column can be considered permanent, unless manually /// deleted. PersistentGraph, + /// Column for storing per-tipset Ethereum block logs blooms. + EthBlockBloom, } impl DbColumn { @@ -76,6 +81,12 @@ impl DbColumn { compression, ..Default::default() }, + DbColumn::EthBlockBloom => parity_db::ColumnOptions { + preimage: false, + btree_index: true, + compression, + ..Default::default() + }, } }) .collect() @@ -240,6 +251,37 @@ impl EthMappingsStore for ParityDb { } } +impl EthBlockBloomStore for ParityDb { + fn read_bloom(&self, key: &Cid) -> anyhow::Result>> { + self.read_from_column(key.to_bytes(), DbColumn::EthBlockBloom)? + .map(|entry| anyhow::Ok(decode_block_bloom(&entry)?.1.to_vec())) + .transpose() + } + + fn write_bloom(&self, key: &Cid, height: i64, bloom: &[u8]) -> anyhow::Result<()> { + self.write_to_column( + key.to_bytes(), + encode_block_bloom(height, bloom), + DbColumn::EthBlockBloom, + ) + } + + fn delete_blooms_before_height(&self, height: i64) -> anyhow::Result<()> { + let mut stale = Vec::new(); + let mut iter = self.db.iter(DbColumn::EthBlockBloom as u8)?; + while let Some((key, entry)) = iter.next()? { + if decode_block_bloom(&entry)?.0 < height { + stale.push(key); + } + } + Ok(self.db.commit( + stale + .into_iter() + .map(|key| (DbColumn::EthBlockBloom as u8, key, None::>)), + )?) + } +} + impl Blockstore for ParityDb { fn get(&self, k: &Cid) -> anyhow::Result>> { let column = Self::choose_column(k); @@ -452,6 +494,7 @@ mod test { DbColumn::GraphFull => DbColumn::GraphDagCborBlake2b256, DbColumn::Settings => panic!("invalid column for IPLD data"), DbColumn::EthMappings => panic!("invalid column for IPLD data"), + DbColumn::EthBlockBloom => panic!("invalid column for IPLD data"), DbColumn::PersistentGraph => panic!("invalid column for GC enabled IPLD data"), }; let actual = db.read_from_column(cid.to_bytes(), other_column).unwrap(); diff --git a/src/db/parity_db/gc.rs b/src/db/parity_db/gc.rs index f7b49468f419..dc1e63416f35 100644 --- a/src/db/parity_db/gc.rs +++ b/src/db/parity_db/gc.rs @@ -161,6 +161,20 @@ impl EthMappingsStore for GarbageCollectableParityDb { } } +impl EthBlockBloomStore for GarbageCollectableParityDb { + fn read_bloom(&self, key: &Cid) -> anyhow::Result>> { + EthBlockBloomStore::read_bloom(&*self.db.read(), key) + } + + fn write_bloom(&self, key: &Cid, height: i64, bloom: &[u8]) -> anyhow::Result<()> { + EthBlockBloomStore::write_bloom(&*self.db.read(), key, height, bloom) + } + + fn delete_blooms_before_height(&self, height: i64) -> anyhow::Result<()> { + EthBlockBloomStore::delete_blooms_before_height(&*self.db.read(), height) + } +} + impl PersistentStore for GarbageCollectableParityDb { fn put_keyed_persistent(&self, k: &Cid, block: &[u8]) -> anyhow::Result<()> { PersistentStore::put_keyed_persistent(&*self.db.read(), k, block) diff --git a/src/db/tests/mem_test.rs b/src/db/tests/mem_test.rs index c1d27f14d312..8a6c1101af43 100644 --- a/src/db/tests/mem_test.rs +++ b/src/db/tests/mem_test.rs @@ -34,3 +34,9 @@ fn mem_write_read_obj() { let db = MemoryDB::default(); subtests::write_read_obj(&db); } + +#[test] +fn mem_block_bloom_prune() { + let db = MemoryDB::default(); + subtests::block_bloom_prune(&db); +} diff --git a/src/db/tests/parity_test.rs b/src/db/tests/parity_test.rs index db7d7be7a2bc..f48d34ba6c1d 100644 --- a/src/db/tests/parity_test.rs +++ b/src/db/tests/parity_test.rs @@ -32,3 +32,9 @@ fn db_write_read_obj() { let db = TempParityDB::new(); subtests::write_read_obj(&*db); } + +#[test] +fn db_block_bloom_prune() { + let db = TempParityDB::new(); + subtests::block_bloom_prune(&*db); +} diff --git a/src/db/tests/subtests/mod.rs b/src/db/tests/subtests/mod.rs index de35af6d7982..4e561d213597 100644 --- a/src/db/tests/subtests/mod.rs +++ b/src/db/tests/subtests/mod.rs @@ -1,7 +1,10 @@ // Copyright 2019-2026 ChainSafe Systems // SPDX-License-Identifier: Apache-2.0, MIT -use crate::db::{SettingsStore, SettingsStoreExt}; +use crate::db::{EthBlockBloomStore, SettingsStore, SettingsStoreExt}; +use crate::utils::multihash::prelude::*; +use cid::Cid; +use fvm_ipld_encoding::DAG_CBOR; pub fn write_bin(db: &DB) where @@ -64,3 +67,25 @@ where assert!(db.read_obj::(key).unwrap().is_none()); assert!(db.require_obj::(key).is_err()); } + +pub fn block_bloom_prune(db: &DB) +where + DB: EthBlockBloomStore, +{ + let a = Cid::new_v1(DAG_CBOR, MultihashCode::Blake2b256.digest(b"a")); + let b = Cid::new_v1(DAG_CBOR, MultihashCode::Blake2b256.digest(b"b")); + let missing = Cid::new_v1(DAG_CBOR, MultihashCode::Blake2b256.digest(b"missing")); + let bloom_a = vec![0x11; 256]; + let bloom_b = vec![0x22; 256]; + + db.write_bloom(&a, 100, &bloom_a).unwrap(); + db.write_bloom(&b, 200, &bloom_b).unwrap(); + assert_eq!(db.read_bloom(&a).unwrap().as_deref(), Some(bloom_a.as_slice())); + assert_eq!(db.read_bloom(&b).unwrap().as_deref(), Some(bloom_b.as_slice())); + assert_eq!(db.read_bloom(&missing).unwrap(), None); + + // Only entries at or above the cutoff survive. + db.delete_blooms_before_height(150).unwrap(); + assert_eq!(db.read_bloom(&a).unwrap(), None); + assert_eq!(db.read_bloom(&b).unwrap().as_deref(), Some(bloom_b.as_slice())); +} diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index fe1532b721c6..eefd109b6a08 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -583,6 +583,9 @@ impl Block { full_transactions.push(tx); } + let logs_bloom = + compute_block_logs_bloom(state_manager, &tipset, &executed_messages).await?; + Ok(Arc::new(Block { hash: block_hash, number: block_number, @@ -596,6 +599,7 @@ impl Block { .into(), gas_used: EthUint64(gas_used), transactions: Transactions::Full(full_transactions), + logs_bloom, ..Block::new(has_transactions, tipset.len()) })) }) @@ -1417,10 +1421,7 @@ async fn new_eth_tx_receipt( let mut bloom = Bloom::default(); for log in tx_receipt.logs.iter() { - for topic in log.topics.iter() { - bloom.accrue(topic.0.as_bytes()); - } - bloom.accrue(log.address.0.as_bytes()); + accrue_eth_log(&mut bloom, &log.address, &log.topics); } tx_receipt.logs_bloom = bloom.into(); @@ -3359,6 +3360,43 @@ fn eth_filter_logs_from_events( Ok(logs) } +/// Accrues a single Ethereum log's address and topics into `bloom` using the standard `M3:2048` scheme. +fn accrue_eth_log(bloom: &mut Bloom, address: &EthAddress, topics: &[EthHash]) { + for topic in topics { + bloom.accrue(topic.0.as_bytes()); + } + bloom.accrue(address.0.as_bytes()); +} + +async fn compute_block_logs_bloom( + state_manager: &StateManager, + tipset: &Tipset, + executed_messages: &[ExecutedMessage], +) -> Result { + let mut events = vec![]; + EthEventHandler::collect_events_from_messages( + state_manager, + tipset, + executed_messages, + None::<&EthFilterSpec>, + SkipEvent::OnUnresolvedAddress, + &mut events, + ) + .await?; + + let mut bloom = Bloom::default(); + for event in &events { + let Some((_data, topics)) = eth_log_from_event(&event.entries) else { + continue; + }; + let Ok(address) = EthAddress::from_filecoin_address(&event.emitter_addr) else { + continue; + }; + accrue_eth_log(&mut bloom, &address, &topics); + } + Ok(bloom) +} + fn eth_filter_result_from_events( ctx: &Ctx, events: &[CollectedEvent], @@ -4745,6 +4783,46 @@ mod test { assert!(eth_log_from_event(&entries).is_none()); } + #[test] + fn test_accrue_eth_log_and_block_bloom_decomposition() { + let empty = Bloom::default(); + let full = Bloom(ethereum_types::Bloom(FULL_BLOOM)); + + // No logs yields the all-zeros bloom — the "definitely no events here" case + // indexers rely on. + assert_eq!(empty.0.0, EMPTY_BLOOM); + + let addr_a = EthAddress(ethereum_types::H160::from_slice(&[0x11; ADDRESS_LENGTH])); + let topic_a = EthHash(ethereum_types::H256::from_slice(&[0x22; EVM_WORD_LENGTH])); + let addr_b = EthAddress(ethereum_types::H160::from_slice(&[0x33; ADDRESS_LENGTH])); + let topic_b = EthHash(ethereum_types::H256::from_slice(&[0x44; EVM_WORD_LENGTH])); + + // A real log sets some bits, but not all of them. + let mut bloom_a = empty.clone(); + accrue_eth_log(&mut bloom_a, &addr_a, std::slice::from_ref(&topic_a)); + assert_ne!(bloom_a, empty); + assert_ne!(bloom_a, full); + + let mut bloom_b = empty.clone(); + accrue_eth_log(&mut bloom_b, &addr_b, std::slice::from_ref(&topic_b)); + + // The block bloom (both logs) equals the bitwise OR of the two individual + // (receipt) blooms. + let mut combined = bloom_a.clone(); + accrue_eth_log(&mut combined, &addr_b, std::slice::from_ref(&topic_b)); + + let mut expected = bloom_a.0.0; + for (out, b) in expected.iter_mut().zip(bloom_b.0.0.iter()) { + *out |= *b; + } + assert_eq!(combined.0.0, expected); + + // Accruing the same log twice equals accruing it once. + let mut twice = bloom_a.clone(); + accrue_eth_log(&mut twice, &addr_a, std::slice::from_ref(&topic_a)); + assert_eq!(twice, bloom_a); + } + #[test] fn test_from_bytes_valid() { let zero_bytes = [0u8; 32]; diff --git a/src/tool/subcommands/api_cmd/generate_test_snapshot.rs b/src/tool/subcommands/api_cmd/generate_test_snapshot.rs index 43395b6f6bc3..9cb218841709 100644 --- a/src/tool/subcommands/api_cmd/generate_test_snapshot.rs +++ b/src/tool/subcommands/api_cmd/generate_test_snapshot.rs @@ -9,8 +9,8 @@ use crate::{ chain_sync::{SyncStatusReport, network_context::SyncNetworkContext}, daemon::{bundle::load_actor_bundles, db_util::load_all_forest_cars}, db::{ - CAR_DB_DIR_NAME, EthMappingsStore, HeaviestTipsetKeyProvider, MemoryDB, SettingsStore, - SettingsStoreExt, db_engine::open_db, parity_db::ParityDb, + CAR_DB_DIR_NAME, EthBlockBloomStore, EthMappingsStore, HeaviestTipsetKeyProvider, MemoryDB, + SettingsStore, SettingsStoreExt, db_engine::open_db, parity_db::ParityDb, }, genesis::read_genesis_header, libp2p::{NetworkMessage, PeerManager}, @@ -340,3 +340,17 @@ impl EthMappingsStore for ReadOpsTrackingStore { self.inner.set_tipset_key_at_epoch(ts) } } + +impl EthBlockBloomStore for ReadOpsTrackingStore { + fn read_bloom(&self, key: &Cid) -> anyhow::Result>> { + self.inner.read_bloom(key) + } + + fn write_bloom(&self, key: &Cid, height: i64, bloom: &[u8]) -> anyhow::Result<()> { + self.inner.write_bloom(key, height, bloom) + } + + fn delete_blooms_before_height(&self, height: i64) -> anyhow::Result<()> { + self.inner.delete_blooms_before_height(height) + } +} diff --git a/src/tool/subcommands/api_cmd/test_snapshots.txt b/src/tool/subcommands/api_cmd/test_snapshots.txt index d9d2f5ddb575..e4a1b60a7feb 100644 --- a/src/tool/subcommands/api_cmd/test_snapshots.txt +++ b/src/tool/subcommands/api_cmd/test_snapshots.txt @@ -66,10 +66,10 @@ filecoin_ethgetbalance_v2_latest_1770291948779489.rpcsnap.json.zst filecoin_ethgetbalance_v2_pending_1770291949065157.rpcsnap.json.zst filecoin_ethgetbalance_v2_safe_1770291948803158.rpcsnap.json.zst filecoin_ethgetbalance_v2_unknown_addr_1770287845559744.rpcsnap.json.zst -filecoin_ethgetblockbyhash_1781166100013912.rpcsnap.json.zst -filecoin_ethgetblockbynumber_1737446676696328.rpcsnap.json.zst +filecoin_ethgetblockbyhash_1781886021508619.rpcsnap.json.zst +filecoin_ethgetblockbynumber_1781887507408523.rpcsnap.json.zst filecoin_ethgetblockbynumber_null_round_1782840028159441.rpcsnap.json.zst -filecoin_ethgetblockbynumber_v2_1781166100024105.rpcsnap.json.zst +filecoin_ethgetblockbynumber_v2_1781887507413486.rpcsnap.json.zst filecoin_ethgetblockreceipts_1781166100049979.rpcsnap.json.zst filecoin_ethgetblockreceipts_v2_1781166100038659.rpcsnap.json.zst filecoin_ethgetblockreceiptslimited_1781166100061549.rpcsnap.json.zst From 78cf48a4e1c1f54e056ee1e017f7bc5045ffc295 Mon Sep 17 00:00:00 2001 From: Aryan Tikarya Date: Wed, 1 Jul 2026 16:01:55 +0530 Subject: [PATCH 2/9] feat: add db migration for EthBlockBloom column --- src/db/migration/migration_map.rs | 2 + src/db/migration/mod.rs | 1 + src/db/migration/v0_33_7.rs | 130 ++++++++++++++++++++++++++++++ 3 files changed, 133 insertions(+) create mode 100644 src/db/migration/v0_33_7.rs diff --git a/src/db/migration/migration_map.rs b/src/db/migration/migration_map.rs index 7ffbaffb9a8a..5f9e34923214 100644 --- a/src/db/migration/migration_map.rs +++ b/src/db/migration/migration_map.rs @@ -11,6 +11,7 @@ use crate::Config; use crate::db::migration::v0_22_1::Migration0_22_0_0_22_1; use crate::db::migration::v0_26_0::Migration0_25_3_0_26_0; use crate::db::migration::v0_31_0::Migration0_30_5_0_31_0; +use crate::db::migration::v0_33_7::Migration0_33_6_0_33_7; use anyhow::Context as _; use anyhow::bail; use itertools::Itertools; @@ -157,6 +158,7 @@ create_migrations!( "0.22.0" -> "0.22.1" @ Migration0_22_0_0_22_1, "0.25.3" -> "0.26.0" @ Migration0_25_3_0_26_0, "0.30.5" -> "0.31.0" @ Migration0_30_5_0_31_0, + "0.33.6" -> "0.33.7" @ Migration0_33_6_0_33_7, ); /// Creates a migration chain from `start` to `goal`. The chain is chosen to be the shortest diff --git a/src/db/migration/mod.rs b/src/db/migration/mod.rs index 522da4b1b946..67617ca9bbc7 100644 --- a/src/db/migration/mod.rs +++ b/src/db/migration/mod.rs @@ -6,6 +6,7 @@ mod migration_map; mod v0_22_1; mod v0_26_0; mod v0_31_0; +mod v0_33_7; mod void_migration; pub use db_migration::DbMigration; diff --git a/src/db/migration/v0_33_7.rs b/src/db/migration/v0_33_7.rs new file mode 100644 index 000000000000..f25405ba9b69 --- /dev/null +++ b/src/db/migration/v0_33_7.rs @@ -0,0 +1,130 @@ +// Copyright 2019-2026 ChainSafe Systems +// SPDX-License-Identifier: Apache-2.0, MIT + +//! Migration logic for databases with the v0.33.6 schema to v0.33.7. +//! An `EthBlockBloom` column has been added to store per-tipset Ethereum block logs blooms. + +use super::migration_map::MigrationOperation; +use crate::Config; +use crate::db::migration::migration_map::MigrationOperationExt as _; +use anyhow::Context; +use semver::Version; +use std::path::{Path, PathBuf}; +use tracing::info; + +pub(super) struct Migration0_33_6_0_33_7 { + from: Version, + to: Version, +} + +/// Migrates the database from version 0.33.6 to 0.33.7 +impl MigrationOperation for Migration0_33_6_0_33_7 { + fn new(from: Version, to: Version) -> Self + where + Self: Sized, + { + Self { from, to } + } + + fn from(&self) -> &Version { + &self.from + } + + fn to(&self) -> &Version { + &self.to + } + + fn migrate_core(&self, chain_data_path: &Path, _: &Config) -> anyhow::Result { + let old_db = self.old_db_path(chain_data_path); + let temp_db = self.temporary_db_path(chain_data_path); + + info!( + "Renaming database directory from {} to {}", + old_db.display(), + temp_db.display() + ); + std::fs::rename(&old_db, &temp_db).context("failed to rename database directory")?; + + // Create a placeholder so the delete step succeeds + std::fs::create_dir_all(&old_db).context("failed to create placeholder directory")?; + + info!("Adding EthBlockBloom column to database"); + let mut opts = paritydb_0_33_6::to_options(temp_db.clone()); + parity_db::Db::add_column(&mut opts, paritydb_0_33_6::eth_block_bloom_column_options()) + .context("failed to add EthBlockBloom column")?; + + info!("Migration completed successfully"); + Ok(temp_db) + } +} + +/// Database settings from Forest `v0.33.6` +mod paritydb_0_33_6 { + use parity_db::{ColumnOptions, CompressionType, Options}; + use std::path::PathBuf; + use strum::{Display, EnumIter, IntoEnumIterator}; + + #[derive(Copy, Clone, Debug, PartialEq, EnumIter, Display)] + #[repr(u8)] + pub(super) enum DbColumn { + GraphDagCborBlake2b256, + GraphFull, + Settings, + EthMappings, + PersistentGraph, + } + + impl DbColumn { + fn create_column_options(compression: CompressionType) -> Vec { + DbColumn::iter() + .map(|col| match col { + DbColumn::GraphDagCborBlake2b256 | DbColumn::PersistentGraph => ColumnOptions { + preimage: true, + compression, + ..Default::default() + }, + DbColumn::GraphFull => ColumnOptions { + preimage: true, + btree_index: true, + compression, + ..Default::default() + }, + DbColumn::Settings => ColumnOptions { + preimage: false, + btree_index: true, + compression, + ..Default::default() + }, + DbColumn::EthMappings => ColumnOptions { + preimage: false, + btree_index: false, + compression, + ..Default::default() + }, + }) + .collect() + } + } + + /// Options for the `EthBlockBloom` column introduced in v0.33.7. + pub(super) fn eth_block_bloom_column_options() -> ColumnOptions { + ColumnOptions { + preimage: false, + btree_index: true, + compression: CompressionType::Lz4, + ..Default::default() + } + } + + pub(super) fn to_options(path: PathBuf) -> Options { + Options { + path, + sync_wal: true, + sync_data: true, + stats: false, + salt: None, + columns: DbColumn::create_column_options(CompressionType::Lz4), + compression_threshold: [(0, 128)].into_iter().collect(), + } + } +} From 40812944cd68fe0ffeccef90615837527b0a0d27 Mon Sep 17 00:00:00 2001 From: Aryan Tikarya Date: Wed, 1 Jul 2026 16:02:18 +0530 Subject: [PATCH 3/9] feat: cache eth block logsBloom in the db --- src/rpc/methods/eth.rs | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index eefd109b6a08..9b7950b75819 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -22,7 +22,7 @@ use crate::blocks::{Tipset, TipsetKey}; use crate::chain::{ChainStore, compute_base_fee, index::ResolveNullTipset}; use crate::chain_sync::NodeSyncStatus; use crate::cid_collections::CidHashSet; -use crate::db::DbImpl; +use crate::db::{DbImpl, EthBlockBloomStore}; use crate::eth::{ EAMMethod, EVMMethod, EthChainId as EthChainIdType, EthEip1559TxArgs, EthLegacyEip155TxArgs, EthLegacyHomesteadTxArgs, parse_eth_transaction, @@ -584,7 +584,7 @@ impl Block { } let logs_bloom = - compute_block_logs_bloom(state_manager, &tipset, &executed_messages).await?; + block_logs_bloom(state_manager, &tipset, &executed_messages).await?; Ok(Arc::new(Block { hash: block_hash, @@ -3378,8 +3378,9 @@ async fn compute_block_logs_bloom( state_manager, tipset, executed_messages, - None::<&EthFilterSpec>, + None::<&ParsedFilter>, SkipEvent::OnUnresolvedAddress, + EventRevertStatus::Applied, &mut events, ) .await?; @@ -3397,6 +3398,33 @@ async fn compute_block_logs_bloom( Ok(bloom) } +/// Returns the block's logs bloom, reading it from the store when the chain indexer is enabled +/// and computing (then storing) it on a miss. +async fn block_logs_bloom( + state_manager: &StateManager, + tipset: &Tipset, + executed_messages: &[ExecutedMessage], +) -> Result { + let config = state_manager.chain_config(); + let store_bloom = config.enable_indexer && !config.is_devnet(); + + let key = tipset.key().cid()?; + if store_bloom + && let Some(bytes) = state_manager.db().read_bloom(&key)? + && let Ok(bloom) = <[u8; BLOOM_SIZE_IN_BYTES]>::try_from(bytes.as_slice()) + { + return Ok(Bloom(ethereum_types::Bloom(bloom))); + } + + let bloom = compute_block_logs_bloom(state_manager, tipset, executed_messages).await?; + if store_bloom { + state_manager + .db() + .write_bloom(&key, tipset.epoch(), bloom.0.0.as_slice())?; + } + Ok(bloom) +} + fn eth_filter_result_from_events( ctx: &Ctx, events: &[CollectedEvent], From 7da1b35f18e87c8e27ea679d2fdd053ad0ec99d8 Mon Sep 17 00:00:00 2001 From: Aryan Tikarya Date: Wed, 1 Jul 2026 16:02:41 +0530 Subject: [PATCH 4/9] feat: prune stale block blooms during gc --- src/db/gc/snapshot.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/db/gc/snapshot.rs b/src/db/gc/snapshot.rs index 911ea668cfef..897b343c28fd 100644 --- a/src/db/gc/snapshot.rs +++ b/src/db/gc/snapshot.rs @@ -43,7 +43,7 @@ use crate::cid_collections::FileBackedCidHashSet; use crate::cli_shared::chain_path; use crate::db::DbImpl; use crate::db::{ - BlockstoreWriteOpsSubscribable, CAR_DB_DIR_NAME, HeaviestTipsetKeyProvider, + BlockstoreWriteOpsSubscribable, CAR_DB_DIR_NAME, EthBlockBloomStore, HeaviestTipsetKeyProvider, car::{ForestCar, ReloadableManyCar, forest::new_forest_car_temp_path_in}, db_engine::db_root, parity_db::GarbageCollectableDb, @@ -348,6 +348,14 @@ impl SnapshotGarbageCollector { .unwrap_or_default() ); + // Prune blooms whose events are no longer retained by the lite snapshot. + if let Ok(head) = db.heaviest_car_tipset() { + let cutoff = head.epoch() - self.recent_state_roots; + if let Err(e) = db.delete_blooms_before_height(cutoff) { + tracing::warn!("failed to prune stale block blooms: {e:#}"); + } + } + // Reset chain head. Note that `self.exported_head_key` is guaranteed to be present, // see `*self.exported_head_key.write() = Some(head_ts.key().clone());` in `export_snapshot`. for tsk_opt in [ From 0b29077a92f51070dcfa9970b345bd4084d492e0 Mon Sep 17 00:00:00 2001 From: Aryan Tikarya Date: Wed, 1 Jul 2026 16:02:57 +0530 Subject: [PATCH 5/9] update the changelog entry --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f25bcf50d179..9576fd3fa885 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,7 +53,7 @@ - [#7096](https://github.com/ChainSafe/forest/issues/7096): `eth_subscribe` `logs` now re-emits the logs of reorg-reverted tipsets with `removed: true`, ahead of the logs of the replacing tipsets. -- [#7156](https://github.com/ChainSafe/forest/pull/7156): Fixed the block-level bloom is now computed from the block's logs. +- [#7156](https://github.com/ChainSafe/forest/pull/7156): The `eth` block `logsBloom` is now correctly computed from the block's logs instead of being all-ones. When the chain indexer is enabled, the computed bloom is cached in the database. ## Forest v0.33.7 "Shimmergloom" From 5bd8ce22d5a3893f6fd1db2101e13badec5fb61a Mon Sep 17 00:00:00 2001 From: Aryan Tikarya Date: Wed, 1 Jul 2026 16:04:25 +0530 Subject: [PATCH 6/9] fix fmt issues --- src/db/tests/subtests/mod.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/db/tests/subtests/mod.rs b/src/db/tests/subtests/mod.rs index 4e561d213597..34651b531fe1 100644 --- a/src/db/tests/subtests/mod.rs +++ b/src/db/tests/subtests/mod.rs @@ -80,12 +80,21 @@ where db.write_bloom(&a, 100, &bloom_a).unwrap(); db.write_bloom(&b, 200, &bloom_b).unwrap(); - assert_eq!(db.read_bloom(&a).unwrap().as_deref(), Some(bloom_a.as_slice())); - assert_eq!(db.read_bloom(&b).unwrap().as_deref(), Some(bloom_b.as_slice())); + assert_eq!( + db.read_bloom(&a).unwrap().as_deref(), + Some(bloom_a.as_slice()) + ); + assert_eq!( + db.read_bloom(&b).unwrap().as_deref(), + Some(bloom_b.as_slice()) + ); assert_eq!(db.read_bloom(&missing).unwrap(), None); // Only entries at or above the cutoff survive. db.delete_blooms_before_height(150).unwrap(); assert_eq!(db.read_bloom(&a).unwrap(), None); - assert_eq!(db.read_bloom(&b).unwrap().as_deref(), Some(bloom_b.as_slice())); + assert_eq!( + db.read_bloom(&b).unwrap().as_deref(), + Some(bloom_b.as_slice()) + ); } From 0e5a0eba97f3446663d722313a97f981040fd4bd Mon Sep 17 00:00:00 2001 From: Aryan Tikarya Date: Wed, 1 Jul 2026 19:23:02 +0530 Subject: [PATCH 7/9] address comments --- src/db/car/many.rs | 13 +++++-- src/db/memory.rs | 27 ++++++++------ src/db/mod.rs | 37 ++++++++++++------- src/db/parity_db.rs | 26 ++++++++----- src/db/parity_db/gc.rs | 9 ++++- src/db/tests/subtests/mod.rs | 19 +++------- src/rpc/methods/eth.rs | 7 +--- .../api_cmd/generate_test_snapshot.rs | 14 +++++-- 8 files changed, 89 insertions(+), 63 deletions(-) diff --git a/src/db/car/many.rs b/src/db/car/many.rs index 81df82b4f9b9..05fc67430793 100644 --- a/src/db/car/many.rs +++ b/src/db/car/many.rs @@ -12,8 +12,8 @@ use super::{AnyCar, ZstdFrameCache}; use crate::blocks::TipsetKey; use crate::db::parity_db::GarbageCollectableDb; use crate::db::{ - BlockstoreWriteOpsSubscribable, EthBlockBloomStore, EthMappingsStore, MemoryDB, - PersistentStore, SettingsStore, SettingsStoreExt, + BLOCK_BLOOM_LEN, BlockstoreWriteOpsSubscribable, EthBlockBloomStore, EthMappingsStore, + MemoryDB, PersistentStore, SettingsStore, SettingsStoreExt, }; use crate::libp2p_bitswap::BitswapStoreReadWrite; use crate::prelude::*; @@ -316,11 +316,16 @@ impl EthMappingsStore for ManyCar { } impl EthBlockBloomStore for ManyCar { - fn read_bloom(&self, key: &Cid) -> anyhow::Result>> { + fn read_bloom(&self, key: &Cid) -> anyhow::Result> { EthBlockBloomStore::read_bloom(self.writer(), key) } - fn write_bloom(&self, key: &Cid, height: i64, bloom: &[u8]) -> anyhow::Result<()> { + fn write_bloom( + &self, + key: &Cid, + height: i64, + bloom: &[u8; BLOCK_BLOOM_LEN], + ) -> anyhow::Result<()> { EthBlockBloomStore::write_bloom(self.writer(), key, height, bloom) } diff --git a/src/db/memory.rs b/src/db/memory.rs index da3025bb1d0c..495e76b3dfc3 100644 --- a/src/db/memory.rs +++ b/src/db/memory.rs @@ -2,8 +2,8 @@ // SPDX-License-Identifier: Apache-2.0, MIT use super::{ - EthBlockBloomStore, EthMappingsStore, SettingsStore, SettingsStoreExt, decode_block_bloom, - encode_block_bloom, + BLOCK_BLOOM_LEN, EthBlockBloomStore, EthMappingsStore, SettingsStore, SettingsStoreExt, + decode_block_bloom, encode_block_bloom, }; use crate::blocks::{Tipset, TipsetKey}; use crate::db::PersistentStore; @@ -148,15 +148,20 @@ impl EthMappingsStore for MemoryDB { } impl EthBlockBloomStore for MemoryDB { - fn read_bloom(&self, key: &Cid) -> anyhow::Result>> { - self.eth_block_bloom_db + fn read_bloom(&self, key: &Cid) -> anyhow::Result> { + Ok(self + .eth_block_bloom_db .read() .get(key) - .map(|entry| anyhow::Ok(decode_block_bloom(entry)?.1.to_vec())) - .transpose() + .and_then(|entry| decode_block_bloom(entry).map(|(_, bloom)| *bloom))) } - fn write_bloom(&self, key: &Cid, height: i64, bloom: &[u8]) -> anyhow::Result<()> { + fn write_bloom( + &self, + key: &Cid, + height: i64, + bloom: &[u8; BLOCK_BLOOM_LEN], + ) -> anyhow::Result<()> { self.eth_block_bloom_db .write() .insert(*key, encode_block_bloom(height, bloom)); @@ -166,7 +171,7 @@ impl EthBlockBloomStore for MemoryDB { fn delete_blooms_before_height(&self, height: i64) -> anyhow::Result<()> { self.eth_block_bloom_db .write() - .retain(|_, entry| decode_block_bloom(entry).is_ok_and(|(h, _)| h >= height)); + .retain(|_, entry| decode_block_bloom(entry).is_some_and(|(h, _)| h >= height)); Ok(()) } } @@ -265,11 +270,11 @@ mod tests { #[test] fn block_bloom_encode_decode() { - let bloom = vec![0xab; 256]; + let bloom = [0xab; 256]; let entry = encode_block_bloom(42, &bloom); let (height, decoded) = decode_block_bloom(&entry).unwrap(); assert_eq!(height, 42); - assert_eq!(decoded, bloom.as_slice()); - assert!(decode_block_bloom(&[0, 1, 2]).is_err()); + assert_eq!(decoded, &bloom); + assert!(decode_block_bloom(&[0, 1, 2]).is_none()); } } diff --git a/src/db/mod.rs b/src/db/mod.rs index 5ed4571ea396..4c93dcaf915e 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -138,36 +138,47 @@ impl EthMappingsStoreExt for T { } } +/// Length in bytes of a stored Ethereum block logs bloom. +pub(crate) const BLOCK_BLOOM_LEN: usize = 256; + /// Interface used to store and retrieve per-tipset Ethereum block logs blooms. #[auto_impl::auto_impl(&, Arc)] #[delegatable_trait] pub trait EthBlockBloomStore { - /// Reads the logs bloom stored for the given tipset key CID. - fn read_bloom(&self, key: &Cid) -> anyhow::Result>>; + /// Reads the logs bloom stored for the given tipset key CID. A missing or malformed entry + /// is reported as `None`. + fn read_bloom(&self, key: &Cid) -> anyhow::Result>; /// Stores the logs bloom for the given tipset key CID, tagged with its height. - fn write_bloom(&self, key: &Cid, height: i64, bloom: &[u8]) -> anyhow::Result<()>; + fn write_bloom( + &self, + key: &Cid, + height: i64, + bloom: &[u8; BLOCK_BLOOM_LEN], + ) -> anyhow::Result<()>; /// Deletes every stored bloom whose height is below `height`. fn delete_blooms_before_height(&self, height: i64) -> anyhow::Result<()>; } -/// Encodes a block bloom entry as its little-endian height followed by the raw bloom bytes. -pub(crate) fn encode_block_bloom(height: i64, bloom: &[u8]) -> Vec { - let mut entry = Vec::with_capacity(size_of::() + bloom.len()); +/// Encodes a block bloom entry as its little-endian height followed by the bloom bytes. +pub(crate) fn encode_block_bloom(height: i64, bloom: &[u8; BLOCK_BLOOM_LEN]) -> Vec { + let mut entry = Vec::with_capacity(size_of::() + BLOCK_BLOOM_LEN); entry.extend_from_slice(&height.to_le_bytes()); entry.extend_from_slice(bloom); entry } -/// Splits a block bloom entry into its height and raw bloom bytes. -pub(crate) fn decode_block_bloom(entry: &[u8]) -> anyhow::Result<(i64, &[u8])> { - anyhow::ensure!( - entry.len() >= size_of::(), - "block bloom entry is too short" - ); +/// Splits a block bloom entry into its (height and `BLOCK_BLOOM_LEN`) and raw bloom bytes. +pub(crate) fn decode_block_bloom(entry: &[u8]) -> Option<(i64, &[u8; BLOCK_BLOOM_LEN])> { + if entry.len() != size_of::() + BLOCK_BLOOM_LEN { + return None; + } let (height, bloom) = entry.split_at(size_of::()); - Ok((i64::from_le_bytes(height.try_into()?), bloom)) + Some(( + i64::from_le_bytes(height.try_into().ok()?), + bloom.try_into().ok()?, + )) } /// Traits for collecting DB stats diff --git a/src/db/parity_db.rs b/src/db/parity_db.rs index e3b19d2f4b50..3e3b21b253d8 100644 --- a/src/db/parity_db.rs +++ b/src/db/parity_db.rs @@ -5,8 +5,8 @@ mod gc; pub use gc::*; use super::{ - EthBlockBloomStore, EthMappingsStore, PersistentStore, SettingsStore, decode_block_bloom, - encode_block_bloom, + BLOCK_BLOOM_LEN, EthBlockBloomStore, EthMappingsStore, PersistentStore, SettingsStore, + decode_block_bloom, encode_block_bloom, }; use crate::blocks::{Tipset, TipsetKey}; use crate::db::{DBStatistics, parity_db_config::ParityDbConfig}; @@ -252,13 +252,18 @@ impl EthMappingsStore for ParityDb { } impl EthBlockBloomStore for ParityDb { - fn read_bloom(&self, key: &Cid) -> anyhow::Result>> { - self.read_from_column(key.to_bytes(), DbColumn::EthBlockBloom)? - .map(|entry| anyhow::Ok(decode_block_bloom(&entry)?.1.to_vec())) - .transpose() + fn read_bloom(&self, key: &Cid) -> anyhow::Result> { + Ok(self + .read_from_column(key.to_bytes(), DbColumn::EthBlockBloom)? + .and_then(|entry| decode_block_bloom(&entry).map(|(_, bloom)| *bloom))) } - fn write_bloom(&self, key: &Cid, height: i64, bloom: &[u8]) -> anyhow::Result<()> { + fn write_bloom( + &self, + key: &Cid, + height: i64, + bloom: &[u8; BLOCK_BLOOM_LEN], + ) -> anyhow::Result<()> { self.write_to_column( key.to_bytes(), encode_block_bloom(height, bloom), @@ -270,14 +275,15 @@ impl EthBlockBloomStore for ParityDb { let mut stale = Vec::new(); let mut iter = self.db.iter(DbColumn::EthBlockBloom as u8)?; while let Some((key, entry)) = iter.next()? { - if decode_block_bloom(&entry)?.0 < height { + // Drop rows below the cutoff and any that fail to decode (corrupt/garbage). + if decode_block_bloom(&entry).is_none_or(|(h, _)| h < height) { stale.push(key); } } - Ok(self.db.commit( + Ok(self.db.commit_changes( stale .into_iter() - .map(|key| (DbColumn::EthBlockBloom as u8, key, None::>)), + .map(|key| (DbColumn::EthBlockBloom as u8, Operation::Dereference(key))), )?) } } diff --git a/src/db/parity_db/gc.rs b/src/db/parity_db/gc.rs index dc1e63416f35..20272a322ee8 100644 --- a/src/db/parity_db/gc.rs +++ b/src/db/parity_db/gc.rs @@ -162,11 +162,16 @@ impl EthMappingsStore for GarbageCollectableParityDb { } impl EthBlockBloomStore for GarbageCollectableParityDb { - fn read_bloom(&self, key: &Cid) -> anyhow::Result>> { + fn read_bloom(&self, key: &Cid) -> anyhow::Result> { EthBlockBloomStore::read_bloom(&*self.db.read(), key) } - fn write_bloom(&self, key: &Cid, height: i64, bloom: &[u8]) -> anyhow::Result<()> { + fn write_bloom( + &self, + key: &Cid, + height: i64, + bloom: &[u8; BLOCK_BLOOM_LEN], + ) -> anyhow::Result<()> { EthBlockBloomStore::write_bloom(&*self.db.read(), key, height, bloom) } diff --git a/src/db/tests/subtests/mod.rs b/src/db/tests/subtests/mod.rs index 34651b531fe1..382336ab99e2 100644 --- a/src/db/tests/subtests/mod.rs +++ b/src/db/tests/subtests/mod.rs @@ -75,26 +75,17 @@ where let a = Cid::new_v1(DAG_CBOR, MultihashCode::Blake2b256.digest(b"a")); let b = Cid::new_v1(DAG_CBOR, MultihashCode::Blake2b256.digest(b"b")); let missing = Cid::new_v1(DAG_CBOR, MultihashCode::Blake2b256.digest(b"missing")); - let bloom_a = vec![0x11; 256]; - let bloom_b = vec![0x22; 256]; + let bloom_a = [0x11; 256]; + let bloom_b = [0x22; 256]; db.write_bloom(&a, 100, &bloom_a).unwrap(); db.write_bloom(&b, 200, &bloom_b).unwrap(); - assert_eq!( - db.read_bloom(&a).unwrap().as_deref(), - Some(bloom_a.as_slice()) - ); - assert_eq!( - db.read_bloom(&b).unwrap().as_deref(), - Some(bloom_b.as_slice()) - ); + assert_eq!(db.read_bloom(&a).unwrap(), Some(bloom_a)); + assert_eq!(db.read_bloom(&b).unwrap(), Some(bloom_b)); assert_eq!(db.read_bloom(&missing).unwrap(), None); // Only entries at or above the cutoff survive. db.delete_blooms_before_height(150).unwrap(); assert_eq!(db.read_bloom(&a).unwrap(), None); - assert_eq!( - db.read_bloom(&b).unwrap().as_deref(), - Some(bloom_b.as_slice()) - ); + assert_eq!(db.read_bloom(&b).unwrap(), Some(bloom_b)); } diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index 9b7950b75819..dfb57cf25622 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -3409,10 +3409,7 @@ async fn block_logs_bloom( let store_bloom = config.enable_indexer && !config.is_devnet(); let key = tipset.key().cid()?; - if store_bloom - && let Some(bytes) = state_manager.db().read_bloom(&key)? - && let Ok(bloom) = <[u8; BLOOM_SIZE_IN_BYTES]>::try_from(bytes.as_slice()) - { + if store_bloom && let Some(bloom) = state_manager.db().read_bloom(&key)? { return Ok(Bloom(ethereum_types::Bloom(bloom))); } @@ -3420,7 +3417,7 @@ async fn block_logs_bloom( if store_bloom { state_manager .db() - .write_bloom(&key, tipset.epoch(), bloom.0.0.as_slice())?; + .write_bloom(&key, tipset.epoch(), &bloom.0.0)?; } Ok(bloom) } diff --git a/src/tool/subcommands/api_cmd/generate_test_snapshot.rs b/src/tool/subcommands/api_cmd/generate_test_snapshot.rs index 9cb218841709..c46e2adb4a90 100644 --- a/src/tool/subcommands/api_cmd/generate_test_snapshot.rs +++ b/src/tool/subcommands/api_cmd/generate_test_snapshot.rs @@ -9,8 +9,9 @@ use crate::{ chain_sync::{SyncStatusReport, network_context::SyncNetworkContext}, daemon::{bundle::load_actor_bundles, db_util::load_all_forest_cars}, db::{ - CAR_DB_DIR_NAME, EthBlockBloomStore, EthMappingsStore, HeaviestTipsetKeyProvider, MemoryDB, - SettingsStore, SettingsStoreExt, db_engine::open_db, parity_db::ParityDb, + BLOCK_BLOOM_LEN, CAR_DB_DIR_NAME, EthBlockBloomStore, EthMappingsStore, + HeaviestTipsetKeyProvider, MemoryDB, SettingsStore, SettingsStoreExt, db_engine::open_db, + parity_db::ParityDb, }, genesis::read_genesis_header, libp2p::{NetworkMessage, PeerManager}, @@ -342,11 +343,16 @@ impl EthMappingsStore for ReadOpsTrackingStore { } impl EthBlockBloomStore for ReadOpsTrackingStore { - fn read_bloom(&self, key: &Cid) -> anyhow::Result>> { + fn read_bloom(&self, key: &Cid) -> anyhow::Result> { self.inner.read_bloom(key) } - fn write_bloom(&self, key: &Cid, height: i64, bloom: &[u8]) -> anyhow::Result<()> { + fn write_bloom( + &self, + key: &Cid, + height: i64, + bloom: &[u8; BLOCK_BLOOM_LEN], + ) -> anyhow::Result<()> { self.inner.write_bloom(key, height, bloom) } From 0fd4c9df772f1133c4a8399c8b96bb1d9158802b Mon Sep 17 00:00:00 2001 From: Aryan Tikarya Date: Thu, 2 Jul 2026 15:28:34 +0530 Subject: [PATCH 8/9] address comments and update forest version --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/db/car/many.rs | 4 +- src/db/memory.rs | 4 +- src/db/migration/migration_map.rs | 4 +- src/db/migration/mod.rs | 2 +- src/db/migration/{v0_33_7.rs => v0_33_8.rs} | 37 ++++++++++++------- src/db/mod.rs | 9 +++-- src/db/parity_db.rs | 5 ++- src/db/parity_db/gc.rs | 4 +- src/rpc/methods/eth/filter/mod.rs | 3 ++ .../api_cmd/generate_test_snapshot.rs | 6 +-- 12 files changed, 49 insertions(+), 33 deletions(-) rename src/db/migration/{v0_33_7.rs => v0_33_8.rs} (77%) diff --git a/Cargo.lock b/Cargo.lock index 948cd71d316a..841829c345e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3243,7 +3243,7 @@ checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] name = "forest-filecoin" -version = "0.33.7" +version = "0.33.8" dependencies = [ "ahash", "all_asserts", diff --git a/Cargo.toml b/Cargo.toml index ddae566ea4bb..411feb006402 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "forest-filecoin" -version = "0.33.7" +version = "0.33.8" authors = ["ChainSafe Systems "] repository = "https://github.com/ChainSafe/forest" edition = "2024" diff --git a/src/db/car/many.rs b/src/db/car/many.rs index 05fc67430793..e1d75caef045 100644 --- a/src/db/car/many.rs +++ b/src/db/car/many.rs @@ -323,13 +323,13 @@ impl EthBlockBloomStore for ManyCar { fn write_bloom( &self, key: &Cid, - height: i64, + height: ChainEpoch, bloom: &[u8; BLOCK_BLOOM_LEN], ) -> anyhow::Result<()> { EthBlockBloomStore::write_bloom(self.writer(), key, height, bloom) } - fn delete_blooms_before_height(&self, height: i64) -> anyhow::Result<()> { + fn delete_blooms_before_height(&self, height: ChainEpoch) -> anyhow::Result<()> { EthBlockBloomStore::delete_blooms_before_height(self.writer(), height) } } diff --git a/src/db/memory.rs b/src/db/memory.rs index 495e76b3dfc3..a4bc83586af9 100644 --- a/src/db/memory.rs +++ b/src/db/memory.rs @@ -159,7 +159,7 @@ impl EthBlockBloomStore for MemoryDB { fn write_bloom( &self, key: &Cid, - height: i64, + height: ChainEpoch, bloom: &[u8; BLOCK_BLOOM_LEN], ) -> anyhow::Result<()> { self.eth_block_bloom_db @@ -168,7 +168,7 @@ impl EthBlockBloomStore for MemoryDB { Ok(()) } - fn delete_blooms_before_height(&self, height: i64) -> anyhow::Result<()> { + fn delete_blooms_before_height(&self, height: ChainEpoch) -> anyhow::Result<()> { self.eth_block_bloom_db .write() .retain(|_, entry| decode_block_bloom(entry).is_some_and(|(h, _)| h >= height)); diff --git a/src/db/migration/migration_map.rs b/src/db/migration/migration_map.rs index 5f9e34923214..4ac0029b10e1 100644 --- a/src/db/migration/migration_map.rs +++ b/src/db/migration/migration_map.rs @@ -11,7 +11,7 @@ use crate::Config; use crate::db::migration::v0_22_1::Migration0_22_0_0_22_1; use crate::db::migration::v0_26_0::Migration0_25_3_0_26_0; use crate::db::migration::v0_31_0::Migration0_30_5_0_31_0; -use crate::db::migration::v0_33_7::Migration0_33_6_0_33_7; +use crate::db::migration::v0_33_8::Migration0_33_7_0_33_8; use anyhow::Context as _; use anyhow::bail; use itertools::Itertools; @@ -158,7 +158,7 @@ create_migrations!( "0.22.0" -> "0.22.1" @ Migration0_22_0_0_22_1, "0.25.3" -> "0.26.0" @ Migration0_25_3_0_26_0, "0.30.5" -> "0.31.0" @ Migration0_30_5_0_31_0, - "0.33.6" -> "0.33.7" @ Migration0_33_6_0_33_7, + "0.33.7" -> "0.33.8" @ Migration0_33_7_0_33_8, ); /// Creates a migration chain from `start` to `goal`. The chain is chosen to be the shortest diff --git a/src/db/migration/mod.rs b/src/db/migration/mod.rs index 67617ca9bbc7..7a9abe7175f9 100644 --- a/src/db/migration/mod.rs +++ b/src/db/migration/mod.rs @@ -6,7 +6,7 @@ mod migration_map; mod v0_22_1; mod v0_26_0; mod v0_31_0; -mod v0_33_7; +mod v0_33_8; mod void_migration; pub use db_migration::DbMigration; diff --git a/src/db/migration/v0_33_7.rs b/src/db/migration/v0_33_8.rs similarity index 77% rename from src/db/migration/v0_33_7.rs rename to src/db/migration/v0_33_8.rs index f25405ba9b69..7a63ecf2bc19 100644 --- a/src/db/migration/v0_33_7.rs +++ b/src/db/migration/v0_33_8.rs @@ -1,7 +1,7 @@ // Copyright 2019-2026 ChainSafe Systems // SPDX-License-Identifier: Apache-2.0, MIT -//! Migration logic for databases with the v0.33.6 schema to v0.33.7. +//! Migration logic for databases with the v0.33.7 schema to v0.33.8. //! An `EthBlockBloom` column has been added to store per-tipset Ethereum block logs blooms. use super::migration_map::MigrationOperation; @@ -12,13 +12,13 @@ use semver::Version; use std::path::{Path, PathBuf}; use tracing::info; -pub(super) struct Migration0_33_6_0_33_7 { +pub(super) struct Migration0_33_7_0_33_8 { from: Version, to: Version, } -/// Migrates the database from version 0.33.6 to 0.33.7 -impl MigrationOperation for Migration0_33_6_0_33_7 { +/// Migrates the database from version 0.33.7 to 0.33.8 +impl MigrationOperation for Migration0_33_7_0_33_8 { fn new(from: Version, to: Version) -> Self where Self: Sized, @@ -45,21 +45,32 @@ impl MigrationOperation for Migration0_33_6_0_33_7 { ); std::fs::rename(&old_db, &temp_db).context("failed to rename database directory")?; - // Create a placeholder so the delete step succeeds - std::fs::create_dir_all(&old_db).context("failed to create placeholder directory")?; - info!("Adding EthBlockBloom column to database"); - let mut opts = paritydb_0_33_6::to_options(temp_db.clone()); - parity_db::Db::add_column(&mut opts, paritydb_0_33_6::eth_block_bloom_column_options()) - .context("failed to add EthBlockBloom column")?; + let mut opts = paritydb_0_33_7::to_options(temp_db.clone()); + if let Err(e) = + parity_db::Db::add_column(&mut opts, paritydb_0_33_7::eth_block_bloom_column_options()) + { + // Restore the original database so a failed migration never strands the only copy in temp. + if let Err(restore) = std::fs::rename(&temp_db, &old_db) { + tracing::error!( + "failed to restore database to {}; data is preserved at {}: {restore}", + old_db.display(), + temp_db.display() + ); + } + return Err(e).context("failed to add EthBlockBloom column"); + } + + // Create a placeholder so the delete step in `migrate` succeeds. + std::fs::create_dir_all(&old_db).context("failed to create placeholder directory")?; info!("Migration completed successfully"); Ok(temp_db) } } -/// Database settings from Forest `v0.33.6` -mod paritydb_0_33_6 { +/// Database settings from Forest `v0.33.7` +mod paritydb_0_33_7 { use parity_db::{ColumnOptions, CompressionType, Options}; use std::path::PathBuf; use strum::{Display, EnumIter, IntoEnumIterator}; @@ -106,7 +117,7 @@ mod paritydb_0_33_6 { } } - /// Options for the `EthBlockBloom` column introduced in v0.33.7. + /// Options for the `EthBlockBloom` column introduced in v0.33.8. pub(super) fn eth_block_bloom_column_options() -> ColumnOptions { ColumnOptions { preimage: false, diff --git a/src/db/mod.rs b/src/db/mod.rs index 4c93dcaf915e..35f1561e7c1e 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -23,6 +23,7 @@ pub use memory::MemoryDB; use crate::blocks::{Tipset, TipsetKey}; use crate::rpc::eth::types::EthHash; +use crate::shim::clock::ChainEpoch; use ambassador::delegatable_trait; use anyhow::Context as _; use cid::Cid; @@ -153,16 +154,16 @@ pub trait EthBlockBloomStore { fn write_bloom( &self, key: &Cid, - height: i64, + height: ChainEpoch, bloom: &[u8; BLOCK_BLOOM_LEN], ) -> anyhow::Result<()>; /// Deletes every stored bloom whose height is below `height`. - fn delete_blooms_before_height(&self, height: i64) -> anyhow::Result<()>; + fn delete_blooms_before_height(&self, height: ChainEpoch) -> anyhow::Result<()>; } /// Encodes a block bloom entry as its little-endian height followed by the bloom bytes. -pub(crate) fn encode_block_bloom(height: i64, bloom: &[u8; BLOCK_BLOOM_LEN]) -> Vec { +pub(crate) fn encode_block_bloom(height: ChainEpoch, bloom: &[u8; BLOCK_BLOOM_LEN]) -> Vec { let mut entry = Vec::with_capacity(size_of::() + BLOCK_BLOOM_LEN); entry.extend_from_slice(&height.to_le_bytes()); entry.extend_from_slice(bloom); @@ -170,7 +171,7 @@ pub(crate) fn encode_block_bloom(height: i64, bloom: &[u8; BLOCK_BLOOM_LEN]) -> } /// Splits a block bloom entry into its (height and `BLOCK_BLOOM_LEN`) and raw bloom bytes. -pub(crate) fn decode_block_bloom(entry: &[u8]) -> Option<(i64, &[u8; BLOCK_BLOOM_LEN])> { +pub(crate) fn decode_block_bloom(entry: &[u8]) -> Option<(ChainEpoch, &[u8; BLOCK_BLOOM_LEN])> { if entry.len() != size_of::() + BLOCK_BLOOM_LEN { return None; } diff --git a/src/db/parity_db.rs b/src/db/parity_db.rs index 3e3b21b253d8..2beade092b98 100644 --- a/src/db/parity_db.rs +++ b/src/db/parity_db.rs @@ -13,6 +13,7 @@ use crate::db::{DBStatistics, parity_db_config::ParityDbConfig}; use crate::libp2p_bitswap::{BitswapStoreRead, BitswapStoreReadWrite}; use crate::prelude::*; use crate::rpc::eth::types::EthHash; +use crate::shim::clock::ChainEpoch; use crate::utils::{broadcast::has_subscribers, multihash::prelude::*}; use bytes::Bytes; use fvm_ipld_encoding::DAG_CBOR; @@ -261,7 +262,7 @@ impl EthBlockBloomStore for ParityDb { fn write_bloom( &self, key: &Cid, - height: i64, + height: ChainEpoch, bloom: &[u8; BLOCK_BLOOM_LEN], ) -> anyhow::Result<()> { self.write_to_column( @@ -271,7 +272,7 @@ impl EthBlockBloomStore for ParityDb { ) } - fn delete_blooms_before_height(&self, height: i64) -> anyhow::Result<()> { + fn delete_blooms_before_height(&self, height: ChainEpoch) -> anyhow::Result<()> { let mut stale = Vec::new(); let mut iter = self.db.iter(DbColumn::EthBlockBloom as u8)?; while let Some((key, entry)) = iter.next()? { diff --git a/src/db/parity_db/gc.rs b/src/db/parity_db/gc.rs index 20272a322ee8..3607bb2c5ead 100644 --- a/src/db/parity_db/gc.rs +++ b/src/db/parity_db/gc.rs @@ -169,13 +169,13 @@ impl EthBlockBloomStore for GarbageCollectableParityDb { fn write_bloom( &self, key: &Cid, - height: i64, + height: ChainEpoch, bloom: &[u8; BLOCK_BLOOM_LEN], ) -> anyhow::Result<()> { EthBlockBloomStore::write_bloom(&*self.db.read(), key, height, bloom) } - fn delete_blooms_before_height(&self, height: i64) -> anyhow::Result<()> { + fn delete_blooms_before_height(&self, height: ChainEpoch) -> anyhow::Result<()> { EthBlockBloomStore::delete_blooms_before_height(&*self.db.read(), height) } } diff --git a/src/rpc/methods/eth/filter/mod.rs b/src/rpc/methods/eth/filter/mod.rs index 3943e1a89ced..10de4aa91da1 100644 --- a/src/rpc/methods/eth/filter/mod.rs +++ b/src/rpc/methods/eth/filter/mod.rs @@ -359,6 +359,9 @@ impl EthEventHandler { .await } + /// Collects the tipset's events from already-loaded executed messages, keeping those that + /// match `spec` and tagging each with `revert_status`. Use [`Self::collect_events`] instead + /// wages sthen the executed messill need to be loaded. pub async fn collect_events_from_messages( state_manager: &StateManager, tipset: &Tipset, diff --git a/src/tool/subcommands/api_cmd/generate_test_snapshot.rs b/src/tool/subcommands/api_cmd/generate_test_snapshot.rs index c46e2adb4a90..43829e9beedf 100644 --- a/src/tool/subcommands/api_cmd/generate_test_snapshot.rs +++ b/src/tool/subcommands/api_cmd/generate_test_snapshot.rs @@ -19,7 +19,7 @@ use crate::{ message_pool::{MessagePool, MpoolLocker, NonceTracker}, networks::ChainConfig, prelude::*, - shim::address::CurrentNetwork, + shim::{address::CurrentNetwork, clock::ChainEpoch}, state_manager::StateManager, }; use api_compare_tests::TestDump; @@ -350,13 +350,13 @@ impl EthBlockBloomStore for ReadOpsTrackingStore { fn write_bloom( &self, key: &Cid, - height: i64, + height: ChainEpoch, bloom: &[u8; BLOCK_BLOOM_LEN], ) -> anyhow::Result<()> { self.inner.write_bloom(key, height, bloom) } - fn delete_blooms_before_height(&self, height: i64) -> anyhow::Result<()> { + fn delete_blooms_before_height(&self, height: ChainEpoch) -> anyhow::Result<()> { self.inner.delete_blooms_before_height(height) } } From 3c8939c869fb6e01d349902ecad5157dab4fdb93 Mon Sep 17 00:00:00 2001 From: Aryan Tikarya Date: Thu, 2 Jul 2026 20:21:46 +0530 Subject: [PATCH 9/9] update the snap versions --- src/rpc/snapshots/forest__rpc__tests__rpc__v0.snap | 2 +- src/rpc/snapshots/forest__rpc__tests__rpc__v1.snap | 2 +- src/rpc/snapshots/forest__rpc__tests__rpc__v2.snap | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/rpc/snapshots/forest__rpc__tests__rpc__v0.snap b/src/rpc/snapshots/forest__rpc__tests__rpc__v0.snap index 62602db8e7cb..8d0883826ecb 100644 --- a/src/rpc/snapshots/forest__rpc__tests__rpc__v0.snap +++ b/src/rpc/snapshots/forest__rpc__tests__rpc__v0.snap @@ -5,7 +5,7 @@ expression: spec openrpc: 1.3.2 info: title: forest - version: 0.33.7 + version: 0.33.8 methods: - name: Filecoin.AuthNew description: Creates a new JWT authentication token with the given permissions. diff --git a/src/rpc/snapshots/forest__rpc__tests__rpc__v1.snap b/src/rpc/snapshots/forest__rpc__tests__rpc__v1.snap index 569cf957063a..597e12ef7bcf 100644 --- a/src/rpc/snapshots/forest__rpc__tests__rpc__v1.snap +++ b/src/rpc/snapshots/forest__rpc__tests__rpc__v1.snap @@ -5,7 +5,7 @@ expression: spec openrpc: 1.3.2 info: title: forest - version: 0.33.7 + version: 0.33.8 methods: - name: Filecoin.AuthNew description: Creates a new JWT authentication token with the given permissions. diff --git a/src/rpc/snapshots/forest__rpc__tests__rpc__v2.snap b/src/rpc/snapshots/forest__rpc__tests__rpc__v2.snap index c50b6d763fcd..8602812f2ddb 100644 --- a/src/rpc/snapshots/forest__rpc__tests__rpc__v2.snap +++ b/src/rpc/snapshots/forest__rpc__tests__rpc__v2.snap @@ -5,7 +5,7 @@ expression: spec openrpc: 1.3.2 info: title: forest - version: 0.33.7 + version: 0.33.8 methods: - name: Filecoin.ChainGetTipSet description: Returns the tipset with the specified CID.